918 lines
36 KiB
TypeScript
918 lines
36 KiB
TypeScript
/**
|
|
* @ai-summary Main app component with routing and mobile navigation
|
|
*/
|
|
|
|
import React, { useState, useEffect, useTransition, useCallback, lazy } from 'react';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
|
import { useAuth0 } from '@auth0/auth0-react';
|
|
import { useIsAuthInitialized } from './core/auth/auth-gate';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { ThemeProvider } from './shared-minimal/theme/ThemeContext';
|
|
import { Layout } from './components/Layout';
|
|
import { UnitsProvider } from './core/units/UnitsContext';
|
|
|
|
// Lazy load route components for better initial bundle size
|
|
const VehiclesPage = lazy(() => import('./features/vehicles/pages/VehiclesPage').then(m => ({ default: m.VehiclesPage })));
|
|
const VehicleDetailPage = lazy(() => import('./features/vehicles/pages/VehicleDetailPage').then(m => ({ default: m.VehicleDetailPage })));
|
|
const SettingsPage = lazy(() => import('./pages/SettingsPage').then(m => ({ default: m.SettingsPage })));
|
|
const SecuritySettingsPage = lazy(() => import('./pages/SecuritySettingsPage').then(m => ({ default: m.SecuritySettingsPage })));
|
|
const FuelLogsPage = lazy(() => import('./features/fuel-logs/pages/FuelLogsPage').then(m => ({ default: m.FuelLogsPage })));
|
|
const DocumentsPage = lazy(() => import('./features/documents/pages/DocumentsPage').then(m => ({ default: m.DocumentsPage })));
|
|
const DocumentDetailPage = lazy(() => import('./features/documents/pages/DocumentDetailPage').then(m => ({ default: m.DocumentDetailPage })));
|
|
const MaintenancePage = lazy(() => import('./features/maintenance/pages/MaintenancePage').then(m => ({ default: m.MaintenancePage })));
|
|
const StationsPage = lazy(() => import('./features/stations/pages/StationsPage').then(m => ({ default: m.StationsPage })));
|
|
const StationsMobileScreen = lazy(() => import('./features/stations/mobile/StationsMobileScreen').then(m => ({ default: m.default })));
|
|
const VehiclesMobileScreen = lazy(() => import('./features/vehicles/mobile/VehiclesMobileScreen').then(m => ({ default: m.VehiclesMobileScreen })));
|
|
const VehicleDetailMobile = lazy(() => import('./features/vehicles/mobile/VehicleDetailMobile').then(m => ({ default: m.VehicleDetailMobile })));
|
|
const DocumentsMobileScreen = lazy(() => import('./features/documents/mobile/DocumentsMobileScreen'));
|
|
|
|
// Admin pages (lazy-loaded)
|
|
const AdminUsersPage = lazy(() => import('./pages/admin/AdminUsersPage').then(m => ({ default: m.AdminUsersPage })));
|
|
const AdminCatalogPage = lazy(() => import('./pages/admin/AdminCatalogPage').then(m => ({ default: m.AdminCatalogPage })));
|
|
const AdminEmailTemplatesPage = lazy(() => import('./pages/admin/AdminEmailTemplatesPage').then(m => ({ default: m.AdminEmailTemplatesPage })));
|
|
const AdminBackupPage = lazy(() => import('./pages/admin/AdminBackupPage').then(m => ({ default: m.AdminBackupPage })));
|
|
|
|
// Admin mobile screens (lazy-loaded)
|
|
const AdminUsersMobileScreen = lazy(() => import('./features/admin/mobile/AdminUsersMobileScreen').then(m => ({ default: m.AdminUsersMobileScreen })));
|
|
const AdminCatalogMobileScreen = lazy(() => import('./features/admin/mobile/AdminCatalogMobileScreen').then(m => ({ default: m.AdminCatalogMobileScreen })));
|
|
const AdminEmailTemplatesMobileScreen = lazy(() => import('./features/admin/mobile/AdminEmailTemplatesMobileScreen'));
|
|
const AdminBackupMobileScreen = lazy(() => import('./features/admin/mobile/AdminBackupMobileScreen'));
|
|
|
|
// Admin Community Stations (lazy-loaded)
|
|
const AdminCommunityStationsPage = lazy(() => import('./features/admin/pages/AdminCommunityStationsPage').then(m => ({ default: m.AdminCommunityStationsPage })));
|
|
const AdminCommunityStationsMobileScreen = lazy(() => import('./features/admin/mobile/AdminCommunityStationsMobileScreen').then(m => ({ default: m.AdminCommunityStationsMobileScreen })));
|
|
|
|
// Auth pages (lazy-loaded)
|
|
const SignupPage = lazy(() => import('./features/auth/pages/SignupPage').then(m => ({ default: m.SignupPage })));
|
|
const VerifyEmailPage = lazy(() => import('./features/auth/pages/VerifyEmailPage').then(m => ({ default: m.VerifyEmailPage })));
|
|
const CallbackPage = lazy(() => import('./features/auth/pages/CallbackPage').then(m => ({ default: m.CallbackPage })));
|
|
const SignupMobileScreen = lazy(() => import('./features/auth/mobile/SignupMobileScreen').then(m => ({ default: m.SignupMobileScreen })));
|
|
const VerifyEmailMobileScreen = lazy(() => import('./features/auth/mobile/VerifyEmailMobileScreen').then(m => ({ default: m.VerifyEmailMobileScreen })));
|
|
const CallbackMobileScreen = lazy(() => import('./features/auth/mobile/CallbackMobileScreen').then(m => ({ default: m.CallbackMobileScreen })));
|
|
|
|
// Onboarding pages (lazy-loaded)
|
|
const OnboardingPage = lazy(() => import('./features/onboarding/pages/OnboardingPage').then(m => ({ default: m.OnboardingPage })));
|
|
const OnboardingMobileScreen = lazy(() => import('./features/onboarding/mobile/OnboardingMobileScreen').then(m => ({ default: m.OnboardingMobileScreen })));
|
|
|
|
import { HomePage } from './pages/HomePage';
|
|
import { BottomNavigation } from './shared-minimal/components/mobile/BottomNavigation';
|
|
import { QuickAction } from './shared-minimal/components/mobile/quickActions';
|
|
import { HamburgerDrawer } from './shared-minimal/components/mobile/HamburgerDrawer';
|
|
import { GlassCard } from './shared-minimal/components/mobile/GlassCard';
|
|
import { RouteSuspense } from './components/SuspenseWrappers';
|
|
import { Vehicle } from './features/vehicles/types/vehicles.types';
|
|
import { FuelLogForm } from './features/fuel-logs/components/FuelLogForm';
|
|
import { FuelLogsList } from './features/fuel-logs/components/FuelLogsList';
|
|
import { FuelLogEditDialog } from './features/fuel-logs/components/FuelLogEditDialog';
|
|
import { useFuelLogs } from './features/fuel-logs/hooks/useFuelLogs';
|
|
import { FuelLogResponse, UpdateFuelLogRequest } from './features/fuel-logs/types/fuel-logs.types';
|
|
import { fuelLogsApi } from './features/fuel-logs/api/fuel-logs.api';
|
|
import { VehicleForm } from './features/vehicles/components/VehicleForm';
|
|
import { useOptimisticVehicles } from './features/vehicles/hooks/useOptimisticVehicles';
|
|
import { CreateVehicleRequest } from './features/vehicles/types/vehicles.types';
|
|
import { MobileSettingsScreen } from './features/settings/mobile/MobileSettingsScreen';
|
|
import { SecurityMobileScreen } from './features/settings/mobile/SecurityMobileScreen';
|
|
import { useNavigationStore, useUserStore } from './core/store';
|
|
import { useDataSync } from './core/hooks/useDataSync';
|
|
import { MobileDebugPanel } from './core/debug/MobileDebugPanel';
|
|
import { MobileErrorBoundary } from './core/error-boundaries/MobileErrorBoundary';
|
|
import { useLoginNotifications } from './features/notifications/hooks/useLoginNotifications';
|
|
|
|
// Hoisted mobile screen components to stabilize identity and prevent remounts
|
|
const DashboardScreen: React.FC = () => (
|
|
<div className="space-y-4">
|
|
<GlassCard>
|
|
<div className="text-center py-12">
|
|
<h2 className="text-lg font-semibold text-slate-800 mb-2">Dashboard</h2>
|
|
<p className="text-slate-500">Coming soon - Vehicle insights and analytics</p>
|
|
</div>
|
|
</GlassCard>
|
|
</div>
|
|
);
|
|
|
|
const LogFuelScreen: React.FC = () => {
|
|
const queryClient = useQueryClient();
|
|
const [editingLog, setEditingLog] = useState<FuelLogResponse | null>(null);
|
|
const { goBack, canGoBack, navigateToScreen } = useNavigationStore();
|
|
useEffect(() => {
|
|
console.log('[LogFuelScreen] Mounted');
|
|
return () => console.log('[LogFuelScreen] Unmounted');
|
|
}, []);
|
|
|
|
let fuelLogs: FuelLogResponse[] | undefined, isLoading: boolean | undefined, error: any;
|
|
try {
|
|
const hookResult = useFuelLogs();
|
|
fuelLogs = hookResult.fuelLogs;
|
|
isLoading = hookResult.isLoading;
|
|
error = hookResult.error;
|
|
} catch (hookError) {
|
|
console.error('[LogFuelScreen] Hook error:', hookError);
|
|
error = hookError;
|
|
}
|
|
|
|
const handleEdit = (log: FuelLogResponse) => {
|
|
if (!log || !log.id) {
|
|
console.error('[LogFuelScreen] Invalid log data for edit:', log);
|
|
return;
|
|
}
|
|
try {
|
|
setEditingLog(log);
|
|
} catch (error) {
|
|
console.error('[LogFuelScreen] Error setting editing log:', error);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async (_logId: string) => {
|
|
try {
|
|
queryClient.invalidateQueries({ queryKey: ['fuelLogs'] });
|
|
queryClient.invalidateQueries({ queryKey: ['fuelLogsStats'] });
|
|
} catch (error) {
|
|
console.error('Failed to refresh fuel logs after delete:', error);
|
|
}
|
|
};
|
|
|
|
const handleSaveEdit = async (id: string, data: UpdateFuelLogRequest) => {
|
|
try {
|
|
await fuelLogsApi.update(id, data);
|
|
queryClient.invalidateQueries({ queryKey: ['fuelLogs'] });
|
|
queryClient.invalidateQueries({ queryKey: ['fuelLogsStats'] });
|
|
setEditingLog(null);
|
|
} catch (error) {
|
|
console.error('Failed to update fuel log:', error);
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const handleCloseEdit = () => setEditingLog(null);
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<GlassCard>
|
|
<div className="text-center py-8">
|
|
<p className="text-red-600 mb-4">Failed to load fuel logs</p>
|
|
<button
|
|
onClick={() => window.location.reload()}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg"
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
</GlassCard>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isLoading === undefined) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<GlassCard>
|
|
<div className="text-center py-8 text-slate-500">
|
|
Initializing fuel logs...
|
|
</div>
|
|
</GlassCard>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<MobileErrorBoundary screenName="FuelLogForm">
|
|
<FuelLogForm onSuccess={() => {
|
|
// Refresh dependent data
|
|
try {
|
|
queryClient.invalidateQueries({ queryKey: ['fuelLogs'] });
|
|
queryClient.invalidateQueries({ queryKey: ['fuelLogsStats'] });
|
|
} catch (error) {
|
|
// Silently ignore cache invalidation errors
|
|
}
|
|
// Navigate back if we have history; otherwise go to Vehicles
|
|
if (canGoBack()) {
|
|
goBack();
|
|
} else {
|
|
navigateToScreen('Vehicles', { source: 'fuel-log-added' });
|
|
}
|
|
}} />
|
|
</MobileErrorBoundary>
|
|
<MobileErrorBoundary screenName="FuelLogsSection">
|
|
<GlassCard>
|
|
<div className="py-2">
|
|
{isLoading ? (
|
|
<div className="text-center py-8 text-slate-500">
|
|
Loading fuel logs...
|
|
</div>
|
|
) : (
|
|
<FuelLogsList
|
|
logs={fuelLogs || []}
|
|
onEdit={handleEdit}
|
|
onDelete={handleDelete}
|
|
/>
|
|
)}
|
|
</div>
|
|
</GlassCard>
|
|
</MobileErrorBoundary>
|
|
|
|
<MobileErrorBoundary screenName="FuelLogEditDialog">
|
|
<FuelLogEditDialog
|
|
open={!!editingLog}
|
|
log={editingLog}
|
|
onClose={handleCloseEdit}
|
|
onSave={handleSaveEdit}
|
|
/>
|
|
</MobileErrorBoundary>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface AddVehicleScreenProps {
|
|
onBack: () => void;
|
|
onAdded: () => void;
|
|
}
|
|
|
|
const AddVehicleScreen: React.FC<AddVehicleScreenProps> = ({ onBack, onAdded }) => {
|
|
const { optimisticCreateVehicle } = useOptimisticVehicles([]);
|
|
|
|
const handleCreateVehicle = async (data: CreateVehicleRequest) => {
|
|
try {
|
|
await optimisticCreateVehicle(data);
|
|
onAdded();
|
|
} catch (error) {
|
|
console.error('Failed to create vehicle:', error);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<GlassCard>
|
|
<div className="p-4">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold text-slate-800">Add Vehicle</h2>
|
|
<button
|
|
onClick={onBack}
|
|
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
<VehicleForm
|
|
onSubmit={handleCreateVehicle}
|
|
onCancel={onBack}
|
|
/>
|
|
</div>
|
|
</GlassCard>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
function App() {
|
|
const { isLoading, isAuthenticated, user } = useAuth0();
|
|
const location = useLocation();
|
|
const isAuthGateReady = useIsAuthInitialized();
|
|
const [_isPending, startTransition] = useTransition();
|
|
console.log('[DEBUG App] Render check - isLoading:', isLoading, 'isAuthenticated:', isAuthenticated, 'isAuthGateReady:', isAuthGateReady);
|
|
|
|
// Initialize data synchronization
|
|
const { prefetchForNavigation } = useDataSync();
|
|
|
|
// Initialize login notifications
|
|
useLoginNotifications();
|
|
|
|
// Enhanced navigation and user state management
|
|
const {
|
|
activeScreen,
|
|
vehicleSubScreen,
|
|
navigateToScreen,
|
|
navigateToVehicleSubScreen,
|
|
goBack,
|
|
canGoBack,
|
|
} = useNavigationStore();
|
|
|
|
const { setUserProfile } = useUserStore();
|
|
|
|
// Mobile mode detection - detect mobile screen size with responsive updates
|
|
const [mobileMode, setMobileMode] = useState(() => {
|
|
if (typeof window !== 'undefined') {
|
|
return window.innerWidth <= 768;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
const [selectedVehicle, setSelectedVehicle] = useState<Vehicle | null>(null);
|
|
const [showAddVehicle, setShowAddVehicle] = useState(false);
|
|
|
|
// Update mobile mode on window resize
|
|
useEffect(() => {
|
|
const checkMobileMode = () => {
|
|
const isMobile = window.innerWidth <= 768 ||
|
|
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
console.log('Window width:', window.innerWidth, 'User agent mobile:', /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent), 'Mobile mode:', isMobile);
|
|
setMobileMode(isMobile);
|
|
};
|
|
|
|
// Check on mount
|
|
checkMobileMode();
|
|
|
|
window.addEventListener('resize', checkMobileMode);
|
|
return () => window.removeEventListener('resize', checkMobileMode);
|
|
}, []);
|
|
|
|
// Global error suppression for Google Maps DOM conflicts
|
|
useEffect(() => {
|
|
const handleGlobalError = (event: ErrorEvent) => {
|
|
const errorMsg = event.error?.message || event.message || '';
|
|
const isDomError =
|
|
errorMsg.includes('removeChild') ||
|
|
errorMsg.includes('insertBefore') ||
|
|
errorMsg.includes('replaceChild') ||
|
|
event.error?.name === 'NotFoundError' ||
|
|
(event.error instanceof DOMException);
|
|
|
|
if (isDomError) {
|
|
// Suppress Google Maps DOM manipulation errors
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
console.debug('[App] Suppressed harmless Google Maps DOM error');
|
|
}
|
|
};
|
|
|
|
const handleGlobalRejection = (event: PromiseRejectionEvent) => {
|
|
const errorMsg = event.reason?.message || String(event.reason) || '';
|
|
const isDomError =
|
|
errorMsg.includes('removeChild') ||
|
|
errorMsg.includes('insertBefore') ||
|
|
errorMsg.includes('replaceChild') ||
|
|
event.reason?.name === 'NotFoundError' ||
|
|
(event.reason instanceof DOMException);
|
|
|
|
if (isDomError) {
|
|
event.preventDefault();
|
|
console.debug('[App] Suppressed harmless Google Maps promise rejection');
|
|
}
|
|
};
|
|
|
|
window.addEventListener('error', handleGlobalError, true); // Use capture phase
|
|
window.addEventListener('unhandledrejection', handleGlobalRejection);
|
|
|
|
return () => {
|
|
window.removeEventListener('error', handleGlobalError, true);
|
|
window.removeEventListener('unhandledrejection', handleGlobalRejection);
|
|
};
|
|
}, []);
|
|
|
|
// Update user profile when authenticated
|
|
useEffect(() => {
|
|
if (isAuthenticated && user) {
|
|
setUserProfile(user);
|
|
}
|
|
}, [isAuthenticated, user, setUserProfile]);
|
|
|
|
// Handle mobile back button and navigation errors
|
|
useEffect(() => {
|
|
const handlePopState = (event: PopStateEvent) => {
|
|
event.preventDefault();
|
|
if (canGoBack() && mobileMode) {
|
|
goBack();
|
|
}
|
|
};
|
|
|
|
if (mobileMode) {
|
|
window.addEventListener('popstate', handlePopState);
|
|
return () => window.removeEventListener('popstate', handlePopState);
|
|
}
|
|
|
|
return undefined;
|
|
}, [goBack, canGoBack, mobileMode]);
|
|
|
|
// Menu state
|
|
const [hamburgerOpen, setHamburgerOpen] = useState(false);
|
|
|
|
// Quick action handler
|
|
const handleQuickAction = useCallback((action: QuickAction) => {
|
|
switch (action) {
|
|
case 'log-fuel':
|
|
navigateToScreen('Log Fuel', { source: 'quick-action' });
|
|
break;
|
|
case 'add-vehicle':
|
|
setShowAddVehicle(true);
|
|
navigateToScreen('Vehicles', { source: 'quick-action' });
|
|
navigateToVehicleSubScreen('add', undefined, { source: 'quick-action' });
|
|
break;
|
|
case 'add-document':
|
|
navigateToScreen('Documents', { source: 'quick-action' });
|
|
break;
|
|
case 'add-maintenance':
|
|
// Navigate to maintenance or open form (future implementation)
|
|
navigateToScreen('Vehicles', { source: 'quick-action' });
|
|
break;
|
|
}
|
|
}, [navigateToScreen, navigateToVehicleSubScreen]);
|
|
|
|
console.log('MotoVaultPro status:', { isLoading, isAuthenticated, mobileMode, activeScreen, vehicleSubScreen, userAgent: navigator.userAgent });
|
|
|
|
const isGarageRoute = location.pathname === '/garage' || location.pathname.startsWith('/garage/');
|
|
const isCallbackRoute = location.pathname === '/callback';
|
|
const isSignupRoute = location.pathname === '/signup';
|
|
const isVerifyEmailRoute = location.pathname === '/verify-email';
|
|
const isOnboardingRoute = location.pathname === '/onboarding';
|
|
const isAuthRoute = isSignupRoute || isVerifyEmailRoute || isOnboardingRoute;
|
|
const shouldShowHomePage = !isGarageRoute && !isCallbackRoute && !isAuthRoute;
|
|
|
|
// Enhanced navigation handlers for mobile
|
|
const handleVehicleSelect = useCallback((vehicle: Vehicle) => {
|
|
setSelectedVehicle(vehicle);
|
|
navigateToVehicleSubScreen('detail', vehicle.id, { source: 'vehicle-list' });
|
|
}, [navigateToVehicleSubScreen]);
|
|
|
|
const handleAddVehicle = useCallback(() => {
|
|
setShowAddVehicle(true);
|
|
navigateToVehicleSubScreen('add', undefined, { source: 'vehicle-list' });
|
|
}, [navigateToVehicleSubScreen]);
|
|
|
|
const handleBackToList = useCallback(() => {
|
|
setSelectedVehicle(null);
|
|
setShowAddVehicle(false);
|
|
navigateToVehicleSubScreen('list', undefined, { source: 'back-navigation' });
|
|
}, [navigateToVehicleSubScreen]);
|
|
|
|
const handleVehicleAdded = useCallback(() => {
|
|
setShowAddVehicle(false);
|
|
navigateToVehicleSubScreen('list', undefined, { source: 'vehicle-added' });
|
|
}, [navigateToVehicleSubScreen]);
|
|
|
|
// Enhanced debug component
|
|
const DebugInfo = () => (
|
|
<MobileDebugPanel visible={import.meta.env.MODE === 'development'} />
|
|
);
|
|
|
|
// Mobile settings now uses the dedicated MobileSettingsScreen component
|
|
const SettingsScreen = MobileSettingsScreen;
|
|
|
|
if (isLoading) {
|
|
if (mobileMode) {
|
|
return (
|
|
<ThemeProvider>
|
|
<Layout mobileMode={true}>
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-slate-500">Loading...</div>
|
|
</div>
|
|
</Layout>
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
return (
|
|
<ThemeProvider>
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="text-lg">Loading...</div>
|
|
</div>
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
// Callback route requires authentication - handled by CallbackPage component
|
|
if (isCallbackRoute && isAuthenticated) {
|
|
return (
|
|
<ThemeProvider>
|
|
<React.Suspense fallback={
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="text-lg">Processing login...</div>
|
|
</div>
|
|
}>
|
|
{mobileMode ? <CallbackMobileScreen /> : <CallbackPage />}
|
|
</React.Suspense>
|
|
<DebugInfo />
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
if (shouldShowHomePage) {
|
|
return (
|
|
<ThemeProvider>
|
|
<HomePage />
|
|
<DebugInfo />
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
// Signup route is public - no authentication required
|
|
if (isSignupRoute) {
|
|
return (
|
|
<ThemeProvider>
|
|
<React.Suspense fallback={
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="text-lg">Loading...</div>
|
|
</div>
|
|
}>
|
|
{mobileMode ? <SignupMobileScreen /> : <SignupPage />}
|
|
</React.Suspense>
|
|
<DebugInfo />
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
// Verify email is public - shown after signup before user can login
|
|
// (Auth0 blocks unverified users from logging in)
|
|
if (isVerifyEmailRoute) {
|
|
return (
|
|
<ThemeProvider>
|
|
<React.Suspense fallback={
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="text-lg">Loading...</div>
|
|
</div>
|
|
}>
|
|
{mobileMode ? <VerifyEmailMobileScreen /> : <VerifyEmailPage />}
|
|
</React.Suspense>
|
|
<DebugInfo />
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
if (!isAuthenticated) {
|
|
return <Navigate to="/" replace />;
|
|
}
|
|
|
|
if (isOnboardingRoute) {
|
|
return (
|
|
<ThemeProvider>
|
|
<React.Suspense fallback={
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="text-lg">Loading...</div>
|
|
</div>
|
|
}>
|
|
{mobileMode ? <OnboardingMobileScreen /> : <OnboardingPage />}
|
|
</React.Suspense>
|
|
<DebugInfo />
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
// Wait for auth gate to be ready before rendering protected routes
|
|
// This prevents a race condition where the page renders before the auth token is ready
|
|
if (!isAuthGateReady) {
|
|
console.log('[DEBUG App] Auth gate not ready yet, showing loading state');
|
|
if (mobileMode) {
|
|
return (
|
|
<ThemeProvider>
|
|
<Layout mobileMode={true}>
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-slate-500">Initializing session...</div>
|
|
</div>
|
|
</Layout>
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
return (
|
|
<ThemeProvider>
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="text-lg">Initializing session...</div>
|
|
</div>
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
// Mobile app rendering
|
|
if (mobileMode) {
|
|
return (
|
|
<ThemeProvider>
|
|
<UnitsProvider>
|
|
<Layout mobileMode={true}>
|
|
<AnimatePresence mode="popLayout" initial={false}>
|
|
{activeScreen === "Dashboard" && (
|
|
<motion.div
|
|
key="dashboard"
|
|
initial={{opacity:0, y:8}}
|
|
animate={{opacity:1, y:0}}
|
|
exit={{opacity:0, y:-8}}
|
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
>
|
|
<MobileErrorBoundary screenName="Dashboard">
|
|
<DashboardScreen />
|
|
</MobileErrorBoundary>
|
|
</motion.div>
|
|
)}
|
|
{activeScreen === "Vehicles" && (
|
|
<motion.div
|
|
key="vehicles"
|
|
initial={{opacity:0, y:8}}
|
|
animate={{opacity:1, y:0}}
|
|
exit={{opacity:0, y:-8}}
|
|
className="space-y-6"
|
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
>
|
|
<MobileErrorBoundary screenName="Vehicles">
|
|
{vehicleSubScreen === 'add' || showAddVehicle ? (
|
|
<AddVehicleScreen onBack={handleBackToList} onAdded={handleVehicleAdded} />
|
|
) : selectedVehicle && (vehicleSubScreen === 'detail') ? (
|
|
<VehicleDetailMobile
|
|
vehicle={selectedVehicle}
|
|
onBack={handleBackToList}
|
|
onLogFuel={() => navigateToScreen("Log Fuel")}
|
|
/>
|
|
) : (
|
|
<VehiclesMobileScreen
|
|
onVehicleSelect={handleVehicleSelect}
|
|
onAddVehicle={handleAddVehicle}
|
|
/>
|
|
)}
|
|
</MobileErrorBoundary>
|
|
</motion.div>
|
|
)}
|
|
{activeScreen === "Log Fuel" && (
|
|
<motion.div
|
|
key="logfuel"
|
|
initial={{opacity:0, y:8}}
|
|
animate={{opacity:1, y:0}}
|
|
exit={{opacity:0, y:-8}}
|
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
>
|
|
<MobileErrorBoundary screenName="Log Fuel">
|
|
<LogFuelScreen />
|
|
</MobileErrorBoundary>
|
|
</motion.div>
|
|
)}
|
|
{activeScreen === "Settings" && (
|
|
<motion.div
|
|
key="settings"
|
|
initial={{opacity:0, y:8}}
|
|
animate={{opacity:1, y:0}}
|
|
exit={{opacity:0, y:-8}}
|
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
>
|
|
<MobileErrorBoundary screenName="Settings">
|
|
<SettingsScreen />
|
|
</MobileErrorBoundary>
|
|
</motion.div>
|
|
)}
|
|
{activeScreen === "Security" && (
|
|
<motion.div
|
|
key="security"
|
|
initial={{opacity:0, y:8}}
|
|
animate={{opacity:1, y:0}}
|
|
exit={{opacity:0, y:-8}}
|
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
>
|
|
<MobileErrorBoundary screenName="Security">
|
|
<SecurityMobileScreen />
|
|
</MobileErrorBoundary>
|
|
</motion.div>
|
|
)}
|
|
{activeScreen === "Documents" && (
|
|
<motion.div
|
|
key="documents"
|
|
initial={{opacity:0, y:8}}
|
|
animate={{opacity:1, y:0}}
|
|
exit={{opacity:0, y:-8}}
|
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
>
|
|
<MobileErrorBoundary screenName="Documents">
|
|
<React.Suspense fallback={
|
|
<div className="space-y-4">
|
|
<GlassCard>
|
|
<div className="p-4">
|
|
<div className="text-slate-500 py-6 text-center">
|
|
{(() => {
|
|
console.log('[App] Documents Suspense fallback triggered');
|
|
return 'Loading documents screen...';
|
|
})()}
|
|
</div>
|
|
</div>
|
|
</GlassCard>
|
|
</div>
|
|
}>
|
|
<DocumentsMobileScreen />
|
|
</React.Suspense>
|
|
</MobileErrorBoundary>
|
|
</motion.div>
|
|
)}
|
|
{activeScreen === "Stations" && (
|
|
<motion.div
|
|
key="stations"
|
|
initial={{opacity:0, y:8}}
|
|
animate={{opacity:1, y:0}}
|
|
exit={{opacity:0, y:-8}}
|
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
>
|
|
<MobileErrorBoundary screenName="Stations">
|
|
<React.Suspense fallback={
|
|
<div className="space-y-4">
|
|
<GlassCard>
|
|
<div className="p-4">
|
|
<div className="text-slate-500 py-6 text-center">
|
|
Loading stations screen...
|
|
</div>
|
|
</div>
|
|
</GlassCard>
|
|
</div>
|
|
}>
|
|
<StationsMobileScreen />
|
|
</React.Suspense>
|
|
</MobileErrorBoundary>
|
|
</motion.div>
|
|
)}
|
|
{activeScreen === "AdminUsers" && (
|
|
<motion.div
|
|
key="admin-users"
|
|
initial={{opacity:0, y:8}}
|
|
animate={{opacity:1, y:0}}
|
|
exit={{opacity:0, y:-8}}
|
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
>
|
|
<MobileErrorBoundary screenName="AdminUsers">
|
|
<React.Suspense fallback={
|
|
<div className="space-y-4">
|
|
<GlassCard>
|
|
<div className="p-4">
|
|
<div className="text-slate-500 py-6 text-center">
|
|
Loading admin users...
|
|
</div>
|
|
</div>
|
|
</GlassCard>
|
|
</div>
|
|
}>
|
|
<AdminUsersMobileScreen />
|
|
</React.Suspense>
|
|
</MobileErrorBoundary>
|
|
</motion.div>
|
|
)}
|
|
{activeScreen === "AdminCatalog" && (
|
|
<motion.div
|
|
key="admin-catalog"
|
|
initial={{opacity:0, y:8}}
|
|
animate={{opacity:1, y:0}}
|
|
exit={{opacity:0, y:-8}}
|
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
>
|
|
<MobileErrorBoundary screenName="AdminCatalog">
|
|
<React.Suspense fallback={
|
|
<div className="space-y-4">
|
|
<GlassCard>
|
|
<div className="p-4">
|
|
<div className="text-slate-500 py-6 text-center">
|
|
Loading vehicle catalog...
|
|
</div>
|
|
</div>
|
|
</GlassCard>
|
|
</div>
|
|
}>
|
|
<AdminCatalogMobileScreen />
|
|
</React.Suspense>
|
|
</MobileErrorBoundary>
|
|
</motion.div>
|
|
)}
|
|
{activeScreen === "AdminCommunityStations" && (
|
|
<motion.div
|
|
key="admin-community-stations"
|
|
initial={{opacity:0, y:8}}
|
|
animate={{opacity:1, y:0}}
|
|
exit={{opacity:0, y:-8}}
|
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
>
|
|
<MobileErrorBoundary screenName="AdminCommunityStations">
|
|
<React.Suspense fallback={
|
|
<div className="space-y-4">
|
|
<GlassCard>
|
|
<div className="p-4">
|
|
<div className="text-slate-500 py-6 text-center">
|
|
Loading community station reviews...
|
|
</div>
|
|
</div>
|
|
</GlassCard>
|
|
</div>
|
|
}>
|
|
<AdminCommunityStationsMobileScreen />
|
|
</React.Suspense>
|
|
</MobileErrorBoundary>
|
|
</motion.div>
|
|
)}
|
|
{activeScreen === "AdminEmailTemplates" && (
|
|
<motion.div
|
|
key="admin-email-templates"
|
|
initial={{opacity:0, y:8}}
|
|
animate={{opacity:1, y:0}}
|
|
exit={{opacity:0, y:-8}}
|
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
>
|
|
<MobileErrorBoundary screenName="AdminEmailTemplates">
|
|
<React.Suspense fallback={
|
|
<div className="space-y-4">
|
|
<GlassCard>
|
|
<div className="p-4">
|
|
<div className="text-slate-500 py-6 text-center">
|
|
Loading email templates...
|
|
</div>
|
|
</div>
|
|
</GlassCard>
|
|
</div>
|
|
}>
|
|
<AdminEmailTemplatesMobileScreen />
|
|
</React.Suspense>
|
|
</MobileErrorBoundary>
|
|
</motion.div>
|
|
)}
|
|
{activeScreen === "AdminBackup" && (
|
|
<motion.div
|
|
key="admin-backup"
|
|
initial={{opacity:0, y:8}}
|
|
animate={{opacity:1, y:0}}
|
|
exit={{opacity:0, y:-8}}
|
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
>
|
|
<MobileErrorBoundary screenName="AdminBackup">
|
|
<React.Suspense fallback={
|
|
<div className="space-y-4">
|
|
<GlassCard>
|
|
<div className="p-4">
|
|
<div className="text-slate-500 py-6 text-center">
|
|
Loading backup management...
|
|
</div>
|
|
</div>
|
|
</GlassCard>
|
|
</div>
|
|
}>
|
|
<AdminBackupMobileScreen />
|
|
</React.Suspense>
|
|
</MobileErrorBoundary>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
<DebugInfo />
|
|
</Layout>
|
|
|
|
<BottomNavigation
|
|
activeScreen={activeScreen}
|
|
onNavigate={(screen) => startTransition(() => {
|
|
// Prefetch data for the target screen
|
|
prefetchForNavigation(screen);
|
|
|
|
// Reset states first, then navigate to prevent race conditions
|
|
if (screen !== 'Vehicles') {
|
|
setSelectedVehicle(null); // Reset selected vehicle when leaving Vehicles
|
|
}
|
|
if (screen !== 'Vehicles' || vehicleSubScreen !== 'add') {
|
|
setShowAddVehicle(false); // Reset add vehicle form when appropriate
|
|
}
|
|
|
|
// Navigate after state cleanup
|
|
navigateToScreen(screen, { source: 'bottom-navigation' });
|
|
})}
|
|
onQuickAction={handleQuickAction}
|
|
onHamburgerPress={() => setHamburgerOpen(true)}
|
|
/>
|
|
|
|
<HamburgerDrawer
|
|
open={hamburgerOpen}
|
|
onClose={() => setHamburgerOpen(false)}
|
|
onNavigate={(screen) => {
|
|
setHamburgerOpen(false);
|
|
startTransition(() => {
|
|
prefetchForNavigation(screen);
|
|
if (screen !== 'Vehicles') {
|
|
setSelectedVehicle(null);
|
|
}
|
|
if (screen !== 'Vehicles' || vehicleSubScreen !== 'add') {
|
|
setShowAddVehicle(false);
|
|
}
|
|
navigateToScreen(screen, { source: 'hamburger-menu' });
|
|
});
|
|
}}
|
|
activeScreen={activeScreen}
|
|
/>
|
|
</UnitsProvider>
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
// Desktop app rendering (fallback)
|
|
return (
|
|
<ThemeProvider>
|
|
<UnitsProvider>
|
|
<Layout mobileMode={false}>
|
|
<RouteSuspense>
|
|
<Routes>
|
|
<Route path="/garage" element={<Navigate to="/garage/vehicles" replace />} />
|
|
<Route path="/garage/vehicles" element={<VehiclesPage />} />
|
|
<Route path="/garage/vehicles/:id" element={<VehicleDetailPage />} />
|
|
<Route path="/garage/fuel-logs" element={<FuelLogsPage />} />
|
|
<Route path="/garage/documents" element={<DocumentsPage />} />
|
|
<Route path="/garage/documents/:id" element={<DocumentDetailPage />} />
|
|
<Route path="/garage/maintenance" element={<MaintenancePage />} />
|
|
<Route path="/garage/stations" element={<StationsPage />} />
|
|
<Route path="/garage/settings" element={<SettingsPage />} />
|
|
<Route path="/garage/settings/security" element={<SecuritySettingsPage />} />
|
|
<Route path="/garage/settings/admin/users" element={<AdminUsersPage />} />
|
|
<Route path="/garage/settings/admin/catalog" element={<AdminCatalogPage />} />
|
|
<Route path="/garage/settings/admin/community-stations" element={<AdminCommunityStationsPage />} />
|
|
<Route path="/garage/settings/admin/email-templates" element={<AdminEmailTemplatesPage />} />
|
|
<Route path="/garage/settings/admin/backup" element={<AdminBackupPage />} />
|
|
<Route path="*" element={<Navigate to="/garage/vehicles" replace />} />
|
|
</Routes>
|
|
</RouteSuspense>
|
|
<DebugInfo />
|
|
</Layout>
|
|
</UnitsProvider>
|
|
</ThemeProvider>
|
|
);
|
|
}
|
|
|
|
export default App;
|