@djangocfg/layouts 2.1.110 → 2.1.111
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/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/ProfileLayout/ProfileLayout.tsx +128 -56
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +12 -4
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +6 -2
- package/src/layouts/_components/UserMenu.tsx +14 -6
- 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
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { ChevronRight, Home } from 'lucide-react';
|
|
3
3
|
import Link from 'next/link';
|
|
4
4
|
import { usePathname } from 'next/navigation';
|
|
5
|
-
import React from 'react';
|
|
5
|
+
import React, { useMemo } from 'react';
|
|
6
|
+
|
|
7
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
6
8
|
|
|
7
9
|
export interface BreadcrumbItem {
|
|
8
10
|
path: string;
|
|
@@ -17,23 +19,32 @@ interface BreadcrumbsProps {
|
|
|
17
19
|
|
|
18
20
|
const Breadcrumbs: React.FC<BreadcrumbsProps> = ({ items, className = "" }) => {
|
|
19
21
|
const pathname = usePathname();
|
|
20
|
-
|
|
22
|
+
const t = useTypedT<I18nTranslations>();
|
|
23
|
+
|
|
24
|
+
const labels = useMemo(() => ({
|
|
25
|
+
home: t('layouts.navigation.home'),
|
|
26
|
+
breadcrumb: t('layouts.navigation.breadcrumb'),
|
|
27
|
+
}), [t]);
|
|
28
|
+
|
|
21
29
|
// Use provided items or generate from pathname
|
|
22
|
-
const breadcrumbs =
|
|
30
|
+
const breadcrumbs = useMemo(() => {
|
|
31
|
+
if (items) return items;
|
|
32
|
+
return generateBreadcrumbsFromPath(pathname, labels.home);
|
|
33
|
+
}, [items, pathname, labels.home]);
|
|
23
34
|
|
|
24
35
|
if (breadcrumbs.length <= 1) {
|
|
25
36
|
return null;
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
return (
|
|
29
|
-
<nav className={`flex items-center space-x-2 text-sm ${className}`} aria-label=
|
|
40
|
+
<nav className={`flex items-center space-x-2 text-sm ${className}`} aria-label={labels.breadcrumb}>
|
|
30
41
|
<ol className="flex items-center space-x-2">
|
|
31
42
|
{breadcrumbs.map((item: BreadcrumbItem, index: number) => (
|
|
32
43
|
<li key={item.path} className="flex items-center">
|
|
33
44
|
{index > 0 && (
|
|
34
45
|
<ChevronRight className="w-4 h-4 text-gray-400 mx-2" />
|
|
35
46
|
)}
|
|
36
|
-
|
|
47
|
+
|
|
37
48
|
{item.isActive ? (
|
|
38
49
|
<span className="text-gray-900 dark:text-white font-medium flex items-center gap-1">
|
|
39
50
|
{index === 0 && <Home className="w-4 h-4" />}
|
|
@@ -42,7 +53,7 @@ const Breadcrumbs: React.FC<BreadcrumbsProps> = ({ items, className = "" }) => {
|
|
|
42
53
|
) : (
|
|
43
54
|
<Link
|
|
44
55
|
href={item.path}
|
|
45
|
-
className="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200
|
|
56
|
+
className="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200
|
|
46
57
|
transition-colors duration-200 flex items-center gap-1 hover:underline"
|
|
47
58
|
>
|
|
48
59
|
{index === 0 && <Home className="w-4 h-4" />}
|
|
@@ -57,10 +68,10 @@ const Breadcrumbs: React.FC<BreadcrumbsProps> = ({ items, className = "" }) => {
|
|
|
57
68
|
};
|
|
58
69
|
|
|
59
70
|
// Helper function to generate breadcrumbs from pathname
|
|
60
|
-
function generateBreadcrumbsFromPath(pathname: string): BreadcrumbItem[] {
|
|
71
|
+
function generateBreadcrumbsFromPath(pathname: string, homeLabel: string = 'Home'): BreadcrumbItem[] {
|
|
61
72
|
const segments = pathname.split('/').filter(Boolean);
|
|
62
73
|
const breadcrumbs: BreadcrumbItem[] = [
|
|
63
|
-
{ path: '/', label:
|
|
74
|
+
{ path: '/', label: homeLabel, isActive: pathname === '/' }
|
|
64
75
|
];
|
|
65
76
|
|
|
66
77
|
let currentPath = '';
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Bot, PanelRight, RotateCcw, X } from 'lucide-react';
|
|
4
|
-
import React from 'react';
|
|
4
|
+
import React, { useMemo } from 'react';
|
|
5
5
|
|
|
6
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
6
7
|
import { Button, Card, CardContent, CardFooter, CardHeader } from '@djangocfg/ui-core';
|
|
7
8
|
|
|
8
9
|
import { useAIChatContext } from '../context/AIChatContext';
|
|
@@ -21,6 +22,15 @@ export const ChatPanel = React.memo(() => {
|
|
|
21
22
|
stopStreaming,
|
|
22
23
|
clearMessages,
|
|
23
24
|
} = useAIChatContext();
|
|
25
|
+
const t = useTypedT<I18nTranslations>();
|
|
26
|
+
|
|
27
|
+
const labels = useMemo(() => ({
|
|
28
|
+
aiAssistant: t('layouts.chat.aiAssistant'),
|
|
29
|
+
defaultTitle: t('layouts.chat.defaultTitle'),
|
|
30
|
+
newChat: t('layouts.chat.newChat'),
|
|
31
|
+
closeChat: t('layouts.chat.closeChat'),
|
|
32
|
+
switchToSidebar: t('layouts.chat.switchToSidebar'),
|
|
33
|
+
}), [t]);
|
|
24
34
|
|
|
25
35
|
// Mobile: fullscreen, Desktop: floating panel
|
|
26
36
|
const panelStyles: React.CSSProperties = isMobile
|
|
@@ -60,8 +70,8 @@ export const ChatPanel = React.memo(() => {
|
|
|
60
70
|
<Bot className="h-4 w-4 text-primary" />
|
|
61
71
|
</div>
|
|
62
72
|
<div>
|
|
63
|
-
<h3 className="font-semibold text-sm">{config.title ||
|
|
64
|
-
<p className="text-xs text-muted-foreground">
|
|
73
|
+
<h3 className="font-semibold text-sm">{config.title || labels.defaultTitle}</h3>
|
|
74
|
+
<p className="text-xs text-muted-foreground">{labels.aiAssistant}</p>
|
|
65
75
|
</div>
|
|
66
76
|
</div>
|
|
67
77
|
<div className="flex gap-1">
|
|
@@ -71,7 +81,7 @@ export const ChatPanel = React.memo(() => {
|
|
|
71
81
|
size="icon"
|
|
72
82
|
className="h-8 w-8"
|
|
73
83
|
onClick={clearMessages}
|
|
74
|
-
title=
|
|
84
|
+
title={labels.newChat}
|
|
75
85
|
>
|
|
76
86
|
<RotateCcw className="h-4 w-4" />
|
|
77
87
|
</Button>
|
|
@@ -83,12 +93,12 @@ export const ChatPanel = React.memo(() => {
|
|
|
83
93
|
size="icon"
|
|
84
94
|
className="h-8 w-8"
|
|
85
95
|
onClick={() => setDisplayMode('sidebar')}
|
|
86
|
-
title=
|
|
96
|
+
title={labels.switchToSidebar}
|
|
87
97
|
>
|
|
88
98
|
<PanelRight className="h-4 w-4" />
|
|
89
99
|
</Button>
|
|
90
100
|
)}
|
|
91
|
-
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={closeChat} title=
|
|
101
|
+
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={closeChat} title={labels.closeChat}>
|
|
92
102
|
<X className="h-4 w-4" />
|
|
93
103
|
</Button>
|
|
94
104
|
</div>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Bot, GripVertical, PanelRightClose, RotateCcw, X } from 'lucide-react';
|
|
4
|
-
import React, { useEffect } from 'react';
|
|
4
|
+
import React, { useEffect, useMemo } from 'react';
|
|
5
5
|
|
|
6
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
6
7
|
import { Button } from '@djangocfg/ui-core';
|
|
7
8
|
|
|
8
9
|
import { useAIChatContext } from '../context/AIChatContext';
|
|
@@ -34,6 +35,17 @@ export const ChatSidebar = React.memo<ChatSidebarProps>(({
|
|
|
34
35
|
stopStreaming,
|
|
35
36
|
clearMessages,
|
|
36
37
|
} = useAIChatContext();
|
|
38
|
+
const t = useTypedT<I18nTranslations>();
|
|
39
|
+
|
|
40
|
+
const labels = useMemo(() => ({
|
|
41
|
+
aiAssistant: t('layouts.chat.aiAssistant'),
|
|
42
|
+
defaultTitle: t('layouts.chat.defaultTitle'),
|
|
43
|
+
newChat: t('layouts.chat.newChat'),
|
|
44
|
+
closeChat: t('layouts.chat.closeChat'),
|
|
45
|
+
switchToFloating: t('layouts.chat.switchToFloating'),
|
|
46
|
+
dragToResize: t('layouts.chat.dragToResize'),
|
|
47
|
+
howCanIHelp: t('layouts.chat.howCanIHelp'),
|
|
48
|
+
}), [t]);
|
|
37
49
|
|
|
38
50
|
// Use the layout hook for content pushing and resizing
|
|
39
51
|
const { applyLayout, getSidebarStyles, startResize, isResizing } = useChatLayout();
|
|
@@ -65,7 +77,7 @@ export const ChatSidebar = React.memo<ChatSidebarProps>(({
|
|
|
65
77
|
`}
|
|
66
78
|
style={{ width: resizeHandleWidth }}
|
|
67
79
|
onMouseDown={startResize}
|
|
68
|
-
title=
|
|
80
|
+
title={labels.dragToResize}
|
|
69
81
|
>
|
|
70
82
|
{showResizeIcon && (
|
|
71
83
|
<GripVertical className={`h-4 w-4 ${isResizing ? 'text-primary' : 'text-muted-foreground/50'}`} />
|
|
@@ -87,8 +99,8 @@ export const ChatSidebar = React.memo<ChatSidebarProps>(({
|
|
|
87
99
|
<Bot className="h-4 w-4 text-primary" />
|
|
88
100
|
</div>
|
|
89
101
|
<div>
|
|
90
|
-
<h3 className="font-semibold text-sm">{config.title ||
|
|
91
|
-
<p className="text-xs text-muted-foreground">
|
|
102
|
+
<h3 className="font-semibold text-sm">{config.title || labels.defaultTitle}</h3>
|
|
103
|
+
<p className="text-xs text-muted-foreground">{labels.aiAssistant}</p>
|
|
92
104
|
</div>
|
|
93
105
|
</div>
|
|
94
106
|
<div className="flex gap-1">
|
|
@@ -98,7 +110,7 @@ export const ChatSidebar = React.memo<ChatSidebarProps>(({
|
|
|
98
110
|
size="icon"
|
|
99
111
|
className="h-8 w-8"
|
|
100
112
|
onClick={clearMessages}
|
|
101
|
-
title=
|
|
113
|
+
title={labels.newChat}
|
|
102
114
|
>
|
|
103
115
|
<RotateCcw className="h-4 w-4" />
|
|
104
116
|
</Button>
|
|
@@ -108,11 +120,11 @@ export const ChatSidebar = React.memo<ChatSidebarProps>(({
|
|
|
108
120
|
size="icon"
|
|
109
121
|
className="h-8 w-8"
|
|
110
122
|
onClick={() => setDisplayMode('floating')}
|
|
111
|
-
title=
|
|
123
|
+
title={labels.switchToFloating}
|
|
112
124
|
>
|
|
113
125
|
<PanelRightClose className="h-4 w-4" />
|
|
114
126
|
</Button>
|
|
115
|
-
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={closeChat} title=
|
|
127
|
+
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={closeChat} title={labels.closeChat}>
|
|
116
128
|
<X className="h-4 w-4" />
|
|
117
129
|
</Button>
|
|
118
130
|
</div>
|
|
@@ -128,7 +140,7 @@ export const ChatSidebar = React.memo<ChatSidebarProps>(({
|
|
|
128
140
|
isCompact={false}
|
|
129
141
|
largeGreetingIcon
|
|
130
142
|
greetingIcon="message"
|
|
131
|
-
greetingTitle=
|
|
143
|
+
greetingTitle={labels.howCanIHelp}
|
|
132
144
|
/>
|
|
133
145
|
</div>
|
|
134
146
|
|
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { ChevronRight, Download, Share, X } from 'lucide-react';
|
|
15
|
-
import React, { useEffect, useState } from 'react';
|
|
15
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
16
16
|
|
|
17
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
17
18
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
18
19
|
|
|
19
20
|
import { useInstall } from '../context/InstallContext';
|
|
@@ -69,6 +70,18 @@ export function A2HSHint({
|
|
|
69
70
|
const [show, setShow] = useState(false);
|
|
70
71
|
const [showGuide, setShowGuide] = useState(false);
|
|
71
72
|
const [installing, setInstalling] = useState(false);
|
|
73
|
+
const t = useTypedT<I18nTranslations>();
|
|
74
|
+
|
|
75
|
+
const labels = useMemo(() => ({
|
|
76
|
+
addToHomeScreen: t('layouts.pwa.addToHomeScreen'),
|
|
77
|
+
installApp: t('layouts.pwa.installApp'),
|
|
78
|
+
tapToLearnHow: t('layouts.pwa.tapToLearnHow'),
|
|
79
|
+
clickToSeeGuide: t('layouts.pwa.clickToSeeGuide'),
|
|
80
|
+
tapToInstall: t('layouts.pwa.tapToInstall'),
|
|
81
|
+
installing: t('layouts.pwa.installing'),
|
|
82
|
+
dismiss: t('layouts.pwa.dismiss'),
|
|
83
|
+
appLogo: t('layouts.pwa.appLogo'),
|
|
84
|
+
}), [t]);
|
|
72
85
|
|
|
73
86
|
// Determine if should show hint
|
|
74
87
|
// Production: iOS (all browsers support PWA via Share) & Android Chrome with native prompt
|
|
@@ -141,15 +154,15 @@ export function A2HSHint({
|
|
|
141
154
|
let subtitle: React.ReactNode;
|
|
142
155
|
|
|
143
156
|
if (isIOS) {
|
|
144
|
-
title =
|
|
145
|
-
subtitle = <>
|
|
157
|
+
title = labels.addToHomeScreen;
|
|
158
|
+
subtitle = <>{labels.tapToLearnHow} <ChevronRight className="w-3 h-3" /></>;
|
|
146
159
|
} else if (isDesktop) {
|
|
147
|
-
title =
|
|
148
|
-
subtitle = <>
|
|
160
|
+
title = labels.installApp;
|
|
161
|
+
subtitle = <>{labels.clickToSeeGuide} <ChevronRight className="w-3 h-3" /></>;
|
|
149
162
|
} else {
|
|
150
163
|
// Android or other mobile with native prompt
|
|
151
|
-
title =
|
|
152
|
-
subtitle = <>
|
|
164
|
+
title = labels.installApp;
|
|
165
|
+
subtitle = <>{labels.tapToInstall} <Download className="w-3 h-3" /></>;
|
|
153
166
|
}
|
|
154
167
|
|
|
155
168
|
return (
|
|
@@ -180,7 +193,7 @@ export function A2HSHint({
|
|
|
180
193
|
{/* App logo or icon */}
|
|
181
194
|
<div className="flex-shrink-0">
|
|
182
195
|
{logo ? (
|
|
183
|
-
<img src={logo} alt=
|
|
196
|
+
<img src={logo} alt={labels.appLogo} className="w-10 h-10 rounded-lg" />
|
|
184
197
|
) : (
|
|
185
198
|
<Share className="w-5 h-5 text-blue-400" />
|
|
186
199
|
)}
|
|
@@ -190,7 +203,7 @@ export function A2HSHint({
|
|
|
190
203
|
<div className="flex-1 min-w-0">
|
|
191
204
|
<p className="text-sm font-medium text-white mb-1">{title}</p>
|
|
192
205
|
<p className="text-xs text-zinc-400 flex items-center gap-1">
|
|
193
|
-
{installing ?
|
|
206
|
+
{installing ? labels.installing : subtitle}
|
|
194
207
|
</p>
|
|
195
208
|
</div>
|
|
196
209
|
|
|
@@ -201,7 +214,7 @@ export function A2HSHint({
|
|
|
201
214
|
handleDismiss();
|
|
202
215
|
}}
|
|
203
216
|
className="flex-shrink-0 p-1 hover:bg-zinc-700 rounded transition-colors"
|
|
204
|
-
aria-label=
|
|
217
|
+
aria-label={labels.dismiss}
|
|
205
218
|
>
|
|
206
219
|
<X className="w-4 h-4 text-zinc-400" />
|
|
207
220
|
</button>
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { ArrowDownToLine, Check, Menu, Monitor, Plus, Search } from 'lucide-react';
|
|
12
12
|
import React, { useMemo } from 'react';
|
|
13
13
|
|
|
14
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
14
15
|
import {
|
|
15
16
|
Button, Card, CardContent, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader,
|
|
16
17
|
DialogTitle
|
|
@@ -32,28 +33,29 @@ function getBrowserCategory(browser: {
|
|
|
32
33
|
return 'unknown';
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
type TranslationFn = (key: string) => string;
|
|
37
|
+
|
|
38
|
+
function getBrowserSteps(category: BrowserCategory, t: TranslationFn): InstallStep[] {
|
|
36
39
|
switch (category) {
|
|
37
40
|
case 'chromium':
|
|
38
|
-
// Chrome, Edge, Brave, Arc, Vivaldi, Opera, Yandex, etc.
|
|
39
41
|
return [
|
|
40
42
|
{
|
|
41
43
|
number: 1,
|
|
42
|
-
title: '
|
|
44
|
+
title: t('layouts.pwa.chromiumStep1Title'),
|
|
43
45
|
icon: ArrowDownToLine,
|
|
44
|
-
description: '
|
|
46
|
+
description: t('layouts.pwa.chromiumStep1Desc'),
|
|
45
47
|
},
|
|
46
48
|
{
|
|
47
49
|
number: 2,
|
|
48
|
-
title: '
|
|
50
|
+
title: t('layouts.pwa.chromiumStep2Title'),
|
|
49
51
|
icon: Plus,
|
|
50
|
-
description: '
|
|
52
|
+
description: t('layouts.pwa.chromiumStep2Desc'),
|
|
51
53
|
},
|
|
52
54
|
{
|
|
53
55
|
number: 3,
|
|
54
|
-
title: '
|
|
56
|
+
title: t('layouts.pwa.chromiumStep3Title'),
|
|
55
57
|
icon: Check,
|
|
56
|
-
description: '
|
|
58
|
+
description: t('layouts.pwa.chromiumStep3Desc'),
|
|
57
59
|
},
|
|
58
60
|
];
|
|
59
61
|
|
|
@@ -61,21 +63,21 @@ function getBrowserSteps(category: BrowserCategory, browserName: string): Instal
|
|
|
61
63
|
return [
|
|
62
64
|
{
|
|
63
65
|
number: 1,
|
|
64
|
-
title: '
|
|
66
|
+
title: t('layouts.pwa.firefoxStep1Title'),
|
|
65
67
|
icon: Menu,
|
|
66
|
-
description: '
|
|
68
|
+
description: t('layouts.pwa.firefoxStep1Desc'),
|
|
67
69
|
},
|
|
68
70
|
{
|
|
69
71
|
number: 2,
|
|
70
|
-
title: '
|
|
72
|
+
title: t('layouts.pwa.firefoxStep2Title'),
|
|
71
73
|
icon: Search,
|
|
72
|
-
description: '
|
|
74
|
+
description: t('layouts.pwa.firefoxStep2Desc'),
|
|
73
75
|
},
|
|
74
76
|
{
|
|
75
77
|
number: 3,
|
|
76
|
-
title: '
|
|
78
|
+
title: t('layouts.pwa.firefoxStep3Title'),
|
|
77
79
|
icon: Check,
|
|
78
|
-
description: '
|
|
80
|
+
description: t('layouts.pwa.firefoxStep3Desc'),
|
|
79
81
|
},
|
|
80
82
|
];
|
|
81
83
|
|
|
@@ -83,15 +85,15 @@ function getBrowserSteps(category: BrowserCategory, browserName: string): Instal
|
|
|
83
85
|
return [
|
|
84
86
|
{
|
|
85
87
|
number: 1,
|
|
86
|
-
title: '
|
|
88
|
+
title: t('layouts.pwa.safariStep1Title'),
|
|
87
89
|
icon: Monitor,
|
|
88
|
-
description: '
|
|
90
|
+
description: t('layouts.pwa.safariStep1Desc'),
|
|
89
91
|
},
|
|
90
92
|
{
|
|
91
93
|
number: 2,
|
|
92
|
-
title: '
|
|
94
|
+
title: t('layouts.pwa.safariStep2Title'),
|
|
93
95
|
icon: ArrowDownToLine,
|
|
94
|
-
description: '
|
|
96
|
+
description: t('layouts.pwa.safariStep2Desc'),
|
|
95
97
|
},
|
|
96
98
|
];
|
|
97
99
|
|
|
@@ -99,21 +101,21 @@ function getBrowserSteps(category: BrowserCategory, browserName: string): Instal
|
|
|
99
101
|
return [
|
|
100
102
|
{
|
|
101
103
|
number: 1,
|
|
102
|
-
title: '
|
|
104
|
+
title: t('layouts.pwa.unknownStep1Title'),
|
|
103
105
|
icon: ArrowDownToLine,
|
|
104
|
-
description: '
|
|
106
|
+
description: t('layouts.pwa.unknownStep1Desc'),
|
|
105
107
|
},
|
|
106
108
|
{
|
|
107
109
|
number: 2,
|
|
108
|
-
title: '
|
|
110
|
+
title: t('layouts.pwa.unknownStep2Title'),
|
|
109
111
|
icon: Menu,
|
|
110
|
-
description: '
|
|
112
|
+
description: t('layouts.pwa.unknownStep2Desc'),
|
|
111
113
|
},
|
|
112
114
|
{
|
|
113
115
|
number: 3,
|
|
114
|
-
title: '
|
|
116
|
+
title: t('layouts.pwa.unknownStep3Title'),
|
|
115
117
|
icon: Check,
|
|
116
|
-
description: '
|
|
118
|
+
description: t('layouts.pwa.unknownStep3Desc'),
|
|
117
119
|
},
|
|
118
120
|
];
|
|
119
121
|
}
|
|
@@ -157,15 +159,25 @@ export function DesktopGuide({ onDismiss, open = true }: IOSGuideModalProps) {
|
|
|
157
159
|
isOpera,
|
|
158
160
|
isYandex,
|
|
159
161
|
} = useInstall();
|
|
162
|
+
const t = useTypedT<I18nTranslations>();
|
|
163
|
+
|
|
164
|
+
const labels = useMemo(() => ({
|
|
165
|
+
title: t('layouts.pwa.desktopTitle'),
|
|
166
|
+
description: t('layouts.pwa.desktopDescription'),
|
|
167
|
+
descSafari: t('layouts.pwa.desktopDescSafari'),
|
|
168
|
+
tip: t('layouts.pwa.desktopTip'),
|
|
169
|
+
firefoxNote: t('layouts.pwa.desktopFirefoxNote'),
|
|
170
|
+
gotIt: t('layouts.pwa.gotIt'),
|
|
171
|
+
}), [t]);
|
|
160
172
|
|
|
161
173
|
const category = useMemo(
|
|
162
174
|
() => getBrowserCategory({ isChromium, isFirefox, isSafari }),
|
|
163
175
|
[isChromium, isFirefox, isSafari]
|
|
164
176
|
);
|
|
165
177
|
|
|
166
|
-
const steps = useMemo(() => getBrowserSteps(category,
|
|
178
|
+
const steps = useMemo(() => getBrowserSteps(category, t), [category, t]);
|
|
167
179
|
|
|
168
|
-
// Get specific browser display name
|
|
180
|
+
// Get specific browser display name
|
|
169
181
|
const displayName = useMemo(() => {
|
|
170
182
|
if (isEdge) return 'Edge';
|
|
171
183
|
if (isBrave) return 'Brave';
|
|
@@ -182,12 +194,12 @@ export function DesktopGuide({ onDismiss, open = true }: IOSGuideModalProps) {
|
|
|
182
194
|
<DialogHeader className="text-left">
|
|
183
195
|
<DialogTitle className="flex items-center gap-2">
|
|
184
196
|
<Monitor className="w-5 h-5 text-primary" />
|
|
185
|
-
|
|
197
|
+
{labels.title}
|
|
186
198
|
</DialogTitle>
|
|
187
199
|
<DialogDescription className="text-left">
|
|
188
200
|
{isSafari
|
|
189
|
-
?
|
|
190
|
-
:
|
|
201
|
+
? labels.descSafari
|
|
202
|
+
: labels.description.replace('{browser}', displayName)
|
|
191
203
|
}
|
|
192
204
|
</DialogDescription>
|
|
193
205
|
</DialogHeader>
|
|
@@ -200,20 +212,20 @@ export function DesktopGuide({ onDismiss, open = true }: IOSGuideModalProps) {
|
|
|
200
212
|
|
|
201
213
|
{category === 'chromium' && (
|
|
202
214
|
<div className="p-3 bg-muted/30 rounded-lg text-xs text-muted-foreground">
|
|
203
|
-
💡
|
|
215
|
+
💡 {labels.tip.replace('{browser}', displayName)}
|
|
204
216
|
</div>
|
|
205
217
|
)}
|
|
206
218
|
|
|
207
219
|
{category === 'firefox' && (
|
|
208
220
|
<div className="p-3 bg-amber-500/10 rounded-lg text-xs text-amber-700 dark:text-amber-400">
|
|
209
|
-
ℹ️
|
|
221
|
+
ℹ️ {labels.firefoxNote}
|
|
210
222
|
</div>
|
|
211
223
|
)}
|
|
212
224
|
|
|
213
225
|
<DialogFooter>
|
|
214
226
|
<Button onClick={onDismiss} variant="default" className="w-full">
|
|
215
227
|
<Check className="w-4 h-4 mr-2" />
|
|
216
|
-
|
|
228
|
+
{labels.gotIt}
|
|
217
229
|
</Button>
|
|
218
230
|
</DialogFooter>
|
|
219
231
|
</DialogContent>
|
|
@@ -8,35 +8,15 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { ArrowDown, ArrowUpRight, Check, CheckCircle, Share } from 'lucide-react';
|
|
11
|
-
import React from 'react';
|
|
11
|
+
import React, { useMemo } from 'react';
|
|
12
12
|
|
|
13
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
13
14
|
import {
|
|
14
15
|
Button, Card, CardContent, Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle
|
|
15
16
|
} from '@djangocfg/ui-core/components';
|
|
16
17
|
|
|
17
18
|
import type { IOSGuideModalProps, InstallStep } from '../types';
|
|
18
19
|
|
|
19
|
-
const steps: InstallStep[] = [
|
|
20
|
-
{
|
|
21
|
-
number: 1,
|
|
22
|
-
title: 'Tap Share',
|
|
23
|
-
icon: ArrowUpRight,
|
|
24
|
-
description: 'At the bottom of Safari',
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
number: 2,
|
|
28
|
-
title: 'Scroll & Tap',
|
|
29
|
-
icon: ArrowDown,
|
|
30
|
-
description: '"Add to Home Screen"',
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
number: 3,
|
|
34
|
-
title: 'Confirm',
|
|
35
|
-
icon: CheckCircle,
|
|
36
|
-
description: 'Tap "Add" in top-right',
|
|
37
|
-
},
|
|
38
|
-
];
|
|
39
|
-
|
|
40
20
|
function StepCard({ step }: { step: InstallStep }) {
|
|
41
21
|
return (
|
|
42
22
|
<Card className="border border-border">
|
|
@@ -63,16 +43,45 @@ function StepCard({ step }: { step: InstallStep }) {
|
|
|
63
43
|
}
|
|
64
44
|
|
|
65
45
|
export function IOSGuideDrawer({ onDismiss, open = true }: IOSGuideModalProps) {
|
|
46
|
+
const t = useTypedT<I18nTranslations>();
|
|
47
|
+
|
|
48
|
+
const labels = useMemo(() => ({
|
|
49
|
+
title: t('layouts.pwa.iosTitle'),
|
|
50
|
+
description: t('layouts.pwa.iosDescription'),
|
|
51
|
+
gotIt: t('layouts.pwa.gotIt'),
|
|
52
|
+
}), [t]);
|
|
53
|
+
|
|
54
|
+
const steps: InstallStep[] = useMemo(() => [
|
|
55
|
+
{
|
|
56
|
+
number: 1,
|
|
57
|
+
title: t('layouts.pwa.iosStep1Title'),
|
|
58
|
+
icon: ArrowUpRight,
|
|
59
|
+
description: t('layouts.pwa.iosStep1Desc'),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
number: 2,
|
|
63
|
+
title: t('layouts.pwa.iosStep2Title'),
|
|
64
|
+
icon: ArrowDown,
|
|
65
|
+
description: t('layouts.pwa.iosStep2Desc'),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
number: 3,
|
|
69
|
+
title: t('layouts.pwa.iosStep3Title'),
|
|
70
|
+
icon: CheckCircle,
|
|
71
|
+
description: t('layouts.pwa.iosStep3Desc'),
|
|
72
|
+
},
|
|
73
|
+
], [t]);
|
|
74
|
+
|
|
66
75
|
return (
|
|
67
76
|
<Drawer open={open} onOpenChange={(isOpen) => !isOpen && onDismiss()}>
|
|
68
77
|
<DrawerContent>
|
|
69
78
|
<DrawerHeader className="text-left">
|
|
70
79
|
<DrawerTitle className="flex items-center gap-2">
|
|
71
80
|
<Share className="w-5 h-5 text-primary" />
|
|
72
|
-
|
|
81
|
+
{labels.title}
|
|
73
82
|
</DrawerTitle>
|
|
74
83
|
<DrawerDescription className="text-left">
|
|
75
|
-
|
|
84
|
+
{labels.description}
|
|
76
85
|
</DrawerDescription>
|
|
77
86
|
</DrawerHeader>
|
|
78
87
|
|
|
@@ -85,7 +94,7 @@ export function IOSGuideDrawer({ onDismiss, open = true }: IOSGuideModalProps) {
|
|
|
85
94
|
<div className="p-4 pt-0">
|
|
86
95
|
<Button onClick={onDismiss} variant="default" className="w-full">
|
|
87
96
|
<Check className="w-4 h-4 mr-2" />
|
|
88
|
-
|
|
97
|
+
{labels.gotIt}
|
|
89
98
|
</Button>
|
|
90
99
|
</div>
|
|
91
100
|
</DrawerContent>
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { ArrowDown, ArrowUpRight, Check, CheckCircle, Share } from 'lucide-react';
|
|
10
|
-
import React from 'react';
|
|
10
|
+
import React, { useMemo } from 'react';
|
|
11
11
|
|
|
12
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
12
13
|
import {
|
|
13
14
|
Button, Card, CardContent, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader,
|
|
14
15
|
DialogTitle
|
|
@@ -16,27 +17,6 @@ import {
|
|
|
16
17
|
|
|
17
18
|
import type { IOSGuideModalProps, InstallStep } from '../types';
|
|
18
19
|
|
|
19
|
-
const steps: InstallStep[] = [
|
|
20
|
-
{
|
|
21
|
-
number: 1,
|
|
22
|
-
title: 'Tap Share',
|
|
23
|
-
icon: ArrowUpRight,
|
|
24
|
-
description: 'At the bottom of Safari',
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
number: 2,
|
|
28
|
-
title: 'Scroll & Tap',
|
|
29
|
-
icon: ArrowDown,
|
|
30
|
-
description: '"Add to Home Screen"',
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
number: 3,
|
|
34
|
-
title: 'Confirm',
|
|
35
|
-
icon: CheckCircle,
|
|
36
|
-
description: 'Tap "Add" in top-right',
|
|
37
|
-
},
|
|
38
|
-
];
|
|
39
|
-
|
|
40
20
|
function StepCard({ step }: { step: InstallStep }) {
|
|
41
21
|
return (
|
|
42
22
|
<Card className="border border-border">
|
|
@@ -63,16 +43,45 @@ function StepCard({ step }: { step: InstallStep }) {
|
|
|
63
43
|
}
|
|
64
44
|
|
|
65
45
|
export function IOSGuideModal({ onDismiss, open = true }: IOSGuideModalProps) {
|
|
46
|
+
const t = useTypedT<I18nTranslations>();
|
|
47
|
+
|
|
48
|
+
const labels = useMemo(() => ({
|
|
49
|
+
title: t('layouts.pwa.iosTitle'),
|
|
50
|
+
description: t('layouts.pwa.iosDescription'),
|
|
51
|
+
gotIt: t('layouts.pwa.gotIt'),
|
|
52
|
+
}), [t]);
|
|
53
|
+
|
|
54
|
+
const steps: InstallStep[] = useMemo(() => [
|
|
55
|
+
{
|
|
56
|
+
number: 1,
|
|
57
|
+
title: t('layouts.pwa.iosStep1Title'),
|
|
58
|
+
icon: ArrowUpRight,
|
|
59
|
+
description: t('layouts.pwa.iosStep1Desc'),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
number: 2,
|
|
63
|
+
title: t('layouts.pwa.iosStep2Title'),
|
|
64
|
+
icon: ArrowDown,
|
|
65
|
+
description: t('layouts.pwa.iosStep2Desc'),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
number: 3,
|
|
69
|
+
title: t('layouts.pwa.iosStep3Title'),
|
|
70
|
+
icon: CheckCircle,
|
|
71
|
+
description: t('layouts.pwa.iosStep3Desc'),
|
|
72
|
+
},
|
|
73
|
+
], [t]);
|
|
74
|
+
|
|
66
75
|
return (
|
|
67
76
|
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && onDismiss()}>
|
|
68
77
|
<DialogContent className="sm:max-w-md">
|
|
69
78
|
<DialogHeader className="text-left">
|
|
70
79
|
<DialogTitle className="flex items-center gap-2">
|
|
71
80
|
<Share className="w-5 h-5 text-primary" />
|
|
72
|
-
|
|
81
|
+
{labels.title}
|
|
73
82
|
</DialogTitle>
|
|
74
83
|
<DialogDescription className="text-left">
|
|
75
|
-
|
|
84
|
+
{labels.description}
|
|
76
85
|
</DialogDescription>
|
|
77
86
|
</DialogHeader>
|
|
78
87
|
|
|
@@ -85,7 +94,7 @@ export function IOSGuideModal({ onDismiss, open = true }: IOSGuideModalProps) {
|
|
|
85
94
|
<DialogFooter>
|
|
86
95
|
<Button onClick={onDismiss} variant="default" className="w-full">
|
|
87
96
|
<Check className="w-4 h-4 mr-2" />
|
|
88
|
-
|
|
97
|
+
{labels.gotIt}
|
|
89
98
|
</Button>
|
|
90
99
|
</DialogFooter>
|
|
91
100
|
</DialogContent>
|