@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.
Files changed (66) hide show
  1. package/package.json +15 -17
  2. package/src/layouts/AppLayout/AppLayout.tsx +0 -7
  3. package/src/layouts/AppLayout/BaseApp.tsx +29 -52
  4. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +6 -4
  5. package/src/layouts/PrivateLayout/PrivateLayout.tsx +7 -3
  6. package/src/layouts/PrivateLayout/components/PrivateContent.tsx +5 -1
  7. package/src/layouts/PrivateLayout/components/PrivateSidebarAccount.tsx +105 -70
  8. package/src/layouts/PrivateLayout/types.ts +8 -0
  9. package/src/layouts/PublicLayout/components/UserMenu.tsx +68 -113
  10. package/src/layouts/PublicLayout/navbars/MinimalNavbar/MinimalNavbar.tsx +0 -6
  11. package/src/layouts/PublicLayout/navbars/MinimalNavbar/index.ts +1 -1
  12. package/src/layouts/SettingsLayout/README.md +258 -0
  13. package/src/layouts/SettingsLayout/SettingsDialog.tsx +101 -0
  14. package/src/layouts/SettingsLayout/SettingsForm.tsx +100 -0
  15. package/src/layouts/SettingsLayout/components/ApiKeySection/ApiKeySection.tsx +189 -0
  16. package/src/layouts/SettingsLayout/components/SettingsNav.tsx +71 -0
  17. package/src/layouts/SettingsLayout/components/SettingsNavItem.tsx +57 -0
  18. package/src/layouts/SettingsLayout/components/SettingsPanel.tsx +48 -0
  19. package/src/layouts/SettingsLayout/components/SettingsSearch.tsx +50 -0
  20. package/src/layouts/SettingsLayout/components/SettingsShell.tsx +77 -0
  21. package/src/layouts/SettingsLayout/components/SettingsTabs.tsx +56 -0
  22. package/src/layouts/{ProfileLayout → SettingsLayout}/components/TwoFactorSection/TwoFactorSection.tsx +84 -130
  23. package/src/layouts/SettingsLayout/components/index.ts +6 -0
  24. package/src/layouts/SettingsLayout/context/SettingsContext.tsx +122 -0
  25. package/src/layouts/SettingsLayout/context/index.ts +2 -0
  26. package/src/layouts/SettingsLayout/hooks/index.ts +12 -0
  27. package/src/layouts/SettingsLayout/hooks/useProfileSave.ts +95 -0
  28. package/src/layouts/SettingsLayout/hooks/useSettingsDialog.ts +52 -0
  29. package/src/layouts/SettingsLayout/hooks/useSettingsSections.ts +123 -0
  30. package/src/layouts/SettingsLayout/hooks/useSettingsUrl.ts +140 -0
  31. package/src/layouts/SettingsLayout/index.ts +67 -0
  32. package/src/layouts/SettingsLayout/sections/AccountSection.tsx +100 -0
  33. package/src/layouts/SettingsLayout/sections/ApiKeysSection.tsx +15 -0
  34. package/src/layouts/SettingsLayout/sections/DeleteAccountRow.tsx +57 -0
  35. package/src/layouts/SettingsLayout/sections/PreferencesRows.tsx +43 -0
  36. package/src/layouts/SettingsLayout/sections/SecuritySection.tsx +15 -0
  37. package/src/layouts/SettingsLayout/sections/builtins.tsx +77 -0
  38. package/src/layouts/SettingsLayout/sections/index.ts +8 -0
  39. package/src/layouts/SettingsLayout/store.ts +47 -0
  40. package/src/layouts/SettingsLayout/types.ts +107 -0
  41. package/src/layouts/index.ts +1 -1
  42. package/src/layouts/types/index.ts +0 -1
  43. package/src/layouts/types/layout.types.ts +0 -4
  44. package/src/layouts/ProfileLayout/ProfileDialog/ProfileDialog.tsx +0 -56
  45. package/src/layouts/ProfileLayout/ProfileDialog/index.ts +0 -4
  46. package/src/layouts/ProfileLayout/ProfileDialog/store.ts +0 -51
  47. package/src/layouts/ProfileLayout/ProfileForm/context.tsx +0 -123
  48. package/src/layouts/ProfileLayout/ProfileForm/index.tsx +0 -147
  49. package/src/layouts/ProfileLayout/README.md +0 -150
  50. package/src/layouts/ProfileLayout/components/ActionButton.tsx +0 -38
  51. package/src/layouts/ProfileLayout/components/ApiKeySection/ApiKeySection.tsx +0 -197
  52. package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +0 -44
  53. package/src/layouts/ProfileLayout/components/EditableField.tsx +0 -128
  54. package/src/layouts/ProfileLayout/components/PreferencesSection.tsx +0 -56
  55. package/src/layouts/ProfileLayout/components/ProfileHeader.tsx +0 -110
  56. package/src/layouts/ProfileLayout/components/ProfileTab.tsx +0 -35
  57. package/src/layouts/ProfileLayout/components/Section.tsx +0 -22
  58. package/src/layouts/ProfileLayout/components/index.ts +0 -11
  59. package/src/layouts/ProfileLayout/hooks/index.ts +0 -2
  60. package/src/layouts/ProfileLayout/hooks/useProfileTabs.ts +0 -56
  61. package/src/layouts/ProfileLayout/index.ts +0 -8
  62. package/src/layouts/ProfileLayout/types.ts +0 -48
  63. /package/src/layouts/{ProfileLayout → SettingsLayout}/components/ApiKeySection/context.tsx +0 -0
  64. /package/src/layouts/{ProfileLayout → SettingsLayout}/components/ApiKeySection/index.ts +0 -0
  65. /package/src/layouts/{ProfileLayout → SettingsLayout}/components/AvatarSection.tsx +0 -0
  66. /package/src/layouts/{ProfileLayout → SettingsLayout}/components/TwoFactorSection/index.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { Loader2, Shield, ShieldCheck, ShieldOff, Smartphone, Trash2 } from 'lucide-react';
