@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.
- package/dist/{PublicLoadingSpinner-C2h8zg67.d.ts → PublicLoadingSpinner-Cvgk-V0F.d.ts} +22 -55
- package/dist/{chunk-2ZYHCFUO.js → chunk-7ME4Z5OY.js} +148 -12
- package/dist/chunk-7ME4Z5OY.js.map +1 -0
- package/dist/{chunk-MKMKUCPF.js → chunk-SZWRW5FD.js} +20 -139
- package/dist/chunk-SZWRW5FD.js.map +1 -0
- package/dist/{chunk-A5DFMP3O.js → chunk-UDWTCBSH.js} +127 -498
- package/dist/chunk-UDWTCBSH.js.map +1 -0
- package/dist/components.d.ts +1 -1
- package/dist/components.js +2 -8
- package/dist/components.js.map +1 -1
- package/dist/hooks.js +5 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -12
- package/dist/index.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/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- 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 +2 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.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/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/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +77 -35
- 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/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.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 +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +11 -24
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.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/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/UseEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UseEventLogoReturn.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/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 +29 -96
- package/docs/implementation-guides/file-reference-system.md +53 -2
- package/package.json +1 -1
- package/src/components/FileDisplay/FileDisplay.test.tsx +1 -1
- package/src/components/FileDisplay/FileDisplay.tsx +189 -300
- package/src/components/PublicLayout/PublicPageHeader.tsx +14 -9
- package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +25 -35
- package/src/components/PublicLayout/index.ts +2 -5
- package/src/components/Toast/Toast.tsx +1 -1
- package/src/examples/PublicEventPage.tsx +17 -7
- package/src/examples/PublicPageApp.tsx +18 -8
- package/src/hooks/useFileReference.ts +10 -1
- package/src/utils/file-reference.ts +24 -7
- package/src/utils/storage/helpers.ts +12 -1
- package/dist/chunk-2ZYHCFUO.js.map +0 -1
- package/dist/chunk-A5DFMP3O.js.map +0 -1
- package/dist/chunk-MKMKUCPF.js.map +0 -1
- package/docs/api/interfaces/EventLogoProps.md +0 -152
- 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
|
-
* -
|
|
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
|
-
|
|
134
|
-
|
|
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
|
|
50
|
-
vi.mock('
|
|
51
|
-
|
|
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"
|
|
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}
|
|
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}
|
|
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"
|
|
175
|
+
<PublicPageHeader event={mockEvent} eventCode="EVENT123" />
|
|
180
176
|
);
|
|
181
177
|
|
|
182
|
-
// PublicPageHeader uses FileDisplay for event logos
|
|
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"
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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"
|
|
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}
|
|
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
|
|
13
|
+
* import { PublicPageLayout, PublicPageHeader } from '@jmruthers/pace-core/components/PublicLayout';
|
|
14
14
|
*
|
|
15
15
|
* // Or import from main package
|
|
16
|
-
* import { PublicPageLayout, PublicPageHeader
|
|
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
|
-
<
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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) {
|