// ============================================================================ // API Client // ============================================================================ import { API_URL, STORAGE_KEYS } from "@/lib/utils/constants"; import type { ApiError } from "@/lib/types/api"; // ============================================================================ // Types // ============================================================================ interface RequestOptions extends RequestInit { params?: Record; requireAuth?: boolean; } // ============================================================================ // Helper Functions // ============================================================================ /** * Build URL with query parameters */ const buildUrl = (endpoint: string, params?: Record): string => { if (!params || Object.keys(params).length === 0) { return endpoint; } const searchParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null) { if (Array.isArray(value)) { value.forEach((v) => searchParams.append(key, String(v))); } else { searchParams.append(key, String(value)); } } }); const queryString = searchParams.toString(); return queryString ? `${endpoint}?${queryString}` : endpoint; }; /** * Get auth token from storage */ const getAuthToken = (): string | null => { if (typeof window === "undefined") return null; return localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN); }; /** * Clear auth tokens from storage */ export const clearAuthTokens = (): void => { if (typeof window === "undefined") return; localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN); localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN); localStorage.removeItem(STORAGE_KEYS.USER); }; /** * Save auth tokens to storage */ export const saveAuthTokens = ( accessToken: string, refreshToken?: string ): void => { if (typeof window === "undefined") return; localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken); if (refreshToken) { localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken); } }; // ============================================================================ // API Client // ============================================================================ /** * Main API request function */ export const apiRequest = async ( endpoint: string, options: RequestOptions = {} ): Promise => { const { params, requireAuth = true, ...fetchOptions } = options; const body = fetchOptions.body; // Build URL const url = buildUrl(`${API_URL}${endpoint}`, params); const headers: HeadersInit = { ...fetchOptions.headers, }; // Don't set Content-Type for FormData - browser will set it with boundary if (!(body instanceof FormData)) { (headers as Record)["Content-Type"] = "application/json"; } // Add auth token if required if (requireAuth) { const token = getAuthToken(); if (token) { (headers as Record)["Authorization"] = `Bearer ${token}`; } } try { // Serialize body to JSON if it's an object (not FormData or string) let serializedBody = body; if (body && typeof body === 'object' && !(body instanceof FormData) && !(body instanceof String)) { serializedBody = JSON.stringify(body); } const response = await fetch(url, { ...fetchOptions, headers, body: serializedBody, }); // Handle 204 No Content if (response.status === 204) { return undefined as T; } // Parse response const data = await response.json(); // Handle errors if (!response.ok) { // FastAPI returns errors in format { "detail": "..." } const error: ApiError = { detail: data.detail || "An error occurred", }; throw error; } return data as T; } catch (error: any) { // Network error or parsing error if (!error.detail) { throw { detail: error.message || "Network error occurred", } as ApiError; } throw error; } }; // ============================================================================ // Convenience Methods // ============================================================================ export const api = { get: (endpoint: string, options?: RequestOptions) => apiRequest(endpoint, { ...options, method: "GET" }), post: (endpoint: string, body?: any, options?: RequestOptions) => apiRequest(endpoint, { ...options, method: "POST", body, }), put: (endpoint: string, body?: any, options?: RequestOptions) => apiRequest(endpoint, { ...options, method: "PUT", body, }), patch: (endpoint: string, body?: any, options?: RequestOptions) => apiRequest(endpoint, { ...options, method: "PATCH", body, }), delete: (endpoint: string, options?: RequestOptions) => apiRequest(endpoint, { ...options, method: "DELETE" }), };