/** * API client — typed fetch wrapper + TanStack Query setup. * * All requests hit relative URLs (e.g. `/api/cases`) which next.config.ts * rewrites transparently proxy to legal-ai.nautilus.marcusgroup.org. No CORS * gymnastics, no direct public-URL references in component code. */ import { QueryClient } from "@tanstack/react-query"; export class ApiError extends Error { constructor( message: string, public readonly status: number, public readonly body: unknown, ) { super(message); this.name = "ApiError"; } } type RequestOptions = { method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; body?: unknown; signal?: AbortSignal; }; /** * Typed JSON request. Throws ApiError on non-2xx responses with the parsed body. * Always returns parsed JSON — callers pass the expected type parameter. */ export async function apiRequest( path: string, { method = "GET", body, signal }: RequestOptions = {}, ): Promise { const res = await fetch(path, { method, headers: body ? { "Content-Type": "application/json" } : undefined, body: body ? JSON.stringify(body) : undefined, signal, }); const contentType = res.headers.get("content-type") ?? ""; const parsed = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => null); if (!res.ok) { throw new ApiError( `${method} ${path} failed with ${res.status}`, res.status, parsed, ); } return parsed as T; } /** * Shared TanStack Query client. Defaults are tuned for a dashboard that polls * for case progress — stale for 5 seconds, 1 automatic retry, no background * refetch on window focus (preserves editor state during long sessions). */ export function makeQueryClient(): QueryClient { return new QueryClient({ defaultOptions: { queries: { staleTime: 5_000, retry: 1, refetchOnWindowFocus: false, }, mutations: { retry: 0, }, }, }); }