From 54e5f051736bac30987ad8e305a6615e51066c44 Mon Sep 17 00:00:00 2001 From: Scorcher Date: Mon, 22 Dec 2025 20:15:31 +0800 Subject: [PATCH] Improvements (#90) * fix(ContributeDetails): adjust height of loading indicators and textarea * feat(AirportPointEditor): add EuroScope editor navigation and conditional rendering based on ICAO prefix feat(Account): implement email scrambling and copy functionality for VATSIM CID fix(Tooltip): adjust tooltip arrow positioning for better alignment fix(ContributeTest): update height class for file upload placeholder --- .../divisions/AirportPointEditor.jsx | 65 ++++++++-- src/components/shared/Tooltip.jsx | 2 +- src/pages/Account.jsx | 113 +++++++++++++++--- src/pages/ContributeDetails.jsx | 6 +- src/pages/ContributeTest.jsx | 2 +- 5 files changed, 158 insertions(+), 30 deletions(-) diff --git a/src/components/divisions/AirportPointEditor.jsx b/src/components/divisions/AirportPointEditor.jsx index 87402da..6fd5754 100644 --- a/src/components/divisions/AirportPointEditor.jsx +++ b/src/components/divisions/AirportPointEditor.jsx @@ -1,11 +1,12 @@ import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import PropTypes from 'prop-types'; import { MapContainer, TileLayer, useMap, Rectangle, Polyline } from 'react-leaflet'; import L from 'leaflet'; import { getVatsimToken } from '../../utils/cookieUtils'; -import { X } from 'lucide-react'; +import { X, ExternalLink } from 'lucide-react'; import { Dropdown } from '../shared/Dropdown'; +import { Layout } from '../layout/Layout'; import '@geoman-io/leaflet-geoman-free'; import '@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css'; import 'leaflet/dist/leaflet.css'; @@ -1172,6 +1173,7 @@ const emptyFormState = { }; const AirportPointEditor = ({ existingPoints = [], onChangesetChange, height = 'dynamic' }) => { + const navigate = useNavigate(); const [remotePoints, setRemotePoints] = useState(null); // null = not loaded const [remoteLoading, setRemoteLoading] = useState(false); const [remoteError, setRemoteError] = useState(null); @@ -1179,6 +1181,12 @@ const AirportPointEditor = ({ existingPoints = [], onChangesetChange, height = ' const fetchInFlightRef = useRef(false); const { airportId } = useParams(); const icao = (airportId || '').toUpperCase(); + const isEuroscopeOnly = useMemo(() => { + const prefixes = ['K', 'M', 'Y', 'N', 'A']; + const first = icao?.[0]; + if (!first) return false; + return !prefixes.includes(first); + }, [icao]); const mapboxToken = import.meta.env.VITE_MAPBOX_TOKEN; const [airportMeta, setAirportMeta] = useState(null); const [airportMetaError, setAirportMetaError] = useState(null); @@ -1189,7 +1197,7 @@ const AirportPointEditor = ({ existingPoints = [], onChangesetChange, height = ' const attemptedFallbackRef = useRef(false); // prevent infinite loop useEffect(() => { - if (!icao) return; + if (!icao || isEuroscopeOnly) return; let aborted = false; const fetchAirport = async () => { try { @@ -1211,7 +1219,7 @@ const AirportPointEditor = ({ existingPoints = [], onChangesetChange, height = ' return () => { aborted = true; }; - }, [icao]); + }, [icao, isEuroscopeOnly]); const [changeset, setChangeset] = useState(defaultChangeset); const [selectedId, setSelectedId] = useState(null); const [formState, setFormState] = useState(emptyFormState); @@ -1390,7 +1398,7 @@ const AirportPointEditor = ({ existingPoints = [], onChangesetChange, height = ' const triggerFetchPoints = useCallback( async (force = false) => { - if (!icao) return; + if (!icao || isEuroscopeOnly) return; if (fetchInFlightRef.current && !force) return; // already fetching if (!force && remotePoints !== null) return; // already loaded fetchInFlightRef.current = true; @@ -1418,7 +1426,7 @@ const AirportPointEditor = ({ existingPoints = [], onChangesetChange, height = ' fetchInFlightRef.current = false; } }, - [icao, remotePoints] + [icao, remotePoints, isEuroscopeOnly] ); useEffect(() => { @@ -1976,7 +1984,7 @@ const AirportPointEditor = ({ existingPoints = [], onChangesetChange, height = ' const [refreshTick, setRefreshTick] = useState(0); useEffect(() => { - if (!icao) return; + if (!icao || isEuroscopeOnly) return; let aborted = false; const fetchAirport = async () => { try { @@ -1998,7 +2006,7 @@ const AirportPointEditor = ({ existingPoints = [], onChangesetChange, height = ' return () => { aborted = true; }; - }, [icao, refreshTick]); + }, [icao, refreshTick, isEuroscopeOnly]); const BoundsController = ({ bounds, suppressClamp }) => { const map = useMap(); @@ -2063,6 +2071,47 @@ const AirportPointEditor = ({ existingPoints = [], onChangesetChange, height = ' return height; }, [height]); + if (isEuroscopeOnly) { + const euroscopeUrl = `https://euroscope.stopbars.com?icao=${encodeURIComponent(icao)}`; + return ( + +
+
+
+
+

+ Use the EuroScope Editor +

+

+ This airport is managed via EuroScope. To edit BARS object data for {icao}, please + use the EuroScope editor. +

+
+
+ + + Open EuroScope Editor + +
+
+
+
+
+ ); + } + return (
diff --git a/src/components/shared/Tooltip.jsx b/src/components/shared/Tooltip.jsx index be9a44b..fcc1aa7 100644 --- a/src/components/shared/Tooltip.jsx +++ b/src/components/shared/Tooltip.jsx @@ -9,7 +9,7 @@ export const Tooltip = ({ children, content, className = '' }) => {
{content} {/* Arrow */} -
+
); diff --git a/src/pages/Account.jsx b/src/pages/Account.jsx index 00dfa6c..4f71506 100644 --- a/src/pages/Account.jsx +++ b/src/pages/Account.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useMemo } from 'react'; import { useAuth } from '../hooks/useAuth'; import { Layout } from '../components/layout/Layout'; import { Card } from '../components/shared/Card'; @@ -23,6 +23,7 @@ import { import { formatDateAccordingToLocale } from '../utils/dateUtils'; import { getVatsimToken } from '../utils/cookieUtils'; import { Toast } from '../components/shared/Toast'; +import { Tooltip } from '../components/shared/Tooltip'; const Account = () => { const { user, loading, logout, setUser, refreshUserData } = useAuth(); @@ -47,6 +48,13 @@ const Account = () => { const [showDeleteSuccessToast, setShowDeleteSuccessToast] = useState(false); const [showDeleteErrorToast, setShowDeleteErrorToast] = useState(false); const [deleteErrorMessage, setDeleteErrorMessage] = useState(''); + const [cidCopied, setCidCopied] = useState(false); + const [hideEmail, setHideEmail] = useState(() => { + if (typeof window === 'undefined') return false; + const stored = window.localStorage?.getItem('bars_hide_email'); + return stored === 'true'; + }); + const cidCopyTimeoutRef = useRef(null); const displayModeRequestRef = useRef({ id: 0, controller: null }); const refreshDebounceRef = useRef(null); @@ -136,6 +144,20 @@ const Account = () => { return name || id || '—'; }; + const scrambleEmail = (email) => { + if (!email) return '—'; + const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; + return email + .split('') + .map((c) => (c === '@' || c === '.' ? c : chars[Math.floor(Math.random() * chars.length)])) + .join(''); + }; + + const scrambledEmail = useMemo(() => { + if (!user?.email || !hideEmail) return null; + return scrambleEmail(user.email); + }, [user?.email, hideEmail]); + const handleCopyApiKey = async () => { try { await navigator.clipboard.writeText(user?.api_key); @@ -146,6 +168,18 @@ const Account = () => { } }; + const handleCopyVatsimCid = async () => { + if (!user?.vatsim_id) return; + try { + await navigator.clipboard.writeText(String(user.vatsim_id)); + setCidCopied(true); + if (cidCopyTimeoutRef.current) clearTimeout(cidCopyTimeoutRef.current); + cidCopyTimeoutRef.current = setTimeout(() => setCidCopied(false), 1500); + } catch (err) { + console.error('Failed to copy CID:', err); + } + }; + const handleDeleteAccount = async () => { setIsDeletingAccount(true); const token = getVatsimToken(); @@ -342,9 +376,18 @@ const Account = () => { clearTimeout(refreshDebounceRef.current); refreshDebounceRef.current = null; } + if (cidCopyTimeoutRef.current) { + clearTimeout(cidCopyTimeoutRef.current); + } }; }, []); + // Restore and persist the email visibility preference + useEffect(() => { + if (typeof window === 'undefined') return; + window.localStorage?.setItem('bars_hide_email', hideEmail ? 'true' : 'false'); + }, [hideEmail]); + if (loading) { return (
@@ -407,7 +450,7 @@ const Account = () => { variant="outline" size="sm" onClick={() => setShowApiKey(!showApiKey)} - className="min-w-[90px] shrink-0 hover:bg-zinc-800" + className="min-w-22.5 shrink-0 hover:bg-zinc-800" > {showApiKey ? ( <> @@ -425,7 +468,7 @@ const Account = () => { variant="outline" size="sm" onClick={handleCopyApiKey} - className={`min-w-[100px] ${copySuccess ? 'bg-green-500/20 text-green-400' : 'hover:bg-zinc-800'}`} + className={`min-w-25 ${copySuccess ? 'bg-green-500/20 text-green-400' : 'hover:bg-zinc-800'}`} > {copySuccess ? ( <> @@ -441,7 +484,7 @@ const Account = () => { variant="outline" size="sm" onClick={() => setIsRegenerateDialogOpen(true)} - className="min-w-[130px] hover:bg-zinc-800 text-blue-400 border-blue-500/20" + className="min-w-32.5 hover:bg-zinc-800 text-blue-400 border-blue-500/20" > Regenerate @@ -454,20 +497,56 @@ const Account = () => {
- {[ - { label: 'VATSIM CID', value: user?.vatsim_id }, - { label: 'Email', value: user?.email }, - ].map((field) => ( -
- -

{field.value}

+ {/* VATSIM CID */} +
+ +
+ { + if (!user?.vatsim_id) return; + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleCopyVatsimCid(); + } + }} + className={`absolute left-0 top-0 inline-flex items-center font-medium transition-all duration-200 ${cidCopied ? 'opacity-0 -translate-y-2' : 'opacity-100 translate-y-0'} ${user?.vatsim_id ? 'cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/40 focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-900' : ''}`} + > + {user?.vatsim_id || '—'} + + + Copied! + +
+
+ + {/* Email */} +
+
+ +

+ {hideEmail ? scrambledEmail : user?.email || '—'} +

- ))} + + + +
diff --git a/src/pages/ContributeDetails.jsx b/src/pages/ContributeDetails.jsx index 0145845..dd1baba 100644 --- a/src/pages/ContributeDetails.jsx +++ b/src/pages/ContributeDetails.jsx @@ -302,10 +302,10 @@ const ContributeDetails = () => { >
-
+
-
+
@@ -425,7 +425,7 @@ const ContributeDetails = () => { onChange={(e) => setNotes(e.target.value)} placeholder="Any additional notes for the approval team." maxLength={1000} - className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-lg focus:outline-none focus:border-blue-500 min-h-[100px] resize-none" + className="w-full px-4 py-2 bg-zinc-800 border border-zinc-700 rounded-lg focus:outline-none focus:border-blue-500 min-h-25 resize-none" >
diff --git a/src/pages/ContributeTest.jsx b/src/pages/ContributeTest.jsx index 87eb04d..7be2602 100644 --- a/src/pages/ContributeTest.jsx +++ b/src/pages/ContributeTest.jsx @@ -244,7 +244,7 @@ const ContributeTest = () => { showRemoveAreas={showRemoveAreas} /> ) : ( -
+

Upload an XML file to preview