import { useUiStore } from "../stores/ui"; export interface ApiError { error: string; status: number; } // Surface infra / unexpected failures (network, 5xx) globally. 4xx are left to // the caller — forms and views show their own inline messages for those. function notifyError(err: ApiError) { try { useUiStore().showToast(err.error); } catch { /* pinia not active yet (very early boot) — skip the toast */ } } async function request(method: string, path: string, body?: unknown): Promise { // FormData carries its own multipart Content-Type (with boundary), so let the // browser set it — only JSON bodies get an explicit header + stringify. const isForm = body instanceof FormData; let resp: Response; try { resp = await fetch(path, { method, credentials: "include", // send/receive the signed session cookie headers: body !== undefined && !isForm ? { "Content-Type": "application/json" } : undefined, body: body === undefined ? undefined : isForm ? body : JSON.stringify(body), }); } catch { const err: ApiError = { error: "Can't reach the server. Check your connection.", status: 0 }; notifyError(err); throw err; } const text = await resp.text(); let data: unknown = {}; if (text) { try { data = JSON.parse(text); } catch { data = {}; // non-JSON body (e.g. a proxy error page) — fall through to status handling } } if (!resp.ok) { const message = typeof data === "object" && data !== null && "error" in data ? String((data as { error: unknown }).error) : "Something went wrong. Please try again."; const err: ApiError = { error: message, status: resp.status }; if (resp.status >= 500) notifyError(err); throw err; } return data as T; } export const api = { get: (path: string) => request("GET", path), post: (path: string, body?: unknown) => request("POST", path, body), patch: (path: string, body?: unknown) => request("PATCH", path, body), put: (path: string, body?: unknown) => request("PUT", path, body), del: (path: string) => request("DELETE", path), postForm: (path: string, form: FormData) => request("POST", path, form), };