@djangocfg/layouts 2.1.101 → 2.1.102

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 (49) hide show
  1. package/dist/AIChatWidget-LUPM7S2O.mjs +1644 -0
  2. package/dist/AIChatWidget-LUPM7S2O.mjs.map +1 -0
  3. package/dist/AIChatWidget-O23TJJ7C.mjs +3 -0
  4. package/dist/AIChatWidget-O23TJJ7C.mjs.map +1 -0
  5. package/dist/chunk-53YKWR6F.mjs +6 -0
  6. package/dist/chunk-53YKWR6F.mjs.map +1 -0
  7. package/dist/chunk-EI7TDN2G.mjs +1652 -0
  8. package/dist/chunk-EI7TDN2G.mjs.map +1 -0
  9. package/dist/components.cjs +925 -0
  10. package/dist/components.cjs.map +1 -0
  11. package/dist/components.d.mts +583 -0
  12. package/dist/components.d.ts +583 -0
  13. package/dist/components.mjs +879 -0
  14. package/dist/components.mjs.map +1 -0
  15. package/dist/index.cjs +7573 -0
  16. package/dist/index.cjs.map +1 -0
  17. package/dist/index.d.mts +2376 -0
  18. package/dist/index.d.ts +2376 -0
  19. package/dist/index.mjs +5673 -0
  20. package/dist/index.mjs.map +1 -0
  21. package/dist/layouts.cjs +6530 -0
  22. package/dist/layouts.cjs.map +1 -0
  23. package/dist/layouts.d.mts +748 -0
  24. package/dist/layouts.d.ts +748 -0
  25. package/dist/layouts.mjs +4741 -0
  26. package/dist/layouts.mjs.map +1 -0
  27. package/dist/pages.cjs +178 -0
  28. package/dist/pages.cjs.map +1 -0
  29. package/dist/pages.d.mts +57 -0
  30. package/dist/pages.d.ts +57 -0
  31. package/dist/pages.mjs +168 -0
  32. package/dist/pages.mjs.map +1 -0
  33. package/dist/snippets.cjs +3793 -0
  34. package/dist/snippets.cjs.map +1 -0
  35. package/dist/snippets.d.mts +1192 -0
  36. package/dist/snippets.d.ts +1192 -0
  37. package/dist/snippets.mjs +3738 -0
  38. package/dist/snippets.mjs.map +1 -0
  39. package/dist/utils.cjs +34 -0
  40. package/dist/utils.cjs.map +1 -0
  41. package/dist/utils.d.mts +40 -0
  42. package/dist/utils.d.ts +40 -0
  43. package/dist/utils.mjs +25 -0
  44. package/dist/utils.mjs.map +1 -0
  45. package/package.json +38 -47
  46. package/src/components/errors/ErrorsTracker/components/ErrorButtons.tsx +2 -1
  47. package/src/layouts/ProfileLayout/ProfileLayout.tsx +507 -86
  48. package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +2 -2
  49. package/src/snippets/AuthDialog/useAuthDialog.ts +1 -1
@@ -1,138 +1,559 @@
1
- // @ts-nocheck
2
1
  'use client';
3
2
 
4
- import React from 'react';
3
+ import { Camera, ChevronRight, LogOut, Mail, Phone, Shield, ShieldCheck, Trash2, User, Building2, Briefcase } from 'lucide-react';
5
4
  import moment from 'moment';
5
+ import React, { useEffect, useState } from 'react';
6
+ import { useForm } from 'react-hook-form';
7
+ import { toast } from '@djangocfg/ui-core/hooks';
8
+ import { cn } from '@djangocfg/ui-core/lib';
6
9
 
