"use client"; import { useCallback, useEffect, useRef, useState } from "react"; /** Generic async loader with abort + refetch. `deps` re-run the fetch. */ export function useAsync(fn: (signal: AbortSignal) => Promise, deps: unknown[], opts: { enabled?: boolean } = {}) { const enabled = opts.enabled ?? true; const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(enabled); const [tick, setTick] = useState(0); const fnRef = useRef(fn); fnRef.current = fn; useEffect(() => { if (!enabled) { setLoading(false); return; } const ctl = new AbortController(); let alive = true; setLoading(true); fnRef .current(ctl.signal) .then((d) => { if (!alive) return; setData(d); setError(null); }) .catch((e) => { if (!alive || ctl.signal.aborted) return; setError(e instanceof Error ? e : new Error(String(e))); }) .finally(() => { if (alive) setLoading(false); }); return () => { alive = false; ctl.abort(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [...deps, tick, enabled]); const reload = useCallback(() => setTick((t) => t + 1), []); return { data, error, loading, reload, setData }; } export function useDebounced(value: T, ms = 300): T { const [v, setV] = useState(value); useEffect(() => { const t = window.setTimeout(() => setV(value), ms); return () => window.clearTimeout(t); }, [value, ms]); return v; } /** Read a query-string parameter client-side (static export cannot use dynamic routes). */ export function useQueryParam(name: string): string | null { const [v, setV] = useState(null); useEffect(() => { const read = () => setV(new URLSearchParams(window.location.search).get(name)); read(); window.addEventListener("popstate", read); return () => window.removeEventListener("popstate", read); }, [name]); return v; } export function useInterval(cb: () => void, ms: number | null) { const ref = useRef(cb); ref.current = cb; useEffect(() => { if (ms === null) return; const id = window.setInterval(() => ref.current(), ms); return () => window.clearInterval(id); }, [ms]); } export function useMediaQuery(q: string): boolean { const [m, setM] = useState(false); useEffect(() => { const mq = window.matchMedia(q); const on = () => setM(mq.matches); on(); mq.addEventListener("change", on); return () => mq.removeEventListener("change", on); }, [q]); return m; } export function useLocalStorage(key: string, initial: T): [T, (v: T) => void] { const [v, setV] = useState(initial); useEffect(() => { try { const raw = window.localStorage.getItem(key); if (raw !== null) setV(JSON.parse(raw) as T); } catch { /* ignore */ } }, [key]); const set = useCallback( (nv: T) => { setV(nv); try { window.localStorage.setItem(key, JSON.stringify(nv)); } catch { /* ignore */ } }, [key], ); return [v, set]; }