feat(performance): enhance routing and component loading efficiency
- Implemented lazy loading for all route components to improve initial load times and reduce bundle size. - Added Suspense boundaries around lazy-loaded components for better loading states and user experience. - Memoized guard components to prevent unnecessary re-renders and optimize performance. - Introduced defensive programming practices in guard components to handle potential errors gracefully. - Updated routing structure to utilize React Router's future features for enhanced performance.
This commit is contained in:
@@ -5,7 +5,6 @@ import i18next from 'i18next';
|
||||
|
||||
// Components
|
||||
import ThemeWrapper from './features/theme/ThemeWrapper';
|
||||
import PreferenceSelector from './components/PreferenceSelector';
|
||||
|
||||
// Routes
|
||||
import router from './app/routes';
|
||||
@@ -14,7 +13,6 @@ import router from './app/routes';
|
||||
import { useAppSelector } from './hooks/useAppSelector';
|
||||
import { initMixpanel } from './utils/mixpanelInit';
|
||||
import { initializeCsrfToken } from './api/api-client';
|
||||
import { useRoutePreloader } from './utils/routePreloader';
|
||||
|
||||
// Types & Constants
|
||||
import { Language } from './features/i18n/localesSlice';
|
||||
@@ -28,9 +26,9 @@ import { SuspenseFallback } from './components/suspense-fallback/suspense-fallba
|
||||
* 1. React.memo() - Prevents unnecessary re-renders
|
||||
* 2. useMemo() - Memoizes expensive computations
|
||||
* 3. useCallback() - Memoizes event handlers
|
||||
* 4. Route preloading - Preloads critical routes
|
||||
* 5. Lazy loading - Components loaded on demand
|
||||
* 6. Suspense boundaries - Better loading states
|
||||
* 4. Lazy loading - All route components loaded on demand
|
||||
* 5. Suspense boundaries - Better loading states
|
||||
* 6. Optimized guard components with memoization
|
||||
*/
|
||||
const App: React.FC = memo(() => {
|
||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||
@@ -39,25 +37,6 @@ const App: React.FC = memo(() => {
|
||||
// Memoize mixpanel initialization to prevent re-initialization
|
||||
const mixpanelToken = useMemo(() => import.meta.env.VITE_MIXPANEL_TOKEN as string, []);
|
||||
|
||||
// Preload critical routes for better navigation performance
|
||||
useRoutePreloader([
|
||||
{
|
||||
path: '/worklenz/home',
|
||||
loader: () => import('./pages/home/home-page'),
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
path: '/worklenz/projects',
|
||||
loader: () => import('./pages/projects/project-list'),
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
path: '/worklenz/schedule',
|
||||
loader: () => import('./pages/schedule/schedule'),
|
||||
priority: 'medium'
|
||||
}
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
initMixpanel(mixpanelToken);
|
||||
}, [mixpanelToken]);
|
||||
@@ -95,7 +74,12 @@ const App: React.FC = memo(() => {
|
||||
return (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<ThemeWrapper>
|
||||
<RouterProvider router={router} future={{ v7_startTransition: true }} />
|
||||
<RouterProvider
|
||||
router={router}
|
||||
future={{
|
||||
v7_startTransition: true
|
||||
}}
|
||||
/>
|
||||
</ThemeWrapper>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
import AuthLayout from '@/layouts/AuthLayout';
|
||||
import LoginPage from '@/pages/auth/login-page';
|
||||
import SignupPage from '@/pages/auth/signup-page';
|
||||
import ForgotPasswordPage from '@/pages/auth/forgot-password-page';
|
||||
import LoggingOutPage from '@/pages/auth/logging-out';
|
||||
import AuthenticatingPage from '@/pages/auth/authenticating';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import VerifyResetEmailPage from '@/pages/auth/verify-reset-email';
|
||||
import { Suspense } from 'react';
|
||||
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
|
||||
|
||||
// Lazy load auth page components for better code splitting
|
||||
const LoginPage = lazy(() => import('@/pages/auth/login-page'));
|
||||
const SignupPage = lazy(() => import('@/pages/auth/signup-page'));
|
||||
const ForgotPasswordPage = lazy(() => import('@/pages/auth/forgot-password-page'));
|
||||
const LoggingOutPage = lazy(() => import('@/pages/auth/logging-out'));
|
||||
const AuthenticatingPage = lazy(() => import('@/pages/auth/authenticating'));
|
||||
const VerifyResetEmailPage = lazy(() => import('@/pages/auth/verify-reset-email'));
|
||||
|
||||
const authRoutes = [
|
||||
{
|
||||
path: '/auth',
|
||||
element: (
|
||||
<AuthLayout />
|
||||
),
|
||||
element: <AuthLayout />,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
@@ -22,27 +22,51 @@ const authRoutes = [
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
element: <LoginPage />,
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<LoginPage />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'signup',
|
||||
element: <SignupPage />,
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<SignupPage />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'forgot-password',
|
||||
element: <ForgotPasswordPage />,
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<ForgotPasswordPage />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'logging-out',
|
||||
element: <LoggingOutPage />,
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<LoggingOutPage />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'authenticating',
|
||||
element: <AuthenticatingPage />,
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<AuthenticatingPage />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'verify-reset-email/:user/:hash',
|
||||
element: <VerifyResetEmailPage />,
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<VerifyResetEmailPage />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createBrowserRouter, Navigate, RouteObject, useLocation } from 'react-router-dom';
|
||||
import { lazy, Suspense, memo, useMemo } from 'react';
|
||||
import rootRoutes from './root-routes';
|
||||
import authRoutes from './auth-routes';
|
||||
import mainRoutes, { licenseExpiredRoute } from './main-routes';
|
||||
@@ -8,116 +9,195 @@ import reportingRoutes from './reporting-routes';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { AuthenticatedLayout } from '@/layouts/AuthenticatedLayout';
|
||||
import ErrorBoundary from '@/components/ErrorBoundary';
|
||||
import NotFoundPage from '@/pages/404-page/404-page';
|
||||
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
|
||||
import { ISUBSCRIPTION_TYPE } from '@/shared/constants';
|
||||
import LicenseExpired from '@/pages/license-expired/license-expired';
|
||||
|
||||
// Lazy load the NotFoundPage component for better code splitting
|
||||
const NotFoundPage = lazy(() => import('@/pages/404-page/404-page'));
|
||||
|
||||
interface GuardProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const AuthGuard = ({ children }: GuardProps) => {
|
||||
const isAuthenticated = useAuthService().isAuthenticated();
|
||||
const location = useLocation();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/auth" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
// Route-based code splitting utility
|
||||
const withCodeSplitting = (Component: React.LazyExoticComponent<React.ComponentType<any>>) => {
|
||||
return memo(() => (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<Component />
|
||||
</Suspense>
|
||||
));
|
||||
};
|
||||
|
||||
export const AdminGuard = ({ children }: GuardProps) => {
|
||||
const isAuthenticated = useAuthService().isAuthenticated();
|
||||
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
const isFreePlan = currentSession?.subscription_type === ISUBSCRIPTION_TYPE.FREE;
|
||||
// Memoized guard components with defensive programming
|
||||
export const AuthGuard = memo(({ children }: GuardProps) => {
|
||||
const authService = useAuthService();
|
||||
const location = useLocation();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/auth" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
if (!isOwnerOrAdmin || isFreePlan) {
|
||||
return <Navigate to="/worklenz/unauthorized" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export const LicenseExpiryGuard = ({ children }: GuardProps) => {
|
||||
const isAuthenticated = useAuthService().isAuthenticated();
|
||||
const currentSession = useAuthService().getCurrentSession();
|
||||
const location = useLocation();
|
||||
const isAdminCenterRoute = location.pathname.includes('/worklenz/admin-center');
|
||||
const isLicenseExpiredRoute = location.pathname === '/worklenz/license-expired';
|
||||
|
||||
// Don't check or redirect if we're already on the license-expired page
|
||||
if (isLicenseExpiredRoute) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
// Check if trial is expired more than 7 days or if is_expired flag is set
|
||||
const isLicenseExpiredMoreThan7Days = () => {
|
||||
// Quick bail if no session data is available
|
||||
if (!currentSession) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check is_expired flag first
|
||||
if (currentSession.is_expired) {
|
||||
// If no trial_expire_date exists but is_expired is true, defer to backend check
|
||||
if (!currentSession.trial_expire_date) {
|
||||
return true;
|
||||
const shouldRedirect = useMemo(() => {
|
||||
try {
|
||||
// Defensive check to ensure authService and its methods exist
|
||||
if (!authService || typeof authService.isAuthenticated !== 'function') {
|
||||
return false; // Don't redirect if auth service is not ready
|
||||
}
|
||||
|
||||
// If there is a trial_expire_date, check if it's more than 7 days past
|
||||
const today = new Date();
|
||||
const expiryDate = new Date(currentSession.trial_expire_date);
|
||||
const diffTime = today.getTime() - expiryDate.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
// Redirect if more than 7 days past expiration
|
||||
return diffDays > 7;
|
||||
return !authService.isAuthenticated();
|
||||
} catch (error) {
|
||||
console.error('Error in AuthGuard:', error);
|
||||
return false; // Don't redirect on error, let the app handle it
|
||||
}
|
||||
|
||||
// If not marked as expired but has trial_expire_date, do a date check
|
||||
if (currentSession.subscription_type === ISUBSCRIPTION_TYPE.TRIAL && currentSession.trial_expire_date) {
|
||||
const today = new Date();
|
||||
const expiryDate = new Date(currentSession.trial_expire_date);
|
||||
}, [authService]);
|
||||
|
||||
const diffTime = today.getTime() - expiryDate.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
if (shouldRedirect) {
|
||||
return <Navigate to="/auth" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
});
|
||||
|
||||
AuthGuard.displayName = 'AuthGuard';
|
||||
|
||||
export const AdminGuard = memo(({ children }: GuardProps) => {
|
||||
const authService = useAuthService();
|
||||
const location = useLocation();
|
||||
|
||||
const guardResult = useMemo(() => {
|
||||
try {
|
||||
// Defensive checks to ensure authService and its methods exist
|
||||
if (!authService ||
|
||||
typeof authService.isAuthenticated !== 'function' ||
|
||||
typeof authService.isOwnerOrAdmin !== 'function' ||
|
||||
typeof authService.getCurrentSession !== 'function') {
|
||||
return null; // Don't redirect if auth service is not ready
|
||||
}
|
||||
|
||||
if (!authService.isAuthenticated()) {
|
||||
return { redirect: '/auth', state: { from: location } };
|
||||
}
|
||||
|
||||
const currentSession = authService.getCurrentSession();
|
||||
const isFreePlan = currentSession?.subscription_type === ISUBSCRIPTION_TYPE.FREE;
|
||||
|
||||
// If expired more than 7 days, redirect
|
||||
return diffDays > 7;
|
||||
}
|
||||
|
||||
// No expiration data found
|
||||
return false;
|
||||
};
|
||||
if (!authService.isOwnerOrAdmin() || isFreePlan) {
|
||||
return { redirect: '/worklenz/unauthorized' };
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error in AdminGuard:', error);
|
||||
return null; // Don't redirect on error
|
||||
}
|
||||
}, [authService, location]);
|
||||
|
||||
if (guardResult) {
|
||||
return <Navigate to={guardResult.redirect} state={guardResult.state} replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
});
|
||||
|
||||
AdminGuard.displayName = 'AdminGuard';
|
||||
|
||||
export const LicenseExpiryGuard = memo(({ children }: GuardProps) => {
|
||||
const authService = useAuthService();
|
||||
const location = useLocation();
|
||||
|
||||
const shouldRedirect = useMemo(() => {
|
||||
try {
|
||||
// Defensive checks to ensure authService and its methods exist
|
||||
if (!authService ||
|
||||
typeof authService.isAuthenticated !== 'function' ||
|
||||
typeof authService.getCurrentSession !== 'function') {
|
||||
return false; // Don't redirect if auth service is not ready
|
||||
}
|
||||
|
||||
if (!authService.isAuthenticated()) return false;
|
||||
|
||||
const isAdminCenterRoute = location.pathname.includes('/worklenz/admin-center');
|
||||
const isLicenseExpiredRoute = location.pathname === '/worklenz/license-expired';
|
||||
|
||||
// Don't check or redirect if we're already on the license-expired page
|
||||
if (isLicenseExpiredRoute) return false;
|
||||
|
||||
const currentSession = authService.getCurrentSession();
|
||||
|
||||
// Check if trial is expired more than 7 days or if is_expired flag is set
|
||||
const isLicenseExpiredMoreThan7Days = () => {
|
||||
// Quick bail if no session data is available
|
||||
if (!currentSession) return false;
|
||||
|
||||
// Check is_expired flag first
|
||||
if (currentSession.is_expired) {
|
||||
// If no trial_expire_date exists but is_expired is true, defer to backend check
|
||||
if (!currentSession.trial_expire_date) return true;
|
||||
|
||||
// If there is a trial_expire_date, check if it's more than 7 days past
|
||||
const today = new Date();
|
||||
const expiryDate = new Date(currentSession.trial_expire_date);
|
||||
const diffTime = today.getTime() - expiryDate.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
// Redirect if more than 7 days past expiration
|
||||
return diffDays > 7;
|
||||
}
|
||||
|
||||
// If not marked as expired but has trial_expire_date, do a date check
|
||||
if (currentSession.subscription_type === ISUBSCRIPTION_TYPE.TRIAL && currentSession.trial_expire_date) {
|
||||
const today = new Date();
|
||||
const expiryDate = new Date(currentSession.trial_expire_date);
|
||||
|
||||
const diffTime = today.getTime() - expiryDate.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
// If expired more than 7 days, redirect
|
||||
return diffDays > 7;
|
||||
}
|
||||
|
||||
// No expiration data found
|
||||
return false;
|
||||
};
|
||||
|
||||
return isLicenseExpiredMoreThan7Days() && !isAdminCenterRoute;
|
||||
} catch (error) {
|
||||
console.error('Error in LicenseExpiryGuard:', error);
|
||||
return false; // Don't redirect on error
|
||||
}
|
||||
}, [authService, location.pathname]);
|
||||
|
||||
// Add this explicit check and log the result
|
||||
const shouldRedirect = isAuthenticated && isLicenseExpiredMoreThan7Days() && !isAdminCenterRoute;
|
||||
if (shouldRedirect) {
|
||||
return <Navigate to="/worklenz/license-expired" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
});
|
||||
|
||||
export const SetupGuard = ({ children }: GuardProps) => {
|
||||
const isAuthenticated = useAuthService().isAuthenticated();
|
||||
LicenseExpiryGuard.displayName = 'LicenseExpiryGuard';
|
||||
|
||||
export const SetupGuard = memo(({ children }: GuardProps) => {
|
||||
const authService = useAuthService();
|
||||
const location = useLocation();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
const shouldRedirect = useMemo(() => {
|
||||
try {
|
||||
// Defensive check to ensure authService and its methods exist
|
||||
if (!authService || typeof authService.isAuthenticated !== 'function') {
|
||||
return false; // Don't redirect if auth service is not ready
|
||||
}
|
||||
return !authService.isAuthenticated();
|
||||
} catch (error) {
|
||||
console.error('Error in SetupGuard:', error);
|
||||
return false; // Don't redirect on error
|
||||
}
|
||||
}, [authService]);
|
||||
|
||||
if (shouldRedirect) {
|
||||
return <Navigate to="/auth" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
});
|
||||
|
||||
// Helper to wrap routes with guards
|
||||
SetupGuard.displayName = 'SetupGuard';
|
||||
|
||||
// Optimized route wrapping function with Suspense boundaries
|
||||
const wrapRoutes = (
|
||||
routes: RouteObject[],
|
||||
Guard: React.ComponentType<{ children: React.ReactNode }>
|
||||
@@ -125,7 +205,11 @@ const wrapRoutes = (
|
||||
return routes.map(route => {
|
||||
const wrappedRoute = {
|
||||
...route,
|
||||
element: <Guard>{route.element}</Guard>,
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<Guard>{route.element}</Guard>
|
||||
</Suspense>
|
||||
),
|
||||
};
|
||||
|
||||
if (route.children) {
|
||||
@@ -140,9 +224,8 @@ const wrapRoutes = (
|
||||
});
|
||||
};
|
||||
|
||||
// Static license expired component that doesn't rely on translations or authentication
|
||||
const StaticLicenseExpired = () => {
|
||||
|
||||
// Optimized static license expired component
|
||||
const StaticLicenseExpired = memo(() => {
|
||||
return (
|
||||
<div style={{
|
||||
marginTop: 65,
|
||||
@@ -184,23 +267,31 @@ const StaticLicenseExpired = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
StaticLicenseExpired.displayName = 'StaticLicenseExpired';
|
||||
|
||||
// Create route arrays (moved outside of useMemo to avoid hook violations)
|
||||
const publicRoutes = [
|
||||
...rootRoutes,
|
||||
...authRoutes,
|
||||
notFoundRoute
|
||||
];
|
||||
|
||||
const protectedMainRoutes = wrapRoutes(mainRoutes, AuthGuard);
|
||||
const adminRoutes = wrapRoutes(reportingRoutes, AdminGuard);
|
||||
const setupRoutes = wrapRoutes([accountSetupRoute], SetupGuard);
|
||||
|
||||
// Apply LicenseExpiryGuard to all protected routes
|
||||
// License expiry check function
|
||||
const withLicenseExpiryCheck = (routes: RouteObject[]): RouteObject[] => {
|
||||
return routes.map(route => {
|
||||
const wrappedRoute = {
|
||||
...route,
|
||||
element: <LicenseExpiryGuard>{route.element}</LicenseExpiryGuard>,
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<LicenseExpiryGuard>{route.element}</LicenseExpiryGuard>
|
||||
</Suspense>
|
||||
),
|
||||
};
|
||||
|
||||
if (route.children) {
|
||||
@@ -213,10 +304,21 @@ const withLicenseExpiryCheck = (routes: RouteObject[]): RouteObject[] => {
|
||||
|
||||
const licenseCheckedMainRoutes = withLicenseExpiryCheck(protectedMainRoutes);
|
||||
|
||||
// Create optimized router with future flags for better performance
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
element: <ErrorBoundary><AuthenticatedLayout /></ErrorBoundary>,
|
||||
errorElement: <ErrorBoundary><NotFoundPage /></ErrorBoundary>,
|
||||
element: (
|
||||
<ErrorBoundary>
|
||||
<AuthenticatedLayout />
|
||||
</ErrorBoundary>
|
||||
),
|
||||
errorElement: (
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<NotFoundPage />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
),
|
||||
children: [
|
||||
...licenseCheckedMainRoutes,
|
||||
...adminRoutes,
|
||||
@@ -225,6 +327,15 @@ const router = createBrowserRouter([
|
||||
],
|
||||
},
|
||||
...publicRoutes,
|
||||
]);
|
||||
], {
|
||||
// Enable React Router future features for better performance
|
||||
future: {
|
||||
v7_relativeSplatPath: true,
|
||||
v7_fetcherPersist: true,
|
||||
v7_normalizeFormMethod: true,
|
||||
v7_partialHydration: true,
|
||||
v7_skipActionErrorRevalidation: true
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,32 +1,49 @@
|
||||
import { RouteObject } from 'react-router-dom';
|
||||
import { lazy, Suspense } from 'react';
|
||||
import MainLayout from '@/layouts/MainLayout';
|
||||
import HomePage from '@/pages/home/home-page';
|
||||
import ProjectList from '@/pages/projects/project-list';
|
||||
import settingsRoutes from './settings-routes';
|
||||
import adminCenterRoutes from './admin-center-routes';
|
||||
import Schedule from '@/pages/schedule/schedule';
|
||||
import ProjectTemplateEditView from '@/pages/settings/project-templates/projectTemplateEditView/ProjectTemplateEditView';
|
||||
import LicenseExpired from '@/pages/license-expired/license-expired';
|
||||
import ProjectView from '@/pages/projects/projectView/project-view';
|
||||
import Unauthorized from '@/pages/unauthorized/unauthorized';
|
||||
import { useAuthService } from '@/hooks/useAuth';
|
||||
import { Navigate, useLocation } from 'react-router-dom';
|
||||
import { SuspenseFallback } from '@/components/suspense-fallback/suspense-fallback';
|
||||
|
||||
// Define AdminGuard component first
|
||||
// Lazy load page components for better code splitting
|
||||
const HomePage = lazy(() => import('@/pages/home/home-page'));
|
||||
const ProjectList = lazy(() => import('@/pages/projects/project-list'));
|
||||
const Schedule = lazy(() => import('@/pages/schedule/schedule'));
|
||||
const ProjectTemplateEditView = lazy(() => import('@/pages/settings/project-templates/projectTemplateEditView/ProjectTemplateEditView'));
|
||||
const LicenseExpired = lazy(() => import('@/pages/license-expired/license-expired'));
|
||||
const ProjectView = lazy(() => import('@/pages/projects/projectView/project-view'));
|
||||
const Unauthorized = lazy(() => import('@/pages/unauthorized/unauthorized'));
|
||||
|
||||
// Define AdminGuard component with defensive programming
|
||||
const AdminGuard = ({ children }: { children: React.ReactNode }) => {
|
||||
const isAuthenticated = useAuthService().isAuthenticated();
|
||||
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
||||
const authService = useAuthService();
|
||||
const location = useLocation();
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/auth" state={{ from: location }} replace />;
|
||||
}
|
||||
try {
|
||||
// Defensive checks to ensure authService and its methods exist
|
||||
if (!authService ||
|
||||
typeof authService.isAuthenticated !== 'function' ||
|
||||
typeof authService.isOwnerOrAdmin !== 'function') {
|
||||
// If auth service is not ready, render children (don't block)
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
if (!isOwnerOrAdmin) {
|
||||
return <Navigate to="/worklenz/unauthorized" replace />;
|
||||
}
|
||||
if (!authService.isAuthenticated()) {
|
||||
return <Navigate to="/auth" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
if (!authService.isOwnerOrAdmin()) {
|
||||
return <Navigate to="/worklenz/unauthorized" replace />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
} catch (error) {
|
||||
console.error('Error in AdminGuard (main-routes):', error);
|
||||
// On error, render children to prevent complete blocking
|
||||
return <>{children}</>;
|
||||
}
|
||||
};
|
||||
|
||||
const mainRoutes: RouteObject[] = [
|
||||
@@ -35,18 +52,56 @@ const mainRoutes: RouteObject[] = [
|
||||
element: <MainLayout />,
|
||||
children: [
|
||||
{ index: true, element: <Navigate to="home" replace /> },
|
||||
{ path: 'home', element: <HomePage /> },
|
||||
{ path: 'projects', element: <ProjectList /> },
|
||||
{
|
||||
path: 'home',
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<HomePage />
|
||||
</Suspense>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'projects',
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<ProjectList />
|
||||
</Suspense>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'schedule',
|
||||
element: <AdminGuard><Schedule /></AdminGuard>
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<AdminGuard>
|
||||
<Schedule />
|
||||
</AdminGuard>
|
||||
</Suspense>
|
||||
)
|
||||
},
|
||||
{
|
||||
path: `projects/:projectId`,
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<ProjectView />
|
||||
</Suspense>
|
||||
)
|
||||
},
|
||||
{ path: `projects/:projectId`, element: <ProjectView /> },
|
||||
{
|
||||
path: `settings/project-templates/edit/:templateId/:templateName`,
|
||||
element: <ProjectTemplateEditView />,
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<ProjectTemplateEditView />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'unauthorized',
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<Unauthorized />
|
||||
</Suspense>
|
||||
)
|
||||
},
|
||||
{ path: 'unauthorized', element: <Unauthorized /> },
|
||||
...settingsRoutes,
|
||||
...adminCenterRoutes,
|
||||
],
|
||||
@@ -58,7 +113,14 @@ export const licenseExpiredRoute: RouteObject = {
|
||||
path: '/worklenz',
|
||||
element: <MainLayout />,
|
||||
children: [
|
||||
{ path: 'license-expired', element: <LicenseExpired /> }
|
||||
{
|
||||
path: 'license-expired',
|
||||
element: (
|
||||
<Suspense fallback={<SuspenseFallback />}>
|
||||
<LicenseExpired />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user