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,73 @@
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { authApi } from '../lib/api/auth';
import { setAuthTokenGetter } from '../lib/http';
import type { UserMeResponse } from '../types/api';
type AuthState = {
token: string | null;
user: UserMeResponse | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => void;
refreshUser: () => Promise<void>;
};
const AuthContext = createContext<AuthState | undefined>(undefined);
const TOKEN_KEY = 'pennyzo_token';
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [token, setToken] = useState<string | null>(() => localStorage.getItem(TOKEN_KEY));
const [user, setUser] = useState<UserMeResponse | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(!!token);
useEffect(() => {
setAuthTokenGetter(() => token);
}, [token]);
const refreshUser = useCallback(async () => {
if (!token) return;
try {
const me = await authApi.me();
setUser(me);
} catch (e) {
// Token might be invalid
setToken(null);
localStorage.removeItem(TOKEN_KEY);
setUser(null);
} finally {
setIsLoading(false);
}
}, [token]);
useEffect(() => {
if (token) refreshUser();
}, [token, refreshUser]);
const login = useCallback(async (email: string, password: string) => {
const res = await authApi.login(email, password);
const access = res.access_token;
setToken(access);
localStorage.setItem(TOKEN_KEY, access);
await refreshUser();
}, [refreshUser]);
const logout = useCallback(() => {
setToken(null);
localStorage.removeItem(TOKEN_KEY);
setUser(null);
}, []);
const value = useMemo(
() => ({ token, user, isLoading, login, logout, refreshUser }),
[token, user, isLoading, login, logout, refreshUser]
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export const useAuth = () => {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
return ctx;
};

View File

@@ -0,0 +1,115 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { PennyzoApiService } from '../services/PennyzoApiService';
import { logger } from '../lib/logger';
interface WaitlistData {
remainingSpots: number;
totalSpots: number;
isLoading: boolean;
error: string | null;
}
interface WaitlistContextType extends WaitlistData {
refreshWaitlist: () => Promise<void>;
}
const WaitlistContext = createContext<WaitlistContextType | undefined>(undefined);
interface WaitlistProviderProps {
children: ReactNode;
}
export const WaitlistProvider: React.FC<WaitlistProviderProps> = ({ children }) => {
console.log('🎯 WAITLIST PROVIDER INITIALIZED - Starting API polling...');
logger.context('WaitlistProvider', 'Initializing with fallback values', {
remainingSpots: 892,
totalSpots: 1000
});
const [waitlistData, setWaitlistData] = useState<WaitlistData>({
remainingSpots: 892, // Initial fallback value
totalSpots: 1000,
isLoading: false,
error: null,
});
const apiService = new PennyzoApiService();
const fetchWaitlistData = async () => {
console.log('🔄 FETCHING WAITLIST DATA NOW...');
logger.context('WaitlistContext', 'Fetching waitlist data...');
setWaitlistData(prev => ({ ...prev, isLoading: true, error: null }));
try {
console.log('📡 Making API call to checkRegistrationHealth...');
const healthData = await apiService.checkRegistrationHealth();
console.log('📊 API CALL SUCCESSFUL! Data received:', healthData);
logger.context('WaitlistContext', 'Received waitlist data', {
remainingSpots: healthData.waitlist_available,
totalSpots: healthData.waitlist_limit,
service: healthData.service,
status: healthData.status
});
setWaitlistData({
remainingSpots: healthData.waitlist_available,
totalSpots: healthData.waitlist_limit,
isLoading: false,
error: null,
});
console.log('✅ STATE UPDATED: New waitlist data set:', {
remainingSpots: healthData.waitlist_available,
totalSpots: healthData.waitlist_limit
});
} catch (error) {
console.error('❌ API CALL FAILED! Error:', error);
logger.error('WaitlistContext: Failed to fetch waitlist data', error);
setWaitlistData(prev => ({
...prev,
isLoading: false,
error: 'Failed to load waitlist data',
}));
}
};
const refreshWaitlist = async () => {
await fetchWaitlistData();
};
useEffect(() => {
logger.context('WaitlistContext', 'Initializing waitlist polling...');
// Initial fetch
fetchWaitlistData();
// Set up polling every 60 seconds (1 minute) for live updates
const interval = setInterval(() => {
logger.context('WaitlistContext', 'Polling for updated waitlist data...');
fetchWaitlistData();
}, 60000);
return () => {
logger.context('WaitlistContext', 'Cleaning up polling interval');
clearInterval(interval);
};
}, []);
const value: WaitlistContextType = {
...waitlistData,
refreshWaitlist,
};
// Log final context value for debugging
console.log('📊 WaitlistContext: Final context value:', value);
return (
<WaitlistContext.Provider value={value}>
{children}
</WaitlistContext.Provider>
);
};
export const useWaitlist = () => {
const context = useContext(WaitlistContext);
if (context === undefined) {
throw new Error('useWaitlist must be used within a WaitlistProvider');
}
console.log('🔍 useWaitlist: Returning context data:', context);
return context;
};