feat: Complete Pennyzo frontend with live waitlist tracking
- 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
This commit is contained in:
95
src/lib/http.ts
Normal file
95
src/lib/http.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
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 }),
|
||||
};
|
||||
Reference in New Issue
Block a user