SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%
3.0 KB · 79 lines typescript
Raw Blame History
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