Add BanTrendChart component and useBanTrend hook
- Add BanTrendBucket / BanTrendResponse interfaces to types/ban.ts - Add dashboardBansTrend endpoint constant to api/endpoints.ts - Add fetchBanTrend() to api/dashboard.ts - Create useBanTrend hook with abort-safe data fetching - Create BanTrendChart: AreaChart with gradient fill, dynamic X-axis labels per range, custom tooltip, loading/error/empty states - tsc --noEmit and ESLint pass with zero warnings
This commit is contained in:
83
frontend/src/hooks/useBanTrend.ts
Normal file
83
frontend/src/hooks/useBanTrend.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* `useBanTrend` hook.
|
||||
*
|
||||
* Fetches time-bucketed ban counts for the trend chart.
|
||||
* Re-fetches automatically when `timeRange` or `origin` changes.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { fetchBanTrend } from "../api/dashboard";
|
||||
import type { BanTrendBucket, BanOriginFilter, TimeRange } from "../types/ban";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Return type
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Return value shape for {@link useBanTrend}. */
|
||||
export interface UseBanTrendResult {
|
||||
/** Ordered list of time buckets with ban counts. */
|
||||
buckets: BanTrendBucket[];
|
||||
/** Human-readable bucket size label, e.g. `"1h"`. */
|
||||
bucketSize: string;
|
||||
/** True while a fetch is in flight. */
|
||||
isLoading: boolean;
|
||||
/** Error message or `null`. */
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Fetch and expose ban trend data for the `BanTrendChart` component.
|
||||
*
|
||||
* @param timeRange - Time-range preset: `"24h"`, `"7d"`, `"30d"`, or `"365d"`.
|
||||
* @param origin - Origin filter: `"all"`, `"blocklist"`, or `"selfblock"`.
|
||||
* @returns Ordered bucket list, bucket-size label, loading state, and error.
|
||||
*/
|
||||
export function useBanTrend(
|
||||
timeRange: TimeRange,
|
||||
origin: BanOriginFilter,
|
||||
): UseBanTrendResult {
|
||||
const [buckets, setBuckets] = useState<BanTrendBucket[]>([]);
|
||||
const [bucketSize, setBucketSize] = useState<string>("1h");
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const load = useCallback((): void => {
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
fetchBanTrend(timeRange, origin)
|
||||
.then((data) => {
|
||||
if (controller.signal.aborted) return;
|
||||
setBuckets(data.buckets);
|
||||
setBucketSize(data.bucket_size);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (controller.signal.aborted) return;
|
||||
setError(err instanceof Error ? err.message : "Failed to fetch trend data");
|
||||
})
|
||||
.finally(() => {
|
||||
if (!controller.signal.aborted) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
});
|
||||
}, [timeRange, origin]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
return (): void => {
|
||||
abortRef.current?.abort();
|
||||
};
|
||||
}, [load]);
|
||||
|
||||
return { buckets, bucketSize, isLoading, error };
|
||||
}
|
||||
Reference in New Issue
Block a user