/** * `useMapData` hook — fetches and manages ban-by-country data. */ import { useCallback, useEffect, useRef, useState } from "react"; import { fetchBansByCountry } from "../api/map"; import type { BansByCountryResponse, MapBanItem, TimeRange } from "../types/map"; import type { BanOriginFilter } from "../types/ban"; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- /** Debounce delay in milliseconds before firing a fetch after filter changes. */ const DEBOUNCE_MS = 300; // --------------------------------------------------------------------------- // Return type // --------------------------------------------------------------------------- export interface UseMapDataResult { /** Per-country ban counts (ISO alpha-2 → count). */ countries: Record; /** ISO alpha-2 → country name mapping. */ countryNames: Record; /** All ban records in the selected window. */ bans: MapBanItem[]; /** Total ban count. */ total: number; /** True while a fetch is in flight. */ loading: boolean; /** Error message or null. */ error: string | null; /** Trigger a manual re-fetch. */ refresh: () => void; } // --------------------------------------------------------------------------- // Hook // --------------------------------------------------------------------------- export function useMapData( range: TimeRange = "24h", origin: BanOriginFilter = "all", ): UseMapDataResult { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const abortRef = useRef(null); const debounceRef = useRef | null>(null); const load = useCallback((): void => { // Cancel any pending debounce timer. if (debounceRef.current != null) { clearTimeout(debounceRef.current); } // Show loading immediately so the skeleton / spinner appears. setLoading(true); setError(null); debounceRef.current = setTimeout((): void => { // Abort any in-flight request from a previous filter selection. abortRef.current?.abort(); abortRef.current = new AbortController(); fetchBansByCountry(range, origin) .then((resp) => { setData(resp); }) .catch((err: unknown) => { if (err instanceof Error && err.name !== "AbortError") { setError(err.message); } }) .finally((): void => { setLoading(false); }); }, DEBOUNCE_MS); }, [range, origin]); useEffect((): (() => void) => { load(); return (): void => { if (debounceRef.current != null) { clearTimeout(debounceRef.current); } abortRef.current?.abort(); }; }, [load]); return { countries: data?.countries ?? {}, countryNames: data?.country_names ?? {}, bans: data?.bans ?? [], total: data?.total ?? 0, loading, error, refresh: load, }; }