@jmruthers/pace-core 0.5.120 → 0.5.123

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 (239) hide show
  1. package/dist/{AuthService-D4646R4b.d.ts → AuthService-DYuQPJj6.d.ts} +0 -9
  2. package/dist/{DataTable-DGZDJUYM.js → DataTable-WTS4IRF2.js} +7 -8
  3. package/dist/{PublicLoadingSpinner-DgDWTFqn.d.ts → PublicLoadingSpinner-CaoRbHvJ.d.ts} +30 -4
  4. package/dist/{UnifiedAuthProvider-UACKFATV.js → UnifiedAuthProvider-6C47WIML.js} +3 -4
  5. package/dist/{chunk-D6BOFXYR.js → chunk-35ZDPMBM.js} +3 -3
  6. package/dist/{chunk-CGURJ27Z.js → chunk-4MXVZVNS.js} +2 -2
  7. package/dist/{chunk-ZYJ6O5CA.js → chunk-C43QIDN3.js} +2 -2
  8. package/dist/{chunk-VKOCWWVY.js → chunk-CX5M4ZAG.js} +1 -6
  9. package/dist/{chunk-VKOCWWVY.js.map → chunk-CX5M4ZAG.js.map} +1 -1
  10. package/dist/{chunk-HFBOFZ3Z.js → chunk-DHMFMXFV.js} +258 -243
  11. package/dist/chunk-DHMFMXFV.js.map +1 -0
  12. package/dist/{chunk-RIEJGKD3.js → chunk-ESJTIADP.js} +15 -6
  13. package/dist/{chunk-RIEJGKD3.js.map → chunk-ESJTIADP.js.map} +1 -1
  14. package/dist/{chunk-SMJZMKYN.js → chunk-GEVIB2UB.js} +43 -10
  15. package/dist/chunk-GEVIB2UB.js.map +1 -0
  16. package/dist/{chunk-TDNI6ZWL.js → chunk-IJOZZOGT.js} +7 -7
  17. package/dist/chunk-IJOZZOGT.js.map +1 -0
  18. package/dist/{chunk-GZRXOUBE.js → chunk-M6DDYFUD.js} +2 -2
  19. package/dist/chunk-M6DDYFUD.js.map +1 -0
  20. package/dist/{chunk-B4GZ2BXO.js → chunk-NZGLXZGP.js} +3 -3
  21. package/dist/{chunk-NZ32EONV.js → chunk-QWNJCQXZ.js} +2 -2
  22. package/dist/{chunk-FKFHZUGF.js → chunk-XN6GWKMV.js} +43 -56
  23. package/dist/chunk-XN6GWKMV.js.map +1 -0
  24. package/dist/{chunk-BHWIUEYH.js → chunk-ZBLK676C.js} +1 -61
  25. package/dist/chunk-ZBLK676C.js.map +1 -0
  26. package/dist/{chunk-QPI2CCBA.js → chunk-ZPJMYGEP.js} +149 -96
  27. package/dist/chunk-ZPJMYGEP.js.map +1 -0
  28. package/dist/components.d.ts +1 -1
  29. package/dist/components.js +11 -11
  30. package/dist/{formatting-B1jSqgl-.d.ts → formatting-DFcCxUEk.d.ts} +1 -1
  31. package/dist/hooks.d.ts +1 -1
  32. package/dist/hooks.js +9 -8
  33. package/dist/hooks.js.map +1 -1
  34. package/dist/index.d.ts +6 -6
  35. package/dist/index.js +19 -17
  36. package/dist/index.js.map +1 -1
  37. package/dist/providers.d.ts +2 -2
  38. package/dist/providers.js +2 -3
  39. package/dist/rbac/index.js +7 -8
  40. package/dist/styles/index.d.ts +1 -1
  41. package/dist/styles/index.js +5 -3
  42. package/dist/theming/runtime.d.ts +73 -1
  43. package/dist/theming/runtime.js +5 -5
  44. package/dist/{usePublicRouteParams-BdF8bZgs.d.ts → usePublicRouteParams-Dyt1tzI9.d.ts} +60 -8
  45. package/dist/utils.d.ts +1 -1
  46. package/dist/utils.js +5 -5
  47. package/docs/api/classes/ColumnFactory.md +1 -1
  48. package/docs/api/classes/ErrorBoundary.md +1 -1
  49. package/docs/api/classes/InvalidScopeError.md +1 -1
  50. package/docs/api/classes/MissingUserContextError.md +1 -1
  51. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  52. package/docs/api/classes/PermissionDeniedError.md +1 -1
  53. package/docs/api/classes/PublicErrorBoundary.md +6 -6
  54. package/docs/api/classes/RBACAuditManager.md +1 -1
  55. package/docs/api/classes/RBACCache.md +1 -1
  56. package/docs/api/classes/RBACEngine.md +1 -1
  57. package/docs/api/classes/RBACError.md +1 -1
  58. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  59. package/docs/api/classes/SecureSupabaseClient.md +6 -6
  60. package/docs/api/classes/StorageUtils.md +1 -1
  61. package/docs/api/enums/FileCategory.md +1 -1
  62. package/docs/api/interfaces/AggregateConfig.md +1 -1
  63. package/docs/api/interfaces/ButtonProps.md +1 -1
  64. package/docs/api/interfaces/CardProps.md +1 -1
  65. package/docs/api/interfaces/ColorPalette.md +1 -1
  66. package/docs/api/interfaces/ColorShade.md +1 -1
  67. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  68. package/docs/api/interfaces/DataRecord.md +1 -1
  69. package/docs/api/interfaces/DataTableAction.md +1 -1
  70. package/docs/api/interfaces/DataTableColumn.md +1 -1
  71. package/docs/api/interfaces/DataTableProps.md +1 -1
  72. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  73. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  74. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  75. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  76. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  77. package/docs/api/interfaces/FileMetadata.md +1 -1
  78. package/docs/api/interfaces/FileReference.md +1 -1
  79. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  80. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  81. package/docs/api/interfaces/FileUploadProps.md +1 -1
  82. package/docs/api/interfaces/FooterProps.md +1 -1
  83. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  84. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  85. package/docs/api/interfaces/InputProps.md +1 -1
  86. package/docs/api/interfaces/LabelProps.md +1 -1
  87. package/docs/api/interfaces/LoginFormProps.md +1 -1
  88. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  89. package/docs/api/interfaces/NavigationContextType.md +1 -1
  90. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  91. package/docs/api/interfaces/NavigationItem.md +1 -1
  92. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  93. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  94. package/docs/api/interfaces/Organisation.md +1 -1
  95. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  96. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  97. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  98. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  99. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  100. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  101. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  102. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  103. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  104. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  105. package/docs/api/interfaces/PaletteData.md +1 -1
  106. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  107. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  108. package/docs/api/interfaces/PublicErrorBoundaryProps.md +7 -7
  109. package/docs/api/interfaces/PublicErrorBoundaryState.md +5 -5
  110. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +7 -7
  111. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  112. package/docs/api/interfaces/PublicPageHeaderProps.md +51 -12
  113. package/docs/api/interfaces/PublicPageLayoutProps.md +72 -12
  114. package/docs/api/interfaces/RBACConfig.md +1 -1
  115. package/docs/api/interfaces/RBACLogger.md +1 -1
  116. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  117. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  118. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  119. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  120. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  121. package/docs/api/interfaces/RouteConfig.md +1 -1
  122. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  123. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  124. package/docs/api/interfaces/StorageConfig.md +1 -1
  125. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  126. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  127. package/docs/api/interfaces/StorageListOptions.md +1 -1
  128. package/docs/api/interfaces/StorageListResult.md +1 -1
  129. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  130. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  131. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  132. package/docs/api/interfaces/StyleImport.md +1 -1
  133. package/docs/api/interfaces/SwitchProps.md +1 -1
  134. package/docs/api/interfaces/ToastActionElement.md +1 -1
  135. package/docs/api/interfaces/ToastProps.md +1 -1
  136. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  137. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  138. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  139. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  140. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  141. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  142. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  143. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  144. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  145. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  146. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  147. package/docs/api/interfaces/UserEventAccess.md +1 -1
  148. package/docs/api/interfaces/UserMenuProps.md +1 -1
  149. package/docs/api/interfaces/UserProfile.md +1 -1
  150. package/docs/api/modules.md +140 -30
  151. package/docs/best-practices/README.md +1 -1
  152. package/docs/implementation-guides/datatable-filtering.md +313 -0
  153. package/docs/implementation-guides/datatable-rbac-usage.md +317 -0
  154. package/docs/implementation-guides/hierarchical-datatable.md +850 -0
  155. package/docs/implementation-guides/large-datasets.md +281 -0
  156. package/docs/implementation-guides/performance.md +403 -0
  157. package/docs/implementation-guides/public-pages.md +4 -4
  158. package/docs/migration/quick-migration-guide.md +320 -0
  159. package/docs/rbac/quick-start.md +16 -16
  160. package/docs/troubleshooting/README.md +4 -4
  161. package/docs/troubleshooting/cake-page-permission-guard-issue-summary.md +1 -1
  162. package/docs/troubleshooting/debugging.md +1117 -0
  163. package/docs/troubleshooting/migration.md +918 -0
  164. package/examples/public-pages/CorrectPublicPageImplementation.tsx +30 -30
  165. package/examples/public-pages/PublicEventPage.tsx +41 -41
  166. package/examples/public-pages/PublicPageApp.tsx +33 -33
  167. package/examples/public-pages/PublicPageUsageExample.tsx +30 -30
  168. package/package.json +4 -4
  169. package/src/__tests__/hooks/usePermissions.test.ts +265 -0
  170. package/src/components/DataTable/DataTable.test.tsx +9 -38
  171. package/src/components/DataTable/DataTable.tsx +0 -7
  172. package/src/components/DataTable/components/DataTableCore.tsx +66 -136
  173. package/src/components/DataTable/components/DataTableModals.tsx +25 -22
  174. package/src/components/DataTable/components/EditableRow.tsx +118 -42
  175. package/src/components/DataTable/components/UnifiedTableBody.tsx +129 -76
  176. package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +33 -14
  177. package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +17 -5
  178. package/src/components/DataTable/utils/exportUtils.ts +3 -2
  179. package/src/components/DataTable/utils/flexibleImport.ts +27 -6
  180. package/src/components/Dialog/Dialog.tsx +1 -1
  181. package/src/components/Dialog/README.md +24 -24
  182. package/src/components/Dialog/examples/BasicHtmlTest.tsx +2 -2
  183. package/src/components/Dialog/examples/DebugHtmlExample.tsx +6 -6
  184. package/src/components/Dialog/examples/HtmlDialogExample.tsx +2 -2
  185. package/src/components/Dialog/examples/SimpleHtmlTest.tsx +3 -3
  186. package/src/components/Dialog/examples/__tests__/SimpleHtmlTest.test.tsx +4 -4
  187. package/src/components/PaceAppLayout/PaceAppLayout.tsx +12 -1
  188. package/src/components/PublicLayout/EventLogo.tsx +175 -0
  189. package/src/components/PublicLayout/PublicErrorBoundary.tsx +22 -18
  190. package/src/components/PublicLayout/PublicLoadingSpinner.tsx +22 -14
  191. package/src/components/PublicLayout/PublicPageHeader.tsx +133 -40
  192. package/src/components/PublicLayout/PublicPageLayout.tsx +75 -72
  193. package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +1 -1
  194. package/src/components/PublicLayout/__tests__/PublicLoadingSpinner.test.tsx +8 -8
  195. package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +23 -16
  196. package/src/components/PublicLayout/__tests__/PublicPageLayout.test.tsx +86 -14
  197. package/src/examples/CorrectPublicPageImplementation.tsx +30 -30
  198. package/src/examples/PublicEventPage.tsx +41 -41
  199. package/src/examples/PublicPageApp.tsx +33 -33
  200. package/src/examples/PublicPageUsageExample.tsx +30 -30
  201. package/src/hooks/__tests__/usePublicEvent.unit.test.ts +583 -0
  202. package/src/hooks/__tests__/usePublicRouteParams.unit.test.ts +10 -3
  203. package/src/hooks/index.ts +1 -1
  204. package/src/hooks/public/usePublicEventLogo.ts +285 -0
  205. package/src/hooks/public/usePublicRouteParams.ts +21 -4
  206. package/src/hooks/useEventTheme.test.ts +119 -43
  207. package/src/hooks/useEventTheme.ts +84 -55
  208. package/src/index.ts +3 -1
  209. package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +630 -0
  210. package/src/rbac/components/__tests__/NavigationProvider.test.tsx +667 -0
  211. package/src/rbac/components/__tests__/PagePermissionProvider.test.tsx +647 -0
  212. package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +496 -0
  213. package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +496 -0
  214. package/src/rbac/secureClient.ts +4 -2
  215. package/src/services/EventService.ts +0 -66
  216. package/src/services/__tests__/EventService.eventColours.test.ts +44 -40
  217. package/src/styles/index.ts +1 -1
  218. package/src/theming/__tests__/parseEventColours.test.ts +209 -0
  219. package/src/theming/parseEventColours.ts +123 -0
  220. package/src/theming/runtime.ts +3 -0
  221. package/src/types/__tests__/file-reference.test.ts +447 -0
  222. package/src/types/database.generated.ts +1515 -424
  223. package/src/utils/formatDate.test.ts +11 -11
  224. package/src/utils/formatting.ts +3 -2
  225. package/dist/chunk-BHWIUEYH.js.map +0 -1
  226. package/dist/chunk-FKFHZUGF.js.map +0 -1
  227. package/dist/chunk-GZRXOUBE.js.map +0 -1
  228. package/dist/chunk-HFBOFZ3Z.js.map +0 -1
  229. package/dist/chunk-QPI2CCBA.js.map +0 -1
  230. package/dist/chunk-SMJZMKYN.js.map +0 -1
  231. package/dist/chunk-TDNI6ZWL.js.map +0 -1
  232. package/src/styles/semantic.css +0 -24
  233. /package/dist/{DataTable-DGZDJUYM.js.map → DataTable-WTS4IRF2.js.map} +0 -0
  234. /package/dist/{UnifiedAuthProvider-UACKFATV.js.map → UnifiedAuthProvider-6C47WIML.js.map} +0 -0
  235. /package/dist/{chunk-D6BOFXYR.js.map → chunk-35ZDPMBM.js.map} +0 -0
  236. /package/dist/{chunk-CGURJ27Z.js.map → chunk-4MXVZVNS.js.map} +0 -0
  237. /package/dist/{chunk-ZYJ6O5CA.js.map → chunk-C43QIDN3.js.map} +0 -0
  238. /package/dist/{chunk-B4GZ2BXO.js.map → chunk-NZGLXZGP.js.map} +0 -0
  239. /package/dist/{chunk-NZ32EONV.js.map → chunk-QWNJCQXZ.js.map} +0 -0
