- Live waitlist integration with backend API - Real-time polling every 60 seconds - Comprehensive API logging and error handling - WaitlistContext for global state management - Responsive UI with live data updates - Registration form with automatic waitlist refresh - Modern React/TypeScript architecture - Tailwind CSS styling with luxury theme
96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
import { API_BASE_URL } from '../config';
|
|
import { logger } from './logger';
|
|
|
|
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
|
|
let tokenGetter: (() => string | null) | null = null;
|
|
|
|
export const setAuthTokenGetter = (getter: () => string | null) => {
|
|
tokenGetter = getter;
|
|
};
|
|
|
|
export interface HttpError extends Error {
|
|
status: number;
|
|
data?: any;
|
|
}
|
|
|
|
async function request<T>(
|
|
path: string,
|
|
options: {
|
|
method?: HttpMethod;
|
|
body?: any;
|
|
headers?: Record<string, string>;
|
|
contentType?: 'json' | 'form' | 'none';
|
|
signal?: AbortSignal;
|
|
} = {}
|
|
): Promise<T> {
|
|
const { method = 'GET', body, headers = {}, contentType = 'json', signal } = options;
|
|
|
|
const finalHeaders: Record<string, string> = { ...headers };
|
|
const token = tokenGetter?.() || null;
|
|
if (token) finalHeaders['Authorization'] = `Bearer ${token}`;
|
|
if (contentType === 'json') finalHeaders['Content-Type'] = 'application/json';
|
|
if (contentType === 'form') finalHeaders['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
|
|
let payload: BodyInit | undefined;
|
|
if (body != null) {
|
|
if (contentType === 'json') payload = JSON.stringify(body);
|
|
else if (contentType === 'form') payload = new URLSearchParams(body).toString();
|
|
else payload = body as BodyInit;
|
|
}
|
|
|
|
const startTime = Date.now();
|
|
const url = `${API_BASE_URL}${path}`;
|
|
|
|
// Log outgoing request
|
|
logger.apiRequest(method, url, {
|
|
headers: { ...finalHeaders, Authorization: finalHeaders.Authorization ? '[REDACTED]' : undefined },
|
|
body: payload ? (typeof payload === 'string' ? payload : '[BLOB]') : undefined
|
|
});
|
|
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers: finalHeaders,
|
|
body: payload,
|
|
signal,
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
const isJson = res.headers.get('content-type')?.includes('application/json');
|
|
const data = isJson ? await res.json().catch(() => undefined) : await res.text();
|
|
|
|
if (!res.ok) {
|
|
// Log error response
|
|
logger.apiError(res.status, method, url, {
|
|
status: res.status,
|
|
statusText: res.statusText,
|
|
data,
|
|
duration
|
|
});
|
|
|
|
const err: HttpError = Object.assign(new Error('HTTP Error'), { status: res.status, data });
|
|
throw err;
|
|
}
|
|
|
|
// Log successful response
|
|
logger.apiResponse(res.status, method, url, data, duration);
|
|
|
|
return data as T;
|
|
}
|
|
|
|
export const http = {
|
|
get: <T>(path: string, options: { headers?: Record<string, string>; signal?: AbortSignal } = {}) =>
|
|
request<T>(path, { method: 'GET', ...options }),
|
|
post: <T>(
|
|
path: string,
|
|
body?: any,
|
|
options: { headers?: Record<string, string>; contentType?: 'json' | 'form' | 'none'; signal?: AbortSignal } = {}
|
|
) => request<T>(path, { method: 'POST', body, ...options }),
|
|
put: <T>(path: string, body?: any, options: { headers?: Record<string, string>; signal?: AbortSignal } = {}) =>
|
|
request<T>(path, { method: 'PUT', body, ...options }),
|
|
patch: <T>(path: string, body?: any, options: { headers?: Record<string, string>; signal?: AbortSignal } = {}) =>
|
|
request<T>(path, { method: 'PATCH', body, ...options }),
|
|
delete: <T>(path: string, options: { headers?: Record<string, string>; signal?: AbortSignal } = {}) =>
|
|
request<T>(path, { method: 'DELETE', ...options }),
|
|
};
|