declare global { interface Window { env?: { API_URL?: string; }; } } const getApiUrl = () => { // 1. Runtime config (Docker) if (window.env?.API_URL) { return `${window.env.API_URL}/api/v1`; } // 2. LocalStorage override const stored = localStorage.getItem("k_notes_api_url"); if (stored) { return `${stored}/api/v1`; } // 3. Default fallback return "http://localhost:3000/api/v1"; }; export const getBaseUrl = () => { if (window.env?.API_URL) { return window.env.API_URL; } const stored = localStorage.getItem("k_notes_api_url"); return stored ? stored : "http://localhost:3000"; } export class ApiError extends Error { public status: number; constructor(status: number, message: string) { super(message); this.status = status; this.name = "ApiError"; } } async function fetchWithAuth(endpoint: string, options: RequestInit = {}) { const url = `${getApiUrl()}${endpoint}`; const headers = { "Content-Type": "application/json", ...options.headers, }; const config: RequestInit = { ...options, headers, credentials: "include", // Important for cookies! // signal: controller.signal, // Removing signal, using race instead }; try { const fetchPromise = fetch(url, config); const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new TypeError("Network request timed out")), 3000) ); const response = (await Promise.race([fetchPromise, timeoutPromise])) as Response; // clearTimeout(timeoutId); // Not needed with race logic here (though leaking timer? No, race settles.) if (!response.ok) { // Try to parse error message let errorMessage = "An error occurred"; try { const errorData = await response.json(); errorMessage = errorData.error?.message || errorData.message || errorMessage; } catch { // failed to parse json } throw new ApiError(response.status, errorMessage); } // For 204 No Content or empty responses if (response.status === 204) { return null; } // Try to parse JSON try { return await response.json(); } catch { return null; } } catch (error) { throw error; } } export const api = { get: (endpoint: string) => fetchWithAuth(endpoint, { method: "GET" }), post: (endpoint: string, body: any) => fetchWithAuth(endpoint, { method: "POST", body: JSON.stringify(body), }), patch: (endpoint: string, body: any) => fetchWithAuth(endpoint, { method: "PATCH", body: JSON.stringify(body), }), delete: (endpoint: string) => fetchWithAuth(endpoint, { method: "DELETE" }), exportData: async () => { const response = await fetch(`${getApiUrl()}/export`, { credentials: "include", }); if (!response.ok) throw new ApiError(response.status, "Failed to export data"); return response.blob(); }, importData: (data: any) => api.post("/import", data), };