@jmruthers/pace-core 0.5.121 → 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.
- package/dist/{AuthService-D4646R4b.d.ts → AuthService-DYuQPJj6.d.ts} +0 -9
- package/dist/{DataTable-DGZDJUYM.js → DataTable-WTS4IRF2.js} +7 -8
- package/dist/{PublicLoadingSpinner-DgDWTFqn.d.ts → PublicLoadingSpinner-CaoRbHvJ.d.ts} +30 -4
- package/dist/{UnifiedAuthProvider-UACKFATV.js → UnifiedAuthProvider-6C47WIML.js} +3 -4
- package/dist/{chunk-D6BOFXYR.js → chunk-35ZDPMBM.js} +3 -3
- package/dist/{chunk-CGURJ27Z.js → chunk-4MXVZVNS.js} +2 -2
- package/dist/{chunk-ZYJ6O5CA.js → chunk-C43QIDN3.js} +2 -2
- package/dist/{chunk-VKOCWWVY.js → chunk-CX5M4ZAG.js} +1 -6
- package/dist/{chunk-VKOCWWVY.js 3.map → chunk-CX5M4ZAG.js.map} +1 -1
- package/dist/{chunk-HFBOFZ3Z.js → chunk-DHMFMXFV.js} +258 -243
- package/dist/chunk-DHMFMXFV.js.map +1 -0
- package/dist/{chunk-RIEJGKD3.js → chunk-ESJTIADP.js} +15 -6
- package/dist/{chunk-RIEJGKD3.js.map → chunk-ESJTIADP.js.map} +1 -1
- package/dist/{chunk-SMJZMKYN.js → chunk-GEVIB2UB.js} +43 -10
- package/dist/chunk-GEVIB2UB.js.map +1 -0
- package/dist/{chunk-TDNI6ZWL.js → chunk-IJOZZOGT.js} +7 -7
- package/dist/chunk-IJOZZOGT.js.map +1 -0
- package/dist/{chunk-GZRXOUBE.js → chunk-M6DDYFUD.js} +2 -2
- package/dist/chunk-M6DDYFUD.js.map +1 -0
- package/dist/{chunk-B4GZ2BXO.js → chunk-NZGLXZGP.js} +3 -3
- package/dist/{chunk-NZ32EONV.js → chunk-QWNJCQXZ.js} +2 -2
- package/dist/{chunk-FKFHZUGF.js → chunk-XN6GWKMV.js} +43 -56
- package/dist/chunk-XN6GWKMV.js.map +1 -0
- package/dist/{chunk-BHWIUEYH.js → chunk-ZBLK676C.js} +1 -61
- package/dist/chunk-ZBLK676C.js.map +1 -0
- package/dist/{chunk-QPI2CCBA.js → chunk-ZPJMYGEP.js} +149 -96
- package/dist/chunk-ZPJMYGEP.js.map +1 -0
- package/dist/components.d.ts +1 -1
- package/dist/components.js +11 -11
- package/dist/{formatting-B1jSqgl-.d.ts → formatting-DFcCxUEk.d.ts} +1 -1
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +9 -8
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.js +19 -17
- package/dist/index.js.map +1 -1
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +2 -3
- package/dist/rbac/index.js +7 -8
- package/dist/styles/index.d.ts +1 -1
- package/dist/styles/index.js +5 -3
- package/dist/theming/runtime.d.ts +73 -1
- package/dist/theming/runtime.js +5 -5
- package/dist/{usePublicRouteParams-BdF8bZgs.d.ts → usePublicRouteParams-Dyt1tzI9.d.ts} +60 -8
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js +5 -5
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +6 -6
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +6 -6
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +7 -7
- package/docs/api/interfaces/PublicErrorBoundaryState.md +5 -5
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +7 -7
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +51 -12
- package/docs/api/interfaces/PublicPageLayoutProps.md +72 -12
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +140 -30
- package/docs/best-practices/README.md +1 -1
- package/docs/implementation-guides/datatable-filtering.md +313 -0
- package/docs/implementation-guides/datatable-rbac-usage.md +317 -0
- package/docs/implementation-guides/hierarchical-datatable.md +850 -0
- package/docs/implementation-guides/large-datasets.md +281 -0
- package/docs/implementation-guides/performance.md +403 -0
- package/docs/implementation-guides/public-pages.md +4 -4
- package/docs/migration/quick-migration-guide.md +320 -0
- package/docs/rbac/quick-start.md +16 -16
- package/docs/troubleshooting/README.md +4 -4
- package/docs/troubleshooting/cake-page-permission-guard-issue-summary.md +1 -1
- package/docs/troubleshooting/debugging.md +1117 -0
- package/docs/troubleshooting/migration.md +918 -0
- package/examples/public-pages/CorrectPublicPageImplementation.tsx +30 -30
- package/examples/public-pages/PublicEventPage.tsx +41 -41
- package/examples/public-pages/PublicPageApp.tsx +33 -33
- package/examples/public-pages/PublicPageUsageExample.tsx +30 -30
- package/package.json +4 -4
- package/src/__tests__/hooks/usePermissions.test.ts +265 -0
- package/src/components/DataTable/DataTable.test.tsx +9 -38
- package/src/components/DataTable/DataTable.tsx +0 -7
- package/src/components/DataTable/components/DataTableCore.tsx +66 -136
- package/src/components/DataTable/components/DataTableModals.tsx +25 -22
- package/src/components/DataTable/components/EditableRow.tsx +118 -42
- package/src/components/DataTable/components/UnifiedTableBody.tsx +129 -76
- package/src/components/DataTable/components/__tests__/DataTableModals.test.tsx +33 -14
- package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +17 -5
- package/src/components/DataTable/utils/exportUtils.ts +3 -2
- package/src/components/Dialog/Dialog.tsx +1 -1
- package/src/components/Dialog/README.md +24 -24
- package/src/components/Dialog/examples/BasicHtmlTest.tsx +2 -2
- package/src/components/Dialog/examples/DebugHtmlExample.tsx +6 -6
- package/src/components/Dialog/examples/HtmlDialogExample.tsx +2 -2
- package/src/components/Dialog/examples/SimpleHtmlTest.tsx +3 -3
- package/src/components/Dialog/examples/__tests__/SimpleHtmlTest.test.tsx +4 -4
- package/src/components/PaceAppLayout/PaceAppLayout.tsx +12 -1
- package/src/components/PublicLayout/EventLogo.tsx +175 -0
- package/src/components/PublicLayout/PublicErrorBoundary.tsx +22 -18
- package/src/components/PublicLayout/PublicLoadingSpinner.tsx +22 -14
- package/src/components/PublicLayout/PublicPageHeader.tsx +133 -40
- package/src/components/PublicLayout/PublicPageLayout.tsx +75 -72
- package/src/components/PublicLayout/__tests__/PublicErrorBoundary.test.tsx +1 -1
- package/src/components/PublicLayout/__tests__/PublicLoadingSpinner.test.tsx +8 -8
- package/src/components/PublicLayout/__tests__/PublicPageHeader.test.tsx +23 -16
- package/src/components/PublicLayout/__tests__/PublicPageLayout.test.tsx +86 -14
- package/src/examples/CorrectPublicPageImplementation.tsx +30 -30
- package/src/examples/PublicEventPage.tsx +41 -41
- package/src/examples/PublicPageApp.tsx +33 -33
- package/src/examples/PublicPageUsageExample.tsx +30 -30
- package/src/hooks/__tests__/usePublicEvent.unit.test.ts +583 -0
- package/src/hooks/__tests__/usePublicRouteParams.unit.test.ts +10 -3
- package/src/hooks/index.ts +1 -1
- package/src/hooks/public/usePublicEventLogo.ts +285 -0
- package/src/hooks/public/usePublicRouteParams.ts +21 -4
- package/src/hooks/useEventTheme.test.ts +119 -43
- package/src/hooks/useEventTheme.ts +84 -55
- package/src/index.ts +3 -1
- package/src/rbac/components/__tests__/EnhancedNavigationMenu.test.tsx +630 -0
- package/src/rbac/components/__tests__/NavigationProvider.test.tsx +667 -0
- package/src/rbac/components/__tests__/PagePermissionProvider.test.tsx +647 -0
- package/src/rbac/components/__tests__/SecureDataProvider.fixed.test.tsx +496 -0
- package/src/rbac/components/__tests__/SecureDataProvider.test.tsx +496 -0
- package/src/rbac/secureClient.ts +4 -2
- package/src/services/EventService.ts +0 -66
- package/src/services/__tests__/EventService.eventColours.test.ts +44 -40
- package/src/styles/index.ts +1 -1
- package/src/theming/__tests__/parseEventColours.test.ts +209 -0
- package/src/theming/parseEventColours.ts +123 -0
- package/src/theming/runtime.ts +3 -0
- package/src/types/__tests__/file-reference.test.ts +447 -0
- package/src/utils/formatDate.test.ts +11 -11
- package/src/utils/formatting.ts +3 -2
- package/dist/chunk-BDZUMRBD.js 3.map +0 -1
- package/dist/chunk-BHWIUEYH.js.map +0 -1
- package/dist/chunk-CGURJ27Z.js.map +0 -1
- package/dist/chunk-FKFHZUGF.js.map +0 -1
- package/dist/chunk-GKHF54DI 2.js +0 -619
- package/dist/chunk-GKHF54DI.js 2.map +0 -1
- package/dist/chunk-GZRXOUBE.js.map +0 -1
- package/dist/chunk-HFBOFZ3Z.js.map +0 -1
- package/dist/chunk-NZ32EONV.js.map +0 -1
- package/dist/chunk-O3NWNXDY 2.js +0 -76
- package/dist/chunk-QPI2CCBA.js.map +0 -1
- package/dist/chunk-SMJZMKYN.js.map +0 -1
- package/dist/chunk-TDNI6ZWL.js 2.map +0 -1
- package/dist/chunk-TDNI6ZWL.js.map +0 -1
- package/dist/chunk-VKOCWWVY.js.map +0 -1
- package/dist/chunk-WP5I5GLN 2.js +0 -1564
- package/dist/index 3.js +0 -856
- package/dist/providers 3.js +0 -38
- package/dist/providers.js 3.map +0 -1
- package/dist/types 3.js +0 -128
- package/dist/types.js 3.map +0 -1
- package/dist/useInactivityTracker-MRUU55XI.js 3.map +0 -1
- package/dist/utils.js 3.map +0 -1
- package/dist/validation 3.js +0 -479
- package/src/styles/semantic.css +0 -24
- /package/dist/{DataTable-DGZDJUYM.js.map → DataTable-WTS4IRF2.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-UACKFATV.js.map → UnifiedAuthProvider-6C47WIML.js.map} +0 -0
- /package/dist/{chunk-D6BOFXYR.js.map → chunk-35ZDPMBM.js.map} +0 -0
- /package/dist/{chunk-CGURJ27Z.js 2.map → chunk-4MXVZVNS.js.map} +0 -0
- /package/dist/{chunk-ZYJ6O5CA.js.map → chunk-C43QIDN3.js.map} +0 -0
- /package/dist/{chunk-B4GZ2BXO.js.map → chunk-NZGLXZGP.js.map} +0 -0
- /package/dist/{chunk-NZ32EONV.js 2.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
|
-
//
|
|
96
|
-
|
|
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_-]{
|
|
268
|
-
|
|
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).
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|