@greatapps/common 1.1.33 → 1.1.35

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 (43) hide show
  1. package/dist/components/account/AccountModals.mjs +18 -0
  2. package/dist/components/account/AccountModals.mjs.map +1 -0
  3. package/dist/components/account/ConfigurationsMyAccountModal.mjs +26 -15
  4. package/dist/components/account/ConfigurationsMyAccountModal.mjs.map +1 -1
  5. package/dist/components/account/ConfirmDeleteAccountModal.mjs +45 -0
  6. package/dist/components/account/ConfirmDeleteAccountModal.mjs.map +1 -0
  7. package/dist/components/account/DeleteAccountModal.mjs +123 -0
  8. package/dist/components/account/DeleteAccountModal.mjs.map +1 -0
  9. package/dist/components/account/IsntPossibleDeleteAccountModal.mjs +42 -0
  10. package/dist/components/account/IsntPossibleDeleteAccountModal.mjs.map +1 -0
  11. package/dist/components/layouts/AppMobileNavBar.mjs +53 -0
  12. package/dist/components/layouts/AppMobileNavBar.mjs.map +1 -0
  13. package/dist/components/layouts/AppNavBar.mjs +75 -0
  14. package/dist/components/layouts/AppNavBar.mjs.map +1 -0
  15. package/dist/components/layouts/ProfilePopover.mjs +3 -3
  16. package/dist/components/layouts/ProfilePopover.mjs.map +1 -1
  17. package/dist/components/modals/ModalManager.mjs +19 -0
  18. package/dist/components/modals/ModalManager.mjs.map +1 -0
  19. package/dist/components/modals/Modals.mjs +20 -0
  20. package/dist/components/modals/Modals.mjs.map +1 -0
  21. package/dist/components/ui/feedback/LoadingOverlay.mjs +19 -0
  22. package/dist/components/ui/feedback/LoadingOverlay.mjs.map +1 -0
  23. package/dist/index.mjs +16 -2
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/store/useAccountModals.mjs +18 -0
  26. package/dist/store/useAccountModals.mjs.map +1 -0
  27. package/dist/store/useModalManager.mjs +75 -0
  28. package/dist/store/useModalManager.mjs.map +1 -0
  29. package/package.json +1 -1
  30. package/src/components/account/AccountModals.tsx +17 -0
  31. package/src/components/account/ConfigurationsMyAccountModal.tsx +29 -26
  32. package/src/components/account/ConfirmDeleteAccountModal.tsx +55 -0
  33. package/src/components/account/DeleteAccountModal.tsx +130 -0
  34. package/src/components/account/IsntPossibleDeleteAccountModal.tsx +49 -0
  35. package/src/components/layouts/AppMobileNavBar.tsx +70 -0
  36. package/src/components/layouts/AppNavBar.tsx +86 -0
  37. package/src/components/layouts/ProfilePopover.tsx +2 -3
  38. package/src/components/modals/ModalManager.tsx +29 -0
  39. package/src/components/modals/Modals.tsx +29 -0
  40. package/src/components/ui/feedback/LoadingOverlay.tsx +31 -0
  41. package/src/index.ts +14 -2
  42. package/src/store/useAccountModals.ts +57 -0
  43. package/src/store/useModalManager.ts +104 -0
