@djangocfg/layouts 2.1.227 → 2.1.229

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.
Files changed (97) hide show
  1. package/README.md +3 -17
  2. package/package.json +18 -18
  3. package/src/components/errors/ErrorLayout.tsx +2 -2
  4. package/src/components/errors/ErrorsTracker/index.ts +1 -0
  5. package/src/components/errors/ErrorsTracker/utils/formatters.ts +23 -1
  6. package/src/hooks/useLogout.ts +9 -12
  7. package/src/layouts/AppLayout/AppLayout.tsx +20 -8
  8. package/src/layouts/AppLayout/BaseApp.tsx +5 -28
  9. package/src/layouts/AuthLayout/AuthLayout.tsx +51 -22
  10. package/src/layouts/AuthLayout/README.md +78 -0
  11. package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +2 -2
  12. package/src/layouts/AuthLayout/components/shared/AuthError.tsx +10 -2
  13. package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +2 -2
  14. package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +3 -2
  15. package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +4 -1
  16. package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +2 -2
  17. package/src/layouts/AuthLayout/components/shared/index.ts +0 -2
  18. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +25 -80
  19. package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +8 -13
  20. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +2 -2
  21. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +2 -2
  22. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +2 -2
  23. package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +61 -42
  24. package/src/layouts/AuthLayout/context.tsx +0 -2
  25. package/src/layouts/AuthLayout/index.ts +9 -6
  26. package/src/layouts/AuthLayout/styles/auth.css +265 -120
  27. package/src/layouts/AuthLayout/types.ts +60 -7
  28. package/src/layouts/ProfileLayout/.claude/.sidecar/activity.jsonl +2 -0
  29. package/src/layouts/ProfileLayout/.claude/.sidecar/history/2026-03-15.md +35 -0
  30. package/src/layouts/ProfileLayout/.claude/.sidecar/review.md +35 -0
  31. package/src/layouts/ProfileLayout/.claude/.sidecar/scan.log +3 -0
  32. package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-001.md +18 -0
  33. package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-002.md +19 -0
  34. package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-003.md +18 -0
  35. package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-004.md +18 -0
  36. package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-005.md +18 -0
  37. package/src/layouts/ProfileLayout/.claude/.sidecar/usage.json +5 -0
  38. package/src/layouts/ProfileLayout/ProfileLayout.tsx +52 -403
  39. package/src/layouts/ProfileLayout/components/ActionButton.tsx +38 -0
  40. package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +109 -148
  41. package/src/layouts/ProfileLayout/components/EditableField.tsx +119 -0
  42. package/src/layouts/ProfileLayout/components/Section.tsx +22 -0
  43. package/src/layouts/ProfileLayout/components/index.ts +4 -1
  44. package/src/layouts/ProfileLayout/context.tsx +31 -0
  45. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +2 -2
  46. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +2 -2
  47. package/src/layouts/_components/UserMenu.tsx +2 -2
  48. package/src/layouts/types/README.md +0 -20
  49. package/src/layouts/types/index.ts +2 -2
  50. package/src/layouts/types/layout.types.ts +3 -5
  51. package/src/layouts/types/providers.types.ts +0 -27
  52. package/src/snippets/AuthDialog/AuthDialog.tsx +2 -2
  53. package/src/snippets/Breadcrumbs.tsx +2 -2
  54. package/src/snippets/index.ts +0 -69
  55. package/src/layouts/AuthLayout/components/shared/ChannelToggle.tsx +0 -56
  56. package/src/snippets/McpChat/README.md +0 -441
  57. package/src/snippets/McpChat/components/AIChatWidget.tsx +0 -361
  58. package/src/snippets/McpChat/components/AskAIButton.tsx +0 -92
  59. package/src/snippets/McpChat/components/ChatMessages.tsx +0 -138
  60. package/src/snippets/McpChat/components/ChatPanel.tsx +0 -131
  61. package/src/snippets/McpChat/components/ChatSidebar.tsx +0 -156
  62. package/src/snippets/McpChat/components/ChatWidget.tsx +0 -115
  63. package/src/snippets/McpChat/components/MessageBubble.tsx +0 -142
  64. package/src/snippets/McpChat/components/MessageInput.tsx +0 -140
  65. package/src/snippets/McpChat/components/index.ts +0 -24
  66. package/src/snippets/McpChat/config.ts +0 -94
  67. package/src/snippets/McpChat/context/AIChatContext.tsx +0 -327
  68. package/src/snippets/McpChat/context/ChatContext.tsx +0 -361
  69. package/src/snippets/McpChat/context/index.ts +0 -7
  70. package/src/snippets/McpChat/hooks/index.ts +0 -6
  71. package/src/snippets/McpChat/hooks/useAIChat.ts +0 -503
  72. package/src/snippets/McpChat/hooks/useChatLayout.ts +0 -442
  73. package/src/snippets/McpChat/hooks/useMcpChat.ts +0 -90
  74. package/src/snippets/McpChat/index.ts +0 -79
  75. package/src/snippets/McpChat/types.ts +0 -189
  76. package/src/snippets/PWAInstall/@docs/README.md +0 -92
  77. package/src/snippets/PWAInstall/@docs/research/ios-android-install-flows.md +0 -576
  78. package/src/snippets/PWAInstall/README.md +0 -235
  79. package/src/snippets/PWAInstall/components/A2HSHint.tsx +0 -236
  80. package/src/snippets/PWAInstall/components/DesktopGuide.tsx +0 -234
  81. package/src/snippets/PWAInstall/components/IOSGuide.tsx +0 -29
  82. package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +0 -103
  83. package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +0 -103
  84. package/src/snippets/PWAInstall/components/PWAPageResumeManager.tsx +0 -33
  85. package/src/snippets/PWAInstall/context/InstallContext.tsx +0 -102
  86. package/src/snippets/PWAInstall/hooks/useInstallPrompt.ts +0 -168
  87. package/src/snippets/PWAInstall/hooks/useIsPWA.ts +0 -116
  88. package/src/snippets/PWAInstall/hooks/usePWAPageResume.ts +0 -163
  89. package/src/snippets/PWAInstall/index.ts +0 -80
  90. package/src/snippets/PWAInstall/types/components.ts +0 -95
  91. package/src/snippets/PWAInstall/types/config.ts +0 -29
  92. package/src/snippets/PWAInstall/types/index.ts +0 -26
  93. package/src/snippets/PWAInstall/types/install.ts +0 -38
  94. package/src/snippets/PWAInstall/types/platform.ts +0 -29
  95. package/src/snippets/PWAInstall/utils/localStorage.ts +0 -181
  96. package/src/snippets/PWAInstall/utils/logger.ts +0 -149
  97. package/src/snippets/PWAInstall/utils/platform.ts +0 -151
