@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.
- package/dist/components/account/AccountModals.mjs +18 -0
- package/dist/components/account/AccountModals.mjs.map +1 -0
- package/dist/components/account/ConfigurationsMyAccountModal.mjs +26 -15
- package/dist/components/account/ConfigurationsMyAccountModal.mjs.map +1 -1
- package/dist/components/account/ConfirmDeleteAccountModal.mjs +45 -0
- package/dist/components/account/ConfirmDeleteAccountModal.mjs.map +1 -0
- package/dist/components/account/DeleteAccountModal.mjs +123 -0
- package/dist/components/account/DeleteAccountModal.mjs.map +1 -0
- package/dist/components/account/IsntPossibleDeleteAccountModal.mjs +42 -0
- package/dist/components/account/IsntPossibleDeleteAccountModal.mjs.map +1 -0
- package/dist/components/layouts/AppMobileNavBar.mjs +53 -0
- package/dist/components/layouts/AppMobileNavBar.mjs.map +1 -0
- package/dist/components/layouts/AppNavBar.mjs +75 -0
- package/dist/components/layouts/AppNavBar.mjs.map +1 -0
- package/dist/components/layouts/ProfilePopover.mjs +3 -3
- package/dist/components/layouts/ProfilePopover.mjs.map +1 -1
- package/dist/components/modals/ModalManager.mjs +19 -0
- package/dist/components/modals/ModalManager.mjs.map +1 -0
- package/dist/components/modals/Modals.mjs +20 -0
- package/dist/components/modals/Modals.mjs.map +1 -0
- package/dist/components/ui/feedback/LoadingOverlay.mjs +19 -0
- package/dist/components/ui/feedback/LoadingOverlay.mjs.map +1 -0
- package/dist/index.mjs +16 -2
- package/dist/index.mjs.map +1 -1
- package/dist/store/useAccountModals.mjs +18 -0
- package/dist/store/useAccountModals.mjs.map +1 -0
- package/dist/store/useModalManager.mjs +75 -0
- package/dist/store/useModalManager.mjs.map +1 -0
- package/package.json +1 -1
- package/src/components/account/AccountModals.tsx +17 -0
- package/src/components/account/ConfigurationsMyAccountModal.tsx +29 -26
- package/src/components/account/ConfirmDeleteAccountModal.tsx +55 -0
- package/src/components/account/DeleteAccountModal.tsx +130 -0
- package/src/components/account/IsntPossibleDeleteAccountModal.tsx +49 -0
- package/src/components/layouts/AppMobileNavBar.tsx +70 -0
- package/src/components/layouts/AppNavBar.tsx +86 -0
- package/src/components/layouts/ProfilePopover.tsx +2 -3
- package/src/components/modals/ModalManager.tsx +29 -0
- package/src/components/modals/Modals.tsx +29 -0
- package/src/components/ui/feedback/LoadingOverlay.tsx +31 -0
- package/src/index.ts +14 -2
- package/src/store/useAccountModals.ts +57 -0
- 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
|
|
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
|
-
|
|
149
|
-
export {
|
|
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
|
+
}));
|