@@ -0,0 +1,285 @@
1
+ /**
2
+ * @file Public Event Logo Hook
3
+ * @package @jmruthers/pace-core
4
+ * @module Hooks/Public
5
+ * @since 1.0.0
6
+ *
7
+ * A React hook for accessing public event logo URLs without authentication.
8
+ * Provides logo URLs with fallback handling for public pages.
9
+ *
10
+ * Features:
11
+ * - No authentication required
12
+ * - Automatic fallback to event initials
13
+ * - Caching for performance
14
+ * - Error handling and loading states
15
+ * - TypeScript support
16
+ * - Image validation
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * import { usePublicEventLogo } from '@jmruthers/pace-core';
21
+ *
22
+ * function EventHeader() {
23
+ * const { logoUrl, fallbackText, isLoading, error } = usePublicEventLogo(
24
+ * eventId,
25
+ * eventName,
26
+ * organisationId
27
+ * );
28
+ *
29
+ * if (isLoading) return <div>Loading logo...</div>;
30
+ * if (error) return <div>Error: {error.message}</div>;
31
+ *
32
+ * return (
33
+ * <div>
34
+ * {logoUrl ? (
35
+ * <img src={logoUrl} alt={`${eventName} logo`} />
36
+ * ) : (
37
+ * <div className="logo-fallback">{fallbackText}</div>
38
+ * )}
39
+ * </div>
40
+ * );
41
+ * }
42
+ * ```
43
+ *
44
+ * @accessibility
45
+ * - No direct accessibility concerns (hook)
46
+ * - Enables accessible logo display with proper alt text
47
+ * - Supports screen reader friendly fallbacks
48
+ *
49
+ * @security
50
+ * - Only returns public-safe logo URLs
51
+ * - Validates image existence before returning URL
52
+ * - No sensitive information exposed
53
+ * - Rate limiting applied at storage level
54
+ *
55
+ * @performance
56
+ * - Built-in caching with TTL
57
+ * - Image validation and optimization
58
+ * - Minimal re-renders with stable references
59
+ * - Lazy loading support
60
+ *
61
+ * @dependencies
62
+ * - React 18+ - Hooks and effects
63
+ * - @supabase/supabase-js - Storage integration
64
+ * - Event types - Type definitions
65
+ */
66
+
67
+ import { useState, useEffect, useCallback, useMemo } from 'react';
68
+ import type { SupabaseClient } from '@supabase/supabase-js';
69
+ import type { Database } from '../../types/database';
70
+
71
+ // Simple in-memory cache for public data
72
+ const publicDataCache = new Map<string, { data: any; timestamp: number; ttl: number }>();
73
+
74
+ export interface UsePublicEventLogoReturn {
75
+ /** The logo URL if available, null if not found or error */
76
+ logoUrl: string | null;
77
+ /** Fallback text (event initials) if no logo is available */
78
+ fallbackText: string;
79
+ /** Whether the logo is currently loading */
80
+ isLoading: boolean;
81
+ /** Any error that occurred during loading */
82
+ error: Error | null;
83
+ /** Function to manually refetch the logo */
84
+ refetch: () => Promise<void>;
85
+ }
86
+
87
+ export interface UsePublicEventLogoOptions {
88
+ /** Cache TTL in milliseconds (default: 30 minutes) */
89
+ cacheTtl?: number;
90
+ /** Whether to enable caching (default: true) */
91
+ enableCache?: boolean;
92
+ /** Whether to validate image existence (default: true) */
93
+ validateImage?: boolean;
94
+ /** Custom fallback text generator */
95
+ generateFallbackText?: (eventName: string) => string;
96
+ /** Supabase client instance (required) */
97
+ supabase: SupabaseClient<Database>;
98
+ }
99
+
100
+ /**
101
+ * Generate fallback text from event name (first letter of each word)
102
+ */
103
+ function defaultGenerateFallbackText(eventName: string): string {
104
+ if (!eventName) return 'EV';
105
+
106
+ return eventName
107
+ .split(' ')
108
+ .map(word => word.charAt(0).toUpperCase())
109
+ .join('')
110
+ .substring(0, 3); // Max 3 characters
111
+ }
112
+
113
+ /**
114
+ * Hook for accessing public event logo URLs
115
+ *
116
+ * This hook provides access to event logo URLs without requiring
117
+ * authentication. It includes fallback handling and image validation.
118
+ *
119
+ * @param eventId - The event ID to fetch logo for
120
+ * @param eventName - The event name for fallback text generation
121
+ * @param organisationId - The organisation ID for storage path
122
+ * @param options - Configuration options for caching and behavior
123
+ * @returns Object containing logo URL, fallback text, loading state, error, and refetch function
124
+ */
125
+ export function usePublicEventLogo(
126
+ eventId: string | undefined,
127
+ eventName: string | undefined,
128
+ organisationId: string | undefined,
129
+ options: UsePublicEventLogoOptions
130
+ ): UsePublicEventLogoReturn {
131
+ const {
132
+ cacheTtl = 30 * 60 * 1000, // 30 minutes
133
+ enableCache = true,
134
+ validateImage = true,
135
+ generateFallbackText = defaultGenerateFallbackText,
136
+ supabase
137
+ } = options;
138
+
139
+ const [logoUrl, setLogoUrl] = useState<string | null>(null);
140
+ const [isLoading, setIsLoading] = useState<boolean>(false);
141
+ const [error, setError] = useState<Error | null>(null);
142
+
143
+ // Generate fallback text
144
+ const fallbackText = useMemo(() => {
145
+ return eventName ? generateFallbackText(eventName) : 'EV';
146
+ }, [eventName, generateFallbackText]);
147
+
148
+ const fetchLogo = useCallback(async (): Promise<void> => {
149
+ if (!eventId || !organisationId || !supabase) {
150
+ setLogoUrl(null);
151
+ setIsLoading(false);
152
+ return;
153
+ }
154
+
155
+ // Validate UUID format for organisationId to prevent database errors
156
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
157
+ if (!uuidRegex.test(organisationId)) {
158
+ console.warn('[usePublicEventLogo] Invalid organisationId format (not a valid UUID):', organisationId);
159
+ // Don't return early - let the database handle the validation
160
+ // This allows for more graceful error handling
161
+ }
162
+
163
+ // Check cache first
164
+ const cacheKey = `public_logo_${eventId}_${organisationId}`;
165
+ if (enableCache) {
166
+ const cached = publicDataCache.get(cacheKey);
167
+ if (cached && Date.now() - cached.timestamp < cached.ttl) {
168
+ setLogoUrl(cached.data);
169
+ setIsLoading(false);
170
+ setError(null);
171
+ return;
172
+ }
173
+ }
174
+
175
+ try {
176
+ setIsLoading(true);
177
+ setError(null);
178
+
179
+ // Call the public logo RPC function
180
+ const { data, error: rpcError } = await (supabase as any).rpc('get_public_event_logo', {
181
+ event_id_param: eventId,
182
+ organisation_id_param: organisationId
183
+ });
184
+
185
+ if (rpcError) {
186
+ throw new Error(rpcError.message || 'Failed to fetch logo');
187
+ }
188
+
189
+ if (!data || data.length === 0 || !data[0] || !data[0].logo_url) {
190
+ setLogoUrl(null);
191
+ return;
192
+ }
193
+
194
+ const logoUrl = data[0].logo_url;
195
+
196
+ // Validate image existence if requested
197
+ if (validateImage) {
198
+ try {
199
+ const response = await fetch(logoUrl, { method: 'HEAD' });
200
+ if (!response.ok) {
201
+ console.warn('[usePublicEventLogo] Logo URL not accessible:', logoUrl);
202
+ setLogoUrl(null);
203
+ return;
204
+ }
205
+ } catch (fetchError) {
206
+ console.warn('[usePublicEventLogo] Error validating logo URL:', fetchError);
207
+ setLogoUrl(null);
208
+ return;
209
+ }
210
+ }
211
+
212
+ setLogoUrl(logoUrl);
213
+
214
+ // Cache the result
215
+ if (enableCache) {
216
+ publicDataCache.set(cacheKey, {
217
+ data: logoUrl,
218
+ timestamp: Date.now(),
219
+ ttl: cacheTtl
220
+ });
221
+ }
222
+
223
+ } catch (err) {
224
+ console.error('[usePublicEventLogo] Error fetching logo:', err);
225
+ const error = err instanceof Error ? err : new Error('Unknown error occurred');
226
+ setError(error);
227
+ setLogoUrl(null);
228
+ } finally {
229
+ setIsLoading(false);
230
+ }
231
+ }, [eventId, organisationId, supabase, cacheTtl, enableCache, validateImage]);
232
+
233
+ // Fetch logo when parameters change
234
+ useEffect(() => {
235
+ if (eventId && organisationId) {
236
+ fetchLogo();
237
+ } else {
238
+ setLogoUrl(null);
239
+ setIsLoading(false);
240
+ setError(null);
241
+ }
242
+ }, [fetchLogo, eventId, organisationId]);
243
+
244
+ const refetch = useCallback(async (): Promise<void> => {
245
+ if (!eventId || !organisationId) return;
246
+
247
+ // Clear cache for this logo
248
+ if (enableCache) {
249
+ const cacheKey = `public_logo_${eventId}_${organisationId}`;
250
+ publicDataCache.delete(cacheKey);
251
+ }
252
+ await fetchLogo();
253
+ }, [fetchLogo, eventId, organisationId, enableCache]);
254
+
255
+ return {
256
+ logoUrl,
257
+ fallbackText,
258
+ isLoading,
259
+ error,
260
+ refetch
261
+ };
262
+ }
263
+
264
+ /**
265
+ * Clear all cached public logo data
266
+ * Useful for testing or when you need to force refresh all data
267
+ */
268
+ export function clearPublicLogoCache(): void {
269
+ for (const [key] of publicDataCache) {
270
+ if (key.startsWith('public_logo_')) {
271
+ publicDataCache.delete(key);
272
+ }
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Get cache statistics for debugging
278
+ */
279
+ export function getPublicLogoCacheStats(): { size: number; keys: string[] } {
280
+ const keys = Array.from(publicDataCache.keys()).filter(key => key.startsWith('public_logo_'));
281
+ return {
282
+ size: keys.length,
283
+ keys
284
+ };
285
+ }
@@ -88,12 +88,20 @@ interface UsePublicRouteParamsOptions {
88
88
  /**
89
89
  * Validate event code format
90
90
  * Event codes should be alphanumeric with optional hyphens/underscores in the middle
91
+ * Supports 2-50 characters
91
92
  */
92
93
  function validateEventCodeFormat(eventCode: string): boolean {
93
94
  if (!eventCode || typeof eventCode !== 'string') return false;
94
95
 
95
- // Allow alphanumeric characters, hyphens, and underscores
96
- // Length between 3 and 50 characters
96
+ // Length check: 2-50 characters
97
+ if (eventCode.length < 2 || eventCode.length > 50) return false;
98
+
99
+ // For 2-character codes: both must be alphanumeric
100
+ if (eventCode.length === 2) {
101
+ return /^[a-zA-Z0-9]{2}$/.test(eventCode);
102
+ }
103
+
104
+ // For 3+ character codes: alphanumeric at start and end, hyphens/underscores allowed in middle
97
105
  // Must not start or end with hyphen/underscore
98
106
  const eventCodeRegex = /^[a-zA-Z0-9][a-zA-Z0-9_-]{1,48}[a-zA-Z0-9]$/;
99
107
  const matchesFormat = eventCodeRegex.test(eventCode);
@@ -262,8 +270,17 @@ export function generatePublicRoutePath(
262
270
 
263
271
  /**
264
272
  * Utility function to extract event code from a public route path
273
+ * Supports 2-50 character event codes
265
274
  */
266
275
  export function extractEventCodeFromPath(path: string): string | null {
267
- const match = path.match(/^\/public\/event\/([a-zA-Z0-9_-]{3,50})(?:\/.*)?$/);
268
- return match ? match[1] : null;
276
+ const match = path.match(/^\/public\/event\/([a-zA-Z0-9_-]{2,50})(?:\/.*)?$/);
277
+ if (!match) return null;
278
+
279
+ const eventCode = match[1];
280
+ // Validate the extracted code using the same validation function
281
+ if (!validateEventCodeFormat(eventCode)) {
282
+ return null;
283
+ }
284
+
285
+ return eventCode;
269
286
  }
@@ -139,15 +139,15 @@ describe('useEventTheme', () => {
139
139
 
140
140
  renderHook(() => useEventTheme());
141
141
 
142
+ // parseAndNormalizeEventColours fills all shades, so empty objects become objects with all shades
142
143
  expect(mockClearPalette).not.toHaveBeenCalled();
143
- expect(mockApplyPalette).toHaveBeenCalledWith({
144
- main: {
145
- '50': '#f0f9ff',
146
- '100': '#e0f2fe'
147
- },
148
- sec: {},
149
- acc: {}
150
- });
144
+ expect(mockApplyPalette).toHaveBeenCalled();
145
+ const callArgs = mockApplyPalette.mock.calls[0][0];
146
+ expect(callArgs.main['50']).toBe('#f0f9ff');
147
+ expect(callArgs.main['100']).toBe('#e0f2fe');
148
+ // sec and acc will have all shades filled (possibly undefined)
149
+ expect(callArgs.sec).toBeDefined();
150
+ expect(callArgs.acc).toBeDefined();
151
151
  });
152
152
 
153
153
  it('applies the palette with secondary colours', () => {
@@ -170,14 +170,14 @@ describe('useEventTheme', () => {
170
170
 
171
171
  renderHook(() => useEventTheme());
172
172
 
173
- expect(mockApplyPalette).toHaveBeenCalledWith({
174
- main: {},
175
- sec: {
176
- '500': '#3b82f6',
177
- '600': '#2563eb'
178
- },
179
- acc: {}
180
- });
173
+ // parseAndNormalizeEventColours fills all shades, so empty objects become objects with all shades
174
+ expect(mockApplyPalette).toHaveBeenCalled();
175
+ const callArgs = mockApplyPalette.mock.calls[0][0];
176
+ expect(callArgs.sec['500']).toBe('#3b82f6');
177
+ expect(callArgs.sec['600']).toBe('#2563eb');
178
+ // main and acc will have all shades filled (possibly undefined)
179
+ expect(callArgs.main).toBeDefined();
180
+ expect(callArgs.acc).toBeDefined();
181
181
  });
182
182
 
183
183
  it('applies the palette with accent colours', () => {
@@ -200,14 +200,14 @@ describe('useEventTheme', () => {
200
200
 
201
201
  renderHook(() => useEventTheme());
202
202
 
203
- expect(mockApplyPalette).toHaveBeenCalledWith({
204
- main: {},
205
- sec: {},
206
- acc: {
207
- '500': '#ef4444',
208
- '600': '#dc2626'
209
- }
210
- });
203
+ // parseAndNormalizeEventColours fills all shades, so empty objects become objects with all shades
204
+ expect(mockApplyPalette).toHaveBeenCalled();
205
+ const callArgs = mockApplyPalette.mock.calls[0][0];
206
+ expect(callArgs.acc['500']).toBe('#ef4444');
207
+ expect(callArgs.acc['600']).toBe('#dc2626');
208
+ // main and sec will have all shades filled (possibly undefined)
209
+ expect(callArgs.main).toBeDefined();
210
+ expect(callArgs.sec).toBeDefined();
211
211
  });
212
212
 
213
213
  it('applies the full palette with all colors', () => {
@@ -236,20 +236,19 @@ describe('useEventTheme', () => {
236
236
 
237
237
  renderHook(() => useEventTheme());
238
238
 
239
- expect(mockApplyPalette).toHaveBeenCalledWith({
240
- main: {
241
- '500': '#0ea5e9',
242
- '600': '#0284c7'
243
- },
244
- sec: {
245
- '500': '#8b5cf6',
246
- '600': '#7c3aed'
247
- },
248
- acc: {
249
- '500': '#f59e0b',
250
- '600': '#d97706'
251
- }
252
- });
239
+ // parseAndNormalizeEventColours fills all shades, so the palette will have all shades filled
240
+ expect(mockApplyPalette).toHaveBeenCalled();
241
+ const callArgs = mockApplyPalette.mock.calls[0][0];
242
+ expect(callArgs.main['500']).toBe('#0ea5e9');
243
+ expect(callArgs.main['600']).toBe('#0284c7');
244
+ expect(callArgs.sec['500']).toBe('#8b5cf6');
245
+ expect(callArgs.sec['600']).toBe('#7c3aed');
246
+ expect(callArgs.acc['500']).toBe('#f59e0b');
247
+ expect(callArgs.acc['600']).toBe('#d97706');
248
+ // All palettes will have all shades (50-950) filled
249
+ expect(callArgs.main).toBeDefined();
250
+ expect(callArgs.sec).toBeDefined();
251
+ expect(callArgs.acc).toBeDefined();
253
252
  });
254
253
  });
255
254
 
@@ -304,11 +303,12 @@ describe('useEventTheme', () => {
304
303
 
305
304
  const { rerender } = renderHook(() => useEventTheme());
306
305
 
307
- expect(mockApplyPalette).toHaveBeenCalledWith({
308
- main: { '500': '#0ea5e9' },
309
- sec: {},
310
- acc: {}
311
- });
306
+ // parseAndNormalizeEventColours fills all shades, so empty objects become objects with all shades
307
+ expect(mockApplyPalette).toHaveBeenCalled();
308
+ const firstCallArgs = mockApplyPalette.mock.calls[0][0];
309
+ expect(firstCallArgs.main['500']).toBe('#0ea5e9');
310
+ expect(firstCallArgs.sec).toBeDefined();
311
+ expect(firstCallArgs.acc).toBeDefined();
312
312
 
313
313
  mockUseEvents.mockReturnValue({
314
314
  selectedEvent: {
@@ -357,5 +357,81 @@ describe('useEventTheme', () => {
357
357
  expect(mockClearPalette).toHaveBeenCalled();
358
358
  });
359
359
  });
360
+
361
+ describe('Public page mode (with event prop)', () => {
362
+ it('uses event prop directly when provided', () => {
363
+ const event = {
364
+ id: 'event-1',
365
+ event_id: 'event-1',
366
+ event_name: 'Public Event',
367
+ event_colours: {
368
+ main: { '500': { L: 0.5, C: 0.2, H: 0 } },
369
+ sec: {},
370
+ acc: {}
371
+ },
372
+ organisation_id: 'org1',
373
+ created_at: new Date().toISOString(),
374
+ updated_at: new Date().toISOString()
375
+ };
376
+
377
+ renderHook(() => useEventTheme(event));
378
+
379
+ // Should not call useEvents when event prop is provided
380
+ expect(mockUseEvents).not.toHaveBeenCalled();
381
+ expect(mockApplyPalette).toHaveBeenCalled();
382
+ });
383
+
384
+ it('clears palette when event prop is null', () => {
385
+ renderHook(() => useEventTheme(null));
386
+
387
+ expect(mockClearPalette).toHaveBeenCalled();
388
+ });
389
+
390
+ it('handles EventProvider not available gracefully when event prop provided', () => {
391
+ // Mock useEvents to throw (simulating no EventProvider)
392
+ mockUseEvents.mockImplementation(() => {
393
+ throw new Error('useEvents must be used within EventServiceProvider');
394
+ });
395
+
396
+ const event = {
397
+ id: 'event-1',
398
+ event_id: 'event-1',
399
+ event_name: 'Public Event',
400
+ event_colours: {
401
+ main: { '500': { L: 0.5, C: 0.2, H: 0 } },
402
+ sec: {},
403
+ acc: {}
404
+ },
405
+ organisation_id: 'org1',
406
+ created_at: new Date().toISOString(),
407
+ updated_at: new Date().toISOString()
408
+ };
409
+
410
+ // Should not throw and should use event prop
411
+ expect(() => renderHook(() => useEventTheme(event))).not.toThrow();
412
+ expect(mockApplyPalette).toHaveBeenCalled();
413
+ });
414
+
415
+ it('falls back to EventProvider when event prop is undefined', () => {
416
+ mockUseEvents.mockReturnValue({
417
+ selectedEvent: {
418
+ id: 'event-1',
419
+ event_name: 'Provider Event',
420
+ event_colours: {
421
+ main: { '500': '#0ea5e9' },
422
+ sec: {},
423
+ acc: {}
424
+ }
425
+ },
426
+ events: [],
427
+ isLoading: false,
428
+ } as any);
429
+
430
+ renderHook(() => useEventTheme(undefined));
431
+
432
+ expect(mockUseEvents).toHaveBeenCalled();
433
+ expect(mockApplyPalette).toHaveBeenCalled();
434
+ });
435
+ });
360
436
  });
361
437