feat: add flight tracking components and hooks

- Introduced FlightCard component for displaying flight information with animations.
- Added ScrollArea component for custom scroll behavior.
- Implemented StatusBar component to show flight count and loading status.
- Created useFlights hook for fetching and managing flight data based on city selection.
- Developed useSettings hook for managing user settings with local storage persistence.
- Added useTrailHistory hook for managing flight trail data.
- Defined City type and CITIES constant for city data management.
- Implemented flight utility functions for altitude and speed conversions.
- Created map styles for different visual representations.
- Added OpenSky API integration for fetching flight data.
- Implemented utility functions for class name merging.
- Configured TypeScript settings for the project.
This commit is contained in:
Kewonit
2026-02-14 12:26:44 +05:30
commit b3f20b7659
37 changed files with 9356 additions and 0 deletions

119
src/hooks/use-flights.ts Normal file
View File

@ -0,0 +1,119 @@
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import {
fetchFlightsByBbox,
bboxFromCenter,
type FlightState,
} from "@/lib/opensky";
import type { City } from "@/lib/cities";
const POLL_INTERVAL_MS = 15_000;
const RATE_LIMIT_BACKOFF_MS = 30_000;
export function useFlights(city: City | null) {
const [flights, setFlights] = useState<FlightState[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [rateLimited, setRateLimited] = useState(false);
const [retryIn, setRetryIn] = useState(0);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const countdownRef = useRef<ReturnType<typeof setInterval> | null>(null);
const abortRef = useRef<AbortController | null>(null);
const clearCountdown = useCallback(() => {
if (countdownRef.current) {
clearInterval(countdownRef.current);
countdownRef.current = null;
}
setRetryIn(0);
}, []);
const startCountdown = useCallback(
(ms: number) => {
clearCountdown();
const endTime = Date.now() + ms;
setRetryIn(Math.ceil(ms / 1000));
countdownRef.current = setInterval(() => {
const remaining = Math.max(0, Math.ceil((endTime - Date.now()) / 1000));
setRetryIn(remaining);
if (remaining <= 0) clearCountdown();
}, 1000);
},
[clearCountdown],
);
const scheduleNext = useCallback(
(target: City, delayMs: number) => {
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => fetchData(target), delayMs);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const fetchData = useCallback(
async (target: City) => {
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
try {
setLoading(true);
setError(null);
const bbox = bboxFromCenter(
target.coordinates[0],
target.coordinates[1],
target.radius,
);
const result = await fetchFlightsByBbox(...bbox, controller.signal);
if (result.rateLimited) {
setRateLimited(true);
startCountdown(RATE_LIMIT_BACKOFF_MS);
scheduleNext(target, RATE_LIMIT_BACKOFF_MS);
return;
}
setRateLimited(false);
clearCountdown();
setFlights(result.flights);
scheduleNext(target, POLL_INTERVAL_MS);
} catch (err) {
if (err instanceof DOMException && err.name === "AbortError") return;
setError(err instanceof Error ? err.message : "Unknown error");
setFlights([]);
scheduleNext(target, RATE_LIMIT_BACKOFF_MS);
} finally {
setLoading(false);
}
},
[scheduleNext, startCountdown, clearCountdown],
);
useEffect(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
if (!city) {
setFlights([]);
setRateLimited(false);
clearCountdown();
return;
}
setRateLimited(false);
clearCountdown();
fetchData(city);
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
abortRef.current?.abort();
clearCountdown();
};
}, [city, fetchData, clearCountdown]);
return { flights, loading, error, rateLimited, retryIn };
}