@jmruthers/pace-core 0.5.101 → 0.5.103

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 (152) hide show
  1. package/dist/{DataTable-DXELRJIX.js → DataTable-EEDFYMJP.js} +2 -2
  2. package/dist/{PublicLoadingSpinner-C2h8zg67.d.ts → PublicLoadingSpinner-48ewSMKK.d.ts} +22 -150
  3. package/dist/{chunk-2ZYHCFUO.js → chunk-5SGBVBRU.js} +2 -2
  4. package/dist/{chunk-EVVRUGQ2.js → chunk-62AVH7CM.js} +78 -55
  5. package/dist/{chunk-EVVRUGQ2.js.map → chunk-62AVH7CM.js.map} +1 -1
  6. package/dist/{chunk-A5DFMP3O.js → chunk-SZWCMVTQ.js} +135 -669
  7. package/dist/chunk-SZWCMVTQ.js.map +1 -0
  8. package/dist/{chunk-MKMKUCPF.js → chunk-X33A4WWI.js} +42 -141
  9. package/dist/chunk-X33A4WWI.js.map +1 -0
  10. package/dist/components.d.ts +1 -1
  11. package/dist/components.js +3 -15
  12. package/dist/components.js.map +1 -1
  13. package/dist/hooks.d.ts +1 -1
  14. package/dist/hooks.js +2 -9
  15. package/dist/hooks.js.map +1 -1
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.js +4 -22
  18. package/dist/index.js.map +1 -1
  19. package/dist/types.js +3 -3
  20. package/dist/{usePublicRouteParams-BwMR2uub.d.ts → usePublicRouteParams-BiXgKiYa.d.ts} +1 -117
  21. package/dist/utils.js +1 -1
  22. package/docs/api/classes/ColumnFactory.md +1 -1
  23. package/docs/api/classes/ErrorBoundary.md +1 -1
  24. package/docs/api/classes/InvalidScopeError.md +1 -1
  25. package/docs/api/classes/MissingUserContextError.md +1 -1
  26. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  27. package/docs/api/classes/PermissionDeniedError.md +1 -1
  28. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  29. package/docs/api/classes/RBACAuditManager.md +1 -1
  30. package/docs/api/classes/RBACCache.md +1 -1
  31. package/docs/api/classes/RBACEngine.md +1 -1
  32. package/docs/api/classes/RBACError.md +1 -1
  33. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  34. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  35. package/docs/api/classes/StorageUtils.md +2 -1
  36. package/docs/api/enums/FileCategory.md +1 -1
  37. package/docs/api/interfaces/AggregateConfig.md +1 -1
  38. package/docs/api/interfaces/ButtonProps.md +1 -1
  39. package/docs/api/interfaces/CardProps.md +1 -1
  40. package/docs/api/interfaces/ColorPalette.md +1 -1
  41. package/docs/api/interfaces/ColorShade.md +1 -1
  42. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  43. package/docs/api/interfaces/DataRecord.md +1 -1
  44. package/docs/api/interfaces/DataTableAction.md +1 -1
  45. package/docs/api/interfaces/DataTableColumn.md +1 -1
  46. package/docs/api/interfaces/DataTableProps.md +1 -1
  47. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  48. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  49. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  50. package/docs/api/interfaces/FileDisplayProps.md +77 -35
  51. package/docs/api/interfaces/FileMetadata.md +1 -1
  52. package/docs/api/interfaces/FileReference.md +1 -1
  53. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  54. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  55. package/docs/api/interfaces/FileUploadProps.md +1 -1
  56. package/docs/api/interfaces/FooterProps.md +1 -1
  57. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  58. package/docs/api/interfaces/InputProps.md +1 -1
  59. package/docs/api/interfaces/LabelProps.md +1 -1
  60. package/docs/api/interfaces/LoginFormProps.md +1 -1
  61. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  62. package/docs/api/interfaces/NavigationContextType.md +1 -1
  63. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  64. package/docs/api/interfaces/NavigationItem.md +1 -1
  65. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  66. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  67. package/docs/api/interfaces/Organisation.md +1 -1
  68. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  69. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  70. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  71. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  72. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  73. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  74. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  75. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  76. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  77. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  78. package/docs/api/interfaces/PaletteData.md +1 -1
  79. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  80. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  81. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  82. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  83. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  84. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  85. package/docs/api/interfaces/PublicPageHeaderProps.md +11 -24
  86. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  87. package/docs/api/interfaces/RBACConfig.md +1 -1
  88. package/docs/api/interfaces/RBACLogger.md +1 -1
  89. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  90. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  91. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  92. package/docs/api/interfaces/RouteConfig.md +1 -1
  93. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  94. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  95. package/docs/api/interfaces/StorageConfig.md +1 -1
  96. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  97. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  98. package/docs/api/interfaces/StorageListOptions.md +1 -1
  99. package/docs/api/interfaces/StorageListResult.md +1 -1
  100. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  101. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  102. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  103. package/docs/api/interfaces/StyleImport.md +1 -1
  104. package/docs/api/interfaces/SwitchProps.md +1 -1
  105. package/docs/api/interfaces/ToastActionElement.md +1 -1
  106. package/docs/api/interfaces/ToastProps.md +1 -1
  107. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  108. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  109. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  110. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  111. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  112. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  113. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  114. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  115. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  116. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  117. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  118. package/docs/api/interfaces/UserEventAccess.md +1 -1
  119. package/docs/api/interfaces/UserMenuProps.md +1 -1
  120. package/docs/api/interfaces/UserProfile.md +1 -1
  121. package/docs/api/modules.md +29 -244
  122. package/docs/implementation-guides/file-reference-system.md +84 -21
  123. package/package.json +1 -1
  124. package/src/components/DataTable/components/DataTableCore.tsx +23 -13
  125. package/src/components/DataTable/hooks/useTableColumns.ts +36 -6
  126. package/src/components/FileDisplay/FileDisplay.test.tsx +1 -1
  127. package/src/components/FileDisplay/FileDisplay.tsx +189 -300
  128. package/src/components/PublicLayout/PublicPageHeader.tsx +15 -10
  129. package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +25 -35
  130. package/src/components/PublicLayout/index.ts +2 -5
  131. package/src/components/Toast/Toast.tsx +1 -1
  132. package/src/components/index.ts +0 -2
  133. package/src/examples/PublicEventPage.tsx +17 -7
  134. package/src/examples/PublicPageApp.tsx +18 -8
  135. package/src/hooks/public/index.ts +2 -4
  136. package/src/hooks/useFileReference.ts +10 -1
  137. package/src/index.ts +0 -2
  138. package/src/utils/file-reference.ts +54 -9
  139. package/src/utils/storage/README.md +22 -20
  140. package/src/utils/storage/helpers.ts +12 -1
  141. package/dist/chunk-A5DFMP3O.js.map +0 -1
  142. package/dist/chunk-MKMKUCPF.js.map +0 -1
  143. package/docs/api/interfaces/EventLogoProps.md +0 -152
  144. package/docs/api/interfaces/UseEventLogoOptions.md +0 -74
  145. package/docs/api/interfaces/UseEventLogoReturn.md +0 -81
  146. package/docs/api/interfaces/UsePublicEventLogoOptions.md +0 -87
  147. package/docs/api/interfaces/UsePublicEventLogoReturn.md +0 -81
  148. package/src/components/PublicLayout/EventLogo.tsx +0 -474
  149. package/src/hooks/public/usePublicEventLogo.ts +0 -295
  150. package/src/hooks/useEventLogo.ts +0 -316
  151. /package/dist/{DataTable-DXELRJIX.js.map → DataTable-EEDFYMJP.js.map} +0 -0
  152. /package/dist/{chunk-2ZYHCFUO.js.map → chunk-5SGBVBRU.js.map} +0 -0
