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