Files
BanGUI/frontend/src/hooks/useDashboardCountryData.ts

99 lines
3.4 KiB
TypeScript

/**
* `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 { handleFetchError } from "../utils/fetchError";
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<string, number>;
/** ISO alpha-2 country code → human-readable country name. */
countryNames: Record<string, string>;
/** 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<Record<string, number>>({});
const [countryNames, setCountryNames] = useState<Record<string, string>>({});
const [bans, setBans] = useState<DashboardBanItem[]>([]);
const [total, setTotal] = useState<number>(0);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const abortRef = useRef<AbortController | null>(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;
handleFetchError(err, setError, "Failed to fetch dashboard country 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 };
}