@@ -1,26 +1,50 @@
1
1
  import React, { useState, useEffect, useCallback, useRef, useContext, useMemo } from 'react';
2
- import { SupabaseClient } from '@supabase/supabase-js';
3
2
  import { FileReference, FileCategory } from '../../types/file-reference';
4
- import { useFileReferenceForRecord, useFileReference } from '../../hooks/useFileReference';
5
3
  import { usePublicFileDisplay } from '../../hooks/public/usePublicFileDisplay';
6
4
  import { useFileDisplay } from '../../hooks/useFileDisplay';
7
5
  import { useFileUrl } from '../../hooks/useFileUrl';
8
- import { getPublicUrl, getSignedUrl } from '../../utils/storage/helpers';
9
6
  import { PublicPageContext, useIsPublicPage } from '../PublicLayout/PublicPageProvider';
10
7
  import { useUnifiedAuth } from '../../providers/services/UnifiedAuthProvider';
11
8
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogBody, DialogFooter } from '../Dialog/Dialog';
12
9
  import { Button } from '../Button/Button';
13
10
 
11
+ /**
12
+ * Size classes for fallback display
13
+ */
14
+ const fallbackSizeClasses = {
15
+ xs: 'h-4 w-4 text-xs',
16
+ sm: 'h-6 w-6 text-sm',
17
+ md: 'h-8 w-8 text-base',
18
+ lg: 'h-12 w-12 text-lg',
19
+ xl: 'h-16 w-16 text-xl',
20
+ '2xl': 'h-20 w-20 text-2xl'
21
+ };
22
+
23
+ /**
24
+ * Default fallback text generator - extracts initials from file name
25
+ */
26
+ function defaultGenerateFallbackText(fileName?: string): string {
27
+ if (!fileName) return 'FL';
28
+
29
+ // Extract initials from file name (without extension)
30
+ const baseName = fileName.replace(/\.[^/.]+$/, '');
31
+ const words = baseName.split(/[\s\-_]+/);
32
+
33
+ if (words.length === 0) return 'FL';
34
+
35
+ return words
36
+ .map(word => word.charAt(0).toUpperCase())
37
+ .join('')
38
+ .substring(0, 3); // Max 3 characters
39
+ }
40
+
14
41
  export interface FileDisplayProps {
15
- /** Supabase client instance. Optional when used in PublicPageProvider or UnifiedAuthProvider context */
16
- supabase?: SupabaseClient;
17
42
  table_name: string;
18
43
  record_id: string;
19
44
  organisation_id: string;
20
45
  category?: FileCategory;
21
46
  /** Display only a single file instead of all files. Uses first file (prefers images) from all files, without category filtering */
22
47
  displayOnly?: boolean;
23
- showUpload?: boolean;
24
48
  showDelete?: boolean;
25
49
  className?: string;
26
50
  children?: React.ReactNode;
@@ -28,6 +52,14 @@ export interface FileDisplayProps {
28
52
  loadingComponent?: React.ComponentType;
29
53
  /** Custom error component to render when an error occurs */
30
54
  errorComponent?: React.ComponentType<{ error: Error | string | null; retry?: () => void }>;
55
+ /** Whether to show fallback UI when no file is available or image fails to load */
56
+ showFallback?: boolean;
57
+ /** Custom function to generate fallback text from file name or other context */
58
+ generateFallbackText?: (fileName?: string) => string;
59
+ /** Explicit fallback text to display (overrides generateFallbackText) */
60
+ fallbackText?: string;
61
+ /** Size variant for fallback display (only applies when showFallback is true) */
62
+ fallbackSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
31
63
  }
32
64
 
33
65
  // Shared rendering logic for file display
@@ -46,11 +78,13 @@ interface FileDisplayContentProps {
46
78
  children?: React.ReactNode;
47
79
  onDelete?: () => Promise<void>;
48
80
  clearError?: () => void;
49
- supabase?: SupabaseClient;
50
81
  organisation_id: string;
51
- loadingUrls?: Set<string>;
52
82
  loadingComponent?: React.ComponentType;
53
83
  errorComponent?: React.ComponentType<{ error: Error | string | null; retry?: () => void }>;
84
+ showFallback?: boolean;
85
+ generateFallbackText?: (fileName?: string) => string;
86
+ fallbackText?: string;
87
+ fallbackSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
54
88
  }
55
89
 
56
90
  function FileDisplayContent({
@@ -68,19 +102,33 @@ function FileDisplayContent({
68
102
  children,
69
103
  onDelete,
70
104
  clearError,
71
- supabase,
72
105
  organisation_id,
73
- loadingUrls = new Set(),
74
106
  loadingComponent: LoadingComponent,
75
- errorComponent: ErrorComponent
107
+ errorComponent: ErrorComponent,
108
+ showFallback = false,
109
+ generateFallbackText = defaultGenerateFallbackText,
110
+ fallbackText,
111
+ fallbackSize = 'md'
76
112
  }: FileDisplayContentProps) {
77
113
  const [imageError, setImageError] = useState(false);
78
114
  const [internalFileUrls, setInternalFileUrls] = useState<Map<string, string>>(new Map(fileUrls));
79
115
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
80
- const loadedFilesRef = useRef<Set<string>>(new Set());
81
- const loadingUrlsRef = useRef<Set<string>>(new Set(loadingUrls));
82
116
  const fileReferencesRef = useRef<FileReference[]>([]);
83
117
 
118
+ // Compute fallback text
119
+ const computedFallbackText = useMemo(() => {
120
+ if (fallbackText) return fallbackText;
121
+ const fileName = fileReference?.file_metadata?.fileName;
122
+ return generateFallbackText(fileName);
123
+ }, [fallbackText, fileReference, generateFallbackText]);
124
+
125
+ // Compute fallback classes
126
+ const fallbackClasses = useMemo(() => {
127
+ const sizeClass = fallbackSizeClasses[fallbackSize];
128
+ const baseClasses = 'flex items-center justify-center bg-sec-100 text-sec-600 font-semibold rounded';
129
+ return `${baseClasses} ${sizeClass} ${className}`.trim();
130
+ }, [fallbackSize, className]);
131
+
84
132
  // Sync fileUrls prop with internal state
85
133
  useEffect(() => {
86
134
  setInternalFileUrls(new Map(fileUrls));
@@ -93,64 +141,11 @@ function FileDisplayContent({
93
141
 
94
142
  if (currentIds !== prevIds) {
95
143
  fileReferencesRef.current = fileReferences;
96
- // Reset loaded files ref when file references change
97
- loadedFilesRef.current.clear();
144
+ // Reset internal URLs when file references change
98
145
  setInternalFileUrls(new Map());
99
146
  }
100
147
  }, [fileReferences]);
101
148
 
102
- // Fetch URLs for all file references (for multiple files view when not using context hooks)
103
- useEffect(() => {
104
- if (!supabase || category || fileReferences.length === 0) return;
105
-
106
- const loadFileUrls = async () => {
107
- // Find files that need URLs loaded
108
- const urlsToLoad = fileReferences.filter(fileRef => {
109
- return !loadedFilesRef.current.has(fileRef.id) && !loadingUrlsRef.current.has(fileRef.id);
110
- });
111
-
112
- if (urlsToLoad.length === 0) return;
113
-
114
- // Mark files as loading (update both state and ref)
115
- loadingUrlsRef.current = new Set([...loadingUrlsRef.current, ...urlsToLoad.map(f => f.id)]);
116
-
117
- // Load URLs for files that need them
118
- for (const fileRef of urlsToLoad) {
119
- try {
120
- let url: string | null = null;
121
-
122
- if (fileRef.is_public) {
123
- // Public files: generate public URL
124
- url = getPublicUrl(supabase, fileRef.file_path, true);
125
- } else {
126
- // Private files: generate signed URL
127
- const signedUrlResult = await getSignedUrl(supabase, fileRef.file_path, {
128
- appName: 'file-reference',
129
- orgId: organisation_id,
130
- expiresIn: 3600
131
- });
132
- url = signedUrlResult?.url || null;
133
- }
134
-
135
- if (url) {
136
- setInternalFileUrls(prev => {
137
- const updated = new Map(prev);
138
- updated.set(fileRef.id, url!);
139
- return updated;
140
- });
141
- loadedFilesRef.current.add(fileRef.id);
142
- }
143
- } catch (error) {
144
- console.error(`Failed to load URL for file ${fileRef.id}:`, error);
145
- } finally {
146
- loadingUrlsRef.current.delete(fileRef.id);
147
- }
148
- }
149
- };
150
-
151
- loadFileUrls();
152
- }, [category, fileReferences.map(f => f.id).join(','), supabase, organisation_id]);
153
-
154
149
  const handleDeleteClick = () => {
155
150
  setDeleteDialogOpen(true);
156
151
  };
@@ -163,8 +158,28 @@ function FileDisplayContent({
163
158
  setImageError(false);
164
159
  };
165
160
 
166
- const handleImageError = () => {
161
+ const handleImageError = (e?: React.SyntheticEvent<HTMLImageElement>) => {
167
162
  setImageError(true);
163
+
164
+ // If fallback is enabled, show fallback UI when image fails to load
165
+ if (showFallback && e) {
166
+ const target = e.target as HTMLImageElement;
167
+ target.style.display = 'none';
168
+
169
+ // Check if fallback already exists
170
+ if (target.nextSibling && (target.nextSibling as HTMLElement).className.includes('bg-sec-100')) {
171
+ return; // Fallback already shown
172
+ }
173
+
174
+ // Create fallback element
175
+ const fallback = document.createElement('div');
176
+ fallback.className = fallbackClasses;
177
+ fallback.textContent = computedFallbackText;
178
+ fallback.title = fileReference?.file_metadata?.fileName || 'File';
179
+
180
+ // Insert fallback after the image
181
+ target.parentNode?.insertBefore(fallback, target.nextSibling);
182
+ }
168
183
  };
169
184
 
170
185
  const getFileIcon = (fileType: string) => {
@@ -186,6 +201,37 @@ function FileDisplayContent({
186
201
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
187
202
  };
188
203
 
204
+ // Show fallback immediately if enabled and we have no files (even during loading)
205
+ // This provides better UX by showing fallback UI instead of a spinner when we know there are no files
206
+ if (fileCount === 0 && !isLoading) {
207
+ // Show fallback if enabled
208
+ if (showFallback) {
209
+ return (
210
+ <div className={fallbackClasses} title="No file">
211
+ {computedFallbackText}
212
+ {children}
213
+ </div>
214
+ );
215
+ }
216
+
217
+ return (
218
+ <div className={`text-sec-500 text-center p-4 ${className}`}>
219
+ No files found
220
+ {children}
221
+ </div>
222
+ );
223
+ }
224
+
225
+ // During loading, show fallback if enabled (better UX than spinner for empty states)
226
+ if (isLoading && showFallback && fileCount === 0) {
227
+ return (
228
+ <div className={fallbackClasses} title="Loading...">
229
+ {computedFallbackText}
230
+ {children}
231
+ </div>
232
+ );
233
+ }
234
+
189
235
  if (isLoading) {
190
236
  if (LoadingComponent) {
191
237
  return <LoadingComponent />;
@@ -201,6 +247,16 @@ function FileDisplayContent({
201
247
  if (ErrorComponent) {
202
248
  return <ErrorComponent error={error} retry={clearError} />;
203
249
  }
250
+
251
+ // Show fallback if enabled
252
+ if (showFallback) {
253
+ return (
254
+ <div className={fallbackClasses} title="File unavailable">
255
+ {computedFallbackText}
256
+ </div>
257
+ );
258
+ }
259
+
204
260
  return (
205
261
  <div className={`p-4 bg-acc-50 border border-acc-200 rounded-lg ${className}`}>
206
262
  <div className="text-acc-600">
@@ -218,21 +274,21 @@ function FileDisplayContent({
218
274
  );
219
275
  }
220
276
 
221
- if (fileCount === 0) {
222
- return (
223
- <div className={`text-sec-500 text-center p-4 ${className}`}>
224
- No files found
225
- {children}
226
- </div>
227
- );
228
- }
229
-
230
277
  // Single file display (when category or displayOnly is specified)
231
278
  if ((category || displayOnly) && fileReference) {
232
279
  const isImage = fileReference.file_metadata.fileType?.startsWith('image/');
233
280
 
234
281
  // Simplified image-only display when displayOnly is true and it's an image
235
- if (displayOnly && isImage && !imageError && !showDelete) {
282
+ if (displayOnly && isImage && !showDelete) {
283
+ // Show fallback if image error occurred and fallback is enabled
284
+ if (imageError && showFallback) {
285
+ return (
286
+ <div className={fallbackClasses} title={fileReference.file_metadata.fileName || 'File'}>
287
+ {computedFallbackText}
288
+ </div>
289
+ );
290
+ }
291
+
236
292
  // Show loading skeleton if URL is not available yet
237
293
  if (!fileUrl) {
238
294
  return (
@@ -243,6 +299,7 @@ function FileDisplayContent({
243
299
  </div>
244
300
  );
245
301
  }
302
+
246
303
  return (
247
304
  <img
248
305
  src={fileUrl}
@@ -254,6 +311,15 @@ function FileDisplayContent({
254
311
  }
255
312
 
256
313
  // Standard single file display with wrapper
314
+ // For displayOnly mode, if fallback is enabled and there's no URL or image error, show fallback instead of folder icon
315
+ if (displayOnly && showFallback && (!fileUrl || imageError || !isImage)) {
316
+ return (
317
+ <div className={fallbackClasses} title={fileReference.file_metadata.fileName || 'File'}>
318
+ {computedFallbackText}
319
+ </div>
320
+ );
321
+ }
322
+
257
323
  return (
258
324
  <div className={`space-y-2 ${className}`}>
259
325
  {isImage && fileUrl && !imageError ? (
@@ -295,6 +361,11 @@ function FileDisplayContent({
295
361
  </>
296
362
  )}
297
363
  </div>
364
+ ) : isImage && imageError && showFallback ? (
365
+ // Show fallback when image fails to load and fallback is enabled
366
+ <div className={fallbackClasses} title={fileReference.file_metadata.fileName || 'File'}>
367
+ {computedFallbackText}
368
+ </div>
298
369
  ) : (
299
370
  <div className="flex items-center space-x-3 p-3 bg-sec-50 rounded-lg border border-sec-200">
300
371
  <span className="text-2xl">
@@ -352,16 +423,11 @@ function FileDisplayContent({
352
423
  {fileReferences.map((fileRef) => {
353
424
  const isImage = fileRef.file_metadata.fileType?.startsWith('image/');
354
425
  const fileUrl = internalFileUrls.get(fileRef.id) || null;
355
- const isLoadingUrl = loadingUrlsRef.current.has(fileRef.id);
356
426
  const canDownload = !isImage && fileUrl;
357
427
 
358
428
  return (
359
429
  <div key={fileRef.id} className="flex items-center space-x-3 p-3 bg-sec-50 rounded-lg border border-sec-200">
360
- {isLoadingUrl ? (
361
- <div className="w-12 h-12 flex items-center justify-center bg-sec-100 rounded animate-pulse">
362
- <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-main-500"></div>
363
- </div>
364
- ) : isImage && fileUrl ? (
430
+ {isImage && fileUrl ? (
365
431
  <img
366
432
  src={fileUrl}
367
433
  alt={fileRef.file_metadata.fileName || 'File'}
@@ -413,181 +479,6 @@ function FileDisplayContent({
413
479
  );
414
480
  }
415
481
 
416
- /**
417
- * Internal component for backwards compatibility (when supabase is explicitly provided)
418
- */
419
- function FileDisplayBackwardsCompat({
420
- supabase,
421
- table_name,
422
- record_id,
423
- organisation_id,
424
- category,
425
- displayOnly = false,
426
- showUpload = false,
427
- showDelete = false,
428
- className = '',
429
- children,
430
- loadingComponent,
431
- errorComponent
432
- }: Required<Pick<FileDisplayProps, 'supabase'>> & Omit<FileDisplayProps, 'supabase'>) {
433
- const {
434
- isLoading: isLoadingForRecord,
435
- error: errorForRecord,
436
- fileUrl,
437
- fileReference,
438
- fileReferences,
439
- fileCount,
440
- loadFileReference,
441
- loadFileUrl,
442
- loadFileReferences,
443
- loadFileCount,
444
- deleteFile,
445
- clearError
446
- } = useFileReferenceForRecord(supabase, table_name, record_id, organisation_id);
447
-
448
- // Use useFileReference to get getFilesByCategory when category is provided
449
- const {
450
- getFilesByCategory,
451
- isLoading: isLoadingCategory,
452
- error: errorCategory
453
- } = useFileReference(supabase);
454
-
455
- // Consolidated state for category mode
456
- const [categoryFileReferences, setCategoryFileReferences] = useState<FileReference[]>([]);
457
- const categoryFileReference = categoryFileReferences[0] || null;
458
- const categoryFileUrlHook = useFileUrl(categoryFileReference, {
459
- supabase,
460
- organisation_id,
461
- autoLoad: !!categoryFileReference
462
- });
463
-
464
- // Consolidated state for displayOnly mode
465
- const [displayOnlyFileReference, setDisplayOnlyFileReference] = useState<FileReference | null>(null);
466
- const displayOnlyFileUrlHook = useFileUrl(displayOnlyFileReference, {
467
- supabase,
468
- organisation_id,
469
- autoLoad: !!displayOnlyFileReference
470
- });
471
-
472
- // Load files by category when category is provided
473
- useEffect(() => {
474
- if (category) {
475
- const loadCategoryFiles = async () => {
476
- try {
477
- const files = await getFilesByCategory(table_name, record_id, category, organisation_id);
478
- setCategoryFileReferences(files);
479
-
480
- // URL generation is handled by useFileUrl hook
481
- if (files.length === 0) {
482
- setCategoryFileReferences([]);
483
- }
484
- } catch (err) {
485
- console.error('[FileDisplayBackwardsCompat] Error loading files by category:', err);
486
- }
487
- };
488
-
489
- loadCategoryFiles();
490
- } else {
491
- // Clear category files when no category
492
- setCategoryFileReferences([]);
493
- }
494
- }, [category, table_name, record_id, organisation_id, supabase, getFilesByCategory]);
495
-
496
- // Load file data on mount (when no category)
497
- useEffect(() => {
498
- if (!category) {
499
- loadFileCount();
500
- loadFileReferences();
501
- }
502
- }, [loadFileCount, loadFileReferences, category]);
503
-
504
- // Load file URL when file reference is available (when no category)
505
- useEffect(() => {
506
- if (!category && fileReference) {
507
- loadFileUrl();
508
- }
509
- }, [category, fileReference, loadFileUrl]);
510
-
511
- // Handle displayOnly mode: select first file (prefer images) from all files
512
- useEffect(() => {
513
- if (displayOnly && !category && fileReferences.length > 0) {
514
- // Prefer image files
515
- const imageFiles = fileReferences.filter(f =>
516
- f.file_metadata.fileType?.startsWith('image/')
517
- );
518
- const targetFile = imageFiles.length > 0 ? imageFiles[0] : fileReferences[0];
519
- setDisplayOnlyFileReference(targetFile);
520
- // URL generation is handled by useFileUrl hook
521
- } else {
522
- // Clear displayOnly files when not in displayOnly mode or when category is specified
523
- setDisplayOnlyFileReference(null);
524
- }
525
- }, [displayOnly, category, fileReferences]);
526
-
527
- const handleDelete = async () => {
528
- await deleteFile(true);
529
- };
530
-
531
- // Determine final file reference and URL based on mode
532
- // Priority: category > displayOnly > default (all files)
533
- let finalFileReference: FileReference | null = null;
534
- let finalFileUrl: string | null = null;
535
- let finalFileReferences: FileReference[] = [];
536
- let finalFileCount = 0;
537
- let finalIsLoading = false;
538
- let finalError: string | Error | null = null;
539
-
540
- if (category) {
541
- // Category mode: use category-filtered files
542
- finalFileReference = categoryFileReference;
543
- finalFileUrl = categoryFileUrlHook.url;
544
- finalFileReferences = categoryFileReferences;
545
- finalFileCount = categoryFileReferences.length;
546
- finalIsLoading = isLoadingCategory || categoryFileUrlHook.isLoading;
547
- finalError = errorCategory || categoryFileUrlHook.error;
548
- } else if (displayOnly) {
549
- // DisplayOnly mode: use first file from all files (prefers images)
550
- finalFileReference = displayOnlyFileReference;
551
- finalFileUrl = displayOnlyFileUrlHook.url;
552
- finalFileReferences = displayOnlyFileReference ? [displayOnlyFileReference] : [];
553
- finalFileCount = displayOnlyFileReference ? 1 : 0;
554
- finalIsLoading = isLoadingForRecord || displayOnlyFileUrlHook.isLoading;
555
- finalError = errorForRecord || displayOnlyFileUrlHook.error;
556
- } else {
557
- // Default mode: show all files
558
- finalFileReference = fileReference;
559
- finalFileUrl = fileUrl;
560
- finalFileReferences = fileReferences;
561
- finalFileCount = fileCount;
562
- finalIsLoading = isLoadingForRecord;
563
- finalError = errorForRecord;
564
- }
565
-
566
- return (
567
- <FileDisplayContent
568
- isLoading={finalIsLoading}
569
- error={finalError}
570
- fileUrl={finalFileUrl}
571
- fileReference={finalFileReference}
572
- fileReferences={finalFileReferences}
573
- fileUrls={new Map()} // Will be populated by FileDisplayContent's useEffect
574
- fileCount={finalFileCount}
575
- category={category}
576
- displayOnly={displayOnly}
577
- showDelete={showDelete}
578
- className={className}
579
- children={children}
580
- onDelete={handleDelete}
581
- clearError={clearError}
582
- supabase={supabase}
583
- organisation_id={organisation_id}
584
- loadingUrls={new Set()}
585
- loadingComponent={loadingComponent}
586
- errorComponent={errorComponent}
587
- />
588
- );
589
- }
590
-
591
482
  /**
592
483
  * Internal component for public page context
593
484
  * Uses PublicPageContext to get Supabase client
@@ -598,13 +489,16 @@ function FileDisplayPublic({
598
489
  organisation_id,
599
490
  category,
600
491
  displayOnly = false,
601
- showUpload = false,
602
492
  showDelete = false,
603
493
  className = '',
604
494
  children,
605
495
  loadingComponent,
606
- errorComponent
607
- }: Omit<FileDisplayProps, 'supabase'>) {
496
+ errorComponent,
497
+ showFallback,
498
+ generateFallbackText,
499
+ fallbackText,
500
+ fallbackSize
501
+ }: FileDisplayProps) {
608
502
  const publicPageContext = useContext(PublicPageContext);
609
503
  const supabase = publicPageContext?.supabase ?? null;
610
504
 
@@ -633,9 +527,9 @@ function FileDisplayPublic({
633
527
  { supabase }
634
528
  );
635
529
 
636
- // Note: Public context doesn't support delete operations
530
+ // Public context doesn't support delete operations
637
531
  const handleDelete = async () => {
638
- console.warn('[FileDisplay] Delete operation not supported in public context');
532
+ // Delete operations are not available in public context for security reasons
639
533
  };
640
534
 
641
535
  // Handle displayOnly mode: select first file (prefer images) from all files
@@ -673,10 +567,13 @@ function FileDisplayPublic({
673
567
  className={className}
674
568
  children={children}
675
569
  onDelete={showDelete ? handleDelete : undefined}
676
- supabase={supabase}
677
570
  organisation_id={organisation_id}
678
571
  loadingComponent={loadingComponent}
679
572
  errorComponent={errorComponent}
573
+ showFallback={showFallback}
574
+ generateFallbackText={generateFallbackText}
575
+ fallbackText={fallbackText}
576
+ fallbackSize={fallbackSize}
680
577
  />
681
578
  );
682
579
  }
@@ -691,13 +588,16 @@ function FileDisplayAuthenticated({
691
588
  organisation_id,
692
589
  category,
693
590
  displayOnly = false,
694
- showUpload = false,
695
591
  showDelete = false,
696
592
  className = '',
697
593
  children,
698
594
  loadingComponent,
699
- errorComponent
700
- }: Omit<FileDisplayProps, 'supabase'>) {
595
+ errorComponent,
596
+ showFallback,
597
+ generateFallbackText,
598
+ fallbackText,
599
+ fallbackSize
600
+ }: FileDisplayProps) {
701
601
  const { supabase } = useUnifiedAuth();
702
602
 
703
603
  if (!supabase) {
@@ -756,10 +656,9 @@ function FileDisplayAuthenticated({
756
656
  }
757
657
  }, [displayOnly, category, fileReferences, fileUrls]);
758
658
 
759
- // Note: Delete would need to be implemented via FileReferenceService
760
- // For now, we'll show a warning
659
+ // Delete operation - implementation pending
761
660
  const handleDelete = async () => {
762
- console.warn('[FileDisplay] Delete operation needs to be implemented via FileReferenceService');
661
+ // TODO: Implement delete via FileReferenceService when delete functionality is needed
763
662
  };
764
663
 
765
664
  // Determine final file reference and URL based on mode
@@ -798,10 +697,13 @@ function FileDisplayAuthenticated({
798
697
  className={className}
799
698
  children={children}
800
699
  onDelete={showDelete ? handleDelete : undefined}
801
- supabase={supabase}
802
700
  organisation_id={organisation_id}
803
701
  loadingComponent={loadingComponent}
804
702
  errorComponent={errorComponent}
703
+ showFallback={showFallback}
704
+ generateFallbackText={generateFallbackText}
705
+ fallbackText={fallbackText}
706
+ fallbackSize={fallbackSize}
805
707
  />
806
708
  );
807
709
  }
@@ -812,8 +714,7 @@ function FileDisplayAuthenticated({
812
714
  * This component is context-aware and automatically detects whether it's being used
813
715
  * in a public or authenticated context. It fetches and displays files from storage.
814
716
  *
815
- * When `supabase` prop is provided, it uses the explicit client (backwards compatible).
816
- * When `supabase` prop is not provided, it automatically detects context and uses:
717
+ * The component automatically detects context and uses:
817
718
  * - PublicPageProvider context for public pages
818
719
  * - UnifiedAuthProvider context for authenticated pages
819
720
  *
@@ -823,39 +724,21 @@ function FileDisplayAuthenticated({
823
724
  * @returns React element with file display
824
725
  */
825
726
  export function FileDisplay({
826
- supabase,
827
727
  table_name,
828
728
  record_id,
829
729
  organisation_id,
830
730
  category,
831
731
  displayOnly = false,
832
- showUpload = false,
833
732
  showDelete = false,
834
733
  className = '',
835
734
  children,
836
735
  loadingComponent,
837
- errorComponent
736
+ errorComponent,
737
+ showFallback,
738
+ generateFallbackText,
739
+ fallbackText,
740
+ fallbackSize
838
741
  }: FileDisplayProps) {
839
- // If supabase is explicitly provided, use backwards compatible mode
840
- if (supabase) {
841
- return (
842
- <FileDisplayBackwardsCompat
843
- supabase={supabase}
844
- table_name={table_name}
845
- record_id={record_id}
846
- organisation_id={organisation_id}
847
- category={category}
848
- displayOnly={displayOnly}
849
- showUpload={showUpload}
850
- showDelete={showDelete}
851
- className={className}
852
- children={children}
853
- loadingComponent={loadingComponent}
854
- errorComponent={errorComponent}
855
- />
856
- );
857
- }
858
-
859
742
  // Check which context we're in and route to the appropriate component
860
743
  const isPublicPage = useIsPublicPage();
861
744
 
@@ -868,17 +751,20 @@ export function FileDisplay({
868
751
  organisation_id={organisation_id}
869
752
  category={category}
870
753
  displayOnly={displayOnly}
871
- showUpload={showUpload}
872
754
  showDelete={showDelete}
873
755
  className={className}
874
756
  children={children}
875
757
  loadingComponent={loadingComponent}
876
758
  errorComponent={errorComponent}
759
+ showFallback={showFallback}
760
+ generateFallbackText={generateFallbackText}
761
+ fallbackText={fallbackText}
762
+ fallbackSize={fallbackSize}
877
763
  />
878
764
  );
879
765
  }
880
766
 
881
- // Otherwise, try to use the authenticated component
767
+ // Otherwise, use the authenticated component
882
768
  // It will show an error if not in UnifiedAuthProvider
883
769
  return (
884
770
  <FileDisplayAuthenticated
@@ -887,12 +773,15 @@ export function FileDisplay({
887
773
  organisation_id={organisation_id}
888
774
  category={category}
889
775
  displayOnly={displayOnly}
890
- showUpload={showUpload}
891
776
  showDelete={showDelete}
892
777
  className={className}
893
778
  children={children}
894
779
  loadingComponent={loadingComponent}
895
780
  errorComponent={errorComponent}
781
+ showFallback={showFallback}
782
+ generateFallbackText={generateFallbackText}
783
+ fallbackText={fallbackText}
784
+ fallbackSize={fallbackSize}
896
785
  />
897
786
  );
898
787
  }