@djangocfg/layouts 2.1.109 → 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 +16 -14
- 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/AuthLayout/AuthLayout.tsx +92 -20
- package/src/layouts/AuthLayout/components/index.ts +11 -7
- package/src/layouts/AuthLayout/components/oauth/index.ts +0 -1
- package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +35 -0
- package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +56 -0
- package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +22 -0
- package/src/layouts/AuthLayout/components/shared/AuthError.tsx +26 -0
- package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +47 -0
- package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +53 -0
- package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +41 -0
- package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +42 -0
- package/src/layouts/AuthLayout/components/shared/ChannelToggle.tsx +48 -0
- package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +57 -0
- package/src/layouts/AuthLayout/components/shared/index.ts +21 -0
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +171 -0
- package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +114 -0
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +70 -0
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +24 -0
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +125 -0
- package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +91 -0
- package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +92 -0
- package/src/layouts/AuthLayout/components/steps/index.ts +6 -0
- package/src/layouts/AuthLayout/constants.ts +24 -0
- package/src/layouts/AuthLayout/content.ts +78 -0
- package/src/layouts/AuthLayout/hooks/index.ts +1 -0
- package/src/layouts/AuthLayout/hooks/useCopyToClipboard.ts +37 -0
- package/src/layouts/AuthLayout/index.ts +9 -5
- package/src/layouts/AuthLayout/styles/auth.css +578 -0
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +130 -58
- package/src/layouts/ProfileLayout/components/TwoFactorSection.tsx +2 -2
- 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
- package/src/layouts/AuthLayout/components/AuthHelp.tsx +0 -114
- package/src/layouts/AuthLayout/components/AuthSuccess.tsx +0 -101
- package/src/layouts/AuthLayout/components/IdentifierForm.tsx +0 -322
- package/src/layouts/AuthLayout/components/OTPForm.tsx +0 -174
- package/src/layouts/AuthLayout/components/TwoFactorForm.tsx +0 -140
- package/src/layouts/AuthLayout/components/TwoFactorSetup.tsx +0 -286
- package/src/layouts/AuthLayout/components/oauth/OAuthProviders.tsx +0 -56
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { Camera, ChevronRight, LogOut, Mail, Phone, Shield, ShieldCheck, Trash2, User, Building2, Briefcase } from 'lucide-react';
|
|
4
4
|
import moment from 'moment';
|
|
5
|
-
import React, { useEffect, useState } from 'react';
|
|
5
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
6
6
|
import { useForm } from 'react-hook-form';
|
|
7
|
+
|
|
8
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
7
9
|
import { toast } from '@djangocfg/ui-core/hooks';
|
|
8
10
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
9
11
|
|
|
@@ -28,7 +30,7 @@ import {
|
|
|
28
30
|
} from '@djangocfg/ui-core/components';
|
|
29
31
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
30
32
|
|
|
31
|
-
import {
|
|
33
|
+
import { SetupStep } from '../AuthLayout/components/steps/SetupStep';
|
|
32
34
|
import { profileLogger } from '../../utils/logger';
|
|
33
35
|
|
|
34
36
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -129,16 +131,23 @@ interface EditFieldDialogProps {
|
|
|
129
131
|
isSaving: boolean;
|
|
130
132
|
}
|
|
131
133
|
|
|
132
|
-
const fieldLabels: Record<string, string> = {
|
|
133
|
-
first_name: 'First Name',
|
|
134
|
-
last_name: 'Last Name',
|
|
135
|
-
company: 'Company',
|
|
136
|
-
position: 'Position',
|
|
137
|
-
phone: 'Phone',
|
|
138
|
-
};
|
|
139
|
-
|
|
140
134
|
const EditFieldDialog = ({ open, onOpenChange, field, currentValue, onSave, isSaving }: EditFieldDialogProps) => {
|
|
141
135
|
const [value, setValue] = useState(currentValue);
|
|
136
|
+
const t = useTypedT<I18nTranslations>();
|
|
137
|
+
|
|
138
|
+
const labels = useMemo(() => ({
|
|
139
|
+
fieldLabels: {
|
|
140
|
+
first_name: t('layouts.profilePage.firstName'),
|
|
141
|
+
last_name: t('layouts.profilePage.lastName'),
|
|
142
|
+
company: t('layouts.profilePage.company'),
|
|
143
|
+
position: t('layouts.profilePage.position'),
|
|
144
|
+
phone: t('layouts.profilePage.phone'),
|
|
145
|
+
} as Record<string, string>,
|
|
146
|
+
save: t('layouts.profilePage.save'),
|
|
147
|
+
saving: t('layouts.profilePage.saving'),
|
|
148
|
+
cancel: t('layouts.profilePage.cancel'),
|
|
149
|
+
enterField: t('layouts.profilePage.enterField'),
|
|
150
|
+
}), [t]);
|
|
142
151
|
|
|
143
152
|
useEffect(() => {
|
|
144
153
|
setValue(currentValue);
|
|
@@ -152,27 +161,29 @@ const EditFieldDialog = ({ open, onOpenChange, field, currentValue, onSave, isSa
|
|
|
152
161
|
|
|
153
162
|
if (!field) return null;
|
|
154
163
|
|
|
164
|
+
const fieldLabel = labels.fieldLabels[field] || field;
|
|
165
|
+
|
|
155
166
|
return (
|
|
156
167
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
157
168
|
<DialogContent className="sm:max-w-md">
|
|
158
169
|
<DialogHeader>
|
|
159
|
-
<DialogTitle>{
|
|
170
|
+
<DialogTitle>{fieldLabel}</DialogTitle>
|
|
160
171
|
</DialogHeader>
|
|
161
172
|
<div className="py-4">
|
|
162
173
|
<Input
|
|
163
174
|
value={value}
|
|
164
175
|
onChange={(e) => setValue(e.target.value)}
|
|
165
|
-
placeholder={
|
|
176
|
+
placeholder={labels.enterField.replace('{field}', fieldLabel.toLowerCase())}
|
|
166
177
|
autoFocus
|
|
167
178
|
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
|
|
168
179
|
/>
|
|
169
180
|
</div>
|
|
170
181
|
<DialogFooter>
|
|
171
182
|
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isSaving}>
|
|
172
|
-
|
|
183
|
+
{labels.cancel}
|
|
173
184
|
</Button>
|
|
174
185
|
<Button onClick={handleSave} disabled={isSaving}>
|
|
175
|
-
{isSaving ?
|
|
186
|
+
{isSaving ? labels.saving : labels.save}
|
|
176
187
|
</Button>
|
|
177
188
|
</DialogFooter>
|
|
178
189
|
</DialogContent>
|
|
@@ -186,7 +197,7 @@ const EditFieldDialog = ({ open, onOpenChange, field, currentValue, onSave, isSa
|
|
|
186
197
|
|
|
187
198
|
const ProfileContent = ({
|
|
188
199
|
onUnauthenticated,
|
|
189
|
-
title
|
|
200
|
+
title,
|
|
190
201
|
enable2FA = false,
|
|
191
202
|
enableDeleteAccount = true,
|
|
192
203
|
}: ProfileLayoutProps) => {
|
|
@@ -194,6 +205,7 @@ const ProfileContent = ({
|
|
|
194
205
|
const [editingField, setEditingField] = useState<EditingField>(null);
|
|
195
206
|
const [isSaving, setIsSaving] = useState(false);
|
|
196
207
|
const [isUploading, setIsUploading] = useState(false);
|
|
208
|
+
const t = useTypedT<I18nTranslations>();
|
|
197
209
|
|
|
198
210
|
// 2FA state
|
|
199
211
|
const [show2FASetup, setShow2FASetup] = useState(false);
|
|
@@ -202,6 +214,56 @@ const ProfileContent = ({
|
|
|
202
214
|
// Delete account state
|
|
203
215
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
204
216
|
|
|
217
|
+
const labels = useMemo(() => ({
|
|
218
|
+
title: title || t('layouts.profilePage.title'),
|
|
219
|
+
// Sections
|
|
220
|
+
account: t('layouts.profilePage.account'),
|
|
221
|
+
personalInfo: t('layouts.profilePage.personalInfo'),
|
|
222
|
+
work: t('layouts.profilePage.work'),
|
|
223
|
+
security: t('layouts.profilePage.security'),
|
|
224
|
+
actions: t('layouts.profilePage.actions'),
|
|
225
|
+
// Fields
|
|
226
|
+
fieldLabels: {
|
|
227
|
+
first_name: t('layouts.profilePage.firstName'),
|
|
228
|
+
last_name: t('layouts.profilePage.lastName'),
|
|
229
|
+
company: t('layouts.profilePage.company'),
|
|
230
|
+
position: t('layouts.profilePage.position'),
|
|
231
|
+
phone: t('layouts.profilePage.phone'),
|
|
232
|
+
} as Record<string, string>,
|
|
233
|
+
email: t('layouts.profilePage.email'),
|
|
234
|
+
// Values
|
|
235
|
+
notSet: t('layouts.profilePage.notSet'),
|
|
236
|
+
on: t('layouts.profilePage.on'),
|
|
237
|
+
off: t('layouts.profilePage.off'),
|
|
238
|
+
// Actions
|
|
239
|
+
save: t('layouts.profilePage.save'),
|
|
240
|
+
saving: t('layouts.profilePage.saving'),
|
|
241
|
+
cancel: t('layouts.profilePage.cancel'),
|
|
242
|
+
signOut: t('layouts.profilePage.signOut'),
|
|
243
|
+
deleteAccount: t('layouts.profilePage.deleteAccount'),
|
|
244
|
+
// 2FA
|
|
245
|
+
twoFactorAuth: t('layouts.profilePage.twoFactorAuth'),
|
|
246
|
+
// Avatar
|
|
247
|
+
avatar: t('layouts.profilePage.avatar'),
|
|
248
|
+
changeAvatar: t('layouts.profilePage.changeAvatar'),
|
|
249
|
+
// Membership
|
|
250
|
+
memberSince: t('layouts.profilePage.memberSince'),
|
|
251
|
+
// Placeholder
|
|
252
|
+
enterField: t('layouts.profilePage.enterField'),
|
|
253
|
+
// Messages
|
|
254
|
+
profileUpdated: t('layouts.profilePage.profileUpdated'),
|
|
255
|
+
avatarUpdated: t('layouts.profilePage.avatarUpdated'),
|
|
256
|
+
failedToUpdate: t('layouts.profilePage.failedToUpdate'),
|
|
257
|
+
failedToUploadAvatar: t('layouts.profilePage.failedToUploadAvatar'),
|
|
258
|
+
selectImageFile: t('layouts.profilePage.selectImageFile'),
|
|
259
|
+
fileTooLarge: t('layouts.profilePage.fileTooLarge'),
|
|
260
|
+
// Not authenticated
|
|
261
|
+
notAuthenticated: t('layouts.profilePage.notAuthenticated'),
|
|
262
|
+
pleaseLogIn: t('layouts.profilePage.pleaseLogIn'),
|
|
263
|
+
// Loading
|
|
264
|
+
loading: t('ui.states.loading'),
|
|
265
|
+
}), [t, title]);
|
|
266
|
+
|
|
205
267
|
const form = useForm<PatchedUserProfileUpdateRequest>({
|
|
206
268
|
resolver: zodResolver(PatchedUserProfileUpdateRequestSchema),
|
|
207
269
|
defaultValues: {
|
|
@@ -247,20 +309,20 @@ const ProfileContent = ({
|
|
|
247
309
|
if (!file) return;
|
|
248
310
|
|
|
249
311
|
if (!file.type.startsWith('image/')) {
|
|
250
|
-
toast.error(
|
|
312
|
+
toast.error(labels.selectImageFile);
|
|
251
313
|
return;
|
|
252
314
|
}
|
|
253
315
|
if (file.size > 5 * 1024 * 1024) {
|
|
254
|
-
toast.error(
|
|
316
|
+
toast.error(labels.fileTooLarge);
|
|
255
317
|
return;
|
|
256
318
|
}
|
|
257
319
|
|
|
258
320
|
setIsUploading(true);
|
|
259
321
|
try {
|
|
260
322
|
await uploadAvatar(file);
|
|
261
|
-
toast.success(
|
|
323
|
+
toast.success(labels.avatarUpdated);
|
|
262
324
|
} catch (error) {
|
|
263
|
-
toast.error(
|
|
325
|
+
toast.error(labels.failedToUploadAvatar);
|
|
264
326
|
profileLogger.error('Avatar upload error:', error);
|
|
265
327
|
} finally {
|
|
266
328
|
setIsUploading(false);
|
|
@@ -271,11 +333,11 @@ const ProfileContent = ({
|
|
|
271
333
|
setIsSaving(true);
|
|
272
334
|
try {
|
|
273
335
|
await updateProfile({ [field]: value });
|
|
274
|
-
toast.success(
|
|
336
|
+
toast.success(labels.profileUpdated);
|
|
275
337
|
setEditingField(null);
|
|
276
338
|
} catch (error: any) {
|
|
277
339
|
profileLogger.error('Profile update error:', error);
|
|
278
|
-
toast.error(error?.response?.data?.[field]?.[0] ||
|
|
340
|
+
toast.error(error?.response?.data?.[field]?.[0] || labels.failedToUpdate);
|
|
279
341
|
} finally {
|
|
280
342
|
setIsSaving(false);
|
|
281
343
|
}
|
|
@@ -290,7 +352,7 @@ const ProfileContent = ({
|
|
|
290
352
|
return (
|
|
291
353
|
<Preloader
|
|
292
354
|
variant="fullscreen"
|
|
293
|
-
text=
|
|
355
|
+
text={labels.loading}
|
|
294
356
|
size="lg"
|
|
295
357
|
backdrop={true}
|
|
296
358
|
backdropOpacity={80}
|
|
@@ -303,8 +365,8 @@ const ProfileContent = ({
|
|
|
303
365
|
return (
|
|
304
366
|
<div className="flex items-center justify-center min-h-screen">
|
|
305
367
|
<div className="text-center">
|
|
306
|
-
<h1 className="text-2xl font-bold mb-4">
|
|
307
|
-
<p className="text-muted-foreground">
|
|
368
|
+
<h1 className="text-2xl font-bold mb-4">{labels.notAuthenticated}</h1>
|
|
369
|
+
<p className="text-muted-foreground">{labels.pleaseLogIn}</p>
|
|
308
370
|
</div>
|
|
309
371
|
</div>
|
|
310
372
|
);
|
|
@@ -314,7 +376,7 @@ const ProfileContent = ({
|
|
|
314
376
|
if (show2FASetup) {
|
|
315
377
|
return (
|
|
316
378
|
<div className="container mx-auto px-4 py-8 max-w-lg">
|
|
317
|
-
<
|
|
379
|
+
<SetupStep
|
|
318
380
|
onComplete={() => {
|
|
319
381
|
setShow2FASetup(false);
|
|
320
382
|
fetch2FAStatus();
|
|
@@ -328,21 +390,21 @@ const ProfileContent = ({
|
|
|
328
390
|
return (
|
|
329
391
|
<div className="container mx-auto px-4 py-8 max-w-lg">
|
|
330
392
|
{/* Header */}
|
|
331
|
-
<h1 className="text-2xl font-bold text-center mb-8">{title}</h1>
|
|
393
|
+
<h1 className="text-2xl font-bold text-center mb-8">{labels.title}</h1>
|
|
332
394
|
|
|
333
395
|
{/* Avatar & Name */}
|
|
334
396
|
<div className="flex flex-col items-center mb-8">
|
|
335
397
|
<div className="relative group mb-3">
|
|
336
398
|
<Avatar className="w-24 h-24 text-3xl">
|
|
337
399
|
{user.avatar ? (
|
|
338
|
-
<img src={user.avatar} alt=
|
|
400
|
+
<img src={user.avatar} alt={labels.avatar} className="w-full h-full object-cover" />
|
|
339
401
|
) : (
|
|
340
402
|
<AvatarFallback className="bg-primary text-primary-foreground">
|
|
341
403
|
{getInitials(user.display_username || user.email || '')}
|
|
342
404
|
</AvatarFallback>
|
|
343
405
|
)}
|
|
344
406
|
</Avatar>
|
|
345
|
-
<label className="absolute inset-0 rounded-full bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer">
|
|
407
|
+
<label className="absolute inset-0 rounded-full bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center cursor-pointer" title={labels.changeAvatar}>
|
|
346
408
|
{isUploading ? (
|
|
347
409
|
<div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
|
348
410
|
) : (
|
|
@@ -360,7 +422,7 @@ const ProfileContent = ({
|
|
|
360
422
|
<h2 className="text-xl font-semibold">{user.display_username || user.email}</h2>
|
|
361
423
|
{user.date_joined && (
|
|
362
424
|
<p className="text-sm text-muted-foreground">
|
|
363
|
-
|
|
425
|
+
{labels.memberSince.replace('{date}', moment.utc(user.date_joined).local().format('MMMM YYYY'))}
|
|
364
426
|
</p>
|
|
365
427
|
)}
|
|
366
428
|
</div>
|
|
@@ -368,11 +430,11 @@ const ProfileContent = ({
|
|
|
368
430
|
<div className="space-y-6">
|
|
369
431
|
{/* Account Info */}
|
|
370
432
|
<div>
|
|
371
|
-
<SettingsGroupHeader>
|
|
433
|
+
<SettingsGroupHeader>{labels.account}</SettingsGroupHeader>
|
|
372
434
|
<SettingsGroup>
|
|
373
435
|
<SettingsRow
|
|
374
436
|
icon={<Mail className="w-5 h-5" />}
|
|
375
|
-
label=
|
|
437
|
+
label={labels.email}
|
|
376
438
|
value={user.email}
|
|
377
439
|
showChevron={false}
|
|
378
440
|
/>
|
|
@@ -381,24 +443,24 @@ const ProfileContent = ({
|
|
|
381
443
|
|
|
382
444
|
{/* Personal Info */}
|
|
383
445
|
<div>
|
|
384
|
-
<SettingsGroupHeader>
|
|
446
|
+
<SettingsGroupHeader>{labels.personalInfo}</SettingsGroupHeader>
|
|
385
447
|
<SettingsGroup>
|
|
386
448
|
<SettingsRow
|
|
387
449
|
icon={<User className="w-5 h-5" />}
|
|
388
|
-
label=
|
|
389
|
-
value={user.first_name ||
|
|
450
|
+
label={labels.fieldLabels.first_name}
|
|
451
|
+
value={user.first_name || labels.notSet}
|
|
390
452
|
onClick={() => setEditingField('first_name')}
|
|
391
453
|
/>
|
|
392
454
|
<SettingsRow
|
|
393
455
|
icon={<User className="w-5 h-5" />}
|
|
394
|
-
label=
|
|
395
|
-
value={user.last_name ||
|
|
456
|
+
label={labels.fieldLabels.last_name}
|
|
457
|
+
value={user.last_name || labels.notSet}
|
|
396
458
|
onClick={() => setEditingField('last_name')}
|
|
397
459
|
/>
|
|
398
460
|
<SettingsRow
|
|
399
461
|
icon={<Phone className="w-5 h-5" />}
|
|
400
|
-
label=
|
|
401
|
-
value={user.phone ||
|
|
462
|
+
label={labels.fieldLabels.phone}
|
|
463
|
+
value={user.phone || labels.notSet}
|
|
402
464
|
onClick={() => setEditingField('phone')}
|
|
403
465
|
/>
|
|
404
466
|
</SettingsGroup>
|
|
@@ -406,18 +468,18 @@ const ProfileContent = ({
|
|
|
406
468
|
|
|
407
469
|
{/* Work Info */}
|
|
408
470
|
<div>
|
|
409
|
-
<SettingsGroupHeader>
|
|
471
|
+
<SettingsGroupHeader>{labels.work}</SettingsGroupHeader>
|
|
410
472
|
<SettingsGroup>
|
|
411
473
|
<SettingsRow
|
|
412
474
|
icon={<Building2 className="w-5 h-5" />}
|
|
413
|
-
label=
|
|
414
|
-
value={user.company ||
|
|
475
|
+
label={labels.fieldLabels.company}
|
|
476
|
+
value={user.company || labels.notSet}
|
|
415
477
|
onClick={() => setEditingField('company')}
|
|
416
478
|
/>
|
|
417
479
|
<SettingsRow
|
|
418
480
|
icon={<Briefcase className="w-5 h-5" />}
|
|
419
|
-
label=
|
|
420
|
-
value={user.position ||
|
|
481
|
+
label={labels.fieldLabels.position}
|
|
482
|
+
value={user.position || labels.notSet}
|
|
421
483
|
onClick={() => setEditingField('position')}
|
|
422
484
|
/>
|
|
423
485
|
</SettingsGroup>
|
|
@@ -426,12 +488,12 @@ const ProfileContent = ({
|
|
|
426
488
|
{/* Security */}
|
|
427
489
|
{enable2FA && (
|
|
428
490
|
<div>
|
|
429
|
-
<SettingsGroupHeader>
|
|
491
|
+
<SettingsGroupHeader>{labels.security}</SettingsGroupHeader>
|
|
430
492
|
<SettingsGroup>
|
|
431
493
|
<SettingsRow
|
|
432
494
|
icon={has2FAEnabled ? <ShieldCheck className="w-5 h-5 text-green-500" /> : <Shield className="w-5 h-5" />}
|
|
433
|
-
label=
|
|
434
|
-
value={has2FAEnabled ?
|
|
495
|
+
label={labels.twoFactorAuth}
|
|
496
|
+
value={has2FAEnabled ? labels.on : labels.off}
|
|
435
497
|
onClick={() => !has2FAEnabled && setShow2FASetup(true)}
|
|
436
498
|
showChevron={!has2FAEnabled}
|
|
437
499
|
/>
|
|
@@ -441,17 +503,17 @@ const ProfileContent = ({
|
|
|
441
503
|
|
|
442
504
|
{/* Actions */}
|
|
443
505
|
<div>
|
|
444
|
-
<SettingsGroupHeader>
|
|
506
|
+
<SettingsGroupHeader>{labels.actions}</SettingsGroupHeader>
|
|
445
507
|
<SettingsGroup>
|
|
446
508
|
<SettingsRow
|
|
447
509
|
icon={<LogOut className="w-5 h-5" />}
|
|
448
|
-
label=
|
|
510
|
+
label={labels.signOut}
|
|
449
511
|
onClick={handleLogout}
|
|
450
512
|
/>
|
|
451
513
|
{enableDeleteAccount && (
|
|
452
514
|
<SettingsRow
|
|
453
515
|
icon={<Trash2 className="w-5 h-5" />}
|
|
454
|
-
label=
|
|
516
|
+
label={labels.deleteAccount}
|
|
455
517
|
onClick={() => setShowDeleteConfirm(true)}
|
|
456
518
|
destructive
|
|
457
519
|
/>
|
|
@@ -485,12 +547,21 @@ const ProfileContent = ({
|
|
|
485
547
|
|
|
486
548
|
import { useDeleteAccount } from '@djangocfg/api/auth';
|
|
487
549
|
|
|
488
|
-
const CONFIRMATION_TEXT = 'DELETE';
|
|
489
|
-
|
|
490
550
|
const DeleteAccountDialog = ({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) => {
|
|
491
551
|
const [confirmationInput, setConfirmationInput] = useState('');
|
|
492
552
|
const { logout } = useAuth();
|
|
493
553
|
const { isLoading, error, deleteAccount, clearError } = useDeleteAccount();
|
|
554
|
+
const t = useTypedT<I18nTranslations>();
|
|
555
|
+
|
|
556
|
+
const labels = useMemo(() => ({
|
|
557
|
+
title: t('layouts.profilePage.deleteAccountTitle'),
|
|
558
|
+
description: t('layouts.profilePage.deleteAccountDesc'),
|
|
559
|
+
typeToConfirm: t('layouts.profilePage.typeToConfirm'),
|
|
560
|
+
confirmationWord: t('layouts.profilePage.confirmationWord'),
|
|
561
|
+
cancel: t('layouts.profilePage.cancel'),
|
|
562
|
+
deleteAccount: t('layouts.profilePage.deleteAccount'),
|
|
563
|
+
deleting: t('layouts.profilePage.deleting'),
|
|
564
|
+
}), [t]);
|
|
494
565
|
|
|
495
566
|
useEffect(() => {
|
|
496
567
|
if (open) {
|
|
@@ -507,15 +578,15 @@ const DeleteAccountDialog = ({ open, onOpenChange }: { open: boolean; onOpenChan
|
|
|
507
578
|
}
|
|
508
579
|
};
|
|
509
580
|
|
|
510
|
-
const isValid = confirmationInput.toUpperCase() ===
|
|
581
|
+
const isValid = confirmationInput.toUpperCase() === labels.confirmationWord.toUpperCase();
|
|
511
582
|
|
|
512
583
|
return (
|
|
513
584
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
514
585
|
<DialogContent>
|
|
515
586
|
<DialogHeader>
|
|
516
|
-
<DialogTitle className="text-destructive">
|
|
587
|
+
<DialogTitle className="text-destructive">{labels.title}</DialogTitle>
|
|
517
588
|
<DialogDescription>
|
|
518
|
-
|
|
589
|
+
{labels.description}
|
|
519
590
|
</DialogDescription>
|
|
520
591
|
</DialogHeader>
|
|
521
592
|
|
|
@@ -525,12 +596,13 @@ const DeleteAccountDialog = ({ open, onOpenChange }: { open: boolean; onOpenChan
|
|
|
525
596
|
)}
|
|
526
597
|
<div className="space-y-2">
|
|
527
598
|
<p className="text-sm text-muted-foreground">
|
|
528
|
-
|
|
599
|
+
{labels.typeToConfirm.replace('{word}', '')}
|
|
600
|
+
<span className="font-mono font-bold">{labels.confirmationWord}</span>
|
|
529
601
|
</p>
|
|
530
602
|
<Input
|
|
531
603
|
value={confirmationInput}
|
|
532
604
|
onChange={(e) => setConfirmationInput(e.target.value)}
|
|
533
|
-
placeholder={
|
|
605
|
+
placeholder={labels.confirmationWord}
|
|
534
606
|
disabled={isLoading}
|
|
535
607
|
autoComplete="off"
|
|
536
608
|
/>
|
|
@@ -539,10 +611,10 @@ const DeleteAccountDialog = ({ open, onOpenChange }: { open: boolean; onOpenChan
|
|
|
539
611
|
|
|
540
612
|
<DialogFooter>
|
|
541
613
|
<Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isLoading}>
|
|
542
|
-
|
|
614
|
+
{labels.cancel}
|
|
543
615
|
</Button>
|
|
544
616
|
<Button variant="destructive" onClick={handleDelete} disabled={isLoading || !isValid}>
|
|
545
|
-
{isLoading ?
|
|
617
|
+
{isLoading ? labels.deleting : labels.deleteAccount}
|
|
546
618
|
</Button>
|
|
547
619
|
</DialogFooter>
|
|
548
620
|
</DialogContent>
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
OTPInput,
|
|
23
23
|
} from '@djangocfg/ui-core/components';
|
|
24
24
|
|
|
25
|
-
import {
|
|
25
|
+
import { SetupStep } from '../../AuthLayout/components/steps/SetupStep';
|
|
26
26
|
|
|
27
27
|
type ViewState = 'status' | 'setup' | 'disable';
|
|
28
28
|
|
|
@@ -102,7 +102,7 @@ export const TwoFactorSection: React.FC = () => {
|
|
|
102
102
|
</CardDescription>
|
|
103
103
|
</CardHeader>
|
|
104
104
|
<CardContent>
|
|
105
|
-
<
|
|
105
|
+
<SetupStep
|
|
106
106
|
onComplete={handleSetupComplete}
|
|
107
107
|
onSkip={handleSetupSkip}
|
|
108
108
|
/>
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
import { X } from 'lucide-react';
|
|
10
10
|
import Link from 'next/link';
|
|
11
|
-
import React from 'react';
|
|
11
|
+
import React, { useMemo } from 'react';
|
|
12
12
|
|
|
13
13
|
import { useAuth } from '@djangocfg/api/auth';
|
|
14
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
14
15
|
import {
|
|
15
16
|
Button, Drawer, DrawerClose, DrawerContent, DrawerHeader, DrawerTitle
|
|
16
17
|
} from '@djangocfg/ui-core/components';
|
|
@@ -38,6 +39,13 @@ export function PublicMobileDrawer({
|
|
|
38
39
|
userMenu,
|
|
39
40
|
}: PublicMobileDrawerProps) {
|
|
40
41
|
const { isAuthenticated } = useAuth();
|
|
42
|
+
const t = useTypedT<I18nTranslations>();
|
|
43
|
+
|
|
44
|
+
const labels = useMemo(() => ({
|
|
45
|
+
closeMenu: t('layouts.mobile.closeMenu'),
|
|
46
|
+
menu: t('layouts.navigation.menu'),
|
|
47
|
+
theme: t('layouts.theme.toggle'),
|
|
48
|
+
}), [t]);
|
|
41
49
|
|
|
42
50
|
return (
|
|
43
51
|
<Drawer open={isOpen} onOpenChange={(open) => !open && onClose()} direction="right">
|
|
@@ -58,7 +66,7 @@ export function PublicMobileDrawer({
|
|
|
58
66
|
</div>
|
|
59
67
|
<DrawerClose className="p-2 rounded-sm transition-colors hover:bg-accent/50">
|
|
60
68
|
<X className="size-5" />
|
|
61
|
-
<span className="sr-only">
|
|
69
|
+
<span className="sr-only">{labels.closeMenu}</span>
|
|
62
70
|
</DrawerClose>
|
|
63
71
|
</DrawerHeader>
|
|
64
72
|
|
|
@@ -75,7 +83,7 @@ export function PublicMobileDrawer({
|
|
|
75
83
|
<div className="space-y-1">
|
|
76
84
|
<div className="px-4 py-2">
|
|
77
85
|
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
78
|
-
|
|
86
|
+
{labels.menu}
|
|
79
87
|
</h3>
|
|
80
88
|
</div>
|
|
81
89
|
<div className="space-y-1">
|
|
@@ -95,7 +103,7 @@ export function PublicMobileDrawer({
|
|
|
95
103
|
{/* Theme Toggle - Fixed at bottom */}
|
|
96
104
|
<div className="border-t border-border/30 p-4">
|
|
97
105
|
<div className="flex items-center justify-between px-4 py-3">
|
|
98
|
-
<span className="text-sm font-medium text-foreground">
|
|
106
|
+
<span className="text-sm font-medium text-foreground">{labels.theme}</span>
|
|
99
107
|
<ThemeToggle />
|
|
100
108
|
</div>
|
|
101
109
|
</div>
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
|
|
9
9
|
import { Menu } from 'lucide-react';
|
|
10
10
|
import Link from 'next/link';
|
|
11
|
-
import React from 'react';
|
|
11
|
+
import React, { useMemo } from 'react';
|
|
12
12
|
|
|
13
13
|
import { useAuth } from '@djangocfg/api/auth';
|
|
14
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
14
15
|
import { Button } from '@djangocfg/ui-core/components';
|
|
15
16
|
import { useIsMobile } from '@djangocfg/ui-core/hooks';
|
|
16
17
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
@@ -37,6 +38,9 @@ export function PublicNavigation({
|
|
|
37
38
|
}: PublicNavigationProps) {
|
|
38
39
|
const { isAuthenticated } = useAuth();
|
|
39
40
|
const isMobile = useIsMobile();
|
|
41
|
+
const t = useTypedT<I18nTranslations>();
|
|
42
|
+
|
|
43
|
+
const toggleMobileLabel = useMemo(() => t('layouts.navigation.toggleMobile'), [t]);
|
|
40
44
|
|
|
41
45
|
// Nav class (background blur with opacity 0.8)
|
|
42
46
|
// bg-background/80 - doesnt work in tailwind4
|
|
@@ -89,7 +93,7 @@ export function PublicNavigation({
|
|
|
89
93
|
variant="ghost"
|
|
90
94
|
size="icon"
|
|
91
95
|
onClick={onMobileMenuClick}
|
|
92
|
-
aria-label=
|
|
96
|
+
aria-label={toggleMobileLabel}
|
|
93
97
|
>
|
|
94
98
|
<Menu className="h-5 w-5" />
|
|
95
99
|
</Button>
|
|
@@ -32,9 +32,10 @@
|
|
|
32
32
|
|
|
33
33
|
import { LogOut } from 'lucide-react';
|
|
34
34
|
import Link from 'next/link';
|
|
35
|
-
import React from 'react';
|
|
35
|
+
import React, { useMemo } from 'react';
|
|
36
36
|
|
|
37
37
|
import { useAuth } from '@djangocfg/api/auth';
|
|
38
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
38
39
|
import {
|
|
39
40
|
Avatar, AvatarFallback, AvatarImage, Button, DropdownMenu, DropdownMenuContent,
|
|
40
41
|
DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator,
|
|
@@ -59,6 +60,13 @@ export function UserMenu({
|
|
|
59
60
|
}: UserMenuProps) {
|
|
60
61
|
const { user, isAuthenticated, logout } = useAuth();
|
|
61
62
|
const [mounted, setMounted] = React.useState(false);
|
|
63
|
+
const t = useTypedT<I18nTranslations>();
|
|
64
|
+
|
|
65
|
+
const labels = useMemo(() => ({
|
|
66
|
+
signIn: t('layouts.profile.login'),
|
|
67
|
+
signOut: t('layouts.profile.signOut'),
|
|
68
|
+
userMenu: t('layouts.profile.userMenu'),
|
|
69
|
+
}), [t]);
|
|
62
70
|
|
|
63
71
|
React.useEffect(() => {
|
|
64
72
|
setMounted(true);
|
|
@@ -78,7 +86,7 @@ export function UserMenu({
|
|
|
78
86
|
allGroups.push({
|
|
79
87
|
items: [
|
|
80
88
|
{
|
|
81
|
-
label:
|
|
89
|
+
label: labels.signOut,
|
|
82
90
|
onClick: () => logout(),
|
|
83
91
|
icon: LogOut,
|
|
84
92
|
variant: 'destructive',
|
|
@@ -87,7 +95,7 @@ export function UserMenu({
|
|
|
87
95
|
});
|
|
88
96
|
|
|
89
97
|
return allGroups;
|
|
90
|
-
}, [groups, logout]);
|
|
98
|
+
}, [groups, logout, labels.signOut]);
|
|
91
99
|
|
|
92
100
|
if (!mounted) {
|
|
93
101
|
return null;
|
|
@@ -99,7 +107,7 @@ export function UserMenu({
|
|
|
99
107
|
return (
|
|
100
108
|
<Link href={authPath}>
|
|
101
109
|
<Button variant="default" className="w-full">
|
|
102
|
-
|
|
110
|
+
{labels.signIn}
|
|
103
111
|
</Button>
|
|
104
112
|
</Link>
|
|
105
113
|
);
|
|
@@ -107,7 +115,7 @@ export function UserMenu({
|
|
|
107
115
|
return (
|
|
108
116
|
<Link href={authPath}>
|
|
109
117
|
<Button variant="default" size="sm">
|
|
110
|
-
|
|
118
|
+
{labels.signIn}
|
|
111
119
|
</Button>
|
|
112
120
|
</Link>
|
|
113
121
|
);
|
|
@@ -194,7 +202,7 @@ export function UserMenu({
|
|
|
194
202
|
<AvatarImage src={userAvatar} alt={displayName} />
|
|
195
203
|
<AvatarFallback>{userInitial}</AvatarFallback>
|
|
196
204
|
</Avatar>
|
|
197
|
-
<span className="sr-only">
|
|
205
|
+
<span className="sr-only">{labels.userMenu}</span>
|
|
198
206
|
</Button>
|
|
199
207
|
</DropdownMenuTrigger>
|
|
200
208
|
<DropdownMenuContent align="end" className="w-56">
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { LogIn } from 'lucide-react';
|
|
4
|
-
import React, { useState } from 'react';
|
|
4
|
+
import React, { useMemo, useState } from 'react';
|
|
5
5
|
|
|
6
|
+
import { useCfgRouter } from '@djangocfg/api/auth';
|
|
7
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
6
8
|
import {
|
|
7
9
|
Button, Dialog, DialogContent, DialogHeader, DialogTitle
|
|
8
10
|
} from '@djangocfg/ui-core/components';
|
|
9
|
-
import { useCfgRouter } from '@djangocfg/api/auth';
|
|
10
11
|
import { useEventListener } from '@djangocfg/ui-core';
|
|
11
12
|
|
|
12
13
|
// Re-export events for backwards compatibility
|
|
@@ -27,7 +28,15 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
|
|
|
27
28
|
authPath = '/auth'
|
|
28
29
|
}) => {
|
|
29
30
|
const [open, setOpen] = useState(false);
|
|
30
|
-
const
|
|
31
|
+
const t = useTypedT<I18nTranslations>();
|
|
32
|
+
|
|
33
|
+
const labels = useMemo(() => ({
|
|
34
|
+
authRequired: t('layouts.auth.authRequired'),
|
|
35
|
+
pleaseSignIn: t('layouts.auth.pleaseSignIn'),
|
|
36
|
+
goToSignIn: t('layouts.auth.goToSignIn'),
|
|
37
|
+
}), [t]);
|
|
38
|
+
|
|
39
|
+
const [message, setMessage] = useState<string>(labels.pleaseSignIn);
|
|
31
40
|
const router = useCfgRouter();
|
|
32
41
|
|
|
33
42
|
// Listen for open auth dialog event
|
|
@@ -44,7 +53,7 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
|
|
|
44
53
|
});
|
|
45
54
|
|
|
46
55
|
const handleClose = () => {
|
|
47
|
-
setMessage(
|
|
56
|
+
setMessage(labels.pleaseSignIn);
|
|
48
57
|
setOpen(false);
|
|
49
58
|
};
|
|
50
59
|
|
|
@@ -67,7 +76,7 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
|
|
|
67
76
|
<Dialog open={open} onOpenChange={handleClose}>
|
|
68
77
|
<DialogContent className="max-w-sm">
|
|
69
78
|
<DialogHeader>
|
|
70
|
-
<DialogTitle>
|
|
79
|
+
<DialogTitle>{labels.authRequired}</DialogTitle>
|
|
71
80
|
</DialogHeader>
|
|
72
81
|
|
|
73
82
|
<div className="space-y-4">
|
|
@@ -75,7 +84,7 @@ export const AuthDialog: React.FC<AuthDialogProps> = ({
|
|
|
75
84
|
|
|
76
85
|
<Button onClick={handleGoToAuth} className="w-full">
|
|
77
86
|
<LogIn className="h-4 w-4 mr-2" />
|
|
78
|
-
|
|
87
|
+
{labels.goToSignIn}
|
|
79
88
|
</Button>
|
|
80
89
|
</div>
|
|
81
90
|
</DialogContent>
|