@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
@@ -1212,7 +1212,152 @@ import { Label } from '@jmruthers/pace-core';
1212
1212
 
1213
1213
  ## Security Components
1214
1214
 
1215
- Currently, security-sensitive UI should be enforced via RBAC hooks and server-side policies. No dedicated Super Admin UI guard is exported.
1215
+ ### ProtectedRoute
1216
+
1217
+ A route protection component that handles authentication and optional event selection without creating blocking issues. This component solves the common chicken-and-egg problem where apps check for `selectedEvent` before rendering, which blocks the event selector from being visible.
1218
+
1219
+ #### Props
1220
+
1221
+ ```typescript
1222
+ interface ProtectedRouteProps {
1223
+ /**
1224
+ * Whether an event is required for routes inside this component.
1225
+ * When true, routes will only render if an event is selected or can be selected.
1226
+ * When false, routes render regardless of event state.
1227
+ * @default true
1228
+ */
1229
+ requireEvent?: boolean;
1230
+
1231
+ /**
1232
+ * Whether super admins can bypass event requirement.
1233
+ * Note: This feature requires additional RBAC setup. For simple bypass, set requireEvent={false} instead.
1234
+ * @default false
1235
+ * @deprecated Use requireEvent={false} for routes that don't need events
1236
+ */
1237
+ allowSuperAdminBypass?: boolean;
1238
+
1239
+ /**
1240
+ * Custom component to render when no events are available.
1241
+ * If not provided, a default message is shown.
1242
+ */
1243
+ noEventsFallback?: React.ReactNode;
1244
+
1245
+ /**
1246
+ * Custom component to render while events are loading.
1247
+ * If not provided, a default loading spinner is shown.
1248
+ */
1249
+ loadingFallback?: React.ReactNode;
1250
+
1251
+ /**
1252
+ * Login redirect path when user is not authenticated.
1253
+ * @default '/login'
1254
+ */
1255
+ loginPath?: string;
1256
+ }
1257
+ ```
1258
+
1259
+ #### Usage
1260
+
1261
+ **Basic Usage (with event requirement):**
1262
+ ```tsx
1263
+ import { ProtectedRoute } from '@jmruthers/pace-core';
1264
+ import { Routes, Route } from 'react-router-dom';
1265
+
1266
+ function App() {
1267
+ return (
1268
+ <Routes>
1269
+ <Route path="/login" element={<LoginPage />} />
1270
+ <Route element={<ProtectedRoute />}>
1271
+ <Route path="/dashboard" element={<DashboardPage />} />
1272
+ <Route path="/events" element={<EventsPage />} />
1273
+ </Route>
1274
+ </Routes>
1275
+ );
1276
+ }
1277
+ ```
1278
+
1279
+ **Without Event Requirement:**
1280
+ ```tsx
1281
+ <Route element={<ProtectedRoute requireEvent={false} />}>
1282
+ <Route path="/settings" element={<SettingsPage />} />
1283
+ </Route>
1284
+ ```
1285
+
1286
+ **Custom Fallbacks:**
1287
+ ```tsx
1288
+ <Route element={
1289
+ <ProtectedRoute
1290
+ requireEvent={true}
1291
+ loginPath="/login"
1292
+ loadingFallback={<CustomLoader />}
1293
+ noEventsFallback={
1294
+ <div>
1295
+ <h2>No Events Available</h2>
1296
+ <p>Contact your administrator for access.</p>
1297
+ </div>
1298
+ }
1299
+ />
1300
+ }>
1301
+ <Route path="/dashboard" element={<DashboardPage />} />
1302
+ </Route>
1303
+ ```
1304
+
1305
+ #### Features
1306
+
1307
+ - ✅ **Authentication checking** - Redirects to login if not authenticated
1308
+ - ✅ **Session restoration** - Handles session restoration states automatically
1309
+ - ✅ **Event loading management** - Allows rendering during event loading (prevents UI blocking)
1310
+ - ✅ **Event selector visibility** - Allows rendering when events exist but none selected (so event selector in header is visible)
1311
+ - ✅ **Clear error states** - Shows helpful messages when no events are available
1312
+ - ✅ **Flexible configuration** - Optional event requirement, customizable fallbacks
1313
+
1314
+ #### Important Notes
1315
+
1316
+ The `ProtectedRoute` component solves a critical UX issue: **When apps check for `selectedEvent` before rendering, the event selector dropdown (typically in `PaceAppLayout` header) is never visible, creating a deadlock where users cannot select an event.**
1317
+
1318
+ This component allows rendering when:
1319
+ - Events are loading (prevents blocking UI)
1320
+ - Events exist but none selected (allows event selector to be visible)
1321
+ - Individual pages should handle "no selected event" state gracefully
1322
+
1323
+ #### Example: Complete App Setup
1324
+
1325
+ ```tsx
1326
+ import {
1327
+ UnifiedAuthProvider,
1328
+ ProtectedRoute,
1329
+ PaceAppLayout
1330
+ } from '@jmruthers/pace-core';
1331
+ import { BrowserRouter, Routes, Route } from 'react-router-dom';
1332
+
1333
+ function App() {
1334
+ return (
1335
+ <UnifiedAuthProvider
1336
+ supabaseClient={supabase}
1337
+ appName="my-app"
1338
+ idleTimeoutMs={30 * 60 * 1000}
1339
+ warnBeforeMs={60 * 1000}
1340
+ onIdleLogout={() => window.location.href = '/login'}
1341
+ >
1342
+ <BrowserRouter>
1343
+ <Routes>
1344
+ <Route path="/login" element={<PaceLoginPage appName="My App" />} />
1345
+ <Route element={<ProtectedRoute />}>
1346
+ <Route element={<PaceAppLayout appName="My App" />}>
1347
+ <Route path="/" element={<DashboardPage />} />
1348
+ <Route path="/events" element={<EventsPage />} />
1349
+ </Route>
1350
+ </Route>
1351
+ </Routes>
1352
+ </BrowserRouter>
1353
+ </UnifiedAuthProvider>
1354
+ );
1355
+ }
1356
+ ```
1357
+
1358
+ For more details, see the [Authentication Implementation Guide](../implementation-guides/authentication.md#protected-routes).
1359
+
1360
+ ---
1216
1361
 
1217
1362
  ### PasswordChangeForm
1218
1363
 
@@ -44,19 +44,37 @@ function App() {
44
44
 
45
45
  ### Protected Routes
46
46
 
47
- ```tsx
48
- import { useUnifiedAuth, Navigate } from '@jmruthers/pace-core';
49
-
50
- function ProtectedRoute({ children }: { children: React.ReactNode }) {
51
- const { user, loading } = useUnifiedAuth();
47
+ **✅ Use the standard ProtectedRoute component from pace-core:**
52
48
 
53
- if (loading) return <div>Loading...</div>;
54
- if (!user) return <Navigate to="/login" />;
49
+ ```tsx
50
+ import { ProtectedRoute } from '@jmruthers/pace-core';
51
+ import { Routes, Route } from 'react-router-dom';
55
52
 
56
- return <>{children}</>;
53
+ function App() {
54
+ return (
55
+ <Routes>
56
+ <Route path="/login" element={<LoginPage />} />
57
+ {/* Routes requiring authentication and event selection */}
58
+ <Route element={<ProtectedRoute />}>
59
+ <Route path="/dashboard" element={<DashboardPage />} />
60
+ </Route>
61
+ {/* Routes requiring authentication only (no event needed) */}
62
+ <Route element={<ProtectedRoute requireEvent={false} />}>
63
+ <Route path="/settings" element={<SettingsPage />} />
64
+ </Route>
65
+ </Routes>
66
+ );
57
67
  }
58
68
  ```
59
69
 
70
+ **Why use the standard component?**
71
+ - Prevents the chicken-and-egg problem where checking `selectedEvent` blocks the event selector from being visible
72
+ - Handles session restoration automatically
73
+ - Manages loading states properly
74
+ - Provides consistent behavior across all pace apps
75
+
76
+ **❌ Avoid custom ProtectedRoute implementations that block rendering when no event is selected**, as this prevents users from seeing the event selector dropdown in the header.
77
+
60
78
  ### Session Management
61
79
 
62
80
  ```tsx
@@ -516,7 +516,7 @@ src/
516
516
  ├── components/
517
517
  │ ├── auth/
518
518
  │ │ ├── LoginPage.tsx # Login form
519
- │ │ └── ProtectedRoute.tsx # Route protection
519
+ │ │ └── (ProtectedRoute now provided by @jmruthers/pace-core)
520
520
  │ ├── layout/
521
521
  │ │ ├── AppLayout.tsx # Main layout
522
522
  │ │ └── Navigation.tsx # Navigation menu
@@ -587,9 +587,7 @@ export default App;
587
587
 
588
588
  ```tsx
589
589
  import { Routes, Route } from 'react-router-dom';
590
- import { LoginPage } from './components/auth/LoginPage';
591
- import { ProtectedRoute } from './components/auth/ProtectedRoute';
592
- import { AppLayout } from './components/layout/AppLayout';
590
+ import { ProtectedRoute, PaceLoginPage, PaceAppLayout } from '@jmruthers/pace-core';
593
591
  import { DashboardPage } from './components/dashboard/DashboardPage';
594
592
  import { MealsPage } from './components/meals/MealsPage';
595
593
  import { UsersPage } from './components/users/UsersPage';
@@ -597,28 +595,14 @@ import { UsersPage } from './components/users/UsersPage';
597
595
  export function AppRoutes() {
598
596
  return (
599
597
  <Routes>
600
- <Route path="/login" element={<LoginPage />} />
601
- <Route path="/" element={
602
- <ProtectedRoute>
603
- <AppLayout>
604
- <DashboardPage />
605
- </AppLayout>
606
- </ProtectedRoute>
607
- } />
608
- <Route path="/meals" element={
609
- <ProtectedRoute>
610
- <AppLayout>
611
- <MealsPage />
612
- </AppLayout>
613
- </ProtectedRoute>
614
- } />
615
- <Route path="/users" element={
616
- <ProtectedRoute>
617
- <AppLayout>
618
- <UsersPage />
619
- </AppLayout>
620
- </ProtectedRoute>
621
- } />
598
+ <Route path="/login" element={<PaceLoginPage appName="Meal Manager" />} />
599
+ <Route element={<ProtectedRoute />}>
600
+ <Route element={<PaceAppLayout appName="Meal Manager" />}>
601
+ <Route path="/" element={<DashboardPage />} />
602
+ <Route path="/meals" element={<MealsPage />} />
603
+ <Route path="/users" element={<UsersPage />} />
604
+ </Route>
605
+ </Route>
622
606
  </Routes>
623
607
  );
624
608
  }
@@ -140,6 +140,29 @@ function MyComponent() {
140
140
  ```
141
141
 
142
142
  ### Protected Routes
143
+
144
+ **✅ Use the standard ProtectedRoute component for authentication and event selection:**
145
+ ```tsx
146
+ import { ProtectedRoute } from '@jmruthers/pace-core';
147
+ import { Routes, Route } from 'react-router-dom';
148
+
149
+ function App() {
150
+ return (
151
+ <Routes>
152
+ <Route path="/login" element={<LoginPage />} />
153
+ <Route element={<ProtectedRoute />}>
154
+ <Route path="/dashboard" element={<DashboardPage />} />
155
+ </Route>
156
+ {/* Routes without event requirement */}
157
+ <Route element={<ProtectedRoute requireEvent={false} />}>
158
+ <Route path="/settings" element={<SettingsPage />} />
159
+ </Route>
160
+ </Routes>
161
+ );
162
+ }
163
+ ```
164
+
165
+ **For permission-based protection, use PermissionGuard:**
143
166
  ```tsx
144
167
  import { PermissionGuard } from '@jmruthers/pace-core';
145
168
 
@@ -134,34 +134,57 @@ export function LoginPage() {
134
134
 
135
135
  ### 6. Protected Routes
136
136
 
137
- ```tsx
138
- // src/components/ProtectedRoute.tsx
139
- import { useUnifiedAuth, Navigate } from '@jmruthers/pace-core';
140
-
141
- function ProtectedRoute({ children }: { children: React.ReactNode }) {
142
- const { user, loading } = useUnifiedAuth();
137
+ PACE Core provides a standard `ProtectedRoute` component that handles authentication, session restoration, and event selection without creating blocking issues.
143
138
 
144
- if (loading) return <div>Loading...</div>;
145
- if (!user) return <Navigate to="/login" />;
139
+ **Key Features:**
140
+ - Handles authentication checks and redirects
141
+ - ✅ Manages session restoration states
142
+ - ✅ Allows rendering during event loading (prevents UI blocking)
143
+ - ✅ Allows rendering when events exist but none selected (so event selector is visible)
144
+ - ✅ Shows clear error states when no events are available
145
+ - ✅ Optional event requirement
146
146
 
147
- return <>{children}</>;
148
- }
147
+ ```tsx
148
+ // ✅ Recommended: Use the standard ProtectedRoute from pace-core
149
+ import { ProtectedRoute } from '@jmruthers/pace-core';
150
+ import { Routes, Route } from 'react-router-dom';
149
151
 
150
- // Usage in App.tsx
151
152
  function App() {
152
153
  return (
153
154
  <Routes>
154
155
  <Route path="/login" element={<LoginPage />} />
155
- <Route path="/" element={
156
- <ProtectedRoute>
157
- <DashboardPage />
158
- </ProtectedRoute>
159
- } />
156
+ {/* Protected routes with event requirement (default) */}
157
+ <Route element={<ProtectedRoute />}>
158
+ <Route path="/" element={<DashboardPage />} />
159
+ <Route path="/events" element={<EventsPage />} />
160
+ </Route>
161
+ {/* Protected routes without event requirement */}
162
+ <Route element={<ProtectedRoute requireEvent={false} />}>
163
+ <Route path="/settings" element={<SettingsPage />} />
164
+ </Route>
160
165
  </Routes>
161
166
  );
162
167
  }
163
168
  ```
164
169
 
170
+ **Advanced Usage:**
171
+
172
+ ```tsx
173
+ // Custom fallbacks for loading and error states
174
+ <Route element={
175
+ <ProtectedRoute
176
+ requireEvent={true}
177
+ loginPath="/login"
178
+ loadingFallback={<CustomLoader />}
179
+ noEventsFallback={<CustomNoEventsMessage />}
180
+ />
181
+ }>
182
+ <Route path="/dashboard" element={<DashboardPage />} />
183
+ </Route>
184
+ ```
185
+
186
+ **Important:** The `ProtectedRoute` component solves the common chicken-and-egg problem where apps check for `selectedEvent` before rendering, which blocks the event selector (typically in the `PaceAppLayout` header) from being visible. The component allows rendering when events exist but none is selected, enabling users to see and use the event selector dropdown.
187
+
165
188
  ## Configuration Options
166
189
 
167
190
  ### UnifiedAuthProvider Props
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmruthers/pace-core",
3
- "version": "0.5.91",
3
+ "version": "0.5.93",
4
4
  "description": "Clean, modern React component library with Tailwind v4 styling and native utilities",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -214,17 +214,35 @@ export function EventSelector({
214
214
  return [...events].sort((a, b) => getTime(b) - getTime(a));
215
215
  }, [events]);
216
216
 
217
- // Default to the next upcoming event if none selected
217
+ // Default to the next upcoming event if none selected, fallback to most recent past event
218
218
  useEffect(() => {
219
219
  if (!selectedEvent && events.length > 0) {
220
220
  const today = new Date();
221
221
  const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
222
+
223
+ // Try to find next future event
222
224
  const next = [...events]
223
225
  .filter(e => e.event_date && new Date(e.event_date).getTime() >= startOfToday)
224
226
  .sort((a, b) => new Date(a.event_date as string).getTime() - new Date(b.event_date as string).getTime())[0];
227
+
225
228
  if (next) {
226
229
  setSelectedEvent(next);
227
230
  if (onEventChange) onEventChange(next);
231
+ } else {
232
+ // Fallback to most recent past event if no future events found
233
+ const mostRecentPast = [...events]
234
+ .filter(e => {
235
+ if (!e.event_date) return false;
236
+ const eventDate = new Date(e.event_date);
237
+ const startOfEventDate = new Date(eventDate.getFullYear(), eventDate.getMonth(), eventDate.getDate()).getTime();
238
+ return startOfEventDate < startOfToday;
239
+ })
240
+ .sort((a, b) => new Date(b.event_date as string).getTime() - new Date(a.event_date as string).getTime())[0];
241
+
242
+ if (mostRecentPast) {
243
+ setSelectedEvent(mostRecentPast);
244
+ if (onEventChange) onEventChange(mostRecentPast);
245
+ }
228
246
  }
229
247
  }
230
248
  }, [events, selectedEvent, setSelectedEvent, onEventChange]);
@@ -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
+