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