@djangocfg/layouts 2.1.426 → 2.1.427
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -17
- package/src/layouts/AppLayout/AppLayout.tsx +0 -7
- package/src/layouts/AppLayout/BaseApp.tsx +29 -52
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +6 -4
- package/src/layouts/PrivateLayout/PrivateLayout.tsx +7 -3
- package/src/layouts/PrivateLayout/components/PrivateContent.tsx +5 -1
- package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +105 -70
- package/src/layouts/PrivateLayout/types.ts +8 -0
- 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 +189 -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 -1
- package/src/layouts/types/index.ts +0 -1
- package/src/layouts/types/layout.types.ts +0 -4
- 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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SettingsDialog store.
|
|
5
|
+
*
|
|
6
|
+
* Holds only transient dialog state: whether it's open and which section is
|
|
7
|
+
* active. Section *definitions* are NOT stored here — they're passed as props
|
|
8
|
+
* to `<SettingsDialog />` so they can include live JSX/handlers from the host
|
|
9
|
+
* app. Consumers open the dialog imperatively via `useSettingsDialog()` or
|
|
10
|
+
* declaratively via the URL hash (see `useSettingsUrl`).
|
|
11
|
+
*
|
|
12
|
+
* Mirrors the established `useProfileDialogStore` pattern so both dialogs feel
|
|
13
|
+
* identical to wire up. ProfileLayout's store stays separate and untouched.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { create } from 'zustand';
|
|
17
|
+
|
|
18
|
+
interface SettingsDialogState {
|
|
19
|
+
/** Whether the dialog is visible. */
|
|
20
|
+
isOpen: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Active section id, or null to mean "fall back to the first section".
|
|
23
|
+
* Kept separate from isOpen so a deep-link can preset the section before open.
|
|
24
|
+
*/
|
|
25
|
+
activeSection: string | null;
|
|
26
|
+
/** Open the dialog, optionally on a specific section. */
|
|
27
|
+
open: (sectionId?: string) => void;
|
|
28
|
+
/** Close the dialog. Leaves activeSection as-is (cheap to reopen same spot). */
|
|
29
|
+
close: () => void;
|
|
30
|
+
/** Toggle visibility. */
|
|
31
|
+
toggle: () => void;
|
|
32
|
+
/** Switch the active section (no-op on open state). */
|
|
33
|
+
setSection: (sectionId: string) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const useSettingsDialogStore = create<SettingsDialogState>((set) => ({
|
|
37
|
+
isOpen: false,
|
|
38
|
+
activeSection: null,
|
|
39
|
+
open: (sectionId) =>
|
|
40
|
+
set((state) => ({
|
|
41
|
+
isOpen: true,
|
|
42
|
+
activeSection: sectionId ?? state.activeSection,
|
|
43
|
+
})),
|
|
44
|
+
close: () => set({ isOpen: false }),
|
|
45
|
+
toggle: () => set((state) => ({ isOpen: !state.isOpen })),
|
|
46
|
+
setSection: (sectionId) => set({ activeSection: sectionId }),
|
|
47
|
+
}));
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SettingsLayout — public types.
|
|
3
|
+
*
|
|
4
|
+
* A settings surface modeled on the Claude.ai modal: a left section-nav rail
|
|
5
|
+
* (grouped, searchable) and a right scrollable detail panel, rendered as a
|
|
6
|
+
* dialog and/or inline page. Sections are plain config objects so apps extend
|
|
7
|
+
* the surface by appending to an array — no JSX registry, no context plumbing.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type * as React from 'react';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A logical grouping of sections in the nav rail. Groups render as a labelled
|
|
14
|
+
* cluster (e.g. "Account", "Workspace"). Unknown / omitted groups fall into the
|
|
15
|
+
* implicit default group, which renders first with no header.
|
|
16
|
+
*/
|
|
17
|
+
export interface SettingsGroup {
|
|
18
|
+
/** Stable key, referenced by `SettingsSection.group`. */
|
|
19
|
+
id: string;
|
|
20
|
+
/** Header shown above the group's items. Omit for an unlabelled cluster. */
|
|
21
|
+
label?: React.ReactNode;
|
|
22
|
+
/** Sort order among groups (ascending). Defaults to declaration order. */
|
|
23
|
+
order?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* One settings section = one nav entry + one detail panel.
|
|
28
|
+
*
|
|
29
|
+
* Mirrors the old `ProfileTab` shape (`{ value, label, content }`) so migrating
|
|
30
|
+
* a ProfileLayout tab into a SettingsLayout section is a near-rename.
|
|
31
|
+
*/
|
|
32
|
+
export interface SettingsSection {
|
|
33
|
+
/** Unique id. Used as the active key and the URL hash segment (`#settings/<id>`). */
|
|
34
|
+
id: string;
|
|
35
|
+
/** Nav label. */
|
|
36
|
+
label: React.ReactNode;
|
|
37
|
+
/** Optional icon (lucide component or any SVG-ish element type). */
|
|
38
|
+
icon?: React.ComponentType<{ className?: string }>;
|
|
39
|
+
/** Group id this section belongs to. Falls into the default group when omitted. */
|
|
40
|
+
group?: string;
|
|
41
|
+
/** Heading shown at the top of the detail panel. Defaults to `label`. */
|
|
42
|
+
title?: React.ReactNode;
|
|
43
|
+
/** Sub-heading under the panel title. */
|
|
44
|
+
description?: React.ReactNode;
|
|
45
|
+
/** The panel body. Rendered only while this section is active (Radix unmounts closed dialogs). */
|
|
46
|
+
content: React.ReactNode;
|
|
47
|
+
/** Extra terms the search box matches against, beyond the label. */
|
|
48
|
+
keywords?: string[];
|
|
49
|
+
/** Sort order within the group (ascending). Defaults to declaration order. */
|
|
50
|
+
order?: number;
|
|
51
|
+
/** Optional trailing accessory in the nav row (badge, chevron, count). */
|
|
52
|
+
badge?: React.ReactNode;
|
|
53
|
+
/** Hide from the rail without removing (e.g. gated by plan). Still reachable by id. */
|
|
54
|
+
hidden?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Toggle flags for the built-in sections shipped by SettingsLayout. */
|
|
58
|
+
export interface SettingsBuiltins {
|
|
59
|
+
/** Account / profile fields + preferences. Default: true. */
|
|
60
|
+
account?: boolean;
|
|
61
|
+
/** Two-factor / security. Default: false (opt-in, needs API support). */
|
|
62
|
+
security?: boolean;
|
|
63
|
+
/** API keys. Default: true. */
|
|
64
|
+
apiKeys?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Shared configuration consumed by both the dialog and the inline form. */
|
|
68
|
+
export interface SettingsContent {
|
|
69
|
+
/** Dialog/page title. Default: "Settings". */
|
|
70
|
+
title?: React.ReactNode;
|
|
71
|
+
/** App-provided sections, merged after the enabled built-ins. */
|
|
72
|
+
sections?: SettingsSection[];
|
|
73
|
+
/** Group definitions for ordering/labelling. Optional — sections can name ad-hoc groups. */
|
|
74
|
+
groups?: SettingsGroup[];
|
|
75
|
+
/** Which built-in sections to include. */
|
|
76
|
+
builtins?: SettingsBuiltins;
|
|
77
|
+
/** Section id to open first. Defaults to the first visible section. */
|
|
78
|
+
initialSection?: string;
|
|
79
|
+
/** Show the search box above the nav rail. Default: true. */
|
|
80
|
+
searchable?: boolean;
|
|
81
|
+
/** Allow account deletion in the account section. Default: true. */
|
|
82
|
+
enableDeleteAccount?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Props for the inline (non-dialog) `<SettingsForm />`. */
|
|
86
|
+
export interface SettingsFormProps extends SettingsContent {
|
|
87
|
+
/** Controlled active section. When set, the form does not own selection state. */
|
|
88
|
+
activeSection?: string;
|
|
89
|
+
/** Called when the user picks a section (controlled mode). */
|
|
90
|
+
onSectionChange?: (id: string) => void;
|
|
91
|
+
/** Fired when an unauthenticated user reaches the surface. */
|
|
92
|
+
onUnauthenticated?: () => void;
|
|
93
|
+
/** Drop the outer card chrome (border/rounding) — used when embedded in a dialog. */
|
|
94
|
+
bare?: boolean;
|
|
95
|
+
className?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Props for the globally-mounted `<SettingsDialog />`. */
|
|
99
|
+
export interface SettingsDialogProps extends SettingsContent {
|
|
100
|
+
/**
|
|
101
|
+
* Sync open/close + active section with the URL hash (`#settings/<id>`).
|
|
102
|
+
* Lets users deep-link and the back button close. Default: true.
|
|
103
|
+
*/
|
|
104
|
+
syncUrl?: boolean;
|
|
105
|
+
/** Hash namespace; the segment before the section id. Default: "settings". */
|
|
106
|
+
urlKey?: string;
|
|
107
|
+
}
|
package/src/layouts/index.ts
CHANGED
|
@@ -22,7 +22,6 @@ export type {
|
|
|
22
22
|
|
|
23
23
|
// External provider configs (re-export for convenience)
|
|
24
24
|
export type { AnalyticsConfig } from '../../snippets/Analytics/types';
|
|
25
|
-
export type { PwaInstallConfig } from '@djangocfg/ui-nextjs/pwa';
|
|
26
25
|
export type {
|
|
27
26
|
ErrorBoundaryConfig,
|
|
28
27
|
ErrorTrackingConfig,
|
|
@@ -9,7 +9,6 @@ import type { AuthConfig } from '@djangocfg/api/auth';
|
|
|
9
9
|
|
|
10
10
|
// Import provider configs from their modules
|
|
11
11
|
import type { AnalyticsConfig } from '../../snippets/Analytics/types';
|
|
12
|
-
import type { PwaInstallConfig } from '@djangocfg/ui-nextjs/pwa';
|
|
13
12
|
import type { ErrorBoundaryConfig, ErrorTrackingConfig } from '../../components/errors/types';
|
|
14
13
|
|
|
15
14
|
/**
|
|
@@ -62,9 +61,6 @@ export interface BaseLayoutProps {
|
|
|
62
61
|
/** Error boundary configuration (from components/errors, enabled by default) */
|
|
63
62
|
errorBoundary?: ErrorBoundaryConfig;
|
|
64
63
|
|
|
65
|
-
/** PWA Install configuration (from snippets/PWAInstall) */
|
|
66
|
-
pwaInstall?: PwaInstallConfig;
|
|
67
|
-
|
|
68
64
|
/** Monitor configuration — initialises window.monitor + auto-captures JS errors & console */
|
|
69
65
|
monitor?: MonitorConfig;
|
|
70
66
|
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import React from 'react';
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
Dialog,
|
|
7
|
-
DialogContent,
|
|
8
|
-
DialogHeader,
|
|
9
|
-
DialogTitle,
|
|
10
|
-
} from '@djangocfg/ui-core/components';
|
|
11
|
-
|
|
12
|
-
import { ProfileForm } from '../ProfileForm';
|
|
13
|
-
import { useProfileDialogStore } from './store';
|
|
14
|
-
|
|
15
|
-
export interface ProfileDialogProps {
|
|
16
|
-
title?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export const ProfileDialog: React.FC<ProfileDialogProps> = ({ title }) => {
|
|
20
|
-
const {
|
|
21
|
-
isOpen,
|
|
22
|
-
close,
|
|
23
|
-
initialTab,
|
|
24
|
-
tabs,
|
|
25
|
-
slots,
|
|
26
|
-
enable2FA,
|
|
27
|
-
enableAPIKeys,
|
|
28
|
-
enableDeleteAccount,
|
|
29
|
-
title: storeTitle,
|
|
30
|
-
} = useProfileDialogStore();
|
|
31
|
-
|
|
32
|
-
// Title precedence: explicit dialog prop > value passed to open() > ProfileForm default.
|
|
33
|
-
const resolvedTitle = title ?? storeTitle;
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<Dialog open={isOpen} onOpenChange={(open) => !open && close()}>
|
|
37
|
-
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto p-0">
|
|
38
|
-
<DialogHeader className="sr-only">
|
|
39
|
-
<DialogTitle>Profile</DialogTitle>
|
|
40
|
-
</DialogHeader>
|
|
41
|
-
{/* Undefined fields fall through to ProfileForm's own defaults
|
|
42
|
-
* (e.g. enableAPIKeys defaults to true), so this stays backward
|
|
43
|
-
* compatible with bare open() / open({ initialTab }). */}
|
|
44
|
-
<ProfileForm
|
|
45
|
-
title={resolvedTitle}
|
|
46
|
-
defaultTab={initialTab}
|
|
47
|
-
tabs={tabs}
|
|
48
|
-
slots={slots}
|
|
49
|
-
enable2FA={enable2FA}
|
|
50
|
-
enableAPIKeys={enableAPIKeys}
|
|
51
|
-
enableDeleteAccount={enableDeleteAccount}
|
|
52
|
-
/>
|
|
53
|
-
</DialogContent>
|
|
54
|
-
</Dialog>
|
|
55
|
-
);
|
|
56
|
-
};
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { create } from 'zustand';
|
|
2
|
-
|
|
3
|
-
import type { ProfileTabValueOrCustom } from '../hooks/useProfileTabs';
|
|
4
|
-
import type { ProfileSlots, ProfileTab } from '../types';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Content the dialog renders into <ProfileForm>. All optional — anything
|
|
8
|
-
* omitted in `open()` falls back to ProfileForm's own default. Lets any caller
|
|
9
|
-
* open the profile dialog with custom tabs/slots/flags in one line.
|
|
10
|
-
*/
|
|
11
|
-
export interface ProfileDialogContent {
|
|
12
|
-
initialTab?: ProfileTabValueOrCustom;
|
|
13
|
-
tabs?: ProfileTab[];
|
|
14
|
-
slots?: ProfileSlots;
|
|
15
|
-
enable2FA?: boolean;
|
|
16
|
-
enableAPIKeys?: boolean;
|
|
17
|
-
enableDeleteAccount?: boolean;
|
|
18
|
-
title?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface ProfileDialogState extends ProfileDialogContent {
|
|
22
|
-
isOpen: boolean;
|
|
23
|
-
open: (options?: ProfileDialogContent) => void;
|
|
24
|
-
close: () => void;
|
|
25
|
-
toggle: () => void;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** Content fields reset to undefined on close (keeps `isOpen` separate). */
|
|
29
|
-
const EMPTY_CONTENT: Required<{ [K in keyof ProfileDialogContent]: undefined }> = {
|
|
30
|
-
initialTab: undefined,
|
|
31
|
-
tabs: undefined,
|
|
32
|
-
slots: undefined,
|
|
33
|
-
enable2FA: undefined,
|
|
34
|
-
enableAPIKeys: undefined,
|
|
35
|
-
enableDeleteAccount: undefined,
|
|
36
|
-
title: undefined,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export const useProfileDialogStore = create<ProfileDialogState>((set) => ({
|
|
40
|
-
isOpen: false,
|
|
41
|
-
...EMPTY_CONTENT,
|
|
42
|
-
open: (options) =>
|
|
43
|
-
set({
|
|
44
|
-
isOpen: true,
|
|
45
|
-
// Replace (not merge) content so a previous open()'s tabs/flags don't leak.
|
|
46
|
-
...EMPTY_CONTENT,
|
|
47
|
-
...options,
|
|
48
|
-
}),
|
|
49
|
-
close: () => set({ isOpen: false, ...EMPTY_CONTENT }),
|
|
50
|
-
toggle: () => set((state) => ({ isOpen: !state.isOpen })),
|
|
51
|
-
}));
|
|
@@ -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';
|