diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 35d47f8f..0ed2f325 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,30 +1,45 @@ -import { Routes, Route, Navigate } from 'react-router-dom'; +import { lazy, Suspense } from 'react'; +import { Routes, Route } from 'react-router-dom'; import { useEffect } from 'react'; -import Home from './pages/Home'; -import Debugger from './pages/Debugger'; -import PayrollScheduler from './pages/PayrollScheduler'; -import EmployeeEntry from './pages/EmployeeEntry'; +import { Loader2 } from 'lucide-react'; import EmployerLayout from './components/EmployerLayout'; -import HelpCenter from './pages/HelpCenter'; import ErrorBoundary from './components/ErrorBoundary'; import ErrorFallback from './components/ErrorFallback'; -import Settings from './pages/Settings'; -import WebhookSettings from './pages/WebhookSettings'; -import CustomReportBuilder from './pages/CustomReportBuilder'; -import CrossAssetPayment from './pages/CrossAssetPayment'; -import TransactionHistory from './pages/TransactionHistory'; -import AdminPanel from './pages/AdminPanel'; -import VestingEscrow from './pages/VestingEscrow'; -import RevenueSplitDashboard from './pages/RevenueSplitDashboard'; -import Forecasting from './pages/Forecasting'; -import TaxComplianceWizard from './pages/TaxComplianceWizard'; - -import EmployeePortal from './pages/EmployeePortal'; -import Login from './pages/Login'; -import AuthCallback from './pages/AuthCallback'; import { useTranslation } from 'react-i18next'; import { contractService } from './services/contracts'; +// Lazy-loaded page components for route-level code splitting +const Home = lazy(() => import('./pages/Home')); +const Debugger = lazy(() => import('./pages/Debugger')); +const PayrollScheduler = lazy(() => import('./pages/PayrollScheduler')); +const EmployeeEntry = lazy(() => import('./pages/EmployeeEntry')); +const HelpCenter = lazy(() => import('./pages/HelpCenter')); +const Settings = lazy(() => import('./pages/Settings')); +const WebhookSettings = lazy(() => import('./pages/WebhookSettings')); +const CustomReportBuilder = lazy(() => import('./pages/CustomReportBuilder')); +const CrossAssetPayment = lazy(() => import('./pages/CrossAssetPayment')); +const TransactionHistory = lazy(() => import('./pages/TransactionHistory')); +const AdminPanel = lazy(() => import('./pages/AdminPanel')); +const VestingEscrow = lazy(() => import('./pages/VestingEscrow')); +const RevenueSplitDashboard = lazy(() => import('./pages/RevenueSplitDashboard')); +const Forecasting = lazy(() => import('./pages/Forecasting')); +const TaxComplianceWizard = lazy(() => import('./pages/TaxComplianceWizard')); +const EmployeePortal = lazy(() => import('./pages/EmployeePortal')); +const Login = lazy(() => import('./pages/Login')); +const AuthCallback = lazy(() => import('./pages/AuthCallback')); +const NotFound = lazy(() => import('./pages/NotFound')); + +function PageLoader() { + return ( +
+
+ +

Loading…

+
+
+ ); +} + function App() { const { t } = useTranslation(); @@ -36,192 +51,194 @@ function App() { }, []); return ( - - }> - - } - > - - - } - /> - - } - > - - - } - /> - - } - > - - - } - /> - - } - > - - - } - /> - }> - - - } - /> - - } - > - - - } - /> - - } - > - - - } - /> - }> - - - } - /> - {}} />}> - - - } - /> - {}} />}> - - - } - /> - {}} />}> - - - } - /> - {}} />}> - - - } - /> - {}} />}> - - - } - /> - {}} />}> - - - } - /> - {}} />}> - - - } - /> - {}} />}> - - - } - /> - {}} />}> - - - } - /> - - } /> - } /> - } /> - + }> + + }> + + } + > + + + } + /> + + } + > + + + } + /> + + } + > + + + } + /> + + } + > + + + } + /> + }> + + + } + /> + + } + > + + + } + /> + + } + > + + + } + /> + }> + + + } + /> + {}} />}> + + + } + /> + {}} />}> + + + } + /> + {}} />}> + + + } + /> + {}} />}> + + + } + /> + {}} />}> + + + } + /> + {}} />}> + + + } + /> + {}} />}> + + + } + /> + {}} />}> + + + } + /> + {}} />}> + + + } + /> + + } /> + } /> + } /> + + ); } -export default App; +export default App; \ No newline at end of file diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts index e13771e3..7ab3eb1f 100644 --- a/frontend/src/utils/api.ts +++ b/frontend/src/utils/api.ts @@ -1,4 +1,6 @@ import axios from 'axios'; +import type { AxiosError, InternalAxiosRequestConfig } from 'axios'; +import { toast } from 'sonner'; const api = axios.create({ baseURL: import.meta.env.VITE_API_URL || 'http://localhost:4000/api', @@ -7,9 +9,12 @@ const api = axios.create({ }, }); +// Guard against duplicate 401 redirects +let isRedirecting = false; + // Add a request interceptor to include the auth token api.interceptors.request.use( - (config) => { + (config: InternalAxiosRequestConfig) => { const token = localStorage.getItem('payd_auth_token'); if (token) { config.headers.Authorization = `Bearer ${token}`; @@ -21,6 +26,23 @@ api.interceptors.request.use( } ); +// Add a response interceptor to handle 401 errors globally +api.interceptors.response.use( + (response) => response, + (error: AxiosError) => { + if (error.response?.status === 401 && !isRedirecting) { + isRedirecting = true; + localStorage.removeItem('payd_auth_token'); + toast.error('Session expired, please log in again'); + // Short delay to let the toast render before redirecting + setTimeout(() => { + window.location.href = '/login'; + }, 500); + } + return Promise.reject(error); + } +); + export default api; export interface ApiError extends Error { @@ -48,4 +70,4 @@ export function safeReject(reason: unknown): Promise { message = String(reason); } return Promise.reject(reason instanceof Error ? reason : new Error(message)); -} +} \ No newline at end of file