@@ -1,38 +1,31 @@
1
1
  'use client';
2
2
 
3
- import { Camera, LogOut, Trash2 } from 'lucide-react';
3
+ import { Camera, LogOut } from 'lucide-react';
4
4
  import moment from 'moment';
5
5
  import React, { useEffect, useMemo, useState } from 'react';
6
- import { useForm } from 'react-hook-form';
7
6
 
8
- import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
7
+ import { useAppT } from '@djangocfg/i18n';
9
8
  import { toast } from '@djangocfg/ui-core/hooks';
10
- import { cn } from '@djangocfg/ui-core/lib';
11
9
 
12
- import {
13
- PatchedUserProfileUpdateRequest,
14
- PatchedUserProfileUpdateRequestSchema,
15
- useAuth,
16
- useTwoFactorStatus,
17
- } from '@djangocfg/api/auth';
10
+ import { useAuth } from '@djangocfg/api/auth';
18
11
  import {
19
12
  Avatar,
20
13
  AvatarFallback,
21
- Dialog,
22
- DialogContent,
23
- DialogDescription,
24
- DialogFooter,
25
- DialogHeader,
26
- DialogTitle,
27
- Button,
28
- Input,
29
14
  Preloader,
30
15
  } from '@djangocfg/ui-core/components';
