@djangocfg/layouts 2.1.264 → 2.1.267
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/README.md +113 -4
- package/package.json +19 -18
- package/src/hooks/index.ts +1 -1
- package/src/hooks/usePathnameWithoutLocale.ts +35 -19
- package/src/layouts/AppLayout/AppLayout.tsx +15 -4
- package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +50 -6
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +206 -235
- package/src/layouts/ProfileLayout/components/AvatarSection.tsx +2 -3
- package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +22 -106
- package/src/layouts/ProfileLayout/components/EditableField.tsx +15 -10
- package/src/layouts/ProfileLayout/components/Section.tsx +1 -1
- package/src/layouts/ProfileLayout/components/TwoFactorSection.tsx +255 -215
- package/src/layouts/ProfileLayout/context.tsx +108 -16
- package/src/layouts/ProfileLayout/index.ts +1 -1
- package/src/layouts/PublicLayout/PublicLayout.tsx +18 -0
- package/src/layouts/PublicLayout/components/NavActions.tsx +50 -0
- package/src/layouts/PublicLayout/components/NavBrand.tsx +26 -0
- package/src/layouts/PublicLayout/components/NavDesktopItems.tsx +207 -0
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +1 -1
- package/src/layouts/PublicLayout/components/PublicNavbar.tsx +44 -6
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +199 -396
- package/src/layouts/PublicLayout/hooks/index.ts +5 -1
- package/src/layouts/PublicLayout/hooks/useDropdownMenu.ts +58 -0
- package/src/layouts/PublicLayout/hooks/useNavbarScroll.ts +61 -0
- package/src/layouts/PublicLayout/hooks/useNavbarViewportVars.ts +46 -0
- package/src/layouts/PublicLayout/index.ts +4 -0
- package/src/layouts/PublicLayout/navbarTypes.ts +17 -0
- package/src/utils/pathMatcher.ts +6 -3
- package/src/layouts/ProfileLayout/.claude/.sidecar/activity.jsonl +0 -2
- package/src/layouts/ProfileLayout/.claude/.sidecar/history/2026-03-15.md +0 -35
- package/src/layouts/ProfileLayout/.claude/.sidecar/review.md +0 -35
- package/src/layouts/ProfileLayout/.claude/.sidecar/scan.log +0 -3
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-001.md +0 -18
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-002.md +0 -19
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-003.md +0 -18
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-004.md +0 -18
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-005.md +0 -18
- package/src/layouts/ProfileLayout/.claude/.sidecar/usage.json +0 -5
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { LogOut, MoreHorizontal, Trash2 } from 'lucide-react';
|
|
4
4
|
import moment from 'moment';
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { useCallback, useEffect } from 'react';
|
|
6
6
|
|
|
7
|
+
import { useAuth, useDeleteAccount } from '@djangocfg/api/auth';
|
|
7
8
|
import { useAppT } from '@djangocfg/i18n';
|
|
8
|
-
import { toast, useImageLoader } from '@djangocfg/ui-core/hooks';
|
|
9
|
-
|
|
10
|
-
import { useAuth } from '@djangocfg/api/auth';
|
|
11
9
|
import {
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
Button,
|
|
11
|
+
DropdownMenu,
|
|
12
|
+
DropdownMenuContent,
|
|
13
|
+
DropdownMenuItem,
|
|
14
|
+
DropdownMenuSeparator,
|
|
15
|
+
DropdownMenuTrigger,
|
|
14
16
|
Preloader,
|
|
17
|
+
Tabs,
|
|
18
|
+
TabsContent,
|
|
19
|
+
TabsList,
|
|
20
|
+
TabsTrigger,
|
|
15
21
|
} from '@djangocfg/ui-core/components';
|
|
16
|
-
import { cn } from '@djangocfg/ui-core/lib';
|
|
17
22
|
|
|
18
|
-
import { profileLogger } from '../../utils/logger';
|
|
19
|
-
import { useLogout } from '../../hooks';
|
|
20
23
|
import {
|
|
21
|
-
|
|
22
|
-
DeleteAccountScreen,
|
|
23
|
-
DeleteAccountSection,
|
|
24
|
+
AvatarSection,
|
|
24
25
|
EditableField,
|
|
25
26
|
Section,
|
|
26
27
|
TwoFactorSection,
|
|
@@ -28,174 +29,189 @@ import {
|
|
|
28
29
|
import { ProfileProvider, useProfileContext } from './context';
|
|
29
30
|
|
|
30
31
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
-
//
|
|
32
|
+
// Slot + Tab types (public API)
|
|
32
33
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
34
|
|
|
34
|
-
interface
|
|
35
|
+
export interface ProfileTab {
|
|
36
|
+
/** Unique key, used as Tabs value */
|
|
37
|
+
value: string;
|
|
38
|
+
/** Trigger label */
|
|
39
|
+
label: React.ReactNode;
|
|
40
|
+
/** Tab panel content */
|
|
41
|
+
content: React.ReactNode;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ProfileSlots {
|
|
45
|
+
/** Extra items rendered inside the ⋯ dropdown, above the separator before Delete */
|
|
46
|
+
headerMenuItems?: React.ReactNode;
|
|
47
|
+
/** Rendered next to the user name (e.g. plan badge, role chip) */
|
|
48
|
+
headerBadge?: React.ReactNode;
|
|
49
|
+
/** Rendered below the avatar row, above the tabs */
|
|
50
|
+
headerAfter?: React.ReactNode;
|
|
51
|
+
/** Rendered below all tab content */
|
|
52
|
+
footer?: React.ReactNode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ProfileLayoutProps {
|
|
35
56
|
onUnauthenticated?: () => void;
|
|
36
57
|
title?: string;
|
|
37
58
|
enable2FA?: boolean;
|
|
38
59
|
enableDeleteAccount?: boolean;
|
|
60
|
+
/** Extra tabs appended after built-in Profile / Security tabs */
|
|
61
|
+
tabs?: ProfileTab[];
|
|
62
|
+
/** Named slots for additional content */
|
|
63
|
+
slots?: ProfileSlots;
|
|
39
64
|
}
|
|
40
65
|
|
|
41
66
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
42
|
-
//
|
|
67
|
+
// Header
|
|
43
68
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
44
69
|
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
isUploading,
|
|
49
|
-
onChange,
|
|
50
|
-
label,
|
|
51
|
-
}: {
|
|
52
|
-
src?: string | null;
|
|
53
|
-
initials: string;
|
|
54
|
-
isUploading: boolean;
|
|
55
|
-
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
56
|
-
label: string;
|
|
70
|
+
function ProfileHeader({ slots, enableDeleteAccount }: {
|
|
71
|
+
slots?: ProfileSlots;
|
|
72
|
+
enableDeleteAccount?: boolean;
|
|
57
73
|
}) {
|
|
58
|
-
const {
|
|
74
|
+
const { labels, onLogout } = useProfileContext();
|
|
75
|
+
const { user, logout } = useAuth();
|
|
76
|
+
const { deleteAccount } = useDeleteAccount();
|
|
77
|
+
const t = useAppT();
|
|
59
78
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
const handleDeleteAccount = useCallback(async () => {
|
|
80
|
+
const confirmationWord = t('layouts.profilePage.confirmationWord');
|
|
81
|
+
const value = await window.dialog.prompt({
|
|
82
|
+
title: t('layouts.profilePage.deleteAccountTitle'),
|
|
83
|
+
message: t('layouts.profilePage.deleteAccountDesc'),
|
|
84
|
+
placeholder: confirmationWord,
|
|
85
|
+
confirmText: t('layouts.profilePage.deleteAccount'),
|
|
86
|
+
cancelText: t('layouts.profilePage.cancel'),
|
|
87
|
+
variant: 'destructive',
|
|
88
|
+
});
|
|
89
|
+
if (value?.toUpperCase() !== confirmationWord.toUpperCase()) return;
|
|
90
|
+
const result = await deleteAccount();
|
|
91
|
+
if (result.success) logout();
|
|
92
|
+
}, [t, deleteAccount, logout]);
|
|
93
|
+
|
|
94
|
+
if (!user) return null;
|
|
67
95
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
alt=""
|
|
73
|
-
className={cn(
|
|
74
|
-
'w-full h-full object-cover rounded-full transition-opacity duration-300',
|
|
75
|
-
isLoaded ? 'opacity-100' : 'opacity-0 absolute inset-0',
|
|
76
|
-
)}
|
|
77
|
-
/>
|
|
78
|
-
)}
|
|
96
|
+
const displayName = user.full_name || user.display_username || user.email;
|
|
97
|
+
const memberSince = user.date_joined
|
|
98
|
+
? moment.utc(user.date_joined).local().format('MMMM YYYY')
|
|
99
|
+
: null;
|
|
79
100
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
{initials}
|
|
84
|
-
</AvatarFallback>
|
|
85
|
-
)}
|
|
86
|
-
</Avatar>
|
|
101
|
+
const badge = slots?.headerBadge ?? null;
|
|
102
|
+
const menuItems = slots?.headerMenuItems ?? null;
|
|
103
|
+
const headerAfter = slots?.headerAfter ?? null;
|
|
87
104
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
<
|
|
105
|
+
return (
|
|
106
|
+
<div className="pb-4 md:pb-6 border-b mb-2">
|
|
107
|
+
<div className="flex items-center gap-3 md:gap-4">
|
|
108
|
+
<AvatarSection />
|
|
109
|
+
|
|
110
|
+
<div className="flex-1 min-w-0">
|
|
111
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
112
|
+
<h1 className="text-lg md:text-xl font-semibold truncate">{displayName}</h1>
|
|
113
|
+
{badge}
|
|
114
|
+
</div>
|
|
115
|
+
<p className="text-sm text-muted-foreground truncate">{user.email}</p>
|
|
116
|
+
{memberSince && (
|
|
117
|
+
<p className="text-xs text-muted-foreground/60 mt-0.5">
|
|
118
|
+
Member since {memberSince}
|
|
119
|
+
</p>
|
|
101
120
|
)}
|
|
102
121
|
</div>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
|
|
123
|
+
<DropdownMenu>
|
|
124
|
+
<DropdownMenuTrigger asChild>
|
|
125
|
+
<Button variant="ghost" size="icon" className="flex-shrink-0 rounded-full">
|
|
126
|
+
<MoreHorizontal className="w-4 h-4" />
|
|
127
|
+
</Button>
|
|
128
|
+
</DropdownMenuTrigger>
|
|
129
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
130
|
+
<DropdownMenuItem onClick={onLogout} className="gap-2">
|
|
131
|
+
<LogOut className="w-4 h-4" />
|
|
132
|
+
{labels.signOut}
|
|
133
|
+
</DropdownMenuItem>
|
|
134
|
+
|
|
135
|
+
{menuItems && <><DropdownMenuSeparator />{menuItems}</>}
|
|
136
|
+
|
|
137
|
+
{enableDeleteAccount && (
|
|
138
|
+
<>
|
|
139
|
+
<DropdownMenuSeparator />
|
|
140
|
+
<DropdownMenuItem
|
|
141
|
+
onClick={handleDeleteAccount}
|
|
142
|
+
className="gap-2 text-destructive focus:text-destructive"
|
|
143
|
+
>
|
|
144
|
+
<Trash2 className="w-4 h-4" />
|
|
145
|
+
{labels.deleteAccount}
|
|
146
|
+
</DropdownMenuItem>
|
|
147
|
+
</>
|
|
148
|
+
)}
|
|
149
|
+
</DropdownMenuContent>
|
|
150
|
+
</DropdownMenu>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{headerAfter && <div className="mt-4">{headerAfter}</div>}
|
|
111
154
|
</div>
|
|
112
155
|
);
|
|
113
156
|
}
|
|
114
157
|
|
|
115
158
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
116
|
-
// Profile
|
|
159
|
+
// Built-in tab: Profile
|
|
117
160
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
118
161
|
|
|
119
|
-
|
|
162
|
+
function TabProfile() {
|
|
163
|
+
const { labels, onFieldSave } = useProfileContext();
|
|
164
|
+
const { user } = useAuth();
|
|
165
|
+
if (!user) return null;
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-6 pt-4">
|
|
169
|
+
<Section title={labels.personalInfo}>
|
|
170
|
+
<EditableField label={labels.firstName} value={user.first_name || ''} placeholder={labels.addFirstName} onSave={(v) => onFieldSave('first_name', v)} />
|
|
171
|
+
<EditableField label={labels.lastName} value={user.last_name || ''} placeholder={labels.addLastName} onSave={(v) => onFieldSave('last_name', v)} />
|
|
172
|
+
<EditableField label={labels.phone} value={user.phone || ''} placeholder={labels.addPhone} onSave={(v) => onFieldSave('phone', v)} type="phone" />
|
|
173
|
+
</Section>
|
|
174
|
+
|
|
175
|
+
<Section title={labels.work}>
|
|
176
|
+
<EditableField label={labels.company} value={user.company || ''} placeholder={labels.addCompany} onSave={(v) => onFieldSave('company', v)} />
|
|
177
|
+
<EditableField label={labels.position} value={user.position || ''} placeholder={labels.addPosition} onSave={(v) => onFieldSave('position', v)} />
|
|
178
|
+
</Section>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
184
|
+
// Built-in tab: Security
|
|
185
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
function TabSecurity() {
|
|
188
|
+
return (
|
|
189
|
+
<div className="pt-4 space-y-4">
|
|
190
|
+
<TwoFactorSection />
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
196
|
+
// Main content
|
|
197
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
function ProfileContent({
|
|
120
200
|
onUnauthenticated,
|
|
121
|
-
|
|
122
|
-
enable2FA = false,
|
|
201
|
+
enable2FA,
|
|
123
202
|
enableDeleteAccount = true,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
const labels = useMemo(() => ({
|
|
130
|
-
title: title || t('layouts.profilePage.title'),
|
|
131
|
-
personalInfo: t('layouts.profilePage.personalInfo'),
|
|
132
|
-
work: t('layouts.profilePage.work'),
|
|
133
|
-
security: t('layouts.profilePage.security'),
|
|
134
|
-
firstName: t('layouts.profilePage.firstName'),
|
|
135
|
-
lastName: t('layouts.profilePage.lastName'),
|
|
136
|
-
email: t('layouts.profilePage.email'),
|
|
137
|
-
phone: t('layouts.profilePage.phone'),
|
|
138
|
-
company: t('layouts.profilePage.company'),
|
|
139
|
-
position: t('layouts.profilePage.position'),
|
|
140
|
-
notSet: t('layouts.profilePage.notSet'),
|
|
141
|
-
addFirstName: t('layouts.profilePage.addFirstName') || 'Add first name',
|
|
142
|
-
addLastName: t('layouts.profilePage.addLastName') || 'Add last name',
|
|
143
|
-
addPhone: t('layouts.profilePage.addPhone') || 'Add phone number',
|
|
144
|
-
addCompany: t('layouts.profilePage.addCompany') || 'Add company',
|
|
145
|
-
addPosition: t('layouts.profilePage.addPosition') || 'Add position',
|
|
146
|
-
signOut: t('layouts.profilePage.signOut'),
|
|
147
|
-
changeAvatar: t('layouts.profilePage.changeAvatar'),
|
|
148
|
-
memberSince: t('layouts.profilePage.memberSince'),
|
|
149
|
-
profileUpdated: t('layouts.profilePage.profileUpdated'),
|
|
150
|
-
avatarUpdated: t('layouts.profilePage.avatarUpdated'),
|
|
151
|
-
failedToUpdate: t('layouts.profilePage.failedToUpdate'),
|
|
152
|
-
failedToUploadAvatar: t('layouts.profilePage.failedToUploadAvatar'),
|
|
153
|
-
selectImageFile: t('layouts.profilePage.selectImageFile'),
|
|
154
|
-
fileTooLarge: t('layouts.profilePage.fileTooLarge'),
|
|
155
|
-
notAuthenticated: t('layouts.profilePage.notAuthenticated'),
|
|
156
|
-
pleaseLogIn: t('layouts.profilePage.pleaseLogIn'),
|
|
157
|
-
loading: t('ui.states.loading'),
|
|
158
|
-
}), [t, title]);
|
|
203
|
+
tabs = [],
|
|
204
|
+
slots,
|
|
205
|
+
}: ProfileLayoutProps) {
|
|
206
|
+
const { labels } = useProfileContext();
|
|
207
|
+
const { user, isLoading } = useAuth();
|
|
159
208
|
|
|
160
209
|
useEffect(() => {
|
|
161
210
|
if (onUnauthenticated && !user && !isLoading) onUnauthenticated();
|
|
162
211
|
}, [onUnauthenticated, user, isLoading]);
|
|
163
212
|
|
|
164
|
-
const handleAvatarChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
165
|
-
const file = event.target.files?.[0];
|
|
166
|
-
if (!file) return;
|
|
167
|
-
if (!file.type.startsWith('image/')) { toast.error(labels.selectImageFile); return; }
|
|
168
|
-
if (file.size > 5 * 1024 * 1024) { toast.error(labels.fileTooLarge); return; }
|
|
169
|
-
|
|
170
|
-
setIsUploading(true);
|
|
171
|
-
try {
|
|
172
|
-
await uploadAvatar(file);
|
|
173
|
-
toast.success(labels.avatarUpdated);
|
|
174
|
-
} catch (error) {
|
|
175
|
-
toast.error(labels.failedToUploadAvatar);
|
|
176
|
-
profileLogger.error('Avatar upload error:', error);
|
|
177
|
-
} finally {
|
|
178
|
-
setIsUploading(false);
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const handleFieldSave = async (field: string, value: string) => {
|
|
183
|
-
try {
|
|
184
|
-
await updateProfile({ [field]: value });
|
|
185
|
-
toast.success(labels.profileUpdated);
|
|
186
|
-
} catch (error: any) {
|
|
187
|
-
profileLogger.error('Profile update error:', error);
|
|
188
|
-
toast.error(error?.response?.data?.[field]?.[0] || labels.failedToUpdate);
|
|
189
|
-
throw error;
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
const handleLogout = useLogout();
|
|
194
|
-
|
|
195
213
|
if (isLoading) {
|
|
196
|
-
return
|
|
197
|
-
<Preloader variant="fullscreen" text={labels.loading} size="lg" backdrop backdropOpacity={80} />
|
|
198
|
-
);
|
|
214
|
+
return <Preloader variant="fullscreen" text={labels.loading} size="lg" backdrop backdropOpacity={80} />;
|
|
199
215
|
}
|
|
200
216
|
|
|
201
217
|
if (!user) {
|
|
@@ -209,105 +225,60 @@ const ProfileContent = ({
|
|
|
209
225
|
);
|
|
210
226
|
}
|
|
211
227
|
|
|
212
|
-
|
|
213
|
-
name ? name.split(' ').map((w) => w[0]).join('').toUpperCase().slice(0, 2) : 'U';
|
|
228
|
+
// ── Prepare data before render ──────────────────────────────────────────────
|
|
214
229
|
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
[user.date_joined, labels.memberSince]
|
|
221
|
-
);
|
|
230
|
+
const extraTriggers = tabs.map((tab) => (
|
|
231
|
+
<TabsTrigger key={tab.value} value={tab.value}>
|
|
232
|
+
{tab.label}
|
|
233
|
+
</TabsTrigger>
|
|
234
|
+
));
|
|
222
235
|
|
|
223
|
-
|
|
224
|
-
<
|
|
225
|
-
{
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
src={user.avatar}
|
|
229
|
-
initials={getInitials(user.display_username || user.email || '')}
|
|
230
|
-
isUploading={isUploading}
|
|
231
|
-
onChange={handleAvatarChange}
|
|
232
|
-
label={labels.changeAvatar}
|
|
233
|
-
/>
|
|
234
|
-
|
|
235
|
-
<h1 className="text-2xl font-semibold tracking-tight">{displayName}</h1>
|
|
236
|
-
<p className="text-[15px] text-muted-foreground mt-1">{user.email}</p>
|
|
237
|
-
{memberSinceText && (
|
|
238
|
-
<p className="text-[13px] text-muted-foreground/60 mt-2">{memberSinceText}</p>
|
|
239
|
-
)}
|
|
240
|
-
</div>
|
|
236
|
+
const extraPanels = tabs.map((tab) => (
|
|
237
|
+
<TabsContent key={tab.value} value={tab.value}>
|
|
238
|
+
{tab.content}
|
|
239
|
+
</TabsContent>
|
|
240
|
+
));
|
|
241
241
|
|
|
242
|
-
|
|
243
|
-
<Section title={labels.personalInfo}>
|
|
244
|
-
<EditableField
|
|
245
|
-
label={labels.firstName}
|
|
246
|
-
value={user.first_name || ''}
|
|
247
|
-
placeholder={labels.addFirstName}
|
|
248
|
-
onSave={(v) => handleFieldSave('first_name', v)}
|
|
249
|
-
/>
|
|
250
|
-
<EditableField
|
|
251
|
-
label={labels.lastName}
|
|
252
|
-
value={user.last_name || ''}
|
|
253
|
-
placeholder={labels.addLastName}
|
|
254
|
-
onSave={(v) => handleFieldSave('last_name', v)}
|
|
255
|
-
/>
|
|
256
|
-
<EditableField
|
|
257
|
-
label={labels.phone}
|
|
258
|
-
value={user.phone || ''}
|
|
259
|
-
placeholder={labels.addPhone}
|
|
260
|
-
onSave={(v) => handleFieldSave('phone', v)}
|
|
261
|
-
type="phone"
|
|
262
|
-
/>
|
|
263
|
-
</Section>
|
|
242
|
+
const footer = slots?.footer ?? null;
|
|
264
243
|
|
|
265
|
-
|
|
266
|
-
<Section title={labels.work}>
|
|
267
|
-
<EditableField
|
|
268
|
-
label={labels.company}
|
|
269
|
-
value={user.company || ''}
|
|
270
|
-
placeholder={labels.addCompany}
|
|
271
|
-
onSave={(v) => handleFieldSave('company', v)}
|
|
272
|
-
/>
|
|
273
|
-
<EditableField
|
|
274
|
-
label={labels.position}
|
|
275
|
-
value={user.position || ''}
|
|
276
|
-
placeholder={labels.addPosition}
|
|
277
|
-
onSave={(v) => handleFieldSave('position', v)}
|
|
278
|
-
/>
|
|
279
|
-
</Section>
|
|
244
|
+
// ── Render ──────────────────────────────────────────────────────────────────
|
|
280
245
|
|
|
281
|
-
|
|
282
|
-
|
|
246
|
+
return (
|
|
247
|
+
<div className="container mx-auto px-4 py-6 md:py-10 max-w-3xl">
|
|
248
|
+
<ProfileHeader slots={slots} enableDeleteAccount={enableDeleteAccount} />
|
|
249
|
+
|
|
250
|
+
<Tabs defaultValue="profile" className="mt-2">
|
|
251
|
+
{/* Underline-style scrollable tabs — mobile friendly */}
|
|
252
|
+
<TabsList variant="underline" scrollable>
|
|
253
|
+
<TabsTrigger value="profile">{labels.personalInfo}</TabsTrigger>
|
|
254
|
+
{enable2FA && <TabsTrigger value="security">{labels.security}</TabsTrigger>}
|
|
255
|
+
{extraTriggers}
|
|
256
|
+
</TabsList>
|
|
257
|
+
|
|
258
|
+
<TabsContent value="profile">
|
|
259
|
+
<TabProfile />
|
|
260
|
+
</TabsContent>
|
|
261
|
+
|
|
262
|
+
{enable2FA && (
|
|
263
|
+
<TabsContent value="security">
|
|
264
|
+
<TabSecurity />
|
|
265
|
+
</TabsContent>
|
|
266
|
+
)}
|
|
283
267
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
<ActionButton
|
|
287
|
-
icon={<LogOut className="w-4 h-4" />}
|
|
288
|
-
label={labels.signOut}
|
|
289
|
-
onClick={handleLogout}
|
|
290
|
-
/>
|
|
291
|
-
</Section>
|
|
268
|
+
{extraPanels}
|
|
269
|
+
</Tabs>
|
|
292
270
|
|
|
293
|
-
{
|
|
271
|
+
{footer && <div className="mt-8">{footer}</div>}
|
|
294
272
|
</div>
|
|
295
273
|
);
|
|
296
|
-
}
|
|
274
|
+
}
|
|
297
275
|
|
|
298
276
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
299
|
-
// Export
|
|
277
|
+
// Router + Export
|
|
300
278
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
301
279
|
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (step === 'delete-account') return <DeleteAccountScreen />;
|
|
306
|
-
return <ProfileContent {...props} />;
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
export const ProfileLayout: React.FC<ProfileLayoutProps> = (props) => (
|
|
310
|
-
<ProfileProvider>
|
|
311
|
-
<ProfileRouter {...props} />
|
|
280
|
+
export const ProfileLayout: React.FC<ProfileLayoutProps> = ({ title, ...props }) => (
|
|
281
|
+
<ProfileProvider title={title}>
|
|
282
|
+
<ProfileContent title={title} {...props} />
|
|
312
283
|
</ProfileProvider>
|
|
313
284
|
);
|
|
@@ -72,11 +72,10 @@ export const AvatarSection = () => {
|
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
return (
|
|
75
|
-
<div className="flex flex-col items-center
|
|
75
|
+
<div className="flex flex-col items-center flex-shrink-0">
|
|
76
76
|
<div className="relative group">
|
|
77
77
|
<Avatar
|
|
78
|
-
className="aspect-square rounded-full overflow-hidden ring-1 ring-foreground/20 transition-transform group-hover:scale-105"
|
|
79
|
-
style={{ width: '80px', height: '80px' }}
|
|
78
|
+
className="w-14 h-14 md:w-20 md:h-20 aspect-square rounded-full overflow-hidden ring-1 ring-foreground/20 transition-transform group-hover:scale-105"
|
|
80
79
|
>
|
|
81
80
|
{avatarPreview ? (
|
|
82
81
|
<img
|