feat: add random airport selection feature; update status bar with new button and styling

This commit is contained in:
Kewonit
2026-02-15 17:44:21 +05:30
3 changed files with 86 additions and 4 deletions

View File

@ -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. 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)
<img width="2559" height="1380" alt="Screenshot 2026-02-15 112222" src="https://github.com/user-attachments/assets/9d1f50ed-be4e-4ef5-95ac-257e9129f8c8" />
<img width="2555" height="1387" alt="image" src="https://github.com/user-attachments/assets/a1d2f673-dfdc-4c82-8ee2-7629d91ad94b" /> <img width="2555" height="1387" alt="image" src="https://github.com/user-attachments/assets/a1d2f673-dfdc-4c82-8ee2-7629d91ad94b" />
## Stack ## Stack
| Layer | Technology | | Layer | Technology |

View File

@ -15,7 +15,7 @@ import { useFlights } from "@/hooks/use-flights";
import { useTrailHistory } from "@/hooks/use-trail-history"; import { useTrailHistory } from "@/hooks/use-trail-history";
import { MAP_STYLES, DEFAULT_STYLE, type MapStyle } from "@/lib/map-styles"; import { MAP_STYLES, DEFAULT_STYLE, type MapStyle } from "@/lib/map-styles";
import { CITIES, type City } from "@/lib/cities"; 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 { FlightState } from "@/lib/opensky";
import type { PickingInfo } from "@deck.gl/core"; import type { PickingInfo } from "@deck.gl/core";
import { Github, Star } from "lucide-react"; 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 DEFAULT_CITY = CITIES.find((c) => c.id === DEFAULT_CITY_ID) ?? CITIES[0];
const GITHUB_REPO_URL = "https://github.com/kewonit/aeris"; const GITHUB_REPO_URL = "https://github.com/kewonit/aeris";
const GITHUB_REPO_API = "https://api.github.com/repos/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<string>(HIGH_TRAFFIC_IATA);
const HIGH_TRAFFIC_AIRPORTS = AIRPORTS.filter((airport) =>
HIGH_TRAFFIC_IATA_SET.has(airport.iata.toUpperCase()),
);
const subscribeNoop = () => () => {}; const subscribeNoop = () => () => {};
@ -93,6 +120,31 @@ function saveMapStyle(style: MapStyle): void {
} }
} }
function chooseRandom<T>(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() { function FlightTrackerInner() {
const hydratedCity = useSyncExternalStore( const hydratedCity = useSyncExternalStore(
subscribeNoop, subscribeNoop,
@ -176,6 +228,11 @@ function FlightTrackerInner() {
); );
}, [activeCity.coordinates]); }, [activeCity.coordinates]);
const handleRandomAirport = useCallback(() => {
const randomCity = pickRandomAirportCity(activeCity.iata);
setActiveCity(randomCity);
}, [activeCity.iata, setActiveCity]);
return ( return (
<main className="relative h-dvh w-screen overflow-hidden bg-black"> <main className="relative h-dvh w-screen overflow-hidden bg-black">
<Map mapStyle={mapStyle.style} isDark={mapStyle.dark}> <Map mapStyle={mapStyle.style} isDark={mapStyle.dark}>
@ -259,6 +316,7 @@ function FlightTrackerInner() {
retryIn={retryIn} retryIn={retryIn}
onNorthUp={handleNorthUp} onNorthUp={handleNorthUp}
onResetView={handleResetView} onResetView={handleResetView}
onRandomAirport={handleRandomAirport}
/> />
</div> </div>

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { motion, AnimatePresence } from "motion/react"; 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 = { type StatusBarProps = {
flightCount: number; flightCount: number;
@ -11,6 +11,7 @@ type StatusBarProps = {
retryIn?: number; retryIn?: number;
onNorthUp?: () => void; onNorthUp?: () => void;
onResetView?: () => void; onResetView?: () => void;
onRandomAirport?: () => void;
}; };
export function StatusBar({ export function StatusBar({
@ -21,6 +22,7 @@ export function StatusBar({
retryIn = 0, retryIn = 0,
onNorthUp, onNorthUp,
onResetView, onResetView,
onRandomAirport,
}: StatusBarProps) { }: StatusBarProps) {
return ( return (
<div className="flex flex-col items-start gap-2"> <div className="flex flex-col items-start gap-2">
@ -122,7 +124,7 @@ export function StatusBar({
damping: 24, damping: 24,
delay: 0.48, 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={{ style={{
borderColor: "rgb(var(--ui-fg) / 0.06)", borderColor: "rgb(var(--ui-fg) / 0.06)",
backgroundColor: "rgb(var(--ui-bg) / 0.5)", backgroundColor: "rgb(var(--ui-bg) / 0.5)",
@ -150,6 +152,21 @@ export function StatusBar({
> >
Reset Reset
</button> </button>
<div
className="h-3 w-px"
style={{ backgroundColor: "rgb(var(--ui-fg) / 0.08)" }}
/>
<button
type="button"
onClick={onRandomAirport}
aria-label="Random airport"
title="Random airport"
className="inline-flex items-center gap-1 text-[11px] font-medium tracking-wide transition-colors"
style={{ color: "rgb(var(--ui-fg) / 0.55)" }}
>
<Dices className="h-3 w-3" />
Random
</button>
</motion.div> </motion.div>
</div> </div>
</div> </div>