@jmruthers/pace-core 0.5.101 → 0.5.102

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 (136) hide show
  1. package/dist/{PublicLoadingSpinner-C2h8zg67.d.ts → PublicLoadingSpinner-Cvgk-V0F.d.ts} +22 -55
  2. package/dist/{chunk-2ZYHCFUO.js → chunk-7ME4Z5OY.js} +148 -12
  3. package/dist/chunk-7ME4Z5OY.js.map +1 -0
  4. package/dist/{chunk-MKMKUCPF.js → chunk-SZWRW5FD.js} +20 -139
  5. package/dist/chunk-SZWRW5FD.js.map +1 -0
  6. package/dist/{chunk-A5DFMP3O.js → chunk-UDWTCBSH.js} +127 -498
  7. package/dist/chunk-UDWTCBSH.js.map +1 -0
  8. package/dist/components.d.ts +1 -1
  9. package/dist/components.js +2 -8
  10. package/dist/components.js.map +1 -1
  11. package/dist/hooks.js +5 -5
  12. package/dist/index.d.ts +2 -1
  13. package/dist/index.js +6 -12
  14. package/dist/index.js.map +1 -1
  15. package/docs/api/classes/ColumnFactory.md +1 -1
  16. package/docs/api/classes/ErrorBoundary.md +1 -1
  17. package/docs/api/classes/InvalidScopeError.md +1 -1
  18. package/docs/api/classes/MissingUserContextError.md +1 -1
  19. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  20. package/docs/api/classes/PermissionDeniedError.md +1 -1
  21. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  22. package/docs/api/classes/RBACAuditManager.md +1 -1
  23. package/docs/api/classes/RBACCache.md +1 -1
  24. package/docs/api/classes/RBACEngine.md +1 -1
  25. package/docs/api/classes/RBACError.md +1 -1
  26. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  27. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  28. package/docs/api/classes/StorageUtils.md +2 -1
  29. package/docs/api/enums/FileCategory.md +1 -1
  30. package/docs/api/interfaces/AggregateConfig.md +1 -1
  31. package/docs/api/interfaces/ButtonProps.md +1 -1
  32. package/docs/api/interfaces/CardProps.md +1 -1
  33. package/docs/api/interfaces/ColorPalette.md +1 -1
  34. package/docs/api/interfaces/ColorShade.md +1 -1
  35. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  36. package/docs/api/interfaces/DataRecord.md +1 -1
  37. package/docs/api/interfaces/DataTableAction.md +1 -1
  38. package/docs/api/interfaces/DataTableColumn.md +1 -1
  39. package/docs/api/interfaces/DataTableProps.md +1 -1
  40. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  41. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  42. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  43. package/docs/api/interfaces/FileDisplayProps.md +77 -35
  44. package/docs/api/interfaces/FileMetadata.md +1 -1
  45. package/docs/api/interfaces/FileReference.md +1 -1
  46. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  47. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  48. package/docs/api/interfaces/FileUploadProps.md +1 -1
  49. package/docs/api/interfaces/FooterProps.md +1 -1
  50. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  51. package/docs/api/interfaces/InputProps.md +1 -1
  52. package/docs/api/interfaces/LabelProps.md +1 -1
  53. package/docs/api/interfaces/LoginFormProps.md +1 -1
  54. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  55. package/docs/api/interfaces/NavigationContextType.md +1 -1
  56. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  57. package/docs/api/interfaces/NavigationItem.md +1 -1
  58. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  59. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  60. package/docs/api/interfaces/Organisation.md +1 -1
  61. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  62. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  63. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  64. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  65. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  66. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  67. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  68. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  69. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  70. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  71. package/docs/api/interfaces/PaletteData.md +1 -1
  72. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  73. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  74. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  75. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  76. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  77. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  78. package/docs/api/interfaces/PublicPageHeaderProps.md +11 -24
  79. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  80. package/docs/api/interfaces/RBACConfig.md +1 -1
  81. package/docs/api/interfaces/RBACLogger.md +1 -1
  82. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  83. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  84. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  85. package/docs/api/interfaces/RouteConfig.md +1 -1
  86. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  87. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  88. package/docs/api/interfaces/StorageConfig.md +1 -1
  89. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  90. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  91. package/docs/api/interfaces/StorageListOptions.md +1 -1
  92. package/docs/api/interfaces/StorageListResult.md +1 -1
  93. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  94. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  95. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  96. package/docs/api/interfaces/StyleImport.md +1 -1
  97. package/docs/api/interfaces/SwitchProps.md +1 -1
  98. package/docs/api/interfaces/ToastActionElement.md +1 -1
  99. package/docs/api/interfaces/ToastProps.md +1 -1
  100. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  101. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  102. package/docs/api/interfaces/UseEventLogoOptions.md +1 -1
  103. package/docs/api/interfaces/UseEventLogoReturn.md +1 -1
  104. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  105. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  106. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  107. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  108. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  109. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  110. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  111. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  112. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  113. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  114. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  115. package/docs/api/interfaces/UserEventAccess.md +1 -1
  116. package/docs/api/interfaces/UserMenuProps.md +1 -1
  117. package/docs/api/interfaces/UserProfile.md +1 -1
  118. package/docs/api/modules.md +29 -96
  119. package/docs/implementation-guides/file-reference-system.md +53 -2
  120. package/package.json +1 -1
  121. package/src/components/FileDisplay/FileDisplay.test.tsx +1 -1
  122. package/src/components/FileDisplay/FileDisplay.tsx +189 -300
  123. package/src/components/PublicLayout/PublicPageHeader.tsx +14 -9
  124. package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +25 -35
  125. package/src/components/PublicLayout/index.ts +2 -5
  126. package/src/components/Toast/Toast.tsx +1 -1
  127. package/src/examples/PublicEventPage.tsx +17 -7
  128. package/src/examples/PublicPageApp.tsx +18 -8
  129. package/src/hooks/useFileReference.ts +10 -1
  130. package/src/utils/file-reference.ts +24 -7
  131. package/src/utils/storage/helpers.ts +12 -1
  132. package/dist/chunk-2ZYHCFUO.js.map +0 -1
  133. package/dist/chunk-A5DFMP3O.js.map +0 -1
  134. package/dist/chunk-MKMKUCPF.js.map +0 -1
  135. package/docs/api/interfaces/EventLogoProps.md +0 -152
  136. package/src/components/PublicLayout/EventLogo.tsx +0 -474
