/** * `useDashboardCountryData` hook. * * Fetches ban-by-country aggregates for dashboard chart components. Unlike * `useMapData`, this hook has no debouncing or map-specific state. * * Re-fetches automatically when `timeRange` or `origin` changes. */ import { useCallback, useEffect, useRef, useState } from "react"; import { fetchBansByCountry } from "../api/map"; import type { DashboardBanItem, BanOriginFilter, TimeRange } from "../types/ban"; // --------------------------------------------------------------------------- // Return type // --------------------------------------------------------------------------- /** Return value shape for {@link useDashboardCountryData}. */ export interface UseDashboardCountryDataResult { /** ISO alpha-2 country code → ban count. */ countries: Record; /** ISO alpha-2 country code → human-readable country name. */ countryNames: Record; /** All ban records in the selected window. */ bans: DashboardBanItem[]; /** Total ban count in the window. */ total: number; /** True while a fetch is in flight. */ isLoading: boolean; /** Error message or `null`. */ error: string | null; /** Re-fetch the data immediately. */ reload: () => void; } // --------------------------------------------------------------------------- // Hook // --------------------------------------------------------------------------- /** * Fetch and expose ban-by-country data for dashboard charts. * * @param timeRange - Time-range preset: `"24h"`, `"7d"`, `"30d"`, or `"365d"`. * @param origin - Origin filter: `"all"`, `"blocklist"`, or `"selfblock"`. * @returns Aggregated country data, ban list, loading state, and error. */ export function useDashboardCountryData( timeRange: TimeRange, origin: BanOriginFilter, ): UseDashboardCountryDataResult { const [countries, setCountries] = useState>({}); const [countryNames, setCountryNames] = useState>({}); const [bans, setBans] = useState([]); const [total, setTotal] = useState(0); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const abortRef = useRef(null); const load = useCallback((): void => { // Abort any in-flight request. abortRef.current?.abort(); const controller = new AbortController(); abortRef.current = controller; setIsLoading(true); setError(null); fetchBansByCountry(timeRange, origin) .then((data) => { if (controller.signal.aborted) return; setCountries(data.countries); setCountryNames(data.country_names); // MapBanItem and DashboardBanItem are structurally identical. setBans(data.bans as DashboardBanItem[]); setTotal(data.total); }) .catch((err: unknown) => { if (controller.signal.aborted) return; setError(err instanceof Error ? err.message : "Failed to fetch data"); }) .finally(() => { if (!controller.signal.aborted) { setIsLoading(false); } }); }, [timeRange, origin]); useEffect(() => { load(); return (): void => { abortRef.current?.abort(); }; }, [load]); return { countries, countryNames, bans, total, isLoading, error, reload: load }; }