@jmruthers/pace-core 0.5.91 → 0.5.93
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-Dx5c2g3S.d.ts → PublicLoadingSpinner-n74JgA9h.d.ts} +50 -1
- package/dist/{UnifiedAuthProvider-6JRTOFPS.js → UnifiedAuthProvider-ZM7VUC45.js} +3 -3
- 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-VJJNZKHO.js → chunk-SVMPR5IV.js} +365 -293
- package/dist/chunk-SVMPR5IV.js.map +1 -0
- 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 +1 -1
- 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 +41 -5
- package/docs/api-reference/components.md +146 -1
- package/docs/best-practices/common-patterns.md +26 -8
- package/docs/getting-started/examples/README.md +10 -26
- package/docs/getting-started/quick-reference.md +23 -0
- package/docs/implementation-guides/authentication.md +39 -16
- package/package.json +1 -1
- package/src/components/EventSelector/EventSelector.tsx +19 -1
- package/src/components/ProtectedRoute/ProtectedRoute.tsx +224 -0
- package/src/components/ProtectedRoute/README.md +164 -0
- package/src/components/ProtectedRoute/index.ts +3 -0
- package/src/components/PublicLayout/EventLogo.tsx +8 -2
- package/src/components/PublicLayout/PublicPageHeader.tsx +7 -12
- package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +6 -3
- 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-VJJNZKHO.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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# ProtectedRoute Component
|
|
2
|
+
|
|
3
|
+
A route protection component that handles authentication and optional event selection without creating blocking issues.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Authentication checking** - Automatically redirects to login if user is not authenticated
|
|
8
|
+
- ✅ **Session restoration** - Handles session restoration states automatically
|
|
9
|
+
- ✅ **Event loading management** - Allows rendering during event loading (prevents UI blocking)
|
|
10
|
+
- ✅ **Event selector visibility** - Allows rendering when events exist but none selected (so event selector in header is visible)
|
|
11
|
+
- ✅ **Clear error states** - Shows helpful messages when no events are available
|
|
12
|
+
- ✅ **Flexible configuration** - Optional event requirement, customizable fallbacks
|
|
13
|
+
|
|
14
|
+
## Problem It Solves
|
|
15
|
+
|
|
16
|
+
This component solves the **chicken-and-egg problem** where apps check for `selectedEvent` before rendering, which blocks the event selector dropdown (typically in `PaceAppLayout` header) from being visible. This creates a deadlock where users cannot select an event.
|
|
17
|
+
|
|
18
|
+
**Before (Problem):**
|
|
19
|
+
```tsx
|
|
20
|
+
// ❌ This blocks rendering, preventing event selector from being visible
|
|
21
|
+
function ProtectedRoute({ children }) {
|
|
22
|
+
const { selectedEvent } = useEvents();
|
|
23
|
+
|
|
24
|
+
if (!selectedEvent) {
|
|
25
|
+
return <div>No event selected. Please select an event from the dropdown above.</div>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return <>{children}</>;
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**After (Solution):**
|
|
33
|
+
```tsx
|
|
34
|
+
// ✅ ProtectedRoute allows rendering when events exist but none selected
|
|
35
|
+
// Event selector in header is now visible and usable
|
|
36
|
+
import { ProtectedRoute } from '@jmruthers/pace-core';
|
|
37
|
+
|
|
38
|
+
<Route element={<ProtectedRoute />}>
|
|
39
|
+
<Route path="/dashboard" element={<DashboardPage />} />
|
|
40
|
+
</Route>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
### Basic Usage
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { ProtectedRoute } from '@jmruthers/pace-core';
|
|
49
|
+
import { Routes, Route } from 'react-router-dom';
|
|
50
|
+
|
|
51
|
+
function App() {
|
|
52
|
+
return (
|
|
53
|
+
<Routes>
|
|
54
|
+
<Route path="/login" element={<LoginPage />} />
|
|
55
|
+
<Route element={<ProtectedRoute />}>
|
|
56
|
+
<Route path="/dashboard" element={<DashboardPage />} />
|
|
57
|
+
<Route path="/events" element={<EventsPage />} />
|
|
58
|
+
</Route>
|
|
59
|
+
</Routes>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Without Event Requirement
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
<Route element={<ProtectedRoute requireEvent={false} />}>
|
|
68
|
+
<Route path="/settings" element={<SettingsPage />} />
|
|
69
|
+
</Route>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Custom Fallbacks
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
<Route element={
|
|
76
|
+
<ProtectedRoute
|
|
77
|
+
requireEvent={true}
|
|
78
|
+
loginPath="/login"
|
|
79
|
+
loadingFallback={<CustomLoader />}
|
|
80
|
+
noEventsFallback={
|
|
81
|
+
<div>
|
|
82
|
+
<h2>No Events Available</h2>
|
|
83
|
+
<p>Contact your administrator for access.</p>
|
|
84
|
+
</div>
|
|
85
|
+
}
|
|
86
|
+
/>
|
|
87
|
+
}>
|
|
88
|
+
<Route path="/dashboard" element={<DashboardPage />} />
|
|
89
|
+
</Route>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Props
|
|
93
|
+
|
|
94
|
+
| Prop | Type | Default | Description |
|
|
95
|
+
|------|------|---------|-------------|
|
|
96
|
+
| `requireEvent` | `boolean` | `true` | Whether an event is required for routes inside this component |
|
|
97
|
+
| `allowSuperAdminBypass` | `boolean` | `false` | Whether super admins can bypass event requirement (deprecated) |
|
|
98
|
+
| `noEventsFallback` | `ReactNode` | - | Custom component to render when no events are available |
|
|
99
|
+
| `loadingFallback` | `ReactNode` | - | Custom component to render while events are loading |
|
|
100
|
+
| `loginPath` | `string` | `'/login'` | Login redirect path when user is not authenticated |
|
|
101
|
+
|
|
102
|
+
## Behavior
|
|
103
|
+
|
|
104
|
+
The component follows this logic flow:
|
|
105
|
+
|
|
106
|
+
1. **Session Restoration** - Shows loader during session restoration
|
|
107
|
+
2. **Authentication Check** - Redirects to login if not authenticated
|
|
108
|
+
3. **Event Requirement Check** - If `requireEvent={false}`, allows rendering
|
|
109
|
+
4. **Event Loading** - Allows rendering during event loading (prevents UI blocking)
|
|
110
|
+
5. **No Events Available** - Shows error message if no events exist
|
|
111
|
+
6. **Events Exist, None Selected** - **KEY FIX**: Allows rendering so event selector is visible
|
|
112
|
+
7. **Event Selected** - Allows rendering normally
|
|
113
|
+
|
|
114
|
+
## Integration with PaceAppLayout
|
|
115
|
+
|
|
116
|
+
The `ProtectedRoute` component is designed to work seamlessly with `PaceAppLayout`, which contains the event selector dropdown in its header:
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
<Route element={<ProtectedRoute />}>
|
|
120
|
+
<Route element={<PaceAppLayout appName="My App" />}>
|
|
121
|
+
<Route path="/" element={<DashboardPage />} />
|
|
122
|
+
</Route>
|
|
123
|
+
</Route>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
When events exist but none is selected, `ProtectedRoute` allows rendering, making the `PaceAppLayout` header (and its event selector) visible. Users can then:
|
|
127
|
+
- See the event selector dropdown
|
|
128
|
+
- Manually select an event
|
|
129
|
+
- Or let auto-selection work when events finish loading
|
|
130
|
+
|
|
131
|
+
## Migration from Custom ProtectedRoute
|
|
132
|
+
|
|
133
|
+
If you have a custom `ProtectedRoute` that blocks rendering when no event is selected:
|
|
134
|
+
|
|
135
|
+
**Before:**
|
|
136
|
+
```tsx
|
|
137
|
+
function ProtectedRoute({ children }) {
|
|
138
|
+
const { isAuthenticated } = useUnifiedAuth();
|
|
139
|
+
const { selectedEvent } = useEvents();
|
|
140
|
+
|
|
141
|
+
if (!isAuthenticated) return <Navigate to="/login" />;
|
|
142
|
+
if (!selectedEvent) return <div>Please select an event</div>; // ❌ Blocks UI
|
|
143
|
+
|
|
144
|
+
return <>{children}</>;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**After:**
|
|
149
|
+
```tsx
|
|
150
|
+
import { ProtectedRoute } from '@jmruthers/pace-core';
|
|
151
|
+
|
|
152
|
+
// Simply replace your custom component
|
|
153
|
+
<Route element={<ProtectedRoute />}>
|
|
154
|
+
{/* Your routes */}
|
|
155
|
+
</Route>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Related Documentation
|
|
159
|
+
|
|
160
|
+
- [Authentication Implementation Guide](../../../../docs/implementation-guides/authentication.md#protected-routes)
|
|
161
|
+
- [Common Patterns](../../../../docs/best-practices/common-patterns.md#protected-routes)
|
|
162
|
+
- [API Reference](../../../../docs/api-reference/components.md#protectedroute)
|
|
163
|
+
- [Component Props Interface](../../../../docs/api/interfaces/ProtectedRouteProps.md)
|
|
164
|
+
|
|
@@ -223,11 +223,14 @@ function EventLogoPublic({
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
// Render the actual logo
|
|
226
|
+
// Apply object-contain to maintain aspect ratio and rounded to match fallback styling
|
|
227
|
+
const imageClasses = `${sizeClass} object-contain rounded ${className}`.trim();
|
|
228
|
+
|
|
226
229
|
return (
|
|
227
230
|
<img
|
|
228
231
|
src={logoUrl}
|
|
229
232
|
alt={`${eventName} logo`}
|
|
230
|
-
className={
|
|
233
|
+
className={imageClasses}
|
|
231
234
|
onError={(e) => {
|
|
232
235
|
// If image fails to load, hide it and show fallback
|
|
233
236
|
const target = e.target as HTMLImageElement;
|
|
@@ -357,11 +360,14 @@ function EventLogoAuthenticated({
|
|
|
357
360
|
}
|
|
358
361
|
|
|
359
362
|
// Render the actual logo
|
|
363
|
+
// Apply object-contain to maintain aspect ratio and rounded to match fallback styling
|
|
364
|
+
const imageClasses = `${sizeClass} object-contain rounded ${className}`.trim();
|
|
365
|
+
|
|
360
366
|
return (
|
|
361
367
|
<img
|
|
362
368
|
src={logoUrl}
|
|
363
369
|
alt={`${eventName} logo`}
|
|
364
|
-
className={
|
|
370
|
+
className={imageClasses}
|
|
365
371
|
onError={(e) => {
|
|
366
372
|
// If image fails to load, hide it and show fallback
|
|
367
373
|
const target = e.target as HTMLImageElement;
|
|
@@ -106,16 +106,11 @@ export function PublicPageHeader({
|
|
|
106
106
|
{showAppLogo && (
|
|
107
107
|
<div className="flex-shrink-0">
|
|
108
108
|
{customAppLogo || (
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
/>
|
|
115
|
-
<span className="ml-2 text-lg font-semibold text-gray-900">
|
|
116
|
-
{appName}
|
|
117
|
-
</span>
|
|
118
|
-
</div>
|
|
109
|
+
<img
|
|
110
|
+
className="h-8 w-8 object-contain"
|
|
111
|
+
src={`/${appName.toLowerCase()}_logo_square.svg`}
|
|
112
|
+
alt={appName}
|
|
113
|
+
/>
|
|
119
114
|
)}
|
|
120
115
|
</div>
|
|
121
116
|
)}
|
|
@@ -128,8 +123,8 @@ export function PublicPageHeader({
|
|
|
128
123
|
eventId={event.event_id}
|
|
129
124
|
eventName={event.event_name}
|
|
130
125
|
organisationId={event.organisation_id}
|
|
131
|
-
size="
|
|
132
|
-
|
|
126
|
+
size="lg"
|
|
127
|
+
validateImage={false}
|
|
133
128
|
/>
|
|
134
129
|
)}
|
|
135
130
|
</div>
|
|
@@ -104,8 +104,10 @@ describe('[component] PublicPageHeader', () => {
|
|
|
104
104
|
<PublicPageHeader event={mockEvent} eventCode="EVENT123" />
|
|
105
105
|
);
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
expect(
|
|
107
|
+
const appLogo = screen.getByAltText('Test App');
|
|
108
|
+
expect(appLogo).toBeInTheDocument();
|
|
109
|
+
// App name text was removed - only logo image is displayed
|
|
110
|
+
expect(appLogo).toHaveAttribute('src', '/test app_logo_square.svg');
|
|
109
111
|
});
|
|
110
112
|
|
|
111
113
|
it('hides app logo when showAppLogo is false', () => {
|
|
@@ -179,7 +181,8 @@ describe('[component] PublicPageHeader', () => {
|
|
|
179
181
|
expect(eventLogo).toHaveAttribute('data-event-id', 'event-123');
|
|
180
182
|
expect(eventLogo).toHaveAttribute('data-event-name', 'Test Event');
|
|
181
183
|
expect(eventLogo).toHaveAttribute('data-organisation-id', 'org-123');
|
|
182
|
-
|
|
184
|
+
// Size was changed from 'md' to 'lg' for better visibility
|
|
185
|
+
expect(eventLogo).toHaveAttribute('data-size', 'lg');
|
|
183
186
|
});
|
|
184
187
|
});
|
|
185
188
|
|
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) {
|