@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.
Files changed (159) hide show
  1. package/dist/{DataTable-VIP44OB6.js → DataTable-HC5S4RKB.js} +6 -6
  2. package/dist/{PublicLoadingSpinner-Dx5c2g3S.d.ts → PublicLoadingSpinner-n74JgA9h.d.ts} +50 -1
  3. package/dist/{UnifiedAuthProvider-6JRTOFPS.js → UnifiedAuthProvider-ZM7VUC45.js} +3 -3
  4. package/dist/{chunk-G2SCPUKC.js → chunk-6WFM22A4.js} +2 -2
  5. package/dist/{chunk-AIV3VYBQ.js → chunk-AAM57AEU.js} +4 -2
  6. package/dist/chunk-AAM57AEU.js.map +1 -0
  7. package/dist/{chunk-7XBW2P7B.js → chunk-AYC2P377.js} +2 -2
  8. package/dist/{chunk-GD3ENUKD.js → chunk-AZ2QJYKU.js} +3 -3
  9. package/dist/{chunk-G2YT64FA.js → chunk-GP3HU6WS.js} +3 -3
  10. package/dist/{chunk-7NIERLC6.js → chunk-HW5BGOWB.js} +4 -4
  11. package/dist/{chunk-JDPFQV3V.js → chunk-M52CQP5W.js} +4 -4
  12. package/dist/{chunk-JQWSAYZC.js → chunk-OXFOS62D.js} +2 -2
  13. package/dist/{chunk-VJJNZKHO.js → chunk-SVMPR5IV.js} +365 -293
  14. package/dist/chunk-SVMPR5IV.js.map +1 -0
  15. package/dist/{chunk-4DYK5KCK.js → chunk-TZXYSZT3.js} +4 -4
  16. package/dist/{chunk-XZHZYSAK.js → chunk-XIBSVWJW.js} +5 -5
  17. package/dist/components.d.ts +1 -1
  18. package/dist/components.js +10 -8
  19. package/dist/components.js.map +1 -1
  20. package/dist/hooks.js +7 -7
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +13 -11
  23. package/dist/index.js.map +1 -1
  24. package/dist/providers.js +2 -2
  25. package/dist/rbac/index.js +7 -7
  26. package/dist/utils.js +1 -1
  27. package/docs/api/classes/ColumnFactory.md +1 -1
  28. package/docs/api/classes/ErrorBoundary.md +1 -1
  29. package/docs/api/classes/InvalidScopeError.md +1 -1
  30. package/docs/api/classes/MissingUserContextError.md +1 -1
  31. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  32. package/docs/api/classes/PermissionDeniedError.md +1 -1
  33. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  34. package/docs/api/classes/RBACAuditManager.md +1 -1
  35. package/docs/api/classes/RBACCache.md +1 -1
  36. package/docs/api/classes/RBACEngine.md +1 -1
  37. package/docs/api/classes/RBACError.md +1 -1
  38. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  39. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  40. package/docs/api/classes/StorageUtils.md +1 -1
  41. package/docs/api/enums/FileCategory.md +1 -1
  42. package/docs/api/interfaces/AggregateConfig.md +1 -1
  43. package/docs/api/interfaces/ButtonProps.md +1 -1
  44. package/docs/api/interfaces/CardProps.md +1 -1
  45. package/docs/api/interfaces/ColorPalette.md +1 -1
  46. package/docs/api/interfaces/ColorShade.md +1 -1
  47. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  48. package/docs/api/interfaces/DataRecord.md +1 -1
  49. package/docs/api/interfaces/DataTableAction.md +1 -1
  50. package/docs/api/interfaces/DataTableColumn.md +1 -1
  51. package/docs/api/interfaces/DataTableProps.md +1 -1
  52. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  53. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  54. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  55. package/docs/api/interfaces/EventLogoProps.md +1 -1
  56. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  57. package/docs/api/interfaces/FileMetadata.md +1 -1
  58. package/docs/api/interfaces/FileReference.md +1 -1
  59. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  60. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  61. package/docs/api/interfaces/FileUploadProps.md +1 -1
  62. package/docs/api/interfaces/FooterProps.md +1 -1
  63. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  64. package/docs/api/interfaces/InputProps.md +1 -1
  65. package/docs/api/interfaces/LabelProps.md +1 -1
  66. package/docs/api/interfaces/LoginFormProps.md +1 -1
  67. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  68. package/docs/api/interfaces/NavigationContextType.md +1 -1
  69. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  70. package/docs/api/interfaces/NavigationItem.md +1 -1
  71. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  72. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  73. package/docs/api/interfaces/Organisation.md +1 -1
  74. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  75. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  76. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  77. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  78. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  79. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  80. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  81. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  82. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  83. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  84. package/docs/api/interfaces/PaletteData.md +1 -1
  85. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  86. package/docs/api/interfaces/ProtectedRouteProps.md +97 -0
  87. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  88. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  89. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  90. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  91. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  92. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  93. package/docs/api/interfaces/RBACConfig.md +1 -1
  94. package/docs/api/interfaces/RBACLogger.md +1 -1
  95. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  96. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  97. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  98. package/docs/api/interfaces/RouteConfig.md +1 -1
  99. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  100. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  101. package/docs/api/interfaces/StorageConfig.md +1 -1
  102. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  103. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  104. package/docs/api/interfaces/StorageListOptions.md +1 -1
  105. package/docs/api/interfaces/StorageListResult.md +1 -1
  106. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  107. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  108. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  109. package/docs/api/interfaces/StyleImport.md +1 -1
  110. package/docs/api/interfaces/SwitchProps.md +1 -1
  111. package/docs/api/interfaces/ToastActionElement.md +1 -1
  112. package/docs/api/interfaces/ToastProps.md +1 -1
  113. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  114. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  115. package/docs/api/interfaces/UseEventLogoOptions.md +1 -1
  116. package/docs/api/interfaces/UseEventLogoReturn.md +1 -1
  117. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  118. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  119. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  120. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  121. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  122. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  123. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  124. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  125. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  126. package/docs/api/interfaces/UserEventAccess.md +1 -1
  127. package/docs/api/interfaces/UserMenuProps.md +1 -1
  128. package/docs/api/interfaces/UserProfile.md +1 -1
  129. package/docs/api/modules.md +41 -5
  130. package/docs/api-reference/components.md +146 -1
  131. package/docs/best-practices/common-patterns.md +26 -8
  132. package/docs/getting-started/examples/README.md +10 -26
  133. package/docs/getting-started/quick-reference.md +23 -0
  134. package/docs/implementation-guides/authentication.md +39 -16
  135. package/package.json +1 -1
  136. package/src/components/EventSelector/EventSelector.tsx +19 -1
  137. package/src/components/ProtectedRoute/ProtectedRoute.tsx +224 -0
  138. package/src/components/ProtectedRoute/README.md +164 -0
  139. package/src/components/ProtectedRoute/index.ts +3 -0
  140. package/src/components/PublicLayout/EventLogo.tsx +8 -2
  141. package/src/components/PublicLayout/PublicPageHeader.tsx +7 -12
  142. package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +6 -3
  143. package/src/components/index.ts +4 -1
  144. package/src/index.ts +3 -0
  145. package/src/providers/AuthProvider.simplified.tsx +108 -14
  146. package/src/services/EventService.ts +6 -1
  147. package/dist/chunk-AIV3VYBQ.js.map +0 -1
  148. package/dist/chunk-VJJNZKHO.js.map +0 -1
  149. /package/dist/{DataTable-VIP44OB6.js.map → DataTable-HC5S4RKB.js.map} +0 -0
  150. /package/dist/{UnifiedAuthProvider-6JRTOFPS.js.map → UnifiedAuthProvider-ZM7VUC45.js.map} +0 -0
  151. /package/dist/{chunk-G2SCPUKC.js.map → chunk-6WFM22A4.js.map} +0 -0
  152. /package/dist/{chunk-7XBW2P7B.js.map → chunk-AYC2P377.js.map} +0 -0
  153. /package/dist/{chunk-GD3ENUKD.js.map → chunk-AZ2QJYKU.js.map} +0 -0
  154. /package/dist/{chunk-G2YT64FA.js.map → chunk-GP3HU6WS.js.map} +0 -0
  155. /package/dist/{chunk-7NIERLC6.js.map → chunk-HW5BGOWB.js.map} +0 -0
  156. /package/dist/{chunk-JDPFQV3V.js.map → chunk-M52CQP5W.js.map} +0 -0
  157. /package/dist/{chunk-JQWSAYZC.js.map → chunk-OXFOS62D.js.map} +0 -0
  158. /package/dist/{chunk-4DYK5KCK.js.map → chunk-TZXYSZT3.js.map} +0 -0
  159. /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