31
- import { zodResolver } from '@hookform/resolvers/zod';
16
+ import { cn } from '@djangocfg/ui-core/lib';
32
17
 
33
- import { SetupStep } from '../AuthLayout/components/steps/SetupStep';
34
18
  import { profileLogger } from '../../utils/logger';
35
19
  import { useLogout } from '../../hooks';
20
+ import {
21
+ ActionButton,
22
+ DeleteAccountScreen,
23
+ DeleteAccountSection,
24
+ EditableField,
25
+ Section,
26
+ TwoFactorSection,
27
+ } from './components';
28
+ import { ProfileProvider, useProfileContext } from './context';
36
29
 
37
30
  // ─────────────────────────────────────────────────────────────────────────────
38
31
  // Types
@@ -45,155 +38,8 @@ interface ProfileLayoutProps {
45
38
  enableDeleteAccount?: boolean;
46
39
  }
47
40
 
48
- // Reserved for future use
49
- // type EditingField = 'first_name' | 'last_name' | 'company' | 'position' | 'phone' | null;
50
-
51
- // ─────────────────────────────────────────────────────────────────────────────
52
- // Editable Field Component
53
- // ─────────────────────────────────────────────────────────────────────────────
54
-
55
- interface EditableFieldProps {
56
- label: string;
57
- value: string;
58
- placeholder: string;
59
- onSave: (value: string) => Promise<void>;
60
- disabled?: boolean;
61
- }
62
-
63
- const EditableField = ({ label, value, placeholder, onSave, disabled }: EditableFieldProps) => {
64
- const [isEditing, setIsEditing] = useState(false);
65
- const [editValue, setEditValue] = useState(value);
66
- const [isSaving, setIsSaving] = useState(false);
67
-
68
- useEffect(() => {
69
- setEditValue(value);
70
- }, [value]);
71
-
72
- const handleSave = async () => {
73
- if (editValue === value) {
74
- setIsEditing(false);
75
- return;
76
- }
77
- setIsSaving(true);
78
- try {
79
- await onSave(editValue);
80
- setIsEditing(false);
81
- } finally {
82
- setIsSaving(false);
83
- }
84
- };
85
-
86
- const handleKeyDown = (e: React.KeyboardEvent) => {
87
- if (e.key === 'Enter') {
88
- handleSave();
89
- } else if (e.key === 'Escape') {
90
- setEditValue(value);
91
- setIsEditing(false);
92
- }
93
- };
94
-
95
- if (isEditing) {
96
- return (
97
- <div className="py-4 border-b border-border/50 last:border-0">
98
- <label className="block text-[13px] text-muted-foreground mb-1.5">
99
- {label}
100
- </label>
101
- <div className="flex items-center gap-2">
102
- <Input
103
- value={editValue}
104
- onChange={(e) => setEditValue(e.target.value)}
105
- onKeyDown={handleKeyDown}
106
- onBlur={handleSave}
107
- placeholder={placeholder}
108
- autoFocus
109
- disabled={isSaving}
110
- className="h-9 text-[15px]"
111
- />
112
- </div>
113
- </div>
114
- );
115
- }
116
-
117
- return (
118
- <button
119
- type="button"
120
- onClick={() => !disabled && setIsEditing(true)}
121
- disabled={disabled}
122
- className={cn(
123
- 'w-full py-4 border-b border-border/50 last:border-0 text-left',
124
- 'transition-colors hover:bg-muted/30',
125
- disabled && 'cursor-default hover:bg-transparent'
126
- )}
127
- >
128
- <div className="text-[13px] text-muted-foreground mb-0.5">
129
- {label}
130
- </div>
131
- <div className={cn(
132
- 'text-[15px]',
133
- value ? 'text-foreground' : 'text-muted-foreground/60'
134
- )}>
135
- {value || placeholder}
136
- </div>
137
- </button>
138
- );
139
- };
140
-
141
- // ─────────────────────────────────────────────────────────────────────────────
142
- // Section Component
143
- // ─────────────────────────────────────────────────────────────────────────────
144
-
145
- interface SectionProps {
146
- title?: string;
147
- children: React.ReactNode;
148
- className?: string;
149
- }
150
-
151
- const Section = ({ title, children, className }: SectionProps) => (
152
- <div className={cn('mb-10', className)}>
153
- {title && (
154
- <h2 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2 px-1">
155
- {title}
156
- </h2>
157
- )}
158
- <div className="bg-card rounded-xl px-4">
159
- {children}
160
- </div>
161
- </div>
162
- );
163
-
164
41
  // ─────────────────────────────────────────────────────────────────────────────
