@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,123 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React, { createContext, useCallback, useContext, useMemo } from 'react';
|
|
4
|
-
|
|
5
|
-
import { useAuth } from '@djangocfg/api/auth';
|
|
6
|
-
import { useAppT } from '@djangocfg/i18n';
|
|
7
|
-
import { toast } from '@djangocfg/ui-core/hooks';
|
|
8
|
-
|
|
9
|
-
import { profileLogger } from '../../../utils/logger';
|
|
10
|
-
import { useLogout } from '../../../hooks';
|
|
11
|
-
|
|
12
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
-
// Types
|
|
14
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
-
|
|
16
|
-
export interface ProfileLabels {
|
|
17
|
-
title: string;
|
|
18
|
-
personalInfo: string;
|
|
19
|
-
work: string;
|
|
20
|
-
security: string;
|
|
21
|
-
apiKeys: string;
|
|
22
|
-
preferences: string;
|
|
23
|
-
firstName: string;
|
|
24
|
-
lastName: string;
|
|
25
|
-
phone: string;
|
|
26
|
-
company: string;
|
|
27
|
-
position: string;
|
|
28
|
-
addFirstName: string;
|
|
29
|
-
addLastName: string;
|
|
30
|
-
addPhone: string;
|
|
31
|
-
addCompany: string;
|
|
32
|
-
addPosition: string;
|
|
33
|
-
signOut: string;
|
|
34
|
-
deleteAccount: string;
|
|
35
|
-
profileUpdated: string;
|
|
36
|
-
failedToUpdate: string;
|
|
37
|
-
notAuthenticated: string;
|
|
38
|
-
pleaseLogIn: string;
|
|
39
|
-
loading: string;
|
|
40
|
-
save: string;
|
|
41
|
-
saving: string;
|
|
42
|
-
cancel: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
interface ProfileContextValue {
|
|
46
|
-
labels: ProfileLabels;
|
|
47
|
-
onLogout: () => void;
|
|
48
|
-
onFieldSave: (field: string, value: string) => Promise<void>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
-
// Context
|
|
53
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
-
|
|
55
|
-
const ProfileContext = createContext<ProfileContextValue | null>(null);
|
|
56
|
-
|
|
57
|
-
export const useProfileContext = (): ProfileContextValue => {
|
|
58
|
-
const ctx = useContext(ProfileContext);
|
|
59
|
-
if (!ctx) throw new Error('useProfileContext must be used within ProfileProvider');
|
|
60
|
-
return ctx;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
-
// Provider
|
|
65
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
interface ProfileProviderProps {
|
|
68
|
-
children: React.ReactNode;
|
|
69
|
-
title?: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export const ProfileProvider: React.FC<ProfileProviderProps> = ({ children, title }) => {
|
|
73
|
-
const { updateProfile } = useAuth();
|
|
74
|
-
const t = useAppT();
|
|
75
|
-
const onLogout = useLogout();
|
|
76
|
-
|
|
77
|
-
const labels = useMemo<ProfileLabels>(() => ({
|
|
78
|
-
title: title || t('layouts.profilePage.title'),
|
|
79
|
-
personalInfo: t('layouts.profilePage.personalInfo'),
|
|
80
|
-
work: t('layouts.profilePage.work'),
|
|
81
|
-
security: t('layouts.profilePage.security'),
|
|
82
|
-
apiKeys: 'API Keys',
|
|
83
|
-
preferences: 'Preferences',
|
|
84
|
-
firstName: t('layouts.profilePage.firstName'),
|
|
85
|
-
lastName: t('layouts.profilePage.lastName'),
|
|
86
|
-
phone: t('layouts.profilePage.phone'),
|
|
87
|
-
company: t('layouts.profilePage.company'),
|
|
88
|
-
position: t('layouts.profilePage.position'),
|
|
89
|
-
addFirstName: t('layouts.profilePage.addFirstName') || 'Add first name',
|
|
90
|
-
addLastName: t('layouts.profilePage.addLastName') || 'Add last name',
|
|
91
|
-
addPhone: t('layouts.profilePage.addPhone') || 'Add phone number',
|
|
92
|
-
addCompany: t('layouts.profilePage.addCompany') || 'Add company',
|
|
93
|
-
addPosition: t('layouts.profilePage.addPosition') || 'Add position',
|
|
94
|
-
signOut: t('layouts.profilePage.signOut'),
|
|
95
|
-
deleteAccount: t('layouts.profilePage.deleteAccount'),
|
|
96
|
-
profileUpdated: t('layouts.profilePage.profileUpdated'),
|
|
97
|
-
failedToUpdate: t('layouts.profilePage.failedToUpdate'),
|
|
98
|
-
notAuthenticated: t('layouts.profilePage.notAuthenticated'),
|
|
99
|
-
pleaseLogIn: t('layouts.profilePage.pleaseLogIn'),
|
|
100
|
-
loading: t('ui.states.loading'),
|
|
101
|
-
save: t('layouts.profilePage.save'),
|
|
102
|
-
saving: t('layouts.profilePage.saving'),
|
|
103
|
-
cancel: t('layouts.profilePage.cancel'),
|
|
104
|
-
}), [t, title]);
|
|
105
|
-
|
|
106
|
-
const onFieldSave = useCallback(async (field: string, value: string) => {
|
|
107
|
-
try {
|
|
108
|
-
await updateProfile({ [field]: value });
|
|
109
|
-
toast.success(labels.profileUpdated);
|
|
110
|
-
} catch (error: unknown) {
|
|
111
|
-
profileLogger.error('Profile update error:', error);
|
|
112
|
-
const apiErr = error as { response?: Record<string, string[]> };
|
|
113
|
-
toast.error(apiErr?.response?.[field]?.[0] || labels.failedToUpdate);
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
}, [updateProfile, labels]);
|
|
117
|
-
|
|
118
|
-
return (
|
|
119
|
-
<ProfileContext.Provider value={{ labels, onLogout, onFieldSave }}>
|
|
120
|
-
{children}
|
|
121
|
-
</ProfileContext.Provider>
|
|
122
|
-
);
|
|
123
|
-
};
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React, { useEffect } from 'react';
|
|
4
|
-
|
|
5
|
-
import { useAuth } from '@djangocfg/api/auth';
|
|
6
|
-
import { useAppT } from '@djangocfg/i18n';
|
|
7
|
-
import {
|
|
8
|
-
Preloader,
|
|
9
|
-
Tabs,
|
|
10
|
-
TabsContent,
|
|
11
|
-
TabsList,
|
|
12
|
-
TabsTrigger,
|
|
13
|
-
} from '@djangocfg/ui-core/components';
|
|
14
|
-
|
|
15
|
-
import { ApiKeySection, ProfileHeader, ProfileTab, TwoFactorSection } from '../components';
|
|
16
|
-
import { ProfileProvider, useProfileContext } from './context';
|
|
17
|
-
import { useProfileTabs } from '../hooks/useProfileTabs';
|
|
18
|
-
import type { ProfileFormProps } from '../types';
|
|
19
|
-
|
|
20
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
-
// Built-in tab panels
|
|
22
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
function TabSecurity() {
|
|
25
|
-
return (
|
|
26
|
-
<div className="pt-4 space-y-4">
|
|
27
|
-
<TwoFactorSection />
|
|
28
|
-
</div>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function TabApiKeys() {
|
|
33
|
-
return (
|
|
34
|
-
<div className="pt-4 space-y-4">
|
|
35
|
-
<ApiKeySection />
|
|
36
|
-
</div>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
-
// Content (inside ProfileProvider)
|
|
42
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
function ProfileContent({
|
|
45
|
-
onUnauthenticated,
|
|
46
|
-
enable2FA,
|
|
47
|
-
enableAPIKeys = true,
|
|
48
|
-
enableDeleteAccount = true,
|
|
49
|
-
tabs = [],
|
|
50
|
-
slots,
|
|
51
|
-
defaultTab,
|
|
52
|
-
}: ProfileFormProps) {
|
|
53
|
-
const { labels } = useProfileContext();
|
|
54
|
-
const { user, isLoading } = useAuth();
|
|
55
|
-
|
|
56
|
-
const extraTabValues = React.useMemo(() => tabs.map((t) => t.value), [tabs]);
|
|
57
|
-
|
|
58
|
-
const { tab, setTab, allowed } = useProfileTabs({
|
|
59
|
-
enable2FA,
|
|
60
|
-
enableAPIKeys,
|
|
61
|
-
extraTabValues,
|
|
62
|
-
defaultTab,
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
const handleTabChange = React.useCallback(
|
|
66
|
-
(value: string) => setTab(value),
|
|
67
|
-
[setTab],
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
useEffect(() => {
|
|
71
|
-
if (onUnauthenticated && !user && !isLoading) onUnauthenticated();
|
|
72
|
-
}, [onUnauthenticated, user, isLoading]);
|
|
73
|
-
|
|
74
|
-
if (isLoading) {
|
|
75
|
-
return <Preloader variant="fullscreen" text={labels.loading} size="lg" backdrop backdropOpacity={80} />;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!user) {
|
|
79
|
-
return (
|
|
80
|
-
<div className="flex items-center justify-center min-h-screen">
|
|
81
|
-
<div className="text-center">
|
|
82
|
-
<h1 className="text-2xl font-bold mb-4">{labels.notAuthenticated}</h1>
|
|
83
|
-
<p className="text-muted-foreground">{labels.pleaseLogIn}</p>
|
|
84
|
-
</div>
|
|
85
|
-
</div>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ── Prepare data before render ──
|
|
90
|
-
|
|
91
|
-
const extraTriggers = tabs.map((t) => (
|
|
92
|
-
<TabsTrigger key={t.value} value={t.value}>{t.label}</TabsTrigger>
|
|
93
|
-
));
|
|
94
|
-
const extraPanels = tabs.map((t) => (
|
|
95
|
-
<TabsContent key={t.value} value={t.value}>{t.content}</TabsContent>
|
|
96
|
-
));
|
|
97
|
-
const footer = slots?.footer ?? null;
|
|
98
|
-
|
|
99
|
-
// ── Render ──
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<div className="container mx-auto px-4 py-6 md:py-10 max-w-3xl">
|
|
103
|
-
<ProfileHeader slots={slots} enableDeleteAccount={enableDeleteAccount} />
|
|
104
|
-
|
|
105
|
-
<Tabs value={tab} onValueChange={handleTabChange} className="mt-2">
|
|
106
|
-
<TabsList variant="underline" scrollable>
|
|
107
|
-
<TabsTrigger value="profile">{labels.personalInfo}</TabsTrigger>
|
|
108
|
-
{enable2FA && <TabsTrigger value="security">{labels.security}</TabsTrigger>}
|
|
109
|
-
{enableAPIKeys && <TabsTrigger value="api-keys">{labels.apiKeys}</TabsTrigger>}
|
|
110
|
-
{extraTriggers}
|
|
111
|
-
</TabsList>
|
|
112
|
-
|
|
113
|
-
<TabsContent value="profile">
|
|
114
|
-
<ProfileTab />
|
|
115
|
-
</TabsContent>
|
|
116
|
-
|
|
117
|
-
{enable2FA && (
|
|
118
|
-
<TabsContent value="security">
|
|
119
|
-
<TabSecurity />
|
|
120
|
-
</TabsContent>
|
|
121
|
-
)}
|
|
122
|
-
|
|
123
|
-
{enableAPIKeys && (
|
|
124
|
-
<TabsContent value="api-keys">
|
|
125
|
-
<TabApiKeys />
|
|
126
|
-
</TabsContent>
|
|
127
|
-
)}
|
|
128
|
-
|
|
129
|
-
{extraPanels}
|
|
130
|
-
</Tabs>
|
|
131
|
-
|
|
132
|
-
{footer && <div className="mt-8">{footer}</div>}
|
|
133
|
-
</div>
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
138
|
-
// Export
|
|
139
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
140
|
-
|
|
141
|
-
export const ProfileForm: React.FC<ProfileFormProps> = ({ title, ...props }) => (
|
|
142
|
-
<ProfileProvider title={title}>
|
|
143
|
-
<ProfileContent title={title} {...props} />
|
|
144
|
-
</ProfileProvider>
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
export { useProfileContext } from './context';
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
# ProfileLayout
|
|
2
|
-
|
|
3
|
-
User profile page with tabbed interface: **Profile** | **Security** | **API Keys**.
|
|
4
|
-
|
|
5
|
-
## Usage
|
|
6
|
-
|
|
7
|
-
### Standalone page
|
|
8
|
-
|
|
9
|
-
```tsx
|
|
10
|
-
import { ProfileForm } from '@djangocfg/layouts';
|
|
11
|
-
|
|
12
|
-
<ProfileForm
|
|
13
|
-
enable2FA
|
|
14
|
-
enableAPIKeys
|
|
15
|
-
enableDeleteAccount
|
|
16
|
-
onUnauthenticated={() => router.push('/login')}
|
|
17
|
-
slots={{ headerBadge: <PlanBadge /> }}
|
|
18
|
-
tabs={[
|
|
19
|
-
{ value: 'billing', label: 'Billing', content: <BillingPanel /> },
|
|
20
|
-
]}
|
|
21
|
-
/>
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
### Dialog
|
|
25
|
-
|
|
26
|
-
```tsx
|
|
27
|
-
import { ProfileDialog, useProfileDialogStore } from '@djangocfg/layouts';
|
|
28
|
-
|
|
29
|
-
// Open on a specific tab
|
|
30
|
-
useProfileDialogStore.getState().open({ initialTab: 'security' });
|
|
31
|
-
// …or with custom tabs/slots — see "Opening with custom content" below.
|
|
32
|
-
|
|
33
|
-
// In your layout (lazy-loaded)
|
|
34
|
-
<ProfileDialog />
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Props
|
|
38
|
-
|
|
39
|
-
| Prop | Default | Description |
|
|
40
|
-
|------|---------|-------------|
|
|
41
|
-
| `enable2FA` | `false` | Show Security tab with 2FA management |
|
|
42
|
-
| `enableAPIKeys` | `true` | Show API Keys tab |
|
|
43
|
-
| `enableDeleteAccount` | `true` | Show Delete Account in header dropdown |
|
|
44
|
-
| `onUnauthenticated` | — | Callback when user is not authenticated |
|
|
45
|
-
| `tabs` | `[]` | Extra `ProfileTab[]` appended after built-in tabs |
|
|
46
|
-
| `slots` | — | Named slots: `headerMenuItems`, `headerBadge`, `headerAfter`, `footer` |
|
|
47
|
-
| `title` | i18n | Page title |
|
|
48
|
-
| `defaultTab` | — | Initial active tab. Accepts a built-in value or a custom tab's `value`. When provided (e.g. by `ProfileDialog`), tabs start at this value. |
|
|
49
|
-
|
|
50
|
-
## Global Profile Dialog
|
|
51
|
-
|
|
52
|
-
`ProfileDialog` is a Zustand-driven dialog that can be opened from anywhere:
|
|
53
|
-
|
|
54
|
-
```tsx
|
|
55
|
-
import { useProfileDialogStore } from '@djangocfg/layouts';
|
|
56
|
-
|
|
57
|
-
const { open, close } = useProfileDialogStore();
|
|
58
|
-
|
|
59
|
-
// Open on Profile tab
|
|
60
|
-
open();
|
|
61
|
-
|
|
62
|
-
// Open on Security tab
|
|
63
|
-
open({ initialTab: 'security' });
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### Opening with custom content
|
|
67
|
-
|
|
68
|
-
`open()` accepts the same content props as `ProfileForm` (all optional), so any
|
|
69
|
-
caller can open the dialog with custom tabs / slots / flags — and start on a
|
|
70
|
-
custom tab — in one line:
|
|
71
|
-
|
|
72
|
-
```tsx
|
|
73
|
-
useProfileDialogStore.getState().open({
|
|
74
|
-
initialTab: 'api-keys',
|
|
75
|
-
tabs: [{ value: 'api-keys', label: 'API keys', content: <MyApiKeysTab /> }],
|
|
76
|
-
enableAPIKeys: false, // hide the built-in API Keys tab
|
|
77
|
-
});
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
The `open()` payload is typed as `ProfileDialogContent`:
|
|
81
|
-
|
|
82
|
-
| Field | Type | Notes |
|
|
83
|
-
|-------|------|-------|
|
|
84
|
-
| `initialTab` | `string` | Built-in (`'profile' \| 'security' \| 'api-keys'`) **or** a custom tab's `value`. |
|
|
85
|
-
| `tabs` | `ProfileTab[]` | Extra tabs appended after the built-in ones. |
|
|
86
|
-
| `slots` | `ProfileSlots` | `headerMenuItems`, `headerBadge`, `headerAfter`, `footer`. |
|
|
87
|
-
| `enable2FA` | `boolean` | Override the Security tab. |
|
|
88
|
-
| `enableAPIKeys` | `boolean` | Override the built-in API Keys tab. |
|
|
89
|
-
| `enableDeleteAccount` | `boolean` | Override Delete Account in the header dropdown. |
|
|
90
|
-
| `title` | `string` | Dialog title (the `<ProfileDialog title>` prop wins if both set). |
|
|
91
|
-
|
|
92
|
-
Each `open()` **replaces** the previous content (it does not merge), and `close()`
|
|
93
|
-
clears it — so a bare `open()` always shows the default profile. Omitted fields
|
|
94
|
-
fall through to `ProfileForm`'s own defaults, keeping `open()` / `open({ initialTab })`
|
|
95
|
-
backward compatible.
|
|
96
|
-
|
|
97
|
-
Wire it into `PrivateLayout` for global access:
|
|
98
|
-
|
|
99
|
-
```tsx
|
|
100
|
-
import { PrivateLayout } from '@djangocfg/layouts';
|
|
101
|
-
|
|
102
|
-
<PrivateLayout sidebar={...} header={...}>
|
|
103
|
-
{children}
|
|
104
|
-
</PrivateLayout>
|
|
105
|
-
// ProfileDialog is rendered automatically inside PrivateLayout
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
## Sidebar Account Button Modes
|
|
109
|
-
|
|
110
|
-
The footer account button in `PrivateLayout` supports two modes via `HeaderConfig`:
|
|
111
|
-
|
|
112
|
-
```tsx
|
|
113
|
-
const header: HeaderConfig = {
|
|
114
|
-
// ...
|
|
115
|
-
accountAction: 'menu', // Default — opens DropdownMenu with links, theme, logout
|
|
116
|
-
accountAction: 'dialog', // Opens ProfileDialog instead
|
|
117
|
-
};
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
## Architecture
|
|
121
|
-
|
|
122
|
-
```
|
|
123
|
-
ProfileLayout/
|
|
124
|
-
├── ProfileForm/
|
|
125
|
-
│ ├── index.tsx Shell: ProfileProvider → header + Tabs
|
|
126
|
-
│ └── context.tsx Root context (labels, onLogout, onFieldSave)
|
|
127
|
-
├── ProfileDialog/
|
|
128
|
-
│ ├── ProfileDialog.tsx Dialog wrapper around ProfileForm
|
|
129
|
-
│ └── store.ts Zustand store (isOpen + ProfileDialogContent: tabs/slots/flags/initialTab/title)
|
|
130
|
-
├── hooks/
|
|
131
|
-
│ └── useProfileTabs.ts Local tab state (useState)
|
|
132
|
-
├── types.ts ProfileFormProps, ProfileTab, ProfileSlots
|
|
133
|
-
└── components/
|
|
134
|
-
├── ProfileHeader Avatar + name + dropdown menu
|
|
135
|
-
├── ProfileTab Editable fields grid (first_name, last_name, phone, company, position)
|
|
136
|
-
├── TwoFactorSection/ Own mini-context (2FA status, setup, disable)
|
|
137
|
-
├── ApiKeySection/ Own mini-context (useApiKey, reveal/arm state)
|
|
138
|
-
├── EditableField Inline-editable text/phone field
|
|
139
|
-
├── Section Card-like section wrapper
|
|
140
|
-
└── DeleteAccount Confirmation dialog + delete action
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Each section (`TwoFactorSection`, `ApiKeySection`) owns a **mini-context** — isolated state, labels, and API logic. Root context stays minimal.
|
|
144
|
-
|
|
145
|
-
## Dependencies
|
|
146
|
-
|
|
147
|
-
- `@djangocfg/api/hooks` — `useApiKey` (retrieve + regenerate)
|
|
148
|
-
- `@djangocfg/api/auth` — `useAuth`, `useDeleteAccount`, `useTwoFactorSetup`, `useTwoFactorStatus`
|
|
149
|
-
- `@djangocfg/ui-core/components` — UI primitives + `CopyButton`
|
|
150
|
-
- `@djangocfg/i18n` — `useAppT`
|
|
@@ -1,38 +0,0 @@
|
|
|
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
|
-
);
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Check, FlaskConical, KeyRound, Loader2, RefreshCw } from 'lucide-react';
|
|
4
|
-
import moment from 'moment';
|
|
5
|
-
import React from 'react';
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
Badge,
|
|
9
|
-
Button,
|
|
10
|
-
Card,
|
|
11
|
-
CardContent,
|
|
12
|
-
CardDescription,
|
|
13
|
-
CardHeader,
|
|
14
|
-
CardTitle,
|
|
15
|
-
CopyButton,
|
|
16
|
-
} from '@djangocfg/ui-core/components';
|
|
17
|
-
import { cn } from '@djangocfg/ui-core/lib';
|
|
18
|
-
|
|
19
|
-
import { ApiKeyProvider, useApiKeyContext } from './context';
|
|
20
|
-
|
|
21
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
-
// Helpers
|
|
23
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
function formatDate(iso: string | null, fallback: string): string {
|
|
26
|
-
if (!iso) return fallback;
|
|
27
|
-
return moment.utc(iso).local().format('MMM D, YYYY');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Returns true if the backend already masked the key (contains •). */
|
|
31
|
-
function isMasked(key: string): boolean {
|
|
32
|
-
return key.includes('•');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
|
-
// Inner component (uses context)
|
|
37
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
function ApiKeyCard() {
|
|
40
|
-
const {
|
|
41
|
-
labels,
|
|
42
|
-
apiKey,
|
|
43
|
-
reissuedAt,
|
|
44
|
-
createdAt,
|
|
45
|
-
isLoading,
|
|
46
|
-
error,
|
|
47
|
-
isArmed,
|
|
48
|
-
arm,
|
|
49
|
-
disarm,
|
|
50
|
-
regenerate,
|
|
51
|
-
isRegenerating,
|
|
52
|
-
isFresh,
|
|
53
|
-
dismissFresh,
|
|
54
|
-
testKey,
|
|
55
|
-
isTesting,
|
|
56
|
-
testResult,
|
|
57
|
-
} = useApiKeyContext();
|
|
58
|
-
|
|
59
|
-
if (isLoading) {
|
|
60
|
-
return (
|
|
61
|
-
<Card>
|
|
62
|
-
<CardContent className="flex items-center justify-center py-10">
|
|
63
|
-
<Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
|
|
64
|
-
</CardContent>
|
|
65
|
-
</Card>
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const masked = apiKey ? isMasked(apiKey) : false;
|
|
70
|
-
const displayKey = apiKey ?? '—';
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<Card>
|
|
74
|
-
<CardHeader>
|
|
75
|
-
<div className="flex items-center gap-3">
|
|
76
|
-
<div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
|
77
|
-
<KeyRound className="w-5 h-5 text-primary" />
|
|
78
|
-
</div>
|
|
79
|
-
<div className="flex-1 min-w-0">
|
|
80
|
-
<div className="flex items-center gap-2">
|
|
81
|
-
<CardTitle className="text-base">{labels.title}</CardTitle>
|
|
82
|
-
{apiKey && <Badge variant="secondary" className="text-xs">Active</Badge>}
|
|
83
|
-
</div>
|
|
84
|
-
<CardDescription className="mt-0.5">{labels.description}</CardDescription>
|
|
85
|
-
</div>
|
|
86
|
-
</div>
|
|
87
|
-
</CardHeader>
|
|
88
|
-
|
|
89
|
-
{apiKey && (
|
|
90
|
-
<CardContent className="pt-0 space-y-3">
|
|
91
|
-
{/* Fresh-key banner */}
|
|
92
|
-
{isFresh && (
|
|
93
|
-
<div className="flex items-center gap-2 px-3 py-2 rounded-md bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-900 text-amber-800 dark:text-amber-200 text-sm">
|
|
94
|
-
<KeyRound className="w-4 h-4 flex-shrink-0" />
|
|
95
|
-
<span className="flex-1">This is your new API key — copy it now. It will be masked when you leave this page.</span>
|
|
96
|
-
<Button variant="ghost" size="sm" className="h-7 px-2" onClick={dismissFresh}>
|
|
97
|
-
<Check className="w-4 h-4 mr-1" />
|
|
98
|
-
{labels.done}
|
|
99
|
-
</Button>
|
|
100
|
-
</div>
|
|
101
|
-
)}
|
|
102
|
-
|
|
103
|
-
{/* Test result banner */}
|
|
104
|
-
{testResult !== null && (
|
|
105
|
-
<div className={cn(
|
|
106
|
-
'flex items-center gap-2 px-3 py-2 rounded-md text-sm',
|
|
107
|
-
testResult
|
|
108
|
-
? 'bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-900 text-green-800 dark:text-green-200'
|
|
109
|
-
: 'bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 text-red-800 dark:text-red-200',
|
|
110
|
-
)}>
|
|
111
|
-
<FlaskConical className="w-4 h-4 flex-shrink-0" />
|
|
112
|
-
<span>{testResult ? labels.testSuccess : labels.testFailed}</span>
|
|
113
|
-
</div>
|
|
114
|
-
)}
|
|
115
|
-
|
|
116
|
-
<div className="flex items-center gap-2">
|
|
117
|
-
<div className="flex-1 min-w-0 px-3 py-2.5 rounded-md bg-muted font-mono text-sm select-all">
|
|
118
|
-
<span className={cn(masked && 'tracking-widest')}>
|
|
119
|
-
{displayKey}
|
|
120
|
-
</span>
|
|
121
|
-
</div>
|
|
122
|
-
|
|
123
|
-
{/* Copy only when the key is fresh (full key after regenerate) */}
|
|
124
|
-
{isFresh && <CopyButton value={apiKey} />}
|
|
125
|
-
</div>
|
|
126
|
-
|
|
127
|
-
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
128
|
-
{createdAt && <span>{labels.created}: {formatDate(createdAt, '—')}</span>}
|
|
129
|
-
<span>{labels.reissued}: {formatDate(reissuedAt, labels.neverReissued)}</span>
|
|
130
|
-
</div>
|
|
131
|
-
</CardContent>
|
|
132
|
-
)}
|
|
133
|
-
|
|
134
|
-
{error && (
|
|
135
|
-
<CardContent className="pt-0">
|
|
136
|
-
<p className="text-sm text-destructive">{error}</p>
|
|
137
|
-
</CardContent>
|
|
138
|
-
)}
|
|
139
|
-
|
|
140
|
-
<CardContent className="pt-0">
|
|
141
|
-
{isArmed ? (
|
|
142
|
-
<div className="flex items-center gap-2">
|
|
143
|
-
<Button
|
|
144
|
-
variant="destructive"
|
|
145
|
-
size="sm"
|
|
146
|
-
onClick={regenerate}
|
|
147
|
-
disabled={isRegenerating}
|
|
148
|
-
>
|
|
149
|
-
{isRegenerating
|
|
150
|
-
? <><Loader2 className="mr-2 h-4 w-4 animate-spin" />{labels.regenerating}</>
|
|
151
|
-
: labels.confirmRegenerate}
|
|
152
|
-
</Button>
|
|
153
|
-
<Button variant="ghost" size="sm" onClick={disarm} disabled={isRegenerating}>
|
|
154
|
-
Cancel
|
|
155
|
-
</Button>
|
|
156
|
-
</div>
|
|
157
|
-
) : (
|
|
158
|
-
<div className="flex items-center gap-2">
|
|
159
|
-
<Button
|
|
160
|
-
variant="outline"
|
|
161
|
-
size="sm"
|
|
162
|
-
onClick={arm}
|
|
163
|
-
disabled={!apiKey || isRegenerating}
|
|
164
|
-
>
|
|
165
|
-
<RefreshCw className="mr-2 h-4 w-4" />
|
|
166
|
-
{labels.regenerate}
|
|
167
|
-
</Button>
|
|
168
|
-
|
|
169
|
-
{/* Test button only when we have a fresh (full) key */}
|
|
170
|
-
{isFresh && (
|
|
171
|
-
<Button
|
|
172
|
-
variant="secondary"
|
|
173
|
-
size="sm"
|
|
174
|
-
onClick={testKey}
|
|
175
|
-
disabled={isTesting}
|
|
176
|
-
>
|
|
177
|
-
{isTesting
|
|
178
|
-
? <><Loader2 className="mr-2 h-4 w-4 animate-spin" />{labels.testing}</>
|
|
179
|
-
: <><FlaskConical className="mr-2 h-4 w-4" />{labels.test}</>}
|
|
180
|
-
</Button>
|
|
181
|
-
)}
|
|
182
|
-
</div>
|
|
183
|
-
)}
|
|
184
|
-
</CardContent>
|
|
185
|
-
</Card>
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
190
|
-
// Export (wraps with provider)
|
|
191
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
192
|
-
|
|
193
|
-
export const ApiKeySection: React.FC = () => (
|
|
194
|
-
<ApiKeyProvider>
|
|
195
|
-
<ApiKeyCard />
|
|
196
|
-
</ApiKeyProvider>
|
|
197
|
-
);
|