"use client"; import { useCallback, useEffect, useRef, useState } from "react"; import { usePathname, useRouter } from "next/navigation"; import { api, ApiClientError } from "./api"; import { useSession } from "./store"; export interface ApiState { data: T | null; error: ApiClientError | null; loading: boolean; reload: () => Promise; setData: React.Dispatch>; } /** * Client-side data hook. Handles the shared failure modes: * 401 → session expired → redirect to /login?next=… * 503 MAINTENANCE / network → surfaced as `error` for . */ export function useApi(path: string | null): ApiState { const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(path !== null); const [tick, setTick] = useState(0); const router = useRouter(); const pathname = usePathname(); const hydrate = useSession((s) => s.hydrate); const seq = useRef(0); // When the path changes, flip back to loading during render (adjust-state-from-props). const [prevPath, setPrevPath] = useState(path); if (path !== prevPath) { setPrevPath(path); setLoading(path !== null); } useEffect(() => { if (!path) return; const my = ++seq.current; api(path) .then((res) => { if (my !== seq.current) return; setData(res); setError(null); setLoading(false); }) .catch((e: unknown) => { if (my !== seq.current) return; const err = e instanceof ApiClientError ? e : new ApiClientError(0, "UNKNOWN", "Something went wrong."); if (err.status === 401) { hydrate(null); router.replace(`/login?next=${encodeURIComponent(pathname)}&reason=expired`); return; } setError(err); setLoading(false); }); }, [path, tick, router, pathname, hydrate]); const reload = useCallback(async () => { setLoading(true); setTick((t) => t + 1); }, []); return { data, error, loading, reload, setData }; } /** Human copy for an API failure. */ export function describeError(e: unknown): { title: string; description: string; kind: "network" | "maintenance" | "server" | "other" } { if (e instanceof ApiClientError) { if (e.status === 0) return { kind: "network", title: "Connection lost", description: "Check your network and try again. Your credits and progress are safe." }; if (e.status === 503 && e.code === "MAINTENANCE") return { kind: "maintenance", title: "Spinza is getting an upgrade.", description: "Your credits and progress are safe. Check back in a few minutes." }; if (e.status >= 500) return { kind: "server", title: "Server unavailable", description: "Spinza is temporarily unreachable. Please try again shortly." }; return { kind: "other", title: "Something went wrong", description: e.message }; } return { kind: "other", title: "Something went wrong", description: "Please try again." }; }