@djangocfg/layouts 2.0.4 → 2.0.5
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 +15 -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/index.ts +5 -0
- package/src/layouts/AppLayout/AppLayout.tsx +4 -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/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.5",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -64,6 +64,16 @@
|
|
|
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
|
+
},
|
|
67
77
|
"./styles": "./src/styles/index.css",
|
|
68
78
|
"./styles/dashboard": "./src/styles/dashboard.css"
|
|
69
79
|
},
|
|
@@ -77,9 +87,9 @@
|
|
|
77
87
|
"check": "tsc --noEmit"
|
|
78
88
|
},
|
|
79
89
|
"peerDependencies": {
|
|
80
|
-
"@djangocfg/api": "^1.4.
|
|
81
|
-
"@djangocfg/centrifugo": "^1.4.
|
|
82
|
-
"@djangocfg/ui": "^1.4.
|
|
90
|
+
"@djangocfg/api": "^1.4.35",
|
|
91
|
+
"@djangocfg/centrifugo": "^1.4.35",
|
|
92
|
+
"@djangocfg/ui": "^1.4.35",
|
|
83
93
|
"@hookform/resolvers": "^5.2.0",
|
|
84
94
|
"consola": "^3.4.2",
|
|
85
95
|
"lucide-react": "^0.468.0",
|
|
@@ -98,7 +108,7 @@
|
|
|
98
108
|
"react-ga4": "^2.1.0"
|
|
99
109
|
},
|
|
100
110
|
"devDependencies": {
|
|
101
|
-
"@djangocfg/typescript-config": "^1.4.
|
|
111
|
+
"@djangocfg/typescript-config": "^1.4.35",
|
|
102
112
|
"@types/node": "^24.7.2",
|
|
103
113
|
"@types/react": "19.2.2",
|
|
104
114
|
"@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
|
+
|
package/src/components/index.ts
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
* Components exports
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
export
|
|
7
|
-
export type { LucideIconProps } from './LucideIcon';
|
|
8
|
-
export { PageProgress } from './PageProgress';
|
|
5
|
+
// Core components
|
|
6
|
+
export * from './core';
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
// Error components
|
|
9
|
+
export * from './errors';
|
|
10
|
+
|
|
11
|
+
// Redirect component
|
|
12
|
+
export * from './RedirectPage';
|
|
13
|
+
|
|
14
|
+
// ErrorsTracker
|
|
15
|
+
export * from './errors/ErrorsTracker';
|
|
16
|
+
|
|
17
|
+
// UpdateNotifier
|
|
18
|
+
export * from './UpdateNotifier';
|
package/src/index.ts
CHANGED
|
@@ -40,13 +40,13 @@ import React, { ReactNode, useMemo } from 'react';
|
|
|
40
40
|
import { usePathname } from 'next/navigation';
|
|
41
41
|
import { ThemeProvider, Toaster } from '@djangocfg/ui';
|
|
42
42
|
import { CentrifugoProvider } from '@djangocfg/centrifugo';
|
|
43
|
-
import { ErrorBoundary } from '../../components/ErrorBoundary';
|
|
43
|
+
import { ErrorBoundary } from '../../components/errors/ErrorBoundary';
|
|
44
44
|
import { AuthProvider, type AuthConfig } from '../../auth/context';
|
|
45
|
-
import { ErrorTrackingProvider, type ValidationErrorConfig, type CORSErrorConfig, type NetworkErrorConfig } from '../../components/ErrorsTracker';
|
|
45
|
+
import { ErrorTrackingProvider, type ValidationErrorConfig, type CORSErrorConfig, type NetworkErrorConfig } from '../../components/errors/ErrorsTracker';
|
|
46
46
|
import { AnalyticsProvider } from '../../snippets/Analytics';
|
|
47
|
-
import { PageProgress } from '../../components/PageProgress';
|
|
47
|
+
import { PageProgress } from '../../components/core/PageProgress';
|
|
48
48
|
import { UpdateNotifier } from '../../components/UpdateNotifier';
|
|
49
|
-
import { Suspense } from '../../components';
|
|
49
|
+
import { Suspense } from '../../components/core';
|
|
50
50
|
|
|
51
51
|
export type LayoutMode = 'public' | 'private' | 'admin';
|
|
52
52
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LegalPage Component
|
|
3
|
+
*
|
|
4
|
+
* Reusable component for legal pages (Privacy, Terms, Cookies, Security)
|
|
5
|
+
* Accepts configuration for flexible content rendering
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { LegalPage } from '@djangocfg/layouts/components/legal';
|
|
10
|
+
* import { privacyConfig } from './config';
|
|
11
|
+
*
|
|
12
|
+
* export default function PrivacyPage() {
|
|
13
|
+
* return <LegalPage config={privacyConfig} />;
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use client';
|
|
19
|
+
|
|
20
|
+
import React from 'react';
|
|
21
|
+
import {
|
|
22
|
+
Card,
|
|
23
|
+
CardHeader,
|
|
24
|
+
CardTitle,
|
|
25
|
+
CardContent,
|
|
26
|
+
} from '@djangocfg/ui/components';
|
|
27
|
+
import type { LegalPageConfig } from './types';
|
|
28
|
+
|
|
29
|
+
export interface LegalPageProps {
|
|
30
|
+
config: LegalPageConfig;
|
|
31
|
+
className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Format date for display
|
|
36
|
+
*/
|
|
37
|
+
function formatDate(date: string | Date | undefined): string {
|
|
38
|
+
if (!date) return '';
|
|
39
|
+
if (typeof date === 'string') return date;
|
|
40
|
+
return date.toISOString().split('T')[0];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* LegalPage Component
|
|
45
|
+
*/
|
|
46
|
+
export function LegalPage({ config, className }: LegalPageProps) {
|
|
47
|
+
const { title, lastUpdated, sections } = config;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className={`container mx-auto max-w-4xl py-16 px-4 ${className || ''}`}>
|
|
51
|
+
<div className="flex flex-col gap-8">
|
|
52
|
+
{/* Header */}
|
|
53
|
+
<div className="flex flex-col gap-4">
|
|
54
|
+
<h1 className="text-4xl font-bold">{title}</h1>
|
|
55
|
+
{lastUpdated && (
|
|
56
|
+
<p className="text-lg text-muted-foreground">
|
|
57
|
+
Last updated: {formatDate(lastUpdated)}
|
|
58
|
+
</p>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{/* Sections */}
|
|
63
|
+
{sections.map((section, index) => (
|
|
64
|
+
<Card key={index}>
|
|
65
|
+
<CardHeader>
|
|
66
|
+
<CardTitle>{section.title}</CardTitle>
|
|
67
|
+
</CardHeader>
|
|
68
|
+
<CardContent className="text-muted-foreground">
|
|
69
|
+
{Array.isArray(section.content) ? (
|
|
70
|
+
<div className="flex flex-col gap-2">
|
|
71
|
+
{section.content.map((paragraph, pIndex) => (
|
|
72
|
+
<p key={pIndex}>{paragraph}</p>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
) : (
|
|
76
|
+
<p>{section.content}</p>
|
|
77
|
+
)}
|
|
78
|
+
</CardContent>
|
|
79
|
+
</Card>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default legal page configurations
|
|
3
|
+
*
|
|
4
|
+
* Pre-configured content for common legal pages
|
|
5
|
+
* Can be customized or extended as needed
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { LegalPageConfig } from './types';
|
|
9
|
+
|
|
10
|
+
export const privacyConfig: LegalPageConfig = {
|
|
11
|
+
title: 'Privacy Policy',
|
|
12
|
+
sections: [
|
|
13
|
+
{
|
|
14
|
+
title: 'Information We Collect',
|
|
15
|
+
content: 'We collect information that you provide directly to us, including when you create an account, use our services, or communicate with us.',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
title: 'How We Use Your Information',
|
|
19
|
+
content: [
|
|
20
|
+
'We use the information we collect to:',
|
|
21
|
+
'Provide, maintain, and improve our services',
|
|
22
|
+
'Process transactions and send related information',
|
|
23
|
+
'Send technical notices and support messages',
|
|
24
|
+
'Respond to your comments and questions',
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
title: 'Information Sharing',
|
|
29
|
+
content: 'We do not share your personal information with third parties except as described in this privacy policy or with your consent.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
title: 'Data Security',
|
|
33
|
+
content: 'We take reasonable measures to help protect your personal information from loss, theft, misuse, unauthorized access, disclosure, alteration, and destruction.',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
title: 'Your Rights',
|
|
37
|
+
content: 'You have the right to access, update, or delete your personal information at any time. Contact us if you wish to exercise these rights.',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
title: 'Changes to This Policy',
|
|
41
|
+
content: 'We may update this privacy policy from time to time. We will notify you of any changes by posting the new policy on this page.',
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const termsConfig: LegalPageConfig = {
|
|
47
|
+
title: 'Terms of Service',
|
|
48
|
+
sections: [
|
|
49
|
+
{
|
|
50
|
+
title: '1. Acceptance of Terms',
|
|
51
|
+
content: 'By accessing and using this service, you accept and agree to be bound by the terms and provision of this agreement.',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
title: '2. Use License',
|
|
55
|
+
content: 'Permission is granted to temporarily download one copy of the materials on our service for personal, non-commercial transitory viewing only.',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
title: '3. Disclaimer',
|
|
59
|
+
content: 'The materials on our service are provided on an \'as is\' basis. We make no warranties, expressed or implied, and hereby disclaim and negate all other warranties including, without limitation, implied warranties or conditions of merchantability, fitness for a particular purpose, or non-infringement of intellectual property or other violation of rights.',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
title: '4. Limitations',
|
|
63
|
+
content: 'In no event shall we or our suppliers be liable for any damages (including, without limitation, damages for loss of data or profit, or due to business interruption) arising out of the use or inability to use the materials on our service.',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
title: '5. Revisions',
|
|
67
|
+
content: 'We may revise these terms of service at any time without notice. By using this service you are agreeing to be bound by the then current version of these terms of service.',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const cookiesConfig: LegalPageConfig = {
|
|
73
|
+
title: 'Cookie Policy',
|
|
74
|
+
sections: [
|
|
75
|
+
{
|
|
76
|
+
title: 'What Are Cookies?',
|
|
77
|
+
content: 'Cookies are small text files that are placed on your device when you visit our website. They help us provide you with a better experience by remembering your preferences and understanding how you use our site.',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
title: 'Types of Cookies We Use',
|
|
81
|
+
content: [
|
|
82
|
+
'Essential Cookies: These cookies are necessary for the website to function properly.',
|
|
83
|
+
'Performance Cookies: These cookies help us understand how visitors interact with our website.',
|
|
84
|
+
'Functionality Cookies: These cookies allow us to remember choices you make and provide enhanced features.',
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
title: 'Managing Cookies',
|
|
89
|
+
content: 'Most web browsers allow you to control cookies through their settings. You can set your browser to refuse cookies or delete certain cookies. However, please note that if you disable cookies, some features of our website may not function properly.',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
title: 'Third-Party Cookies',
|
|
93
|
+
content: 'We may use third-party services that also use cookies. These third parties have their own privacy policies, and we do not accept any responsibility or liability for their policies.',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
title: 'Updates to This Policy',
|
|
97
|
+
content: 'We may update our Cookie Policy from time to time. Any changes will be posted on this page with an updated revision date.',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const securityConfig: LegalPageConfig = {
|
|
103
|
+
title: 'Security Policy',
|
|
104
|
+
sections: [
|
|
105
|
+
{
|
|
106
|
+
title: 'Our Commitment to Security',
|
|
107
|
+
content: 'We take the security of your data seriously. We implement industry-standard security measures to protect your personal information from unauthorized access, disclosure, alteration, and destruction.',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
title: 'Data Encryption',
|
|
111
|
+
content: 'All data transmitted between your device and our servers is encrypted using industry-standard SSL/TLS protocols. Sensitive data stored in our databases is encrypted at rest using advanced encryption standards.',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
title: 'Access Controls',
|
|
115
|
+
content: 'Access to user data is strictly limited to authorized personnel who require it to perform their job functions. We employ multi-factor authentication and regular access reviews to ensure that only authorized individuals can access sensitive information.',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
title: 'Security Monitoring',
|
|
119
|
+
content: 'We continuously monitor our systems for potential security threats and vulnerabilities. Our security team actively tracks and responds to any suspicious activity or potential security incidents.',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
title: 'Reporting Security Issues',
|
|
123
|
+
content: 'If you discover a security vulnerability or have concerns about our security practices, please report it to us immediately. We appreciate responsible disclosure and will work with you to address any legitimate security concerns.',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
title: 'Regular Security Updates',
|
|
127
|
+
content: 'We regularly update our systems and software to ensure that known security vulnerabilities are patched promptly. Our infrastructure undergoes periodic security audits and penetration testing to identify and address potential weaknesses.',
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legal pages exports
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { LegalPage } from './LegalPage';
|
|
6
|
+
export type { LegalPageProps } from './LegalPage';
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
PrivacyPage,
|
|
10
|
+
TermsPage,
|
|
11
|
+
CookiesPage,
|
|
12
|
+
SecurityPage,
|
|
13
|
+
} from './pages';
|
|
14
|
+
export type { LegalPageComponentProps } from './pages';
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
privacyConfig,
|
|
18
|
+
termsConfig,
|
|
19
|
+
cookiesConfig,
|
|
20
|
+
securityConfig,
|
|
21
|
+
} from './configs';
|
|
22
|
+
|
|
23
|
+
export type { LegalPageConfig, LegalPageSection } from './types';
|
|
24
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-built legal page components
|
|
3
|
+
*
|
|
4
|
+
* Ready-to-use page components with default configurations
|
|
5
|
+
* Can be customized by passing custom config
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```tsx
|
|
9
|
+
* // app/legal/privacy/page.tsx
|
|
10
|
+
* import { PrivacyPage } from '@djangocfg/layouts/components/legal';
|
|
11
|
+
*
|
|
12
|
+
* export default PrivacyPage;
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use client';
|
|
17
|
+
|
|
18
|
+
import { LegalPage } from './LegalPage';
|
|
19
|
+
import {
|
|
20
|
+
privacyConfig,
|
|
21
|
+
termsConfig,
|
|
22
|
+
cookiesConfig,
|
|
23
|
+
securityConfig,
|
|
24
|
+
} from './configs';
|
|
25
|
+
import type { LegalPageConfig } from './types';
|
|
26
|
+
|
|
27
|
+
export interface LegalPageComponentProps {
|
|
28
|
+
config?: LegalPageConfig;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Privacy Policy Page
|
|
33
|
+
*/
|
|
34
|
+
export function PrivacyPage({ config }: LegalPageComponentProps = {}) {
|
|
35
|
+
return <LegalPage config={config || privacyConfig} />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Terms of Service Page
|
|
40
|
+
*/
|
|
41
|
+
export function TermsPage({ config }: LegalPageComponentProps = {}) {
|
|
42
|
+
return <LegalPage config={config || termsConfig} />;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Cookie Policy Page
|
|
47
|
+
*/
|
|
48
|
+
export function CookiesPage({ config }: LegalPageComponentProps = {}) {
|
|
49
|
+
return <LegalPage config={config || cookiesConfig} />;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Security Policy Page
|
|
54
|
+
*/
|
|
55
|
+
export function SecurityPage({ config }: LegalPageComponentProps = {}) {
|
|
56
|
+
return <LegalPage config={config || securityConfig} />;
|
|
57
|
+
}
|
|
58
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legal page types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface LegalPageSection {
|
|
6
|
+
title: string;
|
|
7
|
+
content: string | string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface LegalPageConfig {
|
|
11
|
+
title: string;
|
|
12
|
+
lastUpdated?: string | Date;
|
|
13
|
+
sections: LegalPageSection[];
|
|
14
|
+
}
|
|
15
|
+
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OG Image URL Generation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Client-side utilities for generating OG image URLs.
|
|
5
|
+
* Moved from @djangocfg/nextjs to keep nextjs package server-only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Encode string to base64 with Unicode support
|
|
10
|
+
* Works in both browser and Node.js environments
|
|
11
|
+
*/
|
|
12
|
+
function encodeBase64(str: string): string {
|
|
13
|
+
// Node.js environment
|
|
14
|
+
if (typeof Buffer !== 'undefined') {
|
|
15
|
+
return Buffer.from(str, 'utf-8').toString('base64');
|
|
16
|
+
}
|
|
17
|
+
// Browser environment - handle Unicode via UTF-8 encoding
|
|
18
|
+
return btoa(unescape(encodeURIComponent(str)));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* OG Image URL parameters
|
|
23
|
+
* All parameters can be encoded in URL via base64
|
|
24
|
+
*/
|
|
25
|
+
export interface OgImageUrlParams {
|
|
26
|
+
/** Page title */
|
|
27
|
+
title: string;
|
|
28
|
+
/** Page description (optional) */
|
|
29
|
+
description?: string;
|
|
30
|
+
/** Site name (optional) */
|
|
31
|
+
siteName?: string;
|
|
32
|
+
/** Logo URL (optional) */
|
|
33
|
+
logo?: string;
|
|
34
|
+
/** Background type: 'gradient' or 'solid' */
|
|
35
|
+
backgroundType?: 'gradient' | 'solid';
|
|
36
|
+
/** Gradient start color (hex) */
|
|
37
|
+
gradientStart?: string;
|
|
38
|
+
/** Gradient end color (hex) */
|
|
39
|
+
gradientEnd?: string;
|
|
40
|
+
/** Background color (for solid type) */
|
|
41
|
+
backgroundColor?: string;
|
|
42
|
+
/** Title font size (px) */
|
|
43
|
+
titleSize?: number;
|
|
44
|
+
/** Title font weight */
|
|
45
|
+
titleWeight?: number;
|
|
46
|
+
/** Title text color */
|
|
47
|
+
titleColor?: string;
|
|
48
|
+
/** Description font size (px) */
|
|
49
|
+
descriptionSize?: number;
|
|
50
|
+
/** Description text color */
|
|
51
|
+
descriptionColor?: string;
|
|
52
|
+
/** Site name font size (px) */
|
|
53
|
+
siteNameSize?: number;
|
|
54
|
+
/** Site name text color */
|
|
55
|
+
siteNameColor?: string;
|
|
56
|
+
/** Padding (px) */
|
|
57
|
+
padding?: number;
|
|
58
|
+
/** Logo size (px) */
|
|
59
|
+
logoSize?: number;
|
|
60
|
+
/** Show logo flag */
|
|
61
|
+
showLogo?: boolean;
|
|
62
|
+
/** Show site name flag */
|
|
63
|
+
showSiteName?: boolean;
|
|
64
|
+
/** Additional custom parameters */
|
|
65
|
+
[key: string]: string | number | boolean | undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Generate OG image URL with query parameters or base64 encoding
|
|
70
|
+
*
|
|
71
|
+
* @param baseUrl - Base URL of the OG image API route (e.g., '/api/og' or 'https://example.com/api/og')
|
|
72
|
+
* @param params - URL parameters for the OG image
|
|
73
|
+
* @param useBase64 - If true, encode params as base64 for safer URLs (default: true)
|
|
74
|
+
* @returns Complete OG image URL with encoded parameters
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* // Base64 encoding (safe, default) - all parameters can be encoded
|
|
79
|
+
* const url = generateOgImageUrl('/api/og', {
|
|
80
|
+
* title: 'My Page Title',
|
|
81
|
+
* description: 'Page description here',
|
|
82
|
+
* siteName: 'My Site',
|
|
83
|
+
* });
|
|
84
|
+
* // Result: /api/og/[base64-encoded-json]
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function generateOgImageUrl(
|
|
88
|
+
baseUrl: string,
|
|
89
|
+
params: OgImageUrlParams,
|
|
90
|
+
useBase64: boolean = true
|
|
91
|
+
): string {
|
|
92
|
+
if (useBase64) {
|
|
93
|
+
// Clean params - remove undefined/null/empty values
|
|
94
|
+
const cleanParams: Record<string, string | number | boolean> = {};
|
|
95
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
96
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
97
|
+
cleanParams[key] = value;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Encode as base64 (Unicode-safe)
|
|
102
|
+
const jsonString = JSON.stringify(cleanParams);
|
|
103
|
+
const base64Data = encodeBase64(jsonString);
|
|
104
|
+
|
|
105
|
+
// CRITICAL: Use path parameter instead of query parameter
|
|
106
|
+
// Next.js strips query params in internal requests for metadata generation
|
|
107
|
+
// Using /api/og/[data] instead of /api/og?data=... preserves the data
|
|
108
|
+
return `${baseUrl}/${base64Data}`;
|
|
109
|
+
} else {
|
|
110
|
+
// Legacy query params mode
|
|
111
|
+
const searchParams = new URLSearchParams();
|
|
112
|
+
|
|
113
|
+
// Add all defined parameters
|
|
114
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
115
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
116
|
+
searchParams.append(key, String(value));
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const query = searchParams.toString();
|
|
121
|
+
return query ? `${baseUrl}?${query}` : baseUrl;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get absolute OG image URL from relative path
|
|
127
|
+
*
|
|
128
|
+
* Useful for generating absolute URLs required by Open Graph meta tags
|
|
129
|
+
*
|
|
130
|
+
* @param relativePath - Relative OG image path (e.g., '/api/og?title=Hello')
|
|
131
|
+
* @param siteUrl - Base site URL (e.g., 'https://example.com')
|
|
132
|
+
* @returns Absolute URL
|
|
133
|
+
*/
|
|
134
|
+
export function getAbsoluteOgImageUrl(
|
|
135
|
+
relativePath: string,
|
|
136
|
+
siteUrl: string
|
|
137
|
+
): string {
|
|
138
|
+
// Remove trailing slash from site URL
|
|
139
|
+
const cleanSiteUrl = siteUrl.replace(/\/$/, '');
|
|
140
|
+
|
|
141
|
+
// Ensure relative path starts with /
|
|
142
|
+
const cleanPath = relativePath.startsWith('/')
|
|
143
|
+
? relativePath
|
|
144
|
+
: `/${relativePath}`;
|
|
145
|
+
|
|
146
|
+
return `${cleanSiteUrl}${cleanPath}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Create OG image URL builder with preset configuration
|
|
151
|
+
*
|
|
152
|
+
* Useful when you want to reuse the same base URL and default parameters
|
|
153
|
+
*
|
|
154
|
+
* @param baseUrl - Base URL of the OG image API route
|
|
155
|
+
* @param defaults - Default parameters to merge with each URL generation
|
|
156
|
+
* @returns URL builder function
|
|
157
|
+
*/
|
|
158
|
+
export function createOgImageUrlBuilder(
|
|
159
|
+
baseUrl: string,
|
|
160
|
+
defaults: Partial<OgImageUrlParams> = {}
|
|
161
|
+
) {
|
|
162
|
+
return (params: OgImageUrlParams): string => {
|
|
163
|
+
return generateOgImageUrl(baseUrl, {
|
|
164
|
+
...defaults,
|
|
165
|
+
...params,
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/src/components/{ErrorsTracker → errors/ErrorsTracker}/providers/ErrorTrackingProvider.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|