Comprehensive frontend fixes: Updated gitignore, fixed all import paths, enhanced lib utilities
49e7bf6 | /** | |
| * Production-Grade API Client | |
| * - Unified Fetch Wrapper with JWT injection. | |
| * - Centralized 401 Interception (Auto-Logout). | |
| * - Support for JSON and Multipart/Form-Data. | |
| */ | |
| const BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api/v1"; | |
| export type ApiError = { | |
| message: string; | |
| status?: number; | |
| detail?: any; | |
| }; | |
| async function request<T>( | |
| endpoint: string, | |
| options: RequestInit & { params?: Record<string, string> } = {} | |
| ): Promise<T> { | |
| const { params, headers, ...config } = options; | |
| // 1. Construct URL with Search Params | |
| const url = new URL(`${BASE_URL}${endpoint}`); | |
| if (params) { | |
| Object.entries(params).forEach(([key, val]) => url.searchParams.append(key, val)); | |
| } | |
| // 2. Token Retrieval (Direct localStorage for non-React context utility) | |
| const token = typeof window !== "undefined" ? localStorage.getItem("token") : null; | |
| // 3. Header Synthesis | |
| const authHeader = token ? { Authorization: `Bearer ${token}` } : {}; | |
| // Don't set Content-Type if sending FormData (browser handles it) | |
| const isFormData = config.body instanceof FormData; | |
| const contentTypeHeader = isFormData ? {} : { "Content-Type": "application/json" }; | |
| const finalConfig: RequestInit = { | |
| ...config, | |
| headers: { | |
| ...contentTypeHeader, | |
| ...authHeader, | |
| ...headers, | |
| }, | |
| }; | |
| try { | |
| const response = await fetch(url.toString(), finalConfig); | |
| // 4. Global Interceptor: Handle Unauthorized | |
| if (response.status === 401) { | |
| if (typeof window !== "undefined") { | |
| localStorage.removeItem("token"); | |
| // Force hard-redirect to clear state if token is dead | |
| window.location.href = "/login?error=session_expired"; | |
| } | |
| throw new Error("Unauthorized access. Please log in again."); | |
| } | |
| // 5. Success Handlers | |
| if (response.status === 204) return {} as T; | |
| const data = await response.json(); | |
| if (!response.ok) { | |
| // Return the specific backend detail if available (FastAPI style) | |
| const errorMsg = data.detail || "The research server encountered an issue."; | |
| return Promise.reject({ message: errorMsg, status: response.status, detail: data }); | |
| } | |
| return data as T; | |
| } catch (err: any) { | |
| // Handle Network Failures | |
| return Promise.reject({ | |
| message: err.message || "Unable to connect to the research server. Check your connection.", | |
| status: err.status || 500 | |
| }); | |
| } | |
| } | |
| export const api = { | |
| get: <T>(url: string, p?: Record<string, string>) => request<T>(url, { method: "GET", params: p }), | |
| post: <T>(url: string, body: any) => request<T>(url, { method: "POST", body: JSON.stringify(body) }), | |
| put: <T>(url: string, body: any) => request<T>(url, { method: "PUT", body: JSON.stringify(body) }), | |
| delete: <T>(url: string) => request<T>(url, { method: "DELETE" }), | |
| // For PICO/Avatar uploads later: | |
| upload: <T>(url: string, formData: FormData) => request<T>(url, { method: "POST", body: formData }), | |
| }; | |