@jmruthers/pace-core 0.5.125 → 0.5.127

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 (170) hide show
  1. package/dist/{DataTable-IHD4JP4W.js → DataTable-QZH6SEUM.js} +6 -6
  2. package/dist/{PublicLoadingSpinner-CaoRbHvJ.d.ts → PublicLoadingSpinner-qqvM-NUe.d.ts} +34 -21
  3. package/dist/{UnifiedAuthProvider-6C47WIML.js → UnifiedAuthProvider-CQDZRJIS.js} +3 -3
  4. package/dist/{chunk-ZBLK676C.js → chunk-3CG5L6RN.js} +1 -19
  5. package/dist/chunk-3CG5L6RN.js.map +1 -0
  6. package/dist/{chunk-35ZDPMBM.js → chunk-BYXRHAIF.js} +3 -3
  7. package/dist/{chunk-IJOZZOGT.js → chunk-CQZU6TFE.js} +5 -5
  8. package/dist/{chunk-C43QIDN3.js → chunk-CTJRBUX2.js} +2 -2
  9. package/dist/{chunk-ESJTIADP.js → chunk-F64FFPOZ.js} +5 -15
  10. package/dist/{chunk-ESJTIADP.js.map → chunk-F64FFPOZ.js.map} +1 -1
  11. package/dist/{chunk-JJVLYIEO.js → chunk-JDBO5NCG.js} +253 -135
  12. package/dist/chunk-JDBO5NCG.js.map +1 -0
  13. package/dist/{chunk-4MXVZVNS.js → chunk-TGIY2AR2.js} +2 -2
  14. package/dist/{chunk-HC7AOIC2.js → chunk-TMUNK34W.js} +428 -446
  15. package/dist/chunk-TMUNK34W.js.map +1 -0
  16. package/dist/{chunk-XN6GWKMV.js → chunk-VZ5OR6HD.js} +161 -14
  17. package/dist/chunk-VZ5OR6HD.js.map +1 -0
  18. package/dist/{chunk-QWNJCQXZ.js → chunk-ZV77RZMU.js} +2 -2
  19. package/dist/{chunk-NZGLXZGP.js → chunk-ZYZCRSBD.js} +3 -54
  20. package/dist/chunk-ZYZCRSBD.js.map +1 -0
  21. package/dist/components.d.ts +1 -1
  22. package/dist/components.js +9 -9
  23. package/dist/hooks.js +7 -7
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.js +12 -12
  26. package/dist/providers.js +2 -2
  27. package/dist/rbac/index.js +7 -7
  28. package/dist/utils.js +1 -1
  29. package/docs/api/classes/ColumnFactory.md +1 -1
  30. package/docs/api/classes/ErrorBoundary.md +1 -1
  31. package/docs/api/classes/InvalidScopeError.md +1 -1
  32. package/docs/api/classes/MissingUserContextError.md +1 -1
  33. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  34. package/docs/api/classes/PermissionDeniedError.md +1 -1
  35. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  36. package/docs/api/classes/RBACAuditManager.md +1 -1
  37. package/docs/api/classes/RBACCache.md +1 -1
  38. package/docs/api/classes/RBACEngine.md +1 -1
  39. package/docs/api/classes/RBACError.md +1 -1
  40. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  41. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  42. package/docs/api/classes/StorageUtils.md +1 -1
  43. package/docs/api/enums/FileCategory.md +1 -1
  44. package/docs/api/interfaces/AggregateConfig.md +1 -1
  45. package/docs/api/interfaces/ButtonProps.md +1 -1
  46. package/docs/api/interfaces/CardProps.md +1 -1
  47. package/docs/api/interfaces/ColorPalette.md +1 -1
  48. package/docs/api/interfaces/ColorShade.md +1 -1
  49. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  50. package/docs/api/interfaces/DataRecord.md +1 -1
  51. package/docs/api/interfaces/DataTableAction.md +1 -1
  52. package/docs/api/interfaces/DataTableColumn.md +1 -1
  53. package/docs/api/interfaces/DataTableProps.md +1 -1
  54. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  55. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  56. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  57. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  58. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  59. package/docs/api/interfaces/FileMetadata.md +1 -1
  60. package/docs/api/interfaces/FileReference.md +1 -1
  61. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  62. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  63. package/docs/api/interfaces/FileUploadProps.md +1 -1
  64. package/docs/api/interfaces/FooterProps.md +1 -1
  65. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  66. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  67. package/docs/api/interfaces/InputProps.md +1 -1
  68. package/docs/api/interfaces/LabelProps.md +1 -1
  69. package/docs/api/interfaces/LoginFormProps.md +1 -1
  70. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  71. package/docs/api/interfaces/NavigationContextType.md +1 -1
  72. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  73. package/docs/api/interfaces/NavigationItem.md +1 -1
  74. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  75. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  76. package/docs/api/interfaces/Organisation.md +1 -1
  77. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  78. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  79. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  80. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  81. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  82. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  83. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  84. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  85. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  86. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  87. package/docs/api/interfaces/PaletteData.md +1 -1
  88. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  89. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  90. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  91. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  92. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  93. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  94. package/docs/api/interfaces/PublicPageHeaderProps.md +10 -62
  95. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  96. package/docs/api/interfaces/RBACConfig.md +1 -1
  97. package/docs/api/interfaces/RBACLogger.md +1 -1
  98. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  99. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  100. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  101. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  102. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  103. package/docs/api/interfaces/RouteConfig.md +1 -1
  104. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  105. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  106. package/docs/api/interfaces/StorageConfig.md +1 -1
  107. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  108. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  109. package/docs/api/interfaces/StorageListOptions.md +1 -1
  110. package/docs/api/interfaces/StorageListResult.md +1 -1
  111. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  112. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  113. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  114. package/docs/api/interfaces/StyleImport.md +1 -1
  115. package/docs/api/interfaces/SwitchProps.md +1 -1
  116. package/docs/api/interfaces/ToastActionElement.md +1 -1
  117. package/docs/api/interfaces/ToastProps.md +1 -1
  118. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  119. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  120. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  121. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  122. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  123. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  124. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  125. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  126. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  127. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  128. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  129. package/docs/api/interfaces/UserEventAccess.md +1 -1
  130. package/docs/api/interfaces/UserMenuProps.md +1 -1
  131. package/docs/api/interfaces/UserProfile.md +1 -1
  132. package/docs/api/modules.md +46 -28
  133. package/docs/architecture/rpc-function-standards.md +39 -5
  134. package/package.json +1 -1
  135. package/src/components/Button/Button.tsx +1 -1
  136. package/src/components/DataTable/components/ImportModal.tsx +134 -2
  137. package/src/components/DataTable/components/UnifiedTableBody.tsx +6 -3
  138. package/src/components/Dialog/Dialog.tsx +0 -13
  139. package/src/components/FileDisplay/FileDisplay.tsx +76 -0
  140. package/src/components/Header/Header.tsx +5 -0
  141. package/src/components/PaceAppLayout/PaceAppLayout.tsx +12 -39
  142. package/src/components/PublicLayout/PublicPageFooter.tsx +1 -1
  143. package/src/components/PublicLayout/PublicPageHeader.tsx +69 -128
  144. package/src/components/PublicLayout/PublicPageLayout.tsx +4 -4
  145. package/src/components/PublicLayout/PublicPageProvider.tsx +12 -3
  146. package/src/components/PublicLayout/__tests__/PublicPageFooter.test.tsx +1 -1
  147. package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +3 -18
  148. package/src/hooks/__tests__/useAppConfig.unit.test.ts +3 -1
  149. package/src/hooks/__tests__/usePermissionCache.unit.test.ts +11 -5
  150. package/src/hooks/__tests__/usePublicRouteParams.unit.test.ts +8 -7
  151. package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +41 -46
  152. package/src/hooks/public/usePublicFileDisplay.ts +176 -7
  153. package/src/hooks/public/usePublicRouteParams.ts +0 -12
  154. package/src/hooks/useAppConfig.ts +15 -6
  155. package/src/hooks/usePermissionCache.test.ts +12 -4
  156. package/src/hooks/usePermissionCache.ts +3 -19
  157. package/src/hooks/useSecureDataAccess.ts +0 -63
  158. package/src/services/EventService.ts +0 -19
  159. package/dist/chunk-HC7AOIC2.js.map +0 -1
  160. package/dist/chunk-JJVLYIEO.js.map +0 -1
  161. package/dist/chunk-NZGLXZGP.js.map +0 -1
  162. package/dist/chunk-XN6GWKMV.js.map +0 -1
  163. package/dist/chunk-ZBLK676C.js.map +0 -1
  164. /package/dist/{DataTable-IHD4JP4W.js.map → DataTable-QZH6SEUM.js.map} +0 -0
  165. /package/dist/{UnifiedAuthProvider-6C47WIML.js.map → UnifiedAuthProvider-CQDZRJIS.js.map} +0 -0
  166. /package/dist/{chunk-35ZDPMBM.js.map → chunk-BYXRHAIF.js.map} +0 -0
  167. /package/dist/{chunk-IJOZZOGT.js.map → chunk-CQZU6TFE.js.map} +0 -0
  168. /package/dist/{chunk-C43QIDN3.js.map → chunk-CTJRBUX2.js.map} +0 -0
  169. /package/dist/{chunk-4MXVZVNS.js.map → chunk-TGIY2AR2.js.map} +0 -0
  170. /package/dist/{chunk-QWNJCQXZ.js.map → chunk-ZV77RZMU.js.map} +0 -0
