feat: add caching headers for models, update viewport meta tag, and enhance error handling; improve flight tracker layout and status bar styling

This commit is contained in:
Kewonit
2026-02-15 01:34:44 +05:30
parent fdbc604919
commit 3b431e2c8d
8 changed files with 96 additions and 39 deletions

View File

@ -34,6 +34,15 @@ const nextConfig: NextConfig = {
source: "/api/:path*", source: "/api/:path*",
headers: [{ key: "Cache-Control", value: "no-store, max-age=0" }], headers: [{ key: "Cache-Control", value: "no-store, max-age=0" }],
}, },
{
source: "/models/:path*",
headers: [
{
key: "Cache-Control",
value: "public, max-age=31536000, immutable",
},
],
},
]; ];
}, },
}; };

BIN
public/models/airplane.glb Normal file

Binary file not shown.

View File

@ -36,6 +36,7 @@
html, html,
body { body {
height: 100%; height: 100%;
height: 100dvh;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;

View File

@ -61,6 +61,10 @@ export default function RootLayout({
return ( return (
<html lang="en" className="dark"> <html lang="en" className="dark">
<head> <head>
<meta
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
{GA_ID && /^G-[A-Z0-9]+$/.test(GA_ID) && ( {GA_ID && /^G-[A-Z0-9]+$/.test(GA_ID) && (
<> <>
<Script <Script

View File

@ -13,8 +13,10 @@ export class ErrorBoundary extends Component<Props, State> {
} }
componentDidCatch(error: Error, info: React.ErrorInfo) { componentDidCatch(error: Error, info: React.ErrorInfo) {
if (process.env.NODE_ENV === "development") {
console.error("[aeris] Uncaught error:", error, info.componentStack); console.error("[aeris] Uncaught error:", error, info.componentStack);
} }
}
render() { render() {
if (this.state.error) { if (this.state.error) {

View File

@ -177,7 +177,7 @@ function FlightTrackerInner() {
}, [activeCity.coordinates]); }, [activeCity.coordinates]);
return ( return (
<main className="relative h-screen 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}>
<CameraController city={activeCity} /> <CameraController city={activeCity} />
<AirportLayer <AirportLayer
@ -202,11 +202,11 @@ function FlightTrackerInner() {
data-map-theme={mapStyle.dark ? "dark" : "light"} data-map-theme={mapStyle.dark ? "dark" : "light"}
className="pointer-events-none absolute inset-0 z-10" className="pointer-events-none absolute inset-0 z-10"
> >
<div className="pointer-events-auto absolute left-4 top-4 flex items-center gap-3"> <div className="pointer-events-auto absolute left-3 top-3 flex items-center gap-3 sm:left-4 sm:top-4">
<Brand isDark={mapStyle.dark} /> <Brand isDark={mapStyle.dark} />
</div> </div>
<div className="pointer-events-auto absolute right-4 top-4 flex items-center gap-2"> <div className="pointer-events-auto absolute right-3 top-3 flex items-center gap-1.5 sm:right-4 sm:top-4 sm:gap-2">
<a <a
href={GITHUB_REPO_URL} href={GITHUB_REPO_URL}
target="_blank" target="_blank"
@ -250,7 +250,7 @@ function FlightTrackerInner() {
/> />
</div> </div>
<div className="pointer-events-auto absolute bottom-4 left-4"> <div className="pointer-events-auto absolute bottom-[env(safe-area-inset-bottom,0px)] left-3 mb-3 sm:bottom-4 sm:left-4 sm:mb-0">
<StatusBar <StatusBar
flightCount={flights.length} flightCount={flights.length}
cityName={activeCity.name} cityName={activeCity.name}
@ -262,7 +262,7 @@ function FlightTrackerInner() {
/> />
</div> </div>
<div className="pointer-events-auto absolute bottom-4 right-4"> <div className="pointer-events-auto absolute bottom-[env(safe-area-inset-bottom,0px)] right-3 mb-3 sm:bottom-4 sm:right-4 sm:mb-0">
<AltitudeLegend /> <AltitudeLegend />
</div> </div>
</div> </div>

View File

@ -3,6 +3,7 @@
import { useEffect, useRef, useCallback } from "react"; import { useEffect, useRef, useCallback } from "react";
import { MapboxOverlay } from "@deck.gl/mapbox"; import { MapboxOverlay } from "@deck.gl/mapbox";
import { IconLayer, PathLayer } from "@deck.gl/layers"; import { IconLayer, PathLayer } from "@deck.gl/layers";
import { ScenegraphLayer } from "@deck.gl/mesh-layers";
import { useMap } from "./map"; import { useMap } from "./map";
import { altitudeToColor, altitudeToElevation } from "@/lib/flight-utils"; import { altitudeToColor, altitudeToElevation } from "@/lib/flight-utils";
import type { FlightState } from "@/lib/opensky"; import type { FlightState } from "@/lib/opensky";
@ -13,11 +14,13 @@ const DEFAULT_ANIM_DURATION_MS = 30_000;
const MIN_ANIM_DURATION_MS = 8_000; const MIN_ANIM_DURATION_MS = 8_000;
const MAX_ANIM_DURATION_MS = 45_000; const MAX_ANIM_DURATION_MS = 45_000;
const TELEPORT_THRESHOLD = 0.3; const TELEPORT_THRESHOLD = 0.3;
const TRAIL_BELOW_AIRCRAFT_METERS = 20; const TRAIL_BELOW_AIRCRAFT_METERS = 40;
const STARTUP_TRAIL_POLLS = 3; const STARTUP_TRAIL_POLLS = 3;
const STARTUP_TRAIL_STEP_SEC = 12; const STARTUP_TRAIL_STEP_SEC = 12;
const TRACK_DAMPING = 0.18; const TRACK_DAMPING = 0.18;
const TRAIL_SMOOTHING_ITERATIONS = 3; const TRAIL_SMOOTHING_ITERATIONS = 3;
const AIRCRAFT_SCENEGRAPH_URL = "/models/airplane.glb";
const AIRCRAFT_PX_PER_UNIT = 0.3;
function buildStartupFallbackTrail(f: FlightState): [number, number][] { function buildStartupFallbackTrail(f: FlightState): [number, number][] {
if (f.longitude == null || f.latitude == null) return []; if (f.longitude == null || f.latitude == null) return [];
@ -164,32 +167,60 @@ function createAircraftAtlas(): HTMLCanvasElement {
canvas.height = size; canvas.height = size;
const ctx = canvas.getContext("2d")!; const ctx = canvas.getContext("2d")!;
ctx.clearRect(0, 0, size, size);
ctx.fillStyle = "#ffffff"; ctx.fillStyle = "#ffffff";
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(64, 12); ctx.moveTo(64, 6);
ctx.lineTo(72, 48); ctx.lineTo(71, 19);
ctx.lineTo(108, 72); ctx.lineTo(71, 33);
ctx.lineTo(104, 78); ctx.lineTo(100, 44);
ctx.lineTo(72, 66); ctx.lineTo(106, 52);
ctx.lineTo(70, 96); ctx.lineTo(80, 53);
ctx.lineTo(72, 56);
ctx.lineTo(72, 88);
ctx.lineTo(90, 101);
ctx.lineTo(88, 108); ctx.lineTo(88, 108);
ctx.lineTo(86, 114); ctx.lineTo(69, 99);
ctx.lineTo(64, 104); ctx.lineTo(69, 121);
ctx.lineTo(42, 114); ctx.lineTo(64, 126);
ctx.lineTo(59, 121);
ctx.lineTo(59, 99);
ctx.lineTo(40, 108); ctx.lineTo(40, 108);
ctx.lineTo(58, 96); ctx.lineTo(38, 101);
ctx.lineTo(56, 66); ctx.lineTo(56, 88);
ctx.lineTo(24, 78); ctx.lineTo(56, 56);
ctx.lineTo(20, 72); ctx.lineTo(48, 53);
ctx.lineTo(56, 48); ctx.lineTo(22, 52);
ctx.lineTo(28, 44);
ctx.lineTo(57, 33);
ctx.lineTo(57, 19);
ctx.closePath(); ctx.closePath();
ctx.fill(); ctx.fill();
ctx.globalCompositeOperation = "destination-out";
ctx.beginPath();
ctx.moveTo(64, 13);
ctx.lineTo(67, 19);
ctx.lineTo(64, 24);
ctx.lineTo(61, 19);
ctx.closePath();
ctx.fill();
ctx.globalCompositeOperation = "source-over";
return canvas; return canvas;
} }
const AIRCRAFT_ICON_MAPPING = { const AIRCRAFT_ICON_MAPPING = {
aircraft: { x: 0, y: 0, width: 128, height: 128, mask: true }, aircraft: {
x: 0,
y: 0,
width: 128,
height: 128,
anchorX: 64,
anchorY: 64,
mask: true,
},
}; };
let _atlasCache: string | undefined; let _atlasCache: string | undefined;
@ -438,7 +469,7 @@ export function FlightLayers({
data: interpolated, data: interpolated,
getPosition: (d) => [d.longitude!, d.latitude!, 0], getPosition: (d) => [d.longitude!, d.latitude!, 0],
getIcon: () => "aircraft", getIcon: () => "aircraft",
getSize: 18, getSize: 20,
getColor: [0, 0, 0, 60], getColor: [0, 0, 0, 60],
getAngle: (d) => 360 - (d.trueTrack ?? 0), getAngle: (d) => 360 - (d.trueTrack ?? 0),
iconAtlas: atlasUrl, iconAtlas: atlasUrl,
@ -580,7 +611,7 @@ export function FlightLayers({
} }
layers.push( layers.push(
new IconLayer<FlightState>({ new ScenegraphLayer<FlightState>({
id: "flight-aircraft", id: "flight-aircraft",
data: interpolated, data: interpolated,
getPosition: (d) => [ getPosition: (d) => [
@ -588,16 +619,20 @@ export function FlightLayers({
d.latitude!, d.latitude!,
altitudeToElevation(d.baroAltitude), altitudeToElevation(d.baroAltitude),
], ],
getIcon: () => "aircraft", getOrientation: (d) => {
getSize: 22, const vr = d.verticalRate ?? 0;
const v = d.velocity ?? 0;
const pitch = v > 0 ? (-Math.atan2(vr, v) * 180) / Math.PI : 0;
const yaw = -(d.trueTrack ?? 0);
return [pitch, yaw, 90];
},
getColor: (d) => getColor: (d) =>
altColors ? altitudeToColor(d.baroAltitude) : defaultColor, altColors ? altitudeToColor(d.baroAltitude) : defaultColor,
getAngle: (d) => 360 - (d.trueTrack ?? 0), scenegraph: AIRCRAFT_SCENEGRAPH_URL,
iconAtlas: atlasUrl, sizeScale: 25,
iconMapping: AIRCRAFT_ICON_MAPPING, sizeMinPixels: AIRCRAFT_PX_PER_UNIT,
billboard: false, sizeMaxPixels: AIRCRAFT_PX_PER_UNIT,
sizeUnits: "pixels", _lighting: "pbr",
sizeScale: 1,
pickable: true, pickable: true,
onHover: handleHover, onHover: handleHover,
onClick: handleClick, onClick: handleClick,
@ -608,13 +643,15 @@ export function FlightLayers({
overlay.setProps({ layers }); overlay.setProps({ layers });
} catch (err) { } catch (err) {
if (process.env.NODE_ENV === "development") {
console.error("[aeris] FlightLayers render error:", err); console.error("[aeris] FlightLayers render error:", err);
} }
} }
}
buildAndPushLayers(); buildAndPushLayers();
return () => cancelAnimationFrame(animFrameRef.current); return () => cancelAnimationFrame(animFrameRef.current);
}, [atlasUrl, handleHover, handleClick]); }, [atlasUrl, handleHover, handleClick, map]);
return null; return null;
} }

View File

@ -50,7 +50,7 @@ export function StatusBar({
)} )}
</AnimatePresence> </AnimatePresence>
<div className="flex items-center gap-2"> <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<motion.div <motion.div
initial={{ opacity: 0, y: 12 }} initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@ -122,7 +122,7 @@ export function StatusBar({
damping: 24, damping: 24,
delay: 0.48, delay: 0.48,
}} }}
className="flex items-center gap-1 rounded-xl border px-1.5 py-1.5 backdrop-blur-2xl" className="flex items-center gap-1.5 rounded-xl border px-2.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)",
@ -133,15 +133,19 @@ export function StatusBar({
onClick={onNorthUp} onClick={onNorthUp}
aria-label="North up" aria-label="North up"
title="North up" title="North up"
className="rounded-lg px-2.5 py-1 text-[11px] font-medium tracking-wide transition-colors" className="text-[11px] font-medium tracking-wide transition-colors"
style={{ color: "rgb(var(--ui-fg) / 0.55)" }} style={{ color: "rgb(var(--ui-fg) / 0.55)" }}
> >
<Compass className="h-3.5 w-3.5" /> <Compass className="h-3 w-3" />
</button> </button>
<div
className="h-3 w-px"
style={{ backgroundColor: "rgb(var(--ui-fg) / 0.08)" }}
/>
<button <button
type="button" type="button"
onClick={onResetView} onClick={onResetView}
className="rounded-lg px-2.5 py-1 text-[11px] font-medium tracking-wide transition-colors" className="text-[11px] font-medium tracking-wide transition-colors"
style={{ color: "rgb(var(--ui-fg) / 0.55)" }} style={{ color: "rgb(var(--ui-fg) / 0.55)" }}
> >
Reset Reset