@djangocfg/layouts 2.1.110 → 2.1.112
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 +69 -0
- package/package.json +14 -12
- package/src/components/errors/ErrorBoundary.tsx +12 -6
- package/src/components/errors/ErrorLayout.tsx +19 -9
- package/src/components/errors/errorConfig.ts +28 -22
- package/src/layouts/AppLayout/AppLayout.tsx +42 -17
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +8 -1
- package/src/layouts/PrivateLayout/components/PrivateHeader.tsx +14 -1
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +128 -56
- package/src/layouts/PublicLayout/PublicLayout.tsx +6 -0
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +12 -4
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +20 -2
- package/src/layouts/_components/LocaleSwitcher.tsx +114 -0
- package/src/layouts/_components/UserMenu.tsx +14 -6
- package/src/layouts/_components/index.ts +3 -0
- package/src/layouts/index.ts +2 -2
- package/src/snippets/AuthDialog/AuthDialog.tsx +15 -6
- package/src/snippets/Breadcrumbs.tsx +19 -8
- package/src/snippets/McpChat/components/ChatPanel.tsx +16 -6
- package/src/snippets/McpChat/components/ChatSidebar.tsx +20 -8
- package/src/snippets/PWAInstall/components/A2HSHint.tsx +23 -10
- package/src/snippets/PWAInstall/components/DesktopGuide.tsx +44 -32
- package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +34 -25
- package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +34 -25
- package/src/snippets/PushNotifications/components/PushPrompt.tsx +16 -6
package/README.md
CHANGED
|
@@ -168,6 +168,75 @@ import { PublicLayout, PrivateLayout, AuthLayout } from '@djangocfg/layouts';
|
|
|
168
168
|
| `AdminLayout` | Admin panel layout |
|
|
169
169
|
| `ProfileLayout` | User profile pages |
|
|
170
170
|
|
|
171
|
+
### i18n Support
|
|
172
|
+
|
|
173
|
+
Pass `i18n` config to AppLayout for locale switching in all layouts:
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
import { AppLayout } from '@djangocfg/layouts';
|
|
177
|
+
import { useLocaleSwitcher } from '@djangocfg/nextjs/i18n/client';
|
|
178
|
+
|
|
179
|
+
function RootLayout({ children }) {
|
|
180
|
+
const { locale, locales, changeLocale } = useLocaleSwitcher();
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<AppLayout
|
|
184
|
+
// ... other configs
|
|
185
|
+
i18n={{
|
|
186
|
+
locale,
|
|
187
|
+
locales,
|
|
188
|
+
onLocaleChange: changeLocale,
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
{children}
|
|
192
|
+
</AppLayout>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The `LocaleSwitcher` component will automatically appear in PublicLayout and PrivateLayout headers.
|
|
198
|
+
|
|
199
|
+
### LocaleSwitcher Component
|
|
200
|
+
|
|
201
|
+
A presentational locale switcher component that can be used standalone:
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
import { LocaleSwitcher } from '@djangocfg/layouts';
|
|
205
|
+
|
|
206
|
+
// Basic usage (pass locale data via props)
|
|
207
|
+
<LocaleSwitcher
|
|
208
|
+
locale="en"
|
|
209
|
+
locales={['en', 'ru', 'ko']}
|
|
210
|
+
onChange={(locale) => router.push(`/${locale}`)}
|
|
211
|
+
/>
|
|
212
|
+
|
|
213
|
+
// With custom labels and styling
|
|
214
|
+
<LocaleSwitcher
|
|
215
|
+
locale={currentLocale}
|
|
216
|
+
locales={['en', 'ru', 'ko']}
|
|
217
|
+
onChange={handleLocaleChange}
|
|
218
|
+
labels={{ en: 'English', ru: 'Русский', ko: '한국어' }}
|
|
219
|
+
variant="outline"
|
|
220
|
+
size="sm"
|
|
221
|
+
showIcon={true}
|
|
222
|
+
/>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Props:**
|
|
226
|
+
| Prop | Type | Default | Description |
|
|
227
|
+
|------|------|---------|-------------|
|
|
228
|
+
| `locale` | `string` | - | Current locale code |
|
|
229
|
+
| `locales` | `string[]` | - | Available locale codes |
|
|
230
|
+
| `onChange` | `(locale: string) => void` | - | Callback when locale changes |
|
|
231
|
+
| `labels` | `Record<string, string>` | Built-in | Custom labels for locales |
|
|
232
|
+
| `showCode` | `boolean` | `false` | Show locale code with label |
|
|
233
|
+
| `variant` | `'ghost' \| 'outline' \| 'default'` | `'ghost'` | Button variant |
|
|
234
|
+
| `size` | `'sm' \| 'default' \| 'lg' \| 'icon'` | `'sm'` | Button size |
|
|
235
|
+
| `showIcon` | `boolean` | `true` | Show globe icon |
|
|
236
|
+
| `className` | `string` | - | Custom CSS class |
|
|
237
|
+
|
|
238
|
+
> **Smart version:** For automatic locale detection with next-intl hooks, use `@djangocfg/nextjs/i18n/components` which wraps this component.
|
|
239
|
+
|
|
171
240
|
> **Extension Layouts:** Additional layouts like `SupportLayout` and `PaymentsLayout` are available in extension packages:
|
|
172
241
|
> - `@djangocfg/ext-support` - Support ticket layouts
|
|
173
242
|
> - `@djangocfg/ext-payments` - Payment flow layouts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.112",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -74,11 +74,12 @@
|
|
|
74
74
|
"check": "tsc --noEmit"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@djangocfg/api": "^2.1.
|
|
78
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
79
|
-
"@djangocfg/
|
|
80
|
-
"@djangocfg/ui-
|
|
81
|
-
"@djangocfg/ui-
|
|
77
|
+
"@djangocfg/api": "^2.1.112",
|
|
78
|
+
"@djangocfg/centrifugo": "^2.1.112",
|
|
79
|
+
"@djangocfg/i18n": "^2.1.112",
|
|
80
|
+
"@djangocfg/ui-core": "^2.1.112",
|
|
81
|
+
"@djangocfg/ui-nextjs": "^2.1.112",
|
|
82
|
+
"@djangocfg/ui-tools": "^2.1.112",
|
|
82
83
|
"@hookform/resolvers": "^5.2.2",
|
|
83
84
|
"consola": "^3.4.2",
|
|
84
85
|
"lucide-react": "^0.545.0",
|
|
@@ -101,12 +102,13 @@
|
|
|
101
102
|
"uuid": "^11.1.0"
|
|
102
103
|
},
|
|
103
104
|
"devDependencies": {
|
|
104
|
-
"@djangocfg/api": "^2.1.
|
|
105
|
-
"@djangocfg/
|
|
106
|
-
"@djangocfg/
|
|
107
|
-
"@djangocfg/
|
|
108
|
-
"@djangocfg/ui-
|
|
109
|
-
"@djangocfg/ui-
|
|
105
|
+
"@djangocfg/api": "^2.1.112",
|
|
106
|
+
"@djangocfg/i18n": "^2.1.112",
|
|
107
|
+
"@djangocfg/centrifugo": "^2.1.112",
|
|
108
|
+
"@djangocfg/typescript-config": "^2.1.112",
|
|
109
|
+
"@djangocfg/ui-core": "^2.1.112",
|
|
110
|
+
"@djangocfg/ui-nextjs": "^2.1.112",
|
|
111
|
+
"@djangocfg/ui-tools": "^2.1.112",
|
|
110
112
|
"@types/node": "^24.7.2",
|
|
111
113
|
"@types/react": "^19.1.0",
|
|
112
114
|
"@types/react-dom": "^19.1.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Simple ErrorBoundary Component
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Catches React errors and displays a fallback UI
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
|
10
10
|
|
|
11
|
+
import { getT } from '@djangocfg/i18n';
|
|
12
|
+
|
|
11
13
|
interface ErrorBoundaryProps {
|
|
12
14
|
children: ReactNode;
|
|
13
15
|
supportEmail?: string;
|
|
@@ -34,7 +36,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
34
36
|
if (this.props.onError) {
|
|
35
37
|
this.props.onError(error, errorInfo);
|
|
36
38
|
}
|
|
37
|
-
|
|
39
|
+
|
|
38
40
|
// Log to console in development
|
|
39
41
|
if (process.env.NODE_ENV === 'development') {
|
|
40
42
|
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
@@ -43,16 +45,20 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
43
45
|
|
|
44
46
|
render() {
|
|
45
47
|
if (this.state.hasError) {
|
|
48
|
+
const title = getT('layouts.errors.somethingWentWrong');
|
|
49
|
+
const description = getT('layouts.errors.tryRefreshing');
|
|
50
|
+
const refreshButton = getT('layouts.errors.refreshPage');
|
|
51
|
+
|
|
46
52
|
return (
|
|
47
53
|
<div className="flex min-h-screen items-center justify-center bg-background p-4">
|
|
48
54
|
<div className="max-w-md w-full space-y-4 text-center">
|
|
49
|
-
<h1 className="text-2xl font-bold text-foreground">
|
|
55
|
+
<h1 className="text-2xl font-bold text-foreground">{title}</h1>
|
|
50
56
|
<p className="text-muted-foreground">
|
|
51
|
-
|
|
57
|
+
{description}
|
|
52
58
|
</p>
|
|
53
59
|
{this.props.supportEmail && (
|
|
54
60
|
<p className="text-sm text-muted-foreground">
|
|
55
|
-
|
|
61
|
+
{getT('layouts.errors.persistsContact', { email: '' })}{' '}
|
|
56
62
|
<a
|
|
57
63
|
href={`mailto:${this.props.supportEmail}`}
|
|
58
64
|
className="text-primary hover:underline"
|
|
@@ -65,7 +71,7 @@ export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundarySt
|
|
|
65
71
|
onClick={() => window.location.reload()}
|
|
66
72
|
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
|
|
67
73
|
>
|
|
68
|
-
|
|
74
|
+
{refreshButton}
|
|
69
75
|
</button>
|
|
70
76
|
</div>
|
|
71
77
|
</div>
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
|
|
18
18
|
'use client';
|
|
19
19
|
|
|
20
|
-
import React from 'react';
|
|
20
|
+
import React, { useMemo } from 'react';
|
|
21
21
|
|
|
22
22
|
import { Button } from '@djangocfg/ui-core/components';
|
|
23
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
23
24
|
|
|
24
25
|
import { getErrorContent } from './errorConfig';
|
|
25
26
|
|
|
@@ -112,17 +113,26 @@ export function ErrorLayout({
|
|
|
112
113
|
illustration,
|
|
113
114
|
supportEmail = 'support@example.com',
|
|
114
115
|
}: ErrorLayoutProps) {
|
|
115
|
-
|
|
116
|
+
const t = useTypedT<I18nTranslations>();
|
|
117
|
+
|
|
118
|
+
const labels = useMemo(() => ({
|
|
119
|
+
goBack: t('layouts.errors.goBack'),
|
|
120
|
+
goHome: t('layouts.errors.goHome'),
|
|
121
|
+
needHelp: t('layouts.errors.needHelp'),
|
|
122
|
+
contactSupport: t('layouts.errors.contactSupport'),
|
|
123
|
+
error: t('layouts.errors.error'),
|
|
124
|
+
}), [t]);
|
|
125
|
+
|
|
116
126
|
// Get content (Title/Description) from config. Note: Illustration check removed.
|
|
117
127
|
// The function getErrorContent MUST NOT return React components/functions.
|
|
118
128
|
const autoContent = code && (!title || !description)
|
|
119
|
-
? getErrorContent(code)
|
|
129
|
+
? getErrorContent(code, t)
|
|
120
130
|
: null;
|
|
121
131
|
|
|
122
132
|
// Fallback to auto-generated values
|
|
123
|
-
const finalTitle = title || autoContent?.title ||
|
|
133
|
+
const finalTitle = title || autoContent?.title || labels.error;
|
|
124
134
|
const finalDescription = description || autoContent?.description;
|
|
125
|
-
|
|
135
|
+
|
|
126
136
|
// ILLUSTRATION FIX: Use passed prop OR compute the icon locally using getErrorIcon.
|
|
127
137
|
const finalIllustration = illustration ?? getErrorIcon(code);
|
|
128
138
|
|
|
@@ -178,7 +188,7 @@ export function ErrorLayout({
|
|
|
178
188
|
onClick={handleGoBack}
|
|
179
189
|
style={{ minWidth: '140px', padding: '12px 32px' }}
|
|
180
190
|
>
|
|
181
|
-
|
|
191
|
+
{labels.goBack}
|
|
182
192
|
</Button>
|
|
183
193
|
<Button
|
|
184
194
|
variant="default"
|
|
@@ -186,7 +196,7 @@ export function ErrorLayout({
|
|
|
186
196
|
onClick={handleGoHome}
|
|
187
197
|
style={{ minWidth: '140px', padding: '12px 32px' }}
|
|
188
198
|
>
|
|
189
|
-
|
|
199
|
+
{labels.goHome}
|
|
190
200
|
</Button>
|
|
191
201
|
</>
|
|
192
202
|
)}
|
|
@@ -195,12 +205,12 @@ export function ErrorLayout({
|
|
|
195
205
|
{/* Additional Info */}
|
|
196
206
|
<div className="pt-8 text-sm text-muted-foreground">
|
|
197
207
|
<p>
|
|
198
|
-
|
|
208
|
+
{labels.needHelp}{' '}
|
|
199
209
|
<a
|
|
200
210
|
href={`mailto:${supportEmail}`}
|
|
201
211
|
className="text-primary hover:underline"
|
|
202
212
|
>
|
|
203
|
-
|
|
213
|
+
{labels.contactSupport}
|
|
204
214
|
</a>
|
|
205
215
|
</p>
|
|
206
216
|
</div>
|
|
@@ -13,90 +13,96 @@ export interface ErrorContent {
|
|
|
13
13
|
description: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
type TranslationFn = (key: string, params?: Record<string, string | number>) => string;
|
|
17
|
+
|
|
16
18
|
/**
|
|
17
19
|
* Get standardized error content based on status code
|
|
18
20
|
*
|
|
19
21
|
* @param statusCode - HTTP status code or custom error type
|
|
22
|
+
* @param t - Translation function from useT()
|
|
20
23
|
* @returns Error content configuration (title and description only)
|
|
21
24
|
*
|
|
22
25
|
* @example
|
|
23
26
|
* ```tsx
|
|
24
|
-
* const { title, description } = getErrorContent(404);
|
|
27
|
+
* const { title, description } = getErrorContent(404, t);
|
|
25
28
|
* <ErrorLayout title={title} description={description} code={404} />
|
|
26
29
|
* ```
|
|
27
30
|
*/
|
|
28
|
-
export function getErrorContent(statusCode?: number | string): ErrorContent {
|
|
31
|
+
export function getErrorContent(statusCode?: number | string, t?: TranslationFn): ErrorContent {
|
|
29
32
|
const code = typeof statusCode === 'string' ? parseInt(statusCode, 10) : statusCode;
|
|
30
33
|
|
|
34
|
+
// Helper to get translation or fallback
|
|
35
|
+
const tr = (key: string, fallback: string) => t ? t(key) : fallback;
|
|
36
|
+
|
|
31
37
|
switch (code) {
|
|
32
38
|
// 400 Bad Request
|
|
33
39
|
case 400:
|
|
34
40
|
return {
|
|
35
|
-
title: 'Bad Request',
|
|
36
|
-
description: 'The request could not be understood. Please check your input and try again.',
|
|
41
|
+
title: tr('layouts.errors.badRequest', 'Bad Request'),
|
|
42
|
+
description: tr('layouts.errors.badRequestDesc', 'The request could not be understood. Please check your input and try again.'),
|
|
37
43
|
};
|
|
38
44
|
|
|
39
45
|
// 401 Unauthorized
|
|
40
46
|
case 401:
|
|
41
47
|
return {
|
|
42
|
-
title: 'Authentication Required',
|
|
43
|
-
description: 'You need to sign in to access this page.',
|
|
48
|
+
title: tr('layouts.errors.authRequired', 'Authentication Required'),
|
|
49
|
+
description: tr('layouts.errors.authRequiredDesc', 'You need to sign in to access this page.'),
|
|
44
50
|
};
|
|
45
51
|
|
|
46
52
|
// 403 Forbidden
|
|
47
53
|
case 403:
|
|
48
54
|
return {
|
|
49
|
-
title: 'Access Denied',
|
|
50
|
-
description: "You don't have permission to access this resource.",
|
|
55
|
+
title: tr('layouts.errors.accessDenied', 'Access Denied'),
|
|
56
|
+
description: tr('layouts.errors.accessDeniedDesc', "You don't have permission to access this resource."),
|
|
51
57
|
};
|
|
52
58
|
|
|
53
59
|
// 404 Not Found
|
|
54
60
|
case 404:
|
|
55
61
|
return {
|
|
56
|
-
title: 'Page Not Found',
|
|
57
|
-
description: "The page you're looking for doesn't exist or has been moved.",
|
|
62
|
+
title: tr('layouts.errors.pageNotFound', 'Page Not Found'),
|
|
63
|
+
description: tr('layouts.errors.pageNotFoundDesc', "The page you're looking for doesn't exist or has been moved."),
|
|
58
64
|
};
|
|
59
65
|
|
|
60
66
|
// 408 Request Timeout
|
|
61
67
|
case 408:
|
|
62
68
|
return {
|
|
63
|
-
title: 'Request Timeout',
|
|
64
|
-
description: 'The request took too long to process. Please try again.',
|
|
69
|
+
title: tr('layouts.errors.requestTimeout', 'Request Timeout'),
|
|
70
|
+
description: tr('layouts.errors.requestTimeoutDesc', 'The request took too long to process. Please try again.'),
|
|
65
71
|
};
|
|
66
72
|
|
|
67
73
|
// 500 Internal Server Error
|
|
68
74
|
case 500:
|
|
69
75
|
return {
|
|
70
|
-
title: 'Server Error',
|
|
71
|
-
description: "Something went wrong on our end. We're working to fix it.",
|
|
76
|
+
title: tr('layouts.errors.serverError', 'Server Error'),
|
|
77
|
+
description: tr('layouts.errors.serverErrorDesc', "Something went wrong on our end. We're working to fix it."),
|
|
72
78
|
};
|
|
73
79
|
|
|
74
80
|
// 502 Bad Gateway
|
|
75
81
|
case 502:
|
|
76
82
|
return {
|
|
77
|
-
title: 'Bad Gateway',
|
|
78
|
-
description: 'The server received an invalid response. Please try again later.',
|
|
83
|
+
title: tr('layouts.errors.badGateway', 'Bad Gateway'),
|
|
84
|
+
description: tr('layouts.errors.badGatewayDesc', 'The server received an invalid response. Please try again later.'),
|
|
79
85
|
};
|
|
80
86
|
|
|
81
87
|
// 503 Service Unavailable
|
|
82
88
|
case 503:
|
|
83
89
|
return {
|
|
84
|
-
title: 'Service Unavailable',
|
|
85
|
-
description: 'The service is temporarily unavailable. Please try again later.',
|
|
90
|
+
title: tr('layouts.errors.serviceUnavailable', 'Service Unavailable'),
|
|
91
|
+
description: tr('layouts.errors.serviceUnavailableDesc', 'The service is temporarily unavailable. Please try again later.'),
|
|
86
92
|
};
|
|
87
93
|
|
|
88
94
|
// 504 Gateway Timeout
|
|
89
95
|
case 504:
|
|
90
96
|
return {
|
|
91
|
-
title: 'Gateway Timeout',
|
|
92
|
-
description: 'The server took too long to respond. Please try again.',
|
|
97
|
+
title: tr('layouts.errors.gatewayTimeout', 'Gateway Timeout'),
|
|
98
|
+
description: tr('layouts.errors.gatewayTimeoutDesc', 'The server took too long to respond. Please try again.'),
|
|
93
99
|
};
|
|
94
100
|
|
|
95
101
|
// Default / Unknown Error
|
|
96
102
|
default:
|
|
97
103
|
return {
|
|
98
|
-
title: 'Something Went Wrong',
|
|
99
|
-
description: 'An unexpected error occurred. Please try again or contact support.',
|
|
104
|
+
title: tr('layouts.errors.somethingWentWrong', 'Something Went Wrong'),
|
|
105
|
+
description: tr('layouts.errors.unknownErrorDesc', 'An unexpected error occurred. Please try again or contact support.'),
|
|
100
106
|
};
|
|
101
107
|
}
|
|
102
108
|
}
|
|
@@ -85,26 +85,33 @@ function determineLayoutMode(
|
|
|
85
85
|
return 'public';
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/** i18n configuration for locale switching */
|
|
89
|
+
export interface I18nLayoutConfig {
|
|
90
|
+
/** Current locale */
|
|
91
|
+
locale: string;
|
|
92
|
+
/** Available locales */
|
|
93
|
+
locales: string[];
|
|
94
|
+
/** Callback when locale changes */
|
|
95
|
+
onLocaleChange: (locale: string) => void;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Layout configuration with component and enabled paths */
|
|
99
|
+
interface LayoutConfig {
|
|
100
|
+
component: React.ComponentType<{ children: ReactNode; i18n?: I18nLayoutConfig }>;
|
|
101
|
+
enabledPath?: string | string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
88
104
|
export interface AppLayoutProps {
|
|
89
105
|
children: ReactNode;
|
|
90
106
|
|
|
91
107
|
/** Public layout component with enabled paths */
|
|
92
|
-
publicLayout?:
|
|
93
|
-
component: React.ComponentType<{ children: ReactNode }>;
|
|
94
|
-
enabledPath?: string | string[];
|
|
95
|
-
};
|
|
108
|
+
publicLayout?: LayoutConfig;
|
|
96
109
|
|
|
97
110
|
/** Private layout component with enabled paths */
|
|
98
|
-
privateLayout?:
|
|
99
|
-
component: React.ComponentType<{ children: ReactNode }>;
|
|
100
|
-
enabledPath?: string | string[];
|
|
101
|
-
};
|
|
111
|
+
privateLayout?: LayoutConfig;
|
|
102
112
|
|
|
103
113
|
/** Admin layout component with enabled paths */
|
|
104
|
-
adminLayout?:
|
|
105
|
-
component: React.ComponentType<{ children: ReactNode }>;
|
|
106
|
-
enabledPath?: string | string[];
|
|
107
|
-
};
|
|
114
|
+
adminLayout?: LayoutConfig;
|
|
108
115
|
|
|
109
116
|
/**
|
|
110
117
|
* Paths that render without any layout wrapper (fullscreen pages)
|
|
@@ -142,6 +149,9 @@ export interface AppLayoutProps {
|
|
|
142
149
|
|
|
143
150
|
/** Push Notifications configuration */
|
|
144
151
|
pushNotifications?: PushNotificationsConfig;
|
|
152
|
+
|
|
153
|
+
/** i18n configuration for locale switching (applies to all layouts) */
|
|
154
|
+
i18n?: I18nLayoutConfig;
|
|
145
155
|
}
|
|
146
156
|
|
|
147
157
|
/**
|
|
@@ -156,6 +166,7 @@ function AppLayoutContent({
|
|
|
156
166
|
privateLayout,
|
|
157
167
|
adminLayout,
|
|
158
168
|
noLayoutPaths,
|
|
169
|
+
i18n,
|
|
159
170
|
}: AppLayoutProps) {
|
|
160
171
|
const pathname = usePathname();
|
|
161
172
|
|
|
@@ -188,7 +199,9 @@ function AppLayoutContent({
|
|
|
188
199
|
return (
|
|
189
200
|
<ClientOnly>
|
|
190
201
|
<Suspense>
|
|
191
|
-
<privateLayout.component
|
|
202
|
+
<privateLayout.component i18n={i18n}>
|
|
203
|
+
{children}
|
|
204
|
+
</privateLayout.component>
|
|
192
205
|
</Suspense>
|
|
193
206
|
</ClientOnly>
|
|
194
207
|
);
|
|
@@ -199,7 +212,9 @@ function AppLayoutContent({
|
|
|
199
212
|
return (
|
|
200
213
|
<ClientOnly>
|
|
201
214
|
<Suspense>
|
|
202
|
-
<adminLayout.component
|
|
215
|
+
<adminLayout.component i18n={i18n}>
|
|
216
|
+
{children}
|
|
217
|
+
</adminLayout.component>
|
|
203
218
|
</Suspense>
|
|
204
219
|
</ClientOnly>
|
|
205
220
|
);
|
|
@@ -207,14 +222,20 @@ function AppLayoutContent({
|
|
|
207
222
|
case 'private':
|
|
208
223
|
if (!privateLayout) {
|
|
209
224
|
if (publicLayout) {
|
|
210
|
-
return
|
|
225
|
+
return (
|
|
226
|
+
<publicLayout.component i18n={i18n}>
|
|
227
|
+
{children}
|
|
228
|
+
</publicLayout.component>
|
|
229
|
+
);
|
|
211
230
|
}
|
|
212
231
|
return children;
|
|
213
232
|
}
|
|
214
233
|
return (
|
|
215
234
|
<ClientOnly>
|
|
216
235
|
<Suspense>
|
|
217
|
-
<privateLayout.component
|
|
236
|
+
<privateLayout.component i18n={i18n}>
|
|
237
|
+
{children}
|
|
238
|
+
</privateLayout.component>
|
|
218
239
|
</Suspense>
|
|
219
240
|
</ClientOnly>
|
|
220
241
|
);
|
|
@@ -225,7 +246,11 @@ function AppLayoutContent({
|
|
|
225
246
|
if (!publicLayout) {
|
|
226
247
|
return children;
|
|
227
248
|
}
|
|
228
|
-
return
|
|
249
|
+
return (
|
|
250
|
+
<publicLayout.component i18n={i18n}>
|
|
251
|
+
{children}
|
|
252
|
+
</publicLayout.component>
|
|
253
|
+
);
|
|
229
254
|
}
|
|
230
255
|
};
|
|
231
256
|
|
|
@@ -91,6 +91,8 @@ export interface HeaderConfig {
|
|
|
91
91
|
authPath?: string;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
import type { I18nLayoutConfig } from '../AppLayout/AppLayout';
|
|
95
|
+
|
|
94
96
|
export interface PrivateLayoutProps {
|
|
95
97
|
children: ReactNode;
|
|
96
98
|
/** Sidebar configuration */
|
|
@@ -99,6 +101,8 @@ export interface PrivateLayoutProps {
|
|
|
99
101
|
header?: HeaderConfig;
|
|
100
102
|
/** Content padding */
|
|
101
103
|
contentPadding?: 'none' | 'default';
|
|
104
|
+
/** i18n configuration for locale switching */
|
|
105
|
+
i18n?: I18nLayoutConfig;
|
|
102
106
|
}
|
|
103
107
|
|
|
104
108
|
export function PrivateLayout({
|
|
@@ -106,6 +110,7 @@ export function PrivateLayout({
|
|
|
106
110
|
sidebar,
|
|
107
111
|
header,
|
|
108
112
|
contentPadding = 'default',
|
|
113
|
+
i18n,
|
|
109
114
|
}: PrivateLayoutProps) {
|
|
110
115
|
const { isAuthenticated, isLoading, saveRedirectUrl } = useAuth();
|
|
111
116
|
const router = useRouter();
|
|
@@ -145,7 +150,9 @@ export function PrivateLayout({
|
|
|
145
150
|
{/* Main content area */}
|
|
146
151
|
<SidebarInset className="flex flex-col">
|
|
147
152
|
{/* Header with sidebar trigger */}
|
|
148
|
-
{(header || isAuthenticated) &&
|
|
153
|
+
{(header || isAuthenticated) && (
|
|
154
|
+
<PrivateHeader header={header} i18n={i18n} />
|
|
155
|
+
)}
|
|
149
156
|
|
|
150
157
|
{/* Page content */}
|
|
151
158
|
<PrivateContent padding={contentPadding}>{children}</PrivateContent>
|
|
@@ -13,15 +13,19 @@ import { Separator } from '@djangocfg/ui-nextjs/components';
|
|
|
13
13
|
import { SidebarTrigger } from '@djangocfg/ui-nextjs/components';
|
|
14
14
|
import { ThemeToggle } from '@djangocfg/ui-nextjs/theme';
|
|
15
15
|
|
|
16
|
+
import { LocaleSwitcher } from '../../_components/LocaleSwitcher';
|
|
16
17
|
import { UserMenu } from '../../_components/UserMenu';
|
|
17
18
|
|
|
18
19
|
import type { HeaderConfig } from '../PrivateLayout';
|
|
20
|
+
import type { I18nLayoutConfig } from '../../AppLayout/AppLayout';
|
|
19
21
|
|
|
20
22
|
interface PrivateHeaderProps {
|
|
21
23
|
header?: HeaderConfig;
|
|
24
|
+
/** i18n configuration for locale switching */
|
|
25
|
+
i18n?: I18nLayoutConfig;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
export function PrivateHeader({ header }: PrivateHeaderProps) {
|
|
28
|
+
export function PrivateHeader({ header, i18n }: PrivateHeaderProps) {
|
|
25
29
|
const { user, logout } = useAuth();
|
|
26
30
|
|
|
27
31
|
return (
|
|
@@ -43,6 +47,15 @@ export function PrivateHeader({ header }: PrivateHeaderProps) {
|
|
|
43
47
|
|
|
44
48
|
{/* Right side */}
|
|
45
49
|
<div className="flex items-center gap-3">
|
|
50
|
+
{/* Locale Switcher */}
|
|
51
|
+
{i18n && (
|
|
52
|
+
<LocaleSwitcher
|
|
53
|
+
locale={i18n.locale}
|
|
54
|
+
locales={i18n.locales}
|
|
55
|
+
onChange={i18n.onLocaleChange}
|
|
56
|
+
/>
|
|
57
|
+
)}
|
|
58
|
+
|
|
46
59
|
{/* Theme Toggle */}
|
|
47
60
|
<ThemeToggle />
|
|
48
61
|
|