import { useEffect, useMemo, useRef, useState } from 'react' import { Plane, X } from 'lucide-react' import { airportsApi } from '../../api/client' import { useTranslation } from '../../i18n' export interface Airport { iata: string icao: string | null name: string city: string country: string lat: number lng: number tz: string } interface Props { value: Airport | null onChange: (airport: Airport | null) => void placeholder?: string style?: React.CSSProperties } function formatLabel(a: Airport) { return `${a.city || a.name} (${a.iata})` } export default function AirportSelect({ value, onChange, placeholder, style }: Props) { const { t, locale } = useTranslation() const countryName = useMemo(() => { try { return new Intl.DisplayNames([locale || 'en'], { type: 'region' }) } catch { return null } }, [locale]) const displayCountry = (code: string) => { if (!code) return '' try { return countryName?.of(code) || code } catch { return code } } const [query, setQuery] = useState(value ? formatLabel(value) : '') const [open, setOpen] = useState(false) const [results, setResults] = useState([]) const [highlight, setHighlight] = useState(-1) const [loading, setLoading] = useState(false) const wrapRef = useRef(null) const abortRef = useRef(null) const debounceRef = useRef | null>(null) useEffect(() => { setQuery(value ? formatLabel(value) : '') }, [value]) useEffect(() => { const handler = (e: MouseEvent) => { if (!wrapRef.current?.contains(e.target as Node)) setOpen(false) } if (open) document.addEventListener('mousedown', handler) return () => document.removeEventListener('mousedown', handler) }, [open]) useEffect(() => { if (debounceRef.current) clearTimeout(debounceRef.current) const trimmed = query.trim() if (trimmed.length < 2 || (value && trimmed === formatLabel(value))) { setResults([]) return } debounceRef.current = setTimeout(async () => { abortRef.current?.abort() const controller = new AbortController() abortRef.current = controller setLoading(true) try { const data = await airportsApi.search(trimmed, controller.signal) setResults(Array.isArray(data) ? data : []) setHighlight(-1) } catch (err: any) { if (err?.name !== 'AbortError' && err?.name !== 'CanceledError') { setResults([]) } } finally { setLoading(false) } }, 220) return () => { if (debounceRef.current) clearTimeout(debounceRef.current) } }, [query, value]) const pick = (a: Airport) => { onChange(a) setQuery(formatLabel(a)) setOpen(false) setResults([]) } const clear = () => { onChange(null) setQuery('') setResults([]) } const onKey = (e: React.KeyboardEvent) => { if (!open || results.length === 0) return if (e.key === 'ArrowDown') { e.preventDefault(); setHighlight(h => Math.min(h + 1, results.length - 1)) } else if (e.key === 'ArrowUp') { e.preventDefault(); setHighlight(h => Math.max(h - 1, 0)) } else if (e.key === 'Enter' && highlight >= 0) { e.preventDefault(); pick(results[highlight]) } else if (e.key === 'Escape') setOpen(false) } return (
{ setQuery(e.target.value); setOpen(true); if (value) onChange(null) }} onFocus={() => setOpen(true)} onKeyDown={onKey} style={{ flex: 1, minWidth: 0, background: 'transparent', border: 'none', outline: 'none', color: 'var(--text-primary)', fontSize: 13 }} /> {value && ( )}
{open && (loading || results.length > 0) && (
{loading && results.length === 0 && (
{t('common.loading')}
)} {results.map((a, i) => ( ))}
)}
) }