@jmruthers/pace-core 0.5.186 → 0.5.188
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataTable-IX2NBUTP.js → DataTable-GUFUNZ3N.js} +7 -7
- package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
- package/dist/{PublicPageProvider-DIzEzwKl.d.ts → PublicPageProvider-DrLDztHt.d.ts} +211 -106
- package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-643PUAIM.js} +2 -2
- package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
- package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
- package/dist/{chunk-HGPQUCBC.js → chunk-2UUZZJFT.js} +3 -3
- package/dist/{chunk-445GEP27.js → chunk-3GOZZZYH.js} +33 -8
- package/dist/chunk-3GOZZZYH.js.map +1 -0
- package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
- package/dist/chunk-63FOKYGO.js.map +1 -0
- package/dist/{chunk-DAGICKHT.js → chunk-DDM4CCYT.js} +3 -3
- package/dist/{chunk-XAUHJD3L.js → chunk-E7UAOUMY.js} +2 -2
- package/dist/{chunk-HDCUMOOI.js → chunk-EFCLXK7F.js} +792 -559
- package/dist/chunk-EFCLXK7F.js.map +1 -0
- package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
- package/dist/chunk-HEHYGYOX.js.map +1 -0
- package/dist/{chunk-GRIQLQ52.js → chunk-IM4QE42D.js} +27 -23
- package/dist/chunk-IM4QE42D.js.map +1 -0
- package/dist/{chunk-OALXJH4Y.js → chunk-IPCH26AG.js} +8 -8
- package/dist/chunk-IPCH26AG.js.map +1 -0
- package/dist/{chunk-UQWSHFVX.js → chunk-SAUPYVLF.js} +1 -1
- package/dist/{chunk-UQWSHFVX.js.map → chunk-SAUPYVLF.js.map} +1 -1
- package/dist/{chunk-TC7D3CR3.js → chunk-UNOTYLQF.js} +556 -101
- package/dist/chunk-UNOTYLQF.js.map +1 -0
- package/dist/{chunk-FXFJRTKI.js → chunk-VGZZXKBR.js} +5 -5
- package/dist/chunk-VGZZXKBR.js.map +1 -0
- package/dist/chunk-YHCN776L.js +447 -0
- package/dist/chunk-YHCN776L.js.map +1 -0
- package/dist/components.d.ts +4 -4
- package/dist/components.js +12 -10
- package/dist/components.js.map +1 -1
- package/dist/{file-reference-PRTSLxKx.d.ts → file-reference-D037xOFK.d.ts} +0 -1
- package/dist/hooks.d.ts +221 -6
- package/dist/hooks.js +146 -49
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +24 -9
- package/dist/index.js +62 -28
- package/dist/index.js.map +1 -1
- package/dist/providers.js +1 -1
- package/dist/rbac/index.d.ts +124 -7
- package/dist/rbac/index.js +27 -7
- package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-D71QLlg4.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +2 -2
- package/dist/utils.d.ts +213 -3
- package/dist/utils.js +22 -2
- package/dist/utils.js.map +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/Logger.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +21 -17
- package/docs/api/classes/RBACCache.md +31 -23
- package/docs/api/classes/RBACEngine.md +5 -5
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +1 -1
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.md +1 -1
- package/docs/api/interfaces/AddressFieldProps.md +241 -0
- package/docs/api/interfaces/AddressFieldRef.md +94 -0
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/AutocompleteOptions.md +75 -0
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/ComplianceResult.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
- package/docs/api/interfaces/DatabaseIssue.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +15 -15
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoggerConfig.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/ParsedAddress.md +120 -0
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/QuickFix.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +26 -3
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +5 -5
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/TabsContentProps.md +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
- package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +318 -59
- package/docs/best-practices/performance.md +11 -0
- package/docs/getting-started/examples/README.md +2 -2
- package/docs/implementation-guides/file-upload-storage.md +29 -0
- package/docs/implementation-guides/public-pages.md +140 -1230
- package/docs/rbac/README.md +2 -1
- package/docs/rbac/api-reference.md +11 -0
- package/docs/rbac/performance.md +320 -0
- package/docs/standards/01-architecture-standard.md +5 -0
- package/docs/standards/05-security-standard.md +14 -0
- package/docs/standards/07-rbac-and-rls-standard.md +356 -0
- package/package.json +1 -1
- package/src/__tests__/public-recipe-view.test.ts +199 -0
- package/src/__tests__/rls-policies.test.ts +333 -0
- package/src/components/AddressField/AddressField.test.tsx +411 -0
- package/src/components/AddressField/AddressField.tsx +323 -0
- package/src/components/AddressField/README.md +336 -0
- package/src/components/AddressField/index.ts +10 -0
- package/src/components/AddressField/types.ts +65 -0
- package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
- package/src/components/FileDisplay/FileDisplay.tsx +28 -1
- package/src/components/index.ts +2 -0
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
- package/src/hooks/index.ts +6 -0
- package/src/hooks/public/usePublicFileDisplay.ts +8 -10
- package/src/hooks/useAddressAutocomplete.test.ts +318 -0
- package/src/hooks/useAddressAutocomplete.ts +268 -0
- package/src/hooks/useFileDisplay.ts +3 -15
- package/src/hooks/useFileReference.test.ts +20 -3
- package/src/hooks/useFileReference.ts +3 -24
- package/src/hooks/useFileUrlCache.ts +246 -0
- package/src/hooks/useInactivityTracker.ts +31 -20
- package/src/hooks/useOrganisationSecurity.test.ts +10 -7
- package/src/hooks/useOrganisationSecurity.ts +3 -3
- package/src/hooks/useQueryCache.ts +315 -0
- package/src/index.ts +2 -0
- package/src/providers/services/EventServiceProvider.tsx +4 -1
- package/src/rbac/api.test.ts +21 -6
- package/src/rbac/api.ts +32 -11
- package/src/rbac/audit-batched.ts +223 -0
- package/src/rbac/audit-enhanced.ts +2 -2
- package/src/rbac/audit.test.ts +6 -5
- package/src/rbac/audit.ts +34 -6
- package/src/rbac/cache-invalidation.ts +63 -12
- package/src/rbac/cache.test.ts +2 -2
- package/src/rbac/cache.ts +61 -14
- package/src/rbac/components/PagePermissionGuard.tsx +19 -10
- package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
- package/src/rbac/config.ts +9 -0
- package/src/rbac/engine.ts +2 -21
- package/src/rbac/hooks/usePermissions.ts +21 -5
- package/src/rbac/index.ts +19 -0
- package/src/rbac/performance.ts +210 -0
- package/src/rbac/request-deduplication.ts +87 -0
- package/src/rbac/utils/deep-equal.ts +93 -0
- package/src/services/OrganisationService.ts +5 -4
- package/src/types/file-reference.ts +0 -1
- package/src/utils/file-reference/__tests__/file-reference.test.ts +31 -4
- package/src/utils/file-reference/index.ts +44 -15
- package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
- package/src/utils/google-places/googlePlacesUtils.ts +475 -0
- package/src/utils/google-places/index.ts +26 -0
- package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
- package/src/utils/google-places/types.ts +94 -0
- package/src/utils/index.ts +23 -0
- package/src/utils/request-deduplication.ts +165 -0
- package/src/utils/storage/helpers.ts +143 -4
- package/dist/chunk-445GEP27.js.map +0 -1
- package/dist/chunk-FMUCXFII.js +0 -76
- package/dist/chunk-FMUCXFII.js.map +0 -1
- package/dist/chunk-FSFQFJCU.js.map +0 -1
- package/dist/chunk-FXFJRTKI.js.map +0 -1
- package/dist/chunk-GRIQLQ52.js.map +0 -1
- package/dist/chunk-HDCUMOOI.js.map +0 -1
- package/dist/chunk-OALXJH4Y.js.map +0 -1
- package/dist/chunk-TC7D3CR3.js.map +0 -1
- package/dist/chunk-U6WNSFX5.js.map +0 -1
- /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-GUFUNZ3N.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-643PUAIM.js.map} +0 -0
- /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
- /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
- /package/dist/{chunk-HGPQUCBC.js.map → chunk-2UUZZJFT.js.map} +0 -0
- /package/dist/{chunk-DAGICKHT.js.map → chunk-DDM4CCYT.js.map} +0 -0
- /package/dist/{chunk-XAUHJD3L.js.map → chunk-E7UAOUMY.js.map} +0 -0
|
@@ -1,1304 +1,214 @@
|
|
|
1
1
|
---
|
|
2
|
-
lastUpdated: 2025-
|
|
2
|
+
lastUpdated: 2025-01-28T00:00:00+11:00
|
|
3
3
|
version: 0.5.181
|
|
4
4
|
reviewedBy: documentation-standards-audit
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Public Pages Implementation Guide
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## Overview
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
This guide explains how to implement public pages that are accessible to anonymous users without authentication. Public pages use the `public_recipe_details` view and respect the `public_readable` flag on events.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Public Event Configuration
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
### Enabling Public Access
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
```tsx
|
|
19
|
-
// DON'T DO THIS - Public pages inside auth context
|
|
20
|
-
<UnifiedAuthProvider>
|
|
21
|
-
<OrganisationProvider>
|
|
22
|
-
<EventProvider>
|
|
23
|
-
<Routes>
|
|
24
|
-
<Route path="/events/:eventCode" element={<PublicPage />} /> {/* ❌ BREAKS */}
|
|
25
|
-
<Route path="/dashboard" element={<Dashboard />} />
|
|
26
|
-
</Routes>
|
|
27
|
-
</EventProvider>
|
|
28
|
-
</OrganisationProvider>
|
|
29
|
-
</UnifiedAuthProvider>
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
### ✅ **CORRECT - Separate Applications**
|
|
33
|
-
```tsx
|
|
34
|
-
// DO THIS - Public pages completely separate
|
|
35
|
-
<BrowserRouter>
|
|
36
|
-
<Routes>
|
|
37
|
-
{/* Public routes - NO authentication context */}
|
|
38
|
-
<Route path="/events/*" element={<PublicPageApp />} />
|
|
39
|
-
<Route path="/public/*" element={<PublicPageApp />} />
|
|
40
|
-
|
|
41
|
-
{/* Authenticated routes - WITH authentication context */}
|
|
42
|
-
<Route path="/*" element={<AuthenticatedApp />} />
|
|
43
|
-
</Routes>
|
|
44
|
-
</BrowserRouter>
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## 🏗️ **Required Architecture**
|
|
48
|
-
|
|
49
|
-
### 1. **Two Separate Applications**
|
|
17
|
+
To make an event publicly accessible:
|
|
50
18
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
### 2. **Root Router Decision**
|
|
57
|
-
|
|
58
|
-
Your root component decides which app to render based on the route:
|
|
59
|
-
|
|
60
|
-
```tsx
|
|
61
|
-
function RootApp() {
|
|
62
|
-
return (
|
|
63
|
-
<BrowserRouter>
|
|
64
|
-
<Routes>
|
|
65
|
-
{/* Public routes - completely isolated */}
|
|
66
|
-
<Route path="/events/*" element={<PublicPageApp />} />
|
|
67
|
-
<Route path="/public/*" element={<PublicPageApp />} />
|
|
68
|
-
<Route path="/reports/*" element={<PublicPageApp />} />
|
|
69
|
-
|
|
70
|
-
{/* Authenticated routes - normal app */}
|
|
71
|
-
<Route path="/*" element={<AuthenticatedApp />} />
|
|
72
|
-
</Routes>
|
|
73
|
-
</BrowserRouter>
|
|
74
|
-
);
|
|
75
|
-
}
|
|
19
|
+
```sql
|
|
20
|
+
UPDATE event
|
|
21
|
+
SET public_readable = true
|
|
22
|
+
WHERE event_id = 'YOUR_EVENT_ID';
|
|
76
23
|
```
|
|
77
24
|
|
|
78
|
-
|
|
25
|
+
**Security Note**: Only events with `public_readable = true` AND `is_visible = true` are accessible on public pages.
|
|
79
26
|
|
|
80
|
-
###
|
|
27
|
+
### Checking Public Access
|
|
81
28
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
import {
|
|
87
|
-
PublicPageProvider,
|
|
88
|
-
PublicPageLayout,
|
|
89
|
-
PublicPageHeader,
|
|
90
|
-
PublicPageFooter,
|
|
91
|
-
usePublicEvent,
|
|
92
|
-
usePublicRouteParams,
|
|
93
|
-
PublicLoadingSpinner
|
|
94
|
-
} from '@jmruthers/pace-core';
|
|
29
|
+
The `check_public_event_access()` helper function automatically checks:
|
|
30
|
+
- Event is visible (`is_visible = true`)
|
|
31
|
+
- Event is publicly readable (`public_readable = true`)
|
|
32
|
+
- Super admins always have access
|
|
95
33
|
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<PublicPageProvider>
|
|
99
|
-
<Routes>
|
|
100
|
-
<Route path="/events/:eventCode/recipe-grid-report" element={<PublicRecipePage />} />
|
|
101
|
-
<Route path="/events/:eventCode" element={<PublicEventPage />} />
|
|
102
|
-
<Route path="/reports/:reportId" element={<PublicReportPage />} />
|
|
103
|
-
</Routes>
|
|
104
|
-
</PublicPageProvider>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
```
|
|
34
|
+
## Using the Public Recipe View
|
|
108
35
|
|
|
109
|
-
###
|
|
36
|
+
### Querying the View
|
|
110
37
|
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
import React from 'react';
|
|
114
|
-
import {
|
|
115
|
-
PublicPageLayout,
|
|
116
|
-
PublicPageHeader,
|
|
117
|
-
PublicPageFooter,
|
|
118
|
-
usePublicEvent,
|
|
119
|
-
usePublicRouteParams,
|
|
120
|
-
PublicLoadingSpinner
|
|
121
|
-
} from '@jmruthers/pace-core';
|
|
38
|
+
```typescript
|
|
39
|
+
import { useSupabase } from '@jmruthers/pace-core';
|
|
122
40
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const { eventCode } = usePublicRouteParams(
|
|
41
|
+
function RecipeDetailsPage() {
|
|
42
|
+
const { supabase } = useSupabase();
|
|
43
|
+
const { eventCode } = usePublicRouteParams();
|
|
126
44
|
|
|
127
|
-
//
|
|
128
|
-
const {
|
|
45
|
+
// Query public recipe view
|
|
46
|
+
const { data: recipes, error } = await supabase
|
|
47
|
+
.from('public_recipe_details')
|
|
48
|
+
.select('*')
|
|
49
|
+
.eq('event_id', eventId)
|
|
50
|
+
.order('dish_name');
|
|
129
51
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return <
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Step 4: Handle error
|
|
136
|
-
if (error || !event) {
|
|
137
|
-
return (
|
|
138
|
-
<div className="min-h-screen bg-background flex items-center justify-center">
|
|
139
|
-
<div className="text-center">
|
|
140
|
-
<h1 className="text-2xl font-bold text-sec-900 mb-4">
|
|
141
|
-
Recipe Grid Report Not Found
|
|
142
|
-
</h1>
|
|
143
|
-
<p className="text-sec-600">
|
|
144
|
-
The event code "{eventCode}" is invalid or not available for public viewing.
|
|
145
|
-
</p>
|
|
146
|
-
</div>
|
|
147
|
-
</div>
|
|
148
|
-
);
|
|
52
|
+
if (error) {
|
|
53
|
+
console.error('Error fetching recipes:', error);
|
|
54
|
+
return <ErrorPage />;
|
|
149
55
|
}
|
|
150
56
|
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<PublicPageLayout eventCode={eventCode || ''} event={event}>
|
|
154
|
-
<PublicPageHeader
|
|
155
|
-
event={event}
|
|
156
|
-
eventCode={eventCode || ''}
|
|
157
|
-
title="Recipe Grid Report"
|
|
158
|
-
description="Public recipe grid report for this event"
|
|
159
|
-
/>
|
|
160
|
-
|
|
161
|
-
<main className="max-w-6xl mx-auto px-4 py-8">
|
|
162
|
-
{/* Your public page content here */}
|
|
163
|
-
<div className="bg-background rounded-lg shadow p-6">
|
|
164
|
-
<h2 className="text-xl font-semibold mb-4">Recipe Grid Report</h2>
|
|
165
|
-
<p>Event: {event.event_name}</p>
|
|
166
|
-
<p>Code: {event.event_code}</p>
|
|
167
|
-
{/* Add your recipe grid content */}
|
|
168
|
-
</div>
|
|
169
|
-
</main>
|
|
170
|
-
|
|
171
|
-
<PublicPageFooter event={event} />
|
|
172
|
-
</PublicPageLayout>
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### Step 3: Update Your Main App
|
|
178
|
-
|
|
179
|
-
```tsx
|
|
180
|
-
// App.tsx
|
|
181
|
-
import React from 'react';
|
|
182
|
-
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
183
|
-
import { UnifiedAuthProvider, OrganisationProvider, EventProvider } from '@jmruthers/pace-core';
|
|
184
|
-
import { PublicPageApp } from './PublicPageApp';
|
|
185
|
-
import { AuthenticatedApp } from './AuthenticatedApp';
|
|
186
|
-
|
|
187
|
-
function App() {
|
|
188
|
-
return (
|
|
189
|
-
<BrowserRouter>
|
|
190
|
-
<Routes>
|
|
191
|
-
{/* Public routes - completely separate */}
|
|
192
|
-
<Route path="/events/*" element={<PublicPageApp />} />
|
|
193
|
-
<Route path="/public/*" element={<PublicPageApp />} />
|
|
194
|
-
<Route path="/reports/*" element={<PublicPageApp />} />
|
|
195
|
-
|
|
196
|
-
{/* Authenticated routes */}
|
|
197
|
-
<Route path="/*" element={
|
|
198
|
-
<UnifiedAuthProvider supabaseClient={supabase} appName="My App">
|
|
199
|
-
<OrganisationProvider>
|
|
200
|
-
<EventProvider>
|
|
201
|
-
<AuthenticatedApp />
|
|
202
|
-
</EventProvider>
|
|
203
|
-
</OrganisationProvider>
|
|
204
|
-
</UnifiedAuthProvider>
|
|
205
|
-
} />
|
|
206
|
-
</Routes>
|
|
207
|
-
</BrowserRouter>
|
|
208
|
-
);
|
|
57
|
+
return <RecipeGrid recipes={recipes} />;
|
|
209
58
|
}
|
|
210
|
-
|
|
211
|
-
export default App;
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
## 🧩 **Core Components**
|
|
215
|
-
|
|
216
|
-
### PublicPageProvider
|
|
217
|
-
|
|
218
|
-
**Required wrapper for all public pages.** Provides environment variables and context without authentication.
|
|
219
|
-
|
|
220
|
-
```tsx
|
|
221
|
-
import { PublicPageProvider } from '@jmruthers/pace-core';
|
|
222
|
-
|
|
223
|
-
<PublicPageProvider>
|
|
224
|
-
{/* All your public pages go here */}
|
|
225
|
-
</PublicPageProvider>
|
|
226
59
|
```
|
|
227
60
|
|
|
228
|
-
###
|
|
229
|
-
|
|
230
|
-
**Main layout component for public pages.** Handles event data and provides consistent styling.
|
|
61
|
+
### Available Columns
|
|
231
62
|
|
|
232
|
-
|
|
233
|
-
import { PublicPageLayout } from '@jmruthers/pace-core';
|
|
234
|
-
|
|
235
|
-
<PublicPageLayout
|
|
236
|
-
eventCode="EVENT123"
|
|
237
|
-
event={eventData}
|
|
238
|
-
showFooter={true}
|
|
239
|
-
>
|
|
240
|
-
{/* Your page content */}
|
|
241
|
-
</PublicPageLayout>
|
|
242
|
-
```
|
|
63
|
+
The `public_recipe_details` view exposes:
|
|
243
64
|
|
|
244
|
-
**
|
|
245
|
-
-
|
|
246
|
-
-
|
|
247
|
-
- `
|
|
248
|
-
- `isLoading` (optional) - Loading state (if true, shows loading spinner)
|
|
249
|
-
- `error` (optional) - Error state (if provided, shows error message)
|
|
250
|
-
- `refetch` (optional) - Function to retry loading event data
|
|
251
|
-
- `showFooter` (optional) - Show/hide footer (default: true)
|
|
252
|
-
- `className` (optional, deprecated) - Custom CSS classes (deprecated, no longer used as wrapper div was removed)
|
|
253
|
-
- `errorFallback` (optional) - Custom error component
|
|
254
|
-
- `loadingFallback` (optional) - Custom loading component
|
|
255
|
-
- `customHeader` (optional) - Custom header component
|
|
256
|
-
- `customFooter` (optional) - Custom footer component
|
|
257
|
-
- `showValidationErrors` (optional) - Whether to show event validation errors (default: true)
|
|
258
|
-
- `loadingMessage` (optional) - Custom loading message
|
|
259
|
-
|
|
260
|
-
### PublicPageHeader
|
|
261
|
-
|
|
262
|
-
**Header component with event branding and information.**
|
|
263
|
-
|
|
264
|
-
```tsx
|
|
265
|
-
import { PublicPageHeader } from '@jmruthers/pace-core';
|
|
266
|
-
|
|
267
|
-
<PublicPageHeader
|
|
268
|
-
event={event}
|
|
269
|
-
eventCode="EVENT123"
|
|
270
|
-
title="Recipe Grid Report"
|
|
271
|
-
description="Public recipe grid report for this event"
|
|
272
|
-
showEventLogo={true}
|
|
273
|
-
showAppLogo={true}
|
|
274
|
-
/>
|
|
275
|
-
```
|
|
65
|
+
- **Dish Information**: `dish_id`, `dish_name`, `dish_code`, `dish_description`, `dish_equipment`, `dish_dietnote`, `dish_prep`, `dish_step1-5`
|
|
66
|
+
- **Meal Type**: `mealtype_name`, `dish_mealtype_id`
|
|
67
|
+
- **Event Information**: `event_id`, `event_name`, `event_code`, `organisation_id`
|
|
68
|
+
- **Recipe Information**: `recipe_id` (if available)
|
|
276
69
|
|
|
277
|
-
**
|
|
278
|
-
- `eventCode` (required) - Event code from URL
|
|
279
|
-
- `event` (optional) - Event data object
|
|
280
|
-
- `title` (optional) - Page title
|
|
281
|
-
- `description` (optional) - Page description
|
|
282
|
-
- `showEventLogo` (optional) - Show event logo (default: true)
|
|
283
|
-
- `showAppLogo` (optional) - Show app logo (default: true)
|
|
284
|
-
- `className` (optional) - Custom CSS classes for the header
|
|
285
|
-
- `children` (optional) - Custom content to display in the header
|
|
286
|
-
- `customEventLogo` (optional) - Custom event logo component
|
|
70
|
+
**Note**: Sensitive columns like `created_by`, `updated_by` are NOT exposed in the public view.
|
|
287
71
|
|
|
288
|
-
###
|
|
72
|
+
### Filtering
|
|
289
73
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
74
|
+
```typescript
|
|
75
|
+
// Filter by meal type
|
|
76
|
+
const { data: breakfastRecipes } = await supabase
|
|
77
|
+
.from('public_recipe_details')
|
|
78
|
+
.select('*')
|
|
79
|
+
.eq('event_id', eventId)
|
|
80
|
+
.eq('mealtype_name', 'Breakfast');
|
|
294
81
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
82
|
+
// Filter by diet requirements
|
|
83
|
+
const { data: vegetarianRecipes } = await supabase
|
|
84
|
+
.from('public_recipe_details')
|
|
85
|
+
.select('*')
|
|
86
|
+
.eq('event_id', eventId)
|
|
87
|
+
.eq('dish_dietnote', 'Vegetarian');
|
|
301
88
|
```
|
|
302
89
|
|
|
303
|
-
##
|
|
304
|
-
|
|
305
|
-
### usePublicRouteParams
|
|
90
|
+
## Performance Considerations
|
|
306
91
|
|
|
307
|
-
|
|
92
|
+
### View Performance
|
|
308
93
|
|
|
309
|
-
|
|
310
|
-
|
|
94
|
+
The `public_recipe_details` view:
|
|
95
|
+
- Uses performance-optimized helper functions (`check_public_event_access()`)
|
|
96
|
+
- Automatically filters to `public_readable = true` events
|
|
97
|
+
- Joins only necessary tables
|
|
98
|
+
- Should complete queries in < 500ms
|
|
311
99
|
|
|
312
|
-
|
|
313
|
-
const { eventCode, reportId, pageId } = usePublicRouteParams({
|
|
314
|
-
fetchEventData: false // Don't auto-fetch event data
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
// Use the parameters
|
|
318
|
-
console.log('Event Code:', eventCode);
|
|
319
|
-
console.log('Report ID:', reportId);
|
|
320
|
-
}
|
|
321
|
-
```
|
|
100
|
+
### Indexes
|
|
322
101
|
|
|
323
|
-
|
|
102
|
+
The view benefits from indexes created in Phase 0.5:
|
|
103
|
+
- `idx_event_public_readable` on `event(public_readable, is_visible)`
|
|
104
|
+
- Foreign key indexes on `cake_dish` and related tables
|
|
324
105
|
|
|
325
|
-
|
|
106
|
+
## Implementation Example
|
|
326
107
|
|
|
327
|
-
|
|
328
|
-
import { usePublicEvent } from '@jmruthers/pace-core';
|
|
108
|
+
### Complete Public Recipe Page
|
|
329
109
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (isLoading) return <div>Loading...</div>;
|
|
334
|
-
if (error) return <div>Error: {error.message}</div>;
|
|
335
|
-
if (!event) return <div>Event not found</div>;
|
|
336
|
-
|
|
337
|
-
return <div>Event: {event.event_name}</div>;
|
|
338
|
-
}
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
### useIsPublicPage
|
|
342
|
-
|
|
343
|
-
**Check if current page is a public page.**
|
|
344
|
-
|
|
345
|
-
```tsx
|
|
346
|
-
import { useIsPublicPage } from '@jmruthers/pace-core';
|
|
347
|
-
|
|
348
|
-
function MyComponent() {
|
|
349
|
-
const isPublicPage = useIsPublicPage();
|
|
350
|
-
|
|
351
|
-
if (isPublicPage) {
|
|
352
|
-
return <div>This is a public page</div>;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
return <div>This is an authenticated page</div>;
|
|
356
|
-
}
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
## 📋 **Common Implementation Patterns**
|
|
360
|
-
|
|
361
|
-
### Pattern 1: Event-Based Public Pages
|
|
110
|
+
```typescript
|
|
111
|
+
import { usePublicEvent, usePublicRouteParams } from '@jmruthers/pace-core';
|
|
112
|
+
import { PublicPageLayout } from '@jmruthers/pace-core';
|
|
362
113
|
|
|
363
|
-
|
|
364
|
-
// /events/:eventCode/recipe-grid-report
|
|
365
|
-
function PublicRecipePage() {
|
|
114
|
+
function PublicRecipeDetailsPage() {
|
|
366
115
|
const { eventCode } = usePublicRouteParams({ fetchEventData: false });
|
|
367
116
|
const { event, isLoading, error } = usePublicEvent(eventCode || '');
|
|
117
|
+
const { supabase } = useSupabase();
|
|
368
118
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
return (
|
|
373
|
-
<PublicPageLayout eventCode={eventCode || ''} event={event}>
|
|
374
|
-
<PublicPageHeader
|
|
375
|
-
event={event}
|
|
376
|
-
eventCode={eventCode || ''}
|
|
377
|
-
title="Recipe Grid Report"
|
|
378
|
-
/>
|
|
379
|
-
|
|
380
|
-
<main>
|
|
381
|
-
{/* Your recipe grid content */}
|
|
382
|
-
</main>
|
|
383
|
-
|
|
384
|
-
<PublicPageFooter event={event} />
|
|
385
|
-
</PublicPageLayout>
|
|
386
|
-
);
|
|
387
|
-
}
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
### Pattern 2: Report-Based Public Pages
|
|
391
|
-
|
|
392
|
-
```tsx
|
|
393
|
-
// /reports/:reportId
|
|
394
|
-
function PublicReportPage() {
|
|
395
|
-
const { reportId } = usePublicRouteParams({ fetchEventData: false });
|
|
396
|
-
const [report, setReport] = useState(null);
|
|
397
|
-
const [loading, setLoading] = useState(true);
|
|
119
|
+
const [recipes, setRecipes] = useState([]);
|
|
120
|
+
const [selectedMealType, setSelectedMealType] = useState<string | null>(null);
|
|
398
121
|
|
|
399
122
|
useEffect(() => {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
<PublicPageHeader
|
|
410
|
-
event={report.event}
|
|
411
|
-
eventCode={report.eventCode}
|
|
412
|
-
title={report.title}
|
|
413
|
-
description={report.description}
|
|
414
|
-
/>
|
|
415
|
-
|
|
416
|
-
<main>
|
|
417
|
-
{/* Your report content */}
|
|
418
|
-
</main>
|
|
419
|
-
|
|
420
|
-
<PublicPageFooter event={report.event} />
|
|
421
|
-
</PublicPageLayout>
|
|
422
|
-
);
|
|
423
|
-
}
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
### Pattern 3: Simple Public Info Pages
|
|
427
|
-
|
|
428
|
-
```tsx
|
|
429
|
-
// /public/:pageId
|
|
430
|
-
function PublicInfoPage() {
|
|
431
|
-
const { pageId } = usePublicRouteParams({ fetchEventData: false });
|
|
432
|
-
|
|
433
|
-
return (
|
|
434
|
-
<div className="min-h-screen bg-background">
|
|
435
|
-
<div className="max-w-4xl mx-auto px-4 py-8">
|
|
436
|
-
<h1 className="text-3xl font-bold mb-6">Public Information</h1>
|
|
437
|
-
<p>Page ID: {pageId}</p>
|
|
438
|
-
{/* Your content */}
|
|
439
|
-
</div>
|
|
440
|
-
</div>
|
|
441
|
-
);
|
|
442
|
-
}
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
## ⚠️ **Common Mistakes & Solutions**
|
|
446
|
-
|
|
447
|
-
### Mistake 1: Public Pages Inside Auth Context
|
|
448
|
-
|
|
449
|
-
**❌ Problem:**
|
|
450
|
-
```tsx
|
|
451
|
-
<UnifiedAuthProvider>
|
|
452
|
-
<Route path="/events/:eventCode" element={<PublicPage />} />
|
|
453
|
-
</UnifiedAuthProvider>
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
**✅ Solution:**
|
|
457
|
-
```tsx
|
|
458
|
-
<Routes>
|
|
459
|
-
<Route path="/events/*" element={<PublicPageApp />} />
|
|
460
|
-
<Route path="/*" element={
|
|
461
|
-
<UnifiedAuthProvider>
|
|
462
|
-
<AuthenticatedApp />
|
|
463
|
-
</UnifiedAuthProvider>
|
|
464
|
-
} />
|
|
465
|
-
</Routes>
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
### Mistake 2: Using Auth Hooks in Public Pages
|
|
469
|
-
|
|
470
|
-
**❌ Problem:**
|
|
471
|
-
```tsx
|
|
472
|
-
function PublicPage() {
|
|
473
|
-
const { user } = useUnifiedAuth(); // ❌ This will break
|
|
474
|
-
// ...
|
|
475
|
-
}
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
**✅ Solution:**
|
|
479
|
-
```tsx
|
|
480
|
-
function PublicPage() {
|
|
481
|
-
const { event } = usePublicEvent(eventCode); // ✅ Use public hooks
|
|
482
|
-
// ...
|
|
483
|
-
}
|
|
484
|
-
```
|
|
485
|
-
|
|
486
|
-
### Mistake 3: Missing PublicPageProvider
|
|
487
|
-
|
|
488
|
-
**❌ Problem:**
|
|
489
|
-
```tsx
|
|
490
|
-
<Route path="/events/:eventCode" element={<PublicPage />} />
|
|
491
|
-
```
|
|
492
|
-
|
|
493
|
-
**✅ Solution:**
|
|
494
|
-
```tsx
|
|
495
|
-
<Route path="/events/*" element={
|
|
496
|
-
<PublicPageProvider>
|
|
497
|
-
<Routes>
|
|
498
|
-
<Route path=":eventCode" element={<PublicPage />} />
|
|
499
|
-
</Routes>
|
|
500
|
-
</PublicPageProvider>
|
|
501
|
-
} />
|
|
502
|
-
```
|
|
503
|
-
|
|
504
|
-
### Mistake 4: Not Handling Loading/Error States
|
|
505
|
-
|
|
506
|
-
**❌ Problem:**
|
|
507
|
-
```tsx
|
|
508
|
-
function PublicPage() {
|
|
509
|
-
const { event } = usePublicEvent(eventCode);
|
|
510
|
-
return <div>{event.event_name}</div>; // ❌ No loading/error handling
|
|
511
|
-
}
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
**✅ Solution:**
|
|
515
|
-
```tsx
|
|
516
|
-
function PublicPage() {
|
|
517
|
-
const { event, isLoading, error } = usePublicEvent(eventCode);
|
|
518
|
-
|
|
519
|
-
if (isLoading) return <PublicLoadingSpinner message="Loading..." />;
|
|
520
|
-
if (error) return <div>Error: {error.message}</div>;
|
|
521
|
-
if (!event) return <div>Event not found</div>;
|
|
522
|
-
|
|
523
|
-
return <div>{event.event_name}</div>;
|
|
524
|
-
}
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
## 🔧 **Environment Setup**
|
|
528
|
-
|
|
529
|
-
### Required Environment Variables
|
|
530
|
-
|
|
531
|
-
```env
|
|
532
|
-
# .env.local
|
|
533
|
-
VITE_SUPABASE_URL=your_supabase_url
|
|
534
|
-
VITE_SUPABASE_ANON_KEY=your_supabase_anon_key
|
|
535
|
-
VITE_APP_NAME=Your App Name
|
|
536
|
-
```
|
|
537
|
-
|
|
538
|
-
### Database Setup
|
|
539
|
-
|
|
540
|
-
Public pages need access to event data. Ensure your database has:
|
|
541
|
-
|
|
542
|
-
1. **Events table** with public event data
|
|
543
|
-
2. **RLS policies** that allow public access to specific events
|
|
544
|
-
3. **Event codes** that are publicly accessible
|
|
545
|
-
|
|
546
|
-
```sql
|
|
547
|
-
-- Example RLS policy for public events
|
|
548
|
-
CREATE POLICY "Public events are viewable by anyone" ON events
|
|
549
|
-
FOR SELECT USING (is_public = true);
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
## 🧪 **Testing Public Pages**
|
|
553
|
-
|
|
554
|
-
### Unit Testing
|
|
555
|
-
|
|
556
|
-
```tsx
|
|
557
|
-
import { render, screen } from '@testing-library/react';
|
|
558
|
-
import { PublicPageProvider } from '@jmruthers/pace-core';
|
|
559
|
-
import { PublicRecipePage } from './PublicRecipePage';
|
|
560
|
-
|
|
561
|
-
test('renders public recipe page', () => {
|
|
562
|
-
render(
|
|
563
|
-
<PublicPageProvider>
|
|
564
|
-
<PublicRecipePage />
|
|
565
|
-
</PublicPageProvider>
|
|
566
|
-
);
|
|
567
|
-
|
|
568
|
-
expect(screen.getByText('Recipe Grid Report')).toBeInTheDocument();
|
|
569
|
-
});
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
### Integration Testing
|
|
573
|
-
|
|
574
|
-
```tsx
|
|
575
|
-
import { render, screen } from '@testing-library/react';
|
|
576
|
-
import { BrowserRouter } from 'react-router-dom';
|
|
577
|
-
import { PublicPageApp } from './PublicPageApp';
|
|
578
|
-
|
|
579
|
-
test('public page app routing works', () => {
|
|
580
|
-
render(
|
|
581
|
-
<BrowserRouter>
|
|
582
|
-
<PublicPageApp />
|
|
583
|
-
</BrowserRouter>
|
|
584
|
-
);
|
|
585
|
-
|
|
586
|
-
// Test routing to public pages
|
|
587
|
-
expect(screen.getByText('Recipe Grid Report')).toBeInTheDocument();
|
|
588
|
-
});
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
## 🚀 **Deployment Considerations**
|
|
592
|
-
|
|
593
|
-
### Build Configuration
|
|
594
|
-
|
|
595
|
-
Ensure your build process handles both authenticated and public routes:
|
|
596
|
-
|
|
597
|
-
```tsx
|
|
598
|
-
// vite.config.ts
|
|
599
|
-
export default defineConfig({
|
|
600
|
-
plugins: [
|
|
601
|
-
react(),
|
|
602
|
-
tailwindcss({
|
|
603
|
-
content: [
|
|
604
|
-
'./src/**/*.{js,ts,jsx,tsx}',
|
|
605
|
-
'./node_modules/@jmruthers/pace-core/**/*.{js,ts,jsx,tsx}'
|
|
606
|
-
]
|
|
607
|
-
})
|
|
608
|
-
],
|
|
609
|
-
build: {
|
|
610
|
-
rollupOptions: {
|
|
611
|
-
output: {
|
|
612
|
-
manualChunks: {
|
|
613
|
-
// Separate chunks for public and authenticated apps
|
|
614
|
-
'public-app': ['./src/PublicPageApp.tsx'],
|
|
615
|
-
'auth-app': ['./src/AuthenticatedApp.tsx']
|
|
616
|
-
}
|
|
617
|
-
}
|
|
123
|
+
if (!event?.event_id) return;
|
|
124
|
+
|
|
125
|
+
let query = supabase
|
|
126
|
+
.from('public_recipe_details')
|
|
127
|
+
.select('*')
|
|
128
|
+
.eq('event_id', event.event_id);
|
|
129
|
+
|
|
130
|
+
if (selectedMealType) {
|
|
131
|
+
query = query.eq('mealtype_name', selectedMealType);
|
|
618
132
|
}
|
|
619
|
-
|
|
620
|
-
})
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
# nginx.conf
|
|
629
|
-
location /events/ {
|
|
630
|
-
try_files $uri $uri/ /index.html; # SPA routing for public pages
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
location /public/ {
|
|
634
|
-
try_files $uri $uri/ /index.html; # SPA routing for public pages
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
location / {
|
|
638
|
-
try_files $uri $uri/ /index.html; # SPA routing for authenticated pages
|
|
639
|
-
}
|
|
640
|
-
```
|
|
641
|
-
|
|
642
|
-
## 📚 **Complete Example**
|
|
643
|
-
|
|
644
|
-
Here's a complete working example:
|
|
645
|
-
|
|
646
|
-
```tsx
|
|
647
|
-
// App.tsx - Root application
|
|
648
|
-
import React from 'react';
|
|
649
|
-
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
650
|
-
import { UnifiedAuthProvider, OrganisationProvider, EventProvider } from '@jmruthers/pace-core';
|
|
651
|
-
import { PublicPageApp } from './PublicPageApp';
|
|
652
|
-
import { AuthenticatedApp } from './AuthenticatedApp';
|
|
653
|
-
|
|
654
|
-
function App() {
|
|
655
|
-
return (
|
|
656
|
-
<BrowserRouter>
|
|
657
|
-
<Routes>
|
|
658
|
-
{/* Public routes - completely separate */}
|
|
659
|
-
<Route path="/events/*" element={<PublicPageApp />} />
|
|
660
|
-
<Route path="/public/*" element={<PublicPageApp />} />
|
|
661
|
-
<Route path="/reports/*" element={<PublicPageApp />} />
|
|
662
|
-
|
|
663
|
-
{/* Authenticated routes */}
|
|
664
|
-
<Route path="/*" element={
|
|
665
|
-
<UnifiedAuthProvider supabaseClient={supabase} appName="My App">
|
|
666
|
-
<OrganisationProvider>
|
|
667
|
-
<EventProvider>
|
|
668
|
-
<AuthenticatedApp />
|
|
669
|
-
</EventProvider>
|
|
670
|
-
</OrganisationProvider>
|
|
671
|
-
</UnifiedAuthProvider>
|
|
672
|
-
} />
|
|
673
|
-
</Routes>
|
|
674
|
-
</BrowserRouter>
|
|
675
|
-
);
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
export default App;
|
|
679
|
-
```
|
|
680
|
-
|
|
681
|
-
```tsx
|
|
682
|
-
// PublicPageApp.tsx - Public pages application
|
|
683
|
-
import React from 'react';
|
|
684
|
-
import { Routes, Route } from 'react-router-dom';
|
|
685
|
-
import { PublicPageProvider } from '@jmruthers/pace-core';
|
|
686
|
-
import { PublicRecipePage } from './pages/PublicRecipePage';
|
|
687
|
-
import { PublicEventPage } from './pages/PublicEventPage';
|
|
688
|
-
|
|
689
|
-
export function PublicPageApp() {
|
|
690
|
-
return (
|
|
691
|
-
<PublicPageProvider>
|
|
692
|
-
<Routes>
|
|
693
|
-
<Route path="/events/:eventCode/recipe-grid-report" element={<PublicRecipePage />} />
|
|
694
|
-
<Route path="/events/:eventCode" element={<PublicEventPage />} />
|
|
695
|
-
</Routes>
|
|
696
|
-
</PublicPageProvider>
|
|
697
|
-
);
|
|
698
|
-
}
|
|
699
|
-
```
|
|
700
|
-
|
|
701
|
-
```tsx
|
|
702
|
-
// pages/PublicRecipePage.tsx - Individual public page
|
|
703
|
-
import React from 'react';
|
|
704
|
-
import {
|
|
705
|
-
PublicPageLayout,
|
|
706
|
-
PublicPageHeader,
|
|
707
|
-
PublicPageFooter,
|
|
708
|
-
usePublicEvent,
|
|
709
|
-
usePublicRouteParams,
|
|
710
|
-
PublicLoadingSpinner
|
|
711
|
-
} from '@jmruthers/pace-core';
|
|
712
|
-
|
|
713
|
-
export function PublicRecipePage() {
|
|
714
|
-
const { eventCode } = usePublicRouteParams({ fetchEventData: false });
|
|
715
|
-
const { event, isLoading, error } = usePublicEvent(eventCode || '');
|
|
716
|
-
|
|
717
|
-
if (isLoading) {
|
|
718
|
-
return <PublicLoadingSpinner message="Loading recipe grid report..." />;
|
|
719
|
-
}
|
|
133
|
+
|
|
134
|
+
query.order('dish_name').then(({ data, error }) => {
|
|
135
|
+
if (error) {
|
|
136
|
+
console.error('Error fetching recipes:', error);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
setRecipes(data || []);
|
|
140
|
+
});
|
|
141
|
+
}, [event?.event_id, selectedMealType, supabase]);
|
|
720
142
|
|
|
721
|
-
if (
|
|
722
|
-
|
|
723
|
-
<div className="min-h-screen bg-background flex items-center justify-center">
|
|
724
|
-
<div className="text-center">
|
|
725
|
-
<h1 className="text-2xl font-bold text-sec-900 mb-4">
|
|
726
|
-
Recipe Grid Report Not Found
|
|
727
|
-
</h1>
|
|
728
|
-
<p className="text-sec-600">
|
|
729
|
-
The event code "{eventCode}" is invalid or not available for public viewing.
|
|
730
|
-
</p>
|
|
731
|
-
</div>
|
|
732
|
-
</div>
|
|
733
|
-
);
|
|
734
|
-
}
|
|
143
|
+
if (isLoading) return <LoadingSpinner />;
|
|
144
|
+
if (error || !event) return <ErrorPage />;
|
|
735
145
|
|
|
736
146
|
return (
|
|
737
147
|
<PublicPageLayout eventCode={eventCode || ''} event={event}>
|
|
738
|
-
<
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
description="Public recipe grid report for this event"
|
|
148
|
+
<RecipeGrid
|
|
149
|
+
recipes={recipes}
|
|
150
|
+
mealTypes={['Breakfast', 'Lunch', 'Dinner']}
|
|
151
|
+
onMealTypeChange={setSelectedMealType}
|
|
743
152
|
/>
|
|
744
|
-
|
|
745
|
-
<main className="max-w-6xl mx-auto px-4 py-8">
|
|
746
|
-
<div className="bg-background rounded-lg shadow p-6">
|
|
747
|
-
<h2 className="text-xl font-semibold mb-4">Recipe Grid Report</h2>
|
|
748
|
-
<p>Event: {event.event_name}</p>
|
|
749
|
-
<p>Code: {event.event_code}</p>
|
|
750
|
-
{/* Add your recipe grid content here */}
|
|
751
|
-
</div>
|
|
752
|
-
</main>
|
|
753
|
-
|
|
754
|
-
<PublicPageFooter event={event} />
|
|
755
153
|
</PublicPageLayout>
|
|
756
154
|
);
|
|
757
155
|
}
|
|
758
156
|
```
|
|
759
157
|
|
|
760
|
-
##
|
|
761
|
-
|
|
762
|
-
- **[API Reference - Public Components](../api-reference/components.md#public-components)** - Complete component API
|
|
763
|
-
- **[Troubleshooting - Public Pages](../troubleshooting/common-issues.md#public-pages)** - Common issues and solutions
|
|
764
|
-
- **[Examples - Public Page App](../getting-started/examples/)** - Complete working examples
|
|
765
|
-
- **[Migration Guide - Public Pages](../migration/README.md#public-pages)** - Migration from other approaches
|
|
766
|
-
|
|
767
|
-
---
|
|
768
|
-
|
|
769
|
-
## 🚀 **Advanced Topics**
|
|
770
|
-
|
|
771
|
-
This section covers advanced topics for public pages, including event logos, caching strategies, SEO optimization, and performance tuning.
|
|
772
|
-
|
|
773
|
-
### EventLogo Component
|
|
774
|
-
|
|
775
|
-
A component for displaying event logos with automatic loading and caching.
|
|
776
|
-
|
|
777
|
-
#### Props
|
|
778
|
-
|
|
779
|
-
```typescript
|
|
780
|
-
interface EventLogoProps {
|
|
781
|
-
eventId: string;
|
|
782
|
-
size?: 'small' | 'medium' | 'large' | 'xlarge';
|
|
783
|
-
variant?: 'default' | 'square' | 'wide';
|
|
784
|
-
className?: string;
|
|
785
|
-
fallback?: React.ReactNode;
|
|
786
|
-
showLoading?: boolean;
|
|
787
|
-
}
|
|
788
|
-
```
|
|
789
|
-
|
|
790
|
-
#### Usage Examples
|
|
791
|
-
|
|
792
|
-
**Basic Logo:**
|
|
793
|
-
```tsx
|
|
794
|
-
<EventLogo eventId={eventId} size="medium" />
|
|
795
|
-
```
|
|
796
|
-
|
|
797
|
-
**Custom Styling:**
|
|
798
|
-
```tsx
|
|
799
|
-
<EventLogo
|
|
800
|
-
eventId={eventId}
|
|
801
|
-
size="large"
|
|
802
|
-
variant="square"
|
|
803
|
-
className="rounded-lg shadow-lg"
|
|
804
|
-
/>
|
|
805
|
-
```
|
|
806
|
-
|
|
807
|
-
**With Fallback:**
|
|
808
|
-
```tsx
|
|
809
|
-
<EventLogo
|
|
810
|
-
eventId={eventId}
|
|
811
|
-
size="medium"
|
|
812
|
-
fallback={
|
|
813
|
-
<div className="bg-sec-200 text-sec-500 p-4 rounded">
|
|
814
|
-
No Logo Available
|
|
815
|
-
</div>
|
|
816
|
-
}
|
|
817
|
-
/>
|
|
818
|
-
```
|
|
819
|
-
|
|
820
|
-
### usePublicEventLogo Hook
|
|
821
|
-
|
|
822
|
-
Hook for loading and caching event logos.
|
|
823
|
-
|
|
824
|
-
#### Interface
|
|
825
|
-
|
|
826
|
-
```typescript
|
|
827
|
-
interface UsePublicEventLogoReturn {
|
|
828
|
-
logoUrl: string | null;
|
|
829
|
-
loading: boolean;
|
|
830
|
-
error: Error | null;
|
|
831
|
-
refetch: () => Promise<void>;
|
|
832
|
-
clearCache: () => void;
|
|
833
|
-
}
|
|
834
|
-
```
|
|
835
|
-
|
|
836
|
-
#### Usage Examples
|
|
837
|
-
|
|
838
|
-
> **Storage path formats**
|
|
839
|
-
>
|
|
840
|
-
> When configuring `event_logo` for an event, Pace supports multiple formats:
|
|
841
|
-
>
|
|
842
|
-
> - A fully qualified `https://` URL will be used directly.
|
|
843
|
-
> - A storage path like `public-files/org-123/logo.png` automatically resolves against the standard buckets configured via `pace-core`.
|
|
844
|
-
> - If your deployment stores logos in a different bucket, prefix the path with the bucket name using `custombucket/org-123/logo.png` (alphanumeric names) or `custom-bucket::org-123/logo.png` (bucket names containing hyphens).
|
|
845
|
-
>
|
|
846
|
-
> The double colon form helps when your folders start with numbers (for example `2024/`), ensuring they are not mistaken for bucket names.
|
|
847
|
-
|
|
848
|
-
**Basic Logo Loading:**
|
|
849
|
-
```tsx
|
|
850
|
-
import { usePublicEventLogo } from '@jmruthers/pace-core';
|
|
851
|
-
|
|
852
|
-
function EventLogoDisplay({ eventId }: { eventId: string }) {
|
|
853
|
-
const { logoUrl, loading, error } = usePublicEventLogo(eventId, 'large');
|
|
854
|
-
|
|
855
|
-
if (loading) {
|
|
856
|
-
return <div className="animate-pulse bg-sec-200 w-32 h-32 rounded"></div>;
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
if (error) {
|
|
860
|
-
return <div className="text-sec-500">Logo unavailable</div>;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
if (!logoUrl) {
|
|
864
|
-
return <div className="bg-sec-200 text-sec-500 p-4 rounded">No logo</div>;
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
return (
|
|
868
|
-
<img
|
|
869
|
-
src={logoUrl}
|
|
870
|
-
alt="Event logo"
|
|
871
|
-
className="w-32 h-32 object-contain"
|
|
872
|
-
/>
|
|
873
|
-
);
|
|
874
|
-
}
|
|
875
|
-
```
|
|
876
|
-
|
|
877
|
-
**With Caching Control:**
|
|
878
|
-
```tsx
|
|
879
|
-
import { usePublicEventLogo } from '@jmruthers/pace-core';
|
|
158
|
+
## Security Considerations
|
|
880
159
|
|
|
881
|
-
|
|
882
|
-
const {
|
|
883
|
-
logoUrl,
|
|
884
|
-
loading,
|
|
885
|
-
error,
|
|
886
|
-
refetch,
|
|
887
|
-
clearCache
|
|
888
|
-
} = usePublicEventLogo(eventId, 'medium');
|
|
160
|
+
### What's Protected
|
|
889
161
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
162
|
+
- **Organisation Data**: Only public event data is exposed
|
|
163
|
+
- **User Data**: No user information is accessible
|
|
164
|
+
- **Internal Data**: Internal notes, audit logs, etc. are not exposed
|
|
165
|
+
- **RLS Enforcement**: View respects RLS policies on underlying tables
|
|
893
166
|
|
|
894
|
-
|
|
895
|
-
clearCache();
|
|
896
|
-
};
|
|
167
|
+
### What's Exposed
|
|
897
168
|
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
{logoUrl && (
|
|
903
|
-
<img
|
|
904
|
-
src={logoUrl}
|
|
905
|
-
alt="Event logo"
|
|
906
|
-
className="w-16 h-16 object-contain"
|
|
907
|
-
/>
|
|
908
|
-
)}
|
|
909
|
-
|
|
910
|
-
<div className="space-x-2">
|
|
911
|
-
<Button onClick={handleRefresh} size="sm">
|
|
912
|
-
Refresh Logo
|
|
913
|
-
</Button>
|
|
914
|
-
<Button onClick={handleClearCache} size="sm" variant="outline">
|
|
915
|
-
Clear Cache
|
|
916
|
-
</Button>
|
|
917
|
-
</div>
|
|
918
|
-
</div>
|
|
919
|
-
);
|
|
920
|
-
}
|
|
921
|
-
```
|
|
169
|
+
- **Event Information**: Public event details (name, date, venue, etc.)
|
|
170
|
+
- **Recipe Information**: Dish names, descriptions, preparation steps
|
|
171
|
+
- **Diet Information**: Diet notes and requirements
|
|
172
|
+
- **Meal Types**: Available meal types for the event
|
|
922
173
|
|
|
923
|
-
|
|
174
|
+
## Troubleshooting
|
|
924
175
|
|
|
925
|
-
|
|
176
|
+
### View Returns Empty Results
|
|
926
177
|
|
|
927
|
-
|
|
928
|
-
|
|
178
|
+
1. **Check Event Configuration**:
|
|
179
|
+
```sql
|
|
180
|
+
SELECT event_id, event_name, public_readable, is_visible
|
|
181
|
+
FROM event
|
|
182
|
+
WHERE event_id = 'YOUR_EVENT_ID';
|
|
183
|
+
```
|
|
929
184
|
|
|
930
|
-
|
|
931
|
-
|
|
185
|
+
2. **Verify View Exists**:
|
|
186
|
+
```sql
|
|
187
|
+
SELECT * FROM information_schema.views
|
|
188
|
+
WHERE table_name = 'public_recipe_details';
|
|
189
|
+
```
|
|
932
190
|
|
|
933
|
-
|
|
934
|
-
const handleClearCache = () => {
|
|
935
|
-
clearPublicEventCache();
|
|
936
|
-
console.log('Event cache cleared');
|
|
937
|
-
};
|
|
191
|
+
3. **Check RLS Policies**: Ensure underlying tables have correct RLS policies
|
|
938
192
|
|
|
939
|
-
|
|
940
|
-
<div>
|
|
941
|
-
<h1>{event?.name}</h1>
|
|
942
|
-
|
|
943
|
-
<div className="space-x-2">
|
|
944
|
-
<Button onClick={handleClearCache}>
|
|
945
|
-
Clear Cache
|
|
946
|
-
</Button>
|
|
947
|
-
</div>
|
|
948
|
-
</div>
|
|
949
|
-
);
|
|
950
|
-
}
|
|
951
|
-
```
|
|
193
|
+
### Performance Issues
|
|
952
194
|
|
|
953
|
-
|
|
195
|
+
1. **Check Indexes**:
|
|
196
|
+
```sql
|
|
197
|
+
SELECT * FROM pg_indexes
|
|
198
|
+
WHERE tablename = 'event' AND indexname LIKE '%public%';
|
|
199
|
+
```
|
|
954
200
|
|
|
955
|
-
|
|
956
|
-
|
|
201
|
+
2. **Use EXPLAIN ANALYZE**:
|
|
202
|
+
```sql
|
|
203
|
+
EXPLAIN ANALYZE
|
|
204
|
+
SELECT * FROM public_recipe_details
|
|
205
|
+
WHERE event_id = 'YOUR_EVENT_ID';
|
|
206
|
+
```
|
|
957
207
|
|
|
958
|
-
|
|
959
|
-
const eventStats = getPublicEventCacheStats();
|
|
960
|
-
const logoStats = getPublicLogoCacheStats();
|
|
208
|
+
3. **Verify Helper Functions**: Ensure `check_public_event_access()` is STABLE SECURITY DEFINER
|
|
961
209
|
|
|
962
|
-
|
|
963
|
-
<div className="bg-sec-50 p-4 rounded">
|
|
964
|
-
<h3>Cache Statistics</h3>
|
|
965
|
-
<div className="grid grid-cols-2 gap-4">
|
|
966
|
-
<div>
|
|
967
|
-
<h4>Event Cache</h4>
|
|
968
|
-
<p>Entries: {eventStats.entryCount}</p>
|
|
969
|
-
<p>Hit Rate: {eventStats.hitRate}%</p>
|
|
970
|
-
<p>Memory Usage: {eventStats.memoryUsage} bytes</p>
|
|
971
|
-
</div>
|
|
972
|
-
<div>
|
|
973
|
-
<h4>Logo Cache</h4>
|
|
974
|
-
<p>Entries: {logoStats.entryCount}</p>
|
|
975
|
-
<p>Hit Rate: {logoStats.hitRate}%</p>
|
|
976
|
-
<p>Memory Usage: {logoStats.memoryUsage} bytes</p>
|
|
977
|
-
</div>
|
|
978
|
-
</div>
|
|
979
|
-
</div>
|
|
980
|
-
);
|
|
981
|
-
}
|
|
982
|
-
```
|
|
983
|
-
|
|
984
|
-
### SEO Optimization
|
|
985
|
-
|
|
986
|
-
#### Meta Tags and Structured Data
|
|
987
|
-
|
|
988
|
-
```tsx
|
|
989
|
-
import { usePublicEvent } from '@jmruthers/pace-core';
|
|
990
|
-
|
|
991
|
-
function SEOEventPage({ eventId }: { eventId: string }) {
|
|
992
|
-
const { event } = usePublicEvent(eventId);
|
|
993
|
-
|
|
994
|
-
// Generate structured data for search engines
|
|
995
|
-
const structuredData = event ? {
|
|
996
|
-
"@context": "https://schema.org",
|
|
997
|
-
"@type": "Event",
|
|
998
|
-
"name": event.name,
|
|
999
|
-
"description": event.description,
|
|
1000
|
-
"startDate": event.start_date,
|
|
1001
|
-
"endDate": event.end_date,
|
|
1002
|
-
"location": {
|
|
1003
|
-
"@type": "Place",
|
|
1004
|
-
"name": event.location
|
|
1005
|
-
},
|
|
1006
|
-
"url": event.public_url,
|
|
1007
|
-
"image": event.logo_url
|
|
1008
|
-
} : null;
|
|
1009
|
-
|
|
1010
|
-
return (
|
|
1011
|
-
<>
|
|
1012
|
-
{/* Meta tags */}
|
|
1013
|
-
<head>
|
|
1014
|
-
<title>{event?.name || 'Event'}</title>
|
|
1015
|
-
<meta name="description" content={event?.description} />
|
|
1016
|
-
<meta property="og:title" content={event?.name} />
|
|
1017
|
-
<meta property="og:description" content={event?.description} />
|
|
1018
|
-
<meta property="og:image" content={event?.logo_url} />
|
|
1019
|
-
<meta property="og:type" content="event" />
|
|
1020
|
-
<meta name="twitter:card" content="summary_large_image" />
|
|
1021
|
-
|
|
1022
|
-
{/* Structured data */}
|
|
1023
|
-
{structuredData && (
|
|
1024
|
-
<script
|
|
1025
|
-
type="application/ld+json"
|
|
1026
|
-
dangerouslySetInnerHTML={{
|
|
1027
|
-
__html: JSON.stringify(structuredData)
|
|
1028
|
-
}}
|
|
1029
|
-
/>
|
|
1030
|
-
)}
|
|
1031
|
-
</head>
|
|
1032
|
-
|
|
1033
|
-
<main>
|
|
1034
|
-
<h1>{event?.name}</h1>
|
|
1035
|
-
<p>{event?.description}</p>
|
|
1036
|
-
{/* Event content */}
|
|
1037
|
-
</main>
|
|
1038
|
-
</>
|
|
1039
|
-
);
|
|
1040
|
-
}
|
|
1041
|
-
```
|
|
1042
|
-
|
|
1043
|
-
### Performance Optimization
|
|
1044
|
-
|
|
1045
|
-
#### Lazy Loading
|
|
1046
|
-
|
|
1047
|
-
```tsx
|
|
1048
|
-
import { lazy, Suspense } from 'react';
|
|
1049
|
-
import { usePublicEvent } from '@jmruthers/pace-core';
|
|
1050
|
-
|
|
1051
|
-
// Lazy load heavy components
|
|
1052
|
-
const EventSchedule = lazy(() => import('./EventSchedule'));
|
|
1053
|
-
const EventSpeakers = lazy(() => import('./EventSpeakers'));
|
|
1054
|
-
|
|
1055
|
-
function OptimizedEventPage({ eventId }: { eventId: string }) {
|
|
1056
|
-
const { event, loading } = usePublicEvent(eventId);
|
|
1057
|
-
|
|
1058
|
-
if (loading) return <div>Loading...</div>;
|
|
1059
|
-
if (!event) return <div>Event not found</div>;
|
|
1060
|
-
|
|
1061
|
-
return (
|
|
1062
|
-
<PublicPageLayout>
|
|
1063
|
-
<header>
|
|
1064
|
-
<h1>{event.name}</h1>
|
|
1065
|
-
<p>{event.description}</p>
|
|
1066
|
-
</header>
|
|
1067
|
-
|
|
1068
|
-
<main>
|
|
1069
|
-
{/* Critical content loads immediately */}
|
|
1070
|
-
<section>
|
|
1071
|
-
<h2>Event Details</h2>
|
|
1072
|
-
<p>Date: {new Date(event.start_date).toLocaleDateString()}</p>
|
|
1073
|
-
<p>Location: {event.location}</p>
|
|
1074
|
-
</section>
|
|
1075
|
-
|
|
1076
|
-
{/* Non-critical content loads lazily */}
|
|
1077
|
-
<Suspense fallback={<div>Loading schedule...</div>}>
|
|
1078
|
-
<EventSchedule eventId={eventId} />
|
|
1079
|
-
</Suspense>
|
|
1080
|
-
|
|
1081
|
-
<Suspense fallback={<div>Loading speakers...</div>}>
|
|
1082
|
-
<EventSpeakers eventId={eventId} />
|
|
1083
|
-
</Suspense>
|
|
1084
|
-
</main>
|
|
1085
|
-
</PublicPageLayout>
|
|
1086
|
-
);
|
|
1087
|
-
}
|
|
1088
|
-
```
|
|
1089
|
-
|
|
1090
|
-
#### Image Optimization
|
|
1091
|
-
|
|
1092
|
-
```tsx
|
|
1093
|
-
import { EventLogo } from '@jmruthers/pace-core';
|
|
1094
|
-
|
|
1095
|
-
function OptimizedEventLogo({ eventId }: { eventId: string }) {
|
|
1096
|
-
return (
|
|
1097
|
-
<EventLogo
|
|
1098
|
-
eventId={eventId}
|
|
1099
|
-
size="large"
|
|
1100
|
-
className="w-full h-auto max-w-md mx-auto"
|
|
1101
|
-
// The component handles:
|
|
1102
|
-
// - WebP format when supported
|
|
1103
|
-
// - Responsive sizing
|
|
1104
|
-
// - Lazy loading
|
|
1105
|
-
// - Error fallbacks
|
|
1106
|
-
/>
|
|
1107
|
-
);
|
|
1108
|
-
}
|
|
1109
|
-
```
|
|
1110
|
-
|
|
1111
|
-
### Advanced Error Handling
|
|
1112
|
-
|
|
1113
|
-
#### Graceful Degradation
|
|
1114
|
-
|
|
1115
|
-
```tsx
|
|
1116
|
-
import { usePublicEvent, usePublicEventLogo } from '@jmruthers/pace-core';
|
|
1117
|
-
|
|
1118
|
-
function RobustEventPage({ eventId }: { eventId: string }) {
|
|
1119
|
-
const {
|
|
1120
|
-
event,
|
|
1121
|
-
loading: eventLoading,
|
|
1122
|
-
error: eventError
|
|
1123
|
-
} = usePublicEvent(eventId);
|
|
1124
|
-
|
|
1125
|
-
const {
|
|
1126
|
-
logoUrl,
|
|
1127
|
-
loading: logoLoading,
|
|
1128
|
-
error: logoError
|
|
1129
|
-
} = usePublicEventLogo(eventId, 'large');
|
|
1130
|
-
|
|
1131
|
-
// Graceful degradation for missing data
|
|
1132
|
-
const displayName = event?.name || 'Event';
|
|
1133
|
-
const displayDescription = event?.description || 'Event details coming soon';
|
|
1134
|
-
const displayLogo = logoUrl || '/default-event-logo.png';
|
|
1135
|
-
|
|
1136
|
-
if (eventLoading) {
|
|
1137
|
-
return (
|
|
1138
|
-
<div className="min-h-screen flex items-center justify-center">
|
|
1139
|
-
<div className="text-center">
|
|
1140
|
-
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-main-600 mx-auto"></div>
|
|
1141
|
-
<p className="mt-4 text-sec-600">Loading event...</p>
|
|
1142
|
-
</div>
|
|
1143
|
-
</div>
|
|
1144
|
-
);
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
if (eventError) {
|
|
1148
|
-
return (
|
|
1149
|
-
<div className="min-h-screen flex items-center justify-center">
|
|
1150
|
-
<div className="text-center">
|
|
1151
|
-
<h1 className="text-2xl font-bold text-sec-800 mb-2">
|
|
1152
|
-
Unable to load event
|
|
1153
|
-
</h1>
|
|
1154
|
-
<p className="text-sec-600 mb-4">
|
|
1155
|
-
{eventError.message}
|
|
1156
|
-
</p>
|
|
1157
|
-
<Button onClick={() => window.location.reload()}>
|
|
1158
|
-
Try Again
|
|
1159
|
-
</Button>
|
|
1160
|
-
</div>
|
|
1161
|
-
</div>
|
|
1162
|
-
);
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
return (
|
|
1166
|
-
<PublicPageLayout>
|
|
1167
|
-
<header className="bg-main-50 py-8">
|
|
1168
|
-
<div className="container mx-auto px-4 text-center">
|
|
1169
|
-
{logoLoading ? (
|
|
1170
|
-
<div className="animate-pulse bg-sec-200 h-32 w-32 mx-auto rounded"></div>
|
|
1171
|
-
) : logoError ? (
|
|
1172
|
-
<div className="bg-sec-200 text-sec-500 h-32 w-32 mx-auto rounded flex items-center justify-center">
|
|
1173
|
-
No Logo
|
|
1174
|
-
</div>
|
|
1175
|
-
) : (
|
|
1176
|
-
<img
|
|
1177
|
-
src={displayLogo}
|
|
1178
|
-
alt={`${displayName} logo`}
|
|
1179
|
-
className="h-32 w-32 mx-auto object-contain"
|
|
1180
|
-
/>
|
|
1181
|
-
)}
|
|
1182
|
-
|
|
1183
|
-
<h1 className="text-3xl font-bold text-main-900 mt-4">
|
|
1184
|
-
{displayName}
|
|
1185
|
-
</h1>
|
|
1186
|
-
<p className="text-main-700 mt-2">
|
|
1187
|
-
{displayDescription}
|
|
1188
|
-
</p>
|
|
1189
|
-
</div>
|
|
1190
|
-
</header>
|
|
1191
|
-
|
|
1192
|
-
<main className="container mx-auto px-4 py-8">
|
|
1193
|
-
{/* Event content */}
|
|
1194
|
-
</main>
|
|
1195
|
-
</PublicPageLayout>
|
|
1196
|
-
);
|
|
1197
|
-
}
|
|
1198
|
-
```
|
|
1199
|
-
|
|
1200
|
-
### Advanced Troubleshooting
|
|
1201
|
-
|
|
1202
|
-
#### Common Issues
|
|
1203
|
-
|
|
1204
|
-
**Issue: Event not loading**
|
|
1205
|
-
- Check that event exists and is published
|
|
1206
|
-
- Verify `is_public` flag is set to true
|
|
1207
|
-
- Check RLS policies allow public access
|
|
1208
|
-
|
|
1209
|
-
**Issue: Logo not displaying**
|
|
1210
|
-
- Verify logo file exists in storage bucket
|
|
1211
|
-
- Check storage bucket policies
|
|
1212
|
-
- Ensure logo URL is correct
|
|
1213
|
-
|
|
1214
|
-
**Issue: Cache not working**
|
|
1215
|
-
- Check browser storage permissions
|
|
1216
|
-
- Verify cache configuration
|
|
1217
|
-
- Clear browser cache and test
|
|
1218
|
-
|
|
1219
|
-
#### Debug Mode
|
|
1220
|
-
|
|
1221
|
-
```tsx
|
|
1222
|
-
import { usePublicEvent, usePublicEventLogo } from '@jmruthers/pace-core';
|
|
1223
|
-
|
|
1224
|
-
function DebugPublicPage({ eventId }: { eventId: string }) {
|
|
1225
|
-
const { event, loading, error } = usePublicEvent(eventId);
|
|
1226
|
-
const { logoUrl, loading: logoLoading, error: logoError } = usePublicEventLogo(eventId, 'large');
|
|
1227
|
-
|
|
1228
|
-
console.log('Event data:', { event, loading, error });
|
|
1229
|
-
console.log('Logo data:', { logoUrl, logoLoading, logoError });
|
|
1230
|
-
|
|
1231
|
-
return (
|
|
1232
|
-
<div>
|
|
1233
|
-
<h1>Debug Information</h1>
|
|
1234
|
-
<pre>{JSON.stringify({ event, loading, error }, null, 2)}</pre>
|
|
1235
|
-
<pre>{JSON.stringify({ logoUrl, logoLoading, logoError }, null, 2)}</pre>
|
|
1236
|
-
</div>
|
|
1237
|
-
);
|
|
1238
|
-
}
|
|
1239
|
-
```
|
|
1240
|
-
|
|
1241
|
-
## ♿ Accessibility
|
|
1242
|
-
|
|
1243
|
-
Public pages components are designed with accessibility in mind:
|
|
1244
|
-
|
|
1245
|
-
- **WCAG 2.1 AA compliant** - All components meet accessibility standards
|
|
1246
|
-
- **Semantic HTML** - Proper use of `<header>`, `<main>`, `<footer>` elements
|
|
1247
|
-
- **Screen reader friendly** - All interactive elements have proper ARIA labels
|
|
1248
|
-
- **Keyboard navigation** - Full keyboard support for all interactive elements
|
|
1249
|
-
- **High contrast support** - Colors meet WCAG contrast requirements
|
|
1250
|
-
- **Clear content hierarchy** - Proper heading structure and content organization
|
|
1251
|
-
|
|
1252
|
-
### Accessibility Best Practices
|
|
1253
|
-
|
|
1254
|
-
1. **Always provide alt text** for event logos and images
|
|
1255
|
-
2. **Use semantic HTML** - Prefer `<h1>`, `<h2>`, `<nav>`, etc. over generic `<div>` elements
|
|
1256
|
-
3. **Ensure keyboard navigation** - All interactive elements should be keyboard accessible
|
|
1257
|
-
4. **Test with screen readers** - Verify content is properly announced
|
|
1258
|
-
5. **Maintain focus management** - Ensure focus is properly managed during loading states
|
|
1259
|
-
|
|
1260
|
-
## ⚠️ Edge Cases
|
|
1261
|
-
|
|
1262
|
-
### Missing Event Data
|
|
1263
|
-
|
|
1264
|
-
When event data is not available:
|
|
1265
|
-
- `PublicPageLayout` automatically shows an error message
|
|
1266
|
-
- Error message includes the event code for debugging
|
|
1267
|
-
- Provides a "Try Again" button to retry loading
|
|
1268
|
-
- Can be customized with `errorFallback` prop
|
|
1269
|
-
|
|
1270
|
-
### Invalid Event Codes
|
|
1271
|
-
|
|
1272
|
-
When an invalid event code is provided:
|
|
1273
|
-
- `usePublicRouteParams` validates event code format
|
|
1274
|
-
- Invalid codes trigger error states
|
|
1275
|
-
- Error messages are user-friendly
|
|
1276
|
-
- Validation errors can be hidden with `showValidationErrors={false}`
|
|
1277
|
-
|
|
1278
|
-
### Network Failures
|
|
1279
|
-
|
|
1280
|
-
When network requests fail:
|
|
1281
|
-
- All hooks provide error states
|
|
1282
|
-
- Error objects include descriptive messages
|
|
1283
|
-
- Retry mechanisms are available via `refetch` functions
|
|
1284
|
-
- Graceful degradation with fallback UI
|
|
1285
|
-
|
|
1286
|
-
### Missing Logos
|
|
1287
|
-
|
|
1288
|
-
When event logos are not available:
|
|
1289
|
-
- Components show fallback UI with event initials
|
|
1290
|
-
- No broken image icons
|
|
1291
|
-
- Consistent styling regardless of logo availability
|
|
1292
|
-
- Custom fallback can be provided via `customEventLogo` prop
|
|
1293
|
-
|
|
1294
|
-
### Loading States
|
|
1295
|
-
|
|
1296
|
-
During data loading:
|
|
1297
|
-
- Loading spinners are shown automatically
|
|
1298
|
-
- Custom loading messages can be provided
|
|
1299
|
-
- Loading fallback components can be customized
|
|
1300
|
-
- Prevents layout shifts during loading
|
|
1301
|
-
|
|
1302
|
-
---
|
|
210
|
+
## Related Documentation
|
|
1303
211
|
|
|
1304
|
-
|
|
212
|
+
- [RBAC and RLS Standard](../../standards/07-rbac-and-rls-standard.md)
|
|
213
|
+
- [Public Pages Examples](../../examples/PublicPages/)
|
|
214
|
+
- [RLS Policy Remediation Plan](../troubleshooting/rls-policy-remediation-plan-combined.md)
|