@@ -41,7 +41,7 @@
41
41
  * @dependencies
42
42
  * - React 18+ - Component framework
43
43
  * - Event types - Type definitions
44
- * - EventLogo component - Logo display
44
+ * - FileDisplay component - Logo display
45
45
  * - Tailwind CSS - Styling
46
46
  */
47
47
 
@@ -50,7 +50,6 @@ import type { Event } from '../../types/unified';
50
50
  import { FileDisplay } from '../FileDisplay/FileDisplay';
51
51
  import { FileCategory } from '../../types/file-reference';
52
52
  import { useAppConfig } from '../../hooks/useAppConfig';
53
- import type { SupabaseClient } from '@supabase/supabase-js';
54
53
 
55
54
  export interface PublicPageHeaderProps {
56
55
  /** The event data for this public page */
@@ -73,8 +72,6 @@ export interface PublicPageHeaderProps {
73
72
  customAppLogo?: ReactNode;
74
73
  /** Custom event logo component */
75
74
  customEventLogo?: ReactNode;
76
- /** Optional Supabase client (for testing) */
77
- supabase?: SupabaseClient;
78
75
  }
79
76
 
80
77
  /**
@@ -96,8 +93,7 @@ export function PublicPageHeader({
96
93
  className = '',
97
94
  children,
98
95
  customAppLogo,
99
- customEventLogo,
100
- supabase
96
+ customEventLogo
101
97
  }: PublicPageHeaderProps) {
102
98
  const { appName } = useAppConfig();
103
99
  const headerClasses = `bg-white border-b border-gray-200 ${className}`.trim();
@@ -125,13 +121,22 @@ export function PublicPageHeader({
125
121
  <div className="flex-shrink-0">
126
122
  {customEventLogo || (
127
123
  <FileDisplay
128
- supabase={supabase}
129
124
  table_name="event"
130
125
  record_id={event.event_id}
131
126
  organisation_id={event.organisation_id}
132
127
  category={FileCategory.EVENT_LOGOS}
133
- showDelete={false}
134
- className="[&_img]:h-12 [&_img]:w-12 [&_img]:object-contain"
128
+ displayOnly={true}
129
+ showFallback={true}
130
+ fallbackSize="md"
131
+ className="[&_img]:h-12 [&_img]:w-12 [&_img]:object-contain [&>div]:h-12 [&>div]:w-12"
132
+ generateFallbackText={(fileName) => {
133
+ if (!event.event_name) return 'EV';
134
+ return event.event_name
135
+ .split(/[\s\-_]+/)
136
+ .map(word => word.charAt(0).toUpperCase())
137
+ .join('')
138
+ .substring(0, 3);
139
+ }}
135
140
  />
136
141
  )}
137
142
  </div>
@@ -46,9 +46,9 @@ vi.mock('../../../hooks/useAppConfig', () => ({
46
46
  }))
47
47
  }));
48
48
 
49
- // Mock the EventLogo component
50
- vi.mock('../EventLogo', () => ({
51
- EventLogo: vi.fn(({ eventId, eventName, organisationId, size, className }) => (
49
+ // Mock the FileDisplay component
50
+ vi.mock('../../FileDisplay/FileDisplay', () => ({
51
+ FileDisplay: vi.fn(({ table_name, record_id, organisation_id, category, className }) => (
52
52
  <div
53
53
  data-testid="event-logo"
54
54
  data-event-id={eventId}
@@ -85,7 +85,7 @@ describe('[component] PublicPageHeader', () => {
85
85
  describe('Rendering', () => {
86
86
  it('renders with basic props', () => {
87
87
  render(
88
- <PublicPageHeader event={mockEvent} eventCode="EVENT123" supabase={mockSupabase} />
88
+ <PublicPageHeader event={mockEvent} eventCode="EVENT123" />
89
89
  );
90
90
 
91
91
  expect(screen.getByRole('banner')).toBeInTheDocument();
@@ -99,7 +99,6 @@ describe('[component] PublicPageHeader', () => {
99
99
  event={mockEvent}
100
100
  eventCode="EVENT123"
101
101
  className="custom-header-class"
102
- supabase={mockSupabase}
103
102
  />
104
103
  );
105
104
 
@@ -114,7 +113,6 @@ describe('[component] PublicPageHeader', () => {
114
113
  eventCode="EVENT123"
115
114
  title="Page Title"
116
115
  description="Page Description"
117
- supabase={mockSupabase}
118
116
  />
119
117
  );
120
118
 
@@ -124,7 +122,7 @@ describe('[component] PublicPageHeader', () => {
124
122
 
125
123
  it('renders with custom children', () => {
126
124
  render(
127
- <PublicPageHeader event={mockEvent} supabase={mockSupabase} eventCode="EVENT123">
125
+ <PublicPageHeader event={mockEvent} eventCode="EVENT123">
128
126
  <div data-testid="custom-content">Custom Content</div>
129
127
  </PublicPageHeader>
130
128
  );
@@ -136,7 +134,7 @@ describe('[component] PublicPageHeader', () => {
136
134
  describe('Logo Display', () => {
137
135
  it('shows app logo by default', () => {
138
136
  render(
139
- <PublicPageHeader event={mockEvent} supabase={mockSupabase} eventCode="EVENT123" />
137
+ <PublicPageHeader event={mockEvent} eventCode="EVENT123" />
140
138
  );
141
139
 
142
140
  const appLogo = screen.getByAltText('Test App');
@@ -151,7 +149,6 @@ describe('[component] PublicPageHeader', () => {
151
149
  event={mockEvent}
152
150
  eventCode="EVENT123"
153
151
  showAppLogo={false}
154
- supabase={mockSupabase}
155
152
  />
156
153
  );
157
154
 
@@ -166,7 +163,6 @@ describe('[component] PublicPageHeader', () => {
166
163
  event={mockEvent}
167
164
  eventCode="EVENT123"
168
165
  customAppLogo={customAppLogo}
169
- supabase={mockSupabase}
170
166
  />
171
167
  );
172
168
 
@@ -176,10 +172,10 @@ describe('[component] PublicPageHeader', () => {
176
172
 
177
173
  it('shows event logo by default', () => {
178
174
  render(
179
- <PublicPageHeader event={mockEvent} eventCode="EVENT123" supabase={mockSupabase} />
175
+ <PublicPageHeader event={mockEvent} eventCode="EVENT123" />
180
176
  );
181
177
 
182
- // PublicPageHeader uses FileDisplay for event logos, not EventLogo component
178
+ // PublicPageHeader uses FileDisplay for event logos
183
179
  // FileDisplay is rendered but will show "No files found" with our mocks
184
180
  // We just verify the component renders without errors
185
181
  expect(screen.getByRole('banner')).toBeInTheDocument();
@@ -189,9 +185,8 @@ describe('[component] PublicPageHeader', () => {
189
185
  render(
190
186
  <PublicPageHeader
191
187
  event={mockEvent}
192
- eventCode="EVENT123"
188
+ eventCode="EVENT123"
193
189
  showEventLogo={false}
194
- supabase={mockSupabase}
195
190
  />
196
191
  );
197
192
 
@@ -204,9 +199,8 @@ describe('[component] PublicPageHeader', () => {
204
199
  render(
205
200
  <PublicPageHeader
206
201
  event={mockEvent}
207
- eventCode="EVENT123"
202
+ eventCode="EVENT123"
208
203
  customEventLogo={customEventLogo}
209
- supabase={mockSupabase}
210
204
  />
211
205
  );
212
206
 
@@ -216,7 +210,7 @@ describe('[component] PublicPageHeader', () => {
216
210
 
217
211
  it('passes correct props to FileDisplay for event logo', () => {
218
212
  render(
219
- <PublicPageHeader event={mockEvent} eventCode="EVENT123" supabase={mockSupabase} />
213
+ <PublicPageHeader event={mockEvent} eventCode="EVENT123" />
220
214
  );
221
215
 
222
216
  // PublicPageHeader uses FileDisplay with category EVENT_LOGOS
@@ -228,7 +222,7 @@ describe('[component] PublicPageHeader', () => {
228
222
  describe('Event Information', () => {
229
223
  it('displays event name as main heading', () => {
230
224
  render(
231
- <PublicPageHeader event={mockEvent} supabase={mockSupabase} eventCode="EVENT123" />
225
+ <PublicPageHeader event={mockEvent} eventCode="EVENT123" />
232
226
  );
233
227
 
234
228
  const heading = screen.getByRole('heading', { level: 1 });
@@ -237,7 +231,7 @@ describe('[component] PublicPageHeader', () => {
237
231
 
238
232
  it('displays event venue when available', () => {
239
233
  render(
240
- <PublicPageHeader event={mockEvent} supabase={mockSupabase} eventCode="EVENT123" />
234
+ <PublicPageHeader event={mockEvent} eventCode="EVENT123" />
241
235
  );
242
236
 
243
237
  expect(screen.getByText('Test Venue')).toBeInTheDocument();
@@ -247,7 +241,7 @@ describe('[component] PublicPageHeader', () => {
247
241
  const eventWithoutVenue = { ...mockEvent, event_venue: undefined };
248
242
 
249
243
  render(
250
- <PublicPageHeader event={eventWithoutVenue} supabase={mockSupabase} eventCode="EVENT123" />
244
+ <PublicPageHeader event={eventWithoutVenue} eventCode="EVENT123" />
251
245
  );
252
246
 
253
247
  expect(screen.queryByText('Test Venue')).not.toBeInTheDocument();
@@ -259,7 +253,6 @@ describe('[component] PublicPageHeader', () => {
259
253
  event={mockEvent}
260
254
  eventCode="EVENT123"
261
255
  title="Page Title"
262
- supabase={mockSupabase}
263
256
  />
264
257
  );
265
258
 
@@ -274,7 +267,6 @@ describe('[component] PublicPageHeader', () => {
274
267
  eventCode="EVENT123"
275
268
  title="Page Title"
276
269
  description="Page Description"
277
- supabase={mockSupabase}
278
270
  />
279
271
  );
280
272
 
@@ -285,7 +277,7 @@ describe('[component] PublicPageHeader', () => {
285
277
  describe('Layout Structure', () => {
286
278
  it('has correct header classes', () => {
287
279
  const { container } = render(
288
- <PublicPageHeader event={mockEvent} supabase={mockSupabase} eventCode="EVENT123" />
280
+ <PublicPageHeader event={mockEvent} eventCode="EVENT123" />
289
281
  );
290
282
 
291
283
  const header = container.firstChild as HTMLElement;
@@ -294,7 +286,7 @@ describe('[component] PublicPageHeader', () => {
294
286
 
295
287
  it('has proper container structure', () => {
296
288
  render(
297
- <PublicPageHeader event={mockEvent} supabase={mockSupabase} eventCode="EVENT123" />
289
+ <PublicPageHeader event={mockEvent} eventCode="EVENT123" />
298
290
  );
299
291
 
300
292
  const container = screen.getByRole('banner').querySelector('div');
@@ -303,7 +295,7 @@ describe('[component] PublicPageHeader', () => {
303
295
 
304
296
  it('has proper logo row structure', () => {
305
297
  render(
306
- <PublicPageHeader event={mockEvent} supabase={mockSupabase} eventCode="EVENT123" />
298
+ <PublicPageHeader event={mockEvent} eventCode="EVENT123" />
307
299
  );
308
300
 
309
301
  const logoRow = screen.getByRole('banner').querySelector('div > div:first-child');
@@ -312,7 +304,7 @@ describe('[component] PublicPageHeader', () => {
312
304
 
313
305
  it('has proper event info structure', () => {
314
306
  render(
315
- <PublicPageHeader event={mockEvent} supabase={mockSupabase} eventCode="EVENT123" />
307
+ <PublicPageHeader event={mockEvent} eventCode="EVENT123" />
316
308
  );
317
309
 
318
310
  const eventInfo = screen.getByRole('banner').querySelector('div.pb-4');
@@ -323,7 +315,7 @@ describe('[component] PublicPageHeader', () => {
323
315
  describe('Accessibility', () => {
324
316
  it('has proper semantic structure', () => {
325
317
  render(
326
- <PublicPageHeader event={mockEvent} supabase={mockSupabase} eventCode="EVENT123" />
318
+ <PublicPageHeader event={mockEvent} eventCode="EVENT123" />
327
319
  );
328
320
 
329
321
  expect(screen.getByRole('banner')).toBeInTheDocument();
@@ -336,7 +328,6 @@ describe('[component] PublicPageHeader', () => {
336
328
  event={mockEvent}
337
329
  eventCode="EVENT123"
338
330
  title="Page Title"
339
- supabase={mockSupabase}
340
331
  />
341
332
  );
342
333
 
@@ -349,7 +340,7 @@ describe('[component] PublicPageHeader', () => {
349
340
 
350
341
  it('has proper alt text for images', () => {
351
342
  render(
352
- <PublicPageHeader event={mockEvent} supabase={mockSupabase} eventCode="EVENT123" />
343
+ <PublicPageHeader event={mockEvent} eventCode="EVENT123" />
353
344
  );
354
345
 
355
346
  const appLogo = screen.getByAltText('Test App');
@@ -362,7 +353,7 @@ describe('[component] PublicPageHeader', () => {
362
353
  const eventWithoutVenue = { ...mockEvent, event_venue: undefined };
363
354
 
364
355
  render(
365
- <PublicPageHeader event={eventWithoutVenue} supabase={mockSupabase} eventCode="EVENT123" />
356
+ <PublicPageHeader event={eventWithoutVenue} eventCode="EVENT123" />
366
357
  );
367
358
 
368
359
  expect(screen.getByText('Test Event')).toBeInTheDocument();
@@ -373,7 +364,7 @@ describe('[component] PublicPageHeader', () => {
373
364
  const eventWithEmptyVenue = { ...mockEvent, event_venue: '' };
374
365
 
375
366
  render(
376
- <PublicPageHeader event={eventWithEmptyVenue} supabase={mockSupabase} eventCode="EVENT123" />
367
+ <PublicPageHeader event={eventWithEmptyVenue} eventCode="EVENT123" />
377
368
  );
378
369
 
379
370
  expect(screen.getByText('Test Event')).toBeInTheDocument();
@@ -382,7 +373,7 @@ describe('[component] PublicPageHeader', () => {
382
373
 
383
374
  it('handles missing title', () => {
384
375
  render(
385
- <PublicPageHeader event={mockEvent} supabase={mockSupabase} eventCode="EVENT123" />
376
+ <PublicPageHeader event={mockEvent} eventCode="EVENT123" />
386
377
  );
387
378
 
388
379
  expect(screen.getByText('Test Event')).toBeInTheDocument();
@@ -395,7 +386,6 @@ describe('[component] PublicPageHeader', () => {
395
386
  event={mockEvent}
396
387
  eventCode="EVENT123"
397
388
  title="Page Title"
398
- supabase={mockSupabase}
399
389
  />
400
390
  );
401
391
 
@@ -407,13 +397,13 @@ describe('[component] PublicPageHeader', () => {
407
397
  describe('Props Validation', () => {
408
398
  it('handles missing event prop gracefully', () => {
409
399
  // @ts-expect-error - Testing invalid props
410
- const { container } = render(<PublicPageHeader eventCode="EVENT123" supabase={mockSupabase} />);
400
+ const { container } = render(<PublicPageHeader eventCode="EVENT123" />);
411
401
  expect(container.firstChild).toBeInTheDocument();
412
402
  });
413
403
 
414
404
  it('handles missing eventCode prop gracefully', () => {
415
405
  // @ts-expect-error - Testing invalid props
416
- const { container } = render(<PublicPageHeader event={mockEvent} supabase={mockSupabase} />);
406
+ const { container } = render(<PublicPageHeader event={mockEvent} />);
417
407
  expect(container.firstChild).toBeInTheDocument();
418
408
  });
419
409
  });
@@ -10,10 +10,10 @@
10
10
  *
11
11
  * @example
12
12
  * // Import individual components
13
- * import { PublicPageLayout, PublicPageHeader, EventLogo } from '@jmruthers/pace-core/components/PublicLayout';
13
+ * import { PublicPageLayout, PublicPageHeader } from '@jmruthers/pace-core/components/PublicLayout';
14
14
  *
15
15
  * // Or import from main package
16
- * import { PublicPageLayout, PublicPageHeader, EventLogo } from '@jmruthers/pace-core';
16
+ * import { PublicPageLayout, PublicPageHeader } from '@jmruthers/pace-core';
17
17
  */
18
18
 
19
19
  // === MAIN LAYOUT COMPONENTS ===
@@ -25,8 +25,6 @@ export { PublicPageDebugger } from './PublicPageDebugger';
25
25
  export { PublicPageDiagnostic } from './PublicPageDiagnostic';
26
26
  export { PublicPageContextChecker } from './PublicPageContextChecker';
27
27
 
28
- // === LOGO COMPONENTS ===
29
- export { EventLogo, EventLogoCompact, EventLogoLarge } from './EventLogo';
30
28
 
31
29
  // === ERROR HANDLING COMPONENTS ===
32
30
  export {
@@ -46,6 +44,5 @@ export {
46
44
  export type { PublicPageLayoutProps } from './PublicPageLayout';
47
45
  export type { PublicPageHeaderProps } from './PublicPageHeader';
48
46
  export type { PublicPageFooterProps } from './PublicPageFooter';
49
- export type { EventLogoProps } from './EventLogo';
50
47
  export type { PublicErrorBoundaryProps, PublicErrorBoundaryState } from './PublicErrorBoundary';
51
48
  export type { PublicLoadingSpinnerProps } from './PublicLoadingSpinner';
@@ -143,7 +143,7 @@ const Toast = React.forwardRef<
143
143
  ref={ref}
144
144
  data-testid="toast-root"
145
145
  className={cn(
146
- "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
146
+ "group pointer-events-auto bg-background relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
147
147
  className
148
148
  )}
149
149
  {...props}
@@ -35,7 +35,6 @@ import {
35
35
  PublicPageHeader,
36
36
  PublicPageFooter,
37
37
  FileDisplay,
38
- EventLogo,
39
38
  usePublicEvent,
40
39
  usePublicRouteParams,
41
40
  PublicLoadingSpinner,
@@ -234,12 +233,23 @@ export function PublicEventPageCompact() {
234
233
 
235
234
  <main className="max-w-4xl mx-auto px-4 py-6">
236
235
  <div className="text-center mb-6">
237
- <EventLogo
238
- eventId={event.event_id}
239
- eventName={event.event_name}
240
- organisationId={event.organisation_id}
241
- size="xl"
242
- className="mx-auto mb-4"
236
+ <FileDisplay
237
+ table_name="event"
238
+ record_id={event.event_id}
239
+ organisation_id={event.organisation_id}
240
+ category={FileCategory.EVENT_LOGOS}
241
+ displayOnly={true}
242
+ showFallback={true}
243
+ fallbackSize="xl"
244
+ className="h-24 w-24 mx-auto mb-4 object-contain rounded"
245
+ generateFallbackText={(fileName) => {
246
+ if (!event.event_name) return 'EV';
247
+ return event.event_name
248
+ .split(/[\s\-_]+/)
249
+ .map(word => word.charAt(0).toUpperCase())
250
+ .join('')
251
+ .substring(0, 3);
252
+ }}
243
253
  />
244
254
  <h1 className="text-2xl font-bold text-gray-900">{event.event_name}</h1>
245
255
  {event.event_date && (
@@ -39,7 +39,6 @@ import {
39
39
  PublicPageHeader,
40
40
  PublicPageFooter,
41
41
  FileDisplay,
42
- EventLogo,
43
42
  usePublicEvent,
44
43
  usePublicRouteParams,
45
44
  PublicLoadingSpinner,
@@ -251,13 +250,24 @@ function PublicEventPage() {
251
250
 
252
251
  <main className="max-w-4xl mx-auto px-4 py-6">
253
252
  <div className="text-center mb-6">
254
- <EventLogo
255
- eventId={event.event_id}
256
- eventName={event.event_name}
257
- organisationId={event.organisation_id}
258
- size="xl"
259
- className="mx-auto mb-4"
260
- />
253
+ <FileDisplay
254
+ table_name="event"
255
+ record_id={event.event_id}
256
+ organisation_id={event.organisation_id}
257
+ category={FileCategory.EVENT_LOGOS}
258
+ displayOnly={true}
259
+ showFallback={true}
260
+ fallbackSize="xl"
261
+ className="h-24 w-24 mx-auto mb-4 object-contain rounded"
262
+ generateFallbackText={(fileName) => {
263
+ if (!event.event_name) return 'EV';
264
+ return event.event_name
265
+ .split(/[\s\-_]+/)
266
+ .map(word => word.charAt(0).toUpperCase())
267
+ .join('')
268
+ .substring(0, 3);
269
+ }}
270
+ />
261
271
  <h1 className="text-2xl font-bold text-gray-900">{event.event_name}</h1>
262
272
  {event.event_date && (
263
273
  <p className="text-gray-600 mt-2">
@@ -360,7 +360,16 @@ export function useFileReferenceById(
360
360
  }
361
361
 
362
362
  /**
363
- * Hook for getting files by category
363
+ * Convenience hook for getting files by category with automatic URL loading.
364
+ *
365
+ * This hook wraps useFileReference().getFilesByCategory and automatically:
366
+ * - Loads file references filtered by category
367
+ * - Generates URLs for all files (public URLs for public files, signed URLs for private files)
368
+ * - Manages state for fileReferences and fileUrls
369
+ * - Auto-refetches when table_name, record_id, category, or organisation_id changes
370
+ *
371
+ * Use this hook when you need files by category with their URLs ready to display.
372
+ * For more control, use useFileReference() directly and manage URLs yourself.
364
373
  */
365
374
  export function useFilesByCategory(
366
375
  supabase: SupabaseClient,
@@ -15,6 +15,20 @@ import { setOrganisationContext } from './organisationContext';
15
15
  export class FileReferenceServiceImpl implements FileReferenceService {
16
16
  constructor(private supabase: SupabaseClient) {}
17
17
 
18
+ /**
19
+ * Creates a file reference by uploading a file to storage and linking it in the database.
20
+ *
21
+ * Storage Flow:
22
+ * 1. Upload file to storage bucket first (files or public-files based on is_public flag)
23
+ * - Path format: {orgId}/{category}/{timestamp-uuid-filename}
24
+ * - Bucket selection: 'files' (private) or 'public-files' (public)
25
+ * 2. Extract file metadata (dimensions, hash, etc.)
26
+ * 3. Set organisation context for RLS policies
27
+ * 4. Create database reference via RPC function
28
+ * 5. If DB insert fails, rollback by deleting uploaded file
29
+ *
30
+ * This ensures atomicity: either both storage and DB succeed, or both are cleaned up.
31
+ */
18
32
  async createFileReference(options: FileUploadOptions, file: File): Promise<FileReference> {
19
33
  try {
20
34
 
@@ -29,12 +43,14 @@ export class FileReferenceServiceImpl implements FileReferenceService {
29
43
  throw new Error('record_id is required for file upload');
30
44
  }
31
45
 
32
- // Upload file to storage (this will generate the path internally)
46
+ // Step 1: Upload file to storage bucket first
47
+ // This generates a unique path: {orgId}/{category}/{timestamp-uuid-filename}
48
+ // Bucket is automatically selected based on is_public flag
33
49
  const uploadResult = await uploadFile(this.supabase, file, {
34
50
  appName: 'file-reference',
35
51
  orgId: options.organisation_id,
36
52
  isPublic: options.is_public || false,
37
- customPath: options.category // Use category as the custom path
53
+ customPath: options.category // Use category as the custom path segment
38
54
  });
39
55
  if (!uploadResult.success) {
40
56
  throw new Error(`Failed to upload file: ${uploadResult.error}`);
@@ -46,23 +62,24 @@ export class FileReferenceServiceImpl implements FileReferenceService {
46
62
 
47
63
  const filePath = uploadResult.path;
48
64
 
49
- // Extract file metadata
65
+ // Step 2: Extract file metadata (dimensions, hash, etc.)
50
66
  const metadata = await extractFileMetadata(file, {
51
67
  appName: 'file-reference',
52
68
  orgId: options.organisation_id,
53
69
  isPublic: options.is_public || false
54
70
  }, 'system');
55
71
 
56
- // Set organisation context in database session before creating file reference
72
+ // Step 3: Set organisation context in database session before creating file reference
57
73
  // This ensures RLS policies can check the organisation context
58
74
  await setOrganisationContext(this.supabase, options.organisation_id);
59
75
 
60
- // Create file reference in database using new RPC function name
76
+ // Step 4: Create file reference in database using RPC function
77
+ // This links the storage path to the record in file_references table
61
78
  const { data, error } = await this.supabase
62
79
  .rpc('data_file_reference_create', {
63
80
  p_table_name: options.table_name,
64
81
  p_record_id: options.record_id,
65
- p_file_path: filePath,
82
+ p_file_path: filePath, // Storage path from step 1
66
83
  p_organisation_id: options.organisation_id,
67
84
  p_app_id: options.app_id,
68
85
  p_file_metadata: {
@@ -76,8 +93,8 @@ export class FileReferenceServiceImpl implements FileReferenceService {
76
93
  p_is_public: options.is_public || false
77
94
  });
78
95
 
96
+ // Step 5: Rollback - if database insert fails, clean up uploaded file
79
97
  if (error) {
80
- // Clean up uploaded file if database insert fails
81
98
  await deleteFile(this.supabase, filePath, options.is_public || false);
82
99
  throw new Error(`Failed to create file reference: ${error.message}`);
83
100
  }
@@ -46,12 +46,23 @@ export function generateFilePath(options: StorageUploadOptions, fileName: string
46
46
 
47
47
  /**
48
48
  * Generate a unique filename with timestamp and UUID
49
+ * Sanitizes the original filename to remove spaces and invalid characters for storage compatibility
49
50
  */
50
51
  export function generateUniqueFileName(originalName: string): string {
51
52
  const timestamp = Date.now();
52
53
  const uuid = crypto.randomUUID();
53
54
  const extension = originalName.split('.').pop() || '';
54
- const baseName = originalName.replace(/\.[^/.]+$/, '');
55
+ let baseName = originalName.replace(/\.[^/.]+$/, '');
56
+
57
+ // Sanitize the base name for storage compatibility
58
+ // Replace spaces with underscores and remove invalid characters
59
+ baseName = baseName
60
+ .trim()
61
+ .replace(/\s+/g, '_') // Replace spaces with underscores
62
+ .replace(/[<>:"/\\|?*]/g, '') // Remove invalid file name characters
63
+ .replace(/\.\./g, '') // Remove directory traversal attempts
64
+ .replace(/^\.+|\.+$/g, '') // Remove leading/trailing dots
65
+ .substring(0, 200); // Limit length to leave room for timestamp and UUID
55
66
 
56
67
  // If there's no extension, don't add one
57
68
  if (!extension || extension === originalName) {