+
@@ -0,0 +1,3 @@
1
+ export { ProtectedRoute } from './ProtectedRoute';
2
+ export type { ProtectedRouteProps } from './ProtectedRoute';
3
+
@@ -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={`${sizeClass} ${className}`.trim()}
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={`${sizeClass} ${className}`.trim()}
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
- <div className="flex items-center">
110
- <img
111
- className="h-8 w-auto"
112
- src={`/${appName.toLowerCase()}_logo_wide.svg`}
113
- alt={appName}
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="md"
132
- className="h-12 w-12"
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
- expect(screen.getByAltText('Test App')).toBeInTheDocument();
108
- expect(screen.getByText('Test App')).toBeInTheDocument();
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
- expect(eventLogo).toHaveAttribute('data-size', 'md');
184
+ // Size was changed from 'md' to 'lg' for better visibility
185
+ expect(eventLogo).toHaveAttribute('data-size', 'lg');
183
186
  });
184
187
  });
185
188
 
@@ -180,9 +180,12 @@ export type { FooterProps } from './Footer';
180
180
  export * from './PublicLayout';
181
181
 
182
182
  // ============================================================================
183
- // SECURITY COMPONENTS (none currently exported)
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
- start_date?: string;
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 first event if none selected
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
- const eventToSelect = savedEventId
354
- ? orgEvents.find(e => e.event_id === savedEventId) || orgEvents[0]
355
- : orgEvents[0];
356
- setSelectedEvent(eventToSelect);
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
- // Auto-select first event if none selected
763
- if (!selectedEvent && orgEvents.length > 0) {
764
- const savedEventId = persistState ? localStorage.getItem(`pace_${appName}_event`) : null;
765
- const eventToSelect = savedEventId
766
- ? orgEvents.find(e => e.event_id === savedEventId) || orgEvents[0]
767
- : orgEvents[0];
768
- setSelectedEvent(eventToSelect);
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
- return eventDate >= now;
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) {