@djangocfg/layouts 2.1.127 → 2.1.129
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -14
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +255 -270
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.129",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -74,12 +74,12 @@
|
|
|
74
74
|
"check": "tsc --noEmit"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@djangocfg/api": "^2.1.
|
|
78
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
79
|
-
"@djangocfg/i18n": "^2.1.
|
|
80
|
-
"@djangocfg/ui-core": "^2.1.
|
|
81
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
82
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
77
|
+
"@djangocfg/api": "^2.1.129",
|
|
78
|
+
"@djangocfg/centrifugo": "^2.1.129",
|
|
79
|
+
"@djangocfg/i18n": "^2.1.129",
|
|
80
|
+
"@djangocfg/ui-core": "^2.1.129",
|
|
81
|
+
"@djangocfg/ui-nextjs": "^2.1.129",
|
|
82
|
+
"@djangocfg/ui-tools": "^2.1.129",
|
|
83
83
|
"@hookform/resolvers": "^5.2.2",
|
|
84
84
|
"consola": "^3.4.2",
|
|
85
85
|
"lucide-react": "^0.545.0",
|
|
@@ -102,13 +102,13 @@
|
|
|
102
102
|
"uuid": "^11.1.0"
|
|
103
103
|
},
|
|
104
104
|
"devDependencies": {
|
|
105
|
-
"@djangocfg/api": "^2.1.
|
|
106
|
-
"@djangocfg/i18n": "^2.1.
|
|
107
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
108
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
109
|
-
"@djangocfg/ui-core": "^2.1.
|
|
110
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
111
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
105
|
+
"@djangocfg/api": "^2.1.129",
|
|
106
|
+
"@djangocfg/i18n": "^2.1.129",
|
|
107
|
+
"@djangocfg/centrifugo": "^2.1.129",
|
|
108
|
+
"@djangocfg/typescript-config": "^2.1.129",
|
|
109
|
+
"@djangocfg/ui-core": "^2.1.129",
|
|
110
|
+
"@djangocfg/ui-nextjs": "^2.1.129",
|
|
111
|
+
"@djangocfg/ui-tools": "^2.1.129",
|
|
112
112
|
"@types/node": "^24.7.2",
|
|
113
113
|
"@types/react": "^19.1.0",
|
|
114
114
|
"@types/react-dom": "^19.1.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Camera,
|
|
3
|
+
import { Camera, LogOut, Trash2 } from 'lucide-react';
|
|
4
4
|
import moment from 'moment';
|
|
5
5
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
6
6
|
import { useForm } from 'react-hook-form';
|
|
@@ -47,149 +47,148 @@ interface ProfileLayoutProps {
|
|
|
47
47
|
type EditingField = 'first_name' | 'last_name' | 'company' | 'position' | 'phone' | null;
|
|
48
48
|
|
|
49
49
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
-
//
|
|
50
|
+
// Editable Field Component
|
|
51
51
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
const SettingsGroup = ({ children, className }: { children: React.ReactNode; className?: string }) => (
|
|
55
|
-
<div className={cn('bg-card rounded-xl overflow-hidden divide-y divide-border', className)}>
|
|
56
|
-
{children}
|
|
57
|
-
</div>
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
/** Apple-style settings group header */
|
|
61
|
-
const SettingsGroupHeader = ({ children }: { children: React.ReactNode }) => (
|
|
62
|
-
<div className="px-4 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
63
|
-
{children}
|
|
64
|
-
</div>
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
/** Apple-style settings row */
|
|
68
|
-
interface SettingsRowProps {
|
|
69
|
-
icon?: React.ReactNode;
|
|
53
|
+
interface EditableFieldProps {
|
|
70
54
|
label: string;
|
|
71
|
-
value
|
|
72
|
-
|
|
73
|
-
|
|
55
|
+
value: string;
|
|
56
|
+
placeholder: string;
|
|
57
|
+
onSave: (value: string) => Promise<void>;
|
|
74
58
|
disabled?: boolean;
|
|
75
|
-
showChevron?: boolean;
|
|
76
|
-
className?: string;
|
|
77
59
|
}
|
|
78
60
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
61
|
+
const EditableField = ({ label, value, placeholder, onSave, disabled }: EditableFieldProps) => {
|
|
62
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
63
|
+
const [editValue, setEditValue] = useState(value);
|
|
64
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
setEditValue(value);
|
|
68
|
+
}, [value]);
|
|
69
|
+
|
|
70
|
+
const handleSave = async () => {
|
|
71
|
+
if (editValue === value) {
|
|
72
|
+
setIsEditing(false);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
setIsSaving(true);
|
|
76
|
+
try {
|
|
77
|
+
await onSave(editValue);
|
|
78
|
+
setIsEditing(false);
|
|
79
|
+
} finally {
|
|
80
|
+
setIsSaving(false);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
85
|
+
if (e.key === 'Enter') {
|
|
86
|
+
handleSave();
|
|
87
|
+
} else if (e.key === 'Escape') {
|
|
88
|
+
setEditValue(value);
|
|
89
|
+
setIsEditing(false);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (isEditing) {
|
|
94
|
+
return (
|
|
95
|
+
<div className="py-4 border-b border-border/50 last:border-0">
|
|
96
|
+
<label className="block text-[13px] text-muted-foreground mb-1.5">
|
|
97
|
+
{label}
|
|
98
|
+
</label>
|
|
99
|
+
<div className="flex items-center gap-2">
|
|
100
|
+
<Input
|
|
101
|
+
value={editValue}
|
|
102
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
103
|
+
onKeyDown={handleKeyDown}
|
|
104
|
+
onBlur={handleSave}
|
|
105
|
+
placeholder={placeholder}
|
|
106
|
+
autoFocus
|
|
107
|
+
disabled={isSaving}
|
|
108
|
+
className="h-9 text-[15px]"
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
90
114
|
|
|
91
115
|
return (
|
|
92
|
-
<
|
|
93
|
-
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
onClick={() => !disabled && setIsEditing(true)}
|
|
94
119
|
disabled={disabled}
|
|
95
120
|
className={cn(
|
|
96
|
-
'w-full
|
|
97
|
-
|
|
98
|
-
disabled && '
|
|
99
|
-
destructive && 'text-destructive',
|
|
100
|
-
className
|
|
121
|
+
'w-full py-4 border-b border-border/50 last:border-0 text-left',
|
|
122
|
+
'transition-colors hover:bg-muted/30',
|
|
123
|
+
disabled && 'cursor-default hover:bg-transparent'
|
|
101
124
|
)}
|
|
102
125
|
>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)}
|
|
114
|
-
{showChevron && (
|
|
115
|
-
<ChevronRight className="w-4 h-4 text-muted-foreground/50 shrink-0" />
|
|
116
|
-
)}
|
|
117
|
-
</Wrapper>
|
|
126
|
+
<div className="text-[13px] text-muted-foreground mb-0.5">
|
|
127
|
+
{label}
|
|
128
|
+
</div>
|
|
129
|
+
<div className={cn(
|
|
130
|
+
'text-[15px]',
|
|
131
|
+
value ? 'text-foreground' : 'text-muted-foreground/60'
|
|
132
|
+
)}>
|
|
133
|
+
{value || placeholder}
|
|
134
|
+
</div>
|
|
135
|
+
</button>
|
|
118
136
|
);
|
|
119
137
|
};
|
|
120
138
|
|
|
121
139
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
122
|
-
//
|
|
140
|
+
// Section Component
|
|
123
141
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
124
142
|
|
|
125
|
-
interface
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
currentValue: string;
|
|
130
|
-
onSave: (field: string, value: string) => Promise<void>;
|
|
131
|
-
isSaving: boolean;
|
|
143
|
+
interface SectionProps {
|
|
144
|
+
title?: string;
|
|
145
|
+
children: React.ReactNode;
|
|
146
|
+
className?: string;
|
|
132
147
|
}
|
|
133
148
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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]);
|
|
151
|
-
|
|
152
|
-
useEffect(() => {
|
|
153
|
-
setValue(currentValue);
|
|
154
|
-
}, [currentValue, open]);
|
|
155
|
-
|
|
156
|
-
const handleSave = async () => {
|
|
157
|
-
if (field) {
|
|
158
|
-
await onSave(field, value);
|
|
159
|
-
}
|
|
160
|
-
};
|
|
149
|
+
const Section = ({ title, children, className }: SectionProps) => (
|
|
150
|
+
<div className={cn('mb-10', className)}>
|
|
151
|
+
{title && (
|
|
152
|
+
<h2 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2 px-1">
|
|
153
|
+
{title}
|
|
154
|
+
</h2>
|
|
155
|
+
)}
|
|
156
|
+
<div className="bg-card rounded-xl px-4">
|
|
157
|
+
{children}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
163
|
+
// Action Button Component
|
|
164
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
163
165
|
|
|
164
|
-
|
|
166
|
+
interface ActionButtonProps {
|
|
167
|
+
icon?: React.ReactNode;
|
|
168
|
+
label: string;
|
|
169
|
+
onClick: () => void;
|
|
170
|
+
variant?: 'default' | 'destructive';
|
|
171
|
+
disabled?: boolean;
|
|
172
|
+
}
|
|
165
173
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
</Button>
|
|
185
|
-
<Button onClick={handleSave} disabled={isSaving}>
|
|
186
|
-
{isSaving ? labels.saving : labels.save}
|
|
187
|
-
</Button>
|
|
188
|
-
</DialogFooter>
|
|
189
|
-
</DialogContent>
|
|
190
|
-
</Dialog>
|
|
191
|
-
);
|
|
192
|
-
};
|
|
174
|
+
const ActionButton = ({ icon, label, onClick, variant = 'default', disabled }: ActionButtonProps) => (
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
onClick={onClick}
|
|
178
|
+
disabled={disabled}
|
|
179
|
+
className={cn(
|
|
180
|
+
'w-full flex items-center justify-center gap-2 py-3.5 border-b border-border/50 last:border-0',
|
|
181
|
+
'text-[15px] font-medium transition-colors',
|
|
182
|
+
variant === 'destructive'
|
|
183
|
+
? 'text-destructive hover:bg-destructive/5'
|
|
184
|
+
: 'text-foreground hover:bg-muted/30',
|
|
185
|
+
disabled && 'opacity-50 cursor-not-allowed'
|
|
186
|
+
)}
|
|
187
|
+
>
|
|
188
|
+
{icon}
|
|
189
|
+
{label}
|
|
190
|
+
</button>
|
|
191
|
+
);
|
|
193
192
|
|
|
194
193
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
195
194
|
// Main Component
|
|
@@ -202,8 +201,6 @@ const ProfileContent = ({
|
|
|
202
201
|
enableDeleteAccount = true,
|
|
203
202
|
}: ProfileLayoutProps) => {
|
|
204
203
|
const { user, isLoading, logout, uploadAvatar, updateProfile } = useAuth();
|
|
205
|
-
const [editingField, setEditingField] = useState<EditingField>(null);
|
|
206
|
-
const [isSaving, setIsSaving] = useState(false);
|
|
207
204
|
const [isUploading, setIsUploading] = useState(false);
|
|
208
205
|
const t = useTypedT<I18nTranslations>();
|
|
209
206
|
|
|
@@ -217,39 +214,35 @@ const ProfileContent = ({
|
|
|
217
214
|
const labels = useMemo(() => ({
|
|
218
215
|
title: title || t('layouts.profilePage.title'),
|
|
219
216
|
// Sections
|
|
220
|
-
account: t('layouts.profilePage.account'),
|
|
221
217
|
personalInfo: t('layouts.profilePage.personalInfo'),
|
|
222
218
|
work: t('layouts.profilePage.work'),
|
|
223
219
|
security: t('layouts.profilePage.security'),
|
|
224
|
-
actions: t('layouts.profilePage.actions'),
|
|
225
220
|
// Fields
|
|
226
|
-
|
|
227
|
-
|
|
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>,
|
|
221
|
+
firstName: t('layouts.profilePage.firstName'),
|
|
222
|
+
lastName: t('layouts.profilePage.lastName'),
|
|
233
223
|
email: t('layouts.profilePage.email'),
|
|
234
|
-
|
|
224
|
+
phone: t('layouts.profilePage.phone'),
|
|
225
|
+
company: t('layouts.profilePage.company'),
|
|
226
|
+
position: t('layouts.profilePage.position'),
|
|
227
|
+
// Placeholders
|
|
235
228
|
notSet: t('layouts.profilePage.notSet'),
|
|
236
|
-
|
|
237
|
-
|
|
229
|
+
addFirstName: t('layouts.profilePage.addFirstName') || 'Add first name',
|
|
230
|
+
addLastName: t('layouts.profilePage.addLastName') || 'Add last name',
|
|
231
|
+
addPhone: t('layouts.profilePage.addPhone') || 'Add phone number',
|
|
232
|
+
addCompany: t('layouts.profilePage.addCompany') || 'Add company',
|
|
233
|
+
addPosition: t('layouts.profilePage.addPosition') || 'Add position',
|
|
234
|
+
// 2FA
|
|
235
|
+
twoFactorAuth: t('layouts.profilePage.twoFactorAuth'),
|
|
236
|
+
twoFactorEnabled: t('layouts.profilePage.twoFactorEnabled') || 'Two-factor authentication is enabled',
|
|
237
|
+
twoFactorDisabled: t('layouts.profilePage.twoFactorDisabled') || 'Add an extra layer of security',
|
|
238
|
+
enable2FA: t('layouts.profilePage.enable2FA') || 'Enable 2FA',
|
|
238
239
|
// Actions
|
|
239
|
-
save: t('layouts.profilePage.save'),
|
|
240
|
-
saving: t('layouts.profilePage.saving'),
|
|
241
|
-
cancel: t('layouts.profilePage.cancel'),
|
|
242
240
|
signOut: t('layouts.profilePage.signOut'),
|
|
243
241
|
deleteAccount: t('layouts.profilePage.deleteAccount'),
|
|
244
|
-
// 2FA
|
|
245
|
-
twoFactorAuth: t('layouts.profilePage.twoFactorAuth'),
|
|
246
242
|
// Avatar
|
|
247
|
-
avatar: t('layouts.profilePage.avatar'),
|
|
248
243
|
changeAvatar: t('layouts.profilePage.changeAvatar'),
|
|
249
244
|
// Membership
|
|
250
245
|
memberSince: t('layouts.profilePage.memberSince'),
|
|
251
|
-
// Placeholder
|
|
252
|
-
enterField: t('layouts.profilePage.enterField'),
|
|
253
246
|
// Messages
|
|
254
247
|
profileUpdated: t('layouts.profilePage.profileUpdated'),
|
|
255
248
|
avatarUpdated: t('layouts.profilePage.avatarUpdated'),
|
|
@@ -330,16 +323,13 @@ const ProfileContent = ({
|
|
|
330
323
|
};
|
|
331
324
|
|
|
332
325
|
const handleFieldSave = async (field: string, value: string) => {
|
|
333
|
-
setIsSaving(true);
|
|
334
326
|
try {
|
|
335
327
|
await updateProfile({ [field]: value });
|
|
336
328
|
toast.success(labels.profileUpdated);
|
|
337
|
-
setEditingField(null);
|
|
338
329
|
} catch (error: any) {
|
|
339
330
|
profileLogger.error('Profile update error:', error);
|
|
340
331
|
toast.error(error?.response?.data?.[field]?.[0] || labels.failedToUpdate);
|
|
341
|
-
|
|
342
|
-
setIsSaving(false);
|
|
332
|
+
throw error;
|
|
343
333
|
}
|
|
344
334
|
};
|
|
345
335
|
|
|
@@ -388,28 +378,33 @@ const ProfileContent = ({
|
|
|
388
378
|
}
|
|
389
379
|
|
|
390
380
|
return (
|
|
391
|
-
<div className="container mx-auto px-4 py-
|
|
392
|
-
{/* Header */}
|
|
393
|
-
<
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
<div className="flex flex-col items-center mb-8">
|
|
397
|
-
<div className="relative group mb-3">
|
|
398
|
-
<Avatar className="w-24 h-24 text-3xl">
|
|
381
|
+
<div className="container mx-auto px-4 py-12 max-w-md">
|
|
382
|
+
{/* Header with Avatar */}
|
|
383
|
+
<div className="flex flex-col items-center mb-12">
|
|
384
|
+
<div className="relative group mb-4">
|
|
385
|
+
<Avatar className="w-28 h-28 text-3xl">
|
|
399
386
|
{user.avatar ? (
|
|
400
|
-
<img src={user.avatar} alt=
|
|
387
|
+
<img src={user.avatar} alt="" className="w-full h-full object-cover" />
|
|
401
388
|
) : (
|
|
402
|
-
<AvatarFallback className="bg-
|
|
389
|
+
<AvatarFallback className="bg-muted text-muted-foreground text-2xl font-medium">
|
|
403
390
|
{getInitials(user.display_username || user.email || '')}
|
|
404
391
|
</AvatarFallback>
|
|
405
392
|
)}
|
|
406
393
|
</Avatar>
|
|
407
|
-
<label
|
|
408
|
-
{
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
<Camera className="w-6 h-6 text-white" />
|
|
394
|
+
<label
|
|
395
|
+
className={cn(
|
|
396
|
+
'absolute inset-0 rounded-full flex items-center justify-center cursor-pointer',
|
|
397
|
+
'bg-black/0 group-hover:bg-black/40 transition-colors'
|
|
412
398
|
)}
|
|
399
|
+
title={labels.changeAvatar}
|
|
400
|
+
>
|
|
401
|
+
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
|
|
402
|
+
{isUploading ? (
|
|
403
|
+
<div className="w-6 h-6 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
|
404
|
+
) : (
|
|
405
|
+
<Camera className="w-6 h-6 text-white" />
|
|
406
|
+
)}
|
|
407
|
+
</div>
|
|
413
408
|
<input
|
|
414
409
|
type="file"
|
|
415
410
|
accept="image/*"
|
|
@@ -419,118 +414,107 @@ const ProfileContent = ({
|
|
|
419
414
|
/>
|
|
420
415
|
</label>
|
|
421
416
|
</div>
|
|
422
|
-
|
|
417
|
+
|
|
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
|
+
|
|
423
426
|
{user.date_joined && (
|
|
424
|
-
<p className="text-
|
|
427
|
+
<p className="text-[13px] text-muted-foreground/60 mt-2">
|
|
425
428
|
{labels.memberSince.replace('{date}', moment.utc(user.date_joined).local().format('MMMM YYYY'))}
|
|
426
429
|
</p>
|
|
427
430
|
)}
|
|
428
431
|
</div>
|
|
429
432
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
<
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
<
|
|
471
|
-
<
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
label={labels.twoFactorAuth}
|
|
496
|
-
value={has2FAEnabled ? labels.on : labels.off}
|
|
497
|
-
onClick={() => !has2FAEnabled && setShow2FASetup(true)}
|
|
498
|
-
showChevron={!has2FAEnabled}
|
|
499
|
-
/>
|
|
500
|
-
</SettingsGroup>
|
|
433
|
+
{/* Personal Info */}
|
|
434
|
+
<Section title={labels.personalInfo}>
|
|
435
|
+
<EditableField
|
|
436
|
+
label={labels.firstName}
|
|
437
|
+
value={user.first_name || ''}
|
|
438
|
+
placeholder={labels.addFirstName}
|
|
439
|
+
onSave={(value) => handleFieldSave('first_name', value)}
|
|
440
|
+
/>
|
|
441
|
+
<EditableField
|
|
442
|
+
label={labels.lastName}
|
|
443
|
+
value={user.last_name || ''}
|
|
444
|
+
placeholder={labels.addLastName}
|
|
445
|
+
onSave={(value) => handleFieldSave('last_name', value)}
|
|
446
|
+
/>
|
|
447
|
+
<EditableField
|
|
448
|
+
label={labels.phone}
|
|
449
|
+
value={user.phone || ''}
|
|
450
|
+
placeholder={labels.addPhone}
|
|
451
|
+
onSave={(value) => handleFieldSave('phone', value)}
|
|
452
|
+
/>
|
|
453
|
+
</Section>
|
|
454
|
+
|
|
455
|
+
{/* Work */}
|
|
456
|
+
<Section title={labels.work}>
|
|
457
|
+
<EditableField
|
|
458
|
+
label={labels.company}
|
|
459
|
+
value={user.company || ''}
|
|
460
|
+
placeholder={labels.addCompany}
|
|
461
|
+
onSave={(value) => handleFieldSave('company', value)}
|
|
462
|
+
/>
|
|
463
|
+
<EditableField
|
|
464
|
+
label={labels.position}
|
|
465
|
+
value={user.position || ''}
|
|
466
|
+
placeholder={labels.addPosition}
|
|
467
|
+
onSave={(value) => handleFieldSave('position', value)}
|
|
468
|
+
/>
|
|
469
|
+
</Section>
|
|
470
|
+
|
|
471
|
+
{/* 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>
|
|
501
498
|
</div>
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
{/* Actions */}
|
|
505
|
-
<div>
|
|
506
|
-
<SettingsGroupHeader>{labels.actions}</SettingsGroupHeader>
|
|
507
|
-
<SettingsGroup>
|
|
508
|
-
<SettingsRow
|
|
509
|
-
icon={<LogOut className="w-5 h-5" />}
|
|
510
|
-
label={labels.signOut}
|
|
511
|
-
onClick={handleLogout}
|
|
512
|
-
/>
|
|
513
|
-
{enableDeleteAccount && (
|
|
514
|
-
<SettingsRow
|
|
515
|
-
icon={<Trash2 className="w-5 h-5" />}
|
|
516
|
-
label={labels.deleteAccount}
|
|
517
|
-
onClick={() => setShowDeleteConfirm(true)}
|
|
518
|
-
destructive
|
|
519
|
-
/>
|
|
520
|
-
)}
|
|
521
|
-
</SettingsGroup>
|
|
522
|
-
</div>
|
|
523
|
-
</div>
|
|
499
|
+
</Section>
|
|
500
|
+
)}
|
|
524
501
|
|
|
525
|
-
{/*
|
|
526
|
-
<
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
502
|
+
{/* Actions */}
|
|
503
|
+
<Section>
|
|
504
|
+
<ActionButton
|
|
505
|
+
icon={<LogOut className="w-4 h-4" />}
|
|
506
|
+
label={labels.signOut}
|
|
507
|
+
onClick={handleLogout}
|
|
508
|
+
/>
|
|
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
|
+
</Section>
|
|
534
518
|
|
|
535
519
|
{/* Delete Account Dialog */}
|
|
536
520
|
<DeleteAccountDialog
|
|
@@ -582,9 +566,9 @@ const DeleteAccountDialog = ({ open, onOpenChange }: { open: boolean; onOpenChan
|
|
|
582
566
|
|
|
583
567
|
return (
|
|
584
568
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
585
|
-
<DialogContent>
|
|
569
|
+
<DialogContent className="sm:max-w-md">
|
|
586
570
|
<DialogHeader>
|
|
587
|
-
<DialogTitle
|
|
571
|
+
<DialogTitle>{labels.title}</DialogTitle>
|
|
588
572
|
<DialogDescription>
|
|
589
573
|
{labels.description}
|
|
590
574
|
</DialogDescription>
|
|
@@ -595,9 +579,9 @@ const DeleteAccountDialog = ({ open, onOpenChange }: { open: boolean; onOpenChan
|
|
|
595
579
|
<p className="text-sm text-destructive">{error}</p>
|
|
596
580
|
)}
|
|
597
581
|
<div className="space-y-2">
|
|
598
|
-
<p className="text-
|
|
582
|
+
<p className="text-[13px] text-muted-foreground">
|
|
599
583
|
{labels.typeToConfirm.replace('{word}', '')}
|
|
600
|
-
<span className="font-mono font-
|
|
584
|
+
<span className="font-mono font-semibold text-foreground">{labels.confirmationWord}</span>
|
|
601
585
|
</p>
|
|
602
586
|
<Input
|
|
603
587
|
value={confirmationInput}
|
|
@@ -605,6 +589,7 @@ const DeleteAccountDialog = ({ open, onOpenChange }: { open: boolean; onOpenChan
|
|
|
605
589
|
placeholder={labels.confirmationWord}
|
|
606
590
|
disabled={isLoading}
|
|
607
591
|
autoComplete="off"
|
|
592
|
+
className="font-mono"
|
|
608
593
|
/>
|
|
609
594
|
</div>
|
|
610
595
|
</div>
|