feat: add random airport selection feature; update status bar with new button and styling
This commit is contained in:
@ -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)
|
||||
|
||||
|
||||
<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" />
|
||||
|
||||
|
||||
|
||||
## Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|
||||
@ -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<string>(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<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() {
|
||||
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 (
|
||||
<main className="relative h-dvh w-screen overflow-hidden bg-black">
|
||||
<Map mapStyle={mapStyle.style} isDark={mapStyle.dark}>
|
||||
@ -259,6 +316,7 @@ function FlightTrackerInner() {
|
||||
retryIn={retryIn}
|
||||
onNorthUp={handleNorthUp}
|
||||
onResetView={handleResetView}
|
||||
onRandomAirport={handleRandomAirport}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -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 (
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
@ -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
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user