Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 57 additions & 8 deletions src/components/divisions/AirportPointEditor.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -1172,13 +1173,20 @@ 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);
const [uploadState, setUploadState] = useState({ status: 'idle', message: '' }); // uploading|success|error|idle
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);
Expand All @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -1418,7 +1426,7 @@ const AirportPointEditor = ({ existingPoints = [], onChangesetChange, height = '
fetchInFlightRef.current = false;
}
},
[icao, remotePoints]
[icao, remotePoints, isEuroscopeOnly]
);

useEffect(() => {
Expand Down Expand Up @@ -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 {
Expand All @@ -1998,7 +2006,7 @@ const AirportPointEditor = ({ existingPoints = [], onChangesetChange, height = '
return () => {
aborted = true;
};
}, [icao, refreshTick]);
}, [icao, refreshTick, isEuroscopeOnly]);

const BoundsController = ({ bounds, suppressClamp }) => {
const map = useMap();
Expand Down Expand Up @@ -2063,6 +2071,47 @@ const AirportPointEditor = ({ existingPoints = [], onChangesetChange, height = '
return height;
}, [height]);

if (isEuroscopeOnly) {
const euroscopeUrl = `https://euroscope.stopbars.com?icao=${encodeURIComponent(icao)}`;
return (
<Layout>
<div className="min-h-[70vh] pt-28 pb-16 flex items-center justify-center">
<div className="container mx-auto px-4 max-w-208">
<div className="bg-zinc-900/80 border border-zinc-800 rounded-2xl shadow-xl p-12 text-center flex flex-col gap-5">
<div className="space-y-2">
<h1 className="text-2xl font-semibold text-white tracking-tight">
Use the EuroScope Editor
</h1>
<p className="text-sm text-zinc-300">
This airport is managed via EuroScope. To edit BARS object data for {icao}, please
use the EuroScope editor.
</p>
</div>
<div className="flex flex-col sm:flex-row items-center justify-center gap-3 pt-2">
<button
type="button"
onClick={() => navigate(-1)}
className="w-full sm:w-auto px-4 py-2 rounded-lg border border-zinc-700 bg-zinc-900 hover:bg-zinc-800 text-sm text-white transition-colors"
>
Go Back
</button>
<a
href={euroscopeUrl}
target="_blank"
rel="noreferrer"
className="w-full sm:w-auto px-4 py-2 rounded-lg bg-blue-600 hover:bg-blue-500 text-sm font-medium text-white transition-colors shadow inline-flex items-center justify-center gap-2"
>
Open EuroScope Editor
<ExternalLink className="w-4 h-4" aria-hidden="true" />
</a>
</div>
</div>
</div>
</div>
</Layout>
);
}

return (
<div className="flex flex-col px-4 py-6 lg:px-8 pt-16" style={{ height: resolvedHeightValue }}>
<div className="flex items-start justify-between mb-6 gap-4">
Expand Down
2 changes: 1 addition & 1 deletion src/components/shared/Tooltip.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const Tooltip = ({ children, content, className = '' }) => {
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-3 py-1.5 bg-zinc-900 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap border border-zinc-700 z-50 shadow-md">
{content}
{/* Arrow */}
<div className="absolute top-full left-1/2 transform -translate-x-1/2 -mt-[3px] border-r border-b border-zinc-700 bg-zinc-900 w-2 h-2 rotate-45" />
<div className="absolute top-full left-1/2 transform -translate-x-1/2 -mt-0.75 border-r border-b border-zinc-700 bg-zinc-900 w-2 h-2 rotate-45" />
</div>
</div>
);
Expand Down
113 changes: 96 additions & 17 deletions src/pages/Account.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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();
Expand All @@ -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);

Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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 (
<div className="flex items-center justify-center min-h-screen">
Expand Down Expand Up @@ -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 ? (
<>
Expand All @@ -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 ? (
<>
Expand All @@ -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"
>
<RefreshCcw className="w-4 h-4 mr-2" /> Regenerate
</Button>
Expand All @@ -454,20 +497,56 @@ const Account = () => {
</div>

<div className="grid md:grid-cols-2 gap-6">
{[
{ label: 'VATSIM CID', value: user?.vatsim_id },
{ label: 'Email', value: user?.email },
].map((field) => (
<div
key={field.label}
className="bg-zinc-900/50 p-4 rounded-lg border border-zinc-800/50 hover:border-zinc-700/50"
>
<label className="text-sm font-medium text-zinc-400 block mb-1">
{field.label}
</label>
<p className="font-medium">{field.value}</p>
{/* VATSIM CID */}
<div className="bg-zinc-900/50 p-4 rounded-lg border border-zinc-800/50 hover:border-zinc-700/50 transition-colors">
<label className="text-sm font-medium text-zinc-400 block mb-1">
VATSIM CID
</label>
<div className="relative inline-block h-6 min-w-18 overflow-hidden align-middle">
<span
role="button"
tabIndex={0}
onClick={user?.vatsim_id ? handleCopyVatsimCid : undefined}
onKeyDown={(e) => {
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 || '—'}
</span>
<span
className={`absolute left-0 top-0 inline-flex items-center font-medium text-green-400 transition-all duration-200 pointer-events-none ${cidCopied ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-2'}`}
>
Copied!
</span>
</div>
</div>

{/* Email */}
<div className="bg-zinc-900/50 p-4 rounded-lg border border-zinc-800/50 hover:border-zinc-700/50 transition-colors flex items-center justify-between gap-3">
<div className="min-w-0">
<label className="text-sm font-medium text-zinc-400 block mb-1">Email</label>
<p
className={`font-medium break-all ${hideEmail ? 'blur-[3px] select-none' : ''}`}
>
{hideEmail ? scrambledEmail : user?.email || '—'}
</p>
</div>
))}
<Tooltip content={hideEmail ? 'Show email' : 'Hide email'}>
<button
type="button"
onClick={() => setHideEmail((prev) => !prev)}
className="shrink-0 text-zinc-400 hover:text-zinc-200 transition-colors p-1 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/40"
aria-label={hideEmail ? 'Show email' : 'Hide email'}
>
{hideEmail ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
</button>
</Tooltip>
</div>
</div>

<div className="bg-zinc-900/50 rounded-lg border border-zinc-800/50">
Expand Down
6 changes: 3 additions & 3 deletions src/pages/ContributeDetails.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,10 +302,10 @@ const ContributeDetails = () => {
>
<div className="flex items-center space-x-3 min-w-0 flex-1">
<div className="shrink-0 w-6 h-6 rounded-full bg-zinc-700/50"></div>
<div className="h-[18px] bg-zinc-700/50 rounded flex-1 max-w-[60%]"></div>
<div className="h-4.5 bg-zinc-700/50 rounded flex-1 max-w-[60%]"></div>
</div>
<div className="text-right ml-2 shrink-0 space-y-1">
<div className="h-[18px] w-8 bg-zinc-700/50 rounded ml-auto"></div>
<div className="h-4.5 w-8 bg-zinc-700/50 rounded ml-auto"></div>
<div className="h-3.5 w-16 bg-zinc-700/50 rounded"></div>
</div>
</div>
Expand Down Expand Up @@ -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"
></textarea>
</div>

Expand Down
2 changes: 1 addition & 1 deletion src/pages/ContributeTest.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ const ContributeTest = () => {
showRemoveAreas={showRemoveAreas}
/>
) : (
<div className="h-[500px] flex items-center justify-center bg-zinc-800/30 rounded-lg">
<div className="h-125 flex items-center justify-center bg-zinc-800/30 rounded-lg">
<div className="text-center text-zinc-400">
<FileSearch className="w-12 h-12 mx-auto mb-3 opacity-50" />
<p>Upload an XML file to preview</p>
Expand Down
Loading