TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1"use client";23import { useCallback, useEffect, useRef, useState } from "react";4import { api, ApiClientError } from "@/lib/api";5import { useAdmin } from "./store";67interface QueryState<T> {8 path: string | null;9 data: T | null;10 error: ApiClientError | null;11 at: number;12}1314export interface QueryResult<T> {15 /** Latest data — kept (and flagged `stale`) while a new path is loading, so tables never flash. */16 data: T | null;17 error: ApiClientError | null;18 /** True until the first response for the current path arrives. */19 loading: boolean;20 /** `data` belongs to a previous path. */21 stale: boolean;22 /** A user-triggered refresh is in flight. */23 refreshing: boolean;24 refresh: () => Promise<void>;25 /** Replace the cached data locally (optimistic updates). */26 mutate: (fn: (prev: T) => T) => void;27 updatedAt: number;28}2930/** Client-side data hook for `/api/admin/*`. A 401 flips the admin gate back to the login form. */31export function useAdminQuery<T>(path: string | null, opts: { refreshMs?: number } = {}): QueryResult<T> {32 const [state, setState] = useState<QueryState<T>>({ path: null, data: null, error: null, at: 0 });33 const [refreshing, setRefreshing] = useState(false);34 const seq = useRef(0);35 const refreshMs = opts.refreshMs ?? 0;3637 const run = useCallback(async (p: string) => {38 const my = ++seq.current;39 try {40 const data = await api<T>(p);41 if (my !== seq.current) return;42 setState({ path: p, data, error: null, at: Date.now() });43 } catch (e) {44 if (my !== seq.current) return;45 const err = e instanceof ApiClientError ? e : new ApiClientError(0, "UNKNOWN", e instanceof Error ? e.message : "Unexpected error.");46 if (err.status === 401) useAdmin.getState().signedOut();47 setState((s) => ({ path: p, data: s.path === p ? s.data : null, error: err, at: Date.now() }));48 }49 }, []);5051 useEffect(() => {52 if (!path) return;53 const first = setTimeout(() => void run(path), 0);54 const t = refreshMs55 ? setInterval(() => {56 if (document.visibilityState === "visible") void run(path);57 }, refreshMs)58 : null;59 return () => {60 clearTimeout(first);61 if (t) clearInterval(t);62 };63 }, [path, run, refreshMs]);6465 const refresh = useCallback(async () => {66 if (!path) return;67 setRefreshing(true);68 await run(path);69 setRefreshing(false);70 }, [path, run]);7172 const mutate = useCallback((fn: (prev: T) => T) => {73 setState((s) => (s.data ? { ...s, data: fn(s.data) } : s));74 }, []);7576 const current = state.path === path;77 return {78 data: state.data,79 error: current ? state.error : null,80 loading: path !== null && !current,81 stale: path !== null && !current && state.data !== null,82 refreshing,83 refresh,84 mutate,85 updatedAt: state.at,86 };87}88