@@ -212,7 +212,8 @@ function SelectEditField<TData extends DataRecord>({
212
212
  };
213
213
 
214
214
  const hasMatch = (columnDef.fieldOptions || []).some(checkMatch);
215
- const shouldShow = !hasMatch && isCreatable && !!columnDef.onCreateNew;
215
+ // Always show create option when there's a search term (user might want to create even if matches exist)
216
+ const shouldShow = isCreatable && !!columnDef.onCreateNew;
216
217
 
217
218
  // Debug logging for creatable select
218
219
  if (import.meta.env.MODE === 'development' && isCreatable) {
@@ -223,7 +224,8 @@ function SelectEditField<TData extends DataRecord>({
223
224
  isCreatable,
224
225
  hasOnCreateNew: !!columnDef.onCreateNew,
225
226
  shouldShow,
226
- fieldOptionsCount: columnDef.fieldOptions?.length || 0
227
+ fieldOptionsCount: columnDef.fieldOptions?.length || 0,
228
+ note: 'Create option shows when search term exists, regardless of matches'
227
229
  });
228
230
  }
229
231
 
@@ -259,7 +261,8 @@ function SelectEditField<TData extends DataRecord>({
259
261
  };
260
262
 
261
263
  const hasMatch = (columnDef.fieldOptions || []).some(checkMatch);
262
- const shouldShow = !hasMatch && isCreatable && !!columnDef.onCreateNew;
264
+ // Always show create option when there's a search term (user might want to create even if matches exist)
265
+ const shouldShow = isCreatable && !!columnDef.onCreateNew;
263
266
  setShowCreateOption(shouldShow);
264
267
  } else {
265
268
  setShowCreateOption(false);
@@ -617,24 +617,11 @@ const DialogBody = ({
617
617
  return null;
618
618
  }
619
619
 
620
- console.log('🔍 Dialog HTML Debug:', {
621
- originalHtml: htmlContent,
622
- allowHtml,
623
- strictSanitization,
624
- logWarnings
625
- });
626
-
627
620
  const result = renderSafeHtml(htmlContent, {
628
621
  strict: strictSanitization,
629
622
  logWarnings
630
623
  });
631
624
 
632
- console.log('🔍 Dialog HTML Result:', {
633
- sanitizedHtml: result.html,
634
- isValid: result.isValid,
635
- warnings: result.warnings
636
- });
637
-
638
625
  return result.html;
639
626
  }, [htmlContent, allowHtml, strictSanitization, logWarnings]);
640
627
 
@@ -502,7 +502,51 @@ function FileDisplayPublic({
502
502
  const publicPageContext = useContext(PublicPageContext);
503
503
  const supabase = publicPageContext?.supabase ?? null;
504
504
 
505
+ // Step 4: Log Supabase client context
506
+ console.log('[FileDisplayPublic] Supabase Client Context:', {
507
+ hasPublicPageContext: !!publicPageContext,
508
+ hasSupabaseClient: !!supabase,
509
+ supabaseUrl: publicPageContext?.environment?.supabaseUrl || 'not available',
510
+ hasAnonKey: !!publicPageContext?.environment?.supabaseKey,
511
+ hasAuth: !!supabase?.auth,
512
+ organisation_id,
513
+ table_name,
514
+ record_id,
515
+ category,
516
+ context: 'public_page_anonymous_user',
517
+ note: 'Public pages use anonymous Supabase client (no user session)'
518
+ });
519
+
505
520
  if (!supabase) {
521
+ // If fallback is enabled, show fallback UI instead of error
522
+ if (showFallback) {
523
+ return (
524
+ <FileDisplayContent
525
+ isLoading={false}
526
+ error={null}
527
+ fileUrl={null}
528
+ fileReference={null}
529
+ fileReferences={[]}
530
+ fileUrls={new Map()}
531
+ fileCount={0}
532
+ category={category}
533
+ displayOnly={displayOnly}
534
+ showDelete={false}
535
+ className={className}
536
+ children={children}
537
+ onDelete={undefined}
538
+ organisation_id={organisation_id}
539
+ loadingComponent={loadingComponent}
540
+ errorComponent={errorComponent}
541
+ showFallback={showFallback}
542
+ generateFallbackText={generateFallbackText}
543
+ fallbackText={fallbackText}
544
+ fallbackSize={fallbackSize}
545
+ />
546
+ );
547
+ }
548
+
549
+ // Only show error if fallback is not enabled
506
550
  return (
507
551
  <div className={`text-sec-500 text-center p-4 ${className}`}>
508
552
  Supabase client not available in public context
@@ -527,6 +571,38 @@ function FileDisplayPublic({
527
571
  { supabase }
528
572
  );
529
573
 
574
+ // Log errors for debugging public file display issues
575
+ if (error) {
576
+ console.error('[FileDisplayPublic] Error fetching file:', {
577
+ table_name,
578
+ record_id,
579
+ organisation_id,
580
+ category,
581
+ error: error.message,
582
+ errorStack: error.stack
583
+ });
584
+ }
585
+
586
+ // Log when file is successfully loaded
587
+ if (fileUrl && !isLoading && !error) {
588
+ console.log('[FileDisplayPublic] File loaded successfully:', {
589
+ table_name,
590
+ record_id,
591
+ category,
592
+ fileUrl: fileUrl.substring(0, 50) + '...' // Truncate URL for logging
593
+ });
594
+ }
595
+
596
+ // Log when no file is found (but not an error - might be expected)
597
+ if (!isLoading && !error && !fileUrl && !fileReference) {
598
+ console.log('[FileDisplayPublic] No file found (will show fallback if enabled):', {
599
+ table_name,
600
+ record_id,
601
+ category,
602
+ showFallback
603
+ });
604
+ }
605
+
530
606
  // Public context doesn't support delete operations
531
607
  const handleDelete = async () => {
532
608
  // Delete operations are not available in public context for security reasons
@@ -137,6 +137,11 @@ export interface HeaderProps {
137
137
  * A flexible header component that supports various configurations including custom logos,
138
138
  * navigation menus, user authentication, event selection, and custom actions.
139
139
  *
140
+ * **Logo Display:** When used via PaceAppLayout, the logo URL is automatically constructed
141
+ * from the appName prop as `/${appName.toLowerCase()}_logo_wide.svg`. The appName should
142
+ * come from an APP_NAME constant declared in your App.tsx file to ensure consistency across
143
+ * authenticated and public pages.
144
+ *
140
145
  * Features:
141
146
  * - Customizable logo (URL or custom component)
142
147
  * - Clickable logo that automatically routes to dashboard (configurable via logoHref)
@@ -202,6 +202,11 @@ export interface PaceAppLayoutProps {
202
202
  * Outlet to render child routes. It provides integrated authentication, navigation,
203
203
  * and user management functionality.
204
204
  *
205
+ * **Important:** The appName prop should use an APP_NAME constant declared in your App.tsx
206
+ * file. This ensures consistency with public pages (via PublicPageProvider) which should
207
+ * also receive the same APP_NAME constant. The logo URL is automatically constructed as
208
+ * `/${appName.toLowerCase()}_logo_wide.svg` from the public folder.
209
+ *
205
210
  * Features:
206
211
  * - React Router v6 integration with nested routing
207
212
  * - Unified authentication integration
@@ -219,17 +224,20 @@ export interface PaceAppLayoutProps {
219
224
  * ```tsx
220
225
  * import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
221
226
  * import { UnifiedAuthProvider } from '@jmruthers/pace-core/providers';
222
- * import { PaceAppLayout, PaceLoginPage } from '@jmruthers/pace-core';
227
+ * import { PaceAppLayout, PaceLoginPage, PublicPageApp } from '@jmruthers/pace-core';
228
+ *
229
+ * const APP_NAME = 'CORE';
223
230
  *
224
231
  * function App() {
225
232
  * return (
226
- * <UnifiedAuthProvider supabaseClient={supabase} appName="My App">
233
+ * <UnifiedAuthProvider supabaseClient={supabase} appName={APP_NAME}>
227
234
  * <Router>
228
235
  * <Routes>
229
- * <Route path="/login" element={<PaceLoginPage appName="My App" />} />
236
+ * <Route path="/login" element={<PaceLoginPage appName={APP_NAME} />} />
237
+ * <Route path="/events/*" element={<PublicPageApp appName={APP_NAME} />} />
230
238
  * <Route path="/" element={
231
239
  * <PaceAppLayout
232
- * appName="My Application"
240
+ * appName={APP_NAME}
233
241
  * enforcePermissions={true}
234
242
  * defaultPermission="read"
235
243
  * />
@@ -448,18 +456,6 @@ export function PaceAppLayout({
448
456
  }
449
457
 
450
458
  // NEW: Phase 1 - Enhanced Security Features
451
- // Log page access attempt for audit
452
- if (auditLog) {
453
- console.log(`[PaceAppLayout] Page access attempt:`, {
454
- pageName: currentPageId,
455
- operation: currentRoutePermission,
456
- userId: user?.id,
457
- allowed: can,
458
- strictMode,
459
- timestamp: new Date().toISOString()
460
- });
461
- }
462
-
463
459
  // Handle strict mode violations
464
460
  if (strictMode && !can) {
465
461
  console.error(`[PaceAppLayout] STRICT MODE VIOLATION: User attempted to access protected page without permission`, {
@@ -568,16 +564,6 @@ export function PaceAppLayout({
568
564
  // Check permission map (super admin check already handled in getPermissionMap)
569
565
  const hasAccess = permissionMap['*'] === true || permissionMap[fullPermission] === true;
570
566
 
571
- if (auditLog) {
572
- console.log(`[PaceAppLayout] Navigation filtering:`, {
573
- item: item.label,
574
- href: item.href,
575
- pageId,
576
- permission: fullPermission,
577
- hasAccess,
578
- });
579
- }
580
-
581
567
  return { item, hasAccess };
582
568
  });
583
569
 
@@ -692,19 +678,6 @@ export function PaceAppLayout({
692
678
  navigate(fallbackRoute, { replace: true });
693
679
  return;
694
680
  }
695
-
696
- // Log route access attempt for audit
697
- if (auditLog) {
698
- console.log(`[PaceAppLayout] Route access attempt:`, {
699
- route: currentPath,
700
- userId: user?.id,
701
- allowed: hasAccess,
702
- permissions: currentRoute.permissions,
703
- roles: currentRoute.roles,
704
- accessLevel: currentRoute.accessLevel,
705
- timestamp: new Date().toISOString()
706
- });
707
- }
708
681
  };
709
682
 
710
683
  checkRouteAccess();
@@ -90,7 +90,7 @@ export function PublicPageFooter({
90
90
  const copyrightText = copyright || `© Copyright 2022–${year} all rights reserved, ${companyName}.`;
91
91
 
92
92
  return (
93
- <footer className={cn('mt-8 py-6 flex justify-center border-t border-border bg-main-100', className)}>
93
+ <footer className={cn('mt-8 py-6 flex justify-center', className)}>
94
94
  <section className='px-4 w-[min(var(--app-width),100%)] mx-auto text-center'>
95
95
  {logo && (
96
96
  <img src={logo} alt="Logo" className="h-8 w-auto" />
@@ -17,40 +17,24 @@
17
17
  *
18
18
  * @example
19
19
  * ```tsx
20
- * import { PublicPageHeader, FileDisplay } from '@jmruthers/pace-core';
21
- *
20
+ * import { PublicPageHeader, PublicPageProvider } from '@jmruthers/pace-core';
21
+ *
22
+ * const APP_NAME = 'CORE';
23
+ *
22
24
  * function PublicEventPage() {
23
25
  * return (
24
- * <PublicPageHeader
25
- * event={event}
26
- * title="Event Details"
27
- * description="Public information about this event"
28
- * showEventLogo={true}
29
- * />
26
+ * <PublicPageProvider appName={APP_NAME}>
27
+ * <PublicPageHeader
28
+ * event={event}
29
+ * title="Event Details"
30
+ * description="Public information about this event"
31
+ * showEventLogo={true}
32
+ * />
33
+ * </PublicPageProvider>
30
34
  * );
31
35
  * }
32
36
  * ```
33
37
  *
34
- * @example
35
- * ```tsx
36
- * // Using custom logo URL
37
- * <PublicPageHeader
38
- * event={event}
39
- * logoUrl="/custom-logo.svg"
40
- * logoAlt="My Custom Logo"
41
- * logoHref="/"
42
- * />
43
- * ```
44
- *
45
- * @example
46
- * ```tsx
47
- * // Using custom logo component
48
- * <PublicPageHeader
49
- * event={event}
50
- * customAppLogo={<CustomLogoComponent />}
51
- * logoHref="/dashboard"
52
- * />
53
- * ```
54
38
  *
55
39
  * @accessibility
56
40
  * - WCAG 2.1 AA compliant
@@ -67,7 +51,6 @@
67
51
  */
68
52
 
69
53
  import React, { ReactNode } from 'react';
70
- import { Link } from 'react-router-dom';
71
54
  import type { Event } from '../../types/unified';
72
55
  import { FileDisplay } from '../FileDisplay/FileDisplay';
73
56
  import { FileCategory } from '../../types/file-reference';
@@ -91,16 +74,8 @@ export interface PublicPageHeaderProps {
91
74
  className?: string;
92
75
  /** Custom content to display in the header */
93
76
  children?: ReactNode;
94
- /** Custom app logo component (overrides logoUrl and auto-generated path) */
95
- customAppLogo?: ReactNode;
96
77
  /** Custom event logo component */
97
78
  customEventLogo?: ReactNode;
98
- /** URL to the app logo image (overrides auto-generated path, but customAppLogo takes precedence) */
99
- logoUrl?: string;
100
- /** Alt text for the app logo (defaults to appName from useAppConfig) */
101
- logoAlt?: string;
102
- /** URL to navigate to when app logo is clicked */
103
- logoHref?: string;
104
79
  }
105
80
 
106
81
  /**
@@ -109,14 +84,18 @@ export interface PublicPageHeaderProps {
109
84
  * This component displays the app logo, event logo, and event information
110
85
  * in a clean, accessible layout suitable for public pages.
111
86
  *
112
- * Logo handling follows a priority order:
113
- * 1. customAppLogo prop (if provided) - highest priority
114
- * 2. logoUrl prop (if provided) - direct URL override
115
- * 3. Auto-generated path from appName via useAppConfig() - convention-based
116
- * 4. Default SVG fallback - lowest priority
87
+ * The app logo is automatically generated from the appName using the pattern:
88
+ * `/{appName.toLowerCase()}_logo_wide.svg` from the public folder.
117
89
  *
118
- * The logo can be made clickable by providing the logoHref prop, which will
119
- * wrap the logo in a Link component for navigation.
90
+ * **Important:** The appName is obtained from PublicPageProvider context, which should
91
+ * receive the APP_NAME constant from your App.tsx file. This ensures consistency with
92
+ * authenticated pages that use the same APP_NAME constant.
93
+ *
94
+ * **Event Logo Requirements:**
95
+ * - Event logo files must be marked as `is_public = true` in the `file_references` table
96
+ * - The RPC function `data_file_reference_by_category_list` must be accessible to the anonymous/public role
97
+ * - Storage bucket must allow public read access for logo files
98
+ * - If no public logo is available, a fallback UI with event initials will be displayed
120
99
  *
121
100
  * @param props - Header configuration and content
122
101
  * @returns React element with public page header
@@ -130,83 +109,26 @@ export function PublicPageHeader({
130
109
  showAppLogo = true,
131
110
  className = '',
132
111
  children,
133
- customAppLogo,
134
- customEventLogo,
135
- logoUrl,
136
- logoAlt,
137
- logoHref
112
+ customEventLogo
138
113
  }: PublicPageHeaderProps) {
139
114
  const { appName } = useAppConfig();
140
115
 
141
116
  return (
142
117
  <header className={cn(
143
- " px-4 w-[min(var(--app-width),100%)] mx-auto bg-background border-b border-sec-200 grid grid-cols-[auto_1fr_auto] place-items-center gap-2",
118
+
119
+ "w-full px-[max(0rem,calc((100vw-var(--app-width))/2-0.5rem))] grid grid-cols-[auto_1fr_auto] place-items-center gap-2",
144
120
  className
145
121
  )}>
146
122
 
147
123
  {/* Top row with logos */}
148
124
 
149
125
  {/* App Logo */}
150
- {showAppLogo && (
151
- <>
152
- {customAppLogo ? (
153
- logoHref ? (
154
- <Link to={logoHref} className="cursor-pointer hover:opacity-80 transition-opacity">
155
- {customAppLogo}
156
- </Link>
157
- ) : (
158
- customAppLogo
159
- )
160
- ) : logoUrl ? (
161
- logoHref ? (
162
- <Link to={logoHref} className="cursor-pointer hover:opacity-80 transition-opacity">
163
- <img
164
- className="max-w-36 object-contain row-span-2"
165
- src={logoUrl}
166
- alt={logoAlt || appName}
167
- />
168
- </Link>
169
- ) : (
170
- <img
171
- className="max-w-36 object-contain row-span-2"
172
- src={logoUrl}
173
- alt={logoAlt || appName}
174
- />
175
- )
176
- ) : appName ? (
177
- logoHref ? (
178
- <Link to={logoHref} className="cursor-pointer hover:opacity-80 transition-opacity">
179
- <img
180
- className="max-w-36 object-contain row-span-2"
181
- src={`/${appName.toLowerCase()}_logo_wide.svg`}
182
- alt={logoAlt || appName}
183
- />
184
- </Link>
185
- ) : (
186
- <img
187
- className="max-w-36 object-contain row-span-2"
188
- src={`/${appName.toLowerCase()}_logo_wide.svg`}
189
- alt={logoAlt || appName}
190
- />
191
- )
192
- ) : (
193
- logoHref ? (
194
- <Link to={logoHref} className="cursor-pointer hover:opacity-80 transition-opacity">
195
- <img
196
- 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"
197
- alt={logoAlt || 'Logo'}
198
- className="max-w-36 object-contain row-span-2"
199
- />
200
- </Link>
201
- ) : (
202
- <img
203
- 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"
204
- alt={logoAlt || 'Logo'}
205
- className="max-w-36 object-contain row-span-2"
206
- />
207
- )
208
- )}
209
- </>
126
+ {showAppLogo && appName && (
127
+ <img
128
+ className="ml-4 max-w-36 object-contain row-span-2"
129
+ src={`/${appName.toLowerCase()}_logo_wide.svg`}
130
+ alt={appName}
131
+ />
210
132
  )}
211
133
 
212
134
 
@@ -222,24 +144,43 @@ export function PublicPageHeader({
222
144
  {showEventLogo && event && (
223
145
  <>
224
146
  {customEventLogo || (
225
- <FileDisplay
226
- table_name="event"
227
- record_id={event.event_id}
228
- organisation_id={event.organisation_id}
229
- category={FileCategory.EVENT_LOGOS}
230
- displayOnly={true}
231
- showFallback={true}
232
- fallbackSize="md"
233
- className="max-w-36 row-span-2"
234
- generateFallbackText={(fileName) => {
235
- if (!event.event_name) return 'EV';
236
- return event.event_name
237
- .split(/[\s\-_]+/)
238
- .map(word => word.charAt(0).toUpperCase())
239
- .join('')
240
- .substring(0, 3);
241
- }}
242
- />
147
+ <>
148
+ {/* Log organisation_id derivation chain for debugging */}
149
+ {(() => {
150
+ console.log('[PublicPageHeader] Organisation ID Derivation Chain:', {
151
+ eventCode: eventCode,
152
+ eventId: event.event_id,
153
+ eventName: event.event_name,
154
+ organisationId: event.organisation_id,
155
+ organisationIdType: typeof event.organisation_id,
156
+ organisationIdValid: !!event.organisation_id && event.organisation_id !== '',
157
+ derivation: 'URL → eventCode → usePublicEvent → event.organisation_id FileDisplay',
158
+ note: 'Organisation ID is derived from event data fetched using event code from URL'
159
+ });
160
+ return null;
161
+ })()}
162
+ <FileDisplay
163
+ table_name="event"
164
+ record_id={event.event_id}
165
+ organisation_id={event.organisation_id}
166
+ category={FileCategory.EVENT_LOGOS}
167
+ displayOnly={true}
168
+ showFallback={true}
169
+ fallbackSize="md"
170
+ className="mr-4 max-w-36 row-span-2"
171
+ generateFallbackText={(fileName) => {
172
+ if (!event.event_name) return 'EV';
173
+ return event.event_name
174
+ .split(/[\s\-_]+/)
175
+ .map(word => word.charAt(0).toUpperCase())
176
+ .join('')
177
+ .substring(0, 3);
178
+ }}
179
+ // Note: FileDisplay automatically detects public page context
180
+ // and uses FileDisplayPublic component which queries only public files (is_public = true)
181
+ // If no public logo is found, fallback UI with event initials will be displayed
182
+ />
183
+ </>
243
184
  )}
244
185
  </>
245
186
  )}
@@ -159,13 +159,13 @@ export function PublicPageLayout({
159
159
  }
160
160
  return (
161
161
  <main className="flex flex-col items-center justify-center px-4 w-[min(var(--app-width),100%)] mx-auto py-8">
162
- <div className="text-center">
162
+
163
163
  <h1>Event Not Found</h1>
164
164
  <p>
165
165
  The event code "{eventCode}" is invalid or the event is not available for public viewing.
166
166
  </p>
167
167
  <Button onClick={handleRefetch}>Try Again</Button>
168
- </div>
168
+
169
169
  </main>
170
170
  );
171
171
  }
@@ -174,13 +174,13 @@ export function PublicPageLayout({
174
174
  if (!event && showValidationErrors) {
175
175
  return (
176
176
  <main className="flex flex-col items-center justify-center px-4 w-[min(var(--app-width),100%)] mx-auto py-8">
177
- <div className="text-center">
177
+
178
178
  <h1>Event Not Available</h1>
179
179
  <p>
180
180
  This event is not available for public viewing.
181
181
  </p>
182
182
  {handleRefetch && <Button onClick={handleRefetch}>Try Again</Button>}
183
- </div>
183
+
184
184
  </main>
185
185
  );
186
186
  }
@@ -20,9 +20,11 @@
20
20
  * ```tsx
21
21
  * import { PublicPageProvider } from '@jmruthers/pace-core';
22
22
  *
23
+ * const APP_NAME = 'CORE';
24
+ *
23
25
  * function PublicApp() {
24
26
  * return (
25
- * <PublicPageProvider>
27
+ * <PublicPageProvider appName={APP_NAME}>
26
28
  * <Routes>
27
29
  * <Route path="/events/:eventCode/recipe-grid-report" element={<PublicRecipePage />} />
28
30
  * </Routes>
@@ -40,6 +42,7 @@ import { PublicErrorBoundary } from './PublicErrorBoundary';
40
42
  interface PublicPageContextType {
41
43
  isPublicPage: true;
42
44
  supabase: ReturnType<typeof createClient<Database>> | null;
45
+ appName: string | null;
43
46
  environment: {
44
47
  supabaseUrl: string | null;
45
48
  supabaseKey: string | null;
@@ -50,6 +53,8 @@ export const PublicPageContext = createContext<PublicPageContextType | undefined
50
53
 
51
54
  export interface PublicPageProviderProps {
52
55
  children: ReactNode;
56
+ /** Application name for logo display and branding. Should match the APP_NAME constant used in App.tsx */
57
+ appName?: string;
53
58
  }
54
59
 
55
60
  /**
@@ -60,8 +65,9 @@ export interface PublicPageProviderProps {
60
65
  * - Provides environment variables for public data access
61
66
  * - Includes error boundary for graceful error handling
62
67
  * - Is completely separate from the main app context
68
+ * - Provides appName for consistent logo display
63
69
  */
64
- export function PublicPageProvider({ children }: PublicPageProviderProps) {
70
+ export function PublicPageProvider({ children, appName }: PublicPageProviderProps) {
65
71
  // Get environment variables for public data access
66
72
  // Handle both Vite (import.meta.env) and Node.js (process.env) environments
67
73
  const getEnvVar = (key: string): string | undefined => {
@@ -90,12 +96,15 @@ export function PublicPageProvider({ children }: PublicPageProviderProps) {
90
96
  console.warn('[PublicPageProvider] Missing Supabase environment variables. Please ensure VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY are set in your environment.');
91
97
  return null;
92
98
  }
93
- return createClient<Database>(supabaseUrl, supabaseKey);
99
+ const client = createClient<Database>(supabaseUrl, supabaseKey);
100
+ console.log('[PublicPageProvider] Supabase client created successfully for public pages');
101
+ return client;
94
102
  }, [supabaseUrl, supabaseKey]);
95
103
 
96
104
  const contextValue: PublicPageContextType = {
97
105
  isPublicPage: true,
98
106
  supabase,
107
+ appName: appName || null,
99
108
  environment: {
100
109
  supabaseUrl,
101
110
  supabaseKey
@@ -188,7 +188,7 @@ describe('[component] PublicPageFooter', () => {
188
188
  const { container } = render(<PublicPageFooter event={mockEvent} />);
189
189
 
190
190
  const footer = container.firstChild as HTMLElement;
191
- expect(footer).toHaveClass('mt-8', 'py-6', 'flex', 'justify-center', 'border-t', 'border-border', 'bg-main-100');
191
+ expect(footer).toHaveClass('mt-8', 'py-6', 'flex', 'justify-center');
192
192
  });
193
193
 
194
194
  it('has proper section structure', () => {
@@ -155,21 +155,6 @@ describe('[component] PublicPageHeader', () => {
155
155
  expect(screen.queryByAltText('Test App')).not.toBeInTheDocument();
156
156
  });
157
157
 
158
- it('shows custom app logo when provided', () => {
159
- const customAppLogo = <div data-testid="custom-app-logo">Custom App Logo</div>;
160
-
161
- render(
162
- <PublicPageHeader
163
- event={mockEvent}
164
- eventCode="EVENT123"
165
- customAppLogo={customAppLogo}
166
- />
167
- );
168
-
169
- expect(screen.getByTestId('custom-app-logo')).toBeInTheDocument();
170
- expect(screen.queryByAltText('Test App')).not.toBeInTheDocument();
171
- });
172
-
173
158
  it('shows event logo by default', () => {
174
159
  render(
175
160
  <PublicPageHeader event={mockEvent} eventCode="EVENT123" />
@@ -283,7 +268,7 @@ describe('[component] PublicPageHeader', () => {
283
268
  );
284
269
 
285
270
  const header = container.firstChild as HTMLElement;
286
- expect(header).toHaveClass('bg-background', 'border-b', 'border-sec-200');
271
+ expect(header).toHaveClass('w-full', 'grid', 'grid-cols-[auto_1fr_auto]', 'place-items-center', 'gap-2');
287
272
  });
288
273
 
289
274
  it('has proper container structure', () => {
@@ -291,9 +276,9 @@ describe('[component] PublicPageHeader', () => {
291
276
  <PublicPageHeader event={mockEvent} eventCode="EVENT123" />
292
277
  );
293
278
 
294
- // The header itself has the container classes, not a nested div
279
+ // The header itself has the container classes with responsive padding
295
280
  const header = container.firstChild as HTMLElement;
296
- expect(header).toHaveClass('px-4', 'w-[min(var(--app-width),100%)]', 'mx-auto');
281
+ expect(header).toHaveClass('w-full', 'px-[max(0rem,calc((100vw-var(--app-width))/2-0.5rem))]');
297
282
  });
298
283
 
299
284
  it('has proper logo row structure', () => {