7
- import { useAuth } from '@djangocfg/api/auth';
8
10
  import {
9
- Card, CardContent, CardDescription, CardHeader, CardTitle, Preloader
11
+ PatchedUserProfileUpdateRequest,
12
+ PatchedUserProfileUpdateRequestSchema,
13
+ useAuth,
14
+ useTwoFactorStatus,
15
+ } from '@djangocfg/api/auth';
16
+ import {
17
+ Avatar,
18
+ AvatarFallback,
19
+ Dialog,
20
+ DialogContent,
21
+ DialogDescription,
22
+ DialogFooter,
23
+ DialogHeader,
24
+ DialogTitle,
25
+ Button,
26
+ Input,
27
+ Preloader,
10
28
  } from '@djangocfg/ui-nextjs/components';
29
+ import { zodResolver } from '@hookform/resolvers/zod';
30
+
31
+ import { TwoFactorSetup } from '../AuthLayout/components/TwoFactorSetup';
32
+ import { profileLogger } from '../../utils/logger';
11
33
 
12
- import { AvatarSection, DeleteAccountSection, ProfileForm, TwoFactorSection } from './components';
34
+ // ─────────────────────────────────────────────────────────────────────────────
35
+ // Types
36
+ // ─────────────────────────────────────────────────────────────────────────────
13
37
 
14
38
  interface ProfileLayoutProps {
15
- // Callbacks
16
39
  onUnauthenticated?: () => void;
17
-
18
- // Optional customization
19
40
  title?: string;
20
- description?: string;
21
- showMemberSince?: boolean;
22
- showLastLogin?: boolean;
23
-
24
- /**
25
- * Enable 2FA management section in profile.
26
- * When true, users can enable/disable 2FA from their profile.
27
- * @default false
28
- */
29
41
  enable2FA?: boolean;
30
-
31
- /**
32
- * Enable account deletion section in profile.
33
- * When true, users can delete their account from their profile.
34
- * @default true
35
- */
36
42
  enableDeleteAccount?: boolean;
37
43
  }
38
44
 