@@ -0,0 +1,130 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback } from 'react';
4
+ import { Lock, TriangleAlert } from 'lucide-react';
5
+ import { toast } from 'sonner';
6
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/overlay/Dialog';
7
+ import { Button } from '../ui/buttons/Button';
8
+ import { FormField } from '../ui/form/FormField';
9
+ import { SelectField } from '../ui/form/SelectField';
10
+ import { TextAreaField } from '../ui/form/TextAreaField';
11
+ import { Toast } from '../ui/feedback/Toast';
12
+ import usePasswordVisibility from '../../hooks/usePasswordVisibility';
13
+ import { useAccountModals } from '../../store/useAccountModals';
14
+ import { AccountSectionType } from '../../enums/AccountSectionType';
15
+
16
+ const REASON_OPTIONS = [
17
+ { value: 'sem-funcao', label: 'A ferramenta não possui uma função que eu preciso' },
18
+ { value: 'problemas', label: 'Encontrei problemas técnicos ou erros no sistema' },
19
+ { value: 'suporte', label: 'Tive problemas com o suporte ou atendimento' },
20
+ { value: 'demora', label: 'As atualizações demoram mais do que o esperado' },
21
+ { value: 'temporario', label: 'O cancelamento é temporário' },
22
+ { value: 'outro', label: 'Outro motivo' },
23
+ ];
24
+
25
+ export default function DeleteAccountModal() {
26
+ const { activeModal, pagesCount, openConfigurations, openConfirmDelete, close } = useAccountModals();
27
+ const open = activeModal === 'deleteAccount';
28
+
29
+ const [reason, setReason] = useState('');
30
+ const [description, setDescription] = useState('');
31
+ const [password, setPassword] = useState('');
32
+ const { showPassword, togglePassword } = usePasswordVisibility();
33
+
34
+ const isFormValid = Boolean(reason && description && password);
35
+
36
+ const resetForm = useCallback(() => {
37
+ setReason('');
38
+ setDescription('');
39
+ setPassword('');
40
+ }, []);
41
+
42
+ const handleClose = () => {
43
+ resetForm();
44
+ openConfigurations(AccountSectionType.SECURITY);
45
+ };
46
+
47
+ const handleDelete = (e: React.FormEvent) => {
48
+ e.preventDefault();
49
+ if (!isFormValid) {
50
+ toast.custom((t) => (
51
+ <Toast variant="error" message="Preencha todos os campos obrigatórios" toastId={t} />
52
+ ));
53
+ return;
54
+ }
55
+ resetForm();
56
+ openConfirmDelete(pagesCount);
57
+ };
58
+
59
+ return (
60
+ <Dialog open={open} onOpenChange={handleClose}>
61
+ <DialogContent className="flex flex-col p-0 gap-0 max-w-full! border-0 rounded-t-2xl rounded-b-none h-[100dvh] top-0! bottom-0! left-0! right-0! translate-x-0! translate-y-0! lg:max-w-lg! lg:h-auto! lg:max-h-[90vh] lg:rounded-lg lg:border lg:top-[50%]! lg:left-[50%]! lg:right-auto! lg:bottom-auto! lg:translate-x-[-50%]! lg:translate-y-[-50%]! overflow-y-auto">
62
+ <form onSubmit={handleDelete} className="flex flex-col flex-1 lg:flex-none">
63
+ <div className="flex flex-col gap-6 p-4 lg:p-6 flex-1 lg:flex-none">
64
+ <DialogHeader className="p-0">
65
+ <div className="flex items-center justify-center w-fit bg-red-50 rounded-lg p-2.5 mb-5">
66
+ <TriangleAlert size={20} className="text-red-500" />
67
+ </div>
68
+ <DialogTitle>Excluir conta</DialogTitle>
69
+ <DialogDescription className="paragraph-small-regular">
70
+ Esta ação é <span className="font-semibold text-gray-950">irreversível</span>. Ao
71
+ confirmar, sua conta e todos os seus dados serão excluídos.
72
+ </DialogDescription>
73
+ </DialogHeader>
74
+
75
+ <SelectField
76
+ label="Motivo do cancelamento"
77
+ required
78
+ placeholder="Selecione"
79
+ options={REASON_OPTIONS}
80
+ value={reason}
81
+ onChange={(value) => setReason(value as string)}
82
+ className="h-10!"
83
+ />
84
+
85
+ <div>
86
+ <TextAreaField
87
+ label="Descreva o que levou a tomar essa decisão"
88
+ required
89
+ placeholder="Explique sua decisão..."
90
+ value={description}
91
+ onChange={(e) => setDescription(e.target.value)}
92
+ rows={4}
93
+ maxLength={400}
94
+ />
95
+ <span className="paragraph-xsmall-medium text-gray-600 text-left mt-1 block">
96
+ {description.length}/400 caracteres
97
+ </span>
98
+ </div>
99
+
100
+ <FormField
101
+ className="text-gray-600"
102
+ label="Senha"
103
+ type="password"
104
+ placeholder="Informe sua senha"
105
+ leftIcon={Lock}
106
+ value={password}
107
+ onChange={(e) => setPassword(e.target.value)}
108
+ classnameContainer="h-10!"
109
+ showPassword={showPassword}
110
+ onTogglePassword={togglePassword}
111
+ />
112
+ </div>
113
+
114
+ <div className="flex flex-row items-center gap-2 p-4 lg:p-6 border-t border-gray-200 bg-white mt-auto lg:mt-0">
115
+ <Button
116
+ variant="default"
117
+ className="h-10 flex-1 bg-gray-950 text-white hover:bg-gray-800"
118
+ type="submit"
119
+ >
120
+ Excluir conta
121
+ </Button>
122
+ <Button variant="secondary" className="h-10 flex-1" type="button" onClick={handleClose}>
123
+ Fechar
124
+ </Button>
125
+ </div>
126
+ </form>
127
+ </DialogContent>
128
+ </Dialog>
129
+ );
130
+ }
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+
3
+ import { TriangleAlert } from 'lucide-react';
4
+ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../ui/overlay/Dialog';
5
+ import { Button } from '../ui/buttons/Button';
6
+ import { useAccountModals } from '../../store/useAccountModals';
7
+
8
+ export default function IsntPossibleDeleteAccountModal() {
9
+ const { activeModal, close, config } = useAccountModals();
10
+ const { onGoToSubscription } = config;
11
+ const open = activeModal === 'isntPossibleDelete';
12
+
13
+ return (
14
+ <Dialog open={open} onOpenChange={close}>
15
+ <DialogContent className="max-w-[calc(100%-2rem)]! w-full rounded-lg border sm:max-w-[400px]! lg:max-w-[470px]! lg:top-[50%]! lg:left-[50%]! lg:right-auto! lg:bottom-auto! lg:translate-x-[-50%]! lg:translate-y-[-50%]!">
16
+ <div className="flex flex-col gap-5">
17
+ <div className="flex items-center justify-center w-fit bg-red-50 rounded-lg p-2.5">
18
+ <TriangleAlert size={20} className="text-red-500" />
19
+ </div>
20
+ <DialogHeader className="p-0 gap-2">
21
+ <DialogTitle className="paragraph-medium-semibold text-gray-950">
22
+ Não é possível excluir a conta
23
+ </DialogTitle>
24
+ <DialogDescription className="paragraph-small-regular text-gray-600">
25
+ Você tem uma assinatura ativa. Para excluir sua conta,{' '}
26
+ <span className="font-semibold text-gray-950">primeiro cancele a assinatura</span> na
27
+ página Assinatura e depois tente novamente.
28
+ </DialogDescription>
29
+ </DialogHeader>
30
+ </div>
31
+
32
+ <div className="h-px bg-gray-200 -mx-6" />
33
+
34
+ <div className="flex flex-row items-center gap-2">
35
+ <Button className="h-10 flex-1" onClick={close}>
36
+ Ok, entendi
37
+ </Button>
38
+ <Button
39
+ variant="secondary"
40
+ className="h-10 flex-1"
41
+ onClick={onGoToSubscription ?? close}
42
+ >
43
+ Ir para assinatura
44
+ </Button>
45
+ </div>
46
+ </DialogContent>
47
+ </Dialog>
48
+ );
49
+ }
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+
3
+ import { ReactNode } from 'react';
4
+ import Image from 'next/image';
5
+ import { ProfilePopover } from './ProfilePopover';
6
+ import { useAccountModals } from '../../store/useAccountModals';
7
+ import type { ProfileMenuItem } from './ProfilePopover';
8
+
9
+ export interface AppMobileNavBarProps {
10
+ appIconActive?: boolean;
11
+ /** Shows the colored indicator bar below the app icon */
12
+ appIconIndicator?: boolean;
13
+ /** Shows the gray indicator bar below the GreatApps logo */
14
+ logoIndicator?: boolean;
15
+ /** Extra slot rendered after the app icon (e.g. project selector) */
16
+ extraLeftSlot?: ReactNode;
17
+ /** Required: the right-side action element (toggle button, sheet button, etc.) */
18
+ rightActionSlot: ReactNode;
19
+ menuItems?: ProfileMenuItem[];
20
+ onCreditsClick?: () => void;
21
+ }
22
+
23
+ export function AppMobileNavBar({
24
+ appIconActive = false,
25
+ appIconIndicator = false,
26
+ logoIndicator = false,
27
+ extraLeftSlot,
28
+ rightActionSlot,
29
+ menuItems = [],
30
+ onCreditsClick,
31
+ }: AppMobileNavBarProps) {
32
+ const { openConfigurations } = useAccountModals();
33
+
34
+ return (
35
+ <div className="fixed top-0 left-0 right-0 flex md:hidden items-center justify-between p-4 bg-white z-50 border-b border-gray-200">
36
+ <div className="flex items-center gap-1.5">
37
+ <div className="relative size-10 flex items-center justify-center">
38
+ <Image src="/icons/great-icon.svg" alt="GreatApps" width={28} height={28} />
39
+ {logoIndicator && (
40
+ <div className="absolute bottom-[-17px] left-1/2 -translate-x-1/2 w-5 h-[3px] bg-gray-950 rounded-t-xl" />
41
+ )}
42
+ </div>
43
+ <div className="relative">
44
+ <div
45
+ className={`size-10 flex items-center justify-center rounded-lg transition-colors duration-300 ${
46
+ appIconActive ? 'bg-pages-300' : 'bg-pages-50'
47
+ }`}
48
+ >
49
+ <Image src="/icons/icon-navbar.svg" alt="GreatPages" width={24} height={24} />
50
+ </div>
51
+ {appIconIndicator && (
52
+ <div className="absolute -bottom-[17px] left-1/2 -translate-x-1/2 w-5 h-[3px] bg-pages-300 rounded-t-xl" />
53
+ )}
54
+ </div>
55
+ {extraLeftSlot}
56
+ </div>
57
+ <div className="flex items-center gap-1.5">
58
+ <ProfilePopover
59
+ side="bottom"
60
+ align="end"
61
+ contentClassName=""
62
+ onEditProfile={() => openConfigurations()}
63
+ onCreditsClick={onCreditsClick}
64
+ menuItems={menuItems}
65
+ />
66
+ {rightActionSlot}
67
+ </div>
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+
3
+ import Image from 'next/image';
4
+ import { useRouter } from 'next/navigation';
5
+ import { NavBar } from './NavBar';
6
+ import { ProfilePopover } from './ProfilePopover';
7
+ import { NotificationsPopover } from './NotificationsPopover';
8
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/overlay/Tooltip';
9
+ import { useAccountModals } from '../../store/useAccountModals';
10
+ import type { ProfileMenuItem } from './ProfilePopover';
11
+ import type { NotificationData } from './NotificationsPopover';
12
+
13
+ export interface AppNavBarProps {
14
+ onLogoClick?: () => void;
15
+ logoIndicatorClassName?: string;
16
+ appIconHref?: string;
17
+ appIconActive?: boolean;
18
+ /** Controls the left-side indicator bar on the app icon (opacity transition) */
19
+ appIconIndicator?: boolean;
20
+ showExpandButton?: boolean;
21
+ onExpandClick?: () => void;
22
+ menuItems?: ProfileMenuItem[];
23
+ onCreditsClick?: () => void;
24
+ notifications?: NotificationData[];
25
+ onNotificationsViewAll?: () => void;
26
+ }
27
+
28
+ export function AppNavBar({
29
+ onLogoClick,
30
+ logoIndicatorClassName,
31
+ appIconHref = '/',
32
+ appIconActive = false,
33
+ appIconIndicator = false,
34
+ showExpandButton = false,
35
+ onExpandClick,
36
+ menuItems = [],
37
+ onCreditsClick,
38
+ notifications,
39
+ onNotificationsViewAll,
40
+ }: AppNavBarProps) {
41
+ const router = useRouter();
42
+ const { openConfigurations } = useAccountModals();
43
+
44
+ const handleLogoClick = onLogoClick ?? (() => router.push('/'));
45
+ const handleAppIconClick = () => router.push(appIconHref);
46
+
47
+ return (
48
+ <NavBar
49
+ onLogoClick={handleLogoClick}
50
+ logoIndicatorClassName={logoIndicatorClassName}
51
+ showExpandButton={showExpandButton}
52
+ onExpandClick={onExpandClick}
53
+ appIconSlot={
54
+ <div className="relative">
55
+ <Tooltip>
56
+ <TooltipTrigger asChild onClick={handleAppIconClick}>
57
+ <div
58
+ className={`size-10 flex items-center justify-center rounded-lg hover:bg-pages-100 cursor-pointer transition-colors duration-300 ${
59
+ appIconActive ? 'bg-pages-300' : 'bg-pages-50'
60
+ }`}
61
+ >
62
+ <Image src="/icons/icon-navbar.svg" alt="GreatPages" width={24} height={24} />
63
+ </div>
64
+ </TooltipTrigger>
65
+ <TooltipContent side="right">GreatPages</TooltipContent>
66
+ </Tooltip>
67
+ <div
68
+ className={`absolute -left-4 top-1/2 -translate-y-1/2 w-[3px] h-5 bg-pages-300 rounded-r-xl transition-opacity duration-300 ${
69
+ appIconIndicator ? 'opacity-100' : 'opacity-0'
70
+ }`}
71
+ />
72
+ </div>
73
+ }
74
+ >
75
+ <NotificationsPopover
76
+ notifications={notifications}
77
+ onViewAll={onNotificationsViewAll}
78
+ />
79
+ <ProfilePopover
80
+ onEditProfile={() => openConfigurations()}
81
+ onCreditsClick={onCreditsClick}
82
+ menuItems={menuItems}
83
+ />
84
+ </NavBar>
85
+ );
86
+ }
@@ -9,6 +9,7 @@ import { Progress } from "../ui/feedback/Progress";
9
9
  import { Separator } from "../ui/data-display/Separator";
10
10
  import { NavBarItem } from "./NavBarItem";
11
11
  import { UserAvatar } from "../ui/data-display/UserAvatar";
12
+ import { LoadingOverlay } from "../ui/feedback/LoadingOverlay";
12
13
  import { useAuth } from "../../providers/auth.provider";
13
14
  import { cn } from "../../infra/utils/clsx";
14
15
  import { useIaCredits } from "../../modules/ia-credits/hooks/ia-credits.hook";
@@ -27,7 +28,6 @@ export interface ProfilePopoverProps {
27
28
  onCreditsClick?: () => void;
28
29
  menuItems?: ProfileMenuItem[];
29
30
  logoutRedirect?: string;
30
- logoutOverlay?: React.ReactNode;
31
31
  }
32
32
 
33
33
  function ProfilePopover({
@@ -38,7 +38,6 @@ function ProfilePopover({
38
38
  onCreditsClick,
39
39
  menuItems = [],
40
40
  logoutRedirect = "/login",
41
- logoutOverlay,
42
41
  }: ProfilePopoverProps) {
43
42
  const router = useRouter();
44
43
  const { user, logout } = useAuth();
@@ -70,7 +69,7 @@ function ProfilePopover({
70
69
 
71
70
  return (
72
71
  <>
73
- {isLoggingOut && logoutOverlay}
72
+ <LoadingOverlay isLoading={isLoggingOut} message="Saindo da sua conta..." />
74
73
  <Popover>
75
74
  <PopoverPrimitive.Trigger className="cursor-pointer">
76
75
  {user && (
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import { ComponentType } from 'react';
4
+ import { useModalManager } from '../../store/useModalManager';
5
+
6
+ interface ModalManagerProps {
7
+ registry: Record<string, ComponentType>;
8
+ }
9
+
10
+ export function ModalManager({ registry }: ModalManagerProps) {
11
+ const { modalStack } = useModalManager();
12
+
13
+ if (modalStack.length === 0) return null;
14
+
15
+ return (
16
+ <>
17
+ {modalStack.map((modal) => {
18
+ const ModalComponent = registry[modal.key];
19
+
20
+ if (!ModalComponent) {
21
+ console.warn(`Modal "${modal.key}" not found in registry`);
22
+ return null;
23
+ }
24
+
25
+ return <ModalComponent key={modal.key} />;
26
+ })}
27
+ </>
28
+ );
29
+ }
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import { ComponentType, useEffect } from 'react';
4
+ import AccountModals from '../account/AccountModals';
5
+ import { ModalManager } from './ModalManager';
6
+ import { useAccountModals } from '../../store/useAccountModals';
7
+ import type { ComboboxOption } from '../ui/form/ComboboxField';
8
+
9
+ interface ModalsProps {
10
+ registry: Record<string, ComponentType>;
11
+ hasActiveSubscription?: boolean;
12
+ currencies?: ComboboxOption[];
13
+ onGoToSubscription?: () => void;
14
+ }
15
+
16
+ export function Modals({ registry, hasActiveSubscription, currencies, onGoToSubscription }: ModalsProps) {
17
+ const { updateConfig } = useAccountModals();
18
+
19
+ useEffect(() => {
20
+ updateConfig({ hasActiveSubscription, currencies, onGoToSubscription });
21
+ }, [hasActiveSubscription, currencies, onGoToSubscription, updateConfig]);
22
+
23
+ return (
24
+ <>
25
+ <AccountModals />
26
+ <ModalManager registry={registry} />
27
+ </>
28
+ );
29
+ }
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+
3
+ import Image from 'next/image';
4
+ import { ReactNode } from 'react';
5
+
6
+ interface LoadingOverlayProps {
7
+ isLoading: boolean;
8
+ message?: string;
9
+ icon?: ReactNode;
10
+ }
11
+
12
+ export function LoadingOverlay({ isLoading, message, icon }: LoadingOverlayProps) {
13
+ if (!isLoading) return null;
14
+
15
+ const defaultIcon = <Image src="/icons/great-icon.svg" alt="Great Icon" width={20} height={20} />;
16
+
17
+ return (
18
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-[2px]">
19
+ <div className="bg-white rounded-2xl shadow-xl px-16 py-12 flex flex-col items-center gap-6 min-w-[380px]">
20
+ <div className="relative flex items-center justify-center w-20 h-20">
21
+ <div className="absolute inset-0 border-[5px] border-gray-100 rounded-full" />
22
+ <div className="absolute inset-0 border-[5px] border-transparent border-t-black rounded-full animate-spin" />
23
+ <div className="relative z-20 flex items-center justify-center w-14 h-14 bg-gray-100 rounded-full">
24
+ {icon ?? defaultIcon}
25
+ </div>
26
+ </div>
27
+ <span className="font-bold text-gray-900 text-lg">{message}</span>
28
+ </div>
29
+ </div>
30
+ );
31
+ }
package/src/index.ts CHANGED
@@ -38,6 +38,10 @@ export { parseSchema, parseResult } from './infra/utils/parser';
38
38
  export { MainLayout, MainLayoutContent, MainLayoutSpacer, MainLayoutMain } from './components/layouts/MainLayout';
39
39
  export { NavBar } from './components/layouts/NavBar';
40
40
  export type { NavBarProps } from './components/layouts/NavBar';
41
+ export { AppNavBar } from './components/layouts/AppNavBar';
42
+ export type { AppNavBarProps } from './components/layouts/AppNavBar';
43
+ export { AppMobileNavBar } from './components/layouts/AppMobileNavBar';
44
+ export type { AppMobileNavBarProps } from './components/layouts/AppMobileNavBar';
41
45
  export { SideBarNavigation } from './components/layouts/SideBarNavigation';
42
46
  export { MdSideBarNavigation } from './components/layouts/MdSideBarNavigation';
43
47
  export { NotificationItem } from './components/layouts/NotificationItem';
@@ -72,6 +76,7 @@ export type { UserAvatarProps } from './components/ui/data-display/UserAvatar';
72
76
  export { default as CircularProgress } from './components/ui/feedback/CircularProgress';
73
77
  export { default as DefaultCircularProgress } from './components/ui/feedback/DefaultCircularProgress';
74
78
  export { Progress } from './components/ui/feedback/Progress';
79
+ export { LoadingOverlay } from './components/ui/feedback/LoadingOverlay';
75
80
  export { Toast, toastVariants, toastIconContainerVariants } from './components/ui/feedback/Toast';
76
81
  export type { ToastProps } from './components/ui/feedback/Toast';
77
82
 
@@ -96,6 +101,8 @@ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from './comp
96
101
 
97
102
  // Store
98
103
  export { useMdSidebarStore } from './store/useMdSidebarStore';
104
+ export { useModalManager } from './store/useModalManager';
105
+ export type { ModalData } from './store/useModalManager';
99
106
 
100
107
  // Enums
101
108
  export { AccountSectionType } from './enums/AccountSectionType';
@@ -145,8 +152,13 @@ export type {
145
152
  ContactResetResult,
146
153
  } from './modules/accounts/types';
147
154
 
148
- // Account Components
149
- export { default as ConfigurationsMyAccountModal } from './components/account/ConfigurationsMyAccountModal';
155
+ export { default as AccountModals } from './components/account/AccountModals';
156
+ export { useAccountModals } from './store/useAccountModals';
157
+ export type { AccountModalsConfig } from './store/useAccountModals';
158
+
159
+ export { ModalManager } from './components/modals/ModalManager';
160
+ export { Modals } from './components/modals/Modals';
161
+
150
162
  export { default as TwoFactorAuthModal } from './components/account/TwoFactorAuthModal';
151
163
  export { default as DisableTwoFactorAuthModal } from './components/account/DisableTwoFactorAuthModal';
152
164
  export { default as ConfirmGlobalPreferencesModal } from './components/account/ConfirmGlobalPreferencesModal';
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+
3
+ import { create } from 'zustand';
4
+ import type { ComboboxOption } from '../components/ui/form/ComboboxField';
5
+ import type { AccountSectionType } from '../enums/AccountSectionType';
6
+
7
+ type AccountModalType =
8
+ | 'configurations'
9
+ | 'deleteAccount'
10
+ | 'isntPossibleDelete'
11
+ | 'confirmDelete';
12
+
13
+ export interface AccountModalsConfig {
14
+ hasActiveSubscription?: boolean;
15
+ currencies?: ComboboxOption[];
16
+ onGoToSubscription?: () => void;
17
+ }
18
+
19
+ interface AccountModalsState {
20
+ activeModal: AccountModalType | null;
21
+ config: AccountModalsConfig;
22
+ initialSection?: AccountSectionType;
23
+ pagesCount: number;
24
+
25
+ /** Called by <AccountModals> to keep app-level config in sync */
26
+ updateConfig: (config: AccountModalsConfig) => void;
27
+
28
+ openConfigurations: (initialSection?: AccountSectionType) => void;
29
+ openDeleteAccount: (pagesCount?: number) => void;
30
+ openIsntPossibleDelete: () => void;
31
+ openConfirmDelete: (pagesCount?: number) => void;
32
+ close: () => void;
33
+ }
34
+
35
+ export const useAccountModals = create<AccountModalsState>((set) => ({
36
+ activeModal: null,
37
+ config: {},
38
+ initialSection: undefined,
39
+ pagesCount: 0,
40
+
41
+ updateConfig: (config) => set({ config }),
42
+
43
+ openConfigurations: (initialSection) =>
44
+ set({ activeModal: 'configurations', initialSection }),
45
+
46
+ openDeleteAccount: (pagesCount = 0) =>
47
+ set({ activeModal: 'deleteAccount', pagesCount }),
48
+
49
+ openIsntPossibleDelete: () =>
50
+ set({ activeModal: 'isntPossibleDelete' }),
51
+
52
+ openConfirmDelete: (pagesCount = 0) =>
53
+ set({ activeModal: 'confirmDelete', pagesCount }),
54
+
55
+ close: () =>
56
+ set({ activeModal: null, initialSection: undefined }),
57
+ }));
@@ -0,0 +1,104 @@
1
+ 'use client';
2
+
3
+ import { create } from 'zustand';
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ export type ModalData = Record<string, any>;
7
+
8
+ interface ModalStackItem {
9
+ key: string;
10
+ data: ModalData;
11
+ }
12
+
13
+ interface ModalState {
14
+ modalStack: ModalStackItem[];
15
+ activeModal: string | null;
16
+ modalData: ModalData;
17
+ openModal: (key: string, data?: ModalData) => void;
18
+ /** Closes the top modal in the stack */
19
+ closeModal: () => void;
20
+ /** Closes a specific modal by key */
21
+ closeModalByKey: (key: string) => void;
22
+ updateModalData: (data: ModalData) => void;
23
+ isModalOpen: (key: string) => boolean;
24
+ getModalData: (key: string) => ModalData;
25
+ }
26
+
27
+ export const useModalManager = create<ModalState>((set, get) => ({
28
+ modalStack: [],
29
+ activeModal: null,
30
+ modalData: {},
31
+
32
+ openModal: (key, data = {}) => {
33
+ set((state) => {
34
+ const existingIndex = state.modalStack.findIndex((item) => item.key === key);
35
+ if (existingIndex !== -1) {
36
+ const newStack = [...state.modalStack];
37
+ newStack[existingIndex] = { key, data };
38
+ return {
39
+ modalStack: newStack,
40
+ activeModal: newStack[newStack.length - 1]?.key ?? null,
41
+ modalData: newStack[newStack.length - 1]?.data ?? {},
42
+ };
43
+ }
44
+
45
+ const newStack = [...state.modalStack, { key, data }];
46
+ return {
47
+ modalStack: newStack,
48
+ activeModal: key,
49
+ modalData: data,
50
+ };
51
+ });
52
+ },
53
+
54
+ closeModal: () => {
55
+ set((state) => {
56
+ const newStack = state.modalStack.slice(0, -1);
57
+ const topModal = newStack[newStack.length - 1];
58
+ return {
59
+ modalStack: newStack,
60
+ activeModal: topModal?.key ?? null,
61
+ modalData: topModal?.data ?? {},
62
+ };
63
+ });
64
+ },
65
+
66
+ closeModalByKey: (key: string) => {
67
+ set((state) => {
68
+ const newStack = state.modalStack.filter((item) => item.key !== key);
69
+ const topModal = newStack[newStack.length - 1];
70
+ return {
71
+ modalStack: newStack,
72
+ activeModal: topModal?.key ?? null,
73
+ modalData: topModal?.data ?? {},
74
+ };
75
+ });
76
+ },
77
+
78
+ updateModalData: (data) => {
79
+ set((state) => {
80
+ if (state.modalStack.length === 0) return state;
81
+
82
+ const newStack = [...state.modalStack];
83
+ const lastIndex = newStack.length - 1;
84
+ newStack[lastIndex] = {
85
+ ...newStack[lastIndex],
86
+ data: { ...newStack[lastIndex].data, ...data },
87
+ };
88
+
89
+ return {
90
+ modalStack: newStack,
91
+ modalData: newStack[lastIndex].data,
92
+ };
93
+ });
94
+ },
95
+
96
+ isModalOpen: (key: string) => {
97
+ return get().modalStack.some((item) => item.key === key);
98
+ },
99
+
100
+ getModalData: (key: string) => {
101
+ const item = get().modalStack.find((item) => item.key === key);
102
+ return item?.data ?? {};
103
+ },
104
+ }));