/** * Normalizes a URL to ensure it has the correct /api prefix * This prevents double prefixing which causes API errors */ function normalizeApiUrl(url: string): string { // Remove any existing /api or api prefix const cleanPath = url.replace(/^\/?(api\/)+/, ''); // Add a single /api prefix return `/api/${cleanPath.replace(/^\//, '')}`; } /** * Get the authentication token from cookies or localStorage */ function getAuthToken(): string | null { if (typeof document === 'undefined') return null; // Guard for SSR return document.cookie .split('; ') .find(row => row.startsWith('Authorization=')) ?.split('=')[1] || localStorage.getItem('Authorization'); } /** * Creates standard API request headers with authentication * * @param token Optional auth token (fetched automatically if not provided) * @param customHeaders Additional headers to include * @returns Headers object ready for fetch requests */ function createApiHeaders(token?: string | null, customHeaders: Record = {}): Headers { const headers = new Headers({ 'Content-Type': 'application/json', ...customHeaders }); const authToken = token || getAuthToken(); if (authToken) { headers.set('Authorization', `Bearer ${authToken}`); } return headers; } /** * Simple client-side fetch function for making API calls with Authorization header. * Ensures all requests go through the Next.js API proxy. */ export async function clientFetch(url: string, options: RequestInit = {}): Promise { try { // Create headers with authentication const headers = createApiHeaders(null, options.headers as Record); // Normalize URL to ensure it uses the Next.js API proxy const fullUrl = normalizeApiUrl(url); console.log(`Fetching URL: ${fullUrl}`); const res = await fetch(fullUrl, { ...options, headers, credentials: 'include', }); if (!res.ok) { const errorData = await res.json().catch(() => ({})); const errorMessage = errorData.message || errorData.error || `Request failed: ${res.status} ${res.statusText}`; console.error('API Error:', { status: res.status, url: fullUrl, response: errorData, method: options.method || 'GET' }); throw new Error(errorMessage); } // Handle 204 No Content responses if (res.status === 204) { return {} as T; } return await res.json(); } catch (error) { console.error(`Client fetch error at ${url}:`, error); throw error; } } /** * Get a cookie value by name */ export function getCookie(name: string): string | undefined { return document.cookie .split('; ') .find(row => row.startsWith(`${name}=`)) ?.split('=')[1]; }