// Thin fetch wrapper for the meditation API. The app is a static SPA, so every // call runs in the browser and carries the bearer token from localStorage. import { APPRO_TOKEN_STORAGE_KEY, MEDITATION_BASE_URL, TOKEN_STORAGE_KEY, } from "./config"; export class ApiError extends Error { status: number; data: unknown; constructor(message: string, status: number, data: unknown) { super(message); this.name = "ApiError"; this.status = status; this.data = data; } } export function getToken(): string | null { if (typeof window === "undefined") return null; return window.localStorage.getItem(TOKEN_STORAGE_KEY); } export function setToken(token: string) { if (typeof window === "undefined") return; window.localStorage.setItem(TOKEN_STORAGE_KEY, token); } export function clearToken() { if (typeof window === "undefined") return; window.localStorage.removeItem(TOKEN_STORAGE_KEY); } export function getApproToken(): string | null { if (typeof window === "undefined") return null; return window.localStorage.getItem(APPRO_TOKEN_STORAGE_KEY); } export function setApproToken(token: string) { if (typeof window === "undefined") return; window.localStorage.setItem(APPRO_TOKEN_STORAGE_KEY, token); } export function clearApproToken() { if (typeof window === "undefined") return; window.localStorage.removeItem(APPRO_TOKEN_STORAGE_KEY); } type Query = Record; interface RequestOptions { method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; // Plain object -> JSON body. FormData -> multipart (browser sets boundary). body?: unknown; query?: Query; // Override/extend the base url (e.g. the approagency host for login). baseUrl?: string; // Skip attaching the bearer token (used by the login calls). auth?: boolean; // Extra headers to merge into the request. headers?: Record; } function buildUrl(path: string, query?: Query, baseUrl = MEDITATION_BASE_URL) { const url = new URL( `${baseUrl}${path.startsWith("/") ? path : `/${path}`}`, ); if (query) { for (const [key, value] of Object.entries(query)) { if (value !== undefined && value !== null && value !== "") { url.searchParams.set(key, String(value)); } } } return url.toString(); } export async function apiFetch( path: string, options: RequestOptions = {}, ): Promise { const { method = "GET", body, query, baseUrl, auth = true, headers: extraHeaders } = options; const headers: Record = { Accept: "application/json" }; if (auth) { const token = getToken(); if (token) headers.Authorization = `Bearer ${token}`; } if (extraHeaders) { Object.assign(headers, extraHeaders); } let payload: BodyInit | undefined; if (body instanceof FormData) { payload = body; // browser sets multipart Content-Type + boundary } else if (body !== undefined) { headers["Content-Type"] = "application/json"; payload = JSON.stringify(body); } const res = await fetch(buildUrl(path, query, baseUrl), { method, headers, body: payload, }); const text = await res.text(); let data: unknown = null; if (text) { try { data = JSON.parse(text); } catch { data = text; } } if (!res.ok) { const message = (data && typeof data === "object" && "message" in data ? String((data as { message: unknown }).message) : null) ?? `خطای ارتباط با سرور (${res.status})`; throw new ApiError(message, res.status, data); } return data as T; } // Many Laravel resource endpoints wrap the payload as { data: ... }. Unwrap it // when present so callers always get the raw value. export function unwrap(res: unknown): T { if (res && typeof res === "object" && "data" in res) { return (res as { data: T }).data; } return res as T; } // Convenience builder for multipart bodies. Skips undefined/null and expands // arrays to repeated `key[]` entries the way Laravel expects. export function toFormData(fields: Record): FormData { const fd = new FormData(); for (const [key, value] of Object.entries(fields)) { if (value === undefined || value === null || value === "") continue; if (value instanceof File || value instanceof Blob) { fd.append(key, value); } else if (Array.isArray(value)) { for (const item of value) fd.append(`${key}[]`, String(item)); } else if (typeof value === "boolean") { fd.append(key, value ? "1" : "0"); } else { fd.append(key, String(value)); } } return fd; }