@djangocfg/layouts 2.1.426 → 2.1.428
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 +29 -21
- package/package.json +15 -17
- package/src/components/errors/ErrorsTracker/components/ErrorToast.tsx +19 -0
- package/src/components/errors/ErrorsTracker/utils/curl-generator.ts +24 -10
- package/src/components/errors/README.md +63 -0
- package/src/layouts/AppLayout/BaseApp.tsx +36 -52
- package/src/layouts/AppLayout/README.md +79 -64
- package/src/layouts/AppLayout/index.ts +12 -19
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +6 -4
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +7 -4
- package/src/layouts/PrivateLayout/README.md +30 -0
- package/src/layouts/PrivateLayout/components/PrivateContent.tsx +6 -2
- package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +105 -70
- package/src/layouts/PrivateLayout/hooks/useAuthGuard.ts +12 -3
- package/src/layouts/PrivateLayout/types.ts +8 -3
- package/src/layouts/PublicLayout/components/UserMenu.tsx +68 -113
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +0 -6
- package/src/layouts/PublicLayout/navbars/MinimalNavbar/index.ts +1 -1
- package/src/layouts/SettingsLayout/README.md +258 -0
- package/src/layouts/SettingsLayout/SettingsDialog.tsx +101 -0
- package/src/layouts/SettingsLayout/SettingsForm.tsx +100 -0
- package/src/layouts/SettingsLayout/components/ApiKeySection/ApiKeySection.tsx +192 -0
- package/src/layouts/SettingsLayout/components/SettingsNav.tsx +71 -0
- package/src/layouts/SettingsLayout/components/SettingsNavItem.tsx +57 -0
- package/src/layouts/SettingsLayout/components/SettingsPanel.tsx +48 -0
- package/src/layouts/SettingsLayout/components/SettingsSearch.tsx +50 -0
- package/src/layouts/SettingsLayout/components/SettingsShell.tsx +77 -0
- package/src/layouts/SettingsLayout/components/SettingsTabs.tsx +56 -0
- package/src/layouts/{ProfileLayout → SettingsLayout}/components/TwoFactorSection/TwoFactorSection.tsx +84 -130
- package/src/layouts/SettingsLayout/components/index.ts +6 -0
- package/src/layouts/SettingsLayout/context/SettingsContext.tsx +122 -0
- package/src/layouts/SettingsLayout/context/index.ts +2 -0
- package/src/layouts/SettingsLayout/hooks/index.ts +12 -0
- package/src/layouts/SettingsLayout/hooks/useProfileSave.ts +95 -0
- package/src/layouts/SettingsLayout/hooks/useSettingsDialog.ts +52 -0
- package/src/layouts/SettingsLayout/hooks/useSettingsSections.ts +123 -0
- package/src/layouts/SettingsLayout/hooks/useSettingsUrl.ts +140 -0
- package/src/layouts/SettingsLayout/index.ts +67 -0
- package/src/layouts/SettingsLayout/sections/AccountSection.tsx +100 -0
- package/src/layouts/SettingsLayout/sections/ApiKeysSection.tsx +15 -0
- package/src/layouts/SettingsLayout/sections/DeleteAccountRow.tsx +57 -0
- package/src/layouts/SettingsLayout/sections/PreferencesRows.tsx +43 -0
- package/src/layouts/SettingsLayout/sections/SecuritySection.tsx +15 -0
- package/src/layouts/SettingsLayout/sections/builtins.tsx +77 -0
- package/src/layouts/SettingsLayout/sections/index.ts +8 -0
- package/src/layouts/SettingsLayout/store.ts +47 -0
- package/src/layouts/SettingsLayout/types.ts +107 -0
- package/src/layouts/index.ts +1 -2
- package/src/layouts/types/index.ts +0 -1
- package/src/layouts/types/layout.types.ts +0 -4
- package/src/utils/logger.ts +9 -4
- package/src/layouts/AdminLayout/AdminLayout.tsx +0 -57
- package/src/layouts/AdminLayout/index.ts +0 -7
- package/src/layouts/AppLayout/AppLayout.tsx +0 -520
- package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +0 -56
- package/src/layouts/ProfileLayout/ProfileDialog/index.ts +0 -4
- package/src/layouts/ProfileLayout/ProfileDialog/store.ts +0 -51
- package/src/layouts/ProfileLayout/ProfileForm/context.tsx +0 -123
- package/src/layouts/ProfileLayout/ProfileForm/index.tsx +0 -147
- package/src/layouts/ProfileLayout/README.md +0 -150
- package/src/layouts/ProfileLayout/components/ActionButton.tsx +0 -38
- package/src/layouts/ProfileLayout/components/ApiKeySection/ApiKeySection.tsx +0 -197
- package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +0 -44
- package/src/layouts/ProfileLayout/components/EditableField.tsx +0 -128
- package/src/layouts/ProfileLayout/components/PreferencesSection.tsx +0 -56
- package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +0 -110
- package/src/layouts/ProfileLayout/components/ProfileTab.tsx +0 -35
- package/src/layouts/ProfileLayout/components/Section.tsx +0 -22
- package/src/layouts/ProfileLayout/components/index.ts +0 -11
- package/src/layouts/ProfileLayout/hooks/index.ts +0 -2
- package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +0 -56
- package/src/layouts/ProfileLayout/index.ts +0 -8
- package/src/layouts/ProfileLayout/types.ts +0 -48
- /package/src/layouts/{ProfileLayout → SettingsLayout}/components/ApiKeySection/context.tsx +0 -0
- /package/src/layouts/{ProfileLayout → SettingsLayout}/components/ApiKeySection/index.ts +0 -0
- /package/src/layouts/{ProfileLayout → SettingsLayout}/components/AvatarSection.tsx +0 -0
- /package/src/layouts/{ProfileLayout → SettingsLayout}/components/TwoFactorSection/index.ts +0 -0
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Trash2 } from 'lucide-react';
|
|
4
|
-
import React from 'react';
|
|
5
|
-
|
|
6
|
-
import { useAuth, useDeleteAccount } from '@djangocfg/api/auth';
|
|
7
|
-
import { useAppT } from '@djangocfg/i18n';
|
|
8
|
-
|
|
9
|
-
import { ActionButton } from './ActionButton';
|
|
10
|
-
|
|
11
|
-
export const DeleteAccountSection: React.FC = () => {
|
|
12
|
-
const { logout } = useAuth();
|
|
13
|
-
const { deleteAccount } = useDeleteAccount();
|
|
14
|
-
const t = useAppT();
|
|
15
|
-
|
|
16
|
-
const confirmationWord = t('layouts.profilePage.confirmationWord');
|
|
17
|
-
|
|
18
|
-
const handleClick = async () => {
|
|
19
|
-
const value = await window.dialog.prompt({
|
|
20
|
-
title: t('layouts.profilePage.deleteAccountTitle'),
|
|
21
|
-
message: t('layouts.profilePage.deleteAccountDesc'),
|
|
22
|
-
placeholder: confirmationWord,
|
|
23
|
-
confirmText: t('layouts.profilePage.deleteAccount'),
|
|
24
|
-
cancelText: t('layouts.profilePage.cancel'),
|
|
25
|
-
variant: 'destructive',
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
if (value?.toUpperCase() !== confirmationWord.toUpperCase()) return;
|
|
29
|
-
|
|
30
|
-
const result = await deleteAccount();
|
|
31
|
-
if (result.success) logout();
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<ActionButton
|
|
36
|
-
icon={<Trash2 className="w-4 h-4 text-destructive" />}
|
|
37
|
-
label={<span className="text-destructive">{t('layouts.profilePage.deleteAccount')}</span>}
|
|
38
|
-
onClick={handleClick}
|
|
39
|
-
/>
|
|
40
|
-
);
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// Keep export so nothing breaks — no longer used but exported for backwards compat
|
|
44
|
-
export const DeleteAccountScreen: React.FC = () => null;
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React, { useEffect, useState } from 'react';
|
|
4
|
-
import { parsePhoneNumberFromString } from 'libphonenumber-js';
|
|
5
|
-
|
|
6
|
-
import { Button, Input, PhoneInput } from '@djangocfg/ui-core/components';
|
|
7
|
-
import { toast } from '@djangocfg/ui-core/hooks';
|
|
8
|
-
import { cn } from '@djangocfg/ui-core/lib';
|
|
9
|
-
|
|
10
|
-
import { useProfileContext } from '../ProfileForm/context';
|
|
11
|
-
|
|
12
|
-
function formatPhone(raw: string): string {
|
|
13
|
-
if (!raw) return '';
|
|
14
|
-
try {
|
|
15
|
-
return parsePhoneNumberFromString(raw)?.formatInternational() ?? raw;
|
|
16
|
-
} catch {
|
|
17
|
-
return raw;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface EditableFieldProps {
|
|
22
|
-
label: string;
|
|
23
|
-
value: string;
|
|
24
|
-
placeholder: string;
|
|
25
|
-
onSave: (value: string) => Promise<void>;
|
|
26
|
-
disabled?: boolean;
|
|
27
|
-
type?: 'text' | 'phone';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const EditableField = ({
|
|
31
|
-
label,
|
|
32
|
-
value,
|
|
33
|
-
placeholder,
|
|
34
|
-
onSave,
|
|
35
|
-
disabled,
|
|
36
|
-
type = 'text',
|
|
37
|
-
}: EditableFieldProps) => {
|
|
38
|
-
const { labels } = useProfileContext();
|
|
39
|
-
const [isEditing, setIsEditing] = useState(false);
|
|
40
|
-
const [editValue, setEditValue] = useState(value);
|
|
41
|
-
const [isSaving, setIsSaving] = useState(false);
|
|
42
|
-
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
setEditValue(value);
|
|
45
|
-
}, [value]);
|
|
46
|
-
|
|
47
|
-
const handleSave = async () => {
|
|
48
|
-
if (editValue === value) {
|
|
49
|
-
setIsEditing(false);
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
setIsSaving(true);
|
|
53
|
-
try {
|
|
54
|
-
await onSave(editValue);
|
|
55
|
-
setIsEditing(false);
|
|
56
|
-
} catch {
|
|
57
|
-
// Keep editing mode open with the entered value so the user can retry.
|
|
58
|
-
toast.error(labels.failedToUpdate);
|
|
59
|
-
} finally {
|
|
60
|
-
setIsSaving(false);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const handleCancel = () => {
|
|
65
|
-
setEditValue(value);
|
|
66
|
-
setIsEditing(false);
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
70
|
-
if (e.key === 'Enter') handleSave();
|
|
71
|
-
else if (e.key === 'Escape') handleCancel();
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
if (isEditing) {
|
|
75
|
-
return (
|
|
76
|
-
<div className="py-4 border-b border-border/50 last:border-0">
|
|
77
|
-
<label className="block text-[13px] text-muted-foreground mb-1.5">{label}</label>
|
|
78
|
-
{type === 'phone' ? (
|
|
79
|
-
<div className="space-y-2">
|
|
80
|
-
<PhoneInput
|
|
81
|
-
value={editValue}
|
|
82
|
-
onChange={(val) => setEditValue(val ?? '')}
|
|
83
|
-
placeholder={placeholder}
|
|
84
|
-
disabled={isSaving}
|
|
85
|
-
/>
|
|
86
|
-
<div className="flex gap-2">
|
|
87
|
-
<Button size="sm" onClick={handleSave} disabled={isSaving}>
|
|
88
|
-
{isSaving ? labels.saving : labels.save}
|
|
89
|
-
</Button>
|
|
90
|
-
<Button size="sm" variant="ghost" onClick={handleCancel} disabled={isSaving}>
|
|
91
|
-
{labels.cancel}
|
|
92
|
-
</Button>
|
|
93
|
-
</div>
|
|
94
|
-
</div>
|
|
95
|
-
) : (
|
|
96
|
-
<Input
|
|
97
|
-
value={editValue}
|
|
98
|
-
onChange={(e) => setEditValue(e.target.value)}
|
|
99
|
-
onKeyDown={handleKeyDown}
|
|
100
|
-
onBlur={handleSave}
|
|
101
|
-
placeholder={placeholder}
|
|
102
|
-
autoFocus
|
|
103
|
-
disabled={isSaving}
|
|
104
|
-
className="h-9 text-[15px]"
|
|
105
|
-
/>
|
|
106
|
-
)}
|
|
107
|
-
</div>
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return (
|
|
112
|
-
<button
|
|
113
|
-
type="button"
|
|
114
|
-
onClick={() => !disabled && setIsEditing(true)}
|
|
115
|
-
disabled={disabled}
|
|
116
|
-
className={cn(
|
|
117
|
-
'w-full py-4 border-b border-border/50 last:border-0 text-left',
|
|
118
|
-
'transition-colors hover:bg-muted/30',
|
|
119
|
-
disabled && 'cursor-default hover:bg-transparent'
|
|
120
|
-
)}
|
|
121
|
-
>
|
|
122
|
-
<div className="text-[13px] text-muted-foreground mb-0.5">{label}</div>
|
|
123
|
-
<div className={cn('text-[15px]', value ? 'text-foreground' : 'text-muted-foreground/60')}>
|
|
124
|
-
{value ? (type === 'phone' ? formatPhone(value) : value) : placeholder}
|
|
125
|
-
</div>
|
|
126
|
-
</button>
|
|
127
|
-
);
|
|
128
|
-
};
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Preferences Section
|
|
3
|
-
*
|
|
4
|
-
* Language + theme controls for ProfileForm.
|
|
5
|
-
* Uses ThemeToggle from ui-nextjs and LocaleSwitcherDropdown from _components.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
'use client';
|
|
9
|
-
|
|
10
|
-
import React from 'react';
|
|
11
|
-
|
|
12
|
-
import { ThemeToggle } from '@djangocfg/ui-core/theme';
|
|
13
|
-
import { cn } from '@djangocfg/ui-core/lib';
|
|
14
|
-
|
|
15
|
-
import { LocaleSwitcherDropdown } from '../../_components/locale-switcher';
|
|
16
|
-
import { useLayoutI18nOptional } from '../../AppLayout/LayoutI18nProvider';
|
|
17
|
-
|
|
18
|
-
interface PreferencesSectionProps {
|
|
19
|
-
/** Extra className for the root. */
|
|
20
|
-
className?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const PreferencesSection: React.FC<PreferencesSectionProps> = ({
|
|
24
|
-
className,
|
|
25
|
-
}) => {
|
|
26
|
-
const layoutI18n = useLayoutI18nOptional();
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<div className={cn('py-0', className)}>
|
|
30
|
-
{layoutI18n && (
|
|
31
|
-
<>
|
|
32
|
-
<div className="flex items-center justify-between py-3">
|
|
33
|
-
<span className="text-sm">Language</span>
|
|
34
|
-
<LocaleSwitcherDropdown
|
|
35
|
-
locale={layoutI18n.locale}
|
|
36
|
-
locales={layoutI18n.locales}
|
|
37
|
-
onChange={layoutI18n.onLocaleChange}
|
|
38
|
-
variant="outline"
|
|
39
|
-
size="sm"
|
|
40
|
-
showCode
|
|
41
|
-
showIcon={false}
|
|
42
|
-
showFlag
|
|
43
|
-
showTriggerLabel
|
|
44
|
-
/>
|
|
45
|
-
</div>
|
|
46
|
-
<div className="h-px bg-border/60" />
|
|
47
|
-
</>
|
|
48
|
-
)}
|
|
49
|
-
|
|
50
|
-
<div className="flex items-center justify-between py-3">
|
|
51
|
-
<span className="text-sm">Theme</span>
|
|
52
|
-
<ThemeToggle size="default" />
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
);
|
|
56
|
-
};
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { LogOut, MoreHorizontal, Trash2 } from 'lucide-react';
|
|
4
|
-
import moment from 'moment';
|
|
5
|
-
import React, { useCallback } from 'react';
|
|
6
|
-
|
|
7
|
-
import { useAuth, useDeleteAccount } from '@djangocfg/api/auth';
|
|
8
|
-
import { useAppT } from '@djangocfg/i18n';
|
|
9
|
-
import {
|
|
10
|
-
Button,
|
|
11
|
-
DropdownMenu,
|
|
12
|
-
DropdownMenuContent,
|
|
13
|
-
DropdownMenuItem,
|
|
14
|
-
DropdownMenuSeparator,
|
|
15
|
-
DropdownMenuTrigger,
|
|
16
|
-
} from '@djangocfg/ui-core/components';
|
|
17
|
-
|
|
18
|
-
import { AvatarSection } from './AvatarSection';
|
|
19
|
-
import { useProfileContext } from '../ProfileForm/context';
|
|
20
|
-
import type { ProfileSlots } from '../types';
|
|
21
|
-
|
|
22
|
-
interface ProfileHeaderProps {
|
|
23
|
-
slots?: ProfileSlots;
|
|
24
|
-
enableDeleteAccount?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export const ProfileHeader: React.FC<ProfileHeaderProps> = ({ slots, enableDeleteAccount }) => {
|
|
28
|
-
const { labels, onLogout } = useProfileContext();
|
|
29
|
-
const { user, logout } = useAuth();
|
|
30
|
-
const { deleteAccount } = useDeleteAccount();
|
|
31
|
-
const t = useAppT();
|
|
32
|
-
|
|
33
|
-
const handleDeleteAccount = useCallback(async () => {
|
|
34
|
-
const confirmationWord = t('layouts.profilePage.confirmationWord');
|
|
35
|
-
const value = await window.dialog.prompt({
|
|
36
|
-
title: t('layouts.profilePage.deleteAccountTitle'),
|
|
37
|
-
message: t('layouts.profilePage.deleteAccountDesc'),
|
|
38
|
-
placeholder: confirmationWord,
|
|
39
|
-
confirmText: t('layouts.profilePage.deleteAccount'),
|
|
40
|
-
cancelText: t('layouts.profilePage.cancel'),
|
|
41
|
-
variant: 'destructive',
|
|
42
|
-
});
|
|
43
|
-
if (value?.toUpperCase() !== confirmationWord.toUpperCase()) return;
|
|
44
|
-
const result = await deleteAccount();
|
|
45
|
-
if (result.success) logout();
|
|
46
|
-
}, [t, deleteAccount, logout]);
|
|
47
|
-
|
|
48
|
-
if (!user) return null;
|
|
49
|
-
|
|
50
|
-
const displayName = user.full_name || user.display_username || user.email;
|
|
51
|
-
const memberSince = user.date_joined
|
|
52
|
-
? moment.utc(user.date_joined).local().format('MMMM YYYY')
|
|
53
|
-
: null;
|
|
54
|
-
|
|
55
|
-
const badge = slots?.headerBadge ?? null;
|
|
56
|
-
const menuItems = slots?.headerMenuItems ?? null;
|
|
57
|
-
const headerAfter = slots?.headerAfter ?? null;
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<div className="pb-4 md:pb-6 border-b mb-2">
|
|
61
|
-
<div className="flex items-center gap-3 md:gap-4">
|
|
62
|
-
<AvatarSection />
|
|
63
|
-
|
|
64
|
-
<div className="flex-1 min-w-0">
|
|
65
|
-
<div className="flex items-center gap-2 flex-wrap">
|
|
66
|
-
<h1 className="text-lg md:text-xl font-semibold truncate">{displayName}</h1>
|
|
67
|
-
{badge}
|
|
68
|
-
</div>
|
|
69
|
-
<p className="text-sm text-muted-foreground truncate">{user.email}</p>
|
|
70
|
-
{memberSince && (
|
|
71
|
-
<p className="text-xs text-muted-foreground/60 mt-0.5">
|
|
72
|
-
Member since {memberSince}
|
|
73
|
-
</p>
|
|
74
|
-
)}
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
<DropdownMenu>
|
|
78
|
-
<DropdownMenuTrigger asChild>
|
|
79
|
-
<Button variant="ghost" size="icon" className="flex-shrink-0 rounded-full">
|
|
80
|
-
<MoreHorizontal className="w-4 h-4" />
|
|
81
|
-
</Button>
|
|
82
|
-
</DropdownMenuTrigger>
|
|
83
|
-
<DropdownMenuContent align="end" className="w-48">
|
|
84
|
-
<DropdownMenuItem onClick={onLogout} className="gap-2">
|
|
85
|
-
<LogOut className="w-4 h-4" />
|
|
86
|
-
{labels.signOut}
|
|
87
|
-
</DropdownMenuItem>
|
|
88
|
-
|
|
89
|
-
{menuItems && <><DropdownMenuSeparator />{menuItems}</>}
|
|
90
|
-
|
|
91
|
-
{enableDeleteAccount && (
|
|
92
|
-
<>
|
|
93
|
-
<DropdownMenuSeparator />
|
|
94
|
-
<DropdownMenuItem
|
|
95
|
-
onClick={handleDeleteAccount}
|
|
96
|
-
className="gap-2 text-destructive focus:text-destructive"
|
|
97
|
-
>
|
|
98
|
-
<Trash2 className="w-4 h-4" />
|
|
99
|
-
{labels.deleteAccount}
|
|
100
|
-
</DropdownMenuItem>
|
|
101
|
-
</>
|
|
102
|
-
)}
|
|
103
|
-
</DropdownMenuContent>
|
|
104
|
-
</DropdownMenu>
|
|
105
|
-
</div>
|
|
106
|
-
|
|
107
|
-
{headerAfter && <div className="mt-4">{headerAfter}</div>}
|
|
108
|
-
</div>
|
|
109
|
-
);
|
|
110
|
-
};
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React from 'react';
|
|
4
|
-
|
|
5
|
-
import { useAuth } from '@djangocfg/api/auth';
|
|
6
|
-
|
|
7
|
-
import { EditableField, PreferencesSection, Section } from '.';
|
|
8
|
-
import { useProfileContext } from '../ProfileForm/context';
|
|
9
|
-
|
|
10
|
-
export const ProfileTab: React.FC = () => {
|
|
11
|
-
const { labels, onFieldSave } = useProfileContext();
|
|
12
|
-
const { user } = useAuth();
|
|
13
|
-
if (!user) return null;
|
|
14
|
-
|
|
15
|
-
return (
|
|
16
|
-
<div className="space-y-6 pt-4">
|
|
17
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 md:gap-6">
|
|
18
|
-
<Section title={labels.personalInfo}>
|
|
19
|
-
<EditableField label={labels.firstName} value={user.first_name || ''} placeholder={labels.addFirstName} onSave={(v) => onFieldSave('first_name', v)} />
|
|
20
|
-
<EditableField label={labels.lastName} value={user.last_name || ''} placeholder={labels.addLastName} onSave={(v) => onFieldSave('last_name', v)} />
|
|
21
|
-
<EditableField label={labels.phone} value={user.phone || ''} placeholder={labels.addPhone} onSave={(v) => onFieldSave('phone', v)} type="phone" />
|
|
22
|
-
</Section>
|
|
23
|
-
|
|
24
|
-
<Section title={labels.work}>
|
|
25
|
-
<EditableField label={labels.company} value={user.company || ''} placeholder={labels.addCompany} onSave={(v) => onFieldSave('company', v)} />
|
|
26
|
-
<EditableField label={labels.position} value={user.position || ''} placeholder={labels.addPosition} onSave={(v) => onFieldSave('position', v)} />
|
|
27
|
-
</Section>
|
|
28
|
-
</div>
|
|
29
|
-
|
|
30
|
-
<Section title={labels.preferences ?? 'Preferences'}>
|
|
31
|
-
<PreferencesSection />
|
|
32
|
-
</Section>
|
|
33
|
-
</div>
|
|
34
|
-
);
|
|
35
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React from 'react';
|
|
4
|
-
|
|
5
|
-
import { cn } from '@djangocfg/ui-core/lib';
|
|
6
|
-
|
|
7
|
-
interface SectionProps {
|
|
8
|
-
title?: string;
|
|
9
|
-
children: React.ReactNode;
|
|
10
|
-
className?: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export const Section = ({ title, children, className }: SectionProps) => (
|
|
14
|
-
<div className={cn('mb-4 md:mb-6', className)}>
|
|
15
|
-
{title && (
|
|
16
|
-
<h2 className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2 px-1">
|
|
17
|
-
{title}
|
|
18
|
-
</h2>
|
|
19
|
-
)}
|
|
20
|
-
<div className="bg-card rounded-xl px-4">{children}</div>
|
|
21
|
-
</div>
|
|
22
|
-
);
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export { ActionButton } from './ActionButton';
|
|
2
|
-
export { ApiKeySection, ApiKeyProvider, useApiKeyContext } from './ApiKeySection';
|
|
3
|
-
export type { ApiKeyLabels } from './ApiKeySection';
|
|
4
|
-
export { AvatarSection } from './AvatarSection';
|
|
5
|
-
export { DeleteAccountSection, DeleteAccountScreen } from './DeleteAccountSection';
|
|
6
|
-
export { EditableField } from './EditableField';
|
|
7
|
-
export { PreferencesSection } from './PreferencesSection';
|
|
8
|
-
export { ProfileHeader } from './ProfileHeader';
|
|
9
|
-
export { ProfileTab } from './ProfileTab';
|
|
10
|
-
export { Section } from './Section';
|
|
11
|
-
export { TwoFactorSection } from './TwoFactorSection';
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useMemo, useState, useCallback } from 'react';
|
|
4
|
-
|
|
5
|
-
/** Built-in tab values. */
|
|
6
|
-
export type ProfileTabValue = 'profile' | 'security' | 'api-keys';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Any tab value: a built-in one, or a custom tab's `value` string.
|
|
10
|
-
* `(string & {})` keeps the built-in literals as autocomplete hints while
|
|
11
|
-
* still accepting arbitrary strings (custom tabs added via `tabs`).
|
|
12
|
-
*/
|
|
13
|
-
export type ProfileTabValueOrCustom = ProfileTabValue | (string & {});
|
|
14
|
-
|
|
15
|
-
export interface UseProfileTabsOptions {
|
|
16
|
-
enable2FA?: boolean;
|
|
17
|
-
enableAPIKeys?: boolean;
|
|
18
|
-
extraTabValues?: string[];
|
|
19
|
-
/** Initial active tab. Defaults to `'profile'`. Accepts custom tab values. */
|
|
20
|
-
defaultTab?: ProfileTabValueOrCustom;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Manages the active profile tab with local React state.
|
|
25
|
-
*
|
|
26
|
-
* Falls back to `'profile'` when `defaultTab` is missing or not allowed.
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* const { tab, setTab, allowed } = useProfileTabs({ enable2FA: true, defaultTab: 'security' });
|
|
30
|
-
* <Tabs value={tab} onValueChange={setTab}>...</Tabs>
|
|
31
|
-
*/
|
|
32
|
-
export function useProfileTabs(options: UseProfileTabsOptions = {}) {
|
|
33
|
-
const { enable2FA, enableAPIKeys, extraTabValues = [], defaultTab } = options;
|
|
34
|
-
|
|
35
|
-
const allowed = useMemo(() => {
|
|
36
|
-
const base: ProfileTabValueOrCustom[] = ['profile'];
|
|
37
|
-
if (enable2FA) base.push('security');
|
|
38
|
-
if (enableAPIKeys) base.push('api-keys');
|
|
39
|
-
return [...base, ...extraTabValues];
|
|
40
|
-
}, [enable2FA, enableAPIKeys, extraTabValues]);
|
|
41
|
-
|
|
42
|
-
const [tab, setTabState] = useState<ProfileTabValueOrCustom>(
|
|
43
|
-
defaultTab && allowed.includes(defaultTab) ? defaultTab : 'profile',
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const setTab = useCallback(
|
|
47
|
-
(value: ProfileTabValueOrCustom) => {
|
|
48
|
-
if (allowed.includes(value)) {
|
|
49
|
-
setTabState(value);
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
[allowed],
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
return { tab, setTab, allowed };
|
|
56
|
-
}
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export { ProfileForm, useProfileContext } from './ProfileForm';
|
|
2
|
-
export { ProfileProvider } from './ProfileForm/context';
|
|
3
|
-
export { useProfileTabs } from './hooks';
|
|
4
|
-
export { ProfileDialog, useProfileDialogStore } from './ProfileDialog';
|
|
5
|
-
export type { ProfileDialogProps, ProfileDialogContent } from './ProfileDialog';
|
|
6
|
-
export type { ProfileLabels } from './ProfileForm/context';
|
|
7
|
-
export type { ProfileFormProps, ProfileLayoutProps, ProfileTab, ProfileSlots } from './types';
|
|
8
|
-
export type { ProfileTabValue, ProfileTabValueOrCustom, UseProfileTabsOptions } from './hooks';
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
import type { ProfileTabValueOrCustom } from './hooks/useProfileTabs';
|
|
4
|
-
|
|
5
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
-
// Slot + Tab types
|
|
7
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
-
|
|
9
|
-
export interface ProfileTab {
|
|
10
|
-
/** Unique key, used as Tabs value */
|
|
11
|
-
value: string;
|
|
12
|
-
/** Trigger label */
|
|
13
|
-
label: React.ReactNode;
|
|
14
|
-
/** Tab panel content */
|
|
15
|
-
content: React.ReactNode;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface ProfileSlots {
|
|
19
|
-
/** Extra items inside the ⋯ dropdown, above the separator before Delete */
|
|
20
|
-
headerMenuItems?: React.ReactNode;
|
|
21
|
-
/** Rendered next to the user name (e.g. plan badge, role chip) */
|
|
22
|
-
headerBadge?: React.ReactNode;
|
|
23
|
-
/** Rendered below the avatar row, above the tabs */
|
|
24
|
-
headerAfter?: React.ReactNode;
|
|
25
|
-
/** Rendered below all tab content */
|
|
26
|
-
footer?: React.ReactNode;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface ProfileFormProps {
|
|
30
|
-
onUnauthenticated?: () => void;
|
|
31
|
-
title?: string;
|
|
32
|
-
enable2FA?: boolean;
|
|
33
|
-
enableAPIKeys?: boolean;
|
|
34
|
-
enableDeleteAccount?: boolean;
|
|
35
|
-
/** Extra tabs appended after built-in Profile / Security / API Keys tabs */
|
|
36
|
-
tabs?: ProfileTab[];
|
|
37
|
-
/** Named slots for additional content */
|
|
38
|
-
slots?: ProfileSlots;
|
|
39
|
-
/**
|
|
40
|
-
* When provided, the active tab is controlled locally (no URL sync).
|
|
41
|
-
* Useful for dialogs where query-string pollution is undesirable.
|
|
42
|
-
* Accepts a built-in value or a custom tab's `value`.
|
|
43
|
-
*/
|
|
44
|
-
defaultTab?: ProfileTabValueOrCustom;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** @deprecated Use ProfileFormProps instead */
|
|
48
|
-
export type ProfileLayoutProps = ProfileFormProps;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|