Split oversized frontend API client, email service, and admin/booking pages into focused modules while preserving import surfaces, and add Redis-backed queues, stale booking cleanup, stronger auth, and scale deployment configs. Co-authored-by: Cursor <cursoragent@cursor.com>
67 lines
1.9 KiB
TypeScript
67 lines
1.9 KiB
TypeScript
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<T>(
|
|
endpoint: string,
|
|
options: RequestInit = {}
|
|
): Promise<T> {
|
|
const token = getToken();
|
|
|
|
const headers: HeadersInit = {
|
|
'Content-Type': 'application/json',
|
|
...options.headers,
|
|
};
|
|
|
|
if (token) {
|
|
(headers as Record<string, string>)['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');
|
|
throw new Error(errorMessage);
|
|
}
|
|
|
|
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<string, string> = {};
|
|
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 };
|
|
}
|