From 3de9c0b40809fd32af1fa193a4468e0c604dc144 Mon Sep 17 00:00:00 2001 From: waterWang <672684719@qq.com> Date: Sat, 22 Aug 2026 22:23:05 +0800 Subject: [PATCH 1/2] feat: add route-level code splitting with React.lazy and Suspense - Convert all 20 page imports to React.lazy() dynamic imports - Add Suspense wrapper with loading spinner (Loader2 from lucide-react) - Keep layout/utility components (EmployerLayout, ErrorBoundary, ErrorFallback) as regular imports since they are not route pages - Each page chunk loads on demand, reducing initial bundle size Closes #469 --- frontend/src/App.tsx | 429 ++++++++++++++++++++++--------------------- 1 file changed, 223 insertions(+), 206 deletions(-) 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 From 689550bd8c33d445f766d955f80707f7dbea84bd Mon Sep 17 00:00:00 2001 From: waterWang <672684719@qq.com> Date: Sat, 22 Aug 2026 22:24:56 +0800 Subject: [PATCH 2/2] feat: handle 401 responses globally with axios interceptor - Add axios response interceptor for 401 status - Clear auth token from localStorage on 401 - Show toast notification via sonner: 'Session expired, please log in again' - Redirect to /login with guard to prevent duplicate redirects - Uses existing sonner dependency already in the project Closes #473 --- frontend/src/utils/api.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) 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