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:
73
src/context/AuthContext.tsx
Normal file
73
src/context/AuthContext.tsx
Normal 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;
|
||||
};
|
||||
115
src/context/WaitlistContext.tsx
Normal file
115
src/context/WaitlistContext.tsx
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user