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

View File

@@ -0,0 +1,424 @@
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
import { API_CONFIG } from '../config';
import { logger } from '../lib/logger';
import type {
HealthResponse,
RegistrationHealthResponse,
RegisterRequest,
RegisterResponse,
LoginResponse,
VerifyEmailResponse,
ResendOtpResponse,
ForgotPasswordResponse,
PasswordResponse,
UserMeResponse,
UserProfile,
UpdateUserProfileRequest,
UpdateCompanyRequest,
UpdateGstRegistrationRequest,
UpgradeMembershipRequest,
MembershipInfo,
UserStats,
PennyzoApiError,
ErrorSchema
} from '../types/api/pennyzo-api-types';
/**
* PennyzoApiService - A comprehensive TypeScript class for handling all API interactions
* with the Pennyzo General API
*/
export class PennyzoApiService {
private apiClient: AxiosInstance;
private accessToken: string | null = null;
private readonly TOKEN_STORAGE_KEY = 'pennyzo_access_token';
private readonly BASE_URL = API_CONFIG.BASE_URL;
constructor() {
console.log('🔧 PennyzoApiService: Initializing API service...');
// Create Axios instance with base configuration
this.apiClient = axios.create({
baseURL: this.BASE_URL,
timeout: API_CONFIG.TIMEOUT,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
// Load token from storage if available
this.loadTokenFromStorage();
// Setup request interceptor for authentication
this.setupRequestInterceptor();
// Setup response interceptor for error handling
this.setupResponseInterceptor();
console.log('✅ PennyzoApiService: API service initialized successfully');
}
/**
* Sets up the request interceptor to attach the auth token to protected API calls
*/
private setupRequestInterceptor(): void {
this.apiClient.interceptors.request.use((config) => {
// Add start time for duration calculation
(config as any).startTime = Date.now();
// Log outgoing request
logger.apiRequest(
config.method?.toUpperCase() || 'GET',
`${config.baseURL}${config.url}`,
{
headers: { ...config.headers, Authorization: config.headers.Authorization ? '[REDACTED]' : undefined },
data: config.data
}
);
// If we have a token, add it to the request header
if (this.accessToken) {
config.headers.Authorization = `Bearer ${this.accessToken}`;
}
return config;
});
}
/**
* Sets up the response interceptor to handle errors consistently
*/
private setupResponseInterceptor(): void {
this.apiClient.interceptors.response.use(
(response) => {
// Log successful response
logger.apiResponse(
response.status,
response.config.method?.toUpperCase() || 'GET',
response.config.url || '',
response.data,
Date.now() - (response.config as any).startTime
);
return response;
},
(error: AxiosError<ErrorSchema>) => {
// Log error response
logger.apiError(
error.response?.status || 'NETWORK',
error.config?.method?.toUpperCase() || 'GET',
error.config?.url || '',
{
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
message: error.message
}
);
const apiError = this.createApiError(error);
// Handle 401 Unauthorized - Clear token and trigger authentication flow
if (error.response?.status === 401) {
this.clearToken();
}
return Promise.reject(apiError);
}
);
}
/**
* Creates a standardized error object from API errors
*/
private createApiError(error: AxiosError<ErrorSchema>): PennyzoApiError {
const apiError: PennyzoApiError = new Error(
error.response?.data?.error?.message || error.message
);
apiError.name = 'PennyzoApiError';
apiError.status = error.response?.status;
apiError.code = error.response?.data?.error?.type;
apiError.details = error.response?.data?.error?.details;
apiError.originalError = error;
return apiError;
}
/**
* Loads the authentication token from local storage
*/
private loadTokenFromStorage(): void {
if (typeof window !== 'undefined') {
const token = localStorage.getItem(this.TOKEN_STORAGE_KEY);
if (token) {
this.accessToken = token;
}
}
}
/**
* Stores the authentication token in local storage
*/
private storeToken(token: string): void {
if (typeof window !== 'undefined') {
localStorage.setItem(this.TOKEN_STORAGE_KEY, token);
this.accessToken = token;
}
}
/**
* Clears the authentication token from local storage and memory
*/
private clearToken(): void {
if (typeof window !== 'undefined') {
localStorage.removeItem(this.TOKEN_STORAGE_KEY);
}
this.accessToken = null;
}
// ======== SYSTEM & REGISTRATION METHODS (PUBLIC) ========
/**
* Check the health status of the API
* GET /system/health
*/
async checkHealth(): Promise<HealthResponse> {
const response = await this.apiClient.get<HealthResponse>('/system/health');
return response.data;
}
/**
* Check the health status of the registration service
* GET /registration/health
*/
async checkRegistrationHealth(): Promise<RegistrationHealthResponse> {
const response = await this.apiClient.get<RegistrationHealthResponse>('/registration/health');
return response.data;
}
/**
* Register a new user
* POST /registration/register
*/
async registerUser(registrationData: RegisterRequest): Promise<RegisterResponse> {
const response = await this.apiClient.post<RegisterResponse>(
'/registration/register',
registrationData
);
return response.data;
}
// ======== AUTHENTICATION & VERIFICATION METHODS (PUBLIC) ========
/**
* Login with email and password
* POST /auth/login
* Content-Type: application/x-www-form-urlencoded
*/
async login(email: string, password: string): Promise<LoginResponse> {
const formData = new URLSearchParams();
formData.append('username', email); // API uses username field for email
formData.append('password', password);
const config: AxiosRequestConfig = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
};
const response = await this.apiClient.post<LoginResponse>(
'/auth/login',
formData,
config
);
// Store the token
this.storeToken(response.data.access_token);
return response.data;
}
/**
* Verify user's email with OTP
* POST /auth/verify-email
*/
async verifyEmail(email: string, otp_code: string): Promise<VerifyEmailResponse> {
const response = await this.apiClient.post<VerifyEmailResponse>(
'/auth/verify-email',
{ email, otp_code }
);
return response.data;
}
/**
* Resend verification OTP to email
* POST /auth/resend-otp
*/
async resendVerificationOtp(email: string): Promise<ResendOtpResponse> {
const response = await this.apiClient.post<ResendOtpResponse>(
'/auth/resend-otp',
{ email }
);
return response.data;
}
// ======== PASSWORD MANAGEMENT METHODS (PUBLIC/AUTHENTICATED) ========
/**
* Request a password reset (forgot password)
* POST /auth/forgot-password
*/
async forgotPassword(email: string): Promise<ForgotPasswordResponse> {
const response = await this.apiClient.post<ForgotPasswordResponse>(
'/auth/forgot-password',
{ email }
);
return response.data;
}
/**
* Reset password using OTP
* POST /auth/reset-password
*/
async resetPasswordWithOtp(
email: string,
otp_code: string,
new_password: string
): Promise<PasswordResponse> {
const response = await this.apiClient.post<PasswordResponse>(
'/auth/reset-password',
{ email, otp_code, new_password }
);
return response.data;
}
/**
* Change password (requires authentication)
* POST /auth/change-password
*/
async changePassword(
current_password: string,
new_password: string
): Promise<PasswordResponse> {
const response = await this.apiClient.post<PasswordResponse>(
'/auth/change-password',
{ current_password, new_password }
);
return response.data;
}
// ======== USER & PROFILE MANAGEMENT METHODS (AUTHENTICATED) ========
/**
* Get current user information
* GET /auth/me
*/
async getCurrentUser(): Promise<UserMeResponse> {
const response = await this.apiClient.get<UserMeResponse>('/auth/me');
return response.data;
}
/**
* Get user profile details
* GET /users/profile
*/
async getUserProfile(): Promise<UserProfile> {
const response = await this.apiClient.get<UserProfile>('/users/profile');
return response.data;
}
/**
* Update user profile information
* PUT /users/profile
*/
async updateUserProfile(profileUpdateData: UpdateUserProfileRequest): Promise<UserProfile> {
const response = await this.apiClient.put<UserProfile>(
'/users/profile',
profileUpdateData
);
return response.data;
}
/**
* Update company information
* PUT /users/company
*/
async updateCompanyInfo(companyUpdateData: UpdateCompanyRequest): Promise<UserProfile> {
const response = await this.apiClient.put<UserProfile>(
'/users/company',
companyUpdateData
);
return response.data;
}
/**
* Update GST registration information
* PUT /users/gst-registration
*/
async updateGstRegistration(gstUpdateData: UpdateGstRegistrationRequest): Promise<UserProfile> {
const response = await this.apiClient.put<UserProfile>(
'/users/gst-registration',
gstUpdateData
);
return response.data;
}
// ======== MEMBERSHIP & STATS METHODS (AUTHENTICATED) ========
/**
* Upgrade user membership
* POST /users/upgrade-membership
*/
async upgradeMembership(membershipData: UpgradeMembershipRequest): Promise<UserProfile> {
const response = await this.apiClient.post<UserProfile>(
'/users/upgrade-membership',
membershipData
);
return response.data;
}
/**
* Get membership information
* GET /users/membership-info
*/
async getMembershipInfo(): Promise<MembershipInfo> {
const response = await this.apiClient.get<MembershipInfo>('/users/membership-info');
return response.data;
}
/**
* Get user statistics
* GET /users/stats
*/
async getUserStats(): Promise<UserStats> {
const response = await this.apiClient.get<UserStats>('/users/stats');
return response.data;
}
// ======== SESSION MANAGEMENT ========
/**
* Logout user by clearing the access token
*/
logout(): void {
this.clearToken();
}
/**
* Check if the user is currently authenticated
*/
isAuthenticated(): boolean {
return !!this.accessToken;
}
/**
* Get the current access token (if any)
*/
getAccessToken(): string | null {
return this.accessToken;
}
}
// Create a singleton instance for easy import and usage
export const pennyzoApi = new PennyzoApiService();
// Default export for convenient import
export default PennyzoApiService;

187
src/services/README.md Normal file
View File

@@ -0,0 +1,187 @@
# Pennyzo API Service
A comprehensive TypeScript class for handling all API interactions with the Pennyzo General API.
## Features
- Complete JWT authentication lifecycle management
- Automatic token handling for authenticated requests using Axios interceptors
- Comprehensive error handling
- TypeScript interfaces for request bodies and response payloads
- Environment-aware API URL configuration
- Singleton pattern for easy usage throughout the application
## Installation
The service is already part of the Pennyzo Web project. It requires Axios, which is installed as a project dependency.
## Configuration
The service automatically detects the environment and uses the appropriate API URL:
- **Development**: `http://localhost:8000` (when `import.meta.env.DEV` is true)
- **Production**: `https://api.pennyzo.com/api/v1/general` (when `import.meta.env.PROD` is true)
You can override the default by setting the `VITE_API_BASE_URL` environment variable in your `.env` file.
## API Methods
### System & Registration (Public)
```typescript
// Check health status of the API
const healthStatus = await pennyzoApi.checkHealth();
// Register a new user
const registeredUser = await pennyzoApi.registerUser({
first_name: "John",
last_name: "Doe",
email: "john.doe@example.com",
// other required fields...
});
```
### Authentication & Verification (Public)
```typescript
// Login a user
const loginResult = await pennyzoApi.login("user@example.com", "password123");
// Verify email with OTP
const verificationResult = await pennyzoApi.verifyEmail(
"user@example.com",
"123456"
);
// Resend verification OTP
const resendResult = await pennyzoApi.resendVerificationOtp("user@example.com");
```
### Password Management (Public/Authenticated)
```typescript
// Request password reset
const forgotPasswordResult = await pennyzoApi.forgotPassword(
"user@example.com"
);
// Reset password with OTP
const resetResult = await pennyzoApi.resetPasswordWithOtp(
"user@example.com",
"123456",
"newPassword123"
);
// Change password (authenticated)
const changePasswordResult = await pennyzoApi.changePassword(
"currentPassword123",
"newPassword123"
);
```
### User & Profile Management (Authenticated)
```typescript
// Get current user information
const currentUser = await pennyzoApi.getCurrentUser();
// Get user profile
const userProfile = await pennyzoApi.getUserProfile();
// Update user profile
const updatedProfile = await pennyzoApi.updateUserProfile({
first_name: "Updated",
last_name: "Name",
// other fields to update
});
// Update company information
const updatedCompany = await pennyzoApi.updateCompanyInfo({
company_name: "New Company Name",
industry: "Technology",
});
// Update GST registration
const updatedGst = await pennyzoApi.updateGstRegistration({
gst_registered: true,
gst_number: "GSTIN12345678",
});
```
### Membership & Stats (Authenticated)
```typescript
// Upgrade membership
const upgradedMembership = await pennyzoApi.upgradeMembership({
plan_type: "ELITE",
membership_type: "PAID",
billing_cycle: "yearly",
});
// Get membership information
const membershipInfo = await pennyzoApi.getMembershipInfo();
// Get user statistics
const userStats = await pennyzoApi.getUserStats();
```
### Session Management
```typescript
// Logout user (clears stored token)
pennyzoApi.logout();
// Check if user is authenticated
const isAuthenticated = pennyzoApi.isAuthenticated();
// Get the current access token (if any)
const accessToken = pennyzoApi.getAccessToken();
```
## Error Handling
The service handles errors gracefully and provides meaningful error objects with details:
```typescript
try {
const profile = await pennyzoApi.getUserProfile();
} catch (error) {
console.error("Error status:", error.status);
console.error("Error message:", error.message);
console.error("Error code:", error.code);
console.error("Error details:", error.details);
}
```
## Implementation Details
- Uses Axios for all HTTP requests
- Base URL is set to `https://api.pennyzo.com/api/v1/general`
- Automatically attaches Bearer token to authenticated requests
- Handles token storage in localStorage
- Special handling for form-urlencoded content type (login endpoint)
- Automatic 401 error handling to clear invalid tokens
## Usage Example
```typescript
// Import the singleton instance
import { pennyzoApi } from "../services";
// Use in async/await context
async function fetchUserData() {
try {
// First authenticate
await pennyzoApi.login("user@example.com", "password");
// Then make authenticated calls
const profile = await pennyzoApi.getUserProfile();
const stats = await pennyzoApi.getUserStats();
return { profile, stats };
} catch (error) {
console.error("API Error:", error.message);
throw error;
}
}
```

2
src/services/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { PennyzoApiService, pennyzoApi } from './PennyzoApiService';
export { default } from './PennyzoApiService';