@djangocfg/layouts 2.1.100 → 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.
- package/dist/AIChatWidget-LUPM7S2O.mjs +1644 -0
- package/dist/AIChatWidget-LUPM7S2O.mjs.map +1 -0
- package/dist/AIChatWidget-O23TJJ7C.mjs +3 -0
- package/dist/AIChatWidget-O23TJJ7C.mjs.map +1 -0
- package/dist/chunk-53YKWR6F.mjs +6 -0
- package/dist/chunk-53YKWR6F.mjs.map +1 -0
- package/dist/chunk-EI7TDN2G.mjs +1652 -0
- package/dist/chunk-EI7TDN2G.mjs.map +1 -0
- package/dist/components.cjs +925 -0
- package/dist/components.cjs.map +1 -0
- package/dist/components.d.mts +583 -0
- package/dist/components.d.ts +583 -0
- package/dist/components.mjs +879 -0
- package/dist/components.mjs.map +1 -0
- package/dist/index.cjs +7573 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +2376 -0
- package/dist/index.d.ts +2376 -0
- package/dist/index.mjs +5673 -0
- package/dist/index.mjs.map +1 -0
- package/dist/layouts.cjs +6530 -0
- package/dist/layouts.cjs.map +1 -0
- package/dist/layouts.d.mts +748 -0
- package/dist/layouts.d.ts +748 -0
- package/dist/layouts.mjs +4741 -0
- package/dist/layouts.mjs.map +1 -0
- package/dist/pages.cjs +178 -0
- package/dist/pages.cjs.map +1 -0
- package/dist/pages.d.mts +57 -0
- package/dist/pages.d.ts +57 -0
- package/dist/pages.mjs +168 -0
- package/dist/pages.mjs.map +1 -0
- package/dist/snippets.cjs +3793 -0
- package/dist/snippets.cjs.map +1 -0
- package/dist/snippets.d.mts +1192 -0
- package/dist/snippets.d.ts +1192 -0
- package/dist/snippets.mjs +3738 -0
- package/dist/snippets.mjs.map +1 -0
- package/dist/utils.cjs +34 -0
- package/dist/utils.cjs.map +1 -0
- package/dist/utils.d.mts +40 -0
- package/dist/utils.d.ts +40 -0
- package/dist/utils.mjs +25 -0
- package/dist/utils.mjs.map +1 -0
- package/package.json +38 -47
- package/src/components/errors/ErrorsTracker/components/ErrorButtons.tsx +2 -1
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +506 -89
- package/src/layouts/ProfileLayout/components/AvatarSection.tsx +3 -6
- package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +2 -2
- package/src/layouts/ProfileLayout/components/ProfileForm.tsx +4 -5
- package/src/snippets/AuthDialog/useAuthDialog.ts +1 -1
|
@@ -1,142 +1,559 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
'use client';
|
|
3
2
|
|
|
4
|
-
import
|
|
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 { AccountsProvider, useAuth } from '@djangocfg/api/auth';
|
|
8
10
|
import {
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
};
|
|
198
|
+
// 2FA state
|
|
199
|
+
const [show2FASetup, setShow2FASetup] = useState(false);
|
|
200
|
+
const { has2FAEnabled, fetchStatus: fetch2FAStatus } = useTwoFactorStatus();
|
|
53
201
|
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
}
|
|
59
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
|
+
};
|
|
60
269
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
};
|
|
283
|
+
|
|
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
|
|
68
|
-
<p className="text-muted-foreground
|
|
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
|
-
|
|
313
|
+
// 2FA Setup view
|
|
314
|
+
if (show2FASetup) {
|
|
75
315
|
return (
|
|
76
|
-
<div>
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
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>
|
|
95
424
|
</div>
|
|
96
425
|
|
|
97
|
-
{/*
|
|
98
|
-
|
|
99
|
-
<
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
|
135
|
-
|
|
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
|
+
|
|
136
512
|
return (
|
|
137
|
-
<
|
|
138
|
-
<
|
|
139
|
-
|
|
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>
|
|
140
550
|
);
|
|
141
551
|
};
|
|
142
552
|
|
|
553
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
554
|
+
// Export
|
|
555
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
556
|
+
|
|
557
|
+
export const ProfileLayout: React.FC<ProfileLayoutProps> = (props) => {
|
|
558
|
+
return <ProfileContent {...props} />;
|
|
559
|
+
};
|
|
@@ -4,14 +4,13 @@ import { Check, Upload, X } from 'lucide-react';
|
|
|
4
4
|
import React, { useState } from 'react';
|
|
5
5
|
import { toast } from '@djangocfg/ui-core/hooks';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { useAuth } from '@djangocfg/api/auth';
|
|
8
8
|
import { Avatar, AvatarFallback, Button } from '@djangocfg/ui-nextjs/components';
|
|
9
9
|
|
|
10
10
|
import { profileLogger } from '../../../utils/logger';
|
|
11
11
|
|
|
12
12
|
export const AvatarSection = () => {
|
|
13
|
-
const { user } = useAuth();
|
|
14
|
-
const accounts = useAccountsContext();
|
|
13
|
+
const { user, uploadAvatar } = useAuth();
|
|
15
14
|
const [avatarFile, setAvatarFile] = useState<File | null>(null);
|
|
16
15
|
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
|
|
17
16
|
const [isUploading, setIsUploading] = useState(false);
|
|
@@ -55,9 +54,7 @@ export const AvatarSection = () => {
|
|
|
55
54
|
if (!avatarFile) return;
|
|
56
55
|
setIsUploading(true);
|
|
57
56
|
try {
|
|
58
|
-
|
|
59
|
-
formData.append('avatar', avatarFile);
|
|
60
|
-
await accounts.uploadAvatar(formData as any);
|
|
57
|
+
await uploadAvatar(avatarFile);
|
|
61
58
|
toast.success('Avatar updated successfully');
|
|
62
59
|
setAvatarFile(null);
|
|
63
60
|
setAvatarPreview(null);
|
|
@@ -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
|
|