"use client"; import { useState, useMemo, useRef, useEffect, type ReactNode } from "react"; import Image from "next/image"; import { motion, AnimatePresence } from "motion/react"; import { Search, Map as MapIcon, Settings, X, Check, MapPin, ChevronRight, RotateCw, Route, Layers, Palette, ArrowLeftRight, Github, Plane, Eye, Loader2, } from "lucide-react"; import { CITIES, type City } from "@/lib/cities"; import { searchAirports, airportToCity } from "@/lib/airports"; import { MAP_STYLES, type MapStyle } from "@/lib/map-styles"; import { useSettings, type OrbitDirection } from "@/hooks/use-settings"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Slider } from "@/components/ui/slider"; import type { FlightState } from "@/lib/opensky"; import { formatCallsign } from "@/lib/flight-utils"; type TabId = "search" | "style" | "settings"; const TABS: { id: TabId; icon: typeof Search; label: string }[] = [ { id: "search", icon: Search, label: "Search" }, { id: "style", icon: MapIcon, label: "Map Style" }, { id: "settings", icon: Settings, label: "Settings" }, ]; type ControlPanelProps = { activeCity: City; onSelectCity: (city: City) => void; activeStyle: MapStyle; onSelectStyle: (style: MapStyle) => void; flights: FlightState[]; activeFlightIcao24: string | null; onLookupFlight: (query: string, enterFpv?: boolean) => Promise; }; export function ControlPanel({ activeCity, onSelectCity, activeStyle, onSelectStyle, flights, activeFlightIcao24, onLookupFlight, }: ControlPanelProps) { const [openTab, setOpenTab] = useState(null); useEffect(() => { function handleOpenSearch() { setOpenTab("search"); } window.addEventListener("aeris:open-search", handleOpenSearch); return () => window.removeEventListener("aeris:open-search", handleOpenSearch); }, []); const open = (tab: TabId) => setOpenTab(tab); const close = () => setOpenTab(null); return ( <> {TABS.map(({ id, icon: Icon, label }) => ( open(id)} className="flex h-9 w-9 items-center justify-center rounded-xl backdrop-blur-2xl transition-colors" style={{ borderWidth: 1, borderColor: "rgb(var(--ui-fg) / 0.06)", backgroundColor: "rgb(var(--ui-fg) / 0.03)", color: "rgb(var(--ui-fg) / 0.5)", }} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} aria-label={label} > ))} {openTab && ( { onSelectCity(c); close(); }} activeStyle={activeStyle} onSelectStyle={onSelectStyle} flights={flights} activeFlightIcao24={activeFlightIcao24} onLookupFlight={onLookupFlight} /> )} ); } function PanelDialog({ activeTab, onTabChange, onClose, activeCity, onSelectCity, activeStyle, onSelectStyle, flights, activeFlightIcao24, onLookupFlight, }: { activeTab: TabId; onTabChange: (tab: TabId) => void; onClose: () => void; activeCity: City; onSelectCity: (city: City) => void; activeStyle: MapStyle; onSelectStyle: (style: MapStyle) => void; flights: FlightState[]; activeFlightIcao24: string | null; onLookupFlight: (query: string, enterFpv?: boolean) => Promise; }) { const dialogRef = useRef(null); useEffect(() => { function handleKey(e: KeyboardEvent) { if (e.key === "Escape") onClose(); } window.addEventListener("keydown", handleKey); return () => window.removeEventListener("keydown", handleKey); }, [onClose]); useEffect(() => { const dialog = dialogRef.current; if (!dialog) return; const focusable = dialog.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', ); if (focusable.length === 0) return; const first = focusable[0]; first.focus(); function trapFocus(e: KeyboardEvent) { if (e.key !== "Tab") return; const elements = dialog!.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', ); const f = elements[0]; const l = elements[elements.length - 1]; if (e.shiftKey) { if (document.activeElement === f) { e.preventDefault(); l.focus(); } } else { if (document.activeElement === l) { e.preventDefault(); f.focus(); } } } dialog.addEventListener("keydown", trapFocus); return () => dialog.removeEventListener("keydown", trapFocus); }, [activeTab]); return ( <>
{/* Desktop sidebar (hidden on mobile) */}

Controls

v0.1 · OpenSky Network

{/* Mobile header */}

{TABS.find((t) => t.id === activeTab)?.label}

{/* Desktop header */}

{TABS.find((t) => t.id === activeTab)?.label}

{activeTab === "search" && ( { const found = await onLookupFlight(query, enterFpv); if (found) onClose(); return found; }} /> )} {activeTab === "style" && ( )} {activeTab === "settings" && ( )}
{/* Mobile tab bar */}
); } function TabContent({ children }: { children: ReactNode }) { return ( {children} ); } function SearchContent({ activeCity, onSelect, flights, activeFlightIcao24, onLookupFlight, }: { activeCity: City; onSelect: (city: City) => void; flights: FlightState[]; activeFlightIcao24: string | null; onLookupFlight: (query: string, enterFpv?: boolean) => Promise; }) { const [query, setQuery] = useState(""); const [lookupBusy, setLookupBusy] = useState(false); const [lookupError, setLookupError] = useState(null); const inputRef = useRef(null); useEffect(() => { requestAnimationFrame(() => inputRef.current?.focus()); }, []); const { featured, airports } = useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return { featured: CITIES, airports: [] as ReturnType, }; const featured = CITIES.filter( (c) => c.name.toLowerCase().includes(q) || c.iata.toLowerCase().includes(q) || c.country.toLowerCase().includes(q), ); const featuredIatas = new Set(CITIES.map((c) => c.iata)); const airports = searchAirports(q).filter( (a) => !featuredIatas.has(a.iata), ); return { featured, airports }; }, [query]); const normalizedQuery = query.trim().toLowerCase(); const compactQuery = normalizedQuery.replace(/\s+/g, ""); const isIcao24Query = /^[0-9a-f]{6}$/.test(compactQuery); const flightMatches = useMemo(() => { if (!compactQuery) return [] as FlightState[]; return flights .filter((flight) => { const icao = flight.icao24.toLowerCase(); const callsign = (flight.callsign ?? "") .trim() .toLowerCase() .replace(/\s+/g, ""); return icao.includes(compactQuery) || callsign.includes(compactQuery); }) .slice(0, 12); }, [flights, compactQuery]); const hasResults = featured.length > 0 || airports.length > 0 || flightMatches.length > 0; async function runLookup(enterFpv = false) { if (!query.trim() || lookupBusy) return; setLookupBusy(true); setLookupError(null); try { const found = await onLookupFlight(query, enterFpv); if (!found) { setLookupError( isIcao24Query ? "Flight not found for this ICAO24 right now" : "No live worldwide flight match found (or rate-limited)", ); } } finally { setLookupBusy(false); } } async function openFlight(icao24: string, enterFpv = false) { if (lookupBusy) return; setLookupBusy(true); setLookupError(null); try { const found = await onLookupFlight(icao24, enterFpv); if (!found) { setLookupError("Unable to open the selected flight"); } } finally { setLookupBusy(false); } } return (
{ setQuery(e.target.value); setLookupError(null); }} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); void runLookup(false); } }} placeholder="Search airports or flight number (callsign/ICAO24)..." aria-label="Search airports by name, IATA code, city, country, or flight callsign/ICAO24" className="flex-1 bg-transparent text-[14px] font-medium text-white/90 placeholder:text-white/20 outline-none" /> {query && ( )}
{compactQuery && (
)} {lookupError && (

{lookupError}

)} {flightMatches.length > 0 && ( <>

Flights

{flightMatches.map((flight) => ( void openFlight(flight.icao24, false)} onFpv={() => void openFlight(flight.icao24, true)} /> ))} )} {!hasResults && (

No airports or flights found

)} {featured.length > 0 && ( <> {query && (

Featured

)} {featured.map((city) => ( onSelect(city)} /> ))} )} {airports.length > 0 && ( <>

0 ? "pt-3" : "pt-2" }`} > Airports

{airports.map((airport) => ( onSelect(airportToCity(airport))} /> ))} )} {!query && (

Search 9,000+ airports worldwide

)}
); } function LocationRow({ name, detail, isActive, onClick, }: { name: string; detail: string; isActive: boolean; onClick: () => void; }) { return ( ); } function FlightRow({ callsign, detail, isActive, onOpen, onFpv, }: { callsign: string; detail: string; isActive: boolean; onOpen: () => void; onFpv: () => void; }) { return (
); } function StyleContent({ activeStyle, onSelect, }: { activeStyle: MapStyle; onSelect: (style: MapStyle) => void; }) { return (
{MAP_STYLES.map((style, i) => ( onSelect(style)} /> ))}

Satellite © Esri · Terrain © OpenTopoMap · Base maps © CARTO

); } function StyleTile({ style, isActive, index, onSelect, }: { style: MapStyle; isActive: boolean; index: number; onSelect: () => void; }) { const [imgLoaded, setImgLoaded] = useState(false); return (
{`${style.name} setImgLoaded(true)} onError={() => setImgLoaded(true)} className={`object-cover transition-all duration-500 group-hover:scale-105 ${ imgLoaded ? "opacity-100" : "opacity-0" }`} draggable={false} />
{isActive && ( )}
{style.name} {style.dark && ( )}
); } const ORBIT_SPEED_PRESETS = [ { label: "Slow", value: 0.06 }, { label: "Normal", value: 0.15 }, { label: "Fast", value: 0.35 }, ]; const ORBIT_SPEED_MIN = 0.02; const ORBIT_SPEED_MAX = 0.5; const ORBIT_SNAP_THRESHOLD = 0.025; const TRAIL_THICKNESS_MIN = 1; const TRAIL_THICKNESS_MAX = 8; const TRAIL_DISTANCE_MIN = 12; const TRAIL_DISTANCE_MAX = 100; const ORBIT_DIRECTIONS: { label: string; value: OrbitDirection }[] = [ { label: "Clockwise", value: "clockwise" }, { label: "Counter", value: "counter-clockwise" }, ]; function SettingsContent() { const { settings, update, reset } = useSettings(); return (
} title="Auto-orbit" description="Camera slowly rotates around the airport" checked={settings.autoOrbit} onChange={(v) => update("autoOrbit", v)} /> {settings.autoOrbit && ( <> update("orbitSpeed", v)} /> } title="Direction" options={ORBIT_DIRECTIONS} value={settings.orbitDirection} onChange={(v) => update("orbitDirection", v)} /> )}
} title="Flight trails" description="Altitude-colored trails behind aircraft" checked={settings.showTrails} onChange={(v) => update("showTrails", v)} /> {settings.showTrails && ( <> update("trailThickness", v)} /> update("trailDistance", v)} /> )} } title="Ground shadows" description="Shadow projections on the map surface" checked={settings.showShadows} onChange={(v) => update("showShadows", v)} /> } title="Altitude colors" description="Color aircraft and trails by altitude" checked={settings.showAltitudeColors} onChange={(v) => update("showAltitudeColors", v)} />
); } function OrbitSpeedSlider({ value, onChange, }: { value: number; onChange: (v: number) => void; }) { const activeLabel = ORBIT_SPEED_PRESETS.find( (p) => Math.abs(p.value - value) < ORBIT_SNAP_THRESHOLD, )?.label ?? `${value.toFixed(2)}×`; function handleChange(vals: number[]) { let raw = vals[0]; for (const preset of ORBIT_SPEED_PRESETS) { if (Math.abs(raw - preset.value) < ORBIT_SNAP_THRESHOLD) { raw = preset.value; break; } } onChange(raw); } return (

Orbit speed

{activeLabel}
{ORBIT_SPEED_PRESETS.map((preset) => { const pct = ((preset.value - ORBIT_SPEED_MIN) / (ORBIT_SPEED_MAX - ORBIT_SPEED_MIN)) * 100; const isActive = Math.abs(preset.value - value) < ORBIT_SNAP_THRESHOLD; return ( ); })}
); } function TrailThicknessSlider({ value, onChange, }: { value: number; onChange: (v: number) => void; }) { return (

Trail thickness

{value.toFixed(1)} px
onChange(vals[0])} aria-label="Trail thickness" />
); } function TrailDistanceSlider({ value, onChange, }: { value: number; onChange: (v: number) => void; }) { return (

Trail distance

{value} pts
onChange(vals[0])} aria-label="Trail distance" />
); } function SettingRow({ icon, title, description, checked, onChange, }: { icon: ReactNode; title: string; description: string; checked: boolean; onChange: (v: boolean) => void; }) { return ( ); } function SegmentRow({ icon, title, options, value, onChange, }: { icon: ReactNode; title: string; options: { label: string; value: T }[]; value: T; onChange: (v: T) => void; }) { return (
{icon}

{title}

{options.map((opt) => { const isActive = opt.value === value; return ( ); })}
); } function Toggle({ checked }: { checked: boolean }) { return (
); }