@jmruthers/pace-core 0.5.187 → 0.5.189

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 (209) hide show
  1. package/dist/{DataTable-K3RJRSOX.js → DataTable-GUFUNZ3N.js} +5 -5
  2. package/dist/{PublicPageProvider-DrLDztHt.d.ts → PublicPageProvider-B8HaLe69.d.ts} +47 -17
  3. package/dist/{UnifiedAuthProvider-B76OWOAT.js → UnifiedAuthProvider-643PUAIM.js} +2 -2
  4. package/dist/{chunk-FMTK4XNN.js → chunk-2UUZZJFT.js} +3 -3
  5. package/dist/{chunk-3IC5WCMO.js → chunk-3GOZZZYH.js} +3 -3
  6. package/dist/{chunk-ULX5FYEM.js → chunk-DDM4CCYT.js} +3 -3
  7. package/dist/{chunk-K2JGDXGU.js → chunk-E7UAOUMY.js} +2 -2
  8. package/dist/{chunk-T6ZJVI3A.js → chunk-IM4QE42D.js} +4 -4
  9. package/dist/{chunk-3NFNJOO7.js → chunk-MX64ZF6I.js} +4 -4
  10. package/dist/{chunk-C4OYJOV4.js → chunk-UCQSRW7Z.js} +829 -829
  11. package/dist/chunk-UCQSRW7Z.js.map +1 -0
  12. package/dist/{chunk-WK2Y6TGA.js → chunk-VGZZXKBR.js} +3 -3
  13. package/dist/chunk-VGZZXKBR.js.map +1 -0
  14. package/dist/{chunk-LBBUPSSC.js → chunk-YGPFYGA6.js} +3760 -3692
  15. package/dist/chunk-YGPFYGA6.js.map +1 -0
  16. package/dist/components.d.ts +1 -2
  17. package/dist/components.js +6 -10
  18. package/dist/components.js.map +1 -1
  19. package/dist/hooks.js +5 -5
  20. package/dist/index.d.ts +1 -2
  21. package/dist/index.js +9 -13
  22. package/dist/index.js.map +1 -1
  23. package/dist/providers.js +1 -1
  24. package/dist/rbac/index.js +5 -5
  25. package/dist/utils.js +1 -1
  26. package/docs/api/classes/ColumnFactory.md +1 -1
  27. package/docs/api/classes/ErrorBoundary.md +1 -1
  28. package/docs/api/classes/InvalidScopeError.md +1 -1
  29. package/docs/api/classes/Logger.md +1 -1
  30. package/docs/api/classes/MissingUserContextError.md +1 -1
  31. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  32. package/docs/api/classes/PermissionDeniedError.md +1 -1
  33. package/docs/api/classes/RBACAuditManager.md +1 -1
  34. package/docs/api/classes/RBACCache.md +1 -1
  35. package/docs/api/classes/RBACEngine.md +1 -1
  36. package/docs/api/classes/RBACError.md +1 -1
  37. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  38. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  39. package/docs/api/classes/StorageUtils.md +1 -1
  40. package/docs/api/enums/FileCategory.md +1 -1
  41. package/docs/api/enums/LogLevel.md +1 -1
  42. package/docs/api/enums/RBACErrorCode.md +1 -1
  43. package/docs/api/enums/RPCFunction.md +1 -1
  44. package/docs/api/interfaces/AddressFieldProps.md +1 -1
  45. package/docs/api/interfaces/AddressFieldRef.md +1 -1
  46. package/docs/api/interfaces/AggregateConfig.md +1 -1
  47. package/docs/api/interfaces/AutocompleteOptions.md +1 -1
  48. package/docs/api/interfaces/AvatarProps.md +128 -0
  49. package/docs/api/interfaces/BadgeProps.md +1 -1
  50. package/docs/api/interfaces/ButtonProps.md +1 -1
  51. package/docs/api/interfaces/CalendarProps.md +1 -1
  52. package/docs/api/interfaces/CardProps.md +1 -1
  53. package/docs/api/interfaces/ColorPalette.md +1 -1
  54. package/docs/api/interfaces/ColorShade.md +1 -1
  55. package/docs/api/interfaces/ComplianceResult.md +1 -1
  56. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  57. package/docs/api/interfaces/DataRecord.md +1 -1
  58. package/docs/api/interfaces/DataTableAction.md +1 -1
  59. package/docs/api/interfaces/DataTableColumn.md +1 -1
  60. package/docs/api/interfaces/DataTableProps.md +1 -1
  61. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  62. package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
  63. package/docs/api/interfaces/DatabaseIssue.md +1 -1
  64. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  65. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  66. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  67. package/docs/api/interfaces/ExportColumn.md +1 -1
  68. package/docs/api/interfaces/ExportOptions.md +1 -1
  69. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  70. package/docs/api/interfaces/FileMetadata.md +1 -1
  71. package/docs/api/interfaces/FileReference.md +1 -1
  72. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  73. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  74. package/docs/api/interfaces/FileUploadProps.md +1 -1
  75. package/docs/api/interfaces/FooterProps.md +1 -1
  76. package/docs/api/interfaces/FormFieldProps.md +1 -1
  77. package/docs/api/interfaces/FormProps.md +1 -1
  78. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  79. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  80. package/docs/api/interfaces/InputProps.md +1 -1
  81. package/docs/api/interfaces/LabelProps.md +1 -1
  82. package/docs/api/interfaces/LoggerConfig.md +1 -1
  83. package/docs/api/interfaces/LoginFormProps.md +1 -1
  84. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  85. package/docs/api/interfaces/NavigationContextType.md +1 -1
  86. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  87. package/docs/api/interfaces/NavigationItem.md +1 -1
  88. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  89. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  90. package/docs/api/interfaces/Organisation.md +1 -1
  91. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  92. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  93. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  94. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  95. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  96. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  97. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  98. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  99. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  100. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  101. package/docs/api/interfaces/PaletteData.md +1 -1
  102. package/docs/api/interfaces/ParsedAddress.md +1 -1
  103. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  104. package/docs/api/interfaces/ProgressProps.md +1 -1
  105. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  106. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  107. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  108. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  109. package/docs/api/interfaces/QuickFix.md +1 -1
  110. package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
  111. package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
  112. package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
  113. package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
  114. package/docs/api/interfaces/RBACConfig.md +1 -1
  115. package/docs/api/interfaces/RBACContext.md +1 -1
  116. package/docs/api/interfaces/RBACLogger.md +1 -1
  117. package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
  118. package/docs/api/interfaces/RBACPerformanceMetrics.md +1 -1
  119. package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
  120. package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
  121. package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
  122. package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
  123. package/docs/api/interfaces/RBACResult.md +1 -1
  124. package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
  125. package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
  126. package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
  127. package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
  128. package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
  129. package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
  130. package/docs/api/interfaces/RBACRolesListParams.md +1 -1
  131. package/docs/api/interfaces/RBACRolesListResult.md +1 -1
  132. package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
  133. package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
  134. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  135. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  136. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  137. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  138. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  139. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  140. package/docs/api/interfaces/RouteConfig.md +1 -1
  141. package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
  142. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  143. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  144. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  145. package/docs/api/interfaces/SetupIssue.md +1 -1
  146. package/docs/api/interfaces/StorageConfig.md +1 -1
  147. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  148. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  149. package/docs/api/interfaces/StorageListOptions.md +1 -1
  150. package/docs/api/interfaces/StorageListResult.md +1 -1
  151. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  152. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  153. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  154. package/docs/api/interfaces/StyleImport.md +1 -1
  155. package/docs/api/interfaces/SwitchProps.md +1 -1
  156. package/docs/api/interfaces/TabsContentProps.md +1 -1
  157. package/docs/api/interfaces/TabsListProps.md +1 -1
  158. package/docs/api/interfaces/TabsProps.md +1 -1
  159. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  160. package/docs/api/interfaces/TextareaProps.md +1 -1
  161. package/docs/api/interfaces/ToastActionElement.md +1 -1
  162. package/docs/api/interfaces/ToastProps.md +1 -1
  163. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  164. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  165. package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
  166. package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
  167. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  168. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  169. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  170. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  171. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  172. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  173. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  174. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  175. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  176. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  177. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  178. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  179. package/docs/api/interfaces/UserEventAccess.md +1 -1
  180. package/docs/api/interfaces/UserMenuProps.md +1 -1
  181. package/docs/api/interfaces/UserProfile.md +1 -1
  182. package/docs/api/modules.md +6 -45
  183. package/docs/api-reference/components.md +57 -22
  184. package/docs/getting-started/examples/README.md +2 -2
  185. package/docs/implementation-guides/public-pages.md +140 -1230
  186. package/docs/standards/05-security-standard.md +3 -1
  187. package/docs/standards/07-rbac-and-rls-standard.md +356 -0
  188. package/package.json +1 -2
  189. package/src/__tests__/public-recipe-view.test.ts +199 -0
  190. package/src/__tests__/rls-policies.test.ts +333 -0
  191. package/src/components/Avatar/Avatar.test.tsx +183 -209
  192. package/src/components/Avatar/Avatar.tsx +179 -53
  193. package/src/components/Avatar/index.ts +1 -1
  194. package/src/components/UserMenu/UserMenu.test.tsx +7 -9
  195. package/src/components/UserMenu/UserMenu.tsx +7 -5
  196. package/src/components/index.ts +2 -1
  197. package/src/index.ts +2 -1
  198. package/src/services/OrganisationService.ts +5 -4
  199. package/dist/chunk-C4OYJOV4.js.map +0 -1
  200. package/dist/chunk-LBBUPSSC.js.map +0 -1
  201. package/dist/chunk-WK2Y6TGA.js.map +0 -1
  202. /package/dist/{DataTable-K3RJRSOX.js.map → DataTable-GUFUNZ3N.js.map} +0 -0
  203. /package/dist/{UnifiedAuthProvider-B76OWOAT.js.map → UnifiedAuthProvider-643PUAIM.js.map} +0 -0
  204. /package/dist/{chunk-FMTK4XNN.js.map → chunk-2UUZZJFT.js.map} +0 -0
  205. /package/dist/{chunk-3IC5WCMO.js.map → chunk-3GOZZZYH.js.map} +0 -0
  206. /package/dist/{chunk-ULX5FYEM.js.map → chunk-DDM4CCYT.js.map} +0 -0
  207. /package/dist/{chunk-K2JGDXGU.js.map → chunk-E7UAOUMY.js.map} +0 -0
  208. /package/dist/{chunk-T6ZJVI3A.js.map → chunk-IM4QE42D.js.map} +0 -0
  209. /package/dist/{chunk-3NFNJOO7.js.map → chunk-MX64ZF6I.js.map} +0 -0
