@jmruthers/pace-core 0.5.90 → 0.5.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataTable-VIP44OB6.js → DataTable-HC5S4RKB.js} +6 -6
- package/dist/{PublicLoadingSpinner-BQXD1fbO.d.ts → PublicLoadingSpinner-n74JgA9h.d.ts} +59 -4
- package/dist/{UnifiedAuthProvider-6JRTOFPS.js → UnifiedAuthProvider-ZM7VUC45.js} +3 -3
- package/dist/{chunk-EWMXLDIX.js → chunk-5LAY74WM.js} +375 -284
- package/dist/chunk-5LAY74WM.js.map +1 -0
- package/dist/{chunk-G2SCPUKC.js → chunk-6WFM22A4.js} +2 -2
- package/dist/{chunk-AIV3VYBQ.js → chunk-AAM57AEU.js} +4 -2
- package/dist/chunk-AAM57AEU.js.map +1 -0
- package/dist/{chunk-7XBW2P7B.js → chunk-AYC2P377.js} +2 -2
- package/dist/{chunk-GD3ENUKD.js → chunk-AZ2QJYKU.js} +3 -3
- package/dist/{chunk-G2YT64FA.js → chunk-GP3HU6WS.js} +3 -3
- package/dist/{chunk-7NIERLC6.js → chunk-HW5BGOWB.js} +4 -4
- package/dist/{chunk-JDPFQV3V.js → chunk-M52CQP5W.js} +4 -4
- package/dist/{chunk-JQWSAYZC.js → chunk-OXFOS62D.js} +2 -2
- package/dist/{chunk-4DYK5KCK.js → chunk-TZXYSZT3.js} +4 -4
- package/dist/{chunk-XZHZYSAK.js → chunk-XIBSVWJW.js} +5 -5
- package/dist/components.d.ts +1 -1
- package/dist/components.js +10 -8
- package/dist/components.js.map +1 -1
- package/dist/hooks.js +7 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +13 -11
- package/dist/index.js.map +1 -1
- package/dist/providers.js +2 -2
- package/dist/rbac/index.js +7 -7
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventLogoProps.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +34 -21
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +97 -0
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UseEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +43 -5
- package/package.json +1 -1
- package/src/components/EventSelector/EventSelector.tsx +19 -1
- package/src/components/Header/Header.tsx +52 -15
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +4 -0
- package/src/components/PaceAppLayout/README.md +30 -0
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +224 -0
- package/src/components/ProtectedRoute/index.ts +3 -0
- package/src/components/index.ts +4 -1
- package/src/index.ts +3 -0
- package/src/providers/AuthProvider.simplified.tsx +108 -14
- package/src/services/EventService.ts +6 -1
- package/dist/chunk-AIV3VYBQ.js.map +0 -1
- package/dist/chunk-EWMXLDIX.js.map +0 -1
- /package/dist/{DataTable-VIP44OB6.js.map → DataTable-HC5S4RKB.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-6JRTOFPS.js.map → UnifiedAuthProvider-ZM7VUC45.js.map} +0 -0
- /package/dist/{chunk-G2SCPUKC.js.map → chunk-6WFM22A4.js.map} +0 -0
- /package/dist/{chunk-7XBW2P7B.js.map → chunk-AYC2P377.js.map} +0 -0
- /package/dist/{chunk-GD3ENUKD.js.map → chunk-AZ2QJYKU.js.map} +0 -0
- /package/dist/{chunk-G2YT64FA.js.map → chunk-GP3HU6WS.js.map} +0 -0
- /package/dist/{chunk-7NIERLC6.js.map → chunk-HW5BGOWB.js.map} +0 -0
- /package/dist/{chunk-JDPFQV3V.js.map → chunk-M52CQP5W.js.map} +0 -0
- /package/dist/{chunk-JQWSAYZC.js.map → chunk-OXFOS62D.js.map} +0 -0
- /package/dist/{chunk-4DYK5KCK.js.map → chunk-TZXYSZT3.js.map} +0 -0
- /package/dist/{chunk-XZHZYSAK.js.map → chunk-XIBSVWJW.js.map} +0 -0
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Features:
|
|
11
11
|
* - Customizable logo (URL or component)
|
|
12
|
+
* - Clickable logo that routes to dashboard (configurable)
|
|
12
13
|
* - Navigation menu integration
|
|
13
14
|
* - User menu with authentication
|
|
14
15
|
* - Event selector for multi-tenant applications
|
|
@@ -20,10 +21,11 @@
|
|
|
20
21
|
*
|
|
21
22
|
* @example
|
|
22
23
|
* ```tsx
|
|
23
|
-
* // Basic header with logo and user menu
|
|
24
|
+
* // Basic header with logo and user menu (logo routes to /dashboard by default)
|
|
24
25
|
* <Header
|
|
25
26
|
* logoUrl="/logo.svg"
|
|
26
27
|
* logoAlt="My App"
|
|
28
|
+
* logoHref="/dashboard"
|
|
27
29
|
* user={currentUser}
|
|
28
30
|
* onSignOut={handleSignOut}
|
|
29
31
|
* />
|
|
@@ -31,6 +33,7 @@
|
|
|
31
33
|
* // Header with navigation and custom actions
|
|
32
34
|
* <Header
|
|
33
35
|
* logo={<CustomLogo />}
|
|
36
|
+
* logoHref="/home"
|
|
34
37
|
* navItems={[
|
|
35
38
|
* { id: 'dashboard', label: 'Dashboard', href: '/dashboard' },
|
|
36
39
|
* { id: 'users', label: 'Users', href: '/users' },
|
|
@@ -50,6 +53,7 @@
|
|
|
50
53
|
* // Header without event selector
|
|
51
54
|
* <Header
|
|
52
55
|
* logoUrl="/logo.svg"
|
|
56
|
+
* logoHref="/dashboard"
|
|
53
57
|
* showEventSelector={false}
|
|
54
58
|
* user={currentUser}
|
|
55
59
|
* onSignOut={handleSignOut}
|
|
@@ -58,6 +62,7 @@
|
|
|
58
62
|
* // Header with custom user menu
|
|
59
63
|
* <Header
|
|
60
64
|
* logoUrl="/logo.svg"
|
|
65
|
+
* logoHref="/home"
|
|
61
66
|
* userMenu={<CustomUserMenu user={currentUser} />}
|
|
62
67
|
* showUserMenu={true}
|
|
63
68
|
* />
|
|
@@ -81,6 +86,7 @@
|
|
|
81
86
|
*/
|
|
82
87
|
|
|
83
88
|
import React from 'react';
|
|
89
|
+
import { Link } from 'react-router-dom';
|
|
84
90
|
import { User } from '@supabase/supabase-js';
|
|
85
91
|
import { cn } from '../../utils/cn';
|
|
86
92
|
import { EventSelector } from '../EventSelector';
|
|
@@ -120,6 +126,8 @@ export interface HeaderProps {
|
|
|
120
126
|
currentPath?: string;
|
|
121
127
|
/** Custom navigation handler */
|
|
122
128
|
onNavigate?: (item: NavigationItem) => void;
|
|
129
|
+
/** URL to navigate to when logo is clicked (e.g., '/dashboard') */
|
|
130
|
+
logoHref?: string;
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
/**
|
|
@@ -131,6 +139,7 @@ export interface HeaderProps {
|
|
|
131
139
|
*
|
|
132
140
|
* Features:
|
|
133
141
|
* - Customizable logo (URL or custom component)
|
|
142
|
+
* - Clickable logo that automatically routes to dashboard (configurable via logoHref)
|
|
134
143
|
* - Navigation menu integration with highlighting
|
|
135
144
|
* - User menu with authentication and password management
|
|
136
145
|
* - Event selector for multi-tenant applications
|
|
@@ -139,9 +148,9 @@ export interface HeaderProps {
|
|
|
139
148
|
* - Accessibility compliant with proper ARIA attributes
|
|
140
149
|
* - Backdrop blur effects for modern UI
|
|
141
150
|
* - Flexible layout with configurable sections
|
|
142
|
-
*
|
|
151
|
+
*
|
|
143
152
|
* @example
|
|
144
|
-
* Basic header with logo and navigation:
|
|
153
|
+
* Basic header with logo and navigation (logo routes to /dashboard when clicked):
|
|
145
154
|
* ```tsx
|
|
146
155
|
* import { Header } from '@jmruthers/pace-core';
|
|
147
156
|
* import { useNavigate, useLocation } from 'react-router-dom';
|
|
@@ -160,6 +169,7 @@ export interface HeaderProps {
|
|
|
160
169
|
* <Header
|
|
161
170
|
* logoUrl="/company-logo.svg"
|
|
162
171
|
* logoAlt="My Company"
|
|
172
|
+
* logoHref="/dashboard"
|
|
163
173
|
* navItems={navItems}
|
|
164
174
|
* currentPath={location.pathname}
|
|
165
175
|
* onNavigate={(item) => navigate(item.href)}
|
|
@@ -230,7 +240,8 @@ export function Header({
|
|
|
230
240
|
showEventSelector = true,
|
|
231
241
|
showUserMenu = true,
|
|
232
242
|
currentPath,
|
|
233
|
-
onNavigate
|
|
243
|
+
onNavigate,
|
|
244
|
+
logoHref
|
|
234
245
|
}: HeaderProps) {
|
|
235
246
|
return (
|
|
236
247
|
<header className={cn(
|
|
@@ -240,19 +251,45 @@ export function Header({
|
|
|
240
251
|
<nav className="px-4 w-[min(var(--app-width),100%)] mx-auto grid grid-cols-[auto_auto_1fr_auto] gap-4 h-full items-center">
|
|
241
252
|
{/* Logo */}
|
|
242
253
|
{logo ? (
|
|
243
|
-
|
|
254
|
+
logoHref ? (
|
|
255
|
+
<Link to={logoHref} className="cursor-pointer hover:opacity-80 transition-opacity">
|
|
256
|
+
{logo}
|
|
257
|
+
</Link>
|
|
258
|
+
) : (
|
|
259
|
+
logo
|
|
260
|
+
)
|
|
244
261
|
) : logoUrl ? (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
262
|
+
logoHref ? (
|
|
263
|
+
<Link to={logoHref} className="cursor-pointer hover:opacity-80 transition-opacity">
|
|
264
|
+
<img
|
|
265
|
+
src={logoUrl}
|
|
266
|
+
alt={logoAlt || 'Logo'}
|
|
267
|
+
className="h-[2.15rem] w-auto max-w-[200px] object-contain rounded-md shadow-md bg-transparent"
|
|
268
|
+
/>
|
|
269
|
+
</Link>
|
|
270
|
+
) : (
|
|
271
|
+
<img
|
|
272
|
+
src={logoUrl}
|
|
273
|
+
alt={logoAlt || 'Logo'}
|
|
274
|
+
className="h-[2.15rem] w-auto max-w-[200px] object-contain rounded-md shadow-md bg-transparent"
|
|
275
|
+
/>
|
|
276
|
+
)
|
|
250
277
|
) : (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
278
|
+
logoHref ? (
|
|
279
|
+
<Link to={logoHref} className="cursor-pointer hover:opacity-80 transition-opacity">
|
|
280
|
+
<img
|
|
281
|
+
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23000'/%3E%3Ctext x='16' y='20' text-anchor='middle' fill='white' font-family='Arial' font-size='14' font-weight='bold'%3EL%3C/text%3E%3C/svg%3E"
|
|
282
|
+
alt={logoAlt || 'Logo'}
|
|
283
|
+
className="h-8 w-8 shadow-md"
|
|
284
|
+
/>
|
|
285
|
+
</Link>
|
|
286
|
+
) : (
|
|
287
|
+
<img
|
|
288
|
+
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23000'/%3E%3Ctext x='16' y='20' text-anchor='middle' fill='white' font-family='Arial' font-size='14' font-weight='bold'%3EL%3C/text%3E%3C/svg%3E"
|
|
289
|
+
alt={logoAlt || 'Logo'}
|
|
290
|
+
className="h-8 w-8 shadow-md"
|
|
291
|
+
/>
|
|
292
|
+
)
|
|
256
293
|
)}
|
|
257
294
|
|
|
258
295
|
{/* Navigation Menu */}
|
|
@@ -125,6 +125,8 @@ export interface PaceAppLayoutProps {
|
|
|
125
125
|
headerActions?: React.ReactNode;
|
|
126
126
|
/** Custom logo component (overrides default logo) */
|
|
127
127
|
customLogo?: React.ReactNode;
|
|
128
|
+
/** URL to navigate to when logo is clicked (defaults to '/dashboard') */
|
|
129
|
+
logoHref?: string;
|
|
128
130
|
/** Custom user menu component (overrides default user menu) */
|
|
129
131
|
customUserMenu?: React.ReactNode;
|
|
130
132
|
/** Custom className for the header */
|
|
@@ -323,6 +325,7 @@ export function PaceAppLayout({
|
|
|
323
325
|
showEventSelector,
|
|
324
326
|
headerActions,
|
|
325
327
|
customLogo,
|
|
328
|
+
logoHref = '/dashboard',
|
|
326
329
|
customUserMenu,
|
|
327
330
|
headerClassName,
|
|
328
331
|
showUserMenu = true,
|
|
@@ -709,6 +712,7 @@ export function PaceAppLayout({
|
|
|
709
712
|
logo={customLogo || undefined}
|
|
710
713
|
logoUrl={!customLogo ? `/${appName.toLowerCase()}_logo_wide.svg` : undefined}
|
|
711
714
|
logoAlt={`${appName} Logo`}
|
|
715
|
+
logoHref={logoHref}
|
|
712
716
|
navItems={filteredMenuItems}
|
|
713
717
|
actions={headerActions}
|
|
714
718
|
userMenu={customUserMenu}
|
|
@@ -18,6 +18,7 @@ A comprehensive application layout component that provides a consistent structur
|
|
|
18
18
|
- Branding support
|
|
19
19
|
- **Event selector control** - can be hidden for non-event applications
|
|
20
20
|
- **Header customization** - custom logo, actions, user menu, and styling
|
|
21
|
+
- **Clickable logo** - logo automatically routes to dashboard page (configurable)
|
|
21
22
|
|
|
22
23
|
## Props
|
|
23
24
|
|
|
@@ -28,6 +29,7 @@ A comprehensive application layout component that provides a consistent structur
|
|
|
28
29
|
| `showEventSelector` | `boolean` | `true` | Show/hide event selector in the header |
|
|
29
30
|
| `headerActions` | `React.ReactNode` | optional | Custom actions to display in the header (between event selector and user menu) |
|
|
30
31
|
| `customLogo` | `React.ReactNode` | optional | Custom logo component (overrides default logo) |
|
|
32
|
+
| `logoHref` | `string` | `'/dashboard'` | URL to navigate to when logo is clicked (e.g., '/dashboard', '/home') |
|
|
31
33
|
| `customUserMenu` | `React.ReactNode` | optional | Custom user menu component (overrides default user menu) |
|
|
32
34
|
| `headerClassName` | `string` | optional | Custom className for the header |
|
|
33
35
|
| `showUserMenu` | `boolean` | `true` | Show/hide user menu |
|
|
@@ -101,6 +103,7 @@ function App() {
|
|
|
101
103
|
appName="My Application"
|
|
102
104
|
navItems={navItems}
|
|
103
105
|
customLogo={<CustomLogo />}
|
|
106
|
+
logoHref="/dashboard" // Logo clicks navigate to dashboard
|
|
104
107
|
headerActions={headerActions}
|
|
105
108
|
customUserMenu={<CustomUserMenu />}
|
|
106
109
|
headerClassName="custom-header-styles"
|
|
@@ -116,6 +119,27 @@ function App() {
|
|
|
116
119
|
}
|
|
117
120
|
```
|
|
118
121
|
|
|
122
|
+
### Logo Navigation
|
|
123
|
+
|
|
124
|
+
The logo in the header is automatically clickable and routes to the dashboard page. You can customize the destination:
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
// Default: routes to '/dashboard'
|
|
128
|
+
<PaceAppLayout appName="My App" />
|
|
129
|
+
|
|
130
|
+
// Custom route: routes to '/home'
|
|
131
|
+
<PaceAppLayout
|
|
132
|
+
appName="My App"
|
|
133
|
+
logoHref="/home"
|
|
134
|
+
/>
|
|
135
|
+
|
|
136
|
+
// Event-specific dashboard: routes to current event's dashboard
|
|
137
|
+
<PaceAppLayout
|
|
138
|
+
appName="My App"
|
|
139
|
+
logoHref={`/events/${currentEventId}/dashboard`}
|
|
140
|
+
/>
|
|
141
|
+
```
|
|
142
|
+
|
|
119
143
|
### App Layout Without Event Selector (Perfect for TEAM-style apps)
|
|
120
144
|
|
|
121
145
|
```tsx
|
|
@@ -226,6 +250,12 @@ function App() {
|
|
|
226
250
|
- Apps needing custom header styling
|
|
227
251
|
- Applications with context-specific header content
|
|
228
252
|
|
|
253
|
+
### Logo Navigation
|
|
254
|
+
- The logo automatically routes to the dashboard when clicked (default: `/dashboard`)
|
|
255
|
+
- Customize the destination with the `logoHref` prop
|
|
256
|
+
- Works with both default logo and custom logo components
|
|
257
|
+
- Provides visual feedback with hover opacity transition
|
|
258
|
+
|
|
229
259
|
## Integration with UnifiedAuthProvider
|
|
230
260
|
|
|
231
261
|
The PaceAppLayout works seamlessly with the UnifiedAuthProvider. When `showEventSelector={false}`, the provider will:
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Protected Route Component
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/ProtectedRoute
|
|
5
|
+
* @since 0.6.0
|
|
6
|
+
*
|
|
7
|
+
* A route protection component that handles authentication and optional event selection
|
|
8
|
+
* without creating a chicken-and-egg problem where users cannot see the event selector.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Authentication checking with redirect to login
|
|
12
|
+
* - Session restoration handling
|
|
13
|
+
* - Event loading state management
|
|
14
|
+
* - Smart event selection logic (allows rendering when events exist but none selected)
|
|
15
|
+
* - Optional event requirement (can be disabled for apps that don't need events)
|
|
16
|
+
* - Super admin bypass support
|
|
17
|
+
* - Clear error states for no events available
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* Basic protected route:
|
|
21
|
+
* ```tsx
|
|
22
|
+
* import { ProtectedRoute } from '@jmruthers/pace-core';
|
|
23
|
+
* import { Routes, Route } from 'react-router-dom';
|
|
24
|
+
*
|
|
25
|
+
* function App() {
|
|
26
|
+
* return (
|
|
27
|
+
* <Routes>
|
|
28
|
+
* <Route path="/login" element={<LoginPage />} />
|
|
29
|
+
* <Route element={<ProtectedRoute />}>
|
|
30
|
+
* <Route path="/dashboard" element={<DashboardPage />} />
|
|
31
|
+
* </Route>
|
|
32
|
+
* </Routes>
|
|
33
|
+
* );
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* Protected route without event requirement:
|
|
39
|
+
* ```tsx
|
|
40
|
+
* <Route element={<ProtectedRoute requireEvent={false} />}>
|
|
41
|
+
* <Route path="/settings" element={<SettingsPage />} />
|
|
42
|
+
* </Route>
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* Protected route with custom no events message:
|
|
47
|
+
* ```tsx
|
|
48
|
+
* <Route element={
|
|
49
|
+
* <ProtectedRoute
|
|
50
|
+
* requireEvent={true}
|
|
51
|
+
* noEventsFallback={<CustomNoEventsMessage />}
|
|
52
|
+
* />
|
|
53
|
+
* }>
|
|
54
|
+
* <Route path="/dashboard" element={<DashboardPage />} />
|
|
55
|
+
* </Route>
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @accessibility
|
|
59
|
+
* - Proper loading states with screen reader support
|
|
60
|
+
* - Clear error messages
|
|
61
|
+
* - Keyboard navigation support
|
|
62
|
+
*
|
|
63
|
+
* @dependencies
|
|
64
|
+
* - React Router v6 - Routing functionality
|
|
65
|
+
* - useUnifiedAuth - Authentication context
|
|
66
|
+
* - useEvents - Event context
|
|
67
|
+
* - SessionRestorationLoader - Session restoration UI
|
|
68
|
+
* - LoadingSpinner - Loading state UI
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
import React, { useMemo } from 'react';
|
|
72
|
+
import { Navigate, Outlet } from 'react-router-dom';
|
|
73
|
+
import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
|
|
74
|
+
import { useSessionRestoration } from '../../hooks/useSessionRestoration';
|
|
75
|
+
import { useEvents } from '../../hooks/useEvents';
|
|
76
|
+
import { LoadingSpinner } from '../LoadingSpinner/LoadingSpinner';
|
|
77
|
+
import { SessionRestorationLoader } from '../SessionRestorationLoader';
|
|
78
|
+
import { Alert, AlertDescription, AlertTitle } from '../Alert/Alert';
|
|
79
|
+
|
|
80
|
+
export interface ProtectedRouteProps {
|
|
81
|
+
/**
|
|
82
|
+
* Whether an event is required for routes inside this component.
|
|
83
|
+
* When true, routes will only render if an event is selected or can be selected.
|
|
84
|
+
* When false, routes render regardless of event state.
|
|
85
|
+
* @default true
|
|
86
|
+
*/
|
|
87
|
+
requireEvent?: boolean;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whether super admins can bypass event requirement.
|
|
91
|
+
* Note: This feature requires additional RBAC setup. For simple bypass, set requireEvent={false} instead.
|
|
92
|
+
* @default false
|
|
93
|
+
* @deprecated Use requireEvent={false} for routes that don't need events
|
|
94
|
+
*/
|
|
95
|
+
allowSuperAdminBypass?: boolean;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Custom component to render when no events are available.
|
|
99
|
+
* If not provided, a default message is shown.
|
|
100
|
+
*/
|
|
101
|
+
noEventsFallback?: React.ReactNode;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Custom component to render while events are loading.
|
|
105
|
+
* If not provided, a default loading spinner is shown.
|
|
106
|
+
*/
|
|
107
|
+
loadingFallback?: React.ReactNode;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Login redirect path when user is not authenticated.
|
|
111
|
+
* @default '/login'
|
|
112
|
+
*/
|
|
113
|
+
loginPath?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* ProtectedRoute component that handles authentication and optional event selection.
|
|
118
|
+
*
|
|
119
|
+
* This component solves the chicken-and-egg problem where apps check for `selectedEvent`
|
|
120
|
+
* before rendering, which blocks the event selector (typically in the header) from being visible.
|
|
121
|
+
*
|
|
122
|
+
* Strategy:
|
|
123
|
+
* 1. Check authentication first - redirect to login if not authenticated
|
|
124
|
+
* 2. Allow rendering during event loading - prevents blocking UI
|
|
125
|
+
* 3. If events exist but none selected - allow rendering so selector is visible
|
|
126
|
+
* 4. If no events available - show error message
|
|
127
|
+
* 5. Individual pages should handle "no selected event" state gracefully
|
|
128
|
+
*
|
|
129
|
+
* @param props - Configuration for route protection
|
|
130
|
+
* @returns React element with route protection logic
|
|
131
|
+
*/
|
|
132
|
+
export function ProtectedRoute({
|
|
133
|
+
requireEvent = true,
|
|
134
|
+
allowSuperAdminBypass = false,
|
|
135
|
+
noEventsFallback,
|
|
136
|
+
loadingFallback,
|
|
137
|
+
loginPath = '/login'
|
|
138
|
+
}: ProtectedRouteProps) {
|
|
139
|
+
const { isAuthenticated, isLoading } = useUnifiedAuth();
|
|
140
|
+
const { selectedEvent, events, isLoading: eventLoading } = useEvents();
|
|
141
|
+
const sessionRestoration = useSessionRestoration();
|
|
142
|
+
|
|
143
|
+
const isRestoringSession = useMemo(() => {
|
|
144
|
+
return sessionRestoration.isRestoring &&
|
|
145
|
+
!sessionRestoration.restorationComplete &&
|
|
146
|
+
!sessionRestoration.restorationError &&
|
|
147
|
+
!sessionRestoration.hasTimedOut;
|
|
148
|
+
}, [
|
|
149
|
+
sessionRestoration.isRestoring,
|
|
150
|
+
sessionRestoration.restorationComplete,
|
|
151
|
+
sessionRestoration.restorationError,
|
|
152
|
+
sessionRestoration.hasTimedOut
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
// Show session restoration loader during restoration
|
|
156
|
+
if (isRestoringSession) {
|
|
157
|
+
return <SessionRestorationLoader />;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Show loading state while auth is being determined
|
|
161
|
+
if (isLoading && !sessionRestoration.hasTimedOut) {
|
|
162
|
+
return loadingFallback || (
|
|
163
|
+
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
|
164
|
+
<LoadingSpinner />
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Redirect to login if not authenticated
|
|
170
|
+
if (!isAuthenticated) {
|
|
171
|
+
if (sessionRestoration.hasTimedOut || sessionRestoration.restorationError) {
|
|
172
|
+
console.warn('[ProtectedRoute] Session restoration failed, redirecting to login', {
|
|
173
|
+
timedOut: sessionRestoration.hasTimedOut,
|
|
174
|
+
error: sessionRestoration.restorationError?.message
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return <Navigate to={loginPath} replace />;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// If event is not required, allow rendering
|
|
181
|
+
if (!requireEvent) {
|
|
182
|
+
return <Outlet />;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Note: Super admin bypass would require useRBAC hook which adds complexity
|
|
186
|
+
// Apps that need super admin access without events should set requireEvent={false}
|
|
187
|
+
// For now, we keep it simple and always require events when requireEvent=true
|
|
188
|
+
|
|
189
|
+
// Allow rendering during event loading - prevents blocking UI while events load
|
|
190
|
+
if (eventLoading) {
|
|
191
|
+
return <Outlet />;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// If no events are available, show error message
|
|
195
|
+
if (!events || events.length === 0) {
|
|
196
|
+
return noEventsFallback || (
|
|
197
|
+
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', padding: '2rem' }}>
|
|
198
|
+
<Alert variant="destructive" className="max-w-md">
|
|
199
|
+
<AlertTitle>No Events Available</AlertTitle>
|
|
200
|
+
<AlertDescription>
|
|
201
|
+
You don't have access to any events. Please contact your administrator if you believe this is an error.
|
|
202
|
+
</AlertDescription>
|
|
203
|
+
</Alert>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// KEY FIX: Allow rendering when events exist but none selected
|
|
209
|
+
// This allows the event selector (typically in PaceAppLayout header) to be visible
|
|
210
|
+
// Individual pages should handle "no selected event" state gracefully
|
|
211
|
+
// Auto-selection will handle selecting the next event, or user can manually select
|
|
212
|
+
|
|
213
|
+
// If no event selected but events exist, allow rendering
|
|
214
|
+
// The event selector will be visible and user can select, or auto-selection will kick in
|
|
215
|
+
if (!selectedEvent) {
|
|
216
|
+
// Log for debugging - this is expected behavior, not an error
|
|
217
|
+
console.debug('[ProtectedRoute] Events available but none selected - allowing render so selector is visible');
|
|
218
|
+
return <Outlet />;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Event is selected - allow rendering
|
|
222
|
+
return <Outlet />;
|
|
223
|
+
}
|
|
224
|
+
|
package/src/components/index.ts
CHANGED
|
@@ -180,9 +180,12 @@ export type { FooterProps } from './Footer';
|
|
|
180
180
|
export * from './PublicLayout';
|
|
181
181
|
|
|
182
182
|
// ============================================================================
|
|
183
|
-
// SECURITY COMPONENTS
|
|
183
|
+
// SECURITY COMPONENTS
|
|
184
184
|
// ============================================================================
|
|
185
185
|
|
|
186
|
+
export { ProtectedRoute } from './ProtectedRoute';
|
|
187
|
+
export type { ProtectedRouteProps } from './ProtectedRoute';
|
|
188
|
+
|
|
186
189
|
// ============================================================================
|
|
187
190
|
// NAVIGATION COMPONENTS
|
|
188
191
|
// ============================================================================
|
package/src/index.ts
CHANGED
|
@@ -166,6 +166,9 @@ export type { PaceAppLayoutProps } from './components/PaceAppLayout/PaceAppLayou
|
|
|
166
166
|
export { PaceLoginPage } from './components/PaceLoginPage/PaceLoginPage';
|
|
167
167
|
export type { PaceLoginPageProps } from './components/PaceLoginPage/PaceLoginPage';
|
|
168
168
|
|
|
169
|
+
export { ProtectedRoute } from './components/ProtectedRoute/ProtectedRoute';
|
|
170
|
+
export type { ProtectedRouteProps } from './components/ProtectedRoute/ProtectedRoute';
|
|
171
|
+
|
|
169
172
|
// UTILITY COMPONENTS
|
|
170
173
|
export { ErrorBoundary } from './components/ErrorBoundary/ErrorBoundary';
|
|
171
174
|
export { LoadingSpinner } from './components/LoadingSpinner/LoadingSpinner';
|
|
@@ -55,8 +55,10 @@ export interface Event {
|
|
|
55
55
|
id: string;
|
|
56
56
|
event_id: string;
|
|
57
57
|
name: string;
|
|
58
|
+
event_name?: string;
|
|
58
59
|
organisation_id: UUID;
|
|
59
|
-
|
|
60
|
+
event_date?: string; // Date from database for sorting
|
|
61
|
+
start_date?: string; // Alias for event_date
|
|
60
62
|
end_date?: string;
|
|
61
63
|
status?: string;
|
|
62
64
|
settings?: Record<string, any>;
|
|
@@ -347,13 +349,59 @@ export function AuthProvider({
|
|
|
347
349
|
const orgEvents = (data || []).filter(e => e.organisation_id === selectedOrganisation.id);
|
|
348
350
|
setEvents(orgEvents);
|
|
349
351
|
|
|
350
|
-
// Auto-select
|
|
352
|
+
// Auto-select next event in the future by date if none selected
|
|
351
353
|
if (!selectedEvent && orgEvents.length > 0) {
|
|
352
354
|
const savedEventId = persistState ? localStorage.getItem(`pace_${appName}_event`) : null;
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
355
|
+
let eventToSelect: Event | null = null;
|
|
356
|
+
|
|
357
|
+
if (savedEventId) {
|
|
358
|
+
// Try to restore persisted event
|
|
359
|
+
eventToSelect = orgEvents.find(e => e.event_id === savedEventId) || null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// If no persisted event, select the next event in the future by date
|
|
363
|
+
if (!eventToSelect) {
|
|
364
|
+
const now = new Date();
|
|
365
|
+
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
366
|
+
const futureEvents = orgEvents
|
|
367
|
+
.filter((e): e is Event & { event_date: string } => {
|
|
368
|
+
if (!e.event_date) return false;
|
|
369
|
+
const eventDate = new Date(e.event_date);
|
|
370
|
+
const startOfEventDate = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()).getTime();
|
|
371
|
+
return startOfEventDate >= startOfToday;
|
|
372
|
+
})
|
|
373
|
+
.sort((a, b) => {
|
|
374
|
+
const dateA = new Date(a.event_date);
|
|
375
|
+
const dateB = new Date(b.event_date);
|
|
376
|
+
return dateA.getTime() - dateB.getTime();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
eventToSelect = futureEvents.length > 0 ? futureEvents[0] : null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Fallback to most recent past event if no future events found
|
|
383
|
+
if (!eventToSelect && orgEvents.length > 0) {
|
|
384
|
+
const now = new Date();
|
|
385
|
+
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
386
|
+
const pastEvents = orgEvents
|
|
387
|
+
.filter((e): e is Event & { event_date: string } => {
|
|
388
|
+
if (!e.event_date) return false;
|
|
389
|
+
const eventDate = new Date(e.event_date);
|
|
390
|
+
const startOfEventDate = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()).getTime();
|
|
391
|
+
return startOfEventDate < startOfToday;
|
|
392
|
+
})
|
|
393
|
+
.sort((a, b) => {
|
|
394
|
+
const dateA = new Date(a.event_date);
|
|
395
|
+
const dateB = new Date(b.event_date);
|
|
396
|
+
return dateB.getTime() - dateA.getTime(); // Descending order (most recent first)
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
eventToSelect = pastEvents.length > 0 ? pastEvents[0] : orgEvents[0];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (eventToSelect) {
|
|
403
|
+
setSelectedEvent(eventToSelect);
|
|
404
|
+
}
|
|
357
405
|
}
|
|
358
406
|
} catch (error) {
|
|
359
407
|
console.error('[AuthProvider] Failed to load events:', error);
|
|
@@ -759,14 +807,60 @@ export function AuthProvider({
|
|
|
759
807
|
const orgEvents = (data || []).filter(e => e.organisation_id === selectedOrganisation.id);
|
|
760
808
|
setEvents(orgEvents);
|
|
761
809
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
810
|
+
// Auto-select next event in the future by date if none selected
|
|
811
|
+
if (!selectedEvent && orgEvents.length > 0) {
|
|
812
|
+
const savedEventId = persistState ? localStorage.getItem(`pace_${appName}_event`) : null;
|
|
813
|
+
let eventToSelect: Event | null = null;
|
|
814
|
+
|
|
815
|
+
if (savedEventId) {
|
|
816
|
+
// Try to restore persisted event
|
|
817
|
+
eventToSelect = orgEvents.find(e => e.event_id === savedEventId) || null;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// If no persisted event, select the next event in the future by date
|
|
821
|
+
if (!eventToSelect) {
|
|
822
|
+
const now = new Date();
|
|
823
|
+
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
824
|
+
const futureEvents = orgEvents
|
|
825
|
+
.filter((e): e is Event & { event_date: string } => {
|
|
826
|
+
if (!e.event_date) return false;
|
|
827
|
+
const eventDate = new Date(e.event_date);
|
|
828
|
+
const startOfEventDate = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()).getTime();
|
|
829
|
+
return startOfEventDate >= startOfToday;
|
|
830
|
+
})
|
|
831
|
+
.sort((a, b) => {
|
|
832
|
+
const dateA = new Date(a.event_date);
|
|
833
|
+
const dateB = new Date(b.event_date);
|
|
834
|
+
return dateA.getTime() - dateB.getTime();
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
eventToSelect = futureEvents.length > 0 ? futureEvents[0] : null;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Fallback to most recent past event if no future events found
|
|
841
|
+
if (!eventToSelect && orgEvents.length > 0) {
|
|
842
|
+
const now = new Date();
|
|
843
|
+
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
844
|
+
const pastEvents = orgEvents
|
|
845
|
+
.filter((e): e is Event & { event_date: string } => {
|
|
846
|
+
if (!e.event_date) return false;
|
|
847
|
+
const eventDate = new Date(e.event_date);
|
|
848
|
+
const startOfEventDate = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()).getTime();
|
|
849
|
+
return startOfEventDate < startOfToday;
|
|
850
|
+
})
|
|
851
|
+
.sort((a, b) => {
|
|
852
|
+
const dateA = new Date(a.event_date);
|
|
853
|
+
const dateB = new Date(b.event_date);
|
|
854
|
+
return dateB.getTime() - dateA.getTime(); // Descending order (most recent first)
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
eventToSelect = pastEvents.length > 0 ? pastEvents[0] : orgEvents[0];
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (eventToSelect) {
|
|
861
|
+
setSelectedEvent(eventToSelect);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
770
864
|
} catch (error) {
|
|
771
865
|
console.error('[AuthProvider] Failed to refresh events:', error);
|
|
772
866
|
setEventError(error as Error);
|
|
@@ -409,11 +409,16 @@ export class EventService extends BaseService implements IEventService {
|
|
|
409
409
|
return null;
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
// Get start of today (midnight) to compare dates only (ignore time)
|
|
412
413
|
const now = new Date();
|
|
414
|
+
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
415
|
+
|
|
413
416
|
const futureEvents = eventsToUse.filter(event => {
|
|
414
417
|
if (!event.event_date) return false;
|
|
415
418
|
const eventDate = new Date(event.event_date);
|
|
416
|
-
|
|
419
|
+
// Compare by date only (start of day), not by time
|
|
420
|
+
const startOfEventDate = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()).getTime();
|
|
421
|
+
return startOfEventDate >= startOfToday;
|
|
417
422
|
});
|
|
418
423
|
|
|
419
424
|
if (futureEvents.length === 0) {
|