@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,583 @@
1
+ import React from 'react';
2
+ import { renderHook, waitFor, act } from '@testing-library/react';
3
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
4
+ import { usePublicEvent, clearPublicEventCache, getPublicEventCacheStats } from '../public/usePublicEvent';
5
+ import { usePublicPageContext } from '../../components/PublicLayout/PublicPageProvider';
6
+
7
+ // Mock the PublicPageProvider
8
+ vi.mock('../../components/PublicLayout/PublicPageProvider', () => ({
9
+ usePublicPageContext: vi.fn(() => ({
10
+ environment: {
11
+ supabaseUrl: 'https://test.supabase.co',
12
+ supabaseKey: 'test-anon-key'
13
+ }
14
+ }))
15
+ }));
16
+
17
+ // Mock Supabase client
18
+ const mockSupabaseClient = {
19
+ rpc: vi.fn(),
20
+ from: vi.fn(() => ({
21
+ select: vi.fn(() => ({
22
+ eq: vi.fn(() => ({
23
+ eq: vi.fn(() => ({
24
+ not: vi.fn(() => ({
25
+ limit: vi.fn(() => ({
26
+ single: vi.fn()
27
+ }))
28
+ }))
29
+ }))
30
+ }))
31
+ }))
32
+ }))
33
+ };
34
+
35
+ // Helper to create table query mocks
36
+ const createTableQueryMock = () => ({
37
+ data: null,
38
+ error: null,
39
+ mockResolvedValueOnce: vi.fn(),
40
+ mockResolvedValue: vi.fn()
41
+ });
42
+
43
+ // Helper to create a complete table query chain
44
+ const createTableQueryChain = (finalResult: any) => {
45
+ const mockChain = {
46
+ select: vi.fn().mockReturnThis(),
47
+ eq: vi.fn().mockReturnThis(),
48
+ not: vi.fn().mockReturnThis(),
49
+ limit: vi.fn().mockReturnThis(),
50
+ single: vi.fn().mockResolvedValue(finalResult)
51
+ };
52
+ return mockChain;
53
+ };
54
+
55
+ // Mock createClient
56
+ vi.mock('@supabase/supabase-js', () => ({
57
+ createClient: vi.fn(() => mockSupabaseClient)
58
+ }));
59
+
60
+ // Mock environment variables
61
+ const originalEnv = import.meta.env;
62
+
63
+ describe('usePublicEvent', () => {
64
+ beforeEach(() => {
65
+ vi.clearAllMocks();
66
+ clearPublicEventCache();
67
+
68
+ // Reset environment
69
+ Object.defineProperty(import.meta, 'env', {
70
+ value: {
71
+ VITE_SUPABASE_URL: 'https://test.supabase.co',
72
+ VITE_SUPABASE_ANON_KEY: 'test-anon-key'
73
+ },
74
+ writable: true
75
+ });
76
+
77
+ // Mock window object
78
+ Object.defineProperty(window, 'location', {
79
+ value: { href: 'https://test.com' },
80
+ writable: true
81
+ });
82
+
83
+ // Re-establish mock implementations after clearAllMocks
84
+ mockSupabaseClient.rpc.mockImplementation(() =>
85
+ Promise.resolve({
86
+ data: null,
87
+ error: null
88
+ })
89
+ );
90
+
91
+ // Ensure the mock is properly configured
92
+ expect(mockSupabaseClient.rpc).toBeDefined();
93
+ });
94
+
95
+ afterEach(() => {
96
+ vi.clearAllMocks();
97
+ clearPublicEventCache();
98
+ Object.defineProperty(import.meta, 'env', {
99
+ value: originalEnv,
100
+ writable: true
101
+ });
102
+ });
103
+
104
+ describe('Basic Functionality', () => {
105
+ it('should initialize with loading state', () => {
106
+ const { result } = renderHook(() => usePublicEvent('test-event'));
107
+
108
+ expect(result.current.isLoading).toBe(true);
109
+ expect(result.current.event).toBe(null);
110
+ expect(result.current.error).toBe(null);
111
+ });
112
+
113
+ it('should fetch event data successfully via RPC', async () => {
114
+ const mockEventData = {
115
+ event_id: '123',
116
+ event_name: 'Test Event',
117
+ event_date: '2024-01-01',
118
+ event_venue: 'Test Venue',
119
+ event_participants: 100,
120
+ event_colours: { primary: '#000000' },
121
+ organisation_id: 'org-123',
122
+ event_days: 1,
123
+ event_typicalunit: 'km',
124
+ event_rounddown: false,
125
+ event_youthmultiplier: 1.0,
126
+ event_catering_email: 'test@example.com',
127
+ event_news: 'Test news',
128
+ event_billing: 'Test billing',
129
+ event_footer: 'Test footer',
130
+ event_email: 'event@example.com',
131
+ event_logo: null
132
+ };
133
+
134
+ mockSupabaseClient.rpc.mockResolvedValueOnce({
135
+ data: [mockEventData],
136
+ error: null
137
+ });
138
+
139
+ const { result } = renderHook(() => usePublicEvent('test-event'));
140
+
141
+ await waitFor(() => {
142
+ expect(result.current.isLoading).toBe(false);
143
+ }, { interval: 10 });
144
+
145
+ expect(result.current.event).toEqual({
146
+ id: '123',
147
+ event_id: '123',
148
+ event_name: 'Test Event',
149
+ event_code: 'test-event',
150
+ event_date: '2024-01-01',
151
+ event_venue: 'Test Venue',
152
+ event_participants: 100,
153
+ event_logo: null,
154
+ event_colours: { primary: '#000000' },
155
+ organisation_id: 'org-123',
156
+ is_visible: true,
157
+ created_at: expect.any(String),
158
+ updated_at: expect.any(String),
159
+ name: 'Test Event',
160
+ start_date: '2024-01-01'
161
+ });
162
+ expect(result.current.error).toBe(null);
163
+ });
164
+
165
+ it('should handle event not found', async () => {
166
+ mockSupabaseClient.rpc.mockResolvedValueOnce({
167
+ data: [],
168
+ error: null
169
+ });
170
+
171
+ const { result } = renderHook(() => usePublicEvent('nonexistent-event'));
172
+
173
+ await waitFor(() => {
174
+ expect(result.current.isLoading).toBe(false);
175
+ }, { interval: 10 });
176
+
177
+ expect(result.current.event).toBe(null);
178
+ expect(result.current.error).toEqual(new Error('Event not found'));
179
+ });
180
+
181
+ it('should handle invalid event code', async () => {
182
+ const { result } = renderHook(() => usePublicEvent(''));
183
+
184
+ await waitFor(() => {
185
+ expect(result.current.isLoading).toBe(false);
186
+ }, { interval: 10 });
187
+
188
+ expect(result.current.event).toBe(null);
189
+ expect(result.current.error).toBeInstanceOf(Error);
190
+ expect(result.current.error?.message).toContain('Invalid event code or Supabase client not available');
191
+ });
192
+ });
193
+
194
+ describe('Caching', () => {
195
+ it('should cache event data', async () => {
196
+ const mockEventData = {
197
+ event_id: '123',
198
+ event_name: 'Test Event',
199
+ event_date: '2024-01-01',
200
+ event_venue: 'Test Venue',
201
+ event_participants: 100,
202
+ event_colours: { primary: '#000000' },
203
+ organisation_id: 'org-123',
204
+ event_days: 1,
205
+ event_typicalunit: 'km',
206
+ event_rounddown: false,
207
+ event_youthmultiplier: 1.0,
208
+ event_catering_email: 'test@example.com',
209
+ event_news: 'Test news',
210
+ event_billing: 'Test billing',
211
+ event_footer: 'Test footer',
212
+ event_email: 'event@example.com'
213
+ };
214
+
215
+ mockSupabaseClient.rpc.mockResolvedValueOnce({
216
+ data: [mockEventData],
217
+ error: null
218
+ });
219
+
220
+ const { result, rerender } = renderHook(() => usePublicEvent('test-event'));
221
+
222
+ await waitFor(() => {
223
+ expect(result.current.isLoading).toBe(false);
224
+ }, { interval: 10 });
225
+
226
+ // Rerender with same event code - should use cache
227
+ rerender();
228
+
229
+ // Should not call RPC again
230
+ expect(mockSupabaseClient.rpc).toHaveBeenCalledTimes(1);
231
+ });
232
+
233
+ it('should respect cache TTL', async () => {
234
+ const mockEventData = {
235
+ event_id: '123',
236
+ event_name: 'Test Event',
237
+ event_date: '2024-01-01',
238
+ event_venue: 'Test Venue',
239
+ event_participants: 100,
240
+ event_colours: { primary: '#000000' },
241
+ organisation_id: 'org-123',
242
+ event_days: 1,
243
+ event_typicalunit: 'km',
244
+ event_rounddown: false,
245
+ event_youthmultiplier: 1.0,
246
+ event_catering_email: 'test@example.com',
247
+ event_news: 'Test news',
248
+ event_billing: 'Test billing',
249
+ event_footer: 'Test footer',
250
+ event_email: 'event@example.com'
251
+ };
252
+
253
+ mockSupabaseClient.rpc.mockResolvedValueOnce({
254
+ data: [mockEventData],
255
+ error: null
256
+ });
257
+
258
+ const { result } = renderHook(() => usePublicEvent('test-event', { cacheTtl: 100 }));
259
+
260
+ await waitFor(() => {
261
+ expect(result.current.isLoading).toBe(false);
262
+ }, { interval: 10 });
263
+
264
+ // Wait for cache to expire
265
+ await new Promise(resolve => setTimeout(resolve, 150));
266
+
267
+ // Rerender should fetch again
268
+ const { result: result2 } = renderHook(() => usePublicEvent('test-event', { cacheTtl: 100 }));
269
+
270
+ await waitFor(() => {
271
+ expect(result2.current.isLoading).toBe(false);
272
+ }, { interval: 10 });
273
+
274
+ expect(mockSupabaseClient.rpc).toHaveBeenCalledTimes(2);
275
+ });
276
+
277
+ it('should disable caching when requested', async () => {
278
+ const mockEventData = {
279
+ event_id: '123',
280
+ event_name: 'Test Event',
281
+ event_date: '2024-01-01',
282
+ event_venue: 'Test Venue',
283
+ event_participants: 100,
284
+ event_colours: { primary: '#000000' },
285
+ organisation_id: 'org-123',
286
+ event_days: 1,
287
+ event_typicalunit: 'km',
288
+ event_rounddown: false,
289
+ event_youthmultiplier: 1.0,
290
+ event_catering_email: 'test@example.com',
291
+ event_news: 'Test news',
292
+ event_billing: 'Test billing',
293
+ event_footer: 'Test footer',
294
+ event_email: 'event@example.com',
295
+ event_logo: null
296
+ };
297
+
298
+ // Clear any existing cache
299
+ const { clearPublicEventCache } = await import('../public/usePublicEvent');
300
+ clearPublicEventCache();
301
+
302
+ // Use mockImplementation to handle multiple calls
303
+ let callCount = 0;
304
+ mockSupabaseClient.rpc.mockImplementation(() => {
305
+ callCount++;
306
+ return Promise.resolve({
307
+ data: [mockEventData],
308
+ error: null
309
+ });
310
+ });
311
+
312
+ const { result, rerender } = renderHook(
313
+ ({ eventCode }) => usePublicEvent(eventCode, { enableCache: false }),
314
+ { initialProps: { eventCode: 'test-event' } }
315
+ );
316
+
317
+ await waitFor(() => {
318
+ expect(result.current.isLoading).toBe(false);
319
+ }, { interval: 10 });
320
+
321
+ // Change the event code to force a new fetch
322
+ rerender({ eventCode: 'test-event-2' });
323
+
324
+ await waitFor(() => {
325
+ expect(result.current.isLoading).toBe(false);
326
+ }, { interval: 10 });
327
+
328
+ expect(callCount).toBe(2);
329
+ });
330
+ });
331
+
332
+ describe('Error Handling', () => {
333
+ it('should handle RPC errors', async () => {
334
+ // Mock the RPC call to return an error object
335
+ const testError = { message: 'Database error', details: 'Test error details', hint: null, code: 'TEST_ERROR' };
336
+ mockSupabaseClient.rpc.mockImplementation(() =>
337
+ Promise.resolve({
338
+ data: null,
339
+ error: testError
340
+ })
341
+ );
342
+
343
+ const { result } = renderHook(() => usePublicEvent('test-event'));
344
+
345
+ await waitFor(() => {
346
+ expect(result.current.isLoading).toBe(false);
347
+ }, { interval: 10 });
348
+
349
+ expect(result.current.error).toBeInstanceOf(Error);
350
+ expect(result.current.error?.message).toBe('Database error');
351
+ expect(result.current.event).toBe(null);
352
+
353
+ // Verify the mock was called
354
+ expect(mockSupabaseClient.rpc).toHaveBeenCalledWith('get_public_event_by_code', {
355
+ event_code_param: 'test-event'
356
+ });
357
+ });
358
+
359
+ it('should handle missing Supabase client', async () => {
360
+ // Mock the PublicPageProvider to return null environment
361
+ const mockUsePublicPageContext = vi.mocked(usePublicPageContext);
362
+ mockUsePublicPageContext.mockReturnValueOnce({
363
+ environment: {
364
+ supabaseUrl: '',
365
+ supabaseKey: ''
366
+ }
367
+ });
368
+
369
+ // Mock createClient to return null when environment is empty
370
+ const { createClient } = await import('@supabase/supabase-js');
371
+ vi.mocked(createClient).mockReturnValueOnce(null as any);
372
+
373
+ const { result } = renderHook(() => usePublicEvent('test-event'));
374
+
375
+ await waitFor(() => {
376
+ expect(result.current.isLoading).toBe(false);
377
+ }, { interval: 10 });
378
+
379
+ expect(result.current.error).toBeInstanceOf(Error);
380
+ expect(result.current.error?.message).toContain('Invalid event code or Supabase client not available');
381
+ expect(result.current.event).toBe(null);
382
+ });
383
+ });
384
+
385
+ describe('Provider Context Integration', () => {
386
+ it('should use PublicPageContext when available', async () => {
387
+ const mockContext = {
388
+ environment: {
389
+ supabaseUrl: 'https://context.supabase.co',
390
+ supabaseKey: 'context-anon-key'
391
+ }
392
+ };
393
+
394
+ vi.mocked(usePublicPageContext).mockReturnValue(mockContext);
395
+
396
+ const mockEventData = {
397
+ event_id: '123',
398
+ event_name: 'Test Event',
399
+ event_date: '2024-01-01',
400
+ event_venue: 'Test Venue',
401
+ event_participants: 100,
402
+ event_colours: { primary: '#000000' },
403
+ organisation_id: 'org-123',
404
+ event_days: 1,
405
+ event_typicalunit: 'km',
406
+ event_rounddown: false,
407
+ event_youthmultiplier: 1.0,
408
+ event_catering_email: 'test@example.com',
409
+ event_news: 'Test news',
410
+ event_billing: 'Test billing',
411
+ event_footer: 'Test footer',
412
+ event_email: 'event@example.com',
413
+ event_logo: null
414
+ };
415
+
416
+ mockSupabaseClient.rpc.mockResolvedValueOnce({
417
+ data: [mockEventData],
418
+ error: null
419
+ });
420
+
421
+ const { result } = renderHook(() => usePublicEvent('test-event'));
422
+
423
+ await waitFor(() => {
424
+ expect(result.current.isLoading).toBe(false);
425
+ }, { interval: 10 });
426
+
427
+ expect(result.current.event).toBeTruthy();
428
+ });
429
+ });
430
+
431
+ describe('Refetch Functionality', () => {
432
+ it('should refetch data when refetch is called', async () => {
433
+ const mockEventData = {
434
+ event_id: '123',
435
+ event_name: 'Test Event',
436
+ event_date: '2024-01-01',
437
+ event_venue: 'Test Venue',
438
+ event_participants: 100,
439
+ event_colours: { primary: '#000000' },
440
+ organisation_id: 'org-123',
441
+ event_days: 1,
442
+ event_typicalunit: 'km',
443
+ event_rounddown: false,
444
+ event_youthmultiplier: 1.0,
445
+ event_catering_email: 'test@example.com',
446
+ event_news: 'Test news',
447
+ event_billing: 'Test billing',
448
+ event_footer: 'Test footer',
449
+ event_email: 'event@example.com'
450
+ };
451
+
452
+ mockSupabaseClient.rpc.mockResolvedValue({
453
+ data: [mockEventData],
454
+ error: null
455
+ });
456
+
457
+ const { result } = renderHook(() => usePublicEvent('test-event'));
458
+
459
+ await waitFor(() => {
460
+ expect(result.current.isLoading).toBe(false);
461
+ }, { interval: 10 });
462
+
463
+ // Call refetch
464
+ await act(async () => {
465
+ await result.current.refetch();
466
+ });
467
+
468
+ expect(mockSupabaseClient.rpc).toHaveBeenCalledTimes(2);
469
+ });
470
+
471
+ it('should clear cache when refetch is called', async () => {
472
+ const mockEventData = {
473
+ event_id: '123',
474
+ event_name: 'Test Event',
475
+ event_date: '2024-01-01',
476
+ event_venue: 'Test Venue',
477
+ event_participants: 100,
478
+ event_colours: { primary: '#000000' },
479
+ organisation_id: 'org-123',
480
+ event_days: 1,
481
+ event_typicalunit: 'km',
482
+ event_rounddown: false,
483
+ event_youthmultiplier: 1.0,
484
+ event_catering_email: 'test@example.com',
485
+ event_news: 'Test news',
486
+ event_billing: 'Test billing',
487
+ event_footer: 'Test footer',
488
+ event_email: 'event@example.com'
489
+ };
490
+
491
+ mockSupabaseClient.rpc.mockResolvedValue({
492
+ data: [mockEventData],
493
+ error: null
494
+ });
495
+
496
+ const { result } = renderHook(() => usePublicEvent('test-event'));
497
+
498
+ await waitFor(() => {
499
+ expect(result.current.isLoading).toBe(false);
500
+ }, { interval: 10 });
501
+
502
+ // Call refetch
503
+ await act(async () => {
504
+ await result.current.refetch();
505
+ });
506
+
507
+ // Should call RPC again (cache was cleared)
508
+ expect(mockSupabaseClient.rpc).toHaveBeenCalledTimes(2);
509
+ });
510
+ });
511
+
512
+ describe('Cache Management Utilities', () => {
513
+ it('should clear public event cache', () => {
514
+ // Add some data to cache
515
+ const stats = getPublicEventCacheStats();
516
+ expect(stats.size).toBe(0);
517
+
518
+ // Clear cache
519
+ clearPublicEventCache();
520
+
521
+ const statsAfter = getPublicEventCacheStats();
522
+ expect(statsAfter.size).toBe(0);
523
+ });
524
+
525
+ it('should get cache statistics', () => {
526
+ const stats = getPublicEventCacheStats();
527
+ expect(stats).toEqual({
528
+ size: 0,
529
+ keys: []
530
+ });
531
+ });
532
+ });
533
+
534
+ describe('Edge Cases', () => {
535
+ it('should handle null event data from RPC', async () => {
536
+ mockSupabaseClient.rpc.mockResolvedValueOnce({
537
+ data: null,
538
+ error: null
539
+ });
540
+
541
+ const { result } = renderHook(() => usePublicEvent('test-event'));
542
+
543
+ await waitFor(() => {
544
+ expect(result.current.isLoading).toBe(false);
545
+ }, { interval: 10 });
546
+
547
+ expect(result.current.event).toBe(null);
548
+ expect(result.current.error).toEqual(new Error('Event not found'));
549
+ });
550
+
551
+ it('should handle empty event data array from RPC', async () => {
552
+ mockSupabaseClient.rpc.mockResolvedValueOnce({
553
+ data: [],
554
+ error: null
555
+ });
556
+
557
+ const { result } = renderHook(() => usePublicEvent('test-event'));
558
+
559
+ await waitFor(() => {
560
+ expect(result.current.isLoading).toBe(false);
561
+ }, { interval: 10 });
562
+
563
+ expect(result.current.event).toBe(null);
564
+ expect(result.current.error).toEqual(new Error('Event not found'));
565
+ });
566
+
567
+ it('should handle undefined event data from RPC', async () => {
568
+ mockSupabaseClient.rpc.mockResolvedValueOnce({
569
+ data: [undefined],
570
+ error: null
571
+ });
572
+
573
+ const { result } = renderHook(() => usePublicEvent('test-event'));
574
+
575
+ await waitFor(() => {
576
+ expect(result.current.isLoading).toBe(false);
577
+ }, { interval: 10 });
578
+
579
+ expect(result.current.event).toBe(null);
580
+ expect(result.current.error).toEqual(new Error('Event not found'));
581
+ });
582
+ });
583
+ });
@@ -83,7 +83,7 @@ describe('usePublicRouteParams', () => {
83
83
 
84
84
  it('should reject invalid event code formats', () => {
85
85
  const invalidCodes = [
86
- 'ab', // too short
86
+ 'a', // too short (1 character)
87
87
  'a'.repeat(51), // too long
88
88
  'test@event', // invalid character
89
89
  'test event', // space
@@ -361,11 +361,14 @@ describe('usePublicRouteParams', () => {
361
361
  expect(generatePublicRoutePath('test-event')).toBe('/public/event/test-event/index');
362
362
  expect(generatePublicRoutePath('test-event', 'details')).toBe('/public/event/test-event/details');
363
363
  expect(generatePublicRoutePath('test-event', 'registration')).toBe('/public/event/test-event/registration');
364
+ // 2-character codes are now valid
365
+ expect(generatePublicRoutePath('GG')).toBe('/public/event/GG/index');
366
+ expect(generatePublicRoutePath('ab', 'page')).toBe('/public/event/ab/page');
364
367
  });
365
368
 
366
369
  it('should throw error for invalid event code', () => {
367
370
  expect(() => generatePublicRoutePath('invalid@code!')).toThrow('Invalid event code for route generation');
368
- expect(() => generatePublicRoutePath('ab')).toThrow('Invalid event code for route generation');
371
+ expect(() => generatePublicRoutePath('a')).toThrow('Invalid event code for route generation'); // too short (1 character)
369
372
  expect(() => generatePublicRoutePath('')).toThrow('Invalid event code for route generation');
370
373
  });
371
374
  });
@@ -379,11 +382,15 @@ describe('usePublicRouteParams', () => {
379
382
  expect(extractEventCodeFromPath('/public/event/test-event-123')).toBe('test-event-123');
380
383
  expect(extractEventCodeFromPath('/public/event/test_event')).toBe('test_event');
381
384
  expect(extractEventCodeFromPath('/public/event/test123')).toBe('test123');
385
+ // 2-character codes are now valid
386
+ expect(extractEventCodeFromPath('/public/event/ab')).toBe('ab');
387
+ expect(extractEventCodeFromPath('/public/event/GG')).toBe('GG');
388
+ expect(extractEventCodeFromPath('/public/event/A1')).toBe('A1');
382
389
  });
383
390
 
384
391
  it('should return null for invalid paths', () => {
385
392
  expect(extractEventCodeFromPath('/public/event/')).toBe(null);
386
- expect(extractEventCodeFromPath('/public/event/ab')).toBe(null); // too short
393
+ expect(extractEventCodeFromPath('/public/event/a')).toBe(null); // too short (1 character)
387
394
  expect(extractEventCodeFromPath('/public/event/test@event')).toBe(null); // invalid character
388
395
  expect(extractEventCodeFromPath('/public/event/test event')).toBe(null); // space
389
396
  expect(extractEventCodeFromPath('/public/event/')).toBe(null);
@@ -22,7 +22,7 @@ export { useFocusTrap } from './useFocusTrap';
22
22
  export { useKeyboardShortcuts } from './useKeyboardShortcuts';
23
23
  export { useIsMobile } from './useIsMobile';
24
24
 
25
- // === EVENT THEMING HOOK ===
25
+ // === EVENT HOOKS ===
26
26
  export { useEventTheme } from './useEventTheme';
27
27
 
28
28
  // === DATA & STATE HOOKS ===