@@ -1,1304 +1,214 @@
1
1
  ---
2
- lastUpdated: 2025-11-18T14:00:00+11:00
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
- > **🌐 Public Pages** | [← Back to Documentation](../README.md) | [↑ Table of Contents](#table-of-contents)
9
+ ## Overview
10
10
 
11
- Complete guide for implementing public pages that work independently of your main application's authentication context.
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
- ## 🚨 **CRITICAL: Architecture Requirements**
13
+ ## Public Event Configuration
14
14
 
15
- **Public pages MUST be completely separate from your main app's authentication context.** This is the #1 reason consuming apps struggle with this feature.
15
+ ### Enabling Public Access
16
16
 
17
- ### **WRONG - This Will Break**
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
- You need to create **two completely separate applications**:
52
-
53
- 1. **`AuthenticatedApp`** - Your main app with authentication
54
- 2. **`PublicPageApp`** - Public pages with no authentication
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
- ## 🚀 **Quick Start Implementation**
25
+ **Security Note**: Only events with `public_readable = true` AND `is_visible = true` are accessible on public pages.
79
26
 
80
- ### Step 1: Create Public Page App
27
+ ### Checking Public Access
81
28
 
82
- ```tsx
83
- // PublicPageApp.tsx
84
- import React from 'react';
85
- import { Routes, Route } from 'react-router-dom';
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
- export function PublicPageApp() {
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
- ### Step 2: Create Individual Public Pages
36
+ ### Querying the View
110
37
 
111
- ```tsx
112
- // PublicRecipePage.tsx
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
- export function PublicRecipePage() {
124
- // Step 1: Get event code from URL
125
- const { eventCode } = usePublicRouteParams({ fetchEventData: false });
41
+ function RecipeDetailsPage() {
42
+ const { supabase } = useSupabase();
43
+ const { eventCode } = usePublicRouteParams();
126
44
 
127
- // Step 2: Fetch event data
128
- const { event, isLoading, error } = usePublicEvent(eventCode || '');
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
- // Step 3: Handle loading
131
- if (isLoading) {
132
- return <PublicLoadingSpinner message="Loading recipe grid report..." />;
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
- // Step 5: Render public page
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
- ### PublicPageLayout
229
-
230
- **Main layout component for public pages.** Handles event data and provides consistent styling.
61
+ ### Available Columns
231
62
 
232
- ```tsx
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
- **Props:**
245
- - `eventCode` (required) - The event code from the URL
246
- - `children` (required) - Child components to render
247
- - `event` (optional) - Event data object (if not provided, will be fetched by parent component)
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
- **Props:**
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
- ### PublicPageFooter
72
+ ### Filtering
289
73
 
290
- **Footer component with event and app information.**
291
-
292
- ```tsx
293
- import { PublicPageFooter } from '@jmruthers/pace-core';
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
- <PublicPageFooter
296
- event={event}
297
- showEventInfo={true}
298
- showCopyright={true}
299
- showAppBranding={true}
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
- ## 🔗 **Hooks for Public Pages**
304
-
305
- ### usePublicRouteParams
90
+ ## Performance Considerations
306
91
 
307
- **Extract parameters from public page URLs.**
92
+ ### View Performance
308
93
 
309
- ```tsx
310
- import { usePublicRouteParams } from '@jmruthers/pace-core';
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
- function MyPublicPage() {
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
- ### usePublicEvent
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
- **Fetch event data for public pages.**
106
+ ## Implementation Example
326
107
 
327
- ```tsx
328
- import { usePublicEvent } from '@jmruthers/pace-core';
108
+ ### Complete Public Recipe Page
329
109
 
330
- function MyPublicPage() {
331
- const { event, isLoading, error, refetch } = usePublicEvent('EVENT123');
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
- ```tsx
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
- if (isLoading) return <PublicLoadingSpinner message="Loading..." />;
370
- if (error || !event) return <PublicErrorPage eventCode={eventCode} />;
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
- // Fetch report data
401
- fetchReport(reportId).then(setReport).finally(() => setLoading(false));
402
- }, [reportId]);
403
-
404
- if (loading) return <PublicLoadingSpinner message="Loading report..." />;
405
- if (!report) return <div>Report not found</div>;
406
-
407
- return (
408
- <PublicPageLayout eventCode={report.eventCode} event={report.event}>
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
- ### Server Configuration
624
-
625
- Configure your server to handle both route types:
626
-
627
- ```nginx
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 (error || !event) {
722
- return (
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
- <PublicPageHeader
739
- event={event}
740
- eventCode={eventCode || ''}
741
- title="Recipe Grid Report"
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
- ## 🔗 **Related Documentation**
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
- function LogoWithCacheControl({ eventId }: { eventId: string }) {
882
- const {
883
- logoUrl,
884
- loading,
885
- error,
886
- refetch,
887
- clearCache
888
- } = usePublicEventLogo(eventId, 'medium');
160
+ ### What's Protected
889
161
 
890
- const handleRefresh = async () => {
891
- await refetch();
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
- const handleClearCache = () => {
895
- clearCache();
896
- };
167
+ ### What's Exposed
897
168
 
898
- return (
899
- <div className="space-y-4">
900
- {loading && <div>Loading logo...</div>}
901
- {error && <div>Error: {error.message}</div>}
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
- ### Caching Strategies
174
+ ## Troubleshooting
924
175
 
925
- #### Client-Side Caching
176
+ ### View Returns Empty Results
926
177
 
927
- ```tsx
928
- import { usePublicEvent, clearPublicEventCache } from '@jmruthers/pace-core';
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
- function CachedEventPage({ eventId }: { eventId: string }) {
931
- const { event, loading } = usePublicEvent(eventId);
185
+ 2. **Verify View Exists**:
186
+ ```sql
187
+ SELECT * FROM information_schema.views
188
+ WHERE table_name = 'public_recipe_details';
189
+ ```
932
190
 
933
- // Cache management
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
- return (
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
- #### Cache Statistics
195
+ 1. **Check Indexes**:
196
+ ```sql
197
+ SELECT * FROM pg_indexes
198
+ WHERE tablename = 'event' AND indexname LIKE '%public%';
199
+ ```
954
200
 
955
- ```tsx
956
- import { getPublicEventCacheStats, getPublicLogoCacheStats } from '@jmruthers/pace-core';
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
- function CacheStats() {
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
- return (
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
- **Remember**: Public pages are completely separate from your main app. Keep them isolated and they'll work perfectly!
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)