45
+ type EditingField = 'first_name' | 'last_name' | 'company' | 'position' | 'phone' | null;
46
+
47
+ // ─────────────────────────────────────────────────────────────────────────────
48
+ // Reusable Components
49
+ // ─────────────────────────────────────────────────────────────────────────────
50
+
51
+ /** Apple-style settings group container */
52
+ const SettingsGroup = ({ children, className }: { children: React.ReactNode; className?: string }) => (
53
+ <div className={cn('bg-card rounded-xl overflow-hidden divide-y divide-border', className)}>
54
+ {children}
55
+ </div>
56
+ );
57
+
58
+ /** Apple-style settings group header */
59
+ const SettingsGroupHeader = ({ children }: { children: React.ReactNode }) => (
60
+ <div className="px-4 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider">
61
+ {children}
62
+ </div>
63
+ );
64
+
65
+ /** Apple-style settings row */
66
+ interface SettingsRowProps {
67
+ icon?: React.ReactNode;
68
+ label: string;
69
+ value?: string | React.ReactNode;
70
+ onClick?: () => void;
71
+ destructive?: boolean;
72
+ disabled?: boolean;
73
+ showChevron?: boolean;
74
+ className?: string;
75
+ }
76
+
77
+ const SettingsRow = ({
78
+ icon,
79
+ label,
80
+ value,
81
+ onClick,
82
+ destructive,
83
+ disabled,
84
+ showChevron = !!onClick,
85
+ className,
86
+ }: SettingsRowProps) => {
87
+ const Wrapper = onClick ? 'button' : 'div';
88
+
89
+ return (
90
+ <Wrapper
91
+ onClick={disabled ? undefined : onClick}
92
+ disabled={disabled}
93
+ className={cn(
94
+ 'w-full flex items-center gap-3 px-4 py-3 text-left transition-colors',
95
+ onClick && !disabled && 'hover:bg-muted/50 active:bg-muted cursor-pointer',
96
+ disabled && 'opacity-50 cursor-not-allowed',
97
+ destructive && 'text-destructive',
98
+ className
99
+ )}
100
+ >
101
+ {icon && (
102
+ <span className={cn('shrink-0', destructive ? 'text-destructive' : 'text-muted-foreground')}>
103
+ {icon}
104
+ </span>
105
+ )}
106
+ <span className="flex-1 font-medium">{label}</span>
107
+ {value && (
108
+ <span className="text-muted-foreground text-sm truncate max-w-[180px]">
109
+ {value}
110
+ </span>
111
+ )}
112
+ {showChevron && (
113
+ <ChevronRight className="w-4 h-4 text-muted-foreground/50 shrink-0" />
114
+ )}
115
+ </Wrapper>
116
+ );
117
+ };
118
+
119
+ // ─────────────────────────────────────────────────────────────────────────────
120
+ // Edit Field Dialog
121
+ // ─────────────────────────────────────────────────────────────────────────────
122
+
123
+ interface EditFieldDialogProps {
124
+ open: boolean;
125
+ onOpenChange: (open: boolean) => void;
126
+ field: EditingField;
127
+ currentValue: string;
128
+ onSave: (field: string, value: string) => Promise<void>;
129
+ isSaving: boolean;
130
+ }
131
+
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
+ const EditFieldDialog = ({ open, onOpenChange, field, currentValue, onSave, isSaving }: EditFieldDialogProps) => {
141
+ const [value, setValue] = useState(currentValue);
142
+
143
+ useEffect(() => {
144
+ setValue(currentValue);
145
+ }, [currentValue, open]);
146
+
147
+ const handleSave = async () => {
148
+ if (field) {
149
+ await onSave(field, value);
150
+ }
151
+ };
152
+
153
+ if (!field) return null;
154
+
155
+ return (
156
+ <Dialog open={open} onOpenChange={onOpenChange}>
157
+ <DialogContent className="sm:max-w-md">
158
+ <DialogHeader>
159
+ <DialogTitle>{fieldLabels[field]}</DialogTitle>
160
+ </DialogHeader>
161
+ <div className="py-4">
162
+ <Input
163
+ value={value}
164
+ onChange={(e) => setValue(e.target.value)}
165
+ placeholder={`Enter ${fieldLabels[field].toLowerCase()}`}
166
+ autoFocus
167
+ onKeyDown={(e) => e.key === 'Enter' && handleSave()}
168
+ />
169
+ </div>
170
+ <DialogFooter>
171
+ <Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isSaving}>
172
+ Cancel
173
+ </Button>
174
+ <Button onClick={handleSave} disabled={isSaving}>
175
+ {isSaving ? 'Saving...' : 'Save'}
176
+ </Button>
177
+ </DialogFooter>
178
+ </DialogContent>
179
+ </Dialog>
180
+ );
181
+ };
182
+
183
+ // ─────────────────────────────────────────────────────────────────────────────
184
+ // Main Component
185
+ // ─────────────────────────────────────────────────────────────────────────────
186
+
39
187
  const ProfileContent = ({
40
188
  onUnauthenticated,
41
- title = 'Profile Settings',
42
- description = 'Manage your account information and preferences',
43
- showMemberSince = true,
44
- showLastLogin = true,
189
+ title = 'Profile',
45
190
  enable2FA = false,
46
191
  enableDeleteAccount = true,
47
192
  }: ProfileLayoutProps) => {
48
- const { user, isLoading } = useAuth();
193
+ const { user, isLoading, logout, uploadAvatar, updateProfile } = useAuth();
194
+ const [editingField, setEditingField] = useState<EditingField>(null);
195
+ const [isSaving, setIsSaving] = useState(false);
196
+ const [isUploading, setIsUploading] = useState(false);
49
197
 
50
- const formatDate = (dateString: string) => {
51
- return moment.utc(dateString).local().format('MMMM D, YYYY');
52
- };
198
+ // 2FA state
199
+ const [show2FASetup, setShow2FASetup] = useState(false);
200
+ const { has2FAEnabled, fetchStatus: fetch2FAStatus } = useTwoFactorStatus();
53
201
 
54
- React.useEffect(() => {
55
- if (onUnauthenticated) {
202
+ // Delete account state
203
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
204
+
205
+ const form = useForm<PatchedUserProfileUpdateRequest>({
206
+ resolver: zodResolver(PatchedUserProfileUpdateRequestSchema),
207
+ defaultValues: {
208
+ first_name: '',
209
+ last_name: '',
210
+ company: '',
211
+ position: '',
212
+ phone: '',
213
+ },
214
+ });
215
+
216
+ useEffect(() => {
217
+ if (user) {
218
+ form.reset({
219
+ first_name: user.first_name || '',
220
+ last_name: user.last_name || '',
221
+ company: user.company || '',
222
+ position: user.position || '',
223
+ phone: user.phone || '',
224
+ });
225
+ }
226
+ }, [user, form]);
227
+
228
+ useEffect(() => {
229
+ if (enable2FA) {
230
+ fetch2FAStatus();
231
+ }
232
+ }, [enable2FA, fetch2FAStatus]);
233
+
234
+ useEffect(() => {
235
+ if (onUnauthenticated && !user && !isLoading) {
56
236
  onUnauthenticated();
57
237
  }
58
- }, [onUnauthenticated]);
238
+ }, [onUnauthenticated, user, isLoading]);
239
+
240
+ const getInitials = (name: string) => {
241
+ if (!name) return 'U';
242
+ return name.split(' ').map((w) => w[0]).join('').toUpperCase().slice(0, 2);
243
+ };
244
+
245
+ const handleAvatarChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
246
+ const file = event.target.files?.[0];
247
+ if (!file) return;
248
+
249
+ if (!file.type.startsWith('image/')) {
250
+ toast.error('Please select an image file');
251
+ return;
252
+ }
253
+ if (file.size > 5 * 1024 * 1024) {
254
+ toast.error('File size must be less than 5MB');
255
+ return;
256
+ }
257
+
258
+ setIsUploading(true);
259
+ try {
260
+ await uploadAvatar(file);
261
+ toast.success('Avatar updated');
262
+ } catch (error) {
263
+ toast.error('Failed to upload avatar');
264
+ profileLogger.error('Avatar upload error:', error);
265
+ } finally {
266
+ setIsUploading(false);
267
+ }
268
+ };
59
269
 
270
+ const handleFieldSave = async (field: string, value: string) => {
271
+ setIsSaving(true);
272
+ try {
273
+ await updateProfile({ [field]: value });
274
+ toast.success('Profile updated');
275
+ setEditingField(null);
276
+ } catch (error: any) {
277
+ profileLogger.error('Profile update error:', error);
278
+ toast.error(error?.response?.data?.[field]?.[0] || 'Failed to update');
279
+ } finally {
280
+ setIsSaving(false);
281
+ }
282
+ };
60
283
 
61
- // Show auth check if no user
62
- if (!user && !isLoading) {
284
+ const handleLogout = () => {
285
+ logout();
286
+ };
287
+
288
+ // Loading state
289
+ if (isLoading) {
290
+ return (
291
+ <Preloader
292
+ variant="fullscreen"
293
+ text="Loading..."
294
+ size="lg"
295
+ backdrop={true}
296
+ backdropOpacity={80}
297
+ />
298
+ );
299
+ }
63
300
 
301
+ // Not authenticated
302
+ if (!user) {
64
303
  return (
65
304
  <div className="flex items-center justify-center min-h-screen">
66
305
  <div className="text-center">
67
- <h1 className="text-2xl font-bold text-foreground mb-4">Not Authenticated</h1>
68
- <p className="text-muted-foreground mb-4">Please log in to view your profile.</p>
306
+ <h1 className="text-2xl font-bold mb-4">Not Authenticated</h1>
307
+ <p className="text-muted-foreground">Please log in to view your profile.</p>
69
308
  </div>
70
309
  </div>
71
310
  );
72
311
  }
73
312
 
74
- if (isLoading) {
313
+ // 2FA Setup view
314
+ if (show2FASetup) {
75
315
  return (
76
- <div>
77
- <Preloader
78
- variant="fullscreen"
79
- text="Authenticating..."
80
- size="lg"
81
- backdrop={true}
82
- backdropOpacity={80}
316
+ <div className="container mx-auto px-4 py-8 max-w-lg">
317
+ <TwoFactorSetup
318
+ onComplete={() => {
319
+ setShow2FASetup(false);
320
+ fetch2FAStatus();
321
+ }}
322
+ onSkip={() => setShow2FASetup(false)}
83
323
  />
84
324
  </div>
85
325
  );
86
326
  }
87
327
 
88
328
  return (
89
- <div className="container mx-auto px-4 py-8">
90
- <div className="max-w-2xl mx-auto space-y-6">
91
- {/* Header */}
92
- <div className="text-center space-y-2">
93
- <h1 className="text-3xl font-bold text-foreground">{title}</h1>
94
- <p className="text-muted-foreground">{description}</p>
329
+ <div className="container mx-auto px-4 py-8 max-w-lg">
330
+ {/* Header */}
331
+ <h1 className="text-2xl font-bold text-center mb-8">{title}</h1>
332
+
333
+ {/* Avatar & Name */}
334
+ <div className="flex flex-col items-center mb-8">
335
+ <div className="relative group mb-3">
336
+ <Avatar className="w-24 h-24 text-3xl">
337
+ {user.avatar ? (
338
+ <img src={user.avatar} alt="Avatar" className="w-full h-full object-cover" />
339
+ ) : (
340
+ <AvatarFallback className="bg-primary text-primary-foreground">
341
+ {getInitials(user.display_username || user.email || '')}
342
+ </AvatarFallback>
343
+ )}
344
+ </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">
346
+ {isUploading ? (
347
+ <div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin" />
348
+ ) : (
349
+ <Camera className="w-6 h-6 text-white" />
350
+ )}
351
+ <input
352
+ type="file"
353
+ accept="image/*"
354
+ onChange={handleAvatarChange}
355
+ className="hidden"
356
+ disabled={isUploading}
357
+ />
358
+ </label>
359
+ </div>
360
+ <h2 className="text-xl font-semibold">{user.display_username || user.email}</h2>
361
+ {user.date_joined && (
362
+ <p className="text-sm text-muted-foreground">
363
+ Member since {moment.utc(user.date_joined).local().format('MMMM YYYY')}
364
+ </p>
365
+ )}
366
+ </div>
367
+
368
+ <div className="space-y-6">
369
+ {/* Account Info */}
370
+ <div>
371
+ <SettingsGroupHeader>Account</SettingsGroupHeader>
372
+ <SettingsGroup>
373
+ <SettingsRow
374
+ icon={<Mail className="w-5 h-5" />}
375
+ label="Email"
376
+ value={user.email}
377
+ showChevron={false}
378
+ />
379
+ </SettingsGroup>
95
380
  </div>
96
381
 
97
- {/* Main Profile Card */}
98
- <Card className="bg-card/50 backdrop-blur-sm border-border/50">
99
- <CardHeader className="text-center pb-6">
100
- <AvatarSection />
101
-
102
- <CardTitle className="text-xl">
103
- {user?.display_username || user?.email}
104
- </CardTitle>
105
- <div className="space-y-1">
106
- {showMemberSince && user?.date_joined && (
107
- <CardDescription className="text-muted-foreground">
108
- Member since {formatDate(user.date_joined)}
109
- </CardDescription>
110
- )}
111
- {showLastLogin && user?.last_login && (
112
- <CardDescription className="text-muted-foreground text-sm">
113
- Last login: {formatDate(user.last_login)}
114
- </CardDescription>
115
- )}
116
- </div>
117
- </CardHeader>
118
-
119
- <CardContent className="space-y-6">
120
- {/* Profile Form */}
121
- <ProfileForm />
122
- </CardContent>
123
- </Card>
124
-
125
- {/* Two-Factor Authentication Section */}
126
- {enable2FA && <TwoFactorSection />}
127
-
128
- {/* Delete Account Section */}
129
- {enableDeleteAccount && <DeleteAccountSection />}
382
+ {/* Personal Info */}
383
+ <div>
384
+ <SettingsGroupHeader>Personal Information</SettingsGroupHeader>
385
+ <SettingsGroup>
386
+ <SettingsRow
387
+ icon={<User className="w-5 h-5" />}
388
+ label="First Name"
389
+ value={user.first_name || 'Not set'}
390
+ onClick={() => setEditingField('first_name')}
391
+ />
392
+ <SettingsRow
393
+ icon={<User className="w-5 h-5" />}
394
+ label="Last Name"
395
+ value={user.last_name || 'Not set'}
396
+ onClick={() => setEditingField('last_name')}
397
+ />
398
+ <SettingsRow
399
+ icon={<Phone className="w-5 h-5" />}
400
+ label="Phone"
401
+ value={user.phone || 'Not set'}
402
+ onClick={() => setEditingField('phone')}
403
+ />
404
+ </SettingsGroup>
405
+ </div>
406
+
407
+ {/* Work Info */}
408
+ <div>
409
+ <SettingsGroupHeader>Work</SettingsGroupHeader>
410
+ <SettingsGroup>
411
+ <SettingsRow
412
+ icon={<Building2 className="w-5 h-5" />}
413
+ label="Company"
414
+ value={user.company || 'Not set'}
415
+ onClick={() => setEditingField('company')}
416
+ />
417
+ <SettingsRow
418
+ icon={<Briefcase className="w-5 h-5" />}
419
+ label="Position"
420
+ value={user.position || 'Not set'}
421
+ onClick={() => setEditingField('position')}
422
+ />
423
+ </SettingsGroup>
424
+ </div>
425
+
426
+ {/* Security */}
427
+ {enable2FA && (
428
+ <div>
429
+ <SettingsGroupHeader>Security</SettingsGroupHeader>
430
+ <SettingsGroup>
431
+ <SettingsRow
432
+ icon={has2FAEnabled ? <ShieldCheck className="w-5 h-5 text-green-500" /> : <Shield className="w-5 h-5" />}
433
+ label="Two-Factor Authentication"
434
+ value={has2FAEnabled ? 'On' : 'Off'}
435
+ onClick={() => !has2FAEnabled && setShow2FASetup(true)}
436
+ showChevron={!has2FAEnabled}
437
+ />
438
+ </SettingsGroup>
439
+ </div>
440
+ )}
441
+
442
+ {/* Actions */}
443
+ <div>
444
+ <SettingsGroupHeader>Actions</SettingsGroupHeader>
445
+ <SettingsGroup>
446
+ <SettingsRow
447
+ icon={<LogOut className="w-5 h-5" />}
448
+ label="Sign Out"
449
+ onClick={handleLogout}
450
+ />
451
+ {enableDeleteAccount && (
452
+ <SettingsRow
453
+ icon={<Trash2 className="w-5 h-5" />}
454
+ label="Delete Account"
455
+ onClick={() => setShowDeleteConfirm(true)}
456
+ destructive
457
+ />
458
+ )}
459
+ </SettingsGroup>
460
+ </div>
130
461
  </div>
462
+
463
+ {/* Edit Field Dialog */}
464
+ <EditFieldDialog
465
+ open={editingField !== null}
466
+ onOpenChange={(open) => !open && setEditingField(null)}
467
+ field={editingField}
468
+ currentValue={editingField ? (user[editingField] || '') : ''}
469
+ onSave={handleFieldSave}
470
+ isSaving={isSaving}
471
+ />
472
+
473
+ {/* Delete Account Dialog */}
474
+ <DeleteAccountDialog
475
+ open={showDeleteConfirm}
476
+ onOpenChange={setShowDeleteConfirm}
477
+ />
131
478
  </div>
132
479
  );
133
480
  };
134
481
 
482
+ // ─────────────────────────────────────────────────────────────────────────────
483
+ // Delete Account Dialog
484
+ // ─────────────────────────────────────────────────────────────────────────────
485
+
486
+ import { useDeleteAccount } from '@djangocfg/api/auth';
487
+
488
+ const CONFIRMATION_TEXT = 'DELETE';
489
+
490
+ const DeleteAccountDialog = ({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) => {
491
+ const [confirmationInput, setConfirmationInput] = useState('');
492
+ const { logout } = useAuth();
493
+ const { isLoading, error, deleteAccount, clearError } = useDeleteAccount();
494
+
495
+ useEffect(() => {
496
+ if (open) {
497
+ setConfirmationInput('');
498
+ clearError();
499
+ }
500
+ }, [open, clearError]);
501
+
502
+ const handleDelete = async () => {
503
+ const result = await deleteAccount();
504
+ if (result.success) {
505
+ onOpenChange(false);
506
+ await logout({ skipConfirm: true });
507
+ }
508
+ };
509
+
510
+ const isValid = confirmationInput.toUpperCase() === CONFIRMATION_TEXT;
511
+
512
+ return (
513
+ <Dialog open={open} onOpenChange={onOpenChange}>
514
+ <DialogContent>
515
+ <DialogHeader>
516
+ <DialogTitle className="text-destructive">Delete Account</DialogTitle>
517
+ <DialogDescription>
518
+ This action cannot be undone. Your account will be permanently deleted.
519
+ </DialogDescription>
520
+ </DialogHeader>
521
+
522
+ <div className="py-4 space-y-4">
523
+ {error && (
524
+ <p className="text-sm text-destructive">{error}</p>
525
+ )}
526
+ <div className="space-y-2">
527
+ <p className="text-sm text-muted-foreground">
528
+ Type <span className="font-mono font-bold">{CONFIRMATION_TEXT}</span> to confirm:
529
+ </p>
530
+ <Input
531
+ value={confirmationInput}
532
+ onChange={(e) => setConfirmationInput(e.target.value)}
533
+ placeholder={CONFIRMATION_TEXT}
534
+ disabled={isLoading}
535
+ autoComplete="off"
536
+ />
537
+ </div>
538
+ </div>
539
+
540
+ <DialogFooter>
541
+ <Button variant="ghost" onClick={() => onOpenChange(false)} disabled={isLoading}>
542
+ Cancel
543
+ </Button>
544
+ <Button variant="destructive" onClick={handleDelete} disabled={isLoading || !isValid}>
545
+ {isLoading ? 'Deleting...' : 'Delete Account'}
546
+ </Button>
547
+ </DialogFooter>
548
+ </DialogContent>
549
+ </Dialog>
550
+ );
551
+ };
552
+
553
+ // ─────────────────────────────────────────────────────────────────────────────
554
+ // Export
555
+ // ─────────────────────────────────────────────────────────────────────────────
556
+
135
557
  export const ProfileLayout: React.FC<ProfileLayoutProps> = (props) => {
136
558
  return <ProfileContent {...props} />;
137
559
  };
138
-
@@ -50,8 +50,8 @@ export const DeleteAccountSection: React.FC = () => {
50
50
  const result = await deleteAccount();
51
51
  if (result.success) {
52
52
  setShowDeleteDialog(false);
53
- // Perform logout after successful deletion
54
- await logout();
53
+ // Perform logout after successful deletion (skip confirmation since user already confirmed deletion)
54
+ await logout({ skipConfirm: true });
55
55
  }
56
56
  };
57
57
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useCallback } from 'react';
4
4
 
5
- import { events } from '@djangocfg/ui-nextjs';
5
+ import { events } from '@djangocfg/ui-core/hooks';
6
6
 
7
7
  import { AUTH_EVENTS, OpenAuthDialogPayload} from './events';
8
8