TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1"use client";23import { useCallback, useEffect, useRef, useState } from "react";4import { usePathname, useRouter } from "next/navigation";5import { api, ApiClientError } from "./api";6import { useSession } from "./store";78export interface ApiState<T> {9 data: T | null;10 error: ApiClientError | null;11 loading: boolean;12 reload: () => Promise<void>;13 setData: React.Dispatch<React.SetStateAction<T | null>>;14}1516/**17 * Client-side data hook. Handles the shared failure modes:18 * 401 → session expired → redirect to /login?next=…19 * 503 MAINTENANCE / network → surfaced as `error` for <ApiErrorState/>.20 */21export function useApi<T>(path: string | null): ApiState<T> {22 const [data, setData] = useState<T | null>(null);23 const [error, setError] = useState<ApiClientError | null>(null);24 const [loading, setLoading] = useState<boolean>(path !== null);25 const [tick, setTick] = useState(0);26 const router = useRouter();27 const pathname = usePathname();28 const hydrate = useSession((s) => s.hydrate);29 const seq = useRef(0);3031 // When the path changes, flip back to loading during render (adjust-state-from-props).32 const [prevPath, setPrevPath] = useState(path);33 if (path !== prevPath) {34 setPrevPath(path);35 setLoading(path !== null);36 }3738 useEffect(() => {39 if (!path) return;40 const my = ++seq.current;41 api<T>(path)42 .then((res) => {43 if (my !== seq.current) return;44 setData(res);45 setError(null);46 setLoading(false);47 })48 .catch((e: unknown) => {49 if (my !== seq.current) return;50 const err = e instanceof ApiClientError ? e : new ApiClientError(0, "UNKNOWN", "Something went wrong.");51 if (err.status === 401) {52 hydrate(null);53 router.replace(`/login?next=${encodeURIComponent(pathname)}&reason=expired`);54 return;55 }56 setError(err);57 setLoading(false);58 });59 }, [path, tick, router, pathname, hydrate]);6061 const reload = useCallback(async () => {62 setLoading(true);63 setTick((t) => t + 1);64 }, []);6566 return { data, error, loading, reload, setData };67}6869/** Human copy for an API failure. */70export function describeError(e: unknown): { title: string; description: string; kind: "network" | "maintenance" | "server" | "other" } {71 if (e instanceof ApiClientError) {72 if (e.status === 0) return { kind: "network", title: "Connection lost", description: "Check your network and try again. Your credits and progress are safe." };73 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." };74 if (e.status >= 500) return { kind: "server", title: "Server unavailable", description: "Spinza is temporarily unreachable. Please try again shortly." };75 return { kind: "other", title: "Something went wrong", description: e.message };76 }77 return { kind: "other", title: "Something went wrong", description: "Please try again." };78}79