@djangocfg/layouts 2.0.4 → 2.0.6
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/README.md +138 -20
- package/package.json +20 -5
- package/src/components/RedirectPage/RedirectPage.tsx +70 -0
- package/src/components/RedirectPage/index.ts +7 -0
- package/src/components/core/index.ts +10 -0
- package/src/components/errors/ErrorLayout.tsx +228 -0
- package/src/components/errors/errorConfig.ts +118 -0
- package/src/components/errors/index.ts +10 -0
- package/src/components/index.ts +13 -5
- package/src/contexts/LeadsContext.tsx +156 -0
- package/src/contexts/NewsletterContext.tsx +263 -0
- package/src/contexts/SupportContext.tsx +256 -0
- package/src/contexts/index.ts +59 -0
- package/src/contexts/knowbase/ChatContext.tsx +174 -0
- package/src/contexts/knowbase/DocumentsContext.tsx +304 -0
- package/src/contexts/knowbase/SessionsContext.tsx +174 -0
- package/src/contexts/knowbase/index.ts +61 -0
- package/src/contexts/payments/BalancesContext.tsx +65 -0
- package/src/contexts/payments/CurrenciesContext.tsx +66 -0
- package/src/contexts/payments/OverviewContext.tsx +174 -0
- package/src/contexts/payments/PaymentsContext.tsx +132 -0
- package/src/contexts/payments/README.md +201 -0
- package/src/contexts/payments/RootPaymentsContext.tsx +68 -0
- package/src/contexts/payments/index.ts +50 -0
- package/src/index.ts +8 -0
- package/src/layouts/AppLayout/AppLayout.tsx +24 -14
- package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +1 -1
- package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +1 -1
- package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +1 -1
- package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +1 -1
- package/src/layouts/PaymentsLayout/views/transactions/components/TransactionsList.tsx +1 -1
- package/src/layouts/ProfileLayout/components/ProfileForm.tsx +1 -1
- package/src/layouts/SupportLayout/SupportLayout.tsx +1 -1
- package/src/layouts/SupportLayout/components/TicketCard.tsx +1 -1
- package/src/layouts/SupportLayout/context/SupportLayoutContext.tsx +1 -1
- package/src/layouts/SupportLayout/index.ts +2 -0
- package/src/layouts/SupportLayout/types.ts +2 -4
- package/src/pages/index.ts +6 -0
- package/src/pages/legal/LegalPage.tsx +85 -0
- package/src/pages/legal/configs.ts +131 -0
- package/src/pages/legal/index.ts +24 -0
- package/src/pages/legal/pages.tsx +58 -0
- package/src/pages/legal/types.ts +15 -0
- package/src/snippets/Chat/ChatWidget.tsx +1 -1
- package/src/snippets/Chat/components/SessionList.tsx +1 -1
- package/src/snippets/Chat/index.tsx +1 -1
- package/src/snippets/Chat/types.ts +7 -5
- package/src/snippets/ContactForm/ContactForm.tsx +20 -8
- package/src/utils/index.ts +1 -0
- package/src/utils/og-image.ts +169 -0
- /package/src/components/{JsonLd.tsx → core/JsonLd.tsx} +0 -0
- /package/src/components/{LucideIcon.tsx → core/LucideIcon.tsx} +0 -0
- /package/src/components/{PageProgress.tsx → core/PageProgress.tsx} +0 -0
- /package/src/components/{Suspense.tsx → core/Suspense.tsx} +0 -0
- /package/src/components/{ErrorBoundary.tsx → errors/ErrorBoundary.tsx} +0 -0
- /package/src/components/{ErrorsTracker → errors/ErrorsTracker}/README.md +0 -0
- /package/src/components/{ErrorsTracker → errors/ErrorsTracker}/components/ErrorButtons.tsx +0 -0
- /package/src/components/{ErrorsTracker → errors/ErrorsTracker}/components/ErrorToast.tsx +0 -0
- /package/src/components/{ErrorsTracker → errors/ErrorsTracker}/hooks.ts +0 -0
- /package/src/components/{ErrorsTracker → errors/ErrorsTracker}/index.ts +0 -0
- /package/src/components/{ErrorsTracker → errors/ErrorsTracker}/providers/ErrorTrackingProvider.tsx +0 -0
- /package/src/components/{ErrorsTracker → errors/ErrorsTracker}/types.ts +0 -0
- /package/src/components/{ErrorsTracker → errors/ErrorsTracker}/utils/curl-generator.ts +0 -0
- /package/src/components/{ErrorsTracker → errors/ErrorsTracker}/utils/formatters.ts +0 -0
package/README.md
CHANGED
|
@@ -254,41 +254,89 @@ import { ChatWidget, ChatUIProvider, useChatUI } from '@djangocfg/layouts/snippe
|
|
|
254
254
|
|
|
255
255
|
## Components
|
|
256
256
|
|
|
257
|
-
Utility components
|
|
257
|
+
Utility components organized by category.
|
|
258
|
+
|
|
259
|
+
### Core Components
|
|
258
260
|
|
|
259
261
|
```tsx
|
|
260
262
|
import {
|
|
261
|
-
ErrorBoundary,
|
|
262
|
-
PageProgress,
|
|
263
263
|
JsonLd,
|
|
264
|
-
LucideIcon
|
|
265
|
-
|
|
264
|
+
LucideIcon,
|
|
265
|
+
PageProgress,
|
|
266
|
+
Suspense
|
|
267
|
+
} from '@djangocfg/layouts/components/core';
|
|
266
268
|
```
|
|
267
269
|
|
|
268
270
|
| Component | Description |
|
|
269
271
|
|-----------|-------------|
|
|
270
|
-
| `ErrorBoundary` | React error boundary component |
|
|
271
|
-
| `PageProgress` | Page loading progress indicator |
|
|
272
272
|
| `JsonLd` | JSON-LD structured data component |
|
|
273
273
|
| `LucideIcon` | Lucide icon wrapper component |
|
|
274
|
+
| `PageProgress` | Page loading progress indicator |
|
|
275
|
+
| `Suspense` | Suspense wrapper component |
|
|
274
276
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
Error tracking and validation utilities.
|
|
277
|
+
### Error Components
|
|
278
278
|
|
|
279
279
|
```tsx
|
|
280
280
|
import {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
} from '@djangocfg/layouts/
|
|
281
|
+
ErrorBoundary,
|
|
282
|
+
ErrorLayout,
|
|
283
|
+
getErrorContent,
|
|
284
|
+
ERROR_CODES
|
|
285
|
+
} from '@djangocfg/layouts/components/errors';
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
| Component | Description |
|
|
289
|
+
|-----------|-------------|
|
|
290
|
+
| `ErrorBoundary` | React error boundary component |
|
|
291
|
+
| `ErrorLayout` | Reusable error page layout (404, 500, etc.) |
|
|
292
|
+
| `getErrorContent` | Get error content by status code |
|
|
293
|
+
| `ERROR_CODES` | Common HTTP error code constants |
|
|
294
|
+
|
|
295
|
+
**ErrorLayout Usage:**
|
|
296
|
+
|
|
297
|
+
```tsx
|
|
298
|
+
// app/not-found.tsx
|
|
299
|
+
import { ErrorLayout } from '@djangocfg/layouts/components/errors';
|
|
300
|
+
|
|
301
|
+
export default function NotFound() {
|
|
302
|
+
return <ErrorLayout code={404} supportEmail="support@example.com" />;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// app/error.tsx
|
|
306
|
+
'use client';
|
|
307
|
+
import { ErrorLayout } from '@djangocfg/layouts/components/errors';
|
|
308
|
+
|
|
309
|
+
export default function Error({ error, reset }) {
|
|
310
|
+
return <ErrorLayout code={500} supportEmail="support@example.com" />;
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Redirect Component
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
import { RedirectPage } from '@djangocfg/layouts/components/RedirectPage';
|
|
318
|
+
|
|
319
|
+
// app/page.tsx
|
|
320
|
+
export default function Page() {
|
|
321
|
+
return (
|
|
322
|
+
<RedirectPage
|
|
323
|
+
authenticatedPath="/dashboard"
|
|
324
|
+
unauthenticatedPath="/auth"
|
|
325
|
+
loadingText="Loading..."
|
|
326
|
+
/>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
286
329
|
```
|
|
287
330
|
|
|
288
331
|
### Error Tracking
|
|
289
332
|
|
|
290
333
|
```tsx
|
|
291
|
-
import {
|
|
334
|
+
import {
|
|
335
|
+
ErrorTrackingProvider,
|
|
336
|
+
useErrors,
|
|
337
|
+
ErrorButtons,
|
|
338
|
+
ErrorToast
|
|
339
|
+
} from '@djangocfg/layouts/components/errors/ErrorsTracker';
|
|
292
340
|
|
|
293
341
|
<ErrorTrackingProvider>
|
|
294
342
|
<YourApp />
|
|
@@ -298,6 +346,76 @@ import { ErrorTrackingProvider, useErrors } from '@djangocfg/layouts/validation'
|
|
|
298
346
|
const { addError, clearErrors, errors } = useErrors();
|
|
299
347
|
```
|
|
300
348
|
|
|
349
|
+
### Update Notifier
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
import { UpdateNotifier } from '@djangocfg/layouts/components/UpdateNotifier';
|
|
353
|
+
|
|
354
|
+
<UpdateNotifier />
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Pages
|
|
358
|
+
|
|
359
|
+
Ready-to-use page components.
|
|
360
|
+
|
|
361
|
+
### Legal Pages
|
|
362
|
+
|
|
363
|
+
Pre-built legal page components with default configurations.
|
|
364
|
+
|
|
365
|
+
```tsx
|
|
366
|
+
import {
|
|
367
|
+
PrivacyPage,
|
|
368
|
+
TermsPage,
|
|
369
|
+
CookiesPage,
|
|
370
|
+
SecurityPage
|
|
371
|
+
} from '@djangocfg/layouts/pages/legal';
|
|
372
|
+
|
|
373
|
+
// app/legal/privacy/page.tsx
|
|
374
|
+
export default PrivacyPage;
|
|
375
|
+
|
|
376
|
+
// Or customize
|
|
377
|
+
import { PrivacyPage, privacyConfig } from '@djangocfg/layouts/pages/legal';
|
|
378
|
+
|
|
379
|
+
export default function CustomPrivacy() {
|
|
380
|
+
return <PrivacyPage config={{
|
|
381
|
+
...privacyConfig,
|
|
382
|
+
lastUpdated: '2024-01-01',
|
|
383
|
+
}} />;
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
| Page | Description |
|
|
388
|
+
|------|-------------|
|
|
389
|
+
| `PrivacyPage` | Privacy policy page |
|
|
390
|
+
| `TermsPage` | Terms of service page |
|
|
391
|
+
| `CookiesPage` | Cookie policy page |
|
|
392
|
+
| `SecurityPage` | Security policy page |
|
|
393
|
+
|
|
394
|
+
## Utils
|
|
395
|
+
|
|
396
|
+
Utility functions and helpers.
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
import {
|
|
400
|
+
generateOgImageUrl,
|
|
401
|
+
getAbsoluteOgImageUrl,
|
|
402
|
+
createOgImageUrlBuilder
|
|
403
|
+
} from '@djangocfg/layouts/utils/og-image';
|
|
404
|
+
|
|
405
|
+
// Generate OG image URL
|
|
406
|
+
const ogUrl = generateOgImageUrl('/api/og', {
|
|
407
|
+
title: 'My Page',
|
|
408
|
+
description: 'Page description',
|
|
409
|
+
siteName: 'My Site',
|
|
410
|
+
});
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
| Utility | Description |
|
|
414
|
+
|---------|-------------|
|
|
415
|
+
| `generateOgImageUrl` | Generate OG image URL with base64 encoding |
|
|
416
|
+
| `getAbsoluteOgImageUrl` | Get absolute OG image URL |
|
|
417
|
+
| `createOgImageUrlBuilder` | Create OG image URL builder with defaults |
|
|
418
|
+
|
|
301
419
|
## Exports
|
|
302
420
|
|
|
303
421
|
| Path | Content |
|
|
@@ -308,10 +426,10 @@ const { addError, clearErrors, errors } = useErrors();
|
|
|
308
426
|
| `@djangocfg/layouts/auth/context` | Auth context only |
|
|
309
427
|
| `@djangocfg/layouts/auth/hooks` | Auth hooks only |
|
|
310
428
|
| `@djangocfg/layouts/snippets` | Reusable components + Analytics |
|
|
311
|
-
| `@djangocfg/layouts/components` |
|
|
312
|
-
| `@djangocfg/layouts/
|
|
313
|
-
| `@djangocfg/layouts/
|
|
314
|
-
| `@djangocfg/layouts/
|
|
429
|
+
| `@djangocfg/layouts/components` | All utility components |
|
|
430
|
+
| `@djangocfg/layouts/pages` | Page components (legal pages) |
|
|
431
|
+
| `@djangocfg/layouts/pages/legal` | Legal page components |
|
|
432
|
+
| `@djangocfg/layouts/utils` | Utilities (og-image, logger) |
|
|
315
433
|
| `@djangocfg/layouts/styles` | CSS |
|
|
316
434
|
| `@djangocfg/layouts/styles/dashboard` | Dashboard-specific CSS |
|
|
317
435
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -64,6 +64,21 @@
|
|
|
64
64
|
"import": "./src/utils/index.ts",
|
|
65
65
|
"default": "./src/utils/index.ts"
|
|
66
66
|
},
|
|
67
|
+
"./components": {
|
|
68
|
+
"types": "./src/components/index.ts",
|
|
69
|
+
"import": "./src/components/index.ts",
|
|
70
|
+
"default": "./src/components/index.ts"
|
|
71
|
+
},
|
|
72
|
+
"./pages": {
|
|
73
|
+
"types": "./src/pages/index.ts",
|
|
74
|
+
"import": "./src/pages/index.ts",
|
|
75
|
+
"default": "./src/pages/index.ts"
|
|
76
|
+
},
|
|
77
|
+
"./contexts": {
|
|
78
|
+
"types": "./src/contexts/index.ts",
|
|
79
|
+
"import": "./src/contexts/index.ts",
|
|
80
|
+
"default": "./src/contexts/index.ts"
|
|
81
|
+
},
|
|
67
82
|
"./styles": "./src/styles/index.css",
|
|
68
83
|
"./styles/dashboard": "./src/styles/dashboard.css"
|
|
69
84
|
},
|
|
@@ -77,9 +92,9 @@
|
|
|
77
92
|
"check": "tsc --noEmit"
|
|
78
93
|
},
|
|
79
94
|
"peerDependencies": {
|
|
80
|
-
"@djangocfg/api": "^1.4.
|
|
81
|
-
"@djangocfg/centrifugo": "^1.4.
|
|
82
|
-
"@djangocfg/ui": "^1.4.
|
|
95
|
+
"@djangocfg/api": "^1.4.36",
|
|
96
|
+
"@djangocfg/centrifugo": "^1.4.36",
|
|
97
|
+
"@djangocfg/ui": "^1.4.36",
|
|
83
98
|
"@hookform/resolvers": "^5.2.0",
|
|
84
99
|
"consola": "^3.4.2",
|
|
85
100
|
"lucide-react": "^0.468.0",
|
|
@@ -98,7 +113,7 @@
|
|
|
98
113
|
"react-ga4": "^2.1.0"
|
|
99
114
|
},
|
|
100
115
|
"devDependencies": {
|
|
101
|
-
"@djangocfg/typescript-config": "^1.4.
|
|
116
|
+
"@djangocfg/typescript-config": "^1.4.36",
|
|
102
117
|
"@types/node": "^24.7.2",
|
|
103
118
|
"@types/react": "19.2.2",
|
|
104
119
|
"@types/react-dom": "19.2.1",
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { useAuth } from '../../auth';
|
|
5
|
+
import { useRouter } from '@djangocfg/ui/hooks';
|
|
6
|
+
import { Preloader } from '@djangocfg/ui/components';
|
|
7
|
+
|
|
8
|
+
export interface RedirectPageProps {
|
|
9
|
+
/**
|
|
10
|
+
* Path to redirect to when user is authenticated
|
|
11
|
+
* @default '/private'
|
|
12
|
+
*/
|
|
13
|
+
authenticatedPath?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Path to redirect to when user is not authenticated
|
|
16
|
+
* @default '/auth'
|
|
17
|
+
*/
|
|
18
|
+
unauthenticatedPath?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Custom loading text
|
|
21
|
+
* @default 'Loading...'
|
|
22
|
+
*/
|
|
23
|
+
loadingText?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* RedirectPage - Root page component that handles authentication redirect
|
|
28
|
+
*
|
|
29
|
+
* Redirects authenticated users to authenticatedPath, otherwise to unauthenticatedPath
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* ```tsx
|
|
33
|
+
* // app/page.tsx
|
|
34
|
+
* import { RedirectPage } from '@djangocfg/layouts/components';
|
|
35
|
+
*
|
|
36
|
+
* export default function Page() {
|
|
37
|
+
* return (
|
|
38
|
+
* <RedirectPage
|
|
39
|
+
* authenticatedPath="/private"
|
|
40
|
+
* unauthenticatedPath="/auth"
|
|
41
|
+
* />
|
|
42
|
+
* );
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function RedirectPage({
|
|
47
|
+
authenticatedPath = '/private',
|
|
48
|
+
unauthenticatedPath = '/auth',
|
|
49
|
+
loadingText = 'Loading...',
|
|
50
|
+
}: RedirectPageProps) {
|
|
51
|
+
const { isAuthenticated } = useAuth();
|
|
52
|
+
const router = useRouter();
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!isAuthenticated) {
|
|
56
|
+
router.push(unauthenticatedPath);
|
|
57
|
+
} else {
|
|
58
|
+
router.push(authenticatedPath);
|
|
59
|
+
}
|
|
60
|
+
}, [isAuthenticated, router, authenticatedPath, unauthenticatedPath]);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Preloader
|
|
64
|
+
variant="fullscreen"
|
|
65
|
+
text={loadingText}
|
|
66
|
+
size="lg"
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core components exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { JsonLd } from './JsonLd';
|
|
6
|
+
export { LucideIcon } from './LucideIcon';
|
|
7
|
+
export type { LucideIconProps } from './LucideIcon';
|
|
8
|
+
export { PageProgress } from './PageProgress';
|
|
9
|
+
export { Suspense } from './Suspense';
|
|
10
|
+
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ErrorLayout - Universal Error Display
|
|
3
|
+
*
|
|
4
|
+
* Minimalist error page with customizable content
|
|
5
|
+
* Works with Next.js error pages (404.tsx, 500.tsx, error.tsx)
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // app/not-found.tsx
|
|
10
|
+
* import { ErrorLayout } from '@djangocfg/layouts/components';
|
|
11
|
+
*
|
|
12
|
+
* export default function NotFound() {
|
|
13
|
+
* return <ErrorLayout code={404} supportEmail={settings.contact.email} />;
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use client';
|
|
19
|
+
|
|
20
|
+
import React from 'react';
|
|
21
|
+
import { Button } from '@djangocfg/ui/components';
|
|
22
|
+
import { getErrorContent } from './errorConfig';
|
|
23
|
+
|
|
24
|
+
export interface ErrorLayoutProps {
|
|
25
|
+
/** Error code (e.g., "404", "500", "403") - if provided, auto-configures title/description/icon */
|
|
26
|
+
code?: string | number;
|
|
27
|
+
/** Error title (auto-generated from code if not provided) */
|
|
28
|
+
title?: string;
|
|
29
|
+
/** Error description (auto-generated from code if not provided) */
|
|
30
|
+
description?: string;
|
|
31
|
+
/** Custom action buttons */
|
|
32
|
+
actions?: React.ReactNode;
|
|
33
|
+
/** Show default actions (back, home) */
|
|
34
|
+
showDefaultActions?: boolean;
|
|
35
|
+
/** Custom illustration/icon (auto-generated from code if not provided) */
|
|
36
|
+
illustration?: React.ReactNode;
|
|
37
|
+
/** Support email for contact link */
|
|
38
|
+
supportEmail?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Local function to select the icon based on the code.
|
|
42
|
+
// This is safe as it's defined and used inside a Client Component.
|
|
43
|
+
function getErrorIcon(code?: string | number): React.ReactNode {
|
|
44
|
+
const c = code ? String(code) : '';
|
|
45
|
+
|
|
46
|
+
// NOTE: You can replace these SVG paths with imported Lucid Icons
|
|
47
|
+
// (e.g., <AlertTriangle />) if you prefer.
|
|
48
|
+
switch (c) {
|
|
49
|
+
case '404':
|
|
50
|
+
return (
|
|
51
|
+
<svg
|
|
52
|
+
className="w-24 h-24 mx-auto text-muted-foreground/50"
|
|
53
|
+
fill="none"
|
|
54
|
+
stroke="currentColor"
|
|
55
|
+
viewBox="0 0 24 24"
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
>
|
|
58
|
+
{/* Missing Page Icon */}
|
|
59
|
+
<path
|
|
60
|
+
strokeLinecap="round"
|
|
61
|
+
strokeLinejoin="round"
|
|
62
|
+
strokeWidth={1.5}
|
|
63
|
+
d="M9.343 3.07a7.227 7.227 0 0111.558 0c.806.515 1.393 1.39 1.393 2.37v6.636c0 .98-.587 1.855-1.393 2.37a7.227 7.227 0 01-11.558 0c-.806-.515-1.393-1.39-1.393-2.37V5.44c0-.98.587-1.855 1.393-2.37zM12 13a1 1 0 100-2 1 1 0 000 2z"
|
|
64
|
+
/>
|
|
65
|
+
</svg>
|
|
66
|
+
);
|
|
67
|
+
case '500':
|
|
68
|
+
return (
|
|
69
|
+
<svg
|
|
70
|
+
className="w-24 h-24 mx-auto text-muted-foreground/50"
|
|
71
|
+
fill="none"
|
|
72
|
+
stroke="currentColor"
|
|
73
|
+
viewBox="0 0 24 24"
|
|
74
|
+
aria-hidden="true"
|
|
75
|
+
>
|
|
76
|
+
{/* Server Error Icon */}
|
|
77
|
+
<path
|
|
78
|
+
strokeLinecap="round"
|
|
79
|
+
strokeLinejoin="round"
|
|
80
|
+
strokeWidth={1.5}
|
|
81
|
+
d="M18.364 18.364A9 9 0 005.636 5.636m12.728 0l-6.849 6.849m0 0l-6.849-6.849m6.849 6.849V21m0 0h7.5M12 21v-7.5M7.5 21H3m7.5 0h7.5M3 18V9a4.5 4.5 0 014.5-4.5h9A4.5 4.5 0 0121 9v9a4.5 4.5 0 01-4.5 4.5h-9A4.5 4.5 0 013 18z"
|
|
82
|
+
/>
|
|
83
|
+
</svg>
|
|
84
|
+
);
|
|
85
|
+
case '403':
|
|
86
|
+
return (
|
|
87
|
+
<svg
|
|
88
|
+
className="w-24 h-24 mx-auto text-muted-foreground/50"
|
|
89
|
+
fill="none"
|
|
90
|
+
stroke="currentColor"
|
|
91
|
+
viewBox="0 0 24 24"
|
|
92
|
+
aria-hidden="true"
|
|
93
|
+
>
|
|
94
|
+
{/* Forbidden Icon */}
|
|
95
|
+
<path
|
|
96
|
+
strokeLinecap="round"
|
|
97
|
+
strokeLinejoin="round"
|
|
98
|
+
strokeWidth={1.5}
|
|
99
|
+
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
|
100
|
+
/>
|
|
101
|
+
</svg>
|
|
102
|
+
);
|
|
103
|
+
default:
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* ErrorLayout Component
|
|
110
|
+
*/
|
|
111
|
+
export function ErrorLayout({
|
|
112
|
+
code,
|
|
113
|
+
title,
|
|
114
|
+
description,
|
|
115
|
+
actions,
|
|
116
|
+
showDefaultActions = true,
|
|
117
|
+
illustration,
|
|
118
|
+
supportEmail = 'support@example.com',
|
|
119
|
+
}: ErrorLayoutProps) {
|
|
120
|
+
|
|
121
|
+
// Get content (Title/Description) from config. Note: Illustration check removed.
|
|
122
|
+
// The function getErrorContent MUST NOT return React components/functions.
|
|
123
|
+
const autoContent = code && (!title || !description)
|
|
124
|
+
? getErrorContent(code)
|
|
125
|
+
: null;
|
|
126
|
+
|
|
127
|
+
// Fallback to auto-generated values
|
|
128
|
+
const finalTitle = title || autoContent?.title || 'Error';
|
|
129
|
+
const finalDescription = description || autoContent?.description;
|
|
130
|
+
|
|
131
|
+
// ILLUSTRATION FIX: Use passed prop OR compute the icon locally using getErrorIcon.
|
|
132
|
+
const finalIllustration = illustration ?? getErrorIcon(code);
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
const handleGoBack = () => {
|
|
136
|
+
if (document.referrer && document.referrer !== window.location.href) {
|
|
137
|
+
window.location.href = document.referrer;
|
|
138
|
+
} else if (window.history.length > 1) {
|
|
139
|
+
window.history.back();
|
|
140
|
+
} else {
|
|
141
|
+
window.location.href = '/';
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const handleGoHome = () => {
|
|
146
|
+
window.location.href = '/';
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div className="min-h-screen flex items-center justify-center px-4 bg-background">
|
|
151
|
+
<div className="max-w-2xl w-full text-center space-y-8">
|
|
152
|
+
{/* Error Code */}
|
|
153
|
+
{code && (
|
|
154
|
+
<div className="relative">
|
|
155
|
+
<h1
|
|
156
|
+
className="text-[12rem] font-bold leading-none text-muted/20 select-none"
|
|
157
|
+
aria-hidden="true"
|
|
158
|
+
>
|
|
159
|
+
{code}
|
|
160
|
+
</h1>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{/* Illustration */}
|
|
165
|
+
{finalIllustration && (
|
|
166
|
+
<div className="flex justify-center py-8">
|
|
167
|
+
{finalIllustration}
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
{/* Error Content */}
|
|
172
|
+
<div className="space-y-4">
|
|
173
|
+
<h2 className="text-4xl font-bold text-foreground">
|
|
174
|
+
{finalTitle}
|
|
175
|
+
</h2>
|
|
176
|
+
|
|
177
|
+
{finalDescription && (
|
|
178
|
+
<p className="text-lg text-muted-foreground max-w-md mx-auto">
|
|
179
|
+
{finalDescription}
|
|
180
|
+
</p>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{/* Actions */}
|
|
185
|
+
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 pt-4">
|
|
186
|
+
{/* Custom actions */}
|
|
187
|
+
{actions}
|
|
188
|
+
|
|
189
|
+
{/* Default actions */}
|
|
190
|
+
{showDefaultActions && !actions && (
|
|
191
|
+
<>
|
|
192
|
+
<Button
|
|
193
|
+
variant="outline"
|
|
194
|
+
size="lg"
|
|
195
|
+
onClick={handleGoBack}
|
|
196
|
+
style={{ minWidth: '140px', padding: '12px 32px' }}
|
|
197
|
+
>
|
|
198
|
+
Go Back
|
|
199
|
+
</Button>
|
|
200
|
+
<Button
|
|
201
|
+
variant="default"
|
|
202
|
+
size="lg"
|
|
203
|
+
onClick={handleGoHome}
|
|
204
|
+
style={{ minWidth: '140px', padding: '12px 32px' }}
|
|
205
|
+
>
|
|
206
|
+
Go Home
|
|
207
|
+
</Button>
|
|
208
|
+
</>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
{/* Additional Info */}
|
|
213
|
+
<div className="pt-8 text-sm text-muted-foreground">
|
|
214
|
+
<p>
|
|
215
|
+
Need help? Contact{' '}
|
|
216
|
+
<a
|
|
217
|
+
href={`mailto:${supportEmail}`}
|
|
218
|
+
className="text-primary hover:underline"
|
|
219
|
+
>
|
|
220
|
+
support
|
|
221
|
+
</a>
|
|
222
|
+
</p>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Universal Error Configuration
|
|
3
|
+
*
|
|
4
|
+
* Provides standard error content for common HTTP status codes
|
|
5
|
+
* Use this to maintain consistency across error pages
|
|
6
|
+
*
|
|
7
|
+
* NOTE: Only returns primitives (title, description) - NO React components
|
|
8
|
+
* This ensures safe serialization during prerendering
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface ErrorContent {
|
|
12
|
+
title: string;
|
|
13
|
+
description: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get standardized error content based on status code
|
|
18
|
+
*
|
|
19
|
+
* @param statusCode - HTTP status code or custom error type
|
|
20
|
+
* @returns Error content configuration (title and description only)
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const { title, description } = getErrorContent(404);
|
|
25
|
+
* <ErrorLayout title={title} description={description} code={404} />
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function getErrorContent(statusCode?: number | string): ErrorContent {
|
|
29
|
+
const code = typeof statusCode === 'string' ? parseInt(statusCode, 10) : statusCode;
|
|
30
|
+
|
|
31
|
+
switch (code) {
|
|
32
|
+
// 400 Bad Request
|
|
33
|
+
case 400:
|
|
34
|
+
return {
|
|
35
|
+
title: 'Bad Request',
|
|
36
|
+
description: 'The request could not be understood. Please check your input and try again.',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// 401 Unauthorized
|
|
40
|
+
case 401:
|
|
41
|
+
return {
|
|
42
|
+
title: 'Authentication Required',
|
|
43
|
+
description: 'You need to sign in to access this page.',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// 403 Forbidden
|
|
47
|
+
case 403:
|
|
48
|
+
return {
|
|
49
|
+
title: 'Access Denied',
|
|
50
|
+
description: "You don't have permission to access this resource.",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// 404 Not Found
|
|
54
|
+
case 404:
|
|
55
|
+
return {
|
|
56
|
+
title: 'Page Not Found',
|
|
57
|
+
description: "The page you're looking for doesn't exist or has been moved.",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// 408 Request Timeout
|
|
61
|
+
case 408:
|
|
62
|
+
return {
|
|
63
|
+
title: 'Request Timeout',
|
|
64
|
+
description: 'The request took too long to process. Please try again.',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// 500 Internal Server Error
|
|
68
|
+
case 500:
|
|
69
|
+
return {
|
|
70
|
+
title: 'Server Error',
|
|
71
|
+
description: "Something went wrong on our end. We're working to fix it.",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// 502 Bad Gateway
|
|
75
|
+
case 502:
|
|
76
|
+
return {
|
|
77
|
+
title: 'Bad Gateway',
|
|
78
|
+
description: 'The server received an invalid response. Please try again later.',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// 503 Service Unavailable
|
|
82
|
+
case 503:
|
|
83
|
+
return {
|
|
84
|
+
title: 'Service Unavailable',
|
|
85
|
+
description: 'The service is temporarily unavailable. Please try again later.',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// 504 Gateway Timeout
|
|
89
|
+
case 504:
|
|
90
|
+
return {
|
|
91
|
+
title: 'Gateway Timeout',
|
|
92
|
+
description: 'The server took too long to respond. Please try again.',
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Default / Unknown Error
|
|
96
|
+
default:
|
|
97
|
+
return {
|
|
98
|
+
title: 'Something Went Wrong',
|
|
99
|
+
description: 'An unexpected error occurred. Please try again or contact support.',
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Common error codes as constants
|
|
106
|
+
*/
|
|
107
|
+
export const ERROR_CODES = {
|
|
108
|
+
BAD_REQUEST: 400,
|
|
109
|
+
UNAUTHORIZED: 401,
|
|
110
|
+
FORBIDDEN: 403,
|
|
111
|
+
NOT_FOUND: 404,
|
|
112
|
+
TIMEOUT: 408,
|
|
113
|
+
SERVER_ERROR: 500,
|
|
114
|
+
BAD_GATEWAY: 502,
|
|
115
|
+
SERVICE_UNAVAILABLE: 503,
|
|
116
|
+
GATEWAY_TIMEOUT: 504,
|
|
117
|
+
} as const;
|
|
118
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error components exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { ErrorBoundary } from './ErrorBoundary';
|
|
6
|
+
export { ErrorLayout } from './ErrorLayout';
|
|
7
|
+
export type { ErrorLayoutProps } from './ErrorLayout';
|
|
8
|
+
export { getErrorContent, ERROR_CODES } from './errorConfig';
|
|
9
|
+
export type { ErrorContent } from './errorConfig';
|
|
10
|
+
|