Files
Spanglish/frontend/src/lib/api/client.ts
T
MichilisandClaude Fable 5 71c277045b Centralize seat capacity accounting and the payment provider registry.
Replace manualProviders.ts with a paymentProviders.ts registry
(automatic vs manual settlement) and move all seat counting into
capacity.ts as the single source of truth: only paid/checked-in tickets
and pending_approval payments hold a seat, so abandoned checkouts never
block sales. Admins can now knowingly approve a payment over capacity
(allowOverCapacity), with the booking and admin UIs updated to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 04:58:40 +00:00

72 lines
2.1 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');
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<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 };
}