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:
41
src/lib/api/auth.ts
Normal file
41
src/lib/api/auth.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { http } from '../http';
|
||||
import type {
|
||||
VerifyEmailRequest,
|
||||
VerifyEmailResponse,
|
||||
ResendOtpRequest,
|
||||
ResendOtpResponse,
|
||||
LoginResponse,
|
||||
UserMeResponse,
|
||||
} from '../../types/api';
|
||||
|
||||
export const authApi = {
|
||||
verifyEmail: (payload: VerifyEmailRequest) =>
|
||||
http.post<VerifyEmailResponse>('/auth/verify-email', payload),
|
||||
|
||||
resendOtp: (payload: ResendOtpRequest) =>
|
||||
http.post<ResendOtpResponse>('/auth/resend-otp', payload),
|
||||
|
||||
login: (username: string, password: string) =>
|
||||
http.post<LoginResponse>(
|
||||
'/auth/login',
|
||||
{ username, password },
|
||||
{ contentType: 'form' }
|
||||
),
|
||||
|
||||
me: () => http.get<UserMeResponse>('/auth/me'),
|
||||
|
||||
forgotPassword: (email: string) =>
|
||||
http.post<{ message: string; email: string; expires_in: number; next_step?: string }>(
|
||||
'/auth/forgot-password',
|
||||
{ email }
|
||||
),
|
||||
|
||||
resetPassword: (email: string, otp_code: string, new_password: string) =>
|
||||
http.post<{ message: string }>('/auth/reset-password', { email, otp_code, new_password }),
|
||||
|
||||
resetPasswordWithToken: (token: string, new_password: string) =>
|
||||
http.post<{ message: string }>('/auth/reset-password-token', { token, new_password }),
|
||||
|
||||
changePassword: (current_password: string, new_password: string) =>
|
||||
http.post<{ message: string }>('/auth/change-password', { current_password, new_password }),
|
||||
};
|
||||
10
src/lib/api/registration.ts
Normal file
10
src/lib/api/registration.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { http } from '../http';
|
||||
import type { RegisterRequest, RegisterResponse, RegistrationHealthResponse } from '../../types/api';
|
||||
|
||||
export const registrationApi = {
|
||||
register: (payload: RegisterRequest) =>
|
||||
http.post<RegisterResponse>('/registration/register', payload),
|
||||
|
||||
companyIdentityRules: () => http.get<any>('/registration/company/identity-rules'),
|
||||
health: () => http.get<RegistrationHealthResponse>('/registration/health'),
|
||||
};
|
||||
7
src/lib/api/system.ts
Normal file
7
src/lib/api/system.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { http } from '../http';
|
||||
import type { HealthResponse } from '../../types/api';
|
||||
|
||||
export const systemApi = {
|
||||
health: () => http.get<HealthResponse>('/system/health'),
|
||||
root: () => http.get<any>('/system/'),
|
||||
};
|
||||
20
src/lib/api/users.ts
Normal file
20
src/lib/api/users.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { http } from '../http';
|
||||
import type {
|
||||
UpdateCompanyRequest,
|
||||
UpdateGstRegistrationRequest,
|
||||
UpdateUserProfileRequest,
|
||||
UpgradeMembershipRequest,
|
||||
UserProfile,
|
||||
} from '../../types/api';
|
||||
|
||||
export const usersApi = {
|
||||
profile: () => http.get<UserProfile>('/users/profile'),
|
||||
updateProfile: (payload: UpdateUserProfileRequest) => http.put<UserProfile>('/users/profile', payload),
|
||||
|
||||
updateCompany: (payload: UpdateCompanyRequest) => http.put<UserProfile>('/users/company', payload),
|
||||
updateGst: (payload: UpdateGstRegistrationRequest) => http.put<UserProfile>('/users/gst-registration', payload),
|
||||
upgradeMembership: (payload: UpgradeMembershipRequest) => http.post<UserProfile>('/users/upgrade-membership', payload),
|
||||
|
||||
stats: () => http.get<any>('/users/stats'),
|
||||
membershipInfo: () => http.get<any>('/users/membership-info'),
|
||||
};
|
||||
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 }),
|
||||
};
|
||||
70
src/lib/logger.ts
Normal file
70
src/lib/logger.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
// Logging utility for consistent API and application logging
|
||||
type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
||||
|
||||
class Logger {
|
||||
private isDevelopment = import.meta.env.DEV;
|
||||
private logLevel: LogLevel = this.isDevelopment ? 'debug' : 'warn';
|
||||
|
||||
private shouldLog(level: LogLevel): boolean {
|
||||
const levels = ['debug', 'info', 'warn', 'error'];
|
||||
return levels.indexOf(level) >= levels.indexOf(this.logLevel);
|
||||
}
|
||||
|
||||
private formatMessage(level: LogLevel, message: string, data?: any): void {
|
||||
const timestamp = new Date().toISOString();
|
||||
const prefix = `[${timestamp}] ${level.toUpperCase()}:`;
|
||||
|
||||
if (data) {
|
||||
console[level](prefix, message, data);
|
||||
} else {
|
||||
console[level](prefix, message);
|
||||
}
|
||||
}
|
||||
|
||||
debug(message: string, data?: any): void {
|
||||
if (this.shouldLog('debug')) {
|
||||
this.formatMessage('debug', message, data);
|
||||
}
|
||||
}
|
||||
|
||||
info(message: string, data?: any): void {
|
||||
if (this.shouldLog('info')) {
|
||||
this.formatMessage('info', message, data);
|
||||
}
|
||||
}
|
||||
|
||||
warn(message: string, data?: any): void {
|
||||
if (this.shouldLog('warn')) {
|
||||
this.formatMessage('warn', message, data);
|
||||
}
|
||||
}
|
||||
|
||||
error(message: string, data?: any): void {
|
||||
if (this.shouldLog('error')) {
|
||||
this.formatMessage('error', message, data);
|
||||
}
|
||||
}
|
||||
|
||||
// API-specific logging methods
|
||||
apiRequest(method: string, url: string, data?: any): void {
|
||||
this.info(`🚀 API Request: ${method} ${url}`, data);
|
||||
}
|
||||
|
||||
apiResponse(status: number, method: string, url: string, data?: any, duration?: number): void {
|
||||
this.info(`✅ API Response: ${status} ${method} ${url}`, { data, duration });
|
||||
}
|
||||
|
||||
apiError(status: number | 'NETWORK', method: string, url: string, error?: any): void {
|
||||
this.error(`❌ API Error: ${status} ${method} ${url}`, error);
|
||||
}
|
||||
|
||||
component(name: string, action: string, data?: any): void {
|
||||
this.debug(`📱 ${name}: ${action}`, data);
|
||||
}
|
||||
|
||||
context(name: string, action: string, data?: any): void {
|
||||
this.debug(`🔄 ${name}: ${action}`, data);
|
||||
}
|
||||
}
|
||||
|
||||
export const logger = new Logger();
|
||||
Reference in New Issue
Block a user