/** * Shared API client infrastructure: request helper, error class, auth token management. */ export const BASE = "/api/v1"; export const AUTH_TOKEN_KEY = "chrysopedia_token"; export class ApiError extends Error { constructor( public status: number, public detail: string, ) { super(`API ${status}: ${detail}`); this.name = "ApiError"; } } function getStoredToken(): string | null { try { return localStorage.getItem(AUTH_TOKEN_KEY); } catch { return null; } } export async function request(url: string, init?: RequestInit): Promise { const token = getStoredToken(); const headers: Record = { "Content-Type": "application/json", ...(init?.headers as Record), }; if (token && !headers["Authorization"]) { headers["Authorization"] = `Bearer ${token}`; } const res = await fetch(url, { ...init, headers, }); if (!res.ok) { let detail = res.statusText; try { const body: unknown = await res.json(); if (typeof body === "object" && body !== null && "detail" in body) { const d = (body as { detail: unknown }).detail; detail = typeof d === "string" ? d : Array.isArray(d) ? d.map((e: any) => e.msg || JSON.stringify(e)).join("; ") : JSON.stringify(d); } } catch { // body not JSON — keep statusText } throw new ApiError(res.status, detail); } return res.json() as Promise; } /** * Multipart form-data request — does NOT set Content-Type (browser adds * multipart boundary automatically). Still attaches auth token. */ export async function requestMultipart( url: string, formData: FormData, init?: RequestInit, ): Promise { const token = getStoredToken(); const headers: Record = { ...(init?.headers as Record), }; if (token && !headers["Authorization"]) { headers["Authorization"] = `Bearer ${token}`; } const res = await fetch(url, { method: "POST", ...init, headers, body: formData, }); if (!res.ok) { let detail = res.statusText; try { const body: unknown = await res.json(); if (typeof body === "object" && body !== null && "detail" in body) { const d = (body as { detail: unknown }).detail; detail = typeof d === "string" ? d : Array.isArray(d) ? d.map((e: any) => e.msg || JSON.stringify(e)).join("; ") : JSON.stringify(d); } } catch { // body not JSON — keep statusText } throw new ApiError(res.status, detail); } return res.json() as Promise; }