3
+ import { Loader2, Shield, ShieldOff } from 'lucide-react';
4
4
  import React, { useEffect, useState } from 'react';
5
5
 
6
6
  import { useTwoFactorSetup, useTwoFactorStatus } from '@djangocfg/api/auth';
@@ -9,11 +9,6 @@ import {
9
9
  AlertDescription,
10
10
  Badge,
11
11
  Button,
12
- Card,
13
- CardContent,
14
- CardDescription,
15
- CardHeader,
16
- CardTitle,
17
12
  Dialog,
18
13
  DialogContent,
19
14
  DialogDescription,
@@ -21,7 +16,9 @@ import {
21
16
  DialogHeader,
22
17
  DialogTitle,
23
18
  OTPInput,
24
- Separator,
19
+ Preloader,
20
+ SettingRow,
21
+ SettingsBlock,
25
22
  } from '@djangocfg/ui-core/components';
26
23
  import { cn } from '@djangocfg/ui-core/lib';
27
24
 
@@ -43,7 +40,7 @@ function StatusBadge({ enabled }: { enabled: boolean }) {
43
40
  variant={enabled ? 'default' : 'secondary'}
44
41
  className={cn(
45
42
  'text-xs font-medium',
46
- enabled && 'bg-green-500/15 text-green-600 dark:text-green-400 border-green-500/20',
43
+ enabled && 'bg-success-background text-success-foreground border-success-border',
47
44
  )}
48
45
  >
49
46
  {enabled ? 'Enabled' : 'Disabled'}
@@ -131,28 +128,12 @@ function DisableDialog({
131
128
  }
132
129
 
133
130
  // ─────────────────────────────────────────────────────────────────────────────
134
- // Device list row
131
+ // Helpers
135
132
  // ─────────────────────────────────────────────────────────────────────────────
136
133
 
137
- function DeviceRow({ name, createdAt, isPrimary }: {
138
- name: string;
139
- createdAt: string;
140
- isPrimary: boolean;
141
- }) {
142
- return (
143
- <div className="flex items-center gap-3 py-3">
144
- <div className="flex-shrink-0 w-8 h-8 rounded-full bg-muted flex items-center justify-center">
145
- <Smartphone className="w-4 h-4 text-muted-foreground" />
146
- </div>
147
- <div className="flex-1 min-w-0">
148
- <p className="text-sm font-medium truncate">{name}</p>
149
- <p className="text-xs text-muted-foreground">
150
- Added {new Date(createdAt).toLocaleDateString()}
151
- {isPrimary && ' · Primary'}
152
- </p>
153
- </div>
154
- </div>
155
- );
134
+ function deviceDescription(createdAt: string, isPrimary: boolean): string {
135
+ const added = `Added ${new Date(createdAt).toLocaleDateString()}`;
136
+ return isPrimary ? `${added} · Primary` : added;
156
137
  }
157
138
 
158
139
  // ─────────────────────────────────────────────────────────────────────────────
@@ -198,133 +179,106 @@ export const TwoFactorSection: React.FC = () => {
198
179
  };
199
180
 
200
181
  // ── Setup flow ──────────────────────────────────────────────────────────────
182
+ // Lighter, card-free container. The standalone setup flow brings its own
183
+ // visible (Preloader-based) loading state.
201
184
  if (view === 'setup') {
202
185
  return (
203
- <Card>
204
- <CardHeader>
205
- <div className="flex items-center justify-between">
206
- <CardTitle className="flex items-center gap-2 text-base">
207
- <Shield className="w-4 h-4" />
208
- Set up Two-Factor Authentication
209
- </CardTitle>
210
- <Button variant="ghost" size="sm" onClick={() => setView('status')}>
211
- Cancel
212
- </Button>
213
- </div>
214
- <CardDescription>
186
+ <SettingsBlock title="Set up Two-Factor Authentication">
187
+ <div className="flex items-center justify-between pb-2">
188
+ <p className="text-[13px] leading-relaxed text-muted-foreground">
215
189
  Scan the QR code with your authenticator app (Google Authenticator, Authy, etc.)
216
- </CardDescription>
217
- </CardHeader>
218
- <CardContent className="flex justify-center">
190
+ </p>
191
+ <Button variant="ghost" size="sm" onClick={() => setView('status')}>
192
+ Cancel
193
+ </Button>
194
+ </div>
195
+ <div className="flex justify-center py-2">
219
196
  <SetupStepStandalone
220
197
  onComplete={handleSetupDone}
221
198
  onSkip={() => setView('status')}
222
199
  />
223
- </CardContent>
224
- </Card>
200
+ </div>
201
+ </SettingsBlock>
225
202
  );
226
203
  }
227
204
 
228
205
  // ── Loading skeleton ────────────────────────────────────────────────────────
229
206
  if (isLoading && has2FAEnabled === null) {
230
- return (
231
- <Card>
232
- <CardContent className="flex items-center justify-center py-10">
233
- <Loader2 className="w-5 h-5 animate-spin text-muted-foreground" />
234
- </CardContent>
235
- </Card>
236
- );
207
+ return <Preloader variant="inline" className="py-10" />;
237
208
  }
238
209
 
239
210
  // ── Status view ─────────────────────────────────────────────────────────────
240
- return (
241
- <>
242
- <Card>
243
- <CardHeader>
244
- <div className="flex items-start justify-between gap-4">
245
- <div className="flex items-center gap-3">
246
- {has2FAEnabled ? (
247
- <div className="w-10 h-10 rounded-full bg-green-500/10 flex items-center justify-center flex-shrink-0">
248
- <ShieldCheck className="w-5 h-5 text-green-500" />
249
- </div>
250
- ) : (
251
- <div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center flex-shrink-0">
252
- <ShieldOff className="w-5 h-5 text-muted-foreground" />
253
- </div>
254
- )}
255
- <div>
256
- <div className="flex items-center gap-2">
257
- <CardTitle className="text-base">Two-Factor Authentication</CardTitle>
258
- <StatusBadge enabled={!!has2FAEnabled} />
259
- </div>
260
- <CardDescription className="mt-0.5">
261
- {has2FAEnabled
262
- ? `${devices.length} authenticator device${devices.length !== 1 ? 's' : ''} connected`
263
- : 'Add an extra layer of security to your account'}
264
- </CardDescription>
265
- </div>
266
- </div>
211
+ const statusDescription = (
212
+ <span className="inline-flex items-center gap-2">
213
+ <StatusBadge enabled={!!has2FAEnabled} />
214
+ <span>
215
+ {has2FAEnabled
216
+ ? `${devices.length} authenticator device${devices.length !== 1 ? 's' : ''} connected`
217
+ : 'Add an extra layer of security to your account'}
218
+ </span>
219
+ </span>
220
+ );
267
221
 
268
- {has2FAEnabled ? (
269
- <Button
270
- variant="outline"
271
- size="sm"
272
- onClick={() => { clearError(); setShowDisable(true); }}
273
- disabled={isLoading}
274
- className="flex-shrink-0 text-destructive hover:text-destructive hover:bg-destructive/10 border-destructive/30"
275
- >
276
- Disable
277
- </Button>
278
- ) : (
279
- <Button size="sm" onClick={handleEnableClick} disabled={isLoading} className="flex-shrink-0">
280
- Enable 2FA
281
- </Button>
282
- )}
283
- </div>
284
- </CardHeader>
222
+ const statusAction = has2FAEnabled ? (
223
+ <Button
224
+ variant="outline"
225
+ size="sm"
226
+ onClick={() => { clearError(); setShowDisable(true); }}
227
+ disabled={isLoading}
228
+ className="text-destructive hover:text-destructive hover:bg-destructive/10 border-destructive/30"
229
+ >
230
+ Disable
231
+ </Button>
232
+ ) : (
233
+ <Button size="sm" onClick={handleEnableClick} disabled={isLoading}>
234
+ Enable 2FA
235
+ </Button>
236
+ );
285
237
 
286
- {/* Fetch error */}
287
- {error && !showDisable && (
288
- <CardContent className="pt-0">
289
- <Alert variant="destructive">
290
- <AlertDescription>{error}</AlertDescription>
291
- </Alert>
292
- </CardContent>
293
- )}
238
+ return (
239
+ <>
240
+ <div className="space-y-6">
241
+ <SettingsBlock>
242
+ <SettingRow
243
+ label="Two-Factor Authentication"
244
+ description={statusDescription}
245
+ action={statusAction}
246
+ />
294
247
 
295
- {/* Device list */}
296
- {has2FAEnabled && devices.length > 0 && (
297
- <CardContent className="pt-0">
298
- <Separator className="mb-1" />
299
- <p className="text-xs font-medium text-muted-foreground uppercase tracking-wider mt-3 mb-1">
300
- Connected devices
301
- </p>
302
- <div className="divide-y">
303
- {devices.map((device) => (
304
- <DeviceRow
305
- key={device.id}
306
- name={device.name}
307
- createdAt={device.createdAt}
308
- isPrimary={device.isPrimary}
309
- />
310
- ))}
248
+ {/* Fetch error */}
249
+ {error && !showDisable && (
250
+ <div className="py-3">
251
+ <Alert variant="destructive">
252
+ <AlertDescription>{error}</AlertDescription>
253
+ </Alert>
311
254
  </div>
312
- </CardContent>
313
- )}
255
+ )}
314
256
 
315
- {/* Not enabled — info callout */}
316
- {!has2FAEnabled && (
317
- <CardContent className="pt-0">
318
- <div className="flex gap-3 p-3 rounded-lg bg-muted/50 text-sm text-muted-foreground">
319
- <Shield className="w-4 h-4 mt-0.5 flex-shrink-0" />
257
+ {/* Not enabled — subtle muted note */}
258
+ {!has2FAEnabled && (
259
+ <p className="flex gap-2 pt-3 text-[13px] leading-relaxed text-muted-foreground">
260
+ <Shield className="mt-0.5 h-4 w-4 flex-shrink-0" />
320
261
  <span>
321
262
  Two-factor authentication adds a second verification step when signing in,
322
263
  protecting your account even if your password is compromised.
323
264
  </span>
324
- </div>
325
- </CardContent>
265
+ </p>
266
+ )}
267
+ </SettingsBlock>
268
+
269
+ {/* Device list */}
270
+ {has2FAEnabled && devices.length > 0 && (
271
+ <SettingsBlock title="Connected devices">
272
+ {devices.map((device) => (
273
+ <SettingRow
274
+ key={device.id}
275
+ label={device.name}
276
+ description={deviceDescription(device.createdAt, device.isPrimary)}
277
+ />
278
+ ))}
279
+ </SettingsBlock>
326
280
  )}
327
- </Card>
281
+ </div>
328
282
 
329
283
  <DisableDialog
330
284
  open={showDisable}
@@ -0,0 +1,6 @@
1
+ export { SettingsShell } from './SettingsShell';
2
+ export { SettingsNav } from './SettingsNav';
3
+ export { SettingsTabs } from './SettingsTabs';
4
+ export { SettingsNavItem } from './SettingsNavItem';
5
+ export { SettingsPanel } from './SettingsPanel';
6
+ export { SettingsSearch } from './SettingsSearch';
@@ -0,0 +1,122 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * SettingsContext — the resolved runtime state shared by the shell parts
5
+ * (nav rail, search box, detail panel). Holds the derived section structure,
6
+ * the active section, and the search query. Selection can be driven by the
7
+ * zustand store (dialog mode) or local state (inline mode); the provider
8
+ * abstracts that difference so child components never care which.
9
+ */
10
+
11
+ import React, {
12
+ createContext,
13
+ useCallback,
14
+ useContext,
15
+ useMemo,
16
+ useState,
17
+ } from 'react';
18
+
19
+ import { useSettingsSections } from '../hooks/useSettingsSections';
20
+ import type { ResolvedGroup } from '../hooks/useSettingsSections';
21
+ import type { SettingsGroup, SettingsSection } from '../types';
22
+
23
+ interface SettingsContextValue {
24
+ /** Title for the surface header. */
25
+ title: React.ReactNode;
26
+ /** Groups (with sections) after ordering + search filtering. */
27
+ groups: ResolvedGroup[];
28
+ /** Flat visible sections in render order. */
29
+ visible: SettingsSection[];
30
+ /** The currently active section object (or null if none resolvable). */
31
+ active: SettingsSection | null;
32
+ /** Active section id. */
33
+ activeId: string | null;
34
+ /** Switch active section. */
35
+ setActive: (id: string) => void;
36
+ /** Search query + setter. */
37
+ query: string;
38
+ setQuery: (q: string) => void;
39
+ /** Whether the search box should render. */
40
+ searchable: boolean;
41
+ }
42
+
43
+ const SettingsContext = createContext<SettingsContextValue | null>(null);
44
+
45
+ export const useSettingsContext = (): SettingsContextValue => {
46
+ const ctx = useContext(SettingsContext);
47
+ if (!ctx) throw new Error('useSettingsContext must be used within SettingsProvider');
48
+ return ctx;
49
+ };
50
+
51
+ export interface SettingsProviderProps {
52
+ children: React.ReactNode;
53
+ title: React.ReactNode;
54
+ /** Fully-merged sections (built-ins + app), pre-concatenated by the caller. */
55
+ sections: SettingsSection[];
56
+ groups?: SettingsGroup[];
57
+ searchable: boolean;
58
+ /** Controlled active id (dialog mode passes the store value). */
59
+ activeId: string | null;
60
+ /** Called when active section changes (dialog mode writes the store). */
61
+ onActiveChange: (id: string) => void;
62
+ /** Section to fall back to when the controlled id is null/invalid. */
63
+ initialSection?: string;
64
+ }
65
+
66
+ export const SettingsProvider: React.FC<SettingsProviderProps> = ({
67
+ children,
68
+ title,
69
+ sections,
70
+ groups,
71
+ searchable,
72
+ activeId,
73
+ onActiveChange,
74
+ initialSection,
75
+ }) => {
76
+ const [query, setQuery] = useState('');
77
+
78
+ // Filtered/ordered structure. Search filters the rail but never the lookup map.
79
+ const { groups: resolvedGroups, visible, byId, ids } = useSettingsSections({
80
+ sections,
81
+ groups,
82
+ query,
83
+ });
84
+
85
+ // Resolve the active section. Precedence: controlled id (if known) →
86
+ // initialSection (if known) → first visible section → null.
87
+ const fallbackId = useMemo(() => {
88
+ if (initialSection && ids.has(initialSection)) return initialSection;
89
+ return visible[0]?.id ?? sections.find((s) => !s.hidden)?.id ?? null;
90
+ }, [initialSection, ids, visible, sections]);
91
+
92
+ const resolvedActiveId =
93
+ activeId && ids.has(activeId) ? activeId : fallbackId;
94
+
95
+ const active = resolvedActiveId ? byId.get(resolvedActiveId) ?? null : null;
96
+
97
+ const setActive = useCallback(
98
+ (id: string) => {
99
+ // Picking a section clears any search so the panel is reachable.
100
+ setQuery('');
101
+ onActiveChange(id);
102
+ },
103
+ [onActiveChange],
104
+ );
105
+
106
+ const value = useMemo<SettingsContextValue>(
107
+ () => ({
108
+ title,
109
+ groups: resolvedGroups,
110
+ visible,
111
+ active,
112
+ activeId: resolvedActiveId,
113
+ setActive,
114
+ query,
115
+ setQuery,
116
+ searchable,
117
+ }),
118
+ [title, resolvedGroups, visible, active, resolvedActiveId, setActive, query, searchable],
119
+ );
120
+
121
+ return <SettingsContext.Provider value={value}>{children}</SettingsContext.Provider>;
122
+ };
@@ -0,0 +1,2 @@
1
+ export { SettingsProvider, useSettingsContext } from './SettingsContext';
2
+ export type { SettingsProviderProps } from './SettingsContext';
@@ -0,0 +1,12 @@
1
+ export { useSettingsUrl } from './useSettingsUrl';
2
+ export type { UseSettingsUrlOptions } from './useSettingsUrl';
3
+
4
+ export { useSettingsDialog } from './useSettingsDialog';
5
+ export type { UseSettingsDialogReturn } from './useSettingsDialog';
6
+
7
+ export { useSettingsSections } from './useSettingsSections';
8
+ export type {
9
+ ResolvedGroup,
10
+ UseSettingsSectionsArgs,
11
+ UseSettingsSectionsResult,
12
+ } from './useSettingsSections';
@@ -0,0 +1,95 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * useProfileSave — the account-section data layer, as a plain hook (no context).
5
+ *
6
+ * Replaces the old ProfileProvider/useProfileContext plumbing: the labels are
7
+ * just i18n, `save` wraps `updateProfile` + toast, and `logout` is the shared
8
+ * logout hook. Every value here is already reachable via global hooks, so a
9
+ * React context added nothing but a wrapper — sections call this directly.
10
+ */
11
+
12
+ import { useCallback, useMemo } from 'react';
13
+
14
+ import { useAuth } from '@djangocfg/api/auth';
15
+ import { useAppT } from '@djangocfg/i18n';
16
+ import { toast } from '@djangocfg/ui-core/hooks';
17
+
18
+ import { profileLogger } from '../../../utils/logger';
19
+ import { useLogout } from '../../../hooks';
20
+
21
+ export interface ProfileLabels {
22
+ title: string;
23
+ personalInfo: string;
24
+ work: string;
25
+ preferences: string;
26
+ firstName: string;
27
+ lastName: string;
28
+ phone: string;
29
+ company: string;
30
+ position: string;
31
+ addFirstName: string;
32
+ addLastName: string;
33
+ addPhone: string;
34
+ addCompany: string;
35
+ addPosition: string;
36
+ deleteAccount: string;
37
+ profileUpdated: string;
38
+ failedToUpdate: string;
39
+ loading: string;
40
+ }
41
+
42
+ export interface UseProfileSaveReturn {
43
+ labels: ProfileLabels;
44
+ /** Persist a single profile field; shows a toast on success/error. */
45
+ save: (field: string, value: string) => Promise<void>;
46
+ /** Sign the user out. */
47
+ logout: () => void;
48
+ }
49
+
50
+ export function useProfileSave(title?: string): UseProfileSaveReturn {
51
+ const { updateProfile } = useAuth();
52
+ const t = useAppT();
53
+ const logout = useLogout();
54
+
55
+ const labels = useMemo<ProfileLabels>(
56
+ () => ({
57
+ title: title || t('layouts.profilePage.title'),
58
+ personalInfo: t('layouts.profilePage.personalInfo'),
59
+ work: t('layouts.profilePage.work'),
60
+ preferences: 'Preferences',
61
+ firstName: t('layouts.profilePage.firstName'),
62
+ lastName: t('layouts.profilePage.lastName'),
63
+ phone: t('layouts.profilePage.phone'),
64
+ company: t('layouts.profilePage.company'),
65
+ position: t('layouts.profilePage.position'),
66
+ addFirstName: t('layouts.profilePage.addFirstName') || 'Add first name',
67
+ addLastName: t('layouts.profilePage.addLastName') || 'Add last name',
68
+ addPhone: t('layouts.profilePage.addPhone') || 'Add phone number',
69
+ addCompany: t('layouts.profilePage.addCompany') || 'Add company',
70
+ addPosition: t('layouts.profilePage.addPosition') || 'Add position',
71
+ deleteAccount: t('layouts.profilePage.deleteAccount'),
72
+ profileUpdated: t('layouts.profilePage.profileUpdated'),
73
+ failedToUpdate: t('layouts.profilePage.failedToUpdate'),
74
+ loading: t('ui.states.loading'),
75
+ }),
76
+ [t, title],
77
+ );
78
+
79
+ const save = useCallback(
80
+ async (field: string, value: string) => {
81
+ try {
82
+ await updateProfile({ [field]: value });
83
+ toast.success(labels.profileUpdated);
84
+ } catch (error: unknown) {
85
+ profileLogger.error('Profile update error:', error);
86
+ const apiErr = error as { response?: Record<string, string[]> };
87
+ toast.error(apiErr?.response?.[field]?.[0] || labels.failedToUpdate);
88
+ throw error;
89
+ }
90
+ },
91
+ [updateProfile, labels],
92
+ );
93
+
94
+ return { labels, save, logout };
95
+ }
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * useSettingsDialog — the public API external consumers use to drive the
5
+ * globally-mounted <SettingsDialog />.
6
+ *
7
+ * @example
8
+ * const settings = useSettingsDialog();
9
+ * <Button onClick={() => settings.open()}>Settings</Button>
10
+ * <MenuItem onClick={() => settings.open('billing')}>Billing</MenuItem>
11
+ *
12
+ * Returns stable callbacks, safe in deps arrays. The dialog itself must be
13
+ * mounted once in the tree (PrivateLayout does this).
14
+ */
15
+
16
+ import { useCallback, useMemo } from 'react';
17
+
18
+ import { useSettingsDialogStore } from '../store';
19
+
20
+ export interface UseSettingsDialogReturn {
21
+ /** Whether the dialog is currently open. */
22
+ isOpen: boolean;
23
+ /** Currently active section id (null = fall back to first). */
24
+ activeSection: string | null;
25
+ /** Open the dialog, optionally on a specific section. */
26
+ open: (sectionId?: string) => void;
27
+ /** Close the dialog. */
28
+ close: () => void;
29
+ /** Toggle open/closed. */
30
+ toggle: () => void;
31
+ /** Switch the active section. */
32
+ setSection: (sectionId: string) => void;
33
+ }
34
+
35
+ export function useSettingsDialog(): UseSettingsDialogReturn {
36
+ const isOpen = useSettingsDialogStore((s) => s.isOpen);
37
+ const activeSection = useSettingsDialogStore((s) => s.activeSection);
38
+ const storeOpen = useSettingsDialogStore((s) => s.open);
39
+ const storeClose = useSettingsDialogStore((s) => s.close);
40
+ const storeToggle = useSettingsDialogStore((s) => s.toggle);
41
+ const storeSetSection = useSettingsDialogStore((s) => s.setSection);
42
+
43
+ const open = useCallback((sectionId?: string) => storeOpen(sectionId), [storeOpen]);
44
+ const close = useCallback(() => storeClose(), [storeClose]);
45
+ const toggle = useCallback(() => storeToggle(), [storeToggle]);
46
+ const setSection = useCallback((id: string) => storeSetSection(id), [storeSetSection]);
47
+
48
+ return useMemo(
49
+ () => ({ isOpen, activeSection, open, close, toggle, setSection }),
50
+ [isOpen, activeSection, open, close, toggle, setSection],
51
+ );
52
+ }