165
- // Action Button Component
166
- // ─────────────────────────────────────────────────────────────────────────────
167
-
168
- interface ActionButtonProps {
169
- icon?: React.ReactNode;
170
- label: string;
171
- onClick: () => void;
172
- variant?: 'default' | 'destructive';
173
- disabled?: boolean;
174
- }
175
-
176
- const ActionButton = ({ icon, label, onClick, variant = 'default', disabled }: ActionButtonProps) => (
177
- <button
178
- type="button"
179
- onClick={onClick}
180
- disabled={disabled}
181
- className={cn(
182
- 'w-full flex items-center justify-center gap-2 py-3.5 border-b border-border/50 last:border-0',
183
- 'text-[15px] font-medium transition-colors',
184
- variant === 'destructive'
185
- ? 'text-destructive hover:bg-destructive/5'
186
- : 'text-foreground hover:bg-muted/30',
187
- disabled && 'opacity-50 cursor-not-allowed'
188
- )}
189
- >
190
- {icon}
191
- {label}
192
- </button>
193
- );
194
-
195
- // ─────────────────────────────────────────────────────────────────────────────
196
- // Main Component
42
+ // Profile Content
197
43
  // ─────────────────────────────────────────────────────────────────────────────
198
44
 
199
45
  const ProfileContent = ({
@@ -204,113 +50,48 @@ const ProfileContent = ({
204
50
  }: ProfileLayoutProps) => {
205
51
  const { user, isLoading, uploadAvatar, updateProfile } = useAuth();
206
52
  const [isUploading, setIsUploading] = useState(false);
207
- const t = useTypedT<I18nTranslations>();
208
-
209
- // 2FA state
210
- const [show2FASetup, setShow2FASetup] = useState(false);
211
- const { has2FAEnabled, fetchStatus: fetch2FAStatus } = useTwoFactorStatus();
212
-
213
- // Delete account state
214
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
53
+ const t = useAppT();
215
54
 
216
55
  const labels = useMemo(() => ({
217
56
  title: title || t('layouts.profilePage.title'),
218
- // Sections
219
57
  personalInfo: t('layouts.profilePage.personalInfo'),
220
58
  work: t('layouts.profilePage.work'),
221
59
  security: t('layouts.profilePage.security'),
222
- // Fields
223
60
  firstName: t('layouts.profilePage.firstName'),
224
61
  lastName: t('layouts.profilePage.lastName'),
225
62
  email: t('layouts.profilePage.email'),
226
63
  phone: t('layouts.profilePage.phone'),
227
64
  company: t('layouts.profilePage.company'),
228
65
  position: t('layouts.profilePage.position'),
229
- // Placeholders
230
66
  notSet: t('layouts.profilePage.notSet'),
231
67
  addFirstName: t('layouts.profilePage.addFirstName') || 'Add first name',
232
68
  addLastName: t('layouts.profilePage.addLastName') || 'Add last name',
233
69
  addPhone: t('layouts.profilePage.addPhone') || 'Add phone number',
234
70
  addCompany: t('layouts.profilePage.addCompany') || 'Add company',
235
71
  addPosition: t('layouts.profilePage.addPosition') || 'Add position',
236
- // 2FA
237
- twoFactorAuth: t('layouts.profilePage.twoFactorAuth'),
238
- twoFactorEnabled: t('layouts.profilePage.twoFactorEnabled') || 'Two-factor authentication is enabled',
239
- twoFactorDisabled: t('layouts.profilePage.twoFactorDisabled') || 'Add an extra layer of security',
240
- enable2FA: t('layouts.profilePage.enable2FA') || 'Enable 2FA',
241
- // Actions
242
72
  signOut: t('layouts.profilePage.signOut'),
243
- deleteAccount: t('layouts.profilePage.deleteAccount'),
244
- // Avatar
245
73
  changeAvatar: t('layouts.profilePage.changeAvatar'),
246
- // Membership
247
74
  memberSince: t('layouts.profilePage.memberSince'),
248
- // Messages
249
75
  profileUpdated: t('layouts.profilePage.profileUpdated'),
250
76
  avatarUpdated: t('layouts.profilePage.avatarUpdated'),
251
77
  failedToUpdate: t('layouts.profilePage.failedToUpdate'),
252
78
  failedToUploadAvatar: t('layouts.profilePage.failedToUploadAvatar'),
253
79
  selectImageFile: t('layouts.profilePage.selectImageFile'),
254
80
  fileTooLarge: t('layouts.profilePage.fileTooLarge'),
255
- // Not authenticated
256
81
  notAuthenticated: t('layouts.profilePage.notAuthenticated'),
257
82
  pleaseLogIn: t('layouts.profilePage.pleaseLogIn'),
258
- // Loading
259
83
  loading: t('ui.states.loading'),
260
84
  }), [t, title]);
261
85
 
262
- const form = useForm<PatchedUserProfileUpdateRequest>({
263
- resolver: zodResolver(PatchedUserProfileUpdateRequestSchema),
264
- defaultValues: {
265
- first_name: '',
266
- last_name: '',
267
- company: '',
268
- position: '',
269
- phone: '',
270
- },
271
- });
272
-
273
- useEffect(() => {
274
- if (user) {
275
- form.reset({
276
- first_name: user.first_name || '',
277
- last_name: user.last_name || '',
278
- company: user.company || '',
279
- position: user.position || '',
280
- phone: user.phone || '',
281
- });
282
- }
283
- }, [user, form]);
284
-
285
- useEffect(() => {
286
- if (enable2FA) {
287
- fetch2FAStatus();
288
- }
289
- }, [enable2FA, fetch2FAStatus]);
290
-
291
86
  useEffect(() => {
292
- if (onUnauthenticated && !user && !isLoading) {
293
- onUnauthenticated();
294
- }
87
+ if (onUnauthenticated && !user && !isLoading) onUnauthenticated();
295
88
  }, [onUnauthenticated, user, isLoading]);
296
89
 
297
- const getInitials = (name: string) => {
298
- if (!name) return 'U';
299
- return name.split(' ').map((w) => w[0]).join('').toUpperCase().slice(0, 2);
300
- };
301
-
302
90
  const handleAvatarChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
303
91
  const file = event.target.files?.[0];
304
92
  if (!file) return;
305
-
306
- if (!file.type.startsWith('image/')) {
307
- toast.error(labels.selectImageFile);
308
- return;
309
- }
310
- if (file.size > 5 * 1024 * 1024) {
311
- toast.error(labels.fileTooLarge);
312
- return;
313
- }
93
+ if (!file.type.startsWith('image/')) { toast.error(labels.selectImageFile); return; }
94
+ if (file.size > 5 * 1024 * 1024) { toast.error(labels.fileTooLarge); return; }
314
95
 
315
96
  setIsUploading(true);
316
97
  try {
@@ -337,20 +118,12 @@ const ProfileContent = ({
337
118
 
338
119
  const handleLogout = useLogout();
339
120
 
340
- // Loading state
341
121
  if (isLoading) {
342
122
  return (
343
- <Preloader
344
- variant="fullscreen"
345
- text={labels.loading}
346
- size="lg"
347
- backdrop={true}
348
- backdropOpacity={80}
349
- />
123
+ <Preloader variant="fullscreen" text={labels.loading} size="lg" backdrop backdropOpacity={80} />
350
124
  );
351
125
  }
352
126
 
353
- // Not authenticated
354
127
  if (!user) {
355
128
  return (
356
129
  <div className="flex items-center justify-center min-h-screen">
@@ -362,24 +135,20 @@ const ProfileContent = ({
362
135
  );
363
136
  }
364
137
 
365
- // 2FA Setup view
366
- if (show2FASetup) {
367
- return (
368
- <div className="container mx-auto px-4 py-8 max-w-lg">
369
- <SetupStep
370
- onComplete={() => {
371
- setShow2FASetup(false);
372
- fetch2FAStatus();
373
- }}
374
- onSkip={() => setShow2FASetup(false)}
375
- />
376
- </div>
377
- );
378
- }
138
+ const getInitials = (name: string) =>
139
+ name ? name.split(' ').map((w) => w[0]).join('').toUpperCase().slice(0, 2) : 'U';
140
+
141
+ const displayName = user.full_name || user.display_username || user.email;
142
+ const memberSinceText = useMemo(
143
+ () => user.date_joined
144
+ ? labels.memberSince.replace('{date}', moment.utc(user.date_joined).local().format('MMMM YYYY'))
145
+ : null,
146
+ [user.date_joined, labels.memberSince]
147
+ );
379
148
 
380
149
  return (
381
150
  <div className="container mx-auto px-4 py-12 max-w-md">
382
- {/* Header with Avatar */}
151
+ {/* Avatar + header */}
383
152
  <div className="flex flex-col items-center mb-12">
384
153
  <div className="relative group mb-4">
385
154
  <Avatar className="w-28 h-28 text-3xl">
@@ -415,18 +184,10 @@ const ProfileContent = ({
415
184
  </label>
416
185
  </div>
417
186
 
418
- <h1 className="text-2xl font-semibold tracking-tight">
419
- {user.full_name || user.display_username || user.email}
420
- </h1>
421
-
422
- <p className="text-[15px] text-muted-foreground mt-1">
423
- {user.email}
424
- </p>
425
-
426
- {user.date_joined && (
427
- <p className="text-[13px] text-muted-foreground/60 mt-2">
428
- {labels.memberSince.replace('{date}', moment.utc(user.date_joined).local().format('MMMM YYYY'))}
429
- </p>
187
+ <h1 className="text-2xl font-semibold tracking-tight">{displayName}</h1>
188
+ <p className="text-[15px] text-muted-foreground mt-1">{user.email}</p>
189
+ {memberSinceText && (
190
+ <p className="text-[13px] text-muted-foreground/60 mt-2">{memberSinceText}</p>
430
191
  )}
431
192
  </div>
432
193
 
@@ -436,19 +197,20 @@ const ProfileContent = ({
436
197
  label={labels.firstName}
437
198
  value={user.first_name || ''}
438
199
  placeholder={labels.addFirstName}
439
- onSave={(value) => handleFieldSave('first_name', value)}
200
+ onSave={(v) => handleFieldSave('first_name', v)}
440
201
  />
441
202
  <EditableField
442
203
  label={labels.lastName}
443
204
  value={user.last_name || ''}
444
205
  placeholder={labels.addLastName}
445
- onSave={(value) => handleFieldSave('last_name', value)}
206
+ onSave={(v) => handleFieldSave('last_name', v)}
446
207
  />
447
208
  <EditableField
448
209
  label={labels.phone}
449
210
  value={user.phone || ''}
450
211
  placeholder={labels.addPhone}
451
- onSave={(value) => handleFieldSave('phone', value)}
212
+ onSave={(v) => handleFieldSave('phone', v)}
213
+ type="phone"
452
214
  />
453
215
  </Section>
454
216
 
@@ -458,46 +220,18 @@ const ProfileContent = ({
458
220
  label={labels.company}
459
221
  value={user.company || ''}
460
222
  placeholder={labels.addCompany}
461
- onSave={(value) => handleFieldSave('company', value)}
223
+ onSave={(v) => handleFieldSave('company', v)}
462
224
  />
463
225
  <EditableField
464
226
  label={labels.position}
465
227
  value={user.position || ''}
466
228
  placeholder={labels.addPosition}
467
- onSave={(value) => handleFieldSave('position', value)}
229
+ onSave={(v) => handleFieldSave('position', v)}
468
230
  />
469
231
  </Section>
470
232
 
471
233
  {/* Security */}
472
- {enable2FA && (
473
- <Section title={labels.security}>
474
- <div className="py-4 border-b border-border/50 last:border-0">
475
- <div className="flex items-center justify-between">
476
- <div>
477
- <div className="text-[15px] font-medium">
478
- {labels.twoFactorAuth}
479
- </div>
480
- <div className="text-[13px] text-muted-foreground mt-0.5">
481
- {has2FAEnabled ? labels.twoFactorEnabled : labels.twoFactorDisabled}
482
- </div>
483
- </div>
484
- {!has2FAEnabled && (
485
- <Button
486
- variant="outline"
487
- size="sm"
488
- onClick={() => setShow2FASetup(true)}
489
- className="text-[13px] h-8"
490
- >
491
- {labels.enable2FA}
492
- </Button>
493
- )}
494
- {has2FAEnabled && (
495
- <div className="w-2 h-2 rounded-full bg-green-500" />
496
- )}
497
- </div>
498
- </div>
499
- </Section>
500
- )}
234
+ {enable2FA && <TwoFactorSection />}
501
235
 
502
236
  {/* Actions */}
503
237
  <Section>
@@ -506,111 +240,26 @@ const ProfileContent = ({
506
240
  label={labels.signOut}
507
241
  onClick={handleLogout}
508
242
  />
509
- {enableDeleteAccount && (
510
- <ActionButton
511
- icon={<Trash2 className="w-4 h-4" />}
512
- label={labels.deleteAccount}
513
- onClick={() => setShowDeleteConfirm(true)}
514
- variant="destructive"
515
- />
516
- )}
517
243
  </Section>
518
244
 
519
- {/* Delete Account Dialog */}
520
- <DeleteAccountDialog
521
- open={showDeleteConfirm}
522
- onOpenChange={setShowDeleteConfirm}
523
- />
245
+ {enableDeleteAccount && <DeleteAccountSection />}
524
246
  </div>
525
247
  );
526
248
  };
527
249
 
528
- // ─────────────────────────────────────────────────────────────────────────────
529
- // Delete Account Dialog
530
- // ─────────────────────────────────────────────────────────────────────────────
531
-
532
- import { useDeleteAccount } from '@djangocfg/api/auth';
533
-
534
- const DeleteAccountDialog = ({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) => {
535
- const [confirmationInput, setConfirmationInput] = useState('');
536
- const { logout } = useAuth();
537
- const { isLoading, error, deleteAccount, clearError } = useDeleteAccount();
538
- const t = useTypedT<I18nTranslations>();
539
-
540
- const labels = useMemo(() => ({
541
- title: t('layouts.profilePage.deleteAccountTitle'),
542
- description: t('layouts.profilePage.deleteAccountDesc'),
543
- typeToConfirm: t('layouts.profilePage.typeToConfirm'),
544
- confirmationWord: t('layouts.profilePage.confirmationWord'),
545
- cancel: t('layouts.profilePage.cancel'),
546
- deleteAccount: t('layouts.profilePage.deleteAccount'),
547
- deleting: t('layouts.profilePage.deleting'),
548
- }), [t]);
549
-
550
- useEffect(() => {
551
- if (open) {
552
- setConfirmationInput('');
553
- clearError();
554
- }
555
- }, [open, clearError]);
556
-
557
- const handleDelete = async () => {
558
- const result = await deleteAccount();
559
- if (result.success) {
560
- onOpenChange(false);
561
- logout();
562
- }
563
- };
564
-
565
- const isValid = confirmationInput.toUpperCase() === labels.confirmationWord.toUpperCase();
566
-
567
- return (
568
- <Dialog open={open} onOpenChange={onOpenChange}>
569
- <DialogContent className="sm:max-w-md">
570
- <DialogHeader>
571
- <DialogTitle>{labels.title}</DialogTitle>
572
- <DialogDescription>
573
- {labels.description}
574
- </DialogDescription>
575
- </DialogHeader>
576
-
577
- <div className="py-4 space-y-4">
578
- {error && (
579
- <p className="text-sm text-destructive">{error}</p>
580
- )}
581
- <div className="space-y-2">
582
- <p className="text-[13px] text-muted-foreground">
583
- {labels.typeToConfirm.replace('{word}', '')}
584
- <span className="font-mono font-semibold text-foreground">{labels.confirmationWord}</span>
585
- </p>
586
- <Input
587
- value={confirmationInput}
588
- onChange={(e) => setConfirmationInput(e.target.value)}
589
- placeholder={labels.confirmationWord}
590
- disabled={isLoading}
591
- autoComplete="off"
592
- className="font-mono"
593
- />
594
- </div>
595
- </div>
596
-
597
- <DialogFooter>
598
- <Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isLoading}>
599
- {labels.cancel}
600
- </Button>
601
- <Button variant="destructive" onClick={handleDelete} disabled={isLoading || !isValid}>
602
- {isLoading ? labels.deleting : labels.deleteAccount}
603
- </Button>
604
- </DialogFooter>
605
- </DialogContent>
606
- </Dialog>
607
- );
608
- };
609
-
610
250
  // ─────────────────────────────────────────────────────────────────────────────
611
251
  // Export
612
252
  // ─────────────────────────────────────────────────────────────────────────────
613
253
 
614
- export const ProfileLayout: React.FC<ProfileLayoutProps> = (props) => {
254
+ const ProfileRouter: React.FC<ProfileLayoutProps> = (props) => {
255
+ const { step } = useProfileContext();
256
+
257
+ if (step === 'delete-account') return <DeleteAccountScreen />;
615
258
  return <ProfileContent {...props} />;
616
259
  };
260
+
261
+ export const ProfileLayout: React.FC<ProfileLayoutProps> = (props) => (
262
+ <ProfileProvider>
263
+ <ProfileRouter {...props} />
264
+ </ProfileProvider>
265
+ );
@@ -0,0 +1,38 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ import { cn } from '@djangocfg/ui-core/lib';
6
+
7
+ interface ActionButtonProps {
8
+ icon?: React.ReactNode;
9
+ label: React.ReactNode;
10
+ onClick: () => void;
11
+ variant?: 'default' | 'destructive';
12
+ disabled?: boolean;
13
+ }
14
+
15
+ export const ActionButton = ({
16
+ icon,
17
+ label,
18
+ onClick,
19
+ variant = 'default',
20
+ disabled,
21
+ }: ActionButtonProps) => (
22
+ <button
23
+ type="button"
24
+ onClick={onClick}
25
+ disabled={disabled}
26
+ className={cn(
27
+ 'w-full flex items-center justify-center gap-2 py-3.5 border-b border-border/50 last:border-0',
28
+ 'text-[15px] font-medium transition-colors',
29
+ variant === 'destructive'
30
+ ? 'text-destructive hover:bg-destructive/5'
31
+ : 'text-foreground hover:bg-muted/30',
32
+ disabled && 'opacity-50 cursor-not-allowed'
33
+ )}
34
+ >
35
+ {icon}
36
+ {label}
37
+ </button>
38
+ );