diff --git a/README.md b/README.md index 4d48756..b71e505 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,16 @@ Real-time 3D flight tracking — altitude-aware, visually stunning. Aeris renders live air traffic over the world's busiest airspaces on a premium dark-mode map. Flights are separated by altitude in true 3D: low altitudes glow cyan, high altitudes shift to gold. Select a city, and the camera glides to that airspace with spring-eased animation. -**[Live Demo](https://aeris.edbn.me)** +[Live Demo](https://aeris.edbn.me) + + +Screenshot 2026-02-15 112222 + + image + + ## Stack | Layer | Technology | diff --git a/src/components/flight-tracker.tsx b/src/components/flight-tracker.tsx index be06242..5eb9bd5 100644 --- a/src/components/flight-tracker.tsx +++ b/src/components/flight-tracker.tsx @@ -15,7 +15,7 @@ import { useFlights } from "@/hooks/use-flights"; import { useTrailHistory } from "@/hooks/use-trail-history"; import { MAP_STYLES, DEFAULT_STYLE, type MapStyle } from "@/lib/map-styles"; import { CITIES, type City } from "@/lib/cities"; -import { findByIata, airportToCity } from "@/lib/airports"; +import { AIRPORTS, findByIata, airportToCity } from "@/lib/airports"; import type { FlightState } from "@/lib/opensky"; import type { PickingInfo } from "@deck.gl/core"; import { Github, Star } from "lucide-react"; @@ -26,6 +26,33 @@ const STYLE_STORAGE_KEY = "aeris:mapStyle"; const DEFAULT_CITY = CITIES.find((c) => c.id === DEFAULT_CITY_ID) ?? CITIES[0]; const GITHUB_REPO_URL = "https://github.com/kewonit/aeris"; const GITHUB_REPO_API = "https://api.github.com/repos/kewonit/aeris"; +const HIGH_TRAFFIC_IATA = [ + "ATL", + "DXB", + "LHR", + "HND", + "DFW", + "DEN", + "IST", + "LAX", + "CDG", + "AMS", + "FRA", + "MAD", + "JFK", + "SIN", + "ORD", + "SFO", + "MIA", + "LAS", + "MUC", + "CLT", +] as const; +const HUB_PICK_PROBABILITY = 0.75; +const HIGH_TRAFFIC_IATA_SET = new Set(HIGH_TRAFFIC_IATA); +const HIGH_TRAFFIC_AIRPORTS = AIRPORTS.filter((airport) => + HIGH_TRAFFIC_IATA_SET.has(airport.iata.toUpperCase()), +); const subscribeNoop = () => () => {}; @@ -93,6 +120,31 @@ function saveMapStyle(style: MapStyle): void { } } +function chooseRandom(items: readonly T[]): T | null { + if (items.length === 0) return null; + return items[Math.floor(Math.random() * items.length)] ?? null; +} + +function pickRandomAirportCity(excludeIata?: string): City { + const exclude = excludeIata?.toUpperCase(); + const filteredHubs = exclude + ? HIGH_TRAFFIC_AIRPORTS.filter( + (airport) => airport.iata.toUpperCase() !== exclude, + ) + : HIGH_TRAFFIC_AIRPORTS; + + const filteredAirports = exclude + ? AIRPORTS.filter((airport) => airport.iata.toUpperCase() !== exclude) + : AIRPORTS; + + const useHubs = + filteredHubs.length > 0 && Math.random() < HUB_PICK_PROBABILITY; + const source = useHubs ? filteredHubs : filteredAirports; + const randomAirport = chooseRandom(source); + if (!randomAirport) return DEFAULT_CITY; + return airportToCity(randomAirport); +} + function FlightTrackerInner() { const hydratedCity = useSyncExternalStore( subscribeNoop, @@ -176,6 +228,11 @@ function FlightTrackerInner() { ); }, [activeCity.coordinates]); + const handleRandomAirport = useCallback(() => { + const randomCity = pickRandomAirportCity(activeCity.iata); + setActiveCity(randomCity); + }, [activeCity.iata, setActiveCity]); + return (
@@ -259,6 +316,7 @@ function FlightTrackerInner() { retryIn={retryIn} onNorthUp={handleNorthUp} onResetView={handleResetView} + onRandomAirport={handleRandomAirport} /> diff --git a/src/components/ui/status-bar.tsx b/src/components/ui/status-bar.tsx index 760b667..a72ebab 100644 --- a/src/components/ui/status-bar.tsx +++ b/src/components/ui/status-bar.tsx @@ -1,7 +1,7 @@ "use client"; import { motion, AnimatePresence } from "motion/react"; -import { Compass, Plane, Radio, ShieldAlert } from "lucide-react"; +import { Compass, Dices, Plane, Radio, ShieldAlert } from "lucide-react"; type StatusBarProps = { flightCount: number; @@ -11,6 +11,7 @@ type StatusBarProps = { retryIn?: number; onNorthUp?: () => void; onResetView?: () => void; + onRandomAirport?: () => void; }; export function StatusBar({ @@ -21,6 +22,7 @@ export function StatusBar({ retryIn = 0, onNorthUp, onResetView, + onRandomAirport, }: StatusBarProps) { return (
@@ -122,7 +124,7 @@ export function StatusBar({ damping: 24, delay: 0.48, }} - className="flex items-center gap-1.5 rounded-xl border px-2.5 py-2 backdrop-blur-2xl" + className="flex items-center gap-3 rounded-xl border px-3.5 py-2 backdrop-blur-2xl" style={{ borderColor: "rgb(var(--ui-fg) / 0.06)", backgroundColor: "rgb(var(--ui-bg) / 0.5)", @@ -150,6 +152,21 @@ export function StatusBar({ > Reset +
+