@greatapps/common 1.1.24 → 1.1.25
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/ConfigurationsMyAccountModal.mjs +121 -0
- package/dist/components/account/ConfigurationsMyAccountModal.mjs.map +1 -0
- package/dist/components/account/ConfirmGlobalPreferencesModal.mjs +45 -0
- package/dist/components/account/ConfirmGlobalPreferencesModal.mjs.map +1 -0
- package/dist/components/account/DisableTwoFactorAuthModal.mjs +67 -0
- package/dist/components/account/DisableTwoFactorAuthModal.mjs.map +1 -0
- package/dist/components/account/TwoFactorAuthModal.mjs +190 -0
- package/dist/components/account/TwoFactorAuthModal.mjs.map +1 -0
- package/dist/components/account/constants.mjs +32 -0
- package/dist/components/account/constants.mjs.map +1 -0
- package/dist/components/account/hooks/useChangeEmailForm.mjs +53 -0
- package/dist/components/account/hooks/useChangeEmailForm.mjs.map +1 -0
- package/dist/components/account/hooks/useChangePasswordForm.mjs +47 -0
- package/dist/components/account/hooks/useChangePasswordForm.mjs.map +1 -0
- package/dist/components/account/hooks/useChangePhoneForm.mjs +64 -0
- package/dist/components/account/hooks/useChangePhoneForm.mjs.map +1 -0
- package/dist/components/account/hooks/useConfigurationsModal.mjs +33 -0
- package/dist/components/account/hooks/useConfigurationsModal.mjs.map +1 -0
- package/dist/components/account/hooks/useDisableTwoFactorAuthForm.mjs +54 -0
- package/dist/components/account/hooks/useDisableTwoFactorAuthForm.mjs.map +1 -0
- package/dist/components/account/hooks/useOtpVerification.mjs +94 -0
- package/dist/components/account/hooks/useOtpVerification.mjs.map +1 -0
- package/dist/components/account/hooks/useTwoFactorAuthForm.mjs +88 -0
- package/dist/components/account/hooks/useTwoFactorAuthForm.mjs.map +1 -0
- package/dist/components/account/sections/ChangeEmailModal.mjs +180 -0
- package/dist/components/account/sections/ChangeEmailModal.mjs.map +1 -0
- package/dist/components/account/sections/ChangePasswordSection.mjs +64 -0
- package/dist/components/account/sections/ChangePasswordSection.mjs.map +1 -0
- package/dist/components/account/sections/ChangePhoneModal.mjs +182 -0
- package/dist/components/account/sections/ChangePhoneModal.mjs.map +1 -0
- package/dist/components/account/sections/MyProfileSection.mjs +312 -0
- package/dist/components/account/sections/MyProfileSection.mjs.map +1 -0
- package/dist/components/account/sections/PreferencesSection.mjs +246 -0
- package/dist/components/account/sections/PreferencesSection.mjs.map +1 -0
- package/dist/components/account/sections/SecuritySection.mjs +147 -0
- package/dist/components/account/sections/SecuritySection.mjs.map +1 -0
- package/dist/components/ui/form/ComboboxField.mjs +130 -0
- package/dist/components/ui/form/ComboboxField.mjs.map +1 -0
- package/dist/components/ui/form/FormField.mjs +103 -0
- package/dist/components/ui/form/FormField.mjs.map +1 -0
- package/dist/components/ui/form/PhoneInput.mjs +78 -0
- package/dist/components/ui/form/PhoneInput.mjs.map +1 -0
- package/dist/components/ui/form/SelectField.mjs +80 -0
- package/dist/components/ui/form/SelectField.mjs.map +1 -0
- package/dist/components/ui/form/SwitchOptionFieldWithIcon.mjs +40 -0
- package/dist/components/ui/form/SwitchOptionFieldWithIcon.mjs.map +1 -0
- package/dist/components/ui/form/TextAreaField.mjs +56 -0
- package/dist/components/ui/form/TextAreaField.mjs.map +1 -0
- package/dist/enums/AccountSectionType.mjs +11 -0
- package/dist/enums/AccountSectionType.mjs.map +1 -0
- package/dist/hooks/useCountdownTimer.mjs +29 -0
- package/dist/hooks/useCountdownTimer.mjs.map +1 -0
- package/dist/hooks/useIsMobile.mjs +18 -0
- package/dist/hooks/useIsMobile.mjs.map +1 -0
- package/dist/hooks/usePasswordVisibility.mjs +13 -0
- package/dist/hooks/usePasswordVisibility.mjs.map +1 -0
- package/dist/index.mjs +80 -0
- package/dist/index.mjs.map +1 -1
- package/dist/modules/accounts/actions/account-management.action.mjs +163 -0
- package/dist/modules/accounts/actions/account-management.action.mjs.map +1 -0
- package/dist/modules/accounts/hooks/useAccountManagement.mjs +64 -0
- package/dist/modules/accounts/hooks/useAccountManagement.mjs.map +1 -0
- package/dist/modules/accounts/services/account.service.mjs +162 -25
- package/dist/modules/accounts/services/account.service.mjs.map +1 -1
- package/dist/modules/accounts/services/two-factor.service.mjs +77 -0
- package/dist/modules/accounts/services/two-factor.service.mjs.map +1 -0
- package/dist/server.mjs +36 -0
- package/dist/server.mjs.map +1 -1
- package/dist/utils/browser/clipboard.mjs +36 -0
- package/dist/utils/browser/clipboard.mjs.map +1 -0
- package/dist/utils/file/index.mjs +9 -0
- package/dist/utils/file/index.mjs.map +1 -0
- package/dist/utils/format/masks.mjs +56 -0
- package/dist/utils/format/masks.mjs.map +1 -0
- package/dist/utils/intl/locales.mjs +12 -0
- package/dist/utils/intl/locales.mjs.map +1 -0
- package/dist/utils/intl/timezones.mjs +41 -0
- package/dist/utils/intl/timezones.mjs.map +1 -0
- package/dist/utils/validators/account.mjs +26 -0
- package/dist/utils/validators/account.mjs.map +1 -0
- package/dist/utils/validators/common.mjs +47 -0
- package/dist/utils/validators/common.mjs.map +1 -0
- package/package.json +5 -1
- package/src/components/account/ConfigurationsMyAccountModal.tsx +147 -0
- package/src/components/account/ConfirmGlobalPreferencesModal.tsx +66 -0
- package/src/components/account/DisableTwoFactorAuthModal.tsx +86 -0
- package/src/components/account/TwoFactorAuthModal.tsx +221 -0
- package/src/components/account/constants.ts +29 -0
- package/src/components/account/hooks/useChangeEmailForm.tsx +56 -0
- package/src/components/account/hooks/useChangePasswordForm.tsx +59 -0
- package/src/components/account/hooks/useChangePhoneForm.tsx +69 -0
- package/src/components/account/hooks/useConfigurationsModal.ts +40 -0
- package/src/components/account/hooks/useDisableTwoFactorAuthForm.tsx +69 -0
- package/src/components/account/hooks/useOtpVerification.tsx +116 -0
- package/src/components/account/hooks/useTwoFactorAuthForm.tsx +114 -0
- package/src/components/account/sections/ChangeEmailModal.tsx +224 -0
- package/src/components/account/sections/ChangePasswordSection.tsx +64 -0
- package/src/components/account/sections/ChangePhoneModal.tsx +229 -0
- package/src/components/account/sections/MyProfileSection.tsx +337 -0
- package/src/components/account/sections/PreferencesSection.tsx +277 -0
- package/src/components/account/sections/SecuritySection.tsx +160 -0
- package/src/components/ui/form/ComboboxField.tsx +154 -0
- package/src/components/ui/form/FormField.tsx +130 -0
- package/src/components/ui/form/PhoneInput.tsx +98 -0
- package/src/components/ui/form/SelectField.tsx +118 -0
- package/src/components/ui/form/SwitchOptionFieldWithIcon.tsx +47 -0
- package/src/components/ui/form/TextAreaField.tsx +70 -0
- package/src/enums/AccountSectionType.ts +6 -0
- package/src/hooks/useCountdownTimer.ts +41 -0
- package/src/hooks/useIsMobile.ts +19 -0
- package/src/hooks/usePasswordVisibility.ts +13 -0
- package/src/index.ts +70 -0
- package/src/modules/accounts/actions/account-management.action.ts +197 -0
- package/src/modules/accounts/hooks/useAccountManagement.ts +79 -0
- package/src/modules/accounts/services/account.service.ts +229 -50
- package/src/modules/accounts/services/two-factor.service.ts +98 -0
- package/src/modules/accounts/types.ts +157 -0
- package/src/server.ts +21 -0
- package/src/utils/browser/clipboard.ts +34 -0
- package/src/utils/file/index.ts +5 -0
- package/src/utils/format/masks.ts +52 -0
- package/src/utils/intl/locales.ts +15 -0
- package/src/utils/intl/timezones.ts +53 -0
- package/src/utils/validators/account.ts +36 -0
- package/src/utils/validators/common.ts +43 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
import { Lock, Wrench } from 'lucide-react';
|
|
5
|
+
import { Badge } from '../../ui/data-display/Badge';
|
|
6
|
+
import { Button } from '../../ui/buttons/Button';
|
|
7
|
+
import { useAuth } from '../../../providers/auth.provider';
|
|
8
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
9
|
+
import { USER_QUERY_KEY } from '../../../modules/auth/hooks/useUserQuery';
|
|
10
|
+
import { AccountSectionType } from '../../../enums/AccountSectionType';
|
|
11
|
+
import TwoFactorAuthModal from '../TwoFactorAuthModal';
|
|
12
|
+
import DisableTwoFactorAuthModal from '../DisableTwoFactorAuthModal';
|
|
13
|
+
|
|
14
|
+
interface SecuritySectionProps {
|
|
15
|
+
setActiveSection: (section: AccountSectionType) => void;
|
|
16
|
+
onClose: () => void;
|
|
17
|
+
hasActiveSubscription?: boolean;
|
|
18
|
+
onDeleteAccount?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function SecuritySection({
|
|
22
|
+
setActiveSection,
|
|
23
|
+
onClose,
|
|
24
|
+
hasActiveSubscription,
|
|
25
|
+
onDeleteAccount,
|
|
26
|
+
}: SecuritySectionProps) {
|
|
27
|
+
const { user } = useAuth();
|
|
28
|
+
const queryClient = useQueryClient();
|
|
29
|
+
|
|
30
|
+
const twoFactorAuth = user?.two_factor_authentication ?? false;
|
|
31
|
+
const [twoFactorOpen, setTwoFactorOpen] = useState(false);
|
|
32
|
+
const [disableTwoFactorOpen, setDisableTwoFactorOpen] = useState(false);
|
|
33
|
+
|
|
34
|
+
const handleToggleTwoFactor = useCallback(() => {
|
|
35
|
+
if (twoFactorAuth) {
|
|
36
|
+
setDisableTwoFactorOpen(true);
|
|
37
|
+
} else {
|
|
38
|
+
setTwoFactorOpen(true);
|
|
39
|
+
}
|
|
40
|
+
}, [twoFactorAuth]);
|
|
41
|
+
|
|
42
|
+
const handleDisableTwoFactorSuccess = useCallback(() => {
|
|
43
|
+
queryClient.invalidateQueries({ queryKey: USER_QUERY_KEY });
|
|
44
|
+
setDisableTwoFactorOpen(false);
|
|
45
|
+
}, [queryClient]);
|
|
46
|
+
|
|
47
|
+
const handleEnableTwoFactorSuccess = useCallback(() => {
|
|
48
|
+
queryClient.invalidateQueries({ queryKey: USER_QUERY_KEY });
|
|
49
|
+
setTwoFactorOpen(false);
|
|
50
|
+
}, [queryClient]);
|
|
51
|
+
|
|
52
|
+
const handleDeleteAccount = useCallback(() => {
|
|
53
|
+
onDeleteAccount?.();
|
|
54
|
+
}, [onDeleteAccount]);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
<div className="h-full overflow-y-auto overscroll-contain px-4 pb-20 lg:p-5 lg:pt-5 flex flex-col gap-5 lg:gap-6 lg:pb-31 lg:overscroll-auto">
|
|
59
|
+
<span className="paragraph-medium-semibold text-gray-950 hidden lg:block">Segurança</span>
|
|
60
|
+
|
|
61
|
+
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
|
62
|
+
<div
|
|
63
|
+
className="flex flex-col lg:flex-row lg:items-center gap-4 lg:gap-5 p-4 lg:p-5 border-b border-gray-200 cursor-pointer hover:bg-gray-25 transition-colors"
|
|
64
|
+
onClick={handleToggleTwoFactor}
|
|
65
|
+
>
|
|
66
|
+
<div className="flex flex-1 items-start lg:items-center gap-4 lg:gap-5">
|
|
67
|
+
<div className="size-10 rounded-lg bg-gray-50 flex items-center justify-center shrink-0">
|
|
68
|
+
<Wrench size={20} className="text-gray-950" />
|
|
69
|
+
</div>
|
|
70
|
+
<div className="flex flex-col gap-1.5 flex-1">
|
|
71
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
72
|
+
<span className="paragraph-small-semibold text-gray-950">
|
|
73
|
+
Autenticação de dois fatores
|
|
74
|
+
</span>
|
|
75
|
+
<Badge
|
|
76
|
+
className={`paragraph-xsmall-semibold rounded-lg ${twoFactorAuth ? 'text-green-600 bg-green-50' : 'text-gray-600 bg-gray-50'}`}
|
|
77
|
+
>
|
|
78
|
+
{twoFactorAuth ? 'Habilitado' : 'Desabilitado'}
|
|
79
|
+
</Badge>
|
|
80
|
+
</div>
|
|
81
|
+
<span className="paragraph-small-regular text-gray-600">
|
|
82
|
+
Envio de código de segurança (e-mail ou sms) para o usuário acessar o sistema
|
|
83
|
+
</span>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<Button
|
|
87
|
+
variant="secondary"
|
|
88
|
+
className="h-10! shrink-0 w-full lg:w-auto"
|
|
89
|
+
onClick={(e) => {
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
handleToggleTwoFactor();
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
{twoFactorAuth ? 'Desabilitar' : 'Habilitar'}
|
|
95
|
+
</Button>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div
|
|
99
|
+
className="flex flex-col lg:flex-row lg:items-center gap-4 lg:gap-5 p-4 lg:p-5 cursor-pointer hover:bg-gray-25 transition-colors"
|
|
100
|
+
onClick={() => setActiveSection(AccountSectionType.CHANGE_PASSWORD)}
|
|
101
|
+
>
|
|
102
|
+
<div className="flex flex-1 items-start lg:items-center gap-4 lg:gap-5">
|
|
103
|
+
<div className="size-10 rounded-lg bg-gray-50 flex items-center justify-center shrink-0">
|
|
104
|
+
<Lock size={20} className="text-gray-950" />
|
|
105
|
+
</div>
|
|
106
|
+
<div className="flex flex-col gap-1.5 flex-1">
|
|
107
|
+
<span className="paragraph-small-semibold text-gray-950">
|
|
108
|
+
Alterar senha de acesso
|
|
109
|
+
</span>
|
|
110
|
+
<span className="paragraph-small-regular text-gray-600">
|
|
111
|
+
Atualize sua senha sempre que necessário para manter sua conta segura.
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<Button
|
|
116
|
+
variant="secondary"
|
|
117
|
+
className="h-10! shrink-0 w-full lg:w-auto"
|
|
118
|
+
onClick={(e) => {
|
|
119
|
+
e.stopPropagation();
|
|
120
|
+
setActiveSection(AccountSectionType.CHANGE_PASSWORD);
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
Alterar senha
|
|
124
|
+
</Button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
{onDeleteAccount && (
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
className="paragraph-small-semibold text-red-600 bg-red-50 rounded-lg px-3 py-2 cursor-pointer hover:bg-red-100 transition-colors w-fit"
|
|
132
|
+
onClick={handleDeleteAccount}
|
|
133
|
+
>
|
|
134
|
+
Excluir minha conta
|
|
135
|
+
</button>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div className="fixed bottom-0 left-0 right-0 lg:absolute flex items-center h-20 px-4 lg:px-5 border-t border-gray-200 justify-between bg-white z-10">
|
|
140
|
+
<Button variant="secondary" className="h-10!" type="button" onClick={onClose}>
|
|
141
|
+
Cancelar
|
|
142
|
+
</Button>
|
|
143
|
+
<Button className="h-10!" onClick={onClose}>
|
|
144
|
+
Salvar alterações
|
|
145
|
+
</Button>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<DisableTwoFactorAuthModal
|
|
149
|
+
open={disableTwoFactorOpen}
|
|
150
|
+
onClose={() => setDisableTwoFactorOpen(false)}
|
|
151
|
+
onSuccess={handleDisableTwoFactorSuccess}
|
|
152
|
+
/>
|
|
153
|
+
<TwoFactorAuthModal
|
|
154
|
+
open={twoFactorOpen}
|
|
155
|
+
onClose={() => setTwoFactorOpen(false)}
|
|
156
|
+
onSuccess={handleEnableTwoFactorSuccess}
|
|
157
|
+
/>
|
|
158
|
+
</>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react';
|
|
4
|
+
import { CheckIcon, ChevronDownIcon, SearchIcon } from 'lucide-react';
|
|
5
|
+
import { Popover, PopoverContent, PopoverTrigger } from '../overlay/Popover';
|
|
6
|
+
import { cn } from '../../../infra/utils/clsx';
|
|
7
|
+
|
|
8
|
+
export type ComboboxOption = {
|
|
9
|
+
value: string;
|
|
10
|
+
label: string;
|
|
11
|
+
displayValue?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
interface ComboboxFieldProps {
|
|
15
|
+
options: ComboboxOption[];
|
|
16
|
+
label?: string;
|
|
17
|
+
placeholder?: string;
|
|
18
|
+
searchPlaceholder?: string;
|
|
19
|
+
emptyMessage?: string;
|
|
20
|
+
value?: string;
|
|
21
|
+
onChange?: (value: string) => void;
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
icon?: React.ReactNode;
|
|
24
|
+
className?: string;
|
|
25
|
+
containerClassName?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function ComboboxField({
|
|
29
|
+
options,
|
|
30
|
+
label,
|
|
31
|
+
placeholder = 'Selecione uma opção',
|
|
32
|
+
searchPlaceholder = 'Buscar...',
|
|
33
|
+
emptyMessage = 'Nenhuma opção encontrada.',
|
|
34
|
+
value,
|
|
35
|
+
onChange,
|
|
36
|
+
disabled = false,
|
|
37
|
+
icon,
|
|
38
|
+
className,
|
|
39
|
+
containerClassName,
|
|
40
|
+
}: ComboboxFieldProps) {
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
42
|
+
const [search, setSearch] = useState('');
|
|
43
|
+
|
|
44
|
+
const selectedOption = useMemo(
|
|
45
|
+
() => options.find((o) => o.value === value),
|
|
46
|
+
[options, value]
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const filteredOptions = useMemo(() => {
|
|
50
|
+
const q = search.trim().toLowerCase();
|
|
51
|
+
if (!q) return options;
|
|
52
|
+
return options.filter((o) => o.label.toLowerCase().includes(q));
|
|
53
|
+
}, [options, search]);
|
|
54
|
+
|
|
55
|
+
const handleSelect = (optionValue: string) => {
|
|
56
|
+
onChange?.(optionValue);
|
|
57
|
+
setOpen(false);
|
|
58
|
+
setSearch('');
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={cn('flex flex-col gap-1.5', containerClassName)}>
|
|
63
|
+
{label && (
|
|
64
|
+
<div className="flex items-center gap-1 paragraph-xsmall-semibold">
|
|
65
|
+
<span className="text-gray-600">{label}</span>
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
|
|
69
|
+
<Popover
|
|
70
|
+
open={open}
|
|
71
|
+
onOpenChange={(v) => {
|
|
72
|
+
if (!disabled) {
|
|
73
|
+
setOpen(v);
|
|
74
|
+
if (!v) setSearch('');
|
|
75
|
+
}
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<PopoverTrigger asChild>
|
|
79
|
+
<button
|
|
80
|
+
type="button"
|
|
81
|
+
disabled={disabled}
|
|
82
|
+
className={cn(
|
|
83
|
+
'paragraph-small-medium w-full h-10 text-gray-950 px-4 transition-colors',
|
|
84
|
+
'flex items-center justify-between gap-2 rounded-md border bg-transparent',
|
|
85
|
+
'border-gray-200 hover:border-gray-400 focus:border-gray-950 focus:outline-none',
|
|
86
|
+
disabled && 'cursor-not-allowed opacity-50',
|
|
87
|
+
className
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
<span className={cn('truncate text-left', !selectedOption && 'text-gray-400')}>
|
|
91
|
+
{selectedOption
|
|
92
|
+
? (selectedOption.displayValue ?? selectedOption.label)
|
|
93
|
+
: placeholder}
|
|
94
|
+
</span>
|
|
95
|
+
{icon ?? (
|
|
96
|
+
<ChevronDownIcon
|
|
97
|
+
className={cn(
|
|
98
|
+
'size-4 opacity-50 shrink-0 transition-all duration-200',
|
|
99
|
+
open && 'rotate-180 opacity-100 text-gray-950'
|
|
100
|
+
)}
|
|
101
|
+
/>
|
|
102
|
+
)}
|
|
103
|
+
</button>
|
|
104
|
+
</PopoverTrigger>
|
|
105
|
+
|
|
106
|
+
<PopoverContent
|
|
107
|
+
className="p-0 overflow-hidden rounded-md border shadow-md"
|
|
108
|
+
align="start"
|
|
109
|
+
sideOffset={4}
|
|
110
|
+
style={{ width: 'var(--radix-popover-trigger-width)' }}
|
|
111
|
+
onWheel={(e) => e.stopPropagation()}
|
|
112
|
+
>
|
|
113
|
+
<div className="flex items-center gap-2 border-b border-gray-100 px-3 h-9">
|
|
114
|
+
<SearchIcon className="size-4 shrink-0 opacity-50 text-gray-500" />
|
|
115
|
+
<input
|
|
116
|
+
autoFocus
|
|
117
|
+
placeholder={searchPlaceholder}
|
|
118
|
+
value={search}
|
|
119
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
120
|
+
className="flex h-full w-full bg-transparent text-sm outline-none placeholder:text-gray-400 paragraph-small-medium text-gray-950"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div className="overflow-y-auto max-h-[280px] p-1">
|
|
125
|
+
{filteredOptions.length === 0 ? (
|
|
126
|
+
<p className="py-6 text-center paragraph-small-medium text-gray-500">{emptyMessage}</p>
|
|
127
|
+
) : (
|
|
128
|
+
filteredOptions.map((option) => {
|
|
129
|
+
const isSelected = value === option.value;
|
|
130
|
+
return (
|
|
131
|
+
<div
|
|
132
|
+
key={option.value}
|
|
133
|
+
className={cn(
|
|
134
|
+
'flex items-center gap-2 rounded-sm px-2 h-9 cursor-pointer select-none',
|
|
135
|
+
'paragraph-small-medium text-gray-950',
|
|
136
|
+
'hover:bg-gray-100',
|
|
137
|
+
isSelected && 'bg-gray-50'
|
|
138
|
+
)}
|
|
139
|
+
onClick={() => handleSelect(option.value)}
|
|
140
|
+
>
|
|
141
|
+
<span className="flex-1 truncate">{option.label}</span>
|
|
142
|
+
{isSelected && <CheckIcon className="size-4 shrink-0 text-pages-600" />}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
})
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
</PopoverContent>
|
|
149
|
+
</Popover>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export default ComboboxField;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { forwardRef, type ComponentPropsWithoutRef } from 'react';
|
|
4
|
+
import { Lock, Eye, EyeOff, type LucideIcon } from 'lucide-react';
|
|
5
|
+
import { Input } from './Input';
|
|
6
|
+
import { cn } from '../../../infra/utils/clsx';
|
|
7
|
+
|
|
8
|
+
type FormFieldSize = 'default' | 'large';
|
|
9
|
+
|
|
10
|
+
interface FormFieldProps extends Omit<ComponentPropsWithoutRef<'input'>, 'size'> {
|
|
11
|
+
label?: string;
|
|
12
|
+
optional?: boolean;
|
|
13
|
+
required?: boolean;
|
|
14
|
+
leftIcon?: LucideIcon;
|
|
15
|
+
rightIcon?: LucideIcon;
|
|
16
|
+
rightIconColor?: string;
|
|
17
|
+
hintMessage?: string;
|
|
18
|
+
classnameContainer?: string;
|
|
19
|
+
size?: FormFieldSize;
|
|
20
|
+
error?: boolean;
|
|
21
|
+
errorMessage?: string;
|
|
22
|
+
showCharCount?: boolean;
|
|
23
|
+
showPassword?: boolean;
|
|
24
|
+
onTogglePassword?: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const FormField = forwardRef<HTMLInputElement, FormFieldProps>(
|
|
28
|
+
(
|
|
29
|
+
{
|
|
30
|
+
label,
|
|
31
|
+
optional = false,
|
|
32
|
+
required = false,
|
|
33
|
+
leftIcon: LeftIcon,
|
|
34
|
+
rightIcon: RightIcon,
|
|
35
|
+
rightIconColor = 'var(--color-gray-400)',
|
|
36
|
+
hintMessage,
|
|
37
|
+
size = 'default',
|
|
38
|
+
type = 'text',
|
|
39
|
+
error = false,
|
|
40
|
+
errorMessage,
|
|
41
|
+
className,
|
|
42
|
+
classnameContainer,
|
|
43
|
+
showCharCount = false,
|
|
44
|
+
showPassword = false,
|
|
45
|
+
onTogglePassword,
|
|
46
|
+
...inputProps
|
|
47
|
+
},
|
|
48
|
+
ref
|
|
49
|
+
) => {
|
|
50
|
+
const isPasswordField = type === 'password';
|
|
51
|
+
const inputType = isPasswordField && showPassword ? 'text' : type;
|
|
52
|
+
const containerHeight = size === 'large' ? 'h-12' : 'h-10';
|
|
53
|
+
|
|
54
|
+
const FinalLeftIcon = isPasswordField ? Lock : LeftIcon;
|
|
55
|
+
const FinalRightIcon = isPasswordField ? (!showPassword ? EyeOff : Eye) : RightIcon;
|
|
56
|
+
|
|
57
|
+
const finalMessage = error && errorMessage ? errorMessage : hintMessage;
|
|
58
|
+
const messageColor = error ? 'text-red-600' : 'text-gray-600';
|
|
59
|
+
|
|
60
|
+
const currentLength = typeof inputProps.value === 'string' ? inputProps.value.length : 0;
|
|
61
|
+
const shouldShowCharCount = showCharCount && inputProps.maxLength;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="flex flex-col gap-1.5 w-full">
|
|
65
|
+
<div className="flex items-center gap-1 paragraph-xsmall-semibold truncate">
|
|
66
|
+
{label && <span className="text-gray-600">{label}</span>}
|
|
67
|
+
{optional && <span className="text-gray-500">(Opcional)</span>}
|
|
68
|
+
{required && <span className="text-red-600">*</span>}
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div
|
|
72
|
+
className={cn(
|
|
73
|
+
'flex gap-2 px-4 items-center border rounded-lg transition-color',
|
|
74
|
+
containerHeight,
|
|
75
|
+
classnameContainer,
|
|
76
|
+
error
|
|
77
|
+
? 'border-red-600 hover:border-red-700'
|
|
78
|
+
: 'border-gray-200 hover:border-gray-400 focus-within:border-gray-950'
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
{FinalLeftIcon && <FinalLeftIcon size={20} className="text-gray-400" />}
|
|
82
|
+
|
|
83
|
+
<Input
|
|
84
|
+
ref={ref}
|
|
85
|
+
type={inputType}
|
|
86
|
+
className={cn(
|
|
87
|
+
'paragraph-small-medium w-full h-full text-gray-950',
|
|
88
|
+
error && 'placeholder:text-red-400',
|
|
89
|
+
className
|
|
90
|
+
)}
|
|
91
|
+
{...inputProps}
|
|
92
|
+
/>
|
|
93
|
+
|
|
94
|
+
{FinalRightIcon && (
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onClick={isPasswordField ? onTogglePassword : undefined}
|
|
98
|
+
className={cn(
|
|
99
|
+
'flex items-center justify-center',
|
|
100
|
+
isPasswordField && 'cursor-pointer hover:opacity-70 transition-opacity'
|
|
101
|
+
)}
|
|
102
|
+
aria-label={
|
|
103
|
+
isPasswordField ? (showPassword ? 'Ocultar senha' : 'Mostrar senha') : undefined
|
|
104
|
+
}
|
|
105
|
+
>
|
|
106
|
+
<FinalRightIcon
|
|
107
|
+
size={20}
|
|
108
|
+
color={isPasswordField ? 'var(--color-gray-950)' : rightIconColor}
|
|
109
|
+
/>
|
|
110
|
+
</button>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{finalMessage && (
|
|
115
|
+
<span className={cn('paragraph-xsmall-medium', messageColor)}>{finalMessage}</span>
|
|
116
|
+
)}
|
|
117
|
+
|
|
118
|
+
{shouldShowCharCount && (
|
|
119
|
+
<span className="paragraph-xsmall-medium text-gray-600">
|
|
120
|
+
{currentLength}/{inputProps.maxLength}
|
|
121
|
+
</span>
|
|
122
|
+
)}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
FormField.displayName = 'FormField';
|
|
129
|
+
|
|
130
|
+
export default FormField;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ComponentPropsWithoutRef, forwardRef } from 'react';
|
|
4
|
+
import Image from 'next/image';
|
|
5
|
+
import { Input } from './Input';
|
|
6
|
+
import { Separator } from '../data-display/Separator';
|
|
7
|
+
import { cn } from '../../../infra/utils/clsx';
|
|
8
|
+
|
|
9
|
+
interface PhoneInputProps extends Omit<ComponentPropsWithoutRef<'input'>, 'type'> {
|
|
10
|
+
label?: string;
|
|
11
|
+
optional?: boolean;
|
|
12
|
+
required?: boolean;
|
|
13
|
+
hintMessage?: string;
|
|
14
|
+
error?: boolean;
|
|
15
|
+
errorMessage?: string;
|
|
16
|
+
countryCode?: string;
|
|
17
|
+
countryFlagSrc?: string;
|
|
18
|
+
countryFlagAlt?: string;
|
|
19
|
+
classnameContainer?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
23
|
+
(
|
|
24
|
+
{
|
|
25
|
+
label,
|
|
26
|
+
optional = false,
|
|
27
|
+
required = false,
|
|
28
|
+
hintMessage,
|
|
29
|
+
error = false,
|
|
30
|
+
errorMessage,
|
|
31
|
+
countryCode = '+55',
|
|
32
|
+
countryFlagSrc = '/icons/brazil-flag.svg',
|
|
33
|
+
countryFlagAlt = 'Brazil flag',
|
|
34
|
+
className,
|
|
35
|
+
classnameContainer,
|
|
36
|
+
...props
|
|
37
|
+
},
|
|
38
|
+
ref
|
|
39
|
+
) => {
|
|
40
|
+
const finalMessage = error && errorMessage ? errorMessage : hintMessage;
|
|
41
|
+
const showMessage = finalMessage && finalMessage.trim() !== '';
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className={cn('flex flex-col gap-1.5')}>
|
|
45
|
+
{label && (
|
|
46
|
+
<div className="flex items-center gap-1 paragraph-xsmall-semibold">
|
|
47
|
+
<span className="text-gray-600">{label}</span>
|
|
48
|
+
{optional && <span className="text-gray-500">(Opcional)</span>}
|
|
49
|
+
{required && <span className="text-red-600">*</span>}
|
|
50
|
+
</div>
|
|
51
|
+
)}
|
|
52
|
+
|
|
53
|
+
<div
|
|
54
|
+
className={cn(
|
|
55
|
+
'flex gap-2 items-center border rounded-lg h-10 transition-colors',
|
|
56
|
+
error
|
|
57
|
+
? 'border-red-600 hover:border-red-700'
|
|
58
|
+
: 'border-gray-200 hover:border-gray-400 focus-within:border-gray-950',
|
|
59
|
+
classnameContainer
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
<div className="flex items-center gap-2 px-4">
|
|
63
|
+
<Image
|
|
64
|
+
src={countryFlagSrc}
|
|
65
|
+
alt={countryFlagAlt}
|
|
66
|
+
width={20}
|
|
67
|
+
height={20}
|
|
68
|
+
className="h-5! w-5! rounded-full"
|
|
69
|
+
/>
|
|
70
|
+
<span className="paragraph-small-regular text-gray-950">{countryCode}</span>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<Separator orientation="vertical" />
|
|
74
|
+
|
|
75
|
+
<Input
|
|
76
|
+
ref={ref}
|
|
77
|
+
type="tel"
|
|
78
|
+
className={cn(
|
|
79
|
+
'paragraph-small-medium w-full h-full text-gray-950 border-0 focus-visible:ring-0 focus-visible:ring-offset-0',
|
|
80
|
+
className
|
|
81
|
+
)}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{showMessage && (
|
|
87
|
+
<span className={cn('paragraph-xsmall-medium', error ? 'text-red-600' : 'text-gray-600')}>
|
|
88
|
+
{finalMessage}
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
PhoneInput.displayName = 'PhoneInput';
|
|
97
|
+
|
|
98
|
+
export default PhoneInput;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import {
|
|
5
|
+
Select,
|
|
6
|
+
SelectContent,
|
|
7
|
+
SelectItem,
|
|
8
|
+
SelectTrigger,
|
|
9
|
+
SelectValue,
|
|
10
|
+
} from './Select';
|
|
11
|
+
import { cn } from '../../../infra/utils/clsx';
|
|
12
|
+
|
|
13
|
+
type SelectOption = {
|
|
14
|
+
value: string | number | boolean;
|
|
15
|
+
label: React.ReactNode;
|
|
16
|
+
displayValue?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface SelectFieldProps {
|
|
20
|
+
options: SelectOption[];
|
|
21
|
+
label?: string;
|
|
22
|
+
optional?: boolean;
|
|
23
|
+
required?: boolean;
|
|
24
|
+
hintMessage?: string;
|
|
25
|
+
error?: boolean;
|
|
26
|
+
errorMessage?: string;
|
|
27
|
+
placeholder?: string;
|
|
28
|
+
value?: string | number | boolean;
|
|
29
|
+
defaultValue?: string | number | boolean;
|
|
30
|
+
onChange?: (value: string | number | boolean) => void;
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
icon?: React.ReactNode;
|
|
33
|
+
className?: string;
|
|
34
|
+
containerClassName?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function SelectField({
|
|
38
|
+
label,
|
|
39
|
+
optional = false,
|
|
40
|
+
required = false,
|
|
41
|
+
hintMessage,
|
|
42
|
+
error = false,
|
|
43
|
+
errorMessage,
|
|
44
|
+
placeholder = 'Selecione uma opção',
|
|
45
|
+
options,
|
|
46
|
+
value,
|
|
47
|
+
defaultValue,
|
|
48
|
+
onChange,
|
|
49
|
+
disabled = false,
|
|
50
|
+
icon,
|
|
51
|
+
className,
|
|
52
|
+
containerClassName,
|
|
53
|
+
}: SelectFieldProps) {
|
|
54
|
+
const handleValueChange = (selectedValue: string) => {
|
|
55
|
+
if (!onChange) return;
|
|
56
|
+
const selectedOption = options.find((option) => String(option.value) === selectedValue);
|
|
57
|
+
if (selectedOption) {
|
|
58
|
+
onChange(selectedOption.value);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const finalMessage = error && errorMessage ? errorMessage : hintMessage;
|
|
63
|
+
const messageColor = error ? 'text-red-600' : 'text-gray-600';
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div className={cn('flex flex-col gap-1.5', containerClassName)}>
|
|
67
|
+
{label && (
|
|
68
|
+
<div className="flex items-center gap-1 paragraph-xsmall-semibold">
|
|
69
|
+
<span className="text-gray-600">{label}</span>
|
|
70
|
+
{optional && <span className="text-gray-500">(Opcional)</span>}
|
|
71
|
+
{required && <span className="text-red-600">*</span>}
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
<Select
|
|
76
|
+
value={value !== undefined ? String(value) : undefined}
|
|
77
|
+
defaultValue={defaultValue !== undefined ? String(defaultValue) : undefined}
|
|
78
|
+
onValueChange={handleValueChange}
|
|
79
|
+
disabled={disabled}
|
|
80
|
+
>
|
|
81
|
+
<SelectTrigger
|
|
82
|
+
className={cn(
|
|
83
|
+
'paragraph-small-medium w-full h-10 text-gray-950 px-4 transition-colors',
|
|
84
|
+
error
|
|
85
|
+
? 'border-red-600 hover:border-red-700 focus:border-red-600'
|
|
86
|
+
: 'border-gray-200 hover:border-gray-400 focus-within:border-gray-950 focus:border-gray-950',
|
|
87
|
+
disabled && 'cursor-not-allowed opacity-50',
|
|
88
|
+
className
|
|
89
|
+
)}
|
|
90
|
+
icon={icon}
|
|
91
|
+
>
|
|
92
|
+
<SelectValue placeholder={placeholder}>
|
|
93
|
+
{value !== undefined &&
|
|
94
|
+
(() => {
|
|
95
|
+
const selectedOption = options.find((opt) => String(opt.value) === String(value));
|
|
96
|
+
return selectedOption?.displayValue || selectedOption?.label;
|
|
97
|
+
})()}
|
|
98
|
+
</SelectValue>
|
|
99
|
+
</SelectTrigger>
|
|
100
|
+
<SelectContent sideOffset={4}>
|
|
101
|
+
{options.map((option) => (
|
|
102
|
+
<SelectItem key={String(option.value)} value={String(option.value)}>
|
|
103
|
+
{option.label}
|
|
104
|
+
</SelectItem>
|
|
105
|
+
))}
|
|
106
|
+
</SelectContent>
|
|
107
|
+
</Select>
|
|
108
|
+
|
|
109
|
+
{finalMessage && (
|
|
110
|
+
<span className={cn('paragraph-xsmall-medium', messageColor)}>{finalMessage}</span>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
SelectField.displayName = 'SelectField';
|
|
117
|
+
|
|
118
|
+
export default SelectField;
|