@greatapps/common 1.1.23 → 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.
Files changed (127) hide show
  1. package/dist/components/account/ConfigurationsMyAccountModal.mjs +121 -0
  2. package/dist/components/account/ConfigurationsMyAccountModal.mjs.map +1 -0
  3. package/dist/components/account/ConfirmGlobalPreferencesModal.mjs +45 -0
  4. package/dist/components/account/ConfirmGlobalPreferencesModal.mjs.map +1 -0
  5. package/dist/components/account/DisableTwoFactorAuthModal.mjs +67 -0
  6. package/dist/components/account/DisableTwoFactorAuthModal.mjs.map +1 -0
  7. package/dist/components/account/TwoFactorAuthModal.mjs +190 -0
  8. package/dist/components/account/TwoFactorAuthModal.mjs.map +1 -0
  9. package/dist/components/account/constants.mjs +32 -0
  10. package/dist/components/account/constants.mjs.map +1 -0
  11. package/dist/components/account/hooks/useChangeEmailForm.mjs +53 -0
  12. package/dist/components/account/hooks/useChangeEmailForm.mjs.map +1 -0
  13. package/dist/components/account/hooks/useChangePasswordForm.mjs +47 -0
  14. package/dist/components/account/hooks/useChangePasswordForm.mjs.map +1 -0
  15. package/dist/components/account/hooks/useChangePhoneForm.mjs +64 -0
  16. package/dist/components/account/hooks/useChangePhoneForm.mjs.map +1 -0
  17. package/dist/components/account/hooks/useConfigurationsModal.mjs +33 -0
  18. package/dist/components/account/hooks/useConfigurationsModal.mjs.map +1 -0
  19. package/dist/components/account/hooks/useDisableTwoFactorAuthForm.mjs +54 -0
  20. package/dist/components/account/hooks/useDisableTwoFactorAuthForm.mjs.map +1 -0
  21. package/dist/components/account/hooks/useOtpVerification.mjs +94 -0
  22. package/dist/components/account/hooks/useOtpVerification.mjs.map +1 -0
  23. package/dist/components/account/hooks/useTwoFactorAuthForm.mjs +88 -0
  24. package/dist/components/account/hooks/useTwoFactorAuthForm.mjs.map +1 -0
  25. package/dist/components/account/sections/ChangeEmailModal.mjs +180 -0
  26. package/dist/components/account/sections/ChangeEmailModal.mjs.map +1 -0
  27. package/dist/components/account/sections/ChangePasswordSection.mjs +64 -0
  28. package/dist/components/account/sections/ChangePasswordSection.mjs.map +1 -0
  29. package/dist/components/account/sections/ChangePhoneModal.mjs +182 -0
  30. package/dist/components/account/sections/ChangePhoneModal.mjs.map +1 -0
  31. package/dist/components/account/sections/MyProfileSection.mjs +312 -0
  32. package/dist/components/account/sections/MyProfileSection.mjs.map +1 -0
  33. package/dist/components/account/sections/PreferencesSection.mjs +246 -0
  34. package/dist/components/account/sections/PreferencesSection.mjs.map +1 -0
  35. package/dist/components/account/sections/SecuritySection.mjs +147 -0
  36. package/dist/components/account/sections/SecuritySection.mjs.map +1 -0
  37. package/dist/components/ui/form/ComboboxField.mjs +130 -0
  38. package/dist/components/ui/form/ComboboxField.mjs.map +1 -0
  39. package/dist/components/ui/form/FormField.mjs +103 -0
  40. package/dist/components/ui/form/FormField.mjs.map +1 -0
  41. package/dist/components/ui/form/PhoneInput.mjs +78 -0
  42. package/dist/components/ui/form/PhoneInput.mjs.map +1 -0
  43. package/dist/components/ui/form/Select.mjs.map +1 -1
  44. package/dist/components/ui/form/SelectField.mjs +80 -0
  45. package/dist/components/ui/form/SelectField.mjs.map +1 -0
  46. package/dist/components/ui/form/SwitchOptionFieldWithIcon.mjs +40 -0
  47. package/dist/components/ui/form/SwitchOptionFieldWithIcon.mjs.map +1 -0
  48. package/dist/components/ui/form/TextAreaField.mjs +56 -0
  49. package/dist/components/ui/form/TextAreaField.mjs.map +1 -0
  50. package/dist/enums/AccountSectionType.mjs +11 -0
  51. package/dist/enums/AccountSectionType.mjs.map +1 -0
  52. package/dist/hooks/useCountdownTimer.mjs +29 -0
  53. package/dist/hooks/useCountdownTimer.mjs.map +1 -0
  54. package/dist/hooks/useIsMobile.mjs +18 -0
  55. package/dist/hooks/useIsMobile.mjs.map +1 -0
  56. package/dist/hooks/usePasswordVisibility.mjs +13 -0
  57. package/dist/hooks/usePasswordVisibility.mjs.map +1 -0
  58. package/dist/index.mjs +80 -0
  59. package/dist/index.mjs.map +1 -1
  60. package/dist/modules/accounts/actions/account-management.action.mjs +163 -0
  61. package/dist/modules/accounts/actions/account-management.action.mjs.map +1 -0
  62. package/dist/modules/accounts/hooks/useAccountManagement.mjs +64 -0
  63. package/dist/modules/accounts/hooks/useAccountManagement.mjs.map +1 -0
  64. package/dist/modules/accounts/services/account.service.mjs +162 -25
  65. package/dist/modules/accounts/services/account.service.mjs.map +1 -1
  66. package/dist/modules/accounts/services/two-factor.service.mjs +77 -0
  67. package/dist/modules/accounts/services/two-factor.service.mjs.map +1 -0
  68. package/dist/server.mjs +36 -0
  69. package/dist/server.mjs.map +1 -1
  70. package/dist/utils/browser/clipboard.mjs +36 -0
  71. package/dist/utils/browser/clipboard.mjs.map +1 -0
  72. package/dist/utils/file/index.mjs +9 -0
  73. package/dist/utils/file/index.mjs.map +1 -0
  74. package/dist/utils/format/masks.mjs +56 -0
  75. package/dist/utils/format/masks.mjs.map +1 -0
  76. package/dist/utils/intl/locales.mjs +12 -0
  77. package/dist/utils/intl/locales.mjs.map +1 -0
  78. package/dist/utils/intl/timezones.mjs +41 -0
  79. package/dist/utils/intl/timezones.mjs.map +1 -0
  80. package/dist/utils/validators/account.mjs +26 -0
  81. package/dist/utils/validators/account.mjs.map +1 -0
  82. package/dist/utils/validators/common.mjs +47 -0
  83. package/dist/utils/validators/common.mjs.map +1 -0
  84. package/package.json +6 -1
  85. package/src/components/account/ConfigurationsMyAccountModal.tsx +147 -0
  86. package/src/components/account/ConfirmGlobalPreferencesModal.tsx +66 -0
  87. package/src/components/account/DisableTwoFactorAuthModal.tsx +86 -0
  88. package/src/components/account/TwoFactorAuthModal.tsx +221 -0
  89. package/src/components/account/constants.ts +29 -0
  90. package/src/components/account/hooks/useChangeEmailForm.tsx +56 -0
  91. package/src/components/account/hooks/useChangePasswordForm.tsx +59 -0
  92. package/src/components/account/hooks/useChangePhoneForm.tsx +69 -0
  93. package/src/components/account/hooks/useConfigurationsModal.ts +40 -0
  94. package/src/components/account/hooks/useDisableTwoFactorAuthForm.tsx +69 -0
  95. package/src/components/account/hooks/useOtpVerification.tsx +116 -0
  96. package/src/components/account/hooks/useTwoFactorAuthForm.tsx +114 -0
  97. package/src/components/account/sections/ChangeEmailModal.tsx +224 -0
  98. package/src/components/account/sections/ChangePasswordSection.tsx +64 -0
  99. package/src/components/account/sections/ChangePhoneModal.tsx +229 -0
  100. package/src/components/account/sections/MyProfileSection.tsx +337 -0
  101. package/src/components/account/sections/PreferencesSection.tsx +277 -0
  102. package/src/components/account/sections/SecuritySection.tsx +160 -0
  103. package/src/components/ui/form/ComboboxField.tsx +154 -0
  104. package/src/components/ui/form/FormField.tsx +130 -0
  105. package/src/components/ui/form/PhoneInput.tsx +98 -0
  106. package/src/components/ui/form/Select.tsx +1 -0
  107. package/src/components/ui/form/SelectField.tsx +118 -0
  108. package/src/components/ui/form/SwitchOptionFieldWithIcon.tsx +47 -0
  109. package/src/components/ui/form/TextAreaField.tsx +70 -0
  110. package/src/enums/AccountSectionType.ts +6 -0
  111. package/src/hooks/useCountdownTimer.ts +41 -0
  112. package/src/hooks/useIsMobile.ts +19 -0
  113. package/src/hooks/usePasswordVisibility.ts +13 -0
  114. package/src/index.ts +70 -0
  115. package/src/modules/accounts/actions/account-management.action.ts +197 -0
  116. package/src/modules/accounts/hooks/useAccountManagement.ts +79 -0
  117. package/src/modules/accounts/services/account.service.ts +229 -50
  118. package/src/modules/accounts/services/two-factor.service.ts +98 -0
  119. package/src/modules/accounts/types.ts +157 -0
  120. package/src/server.ts +21 -0
  121. package/src/utils/browser/clipboard.ts +34 -0
  122. package/src/utils/file/index.ts +5 -0
  123. package/src/utils/format/masks.ts +52 -0
  124. package/src/utils/intl/locales.ts +15 -0
  125. package/src/utils/intl/timezones.ts +53 -0
  126. package/src/utils/validators/account.ts +36 -0
  127. 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;
@@ -31,6 +31,7 @@ function SelectTrigger({
31
31
  ...props
32
32
  }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
33
33
  size?: "sm" | "default";
34
+ icon?: React.ReactNode;
34
35
  }) {
35
36
  return (
36
37
  <SelectPrimitive.Trigger
@@ -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;