export const API_BASE = process.env.NEXT_PUBLIC_API_URL || ''; export interface ApiError { error: string; } /** Read the stored auth token (browser only). */ export function getToken(): string | null { return typeof window !== 'undefined' ? localStorage.getItem('spanglish-token') : null; } export async function fetchApi( endpoint: string, options: RequestInit = {} ): Promise { const token = getToken(); const headers: HeadersInit = { 'Content-Type': 'application/json', ...options.headers, }; if (token) { (headers as Record)['Authorization'] = `Bearer ${token}`; } const res = await fetch(`${API_BASE}${endpoint}`, { ...options, headers, }); if (!res.ok) { const errorData = await res.json().catch(() => ({ error: 'Request failed' })); const errorMessage = typeof errorData.error === 'string' ? errorData.error : (errorData.message || JSON.stringify(errorData) || 'Request failed'); const error = new Error(errorMessage); // Preserve structured error info (e.g. code: 'EVENT_OVER_CAPACITY') so // callers can react beyond the message text. (error as any).code = errorData.code; (error as any).data = errorData; throw error; } return res.json(); } /** * Fetch a file download (CSV/blob) with auth, returning the blob and the * filename parsed from the Content-Disposition header. */ export async function fetchBlob( endpoint: string, fallbackFilename: string ): Promise<{ blob: Blob; filename: string }> { const token = getToken(); const headers: Record = {}; if (token) headers['Authorization'] = `Bearer ${token}`; const res = await fetch(`${API_BASE}${endpoint}`, { headers }); if (!res.ok) { const errorData = await res.json().catch(() => ({ error: 'Export failed' })); throw new Error(errorData.error || 'Export failed'); } const disposition = res.headers.get('Content-Disposition') || ''; const filenameMatch = disposition.match(/filename="?([^"]+)"?/); const filename = filenameMatch ? filenameMatch[1] : fallbackFilename; const blob = await res.blob(); return { blob, filename }; }