@jmruthers/pace-core 0.5.121 → 0.5.124
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-OKDYRW2S.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-RIEJGKD3.js → chunk-ESJTIADP.js} +15 -6
- package/dist/{chunk-RIEJGKD3.js.map → chunk-ESJTIADP.js.map} +1 -1
- package/dist/{chunk-HFBOFZ3Z.js → chunk-GBGYYMC6.js} +317 -251
- package/dist/chunk-GBGYYMC6.js.map +1 -0
- 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-QPI2CCBA.js → chunk-VPUCTHTY.js} +149 -96
- package/dist/chunk-VPUCTHTY.js.map +1 -0
- 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/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 +125 -144
- package/src/components/DataTable/components/DataTableModals.tsx +25 -22
- package/src/components/DataTable/components/DataTableToolbar.tsx +14 -1
- 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-OKDYRW2S.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
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
import React from 'react';
|
|
40
|
+
import { cn } from '../../utils/cn';
|
|
40
41
|
|
|
41
42
|
export interface PublicLoadingSpinnerProps {
|
|
42
43
|
/** Loading message to display */
|
|
@@ -84,7 +85,7 @@ export function PublicLoadingSpinner({
|
|
|
84
85
|
const displayMessage = customMessage || message;
|
|
85
86
|
|
|
86
87
|
const content = (
|
|
87
|
-
<div className={
|
|
88
|
+
<div className={cn("flex flex-col items-center", className)}>
|
|
88
89
|
{/* App Logo */}
|
|
89
90
|
{showLogo && (
|
|
90
91
|
<div className="mb-4">
|
|
@@ -99,7 +100,10 @@ export function PublicLoadingSpinner({
|
|
|
99
100
|
{/* Spinner */}
|
|
100
101
|
<div className="relative">
|
|
101
102
|
<div
|
|
102
|
-
className={
|
|
103
|
+
className={cn(
|
|
104
|
+
sizeClass,
|
|
105
|
+
"border-2 border-sec-200 border-t-main-600 rounded-full animate-spin"
|
|
106
|
+
)}
|
|
103
107
|
role="status"
|
|
104
108
|
aria-label="Loading"
|
|
105
109
|
/>
|
|
@@ -108,7 +112,7 @@ export function PublicLoadingSpinner({
|
|
|
108
112
|
|
|
109
113
|
{/* Loading Message */}
|
|
110
114
|
{displayMessage && (
|
|
111
|
-
<p className="mt-4 text-sm text-
|
|
115
|
+
<p className="mt-4 text-sm text-sec-600 text-center">
|
|
112
116
|
{displayMessage}
|
|
113
117
|
</p>
|
|
114
118
|
)}
|
|
@@ -117,7 +121,7 @@ export function PublicLoadingSpinner({
|
|
|
117
121
|
|
|
118
122
|
if (centered) {
|
|
119
123
|
return (
|
|
120
|
-
<div className="min-h-screen bg-
|
|
124
|
+
<div className="min-h-screen bg-background flex items-center justify-center">
|
|
121
125
|
<div className="max-w-md mx-auto px-4">
|
|
122
126
|
{content}
|
|
123
127
|
</div>
|
|
@@ -140,7 +144,10 @@ export function PublicLoadingSpinnerFullPage({
|
|
|
140
144
|
eventName?: string;
|
|
141
145
|
}) {
|
|
142
146
|
return (
|
|
143
|
-
<div className={
|
|
147
|
+
<div className={cn(
|
|
148
|
+
"min-h-screen bg-background flex items-center justify-center",
|
|
149
|
+
className
|
|
150
|
+
)}>
|
|
144
151
|
<div className="max-w-md mx-auto text-center px-4">
|
|
145
152
|
{/* App Logo */}
|
|
146
153
|
<div className="mb-8">
|
|
@@ -153,7 +160,7 @@ export function PublicLoadingSpinnerFullPage({
|
|
|
153
160
|
|
|
154
161
|
{/* Event Name */}
|
|
155
162
|
{eventName && (
|
|
156
|
-
<h1 className="text-2xl font-bold text-
|
|
163
|
+
<h1 className="text-2xl font-bold text-sec-900 mb-4">
|
|
157
164
|
{eventName}
|
|
158
165
|
</h1>
|
|
159
166
|
)}
|
|
@@ -161,22 +168,22 @@ export function PublicLoadingSpinnerFullPage({
|
|
|
161
168
|
{/* Spinner */}
|
|
162
169
|
<div className="relative mb-6">
|
|
163
170
|
<div
|
|
164
|
-
className="h-12 w-12 border-4 border-
|
|
171
|
+
className="h-12 w-12 border-4 border-sec-200 border-t-main-600 rounded-full animate-spin mx-auto"
|
|
165
172
|
role="status"
|
|
166
173
|
aria-label="Loading"
|
|
167
174
|
/>
|
|
168
175
|
</div>
|
|
169
176
|
|
|
170
177
|
{/* Loading Message */}
|
|
171
|
-
<p className="text-lg text-
|
|
178
|
+
<p className="text-lg text-sec-600">
|
|
172
179
|
{message}
|
|
173
180
|
</p>
|
|
174
181
|
|
|
175
182
|
{/* Loading Dots Animation */}
|
|
176
183
|
<div className="mt-4 flex justify-center space-x-1">
|
|
177
|
-
<div className="h-2 w-2 bg-
|
|
178
|
-
<div className="h-2 w-2 bg-
|
|
179
|
-
<div className="h-2 w-2 bg-
|
|
184
|
+
<div className="h-2 w-2 bg-main-600 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
|
185
|
+
<div className="h-2 w-2 bg-main-600 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
|
186
|
+
<div className="h-2 w-2 bg-main-600 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
|
180
187
|
</div>
|
|
181
188
|
</div>
|
|
182
189
|
</div>
|
|
@@ -194,13 +201,14 @@ export function PublicLoadingSkeleton({
|
|
|
194
201
|
className?: string;
|
|
195
202
|
}) {
|
|
196
203
|
return (
|
|
197
|
-
<div className={
|
|
204
|
+
<div className={cn("animate-pulse", className)}>
|
|
198
205
|
{Array.from({ length: lines }).map((_, index) => (
|
|
199
206
|
<div
|
|
200
207
|
key={index}
|
|
201
|
-
className={
|
|
208
|
+
className={cn(
|
|
209
|
+
"h-4 bg-sec-200 rounded mb-2",
|
|
202
210
|
index === lines - 1 ? 'w-3/4' : 'w-full'
|
|
203
|
-
}
|
|
211
|
+
)}
|
|
204
212
|
/>
|
|
205
213
|
))}
|
|
206
214
|
</div>
|
|
@@ -30,6 +30,27 @@
|
|
|
30
30
|
* );
|
|
31
31
|
* }
|
|
32
32
|
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```tsx
|
|
36
|
+
* // Using custom logo URL
|
|
37
|
+
* <PublicPageHeader
|
|
38
|
+
* event={event}
|
|
39
|
+
* logoUrl="/custom-logo.svg"
|
|
40
|
+
* logoAlt="My Custom Logo"
|
|
41
|
+
* logoHref="/"
|
|
42
|
+
* />
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```tsx
|
|
47
|
+
* // Using custom logo component
|
|
48
|
+
* <PublicPageHeader
|
|
49
|
+
* event={event}
|
|
50
|
+
* customAppLogo={<CustomLogoComponent />}
|
|
51
|
+
* logoHref="/dashboard"
|
|
52
|
+
* />
|
|
53
|
+
* ```
|
|
33
54
|
*
|
|
34
55
|
* @accessibility
|
|
35
56
|
* - WCAG 2.1 AA compliant
|
|
@@ -46,10 +67,12 @@
|
|
|
46
67
|
*/
|
|
47
68
|
|
|
48
69
|
import React, { ReactNode } from 'react';
|
|
70
|
+
import { Link } from 'react-router-dom';
|
|
49
71
|
import type { Event } from '../../types/unified';
|
|
50
72
|
import { FileDisplay } from '../FileDisplay/FileDisplay';
|
|
51
73
|
import { FileCategory } from '../../types/file-reference';
|
|
52
74
|
import { useAppConfig } from '../../hooks/useAppConfig';
|
|
75
|
+
import { cn } from '../../utils/cn';
|
|
53
76
|
|
|
54
77
|
export interface PublicPageHeaderProps {
|
|
55
78
|
/** The event data for this public page */
|
|
@@ -68,10 +91,16 @@ export interface PublicPageHeaderProps {
|
|
|
68
91
|
className?: string;
|
|
69
92
|
/** Custom content to display in the header */
|
|
70
93
|
children?: ReactNode;
|
|
71
|
-
/** Custom app logo component */
|
|
94
|
+
/** Custom app logo component (overrides logoUrl and auto-generated path) */
|
|
72
95
|
customAppLogo?: ReactNode;
|
|
73
96
|
/** Custom event logo component */
|
|
74
97
|
customEventLogo?: ReactNode;
|
|
98
|
+
/** URL to the app logo image (overrides auto-generated path, but customAppLogo takes precedence) */
|
|
99
|
+
logoUrl?: string;
|
|
100
|
+
/** Alt text for the app logo (defaults to appName from useAppConfig) */
|
|
101
|
+
logoAlt?: string;
|
|
102
|
+
/** URL to navigate to when app logo is clicked */
|
|
103
|
+
logoHref?: string;
|
|
75
104
|
}
|
|
76
105
|
|
|
77
106
|
/**
|
|
@@ -80,6 +109,15 @@ export interface PublicPageHeaderProps {
|
|
|
80
109
|
* This component displays the app logo, event logo, and event information
|
|
81
110
|
* in a clean, accessible layout suitable for public pages.
|
|
82
111
|
*
|
|
112
|
+
* Logo handling follows a priority order:
|
|
113
|
+
* 1. customAppLogo prop (if provided) - highest priority
|
|
114
|
+
* 2. logoUrl prop (if provided) - direct URL override
|
|
115
|
+
* 3. Auto-generated path from appName via useAppConfig() - convention-based
|
|
116
|
+
* 4. Default SVG fallback - lowest priority
|
|
117
|
+
*
|
|
118
|
+
* The logo can be made clickable by providing the logoHref prop, which will
|
|
119
|
+
* wrap the logo in a Link component for navigation.
|
|
120
|
+
*
|
|
83
121
|
* @param props - Header configuration and content
|
|
84
122
|
* @returns React element with public page header
|
|
85
123
|
*/
|
|
@@ -93,32 +131,96 @@ export function PublicPageHeader({
|
|
|
93
131
|
className = '',
|
|
94
132
|
children,
|
|
95
133
|
customAppLogo,
|
|
96
|
-
customEventLogo
|
|
134
|
+
customEventLogo,
|
|
135
|
+
logoUrl,
|
|
136
|
+
logoAlt,
|
|
137
|
+
logoHref
|
|
97
138
|
}: PublicPageHeaderProps) {
|
|
98
139
|
const { appName } = useAppConfig();
|
|
99
|
-
const headerClasses = `bg-white border-b border-gray-200 ${className}`.trim();
|
|
100
140
|
|
|
101
141
|
return (
|
|
102
|
-
<header className={
|
|
103
|
-
|
|
142
|
+
<header className={cn(
|
|
143
|
+
" px-4 w-[min(var(--app-width),100%)] mx-auto bg-background border-b border-sec-200 grid grid-cols-[auto_1fr_auto] place-items-center gap-2",
|
|
144
|
+
className
|
|
145
|
+
)}>
|
|
146
|
+
|
|
104
147
|
{/* Top row with logos */}
|
|
105
|
-
|
|
148
|
+
|
|
106
149
|
{/* App Logo */}
|
|
107
150
|
{showAppLogo && (
|
|
108
|
-
|
|
109
|
-
{customAppLogo
|
|
110
|
-
|
|
111
|
-
className="
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
151
|
+
<>
|
|
152
|
+
{customAppLogo ? (
|
|
153
|
+
logoHref ? (
|
|
154
|
+
<Link to={logoHref} className="cursor-pointer hover:opacity-80 transition-opacity">
|
|
155
|
+
{customAppLogo}
|
|
156
|
+
</Link>
|
|
157
|
+
) : (
|
|
158
|
+
customAppLogo
|
|
159
|
+
)
|
|
160
|
+
) : logoUrl ? (
|
|
161
|
+
logoHref ? (
|
|
162
|
+
<Link to={logoHref} className="cursor-pointer hover:opacity-80 transition-opacity">
|
|
163
|
+
<img
|
|
164
|
+
className="max-w-36 object-contain row-span-2"
|
|
165
|
+
src={logoUrl}
|
|
166
|
+
alt={logoAlt || appName}
|
|
167
|
+
/>
|
|
168
|
+
</Link>
|
|
169
|
+
) : (
|
|
170
|
+
<img
|
|
171
|
+
className="max-w-36 object-contain row-span-2"
|
|
172
|
+
src={logoUrl}
|
|
173
|
+
alt={logoAlt || appName}
|
|
174
|
+
/>
|
|
175
|
+
)
|
|
176
|
+
) : appName ? (
|
|
177
|
+
logoHref ? (
|
|
178
|
+
<Link to={logoHref} className="cursor-pointer hover:opacity-80 transition-opacity">
|
|
179
|
+
<img
|
|
180
|
+
className="max-w-36 object-contain row-span-2"
|
|
181
|
+
src={`/${appName.toLowerCase()}_logo_wide.svg`}
|
|
182
|
+
alt={logoAlt || appName}
|
|
183
|
+
/>
|
|
184
|
+
</Link>
|
|
185
|
+
) : (
|
|
186
|
+
<img
|
|
187
|
+
className="max-w-36 object-contain row-span-2"
|
|
188
|
+
src={`/${appName.toLowerCase()}_logo_wide.svg`}
|
|
189
|
+
alt={logoAlt || appName}
|
|
190
|
+
/>
|
|
191
|
+
)
|
|
192
|
+
) : (
|
|
193
|
+
logoHref ? (
|
|
194
|
+
<Link to={logoHref} className="cursor-pointer hover:opacity-80 transition-opacity">
|
|
195
|
+
<img
|
|
196
|
+
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23000'/%3E%3Ctext x='16' y='20' text-anchor='middle' fill='white' font-family='Arial' font-size='14' font-weight='bold'%3EL%3C/text%3E%3C/svg%3E"
|
|
197
|
+
alt={logoAlt || 'Logo'}
|
|
198
|
+
className="max-w-36 object-contain row-span-2"
|
|
199
|
+
/>
|
|
200
|
+
</Link>
|
|
201
|
+
) : (
|
|
202
|
+
<img
|
|
203
|
+
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23000'/%3E%3Ctext x='16' y='20' text-anchor='middle' fill='white' font-family='Arial' font-size='14' font-weight='bold'%3EL%3C/text%3E%3C/svg%3E"
|
|
204
|
+
alt={logoAlt || 'Logo'}
|
|
205
|
+
className="max-w-36 object-contain row-span-2"
|
|
206
|
+
/>
|
|
207
|
+
)
|
|
115
208
|
)}
|
|
116
|
-
|
|
209
|
+
</>
|
|
117
210
|
)}
|
|
118
211
|
|
|
212
|
+
|
|
213
|
+
{/* Event Information */}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
{event && (
|
|
217
|
+
<>
|
|
218
|
+
<h1>
|
|
219
|
+
{event.event_name}
|
|
220
|
+
</h1>
|
|
119
221
|
{/* Event Logo */}
|
|
120
222
|
{showEventLogo && event && (
|
|
121
|
-
|
|
223
|
+
<>
|
|
122
224
|
{customEventLogo || (
|
|
123
225
|
<FileDisplay
|
|
124
226
|
table_name="event"
|
|
@@ -128,7 +230,7 @@ export function PublicPageHeader({
|
|
|
128
230
|
displayOnly={true}
|
|
129
231
|
showFallback={true}
|
|
130
232
|
fallbackSize="md"
|
|
131
|
-
className="
|
|
233
|
+
className="max-w-36 row-span-2"
|
|
132
234
|
generateFallbackText={(fileName) => {
|
|
133
235
|
if (!event.event_name) return 'EV';
|
|
134
236
|
return event.event_name
|
|
@@ -139,50 +241,41 @@ export function PublicPageHeader({
|
|
|
139
241
|
}}
|
|
140
242
|
/>
|
|
141
243
|
)}
|
|
142
|
-
|
|
244
|
+
</>
|
|
143
245
|
)}
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
{/* Event Information */}
|
|
147
|
-
<div className="pb-4">
|
|
148
|
-
<div className="text-center">
|
|
149
|
-
{event && (
|
|
150
|
-
<>
|
|
151
|
-
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
|
152
|
-
{event.event_name}
|
|
153
|
-
</h1>
|
|
154
|
-
|
|
155
246
|
{event.event_venue && (
|
|
156
|
-
<
|
|
247
|
+
<h4>
|
|
157
248
|
{event.event_venue}
|
|
158
|
-
</
|
|
249
|
+
</h4>
|
|
159
250
|
)}
|
|
160
251
|
</>
|
|
161
252
|
)}
|
|
162
253
|
|
|
163
254
|
{/* Page Title and Description */}
|
|
164
255
|
{title && (
|
|
165
|
-
|
|
166
|
-
<
|
|
256
|
+
<>
|
|
257
|
+
<h1>
|
|
167
258
|
{title}
|
|
168
|
-
</
|
|
259
|
+
</h1>
|
|
169
260
|
{description && (
|
|
170
|
-
<p className="text-lg text-
|
|
261
|
+
<p className="text-lg text-sec-600 max-w-3xl mx-auto">
|
|
171
262
|
{description}
|
|
172
263
|
</p>
|
|
173
264
|
)}
|
|
174
|
-
|
|
265
|
+
</>
|
|
175
266
|
)}
|
|
176
267
|
|
|
177
268
|
{/* Custom Content */}
|
|
178
269
|
{children && (
|
|
179
|
-
|
|
270
|
+
<>
|
|
180
271
|
{children}
|
|
181
|
-
|
|
272
|
+
</>
|
|
182
273
|
)}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
|
|
186
279
|
</header>
|
|
187
280
|
);
|
|
188
281
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Features:
|
|
11
11
|
* - No authentication required
|
|
12
12
|
* - Event-specific header and branding
|
|
13
|
+
* - Automatic event color theming (applies event_colours when available)
|
|
13
14
|
* - Responsive design
|
|
14
15
|
* - Print-friendly styling
|
|
15
16
|
* - Error boundary integration
|
|
@@ -18,18 +19,22 @@
|
|
|
18
19
|
*
|
|
19
20
|
* @example
|
|
20
21
|
* ```tsx
|
|
21
|
-
* import { PublicPageLayout,
|
|
22
|
+
* import { PublicPageLayout, usePublicRouteParams, usePublicEvent } from '@jmruthers/pace-core';
|
|
22
23
|
*
|
|
23
24
|
* function PublicEventPage() {
|
|
24
|
-
* const { eventCode } = usePublicRouteParams();
|
|
25
|
+
* const { eventCode } = usePublicRouteParams({ fetchEventData: false });
|
|
26
|
+
* const { event, isLoading, error, refetch } = usePublicEvent(eventCode || '');
|
|
25
27
|
*
|
|
28
|
+
* // PublicPageLayout handles all loading, error, and missing event states
|
|
26
29
|
* return (
|
|
27
|
-
* <PublicPageLayout
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* <PublicPageLayout
|
|
31
|
+
* eventCode={eventCode || ''}
|
|
32
|
+
* event={event}
|
|
33
|
+
* isLoading={isLoading}
|
|
34
|
+
* error={error}
|
|
35
|
+
* refetch={refetch}
|
|
36
|
+
* >
|
|
37
|
+
* <h1>Event Details</h1>
|
|
33
38
|
* <div className="content">
|
|
34
39
|
* Your public page content
|
|
35
40
|
* </div>
|
|
@@ -59,11 +64,13 @@
|
|
|
59
64
|
* - Tailwind CSS - Styling
|
|
60
65
|
*/
|
|
61
66
|
|
|
62
|
-
import React, { ReactNode
|
|
67
|
+
import React, { ReactNode } from 'react';
|
|
63
68
|
import { PublicPageHeader } from './PublicPageHeader';
|
|
64
69
|
import { PublicPageFooter } from './PublicPageFooter';
|
|
65
70
|
import { PublicErrorBoundary } from './PublicErrorBoundary';
|
|
66
71
|
import { PublicLoadingSpinner } from './PublicLoadingSpinner';
|
|
72
|
+
import { Button } from '../Button';
|
|
73
|
+
import { useEventTheme } from '../../hooks/useEventTheme';
|
|
67
74
|
import type { Event } from '../../types/unified';
|
|
68
75
|
|
|
69
76
|
export interface PublicPageLayoutProps {
|
|
@@ -73,9 +80,15 @@ export interface PublicPageLayoutProps {
|
|
|
73
80
|
children: ReactNode;
|
|
74
81
|
/** Optional event data - if not provided, will be fetched by parent component */
|
|
75
82
|
event?: Event | null;
|
|
83
|
+
/** Loading state - if true, shows loading spinner */
|
|
84
|
+
isLoading?: boolean;
|
|
85
|
+
/** Error state - if provided, shows error message */
|
|
86
|
+
error?: Error | null;
|
|
87
|
+
/** Function to retry loading event data */
|
|
88
|
+
refetch?: () => Promise<void> | void;
|
|
76
89
|
/** Whether to show the footer (default: true) */
|
|
77
90
|
showFooter?: boolean;
|
|
78
|
-
/** Custom CSS classes for the layout */
|
|
91
|
+
/** @deprecated Custom CSS classes for the layout - no longer used as wrapper div was removed */
|
|
79
92
|
className?: string;
|
|
80
93
|
/** Custom error fallback component */
|
|
81
94
|
errorFallback?: React.ComponentType<{ error: Error; retry: () => void }>;
|
|
@@ -87,6 +100,8 @@ export interface PublicPageLayoutProps {
|
|
|
87
100
|
customFooter?: ReactNode;
|
|
88
101
|
/** Whether to show event validation errors (default: true) */
|
|
89
102
|
showValidationErrors?: boolean;
|
|
103
|
+
/** Custom loading message */
|
|
104
|
+
loadingMessage?: string;
|
|
90
105
|
}
|
|
91
106
|
|
|
92
107
|
/**
|
|
@@ -95,6 +110,9 @@ export interface PublicPageLayoutProps {
|
|
|
95
110
|
* This component provides a consistent structure for public event pages
|
|
96
111
|
* with event-specific branding, error handling, and loading states.
|
|
97
112
|
*
|
|
113
|
+
* Automatically applies event colors from the event's event_colours field
|
|
114
|
+
* when an event is provided, ensuring consistent theming across public pages.
|
|
115
|
+
*
|
|
98
116
|
* @param props - Layout configuration and content
|
|
99
117
|
* @returns React element with complete public page layout
|
|
100
118
|
*/
|
|
@@ -102,96 +120,81 @@ export function PublicPageLayout({
|
|
|
102
120
|
eventCode,
|
|
103
121
|
children,
|
|
104
122
|
event = null,
|
|
123
|
+
isLoading = false,
|
|
124
|
+
error = null,
|
|
125
|
+
refetch,
|
|
105
126
|
showFooter = true,
|
|
106
127
|
className = '',
|
|
107
128
|
errorFallback: ErrorFallback,
|
|
108
129
|
loadingFallback: LoadingFallback = PublicLoadingSpinner,
|
|
109
130
|
customHeader,
|
|
110
131
|
customFooter,
|
|
111
|
-
showValidationErrors = true
|
|
132
|
+
showValidationErrors = true,
|
|
133
|
+
loadingMessage
|
|
112
134
|
}: PublicPageLayoutProps) {
|
|
113
|
-
//
|
|
114
|
-
// This
|
|
115
|
-
|
|
116
|
-
const error = null;
|
|
117
|
-
const refetch = async () => {};
|
|
135
|
+
// Apply event theme colors automatically when event is available
|
|
136
|
+
// This works in public page context without requiring EventProvider
|
|
137
|
+
useEventTheme(event);
|
|
118
138
|
|
|
119
|
-
//
|
|
120
|
-
const
|
|
121
|
-
const baseClasses = 'min-h-screen bg-white flex flex-col';
|
|
122
|
-
return `${baseClasses} ${className}`.trim();
|
|
123
|
-
}, [className]);
|
|
139
|
+
// Default refetch function
|
|
140
|
+
const handleRefetch = refetch || (async () => {});
|
|
124
141
|
|
|
125
142
|
// Handle loading state
|
|
126
143
|
if (isLoading) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
if (LoadingFallback === PublicLoadingSpinner) {
|
|
145
|
+
return (
|
|
146
|
+
<PublicLoadingSpinner
|
|
147
|
+
className="items-center justify-center"
|
|
148
|
+
message={loadingMessage}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return <LoadingFallback />;
|
|
134
153
|
}
|
|
135
154
|
|
|
136
155
|
// Handle error state
|
|
137
156
|
if (error && showValidationErrors) {
|
|
157
|
+
if (ErrorFallback) {
|
|
158
|
+
return <ErrorFallback error={error} retry={handleRefetch} />;
|
|
159
|
+
}
|
|
138
160
|
return (
|
|
139
|
-
<
|
|
140
|
-
<div className="
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
Event Not Found
|
|
147
|
-
</h1>
|
|
148
|
-
<p className="text-gray-600 mb-6">
|
|
149
|
-
The event code "{eventCode}" is invalid or the event is not available for public viewing.
|
|
150
|
-
</p>
|
|
151
|
-
<button
|
|
152
|
-
onClick={refetch}
|
|
153
|
-
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
|
154
|
-
>
|
|
155
|
-
Try Again
|
|
156
|
-
</button>
|
|
157
|
-
</div>
|
|
158
|
-
)}
|
|
161
|
+
<main className="flex flex-col items-center justify-center px-4 w-[min(var(--app-width),100%)] mx-auto py-8">
|
|
162
|
+
<div className="text-center">
|
|
163
|
+
<h1>Event Not Found</h1>
|
|
164
|
+
<p>
|
|
165
|
+
The event code "{eventCode}" is invalid or the event is not available for public viewing.
|
|
166
|
+
</p>
|
|
167
|
+
<Button onClick={handleRefetch}>Try Again</Button>
|
|
159
168
|
</div>
|
|
160
|
-
</
|
|
169
|
+
</main>
|
|
161
170
|
);
|
|
162
171
|
}
|
|
163
172
|
|
|
164
173
|
// Handle missing event
|
|
165
|
-
if (!event) {
|
|
174
|
+
if (!event && showValidationErrors) {
|
|
166
175
|
return (
|
|
167
|
-
<
|
|
168
|
-
<div className="
|
|
169
|
-
<
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
This event is not available for public viewing.
|
|
175
|
-
</p>
|
|
176
|
-
<button
|
|
177
|
-
onClick={refetch}
|
|
178
|
-
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors"
|
|
179
|
-
>
|
|
180
|
-
Try Again
|
|
181
|
-
</button>
|
|
182
|
-
</div>
|
|
176
|
+
<main className="flex flex-col items-center justify-center px-4 w-[min(var(--app-width),100%)] mx-auto py-8">
|
|
177
|
+
<div className="text-center">
|
|
178
|
+
<h1>Event Not Available</h1>
|
|
179
|
+
<p>
|
|
180
|
+
This event is not available for public viewing.
|
|
181
|
+
</p>
|
|
182
|
+
{handleRefetch && <Button onClick={handleRefetch}>Try Again</Button>}
|
|
183
183
|
</div>
|
|
184
|
-
</
|
|
184
|
+
</main>
|
|
185
185
|
);
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// At this point, if showValidationErrors is true, event must exist
|
|
189
|
+
// If showValidationErrors is false, we allow rendering without event
|
|
190
|
+
// But footer requires event, so only show it if event exists
|
|
188
191
|
return (
|
|
189
192
|
<PublicErrorBoundary>
|
|
190
|
-
|
|
193
|
+
<>
|
|
191
194
|
{/* Header */}
|
|
192
195
|
{customHeader || (
|
|
193
196
|
<PublicPageHeader
|
|
194
|
-
event={event}
|
|
197
|
+
event={event || undefined}
|
|
195
198
|
eventCode={eventCode}
|
|
196
199
|
/>
|
|
197
200
|
)}
|
|
@@ -201,11 +204,11 @@ export function PublicPageLayout({
|
|
|
201
204
|
{children}
|
|
202
205
|
</main>
|
|
203
206
|
|
|
204
|
-
{/* Footer */}
|
|
205
|
-
{showFooter && (
|
|
207
|
+
{/* Footer - only show if event exists */}
|
|
208
|
+
{showFooter && event && (
|
|
206
209
|
customFooter || <PublicPageFooter event={event} />
|
|
207
210
|
)}
|
|
208
|
-
|
|
211
|
+
</>
|
|
209
212
|
</PublicErrorBoundary>
|
|
210
213
|
);
|
|
211
214
|
}
|
|
@@ -278,7 +278,7 @@ describe('[component] PublicErrorBoundary', () => {
|
|
|
278
278
|
);
|
|
279
279
|
|
|
280
280
|
const tryAgainButton = screen.getByRole('button', { name: 'Try Again' });
|
|
281
|
-
expect(tryAgainButton).toHaveClass('focus:outline-none', 'focus:ring-2', 'focus:ring-
|
|
281
|
+
expect(tryAgainButton).toHaveClass('focus:outline-none', 'focus:ring-2', 'focus:ring-main-500', 'focus:ring-offset-2');
|
|
282
282
|
});
|
|
283
283
|
});
|
|
284
284
|
});
|