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:
Hardik Sankhla
2025-09-26 04:25:47 +05:30
commit b7247f21a3
58 changed files with 9359 additions and 0 deletions

41
src/lib/api/auth.ts Normal file
View 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 }),
};

View 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
View 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
View 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
View 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
View 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();