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
|
// Components
|
||||||
import ThemeWrapper from './features/theme/ThemeWrapper';
|
import ThemeWrapper from './features/theme/ThemeWrapper';
|
||||||
import PreferenceSelector from './components/PreferenceSelector';
|
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
import router from './app/routes';
|
import router from './app/routes';
|
||||||
@@ -14,7 +13,6 @@ import router from './app/routes';
|
|||||||
import { useAppSelector } from './hooks/useAppSelector';
|
import { useAppSelector } from './hooks/useAppSelector';
|
||||||
import { initMixpanel } from './utils/mixpanelInit';
|
import { initMixpanel } from './utils/mixpanelInit';
|
||||||
import { initializeCsrfToken } from './api/api-client';
|
import { initializeCsrfToken } from './api/api-client';
|
||||||
import { useRoutePreloader } from './utils/routePreloader';
|
|
||||||
|
|
||||||
// Types & Constants
|
// Types & Constants
|
||||||
import { Language } from './features/i18n/localesSlice';
|
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
|
* 1. React.memo() - Prevents unnecessary re-renders
|
||||||
* 2. useMemo() - Memoizes expensive computations
|
* 2. useMemo() - Memoizes expensive computations
|
||||||
* 3. useCallback() - Memoizes event handlers
|
* 3. useCallback() - Memoizes event handlers
|
||||||
* 4. Route preloading - Preloads critical routes
|
* 4. Lazy loading - All route components loaded on demand
|
||||||
* 5. Lazy loading - Components loaded on demand
|
* 5. Suspense boundaries - Better loading states
|
||||||
* 6. Suspense boundaries - Better loading states
|
* 6. Optimized guard components with memoization
|
||||||
*/
|
*/
|
||||||
const App: React.FC = memo(() => {
|
const App: React.FC = memo(() => {
|
||||||
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
const themeMode = useAppSelector(state => state.themeReducer.mode);
|
||||||
@@ -39,25 +37,6 @@ const App: React.FC = memo(() => {
|
|||||||
// Memoize mixpanel initialization to prevent re-initialization
|
// Memoize mixpanel initialization to prevent re-initialization
|
||||||
const mixpanelToken = useMemo(() => import.meta.env.VITE_MIXPANEL_TOKEN as string, []);
|
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(() => {
|
useEffect(() => {
|
||||||
initMixpanel(mixpanelToken);
|
initMixpanel(mixpanelToken);
|
||||||
}, [mixpanelToken]);
|
}, [mixpanelToken]);
|
||||||
@@ -95,7 +74,12 @@ const App: React.FC = memo(() => {
|
|||||||
return (
|
return (
|
||||||
<Suspense fallback={<SuspenseFallback />}>
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
<ThemeWrapper>
|
<ThemeWrapper>
|
||||||
<RouterProvider router={router} future={{ v7_startTransition: true }} />
|
<RouterProvider
|
||||||
|
router={router}
|
||||||
|
future={{
|
||||||
|
v7_startTransition: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ThemeWrapper>
|
</ThemeWrapper>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
|
import { lazy, Suspense } from 'react';
|
||||||
import AuthLayout from '@/layouts/AuthLayout';
|
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 { 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';
|
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 = [
|
const authRoutes = [
|
||||||
{
|
{
|
||||||
path: '/auth',
|
path: '/auth',
|
||||||
element: (
|
element: <AuthLayout />,
|
||||||
<AuthLayout />
|
|
||||||
),
|
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
@@ -22,27 +22,51 @@ const authRoutes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'login',
|
path: 'login',
|
||||||
element: <LoginPage />,
|
element: (
|
||||||
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
|
<LoginPage />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'signup',
|
path: 'signup',
|
||||||
element: <SignupPage />,
|
element: (
|
||||||
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
|
<SignupPage />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'forgot-password',
|
path: 'forgot-password',
|
||||||
element: <ForgotPasswordPage />,
|
element: (
|
||||||
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
|
<ForgotPasswordPage />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'logging-out',
|
path: 'logging-out',
|
||||||
element: <LoggingOutPage />,
|
element: (
|
||||||
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
|
<LoggingOutPage />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'authenticating',
|
path: 'authenticating',
|
||||||
element: <AuthenticatingPage />,
|
element: (
|
||||||
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
|
<AuthenticatingPage />
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'verify-reset-email/:user/:hash',
|
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 { createBrowserRouter, Navigate, RouteObject, useLocation } from 'react-router-dom';
|
||||||
|
import { lazy, Suspense, memo, useMemo } from 'react';
|
||||||
import rootRoutes from './root-routes';
|
import rootRoutes from './root-routes';
|
||||||
import authRoutes from './auth-routes';
|
import authRoutes from './auth-routes';
|
||||||
import mainRoutes, { licenseExpiredRoute } from './main-routes';
|
import mainRoutes, { licenseExpiredRoute } from './main-routes';
|
||||||
@@ -8,68 +9,125 @@ import reportingRoutes from './reporting-routes';
|
|||||||
import { useAuthService } from '@/hooks/useAuth';
|
import { useAuthService } from '@/hooks/useAuth';
|
||||||
import { AuthenticatedLayout } from '@/layouts/AuthenticatedLayout';
|
import { AuthenticatedLayout } from '@/layouts/AuthenticatedLayout';
|
||||||
import ErrorBoundary from '@/components/ErrorBoundary';
|
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 { 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 {
|
interface GuardProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthGuard = ({ children }: GuardProps) => {
|
// Route-based code splitting utility
|
||||||
const isAuthenticated = useAuthService().isAuthenticated();
|
const withCodeSplitting = (Component: React.LazyExoticComponent<React.ComponentType<any>>) => {
|
||||||
|
return memo(() => (
|
||||||
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
|
<Component />
|
||||||
|
</Suspense>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Memoized guard components with defensive programming
|
||||||
|
export const AuthGuard = memo(({ children }: GuardProps) => {
|
||||||
|
const authService = useAuthService();
|
||||||
const location = useLocation();
|
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 AuthGuard:', error);
|
||||||
|
return false; // Don't redirect on error, let the app handle it
|
||||||
|
}
|
||||||
|
}, [authService]);
|
||||||
|
|
||||||
|
if (shouldRedirect) {
|
||||||
return <Navigate to="/auth" state={{ from: location }} replace />;
|
return <Navigate to="/auth" state={{ from: location }} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
});
|
||||||
|
|
||||||
export const AdminGuard = ({ children }: GuardProps) => {
|
AuthGuard.displayName = 'AuthGuard';
|
||||||
const isAuthenticated = useAuthService().isAuthenticated();
|
|
||||||
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
export const AdminGuard = memo(({ children }: GuardProps) => {
|
||||||
const currentSession = useAuthService().getCurrentSession();
|
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;
|
const isFreePlan = currentSession?.subscription_type === ISUBSCRIPTION_TYPE.FREE;
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!authService.isOwnerOrAdmin() || isFreePlan) {
|
||||||
return <Navigate to="/auth" state={{ from: location }} replace />;
|
return { redirect: '/worklenz/unauthorized' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isOwnerOrAdmin || isFreePlan) {
|
return null;
|
||||||
return <Navigate to="/worklenz/unauthorized" replace />;
|
} 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}</>;
|
return <>{children}</>;
|
||||||
};
|
});
|
||||||
|
|
||||||
export const LicenseExpiryGuard = ({ children }: GuardProps) => {
|
AdminGuard.displayName = 'AdminGuard';
|
||||||
const isAuthenticated = useAuthService().isAuthenticated();
|
|
||||||
const currentSession = useAuthService().getCurrentSession();
|
export const LicenseExpiryGuard = memo(({ children }: GuardProps) => {
|
||||||
|
const authService = useAuthService();
|
||||||
const location = useLocation();
|
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 isAdminCenterRoute = location.pathname.includes('/worklenz/admin-center');
|
||||||
const isLicenseExpiredRoute = location.pathname === '/worklenz/license-expired';
|
const isLicenseExpiredRoute = location.pathname === '/worklenz/license-expired';
|
||||||
|
|
||||||
// Don't check or redirect if we're already on the license-expired page
|
// Don't check or redirect if we're already on the license-expired page
|
||||||
if (isLicenseExpiredRoute) {
|
if (isLicenseExpiredRoute) return false;
|
||||||
return <>{children}</>;
|
|
||||||
}
|
const currentSession = authService.getCurrentSession();
|
||||||
|
|
||||||
// Check if trial is expired more than 7 days or if is_expired flag is set
|
// Check if trial is expired more than 7 days or if is_expired flag is set
|
||||||
const isLicenseExpiredMoreThan7Days = () => {
|
const isLicenseExpiredMoreThan7Days = () => {
|
||||||
// Quick bail if no session data is available
|
// Quick bail if no session data is available
|
||||||
if (!currentSession) {
|
if (!currentSession) return false;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check is_expired flag first
|
// Check is_expired flag first
|
||||||
if (currentSession.is_expired) {
|
if (currentSession.is_expired) {
|
||||||
// If no trial_expire_date exists but is_expired is true, defer to backend check
|
// If no trial_expire_date exists but is_expired is true, defer to backend check
|
||||||
if (!currentSession.trial_expire_date) {
|
if (!currentSession.trial_expire_date) return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is a trial_expire_date, check if it's more than 7 days past
|
// If there is a trial_expire_date, check if it's more than 7 days past
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
@@ -97,27 +155,49 @@ export const LicenseExpiryGuard = ({ children }: GuardProps) => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add this explicit check and log the result
|
return isLicenseExpiredMoreThan7Days() && !isAdminCenterRoute;
|
||||||
const shouldRedirect = isAuthenticated && isLicenseExpiredMoreThan7Days() && !isAdminCenterRoute;
|
} catch (error) {
|
||||||
|
console.error('Error in LicenseExpiryGuard:', error);
|
||||||
|
return false; // Don't redirect on error
|
||||||
|
}
|
||||||
|
}, [authService, location.pathname]);
|
||||||
|
|
||||||
if (shouldRedirect) {
|
if (shouldRedirect) {
|
||||||
return <Navigate to="/worklenz/license-expired" replace />;
|
return <Navigate to="/worklenz/license-expired" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
});
|
||||||
|
|
||||||
export const SetupGuard = ({ children }: GuardProps) => {
|
LicenseExpiryGuard.displayName = 'LicenseExpiryGuard';
|
||||||
const isAuthenticated = useAuthService().isAuthenticated();
|
|
||||||
|
export const SetupGuard = memo(({ children }: GuardProps) => {
|
||||||
|
const authService = useAuthService();
|
||||||
const location = useLocation();
|
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 <Navigate to="/auth" state={{ from: location }} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
};
|
});
|
||||||
|
|
||||||
// Helper to wrap routes with guards
|
SetupGuard.displayName = 'SetupGuard';
|
||||||
|
|
||||||
|
// Optimized route wrapping function with Suspense boundaries
|
||||||
const wrapRoutes = (
|
const wrapRoutes = (
|
||||||
routes: RouteObject[],
|
routes: RouteObject[],
|
||||||
Guard: React.ComponentType<{ children: React.ReactNode }>
|
Guard: React.ComponentType<{ children: React.ReactNode }>
|
||||||
@@ -125,7 +205,11 @@ const wrapRoutes = (
|
|||||||
return routes.map(route => {
|
return routes.map(route => {
|
||||||
const wrappedRoute = {
|
const wrappedRoute = {
|
||||||
...route,
|
...route,
|
||||||
element: <Guard>{route.element}</Guard>,
|
element: (
|
||||||
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
|
<Guard>{route.element}</Guard>
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (route.children) {
|
if (route.children) {
|
||||||
@@ -140,9 +224,8 @@ const wrapRoutes = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Static license expired component that doesn't rely on translations or authentication
|
// Optimized static license expired component
|
||||||
const StaticLicenseExpired = () => {
|
const StaticLicenseExpired = memo(() => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
marginTop: 65,
|
marginTop: 65,
|
||||||
@@ -184,23 +267,31 @@ const StaticLicenseExpired = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
StaticLicenseExpired.displayName = 'StaticLicenseExpired';
|
||||||
|
|
||||||
|
// Create route arrays (moved outside of useMemo to avoid hook violations)
|
||||||
const publicRoutes = [
|
const publicRoutes = [
|
||||||
...rootRoutes,
|
...rootRoutes,
|
||||||
...authRoutes,
|
...authRoutes,
|
||||||
notFoundRoute
|
notFoundRoute
|
||||||
];
|
];
|
||||||
|
|
||||||
const protectedMainRoutes = wrapRoutes(mainRoutes, AuthGuard);
|
const protectedMainRoutes = wrapRoutes(mainRoutes, AuthGuard);
|
||||||
const adminRoutes = wrapRoutes(reportingRoutes, AdminGuard);
|
const adminRoutes = wrapRoutes(reportingRoutes, AdminGuard);
|
||||||
const setupRoutes = wrapRoutes([accountSetupRoute], SetupGuard);
|
const setupRoutes = wrapRoutes([accountSetupRoute], SetupGuard);
|
||||||
|
|
||||||
// Apply LicenseExpiryGuard to all protected routes
|
// License expiry check function
|
||||||
const withLicenseExpiryCheck = (routes: RouteObject[]): RouteObject[] => {
|
const withLicenseExpiryCheck = (routes: RouteObject[]): RouteObject[] => {
|
||||||
return routes.map(route => {
|
return routes.map(route => {
|
||||||
const wrappedRoute = {
|
const wrappedRoute = {
|
||||||
...route,
|
...route,
|
||||||
element: <LicenseExpiryGuard>{route.element}</LicenseExpiryGuard>,
|
element: (
|
||||||
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
|
<LicenseExpiryGuard>{route.element}</LicenseExpiryGuard>
|
||||||
|
</Suspense>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (route.children) {
|
if (route.children) {
|
||||||
@@ -213,10 +304,21 @@ const withLicenseExpiryCheck = (routes: RouteObject[]): RouteObject[] => {
|
|||||||
|
|
||||||
const licenseCheckedMainRoutes = withLicenseExpiryCheck(protectedMainRoutes);
|
const licenseCheckedMainRoutes = withLicenseExpiryCheck(protectedMainRoutes);
|
||||||
|
|
||||||
|
// Create optimized router with future flags for better performance
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
element: <ErrorBoundary><AuthenticatedLayout /></ErrorBoundary>,
|
element: (
|
||||||
errorElement: <ErrorBoundary><NotFoundPage /></ErrorBoundary>,
|
<ErrorBoundary>
|
||||||
|
<AuthenticatedLayout />
|
||||||
|
</ErrorBoundary>
|
||||||
|
),
|
||||||
|
errorElement: (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
|
<NotFoundPage />
|
||||||
|
</Suspense>
|
||||||
|
</ErrorBoundary>
|
||||||
|
),
|
||||||
children: [
|
children: [
|
||||||
...licenseCheckedMainRoutes,
|
...licenseCheckedMainRoutes,
|
||||||
...adminRoutes,
|
...adminRoutes,
|
||||||
@@ -225,6 +327,15 @@ const router = createBrowserRouter([
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
...publicRoutes,
|
...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;
|
export default router;
|
||||||
|
|||||||
@@ -1,32 +1,49 @@
|
|||||||
import { RouteObject } from 'react-router-dom';
|
import { RouteObject } from 'react-router-dom';
|
||||||
|
import { lazy, Suspense } from 'react';
|
||||||
import MainLayout from '@/layouts/MainLayout';
|
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 settingsRoutes from './settings-routes';
|
||||||
import adminCenterRoutes from './admin-center-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 { useAuthService } from '@/hooks/useAuth';
|
||||||
import { Navigate, useLocation } from 'react-router-dom';
|
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 AdminGuard = ({ children }: { children: React.ReactNode }) => {
|
||||||
const isAuthenticated = useAuthService().isAuthenticated();
|
const authService = useAuthService();
|
||||||
const isOwnerOrAdmin = useAuthService().isOwnerOrAdmin();
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
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 (!authService.isAuthenticated()) {
|
||||||
return <Navigate to="/auth" state={{ from: location }} replace />;
|
return <Navigate to="/auth" state={{ from: location }} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isOwnerOrAdmin) {
|
if (!authService.isOwnerOrAdmin()) {
|
||||||
return <Navigate to="/worklenz/unauthorized" replace />;
|
return <Navigate to="/worklenz/unauthorized" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{children}</>;
|
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[] = [
|
const mainRoutes: RouteObject[] = [
|
||||||
@@ -35,18 +52,56 @@ const mainRoutes: RouteObject[] = [
|
|||||||
element: <MainLayout />,
|
element: <MainLayout />,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <Navigate to="home" replace /> },
|
{ 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',
|
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`,
|
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,
|
...settingsRoutes,
|
||||||
...adminCenterRoutes,
|
...adminCenterRoutes,
|
||||||
],
|
],
|
||||||
@@ -58,7 +113,14 @@ export const licenseExpiredRoute: RouteObject = {
|
|||||||
path: '/worklenz',
|
path: '/worklenz',
|
||||||
element: <MainLayout />,
|
element: <MainLayout />,
|
||||||
children: [
|
children: [
|
||||||
{ path: 'license-expired', element: <LicenseExpired /> }
|
{
|
||||||
|
path: 'license-expired',
|
||||||
|
element: (
|
||||||
|
<Suspense fallback={<SuspenseFallback />}>
|
||||||
|
<LicenseExpired />
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user