From 41565c4c53144f75d352acd96ea524fe8c294ca0 Mon Sep 17 00:00:00 2001 From: Yashwin Date: Tue, 3 Feb 2026 12:31:50 +0530 Subject: [PATCH] Enhance Vite configuration for optimized chunking and build performance. Implement manual chunking for feature-based and vendor dependencies to improve loading efficiency. Update ActionDropdown, FilterDropdown, FormSelect, and MultiselectPaginatedSelect components to prevent closing on internal scroll events. Refactor DataTable and other components for improved styling and responsiveness. Introduce lazy loading for route components to enhance application performance. --- src/components/shared/ActionDropdown.tsx | 11 + src/components/shared/DataTable.tsx | 146 ++++---- src/components/shared/EditRoleModal.tsx | 28 +- src/components/shared/FilterDropdown.tsx | 13 + src/components/shared/FormSelect.tsx | 11 + .../shared/MultiselectPaginatedSelect.tsx | 15 + src/components/shared/NewRoleModal.tsx | 8 +- src/components/shared/PageHeader.tsx | 8 +- src/components/shared/PaginatedSelect.tsx | 15 + src/components/superadmin/NewModuleModal.tsx | 2 +- src/components/superadmin/RolesTable.tsx | 2 +- src/components/superadmin/UsersTable.tsx | 9 +- src/components/superadmin/ViewTenantModal.tsx | 318 +++++++++--------- src/components/superadmin/index.ts | 2 +- .../dashboard/components/QuickActions.tsx | 4 +- src/pages/superadmin/TenantDetails.tsx | 4 +- src/pages/tenant/Roles.tsx | 16 +- src/pages/tenant/Users.tsx | 21 +- src/routes/index.tsx | 16 +- src/routes/public-routes.tsx | 37 +- src/routes/super-admin-routes.tsx | 55 +-- src/routes/tenant-admin-routes.tsx | 41 ++- vite.config.ts | 43 ++- 23 files changed, 499 insertions(+), 326 deletions(-) diff --git a/src/components/shared/ActionDropdown.tsx b/src/components/shared/ActionDropdown.tsx index 5867a97..f71083c 100644 --- a/src/components/shared/ActionDropdown.tsx +++ b/src/components/shared/ActionDropdown.tsx @@ -35,8 +35,18 @@ export const ActionDropdown = ({ } }; + const handleScroll = (event: Event) => { + // Don't close if scrolling inside the dropdown menu itself + const target = event.target as HTMLElement; + if (dropdownMenuRef.current && (dropdownMenuRef.current === target || dropdownMenuRef.current.contains(target))) { + return; + } + setIsOpen(false); + }; + if (isOpen && buttonRef.current) { document.addEventListener('mousedown', handleClickOutside); + window.addEventListener('scroll', handleScroll, true); // Calculate position when dropdown opens const rect = buttonRef.current.getBoundingClientRect(); @@ -72,6 +82,7 @@ export const ActionDropdown = ({ return () => { document.removeEventListener('mousedown', handleClickOutside); + window.removeEventListener('scroll', handleScroll, true); }; }, [isOpen]); diff --git a/src/components/shared/DataTable.tsx b/src/components/shared/DataTable.tsx index 2b26059..866db1a 100644 --- a/src/components/shared/DataTable.tsx +++ b/src/components/shared/DataTable.tsx @@ -30,8 +30,8 @@ export const DataTable = ({ // Loading State if (isLoading) { return ( -
-

Loading...

+
+

Loading...

); } @@ -39,8 +39,8 @@ export const DataTable = ({ // Error State if (error) { return ( -
-

{error}

+
+

{error}

); } @@ -50,7 +50,52 @@ export const DataTable = ({ return ( <> {/* Desktop Table Empty State */} -
+
+
+ + + + {columns.map((column) => { + const alignClass = + column.align === 'right' + ? 'text-right' + : column.align === 'center' + ? 'text-center' + : 'text-left'; + return ( + + ); + })} + + + + + + + +
+ {column.label} +
+ {emptyMessage} +
+
+
+ {/* Mobile Empty State */} +
+

{emptyMessage}

+
+ + ); + } + + return ( + <> + {/* Desktop Table */} +
+
@@ -64,7 +109,7 @@ export const DataTable = ({ return ( @@ -73,70 +118,29 @@ export const DataTable = ({ - - - + {data.map((item) => ( + + {columns.map((column) => { + const alignClass = + column.align === 'right' + ? 'text-right' + : column.align === 'center' + ? 'text-center' + : 'text-left'; + return ( + + ); + })} + + ))}
{column.label}
- {emptyMessage} -
+ {column.render ? column.render(item) : String((item as any)[column.key])} +
- {/* Mobile Empty State */} -
-

{emptyMessage}

-
- - ); - } - - return ( - <> - {/* Desktop Table */} -
- - - - {columns.map((column) => { - const alignClass = - column.align === 'right' - ? 'text-right' - : column.align === 'center' - ? 'text-center' - : 'text-left'; - return ( - - ); - })} - - - - {data.map((item) => ( - - {columns.map((column) => { - const alignClass = - column.align === 'right' - ? 'text-right' - : column.align === 'center' - ? 'text-center' - : 'text-left'; - return ( - - ); - })} - - ))} - -
- {column.label} -
- {column.render ? column.render(item) : String((item as any)[column.key])} -
{/* Mobile Card View */} @@ -144,13 +148,13 @@ export const DataTable = ({ {mobileCardRenderer ? data.map((item) =>
{mobileCardRenderer(item)}
) : data.map((item) => ( -
+
{columns.map((column) => ( -
- +
+ {column.mobileLabel || column.label}: -
+
{column.render ? column.render(item) : String((item as any)[column.key])}
diff --git a/src/components/shared/EditRoleModal.tsx b/src/components/shared/EditRoleModal.tsx index e5dbfa1..9244ebf 100644 --- a/src/components/shared/EditRoleModal.tsx +++ b/src/components/shared/EditRoleModal.tsx @@ -310,17 +310,17 @@ export const EditRoleModal = ({ // Map role modules to options from available modules const moduleOptions = roleModules - .map((moduleId: string) => { + .map((moduleId: string) => { const module = availableModulesResponse.data.find((m) => m.id === moduleId); - if (module) { - return { - value: moduleId, - label: module.name, - }; - } - return null; - }) - .filter((opt) => opt !== null) as Array<{ value: string; label: string }>; + if (module) { + return { + value: moduleId, + label: module.name, + }; + } + return null; + }) + .filter((opt) => opt !== null) as Array<{ value: string; label: string }>; setInitialAvailableModuleOptions(moduleOptions); } catch (err) { @@ -527,18 +527,18 @@ export const EditRoleModal = ({ /> {/* Available Modules Selection */} - { + onValueChange={(values) => { setSelectedAvailableModules(values); setValue('modules', values.length > 0 ? values : []); - }} + }} onLoadOptions={loadAvailableModules} initialOptions={initialAvailableModuleOptions} error={errors.modules?.message} - /> + /> {/* Permissions Section */}
diff --git a/src/components/shared/FilterDropdown.tsx b/src/components/shared/FilterDropdown.tsx index b5c5acf..1fa8250 100644 --- a/src/components/shared/FilterDropdown.tsx +++ b/src/components/shared/FilterDropdown.tsx @@ -31,6 +31,7 @@ export const FilterDropdown = ({ const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); const buttonRef = useRef(null); + const dropdownMenuRef = useRef(null); const [dropdownStyle, setDropdownStyle] = useState<{ top?: string; bottom?: string; @@ -54,8 +55,18 @@ export const FilterDropdown = ({ } }; + const handleScroll = (event: Event) => { + // Don't close if scrolling inside the dropdown menu itself + const target = event.target as HTMLElement; + if (dropdownMenuRef.current && (dropdownMenuRef.current === target || dropdownMenuRef.current.contains(target))) { + return; + } + setIsOpen(false); + }; + if (isOpen) { document.addEventListener('mousedown', handleClickOutside); + window.addEventListener('scroll', handleScroll, true); if (buttonRef.current) { const rect = buttonRef.current.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; @@ -86,6 +97,7 @@ export const FilterDropdown = ({ return () => { document.removeEventListener('mousedown', handleClickOutside); + window.removeEventListener('scroll', handleScroll, true); }; }, [isOpen, options.length]); @@ -122,6 +134,7 @@ export const FilterDropdown = ({ buttonRef.current && createPortal(
{ + // Don't close if scrolling inside the dropdown menu itself + const target = event.target as HTMLElement; + if (dropdownMenuRef.current && (dropdownMenuRef.current === target || dropdownMenuRef.current.contains(target))) { + return; + } + setIsOpen(false); + }; + if (isOpen && buttonRef.current) { document.addEventListener('mousedown', handleClickOutside); + window.addEventListener('scroll', handleScroll, true); // Calculate position when dropdown opens const rect = buttonRef.current.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; @@ -87,6 +97,7 @@ export const FormSelect = ({ return () => { document.removeEventListener('mousedown', handleClickOutside); + window.removeEventListener('scroll', handleScroll, true); }; }, [isOpen]); diff --git a/src/components/shared/MultiselectPaginatedSelect.tsx b/src/components/shared/MultiselectPaginatedSelect.tsx index a0b77d4..480f344 100644 --- a/src/components/shared/MultiselectPaginatedSelect.tsx +++ b/src/components/shared/MultiselectPaginatedSelect.tsx @@ -146,8 +146,22 @@ export const MultiselectPaginatedSelect = ({ } }; + const handleScroll = (event: Event) => { + // Don't close if scrolling inside the dropdown's internal scroll container + const target = event.target as HTMLElement; + if (scrollContainerRef.current && (scrollContainerRef.current === target || scrollContainerRef.current.contains(target))) { + return; + } + // Don't close if scrolling inside the dropdown menu itself + if (dropdownMenuRef.current && (dropdownMenuRef.current === target || dropdownMenuRef.current.contains(target))) { + return; + } + setIsOpen(false); + }; + if (isOpen && buttonRef.current) { document.addEventListener('mousedown', handleClickOutside); + window.addEventListener('scroll', handleScroll, true); const rect = buttonRef.current.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; @@ -173,6 +187,7 @@ export const MultiselectPaginatedSelect = ({ return () => { document.removeEventListener('mousedown', handleClickOutside); + window.removeEventListener('scroll', handleScroll, true); }; }, [isOpen]); diff --git a/src/components/shared/NewRoleModal.tsx b/src/components/shared/NewRoleModal.tsx index 28f1131..b07cc09 100644 --- a/src/components/shared/NewRoleModal.tsx +++ b/src/components/shared/NewRoleModal.tsx @@ -369,17 +369,17 @@ export const NewRoleModal = ({ /> {/* Available Modules Selection */} - { + onValueChange={(values) => { setSelectedAvailableModules(values); setValue('modules', values.length > 0 ? values : []); - }} + }} onLoadOptions={loadAvailableModules} error={errors.modules?.message} - /> + /> {/* Permissions Section */}
diff --git a/src/components/shared/PageHeader.tsx b/src/components/shared/PageHeader.tsx index bf2b216..e1951f5 100644 --- a/src/components/shared/PageHeader.tsx +++ b/src/components/shared/PageHeader.tsx @@ -18,8 +18,8 @@ interface PageHeaderProps { const defaultTabs: TabItem[] = [ { label: 'Overview', path: '/dashboard' }, { label: 'Tenants', path: '/tenants' }, - { label: 'Users', path: '/users' }, - { label: 'Roles', path: '/roles' }, + // { label: 'Users', path: '/users' }, + // { label: 'Roles', path: '/roles' }, { label: 'Modules', path: '/modules' }, { label: 'Audit Logs', path: '/audit-logs' }, ]; @@ -58,11 +58,11 @@ export const PageHeader = ({
{/* Title and Description */}
-

+

{title}

{description && ( -

+

{description}

)} diff --git a/src/components/shared/PaginatedSelect.tsx b/src/components/shared/PaginatedSelect.tsx index 8678c77..ad02251 100644 --- a/src/components/shared/PaginatedSelect.tsx +++ b/src/components/shared/PaginatedSelect.tsx @@ -151,8 +151,22 @@ export const PaginatedSelect = ({ } }; + const handleScroll = (event: Event) => { + // Don't close if scrolling inside the dropdown's internal scroll container + const target = event.target as HTMLElement; + if (scrollContainerRef.current && (scrollContainerRef.current === target || scrollContainerRef.current.contains(target))) { + return; + } + // Don't close if scrolling inside the dropdown menu itself + if (dropdownMenuRef.current && (dropdownMenuRef.current === target || dropdownMenuRef.current.contains(target))) { + return; + } + setIsOpen(false); + }; + if (isOpen && buttonRef.current) { document.addEventListener('mousedown', handleClickOutside); + window.addEventListener('scroll', handleScroll, true); const rect = buttonRef.current.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; @@ -178,6 +192,7 @@ export const PaginatedSelect = ({ return () => { document.removeEventListener('mousedown', handleClickOutside); + window.removeEventListener('scroll', handleScroll, true); }; }, [isOpen]); diff --git a/src/components/superadmin/NewModuleModal.tsx b/src/components/superadmin/NewModuleModal.tsx index c4c85bb..adecdec 100644 --- a/src/components/superadmin/NewModuleModal.tsx +++ b/src/components/superadmin/NewModuleModal.tsx @@ -230,7 +230,7 @@ export const NewModuleModal = ({ } > -
+ {/* API Key Display Section */} {apiKey && (
diff --git a/src/components/superadmin/RolesTable.tsx b/src/components/superadmin/RolesTable.tsx index b90452e..b3d0e9d 100644 --- a/src/components/superadmin/RolesTable.tsx +++ b/src/components/superadmin/RolesTable.tsx @@ -324,7 +324,7 @@ export const RolesTable = ({ tenantId, showHeader = true, compact = false }: Rol isLoading={isLoading} error={error} /> - {pagination.totalPages > 1 && ( + {pagination.totalPages > 0 && ( {user.email}, }, + { + key: 'role', + label: 'role', + render: (user) => {user.role?.name}, + }, { key: 'status', label: 'Status', @@ -361,7 +366,7 @@ export const UsersTable = ({ tenantId, showHeader = true, compact = false }: Use isLoading={isLoading} error={error} /> - {pagination.totalPages > 1 && ( + {pagination.totalPages > 0 && ( { setLimit(newLimit); - setCurrentPage(1); + setCurrentPage(1); // Reset to first page when limit changes }} /> )} diff --git a/src/components/superadmin/ViewTenantModal.tsx b/src/components/superadmin/ViewTenantModal.tsx index 2f9975c..cb67eb5 100644 --- a/src/components/superadmin/ViewTenantModal.tsx +++ b/src/components/superadmin/ViewTenantModal.tsx @@ -1,169 +1,169 @@ -import { useEffect, useState } from 'react'; -import type { ReactElement } from 'react'; -import { Loader2 } from 'lucide-react'; -import { Modal, SecondaryButton, StatusBadge } from '@/components/shared'; -import type { Tenant } from '@/types/tenant'; +// import { useEffect, useState } from 'react'; +// import type { ReactElement } from 'react'; +// import { Loader2 } from 'lucide-react'; +// import { Modal, SecondaryButton, StatusBadge } from '@/components/shared'; +// import type { Tenant } from '@/types/tenant'; -interface ViewTenantModalProps { - isOpen: boolean; - onClose: () => void; - tenantId: string | null; - onLoadTenant: (id: string) => Promise; -} +// interface ViewTenantModalProps { +// isOpen: boolean; +// onClose: () => void; +// tenantId: string | null; +// onLoadTenant: (id: string) => Promise; +// } -// Helper function to get status badge variant -const getStatusVariant = (status: string): 'success' | 'failure' | 'process' => { - switch (status.toLowerCase()) { - case 'active': - return 'success'; - case 'deleted': - return 'failure'; - case 'suspended': - return 'process'; - default: - return 'success'; - } -}; +// // Helper function to get status badge variant +// const getStatusVariant = (status: string): 'success' | 'failure' | 'process' => { +// switch (status.toLowerCase()) { +// case 'active': +// return 'success'; +// case 'deleted': +// return 'failure'; +// case 'suspended': +// return 'process'; +// default: +// return 'success'; +// } +// }; -// Helper function to format date -const formatDate = (dateString: string): string => { - const date = new Date(dateString); - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); -}; +// // Helper function to format date +// const formatDate = (dateString: string): string => { +// const date = new Date(dateString); +// return date.toLocaleDateString('en-US', { +// month: 'short', +// day: 'numeric', +// year: 'numeric', +// hour: '2-digit', +// minute: '2-digit', +// }); +// }; -export const ViewTenantModal = ({ - isOpen, - onClose, - tenantId, - onLoadTenant, -}: ViewTenantModalProps): ReactElement | null => { - const [tenant, setTenant] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); +// export const ViewTenantModal = ({ +// isOpen, +// onClose, +// tenantId, +// onLoadTenant, +// }: ViewTenantModalProps): ReactElement | null => { +// const [tenant, setTenant] = useState(null); +// const [isLoading, setIsLoading] = useState(false); +// const [error, setError] = useState(null); - // Load tenant data when modal opens - useEffect(() => { - if (isOpen && tenantId) { - const loadTenant = async (): Promise => { - try { - setIsLoading(true); - setError(null); - const data = await onLoadTenant(tenantId); - setTenant(data); - } catch (err: any) { - setError(err?.response?.data?.error?.message || 'Failed to load tenant details'); - } finally { - setIsLoading(false); - } - }; - loadTenant(); - } else { - setTenant(null); - setError(null); - } - }, [isOpen, tenantId, onLoadTenant]); +// // Load tenant data when modal opens +// useEffect(() => { +// if (isOpen && tenantId) { +// const loadTenant = async (): Promise => { +// try { +// setIsLoading(true); +// setError(null); +// const data = await onLoadTenant(tenantId); +// setTenant(data); +// } catch (err: any) { +// setError(err?.response?.data?.error?.message || 'Failed to load tenant details'); +// } finally { +// setIsLoading(false); +// } +// }; +// loadTenant(); +// } else { +// setTenant(null); +// setError(null); +// } +// }, [isOpen, tenantId, onLoadTenant]); - return ( - - Close - - } - > -
- {isLoading && ( -
- -
- )} +// return ( +// +// Close +// +// } +// > +//
+// {isLoading && ( +//
+// +//
+// )} - {error && ( -
-

{error}

-
- )} +// {error && ( +//
+//

{error}

+//
+// )} - {!isLoading && !error && tenant && ( -
- {/* Basic Information */} -
-

Basic Information

-
-
- -

{tenant.name}

-
-
- -

{tenant.slug}

-
-
- -
- - {tenant.status} - -
-
-
-
+// {!isLoading && !error && tenant && ( +//
+// {/* Basic Information */} +//
+//

Basic Information

+//
+//
+// +//

{tenant.name}

+//
+//
+// +//

{tenant.slug}

+//
+//
+// +//
+// +// {tenant.status} +// +//
+//
+//
+//
- {/* Settings */} -
-

Settings

-
-
- -

- {tenant.settings?.timezone || 'N/A'} -

-
-
- -

- {tenant.subscription_tier ? tenant.subscription_tier.charAt(0).toUpperCase() + tenant.subscription_tier.slice(1) : 'N/A'} -

-
-
- -

{tenant.max_users ?? 'N/A'}

-
-
- -

{tenant.max_modules ?? 'N/A'}

-
-
-
+// {/* Settings */} +//
+//

Settings

+//
+//
+// +//

+// {tenant.settings?.timezone || 'N/A'} +//

+//
+//
+// +//

+// {tenant.subscription_tier ? tenant.subscription_tier.charAt(0).toUpperCase() + tenant.subscription_tier.slice(1) : 'N/A'} +//

+//
+//
+// +//

{tenant.max_users ?? 'N/A'}

+//
+//
+// +//

{tenant.max_modules ?? 'N/A'}

+//
+//
+//
- {/* Timestamps */} -
-

Timestamps

-
-
- -

{formatDate(tenant.created_at)}

-
-
- -

{formatDate(tenant.updated_at)}

-
-
-
-
- )} -
- - ); -}; +// {/* Timestamps */} +//
+//

Timestamps

+//
+//
+// +//

{formatDate(tenant.created_at)}

+//
+//
+// +//

{formatDate(tenant.updated_at)}

+//
+//
+//
+//
+// )} +//
+//
+// ); +// }; diff --git a/src/components/superadmin/index.ts b/src/components/superadmin/index.ts index 89883c7..8d49774 100644 --- a/src/components/superadmin/index.ts +++ b/src/components/superadmin/index.ts @@ -1,5 +1,5 @@ // NewTenantModal is commented out and not exported - using CreateTenantWizard instead -export { ViewTenantModal } from './ViewTenantModal'; +// export { ViewTenantModal } from './ViewTenantModal'; // export { EditTenantModal } from './EditTenantModal'; export { NewModuleModal } from './NewModuleModal'; export { ViewModuleModal } from './ViewModuleModal'; diff --git a/src/features/dashboard/components/QuickActions.tsx b/src/features/dashboard/components/QuickActions.tsx index 4c8db66..10ed81a 100644 --- a/src/features/dashboard/components/QuickActions.tsx +++ b/src/features/dashboard/components/QuickActions.tsx @@ -8,8 +8,8 @@ export const QuickActions = () => { const quickActions: QuickAction[] = [ { icon: Plus, label: 'New Tenant', onClick: () => navigate('/tenants') }, - { icon: UserPlus, label: 'Invite User', onClick: () => navigate('/users') }, - { icon: Shield, label: 'Add Role', onClick: () => navigate('/roles') }, + { icon: UserPlus, label: 'Invite User', onClick: () => navigate('/tenants') }, + { icon: Shield, label: 'Add Role', onClick: () => navigate('/tenants') }, { icon: Settings, label: 'Config', onClick: () => console.log('Config') }, ]; diff --git a/src/pages/superadmin/TenantDetails.tsx b/src/pages/superadmin/TenantDetails.tsx index c31aaff..d1bd864 100644 --- a/src/pages/superadmin/TenantDetails.tsx +++ b/src/pages/superadmin/TenantDetails.tsx @@ -206,14 +206,14 @@ const TenantDetails = (): ReactElement => {
-

+

{tenant.name}

{tenant.status}
-
+
{tenant.slug} diff --git a/src/pages/tenant/Roles.tsx b/src/pages/tenant/Roles.tsx index 13eb714..99c8569 100644 --- a/src/pages/tenant/Roles.tsx +++ b/src/pages/tenant/Roles.tsx @@ -352,14 +352,14 @@ const Roles = (): ReactElement => { {/* New Role Button */} {canCreate('roles') && ( - setIsModalOpen(true)} - > - - New Role - + setIsModalOpen(true)} + > + + New Role + )}
diff --git a/src/pages/tenant/Users.tsx b/src/pages/tenant/Users.tsx index 0774ab0..d7b59c6 100644 --- a/src/pages/tenant/Users.tsx +++ b/src/pages/tenant/Users.tsx @@ -234,6 +234,11 @@ const Users = (): ReactElement => { label: 'Email', render: (user) => {user.email}, }, + { + key: 'role', + label: 'role', + render: (user) => {user.role?.name}, + }, { key: 'status', label: 'Status', @@ -385,14 +390,14 @@ const Users = (): ReactElement => { {/* New User Button */} {canCreate('users') && ( - setIsModalOpen(true)} - > - - New User - + setIsModalOpen(true)} + > + + New User + )}
diff --git a/src/routes/index.tsx b/src/routes/index.tsx index ddbb99a..6021c5e 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -1,6 +1,6 @@ import { Routes, Route } from 'react-router-dom'; +import { lazy, Suspense } from 'react'; import type { ReactElement } from 'react'; -import NotFound from '@/pages/NotFound'; import ProtectedRoute from '@/pages/ProtectedRoute'; import TenantProtectedRoute from '@/pages/tenant/TenantProtectedRoute'; import { NavigationInitializer } from '@/components/NavigationInitializer'; @@ -8,6 +8,16 @@ import { publicRoutes } from './public-routes'; import { superAdminRoutes } from './super-admin-routes'; import { tenantAdminRoutes } from './tenant-admin-routes'; +// Lazy load NotFound page +const NotFound = lazy(() => import('@/pages/NotFound')); + +// Loading fallback component +const RouteLoader = (): ReactElement => ( +
+
Loading...
+
+); + // App Routes Component export const AppRoutes = (): ReactElement => { return ( @@ -42,7 +52,9 @@ export const AppRoutes = (): ReactElement => { path="*" element={ - + }> + + } /> diff --git a/src/routes/public-routes.tsx b/src/routes/public-routes.tsx index 9d67444..015022c 100644 --- a/src/routes/public-routes.tsx +++ b/src/routes/public-routes.tsx @@ -1,9 +1,26 @@ -import Login from '@/pages/Login'; -import TenantLogin from '@/pages/tenant/TenantLogin'; -import ForgotPassword from '@/pages/ForgotPassword'; -import ResetPassword from '@/pages/ResetPassword'; +import { lazy, Suspense } from 'react'; import type { ReactElement } from 'react'; +// Lazy load route components for code splitting +const Login = lazy(() => import('@/pages/Login')); +const TenantLogin = lazy(() => import('@/pages/tenant/TenantLogin')); +const ForgotPassword = lazy(() => import('@/pages/ForgotPassword')); +const ResetPassword = lazy(() => import('@/pages/ResetPassword')); + +// Loading fallback component +const RouteLoader = (): ReactElement => ( +
+
Loading...
+
+); + +// Wrapper component with Suspense +const LazyRoute = ({ component: Component }: { component: React.ComponentType }): ReactElement => ( + }> + + +); + export interface RouteConfig { path: string; element: ReactElement; @@ -13,26 +30,26 @@ export interface RouteConfig { export const publicRoutes: RouteConfig[] = [ { path: '/', - element: , + element: , }, { path: '/forgot-password', - element: , + element: , }, { path: '/reset-password', - element: , + element: , }, { path: '/tenant/login', - element: , + element: , }, { path: '/tenant/forgot-password', - element: , + element: , }, { path: '/tenant/reset-password', - element: , + element: , }, ]; diff --git a/src/routes/super-admin-routes.tsx b/src/routes/super-admin-routes.tsx index aa7842b..85a717e 100644 --- a/src/routes/super-admin-routes.tsx +++ b/src/routes/super-admin-routes.tsx @@ -1,14 +1,29 @@ -import Dashboard from '@/pages/superadmin/Dashboard'; -import Tenants from '@/pages/superadmin/Tenants'; -import CreateTenantWizard from '@/pages/superadmin/CreateTenantWizard'; -import EditTenant from '@/pages/superadmin/EditTenant'; -import TenantDetails from '@/pages/superadmin/TenantDetails'; -// import Users from '@/pages/superadmin/Users'; -// import Roles from '@/pages/superadmin/Roles'; -import Modules from '@/pages/superadmin/Modules'; -import AuditLogs from '@/pages/superadmin/AuditLogs'; +import { lazy, Suspense } from 'react'; import type { ReactElement } from 'react'; +// Lazy load route components for code splitting +const Dashboard = lazy(() => import('@/pages/superadmin/Dashboard')); +const Tenants = lazy(() => import('@/pages/superadmin/Tenants')); +const CreateTenantWizard = lazy(() => import('@/pages/superadmin/CreateTenantWizard')); +const EditTenant = lazy(() => import('@/pages/superadmin/EditTenant')); +const TenantDetails = lazy(() => import('@/pages/superadmin/TenantDetails')); +const Modules = lazy(() => import('@/pages/superadmin/Modules')); +const AuditLogs = lazy(() => import('@/pages/superadmin/AuditLogs')); + +// Loading fallback component +const RouteLoader = (): ReactElement => ( +
+
Loading...
+
+); + +// Wrapper component with Suspense +const LazyRoute = ({ component: Component }: { component: React.ComponentType }): ReactElement => ( + }> + + +); + export interface RouteConfig { path: string; element: ReactElement; @@ -18,38 +33,30 @@ export interface RouteConfig { export const superAdminRoutes: RouteConfig[] = [ { path: '/dashboard', - element: , + element: , }, { path: '/tenants', - element: , + element: , }, { path: '/tenants/create-wizard', - element: , + element: , }, { path: '/tenants/:id/edit', - element: , + element: , }, { path: '/tenants/:id', - element: , + element: , }, - // { - // path: '/users', - // element: , - // }, - // { - // path: '/roles', - // element: , - // }, { path: '/modules', - element: , + element: , }, { path: '/audit-logs', - element: , + element: , }, ]; diff --git a/src/routes/tenant-admin-routes.tsx b/src/routes/tenant-admin-routes.tsx index 5c10cc4..edcec6c 100644 --- a/src/routes/tenant-admin-routes.tsx +++ b/src/routes/tenant-admin-routes.tsx @@ -1,10 +1,27 @@ -import Dashboard from '@/pages/tenant/Dashboard'; -import Roles from '@/pages/tenant/Roles'; -import Settings from '@/pages/tenant/Settings'; -import Users from '@/pages/tenant/Users'; -import AuditLogs from '@/pages/tenant/AuditLogs'; +import { lazy, Suspense } from 'react'; import type { ReactElement } from 'react'; -import Modules from '@/pages/tenant/Modules'; + +// Lazy load route components for code splitting +const Dashboard = lazy(() => import('@/pages/tenant/Dashboard')); +const Roles = lazy(() => import('@/pages/tenant/Roles')); +const Settings = lazy(() => import('@/pages/tenant/Settings')); +const Users = lazy(() => import('@/pages/tenant/Users')); +const AuditLogs = lazy(() => import('@/pages/tenant/AuditLogs')); +const Modules = lazy(() => import('@/pages/tenant/Modules')); + +// Loading fallback component +const RouteLoader = (): ReactElement => ( +
+
Loading...
+
+); + +// Wrapper component with Suspense +const LazyRoute = ({ component: Component }: { component: React.ComponentType }): ReactElement => ( + }> + + +); export interface RouteConfig { path: string; @@ -15,26 +32,26 @@ export interface RouteConfig { export const tenantAdminRoutes: RouteConfig[] = [ { path: '/tenant', - element: , + element: , }, { path: '/tenant/roles', - element: , + element: , }, { path: '/tenant/users', - element: , + element: , }, { path: '/tenant/modules', - element: , + element: , }, { path: '/tenant/audit-logs', - element: , + element: , }, { path: '/tenant/settings', - element: , + element: , }, ]; diff --git a/vite.config.ts b/vite.config.ts index d278b27..54e8554 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -11,5 +11,46 @@ export default defineConfig({ "@": path.resolve(__dirname, "src"), }, }, - + build: { + rollupOptions: { + output: { + manualChunks(id) { + // Feature-based chunks - only for source files + if (!id.includes('node_modules')) { + if (id.includes('/src/pages/superadmin/')) { + return 'superadmin'; + } + if (id.includes('/src/pages/tenant/')) { + return 'tenant'; + } + return; + } + + // Vendor chunks - group React ecosystem (including Redux) together + // to avoid circular dependencies + if ( + id.includes('node_modules/react') || + id.includes('node_modules/react-dom') || + id.includes('node_modules/react-router') || + id.includes('node_modules/@reduxjs') || + id.includes('node_modules/redux') || + id.includes('node_modules/react-redux') || + id.includes('node_modules/scheduler') || + id.includes('node_modules/object-assign') + ) { + return 'react-vendor'; + } + + // UI libraries + if (id.includes('node_modules/lucide-react')) { + return 'ui-vendor'; + } + + // All other node_modules go to vendor + return 'vendor'; + }, + }, + }, + chunkSizeWarningLimit: 600, + }, })