mitre-form-component 0.0.1

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.
@@ -0,0 +1,215 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from "react";
4
+ import { useError } from "../hooks/useError";
5
+ import { useForm, SubmitHandler } from "react-hook-form";
6
+ import { yupResolver } from "@hookform/resolvers/yup";
7
+ import * as yup from "yup";
8
+
9
+ import {
10
+ FormContainer,
11
+ HeaderContainer,
12
+ ButtonContainer,
13
+ Form,
14
+ Title,
15
+ Text
16
+ } from "./styles";
17
+ import FontLoader, { GlobalStyles } from "../styles/global";
18
+
19
+ import { Input } from "../Input";
20
+ import { Button } from "../Button";
21
+ import { Alert } from "../Alert";
22
+
23
+ export interface MitreFormComponentProps {
24
+ productId: string;
25
+ apiUrl: string;
26
+ apiToken: string;
27
+ utm_source: string;
28
+ utm_medium: string;
29
+ utm_campaign: string;
30
+ utm_term: string;
31
+ showHeader?: boolean;
32
+ colorPrimary?: string;
33
+ textColor?: string;
34
+ }
35
+
36
+ const schema = yup.object().shape({
37
+ name: yup.string().required("Nome é obrigatório"),
38
+ email: yup.string().required("Email é obrigatório").email("Email inválido"),
39
+ phone: yup.string().required("Telefone é obrigatório")
40
+ .test(
41
+ 'min-digits',
42
+ 'Número de telefone inválido!',
43
+ (value) => {
44
+ const digitsOnly = value?.replace(/\D/g, '') || '';
45
+ //TODO melhorar essa lógica com algum regex
46
+ return digitsOnly.length >= 8
47
+ })
48
+ });
49
+
50
+ const MitreFormComponent = React.forwardRef<HTMLDivElement, MitreFormComponentProps>(({
51
+ productId,
52
+ apiUrl,
53
+ apiToken,
54
+ utm_source,
55
+ utm_medium,
56
+ utm_campaign,
57
+ utm_term,
58
+ showHeader = true,
59
+ colorPrimary = "#F6C76B",
60
+ textColor = "#2F2F2F",
61
+ }, ref) => {
62
+ const [loading, setIsLoading] = useState(false);
63
+ const { error, handleError, clearError } = useError();
64
+ const [successMessage, setSuccessMessage] = useState('');
65
+
66
+ const { register, handleSubmit, formState: { errors }, reset, watch } = useForm({
67
+ resolver: yupResolver(schema),
68
+ });
69
+
70
+ const phoneValue = watch("phone");
71
+
72
+ const sendMessage: SubmitHandler<{ name: string; email: string; phone: string; }> = async (data) => {
73
+ const { name, email, phone } = data;
74
+ const message = "Gostaria de mais informações sobre o produto";
75
+
76
+ try {
77
+ setIsLoading(true);
78
+
79
+ if (!productId || !utm_source || !utm_medium || !utm_campaign || !utm_term || !apiToken) {
80
+ throw new Error("Parâmetros obrigatórios não informados");
81
+ }
82
+
83
+ const response = await fetch(`${apiUrl}/leads`, {
84
+ method: "POST",
85
+ headers: {
86
+ "Content-Type": "application/json",
87
+ Authorization: `Basic ${apiToken}`,
88
+ },
89
+ body: JSON.stringify({
90
+ name,
91
+ email,
92
+ phone,
93
+ message,
94
+ productId,
95
+ utm_source,
96
+ utm_medium,
97
+ utm_campaign,
98
+ utm_term,
99
+ }),
100
+ });
101
+
102
+ if (!response.ok) {
103
+ throw new Error("Falha ao enviar a mensagem!");
104
+ }
105
+
106
+ setSuccessMessage("Mensagem enviada com sucesso!");
107
+ reset();
108
+ } catch (err) {
109
+ handleError(err);
110
+ } finally {
111
+ setIsLoading(false);
112
+ }
113
+ };
114
+
115
+ return (
116
+ <>
117
+ <FontLoader />
118
+ <GlobalStyles />
119
+
120
+ {error && (
121
+ <Alert
122
+ type="error"
123
+ dismissible
124
+ onDismiss={clearError}
125
+ autoDismiss={5000}
126
+ >
127
+ {error!.message}
128
+ </Alert>
129
+ )}
130
+
131
+ {successMessage && (
132
+ <Alert
133
+ type="success"
134
+ dismissible
135
+ onDismiss={() => setSuccessMessage('')}
136
+ autoDismiss={5000}
137
+ >
138
+ {successMessage}
139
+ </Alert>
140
+ )}
141
+
142
+ <FormContainer ref={ref} >
143
+ {showHeader &&
144
+ <HeaderContainer>
145
+ <Title $textColor={textColor}>Atendimento por mensagem</Title>
146
+
147
+ <Text $textColor={textColor}>Informe seus dados e retornaremos a mensagem.</Text>
148
+ </HeaderContainer>
149
+ }
150
+
151
+ <Form $textColor={textColor} onSubmit={handleSubmit(sendMessage)} noValidate>
152
+ <Input
153
+ id="name"
154
+ label="Nome *"
155
+ placeholder="Digite seu nome"
156
+ {...register("name")}
157
+ borderColor={colorPrimary}
158
+ textColor={textColor}
159
+ error={errors.name?.message}
160
+ autoComplete="name"
161
+ required
162
+ />
163
+
164
+ <Input
165
+ id="email"
166
+ label="Email *"
167
+ type="email"
168
+ placeholder="exemplo@email.com"
169
+ {...register("email")}
170
+ borderColor={colorPrimary}
171
+ textColor={textColor}
172
+ error={errors.email?.message}
173
+ autoComplete="email"
174
+ required
175
+ />
176
+
177
+ <Input
178
+ id="phone"
179
+ label="Telefone *"
180
+ placeholder="(11) 00000-0000"
181
+ mask="phone"
182
+ {...register("phone")}
183
+ borderColor={colorPrimary}
184
+ textColor={textColor}
185
+ error={errors.phone?.message}
186
+ required
187
+ value={phoneValue}
188
+ />
189
+
190
+ <h6>* Campos de preenchimento obrigatório.</h6>
191
+
192
+ <ButtonContainer>
193
+ <Button bgColor={colorPrimary} color={textColor} type="submit" isSubmitting={loading}>
194
+ Enviar mensagem
195
+ </Button>
196
+ </ButtonContainer>
197
+
198
+ <p>A Mitre Realty respeita a sua privacidade e utiliza os seus dados pessoais para contatá-lo por e-mail ou telefone aqui registrados. Para saber mais, acesse a nossa{ ' '}
199
+ <a
200
+ href="https://www.mitrerealty.com.br/politica-de-privacidade"
201
+ target="_blank"
202
+ rel="noopener noreferrer"
203
+ >
204
+ Política de Privacidade
205
+ </a>. Ao clicar em {'"'}enviar{'"'}, você concorda em permitir que a Mitre Realty, armazene e processe os dados pessoais fornecidos por você para finalidade informada</p>
206
+
207
+ </Form>
208
+ </FormContainer>
209
+ </>
210
+ );
211
+ });
212
+
213
+ MitreFormComponent.displayName = "MitreFormComponent";
214
+
215
+ export default MitreFormComponent;
@@ -0,0 +1,99 @@
1
+ import { flex, opacityEffect } from "../styles/utils";
2
+ import styled from "styled-components";
3
+
4
+ export const FormContainer = styled.div`
5
+ ${flex("column")}
6
+ align-items: stretch;
7
+ justify-content: flex-start;
8
+ overflow-x: hidden;
9
+ overflow-y: auto;
10
+
11
+ /* Hide scrollbars for WebKit browsers */
12
+ ::-webkit-scrollbar {
13
+ display: none;
14
+ }
15
+
16
+ /* Hide scrollbars for Firefox */
17
+ scrollbar-width: none;
18
+
19
+ box-sizing: border-box;
20
+ `;
21
+
22
+ export const HeaderContainer = styled.div`
23
+ margin-bottom: 1rem;
24
+ `;
25
+
26
+ export const ButtonContainer = styled.div`
27
+ display: flex;
28
+ flex-direction: column;
29
+ align-items: center;
30
+ justify-content: center;
31
+ width: 100%;
32
+ margin-top: 0.75rem;
33
+ `;
34
+
35
+ export const Form = styled.form<{ $textColor: string }>`
36
+ label {
37
+ font-weight: 700;
38
+ }
39
+
40
+ input {
41
+ background: white;
42
+ margin-bottom: 0.75rem;
43
+ }
44
+
45
+ p {
46
+ font-family: "Montserrat", sans-serif;
47
+ font-style: italic;
48
+ font-weight: 200;
49
+ font-size: 0.8rem;
50
+ color: ${(props) => props.$textColor || "var(--black)"};
51
+ text-align: start;
52
+ }
53
+
54
+ a {
55
+ font-family: "Montserrat", sans-serif;
56
+ font-style: italic;
57
+ font-weight: 200;
58
+ font-size: 0.8rem;
59
+ color: ${(props) => props.$textColor || "var(--black)"};
60
+ }
61
+
62
+ h6 {
63
+ text-align: start;
64
+ margin-left: 10px;
65
+ color: ${(props) => props.$textColor || "var(--black)"};
66
+ }
67
+
68
+ & > div {
69
+ margin-bottom: 10px;,
70
+ }
71
+
72
+ button {
73
+ ${opacityEffect}
74
+ color: var(--black);
75
+ font-weight: 600;
76
+ border: none;
77
+ border-radius: 8px;
78
+ width: 60%;
79
+ margin-top: 10px;
80
+ margin-bottom: 10px;
81
+ }
82
+ `;
83
+
84
+ export const Title = styled.h2<{ $textColor: string }>`
85
+ font-size: 1.25rem;
86
+ font-weight: 700;
87
+ line-height: 24px;
88
+ letter-spacing: 0em;
89
+ color: ${(props) => props.$textColor || "var(--black)"};
90
+ `;
91
+
92
+ export const Text = styled.p<{ $textColor: string }>`
93
+ font-size: 1rem;
94
+ font-weight: 400;
95
+ line-height: 23px;
96
+ letter-spacing: 0em;
97
+ margin-top: 10px;
98
+ color: ${(props) => props.$textColor || "var(--black)"};
99
+ `;
@@ -0,0 +1,132 @@
1
+ import {
2
+ FormEvent,
3
+ forwardRef,
4
+ ForwardRefRenderFunction,
5
+ InputHTMLAttributes,
6
+ useCallback,
7
+ useRef,
8
+ } from "react";
9
+ import { FieldError } from "react-hook-form";
10
+ import { cep, cpf, currency, date } from "./masks";
11
+
12
+ import 'react-phone-input-2/lib/style.css'
13
+
14
+ import {
15
+ FormControl,
16
+ FormErrorMessage,
17
+ FormLabel,
18
+ Input as FormInput,
19
+ FormPhoneInput
20
+ } from "./styles";
21
+
22
+ type InputType =
23
+ | "text"
24
+ | "email"
25
+ | "password"
26
+ | "number"
27
+ | "tel"
28
+ | "url"
29
+ | "date"
30
+ | "time"
31
+ | "datetime-local";
32
+
33
+ interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
34
+ id: string;
35
+ label?: string;
36
+ error?: string | FieldError;
37
+ showErrorMessage?: boolean;
38
+ borderColor: string;
39
+ textColor?: string;
40
+
41
+ mask?: "cep" | "currency" | "cpf" | "phone" | "date";
42
+ type?: InputType;
43
+ }
44
+
45
+ const InputBase: ForwardRefRenderFunction<HTMLInputElement, InputProps> = (
46
+ { id, label, error, showErrorMessage = true, borderColor, textColor, mask = "", type = "text", ...rest },
47
+ ref
48
+ ) => {
49
+ const phoneInputRef = useRef<{ input: HTMLInputElement }>(null);
50
+ const { onChange, name } = rest;
51
+
52
+ const handleKeyUp = useCallback(
53
+ (e: FormEvent<HTMLInputElement>) => {
54
+ if (mask === "cep") cep(e);
55
+ if (mask === "currency") currency(e);
56
+ if (mask === "cpf") cpf(e);
57
+ if (mask === "date") date(e);
58
+ },
59
+ [mask]
60
+ );
61
+
62
+ const handlePhoneChange = useCallback((value: string) => {
63
+ onChange?.({ target: { value, name } } as React.ChangeEvent<HTMLInputElement>);
64
+
65
+ if (phoneInputRef.current?.input) {
66
+ phoneInputRef.current.input.value = value;
67
+ }
68
+ }, [onChange, name]);
69
+
70
+ return (
71
+ <FormControl isInvalid={!!error}>
72
+ {!!label && <FormLabel htmlFor={id} $textColor={textColor}>{label}</FormLabel>}
73
+
74
+ {!mask ? (
75
+ <FormInput
76
+ id={id}
77
+ ref={ref}
78
+ type={type}
79
+ $bordercolor={borderColor}
80
+ aria-invalid={!!error && showErrorMessage ? "true" : "false"}
81
+ autoComplete={rest.autoComplete || "on"}
82
+ {...rest}
83
+ />
84
+ ) : mask === 'phone' ? (
85
+ <FormPhoneInput
86
+ country={"br"}
87
+ $bordercolor={borderColor}
88
+ placeholder={rest.placeholder}
89
+ aria-invalid={!!error && showErrorMessage ? "true" : "false"}
90
+ isInvalid={!!error}
91
+ onChange={handlePhoneChange}
92
+ masks={{
93
+ br: "(..) .....-....",}}
94
+ inputProps={{
95
+ id,
96
+ name: 'phone',
97
+ required: true,
98
+ autoFocus: true,
99
+ autoComplete: "tel",
100
+ ref: phoneInputRef,
101
+ }}
102
+ //TODO no futuro enviar com o ddi, só retirar o disableCountryCode e disableCountryGuess
103
+ dropdownStyle={{
104
+ color: textColor,
105
+ }}
106
+ disableCountryGuess={true}
107
+ disableCountryCode={true}
108
+ value={rest.value as string}
109
+ />
110
+ ) : (
111
+ <FormInput
112
+ id={id}
113
+ ref={ref}
114
+ type={type}
115
+ $bordercolor={borderColor}
116
+ aria-invalid={!!error && showErrorMessage ? "true" : "false"}
117
+ onKeyUp={handleKeyUp}
118
+ autoComplete={rest.autoComplete || "on"}
119
+ {...rest}
120
+ />
121
+ )}
122
+
123
+ {!!error && showErrorMessage && (
124
+ <FormErrorMessage data-testid="error-message">
125
+ {typeof error === 'string' ? error : error.message}
126
+ </FormErrorMessage>
127
+ )}
128
+ </FormControl>
129
+ );
130
+ };
131
+
132
+ export const Input = forwardRef(InputBase);
@@ -0,0 +1,52 @@
1
+ import { FormEvent } from "react";
2
+
3
+ export function cep(e: FormEvent<HTMLInputElement>) {
4
+ e.currentTarget.maxLength = 9;
5
+ let value = e.currentTarget.value;
6
+ value = value.replace(/\D/g, "");
7
+ value = value.replace(/^(\d{5})(\d)/, "$1-$2");
8
+ e.currentTarget.value = value;
9
+ return e;
10
+ }
11
+
12
+ export function currency(e: FormEvent<HTMLInputElement>) {
13
+ let value = e.currentTarget.value;
14
+ value = value.replace(/\D/g, "");
15
+ value = value.replace(/(\d)(\d{2})$/, "$1,$2");
16
+ value = value.replace(/(?=(\d{3})+(\D))\B/g, ".");
17
+
18
+ e.currentTarget.value = value;
19
+ return e;
20
+ }
21
+
22
+ export function cpf(e: FormEvent<HTMLInputElement>) {
23
+ e.currentTarget.maxLength = 14;
24
+ let value = e.currentTarget.value;
25
+ if (!value.match(/^(\d{3}).(\d{3}).(\d{3})-(\d{2})$/)) {
26
+ value = value.replace(/\D/g, "");
27
+ value = value.replace(/(\d{3})(\d)/, "$1.$2");
28
+ value = value.replace(/(\d{3})(\d)/, "$1.$2");
29
+ value = value.replace(/(\d{3})(\d{2})$/, "$1-$2");
30
+
31
+ e.currentTarget.value = value;
32
+ }
33
+ return e;
34
+ }
35
+
36
+ export function date(e: FormEvent<HTMLInputElement>) {
37
+ let value = e.currentTarget.value;
38
+ value = value.replace(/\D/g, "");
39
+ value = value.replace(/(\d{2})(\d)/, "$1/$2");
40
+ value = value.replace(/(\d{2})(\d)/, "$1/$2");
41
+ e.currentTarget.value = value;
42
+ return e;
43
+ }
44
+
45
+ export function phone(e: FormEvent<HTMLInputElement>) {
46
+ let value = e.currentTarget.value;
47
+ value = value.replace(/\D/g, "");
48
+ value = value.replace(/(\d{2})(\d)/, "$1/$2");
49
+ value = value.replace(/(\d{2})(\d)/, "$1/$2");
50
+ e.currentTarget.value = value;
51
+ return e;
52
+ }
@@ -0,0 +1,201 @@
1
+ import styled, { css } from "styled-components";
2
+ import { InputHTMLAttributes } from "react";
3
+
4
+ import PhoneInput from "react-phone-input-2";
5
+
6
+ type InputProps = {
7
+ isInvalid?: boolean;
8
+ bordercolor?: string;
9
+ };
10
+
11
+ export const FormLabel = styled.label<InputProps & { $textColor?: string }>`
12
+ font-family: "Montserrat", sans-serif;
13
+ font-style: normal;
14
+ font-weight: 500;
15
+ font-size: 1rem;
16
+ color: ${(props) =>
17
+ props.isInvalid ? "var(--red)" : props.$textColor || "var(--black)"};
18
+ display: block;
19
+ margin-bottom: 0.5rem;
20
+ text-align: left;
21
+ `;
22
+
23
+ export const Input = styled.input<
24
+ InputHTMLAttributes<HTMLInputElement> & { $bordercolor?: string }
25
+ >`
26
+ font-family: "Montserrat", sans-serif;
27
+ font-style: normal;
28
+ font-weight: 500;
29
+ font-size: 1rem;
30
+ line-height: 1.5rem;
31
+ background: var(--gray-500);
32
+ color: var(--black);
33
+ padding: 0.5rem;
34
+ border-radius: 0.125rem;
35
+ border: 1px solid transparent;
36
+ display: block;
37
+ height: 3.125rem;
38
+ width: 100%;
39
+
40
+ &:focus {
41
+ border-radius: 0.125rem;
42
+ border: 2px solid ${(props) => props.$bordercolor || "var(--yellow-500)"};
43
+ outline: none;
44
+ }
45
+
46
+ &::placeholder {
47
+ font-size: 1rem;
48
+ line-height: 1.5rem;
49
+ color: #b6b6b6;
50
+ font-weight: 800;
51
+ }
52
+
53
+ /* Autofill styles */
54
+ &:-webkit-autofill {
55
+ background: var(--gray-500) !important;
56
+ color: var(--black) !important;
57
+ -webkit-text-fill-color: var(--black) !important;
58
+ transition: background-color 5000s ease-in-out 0s; /* Prevent flashing */
59
+ }
60
+
61
+ &:-webkit-autofill::first-line {
62
+ font-family: "Montserrat", sans-serif;
63
+ font-size: 1rem;
64
+ font-weight: 500;
65
+ }
66
+ `;
67
+
68
+ export const FormPhoneInput = styled(PhoneInput)<
69
+ InputProps & { $bordercolor?: string; $textColor?: string }
70
+ >`
71
+ .form-control {
72
+ background: white;
73
+ color: ${(props) =>
74
+ props.isInvalid ? "var(--red)" : props.$textColor || "var(--black)"};
75
+ padding: 0.5rem;
76
+ border-radius: 0.125rem;
77
+ border: 1px solid transparent;
78
+ height: 3.125rem;
79
+ width: 100%;
80
+ padding-left: 4rem;
81
+ font-family: "Montserrat", sans-serif;
82
+ font-style: normal;
83
+ font-weight: 500;
84
+ font-size: 1rem;
85
+ line-height: 1.5rem;
86
+ text &:focus,
87
+ &:focus-within {
88
+ border-radius: 0.125rem;
89
+ border: 2px solid
90
+ ${(props) =>
91
+ !props.isValid
92
+ ? "var(--red)"
93
+ : props.$bordercolor || "var(--yellow-500)"};
94
+ }
95
+
96
+ &::placeholder {
97
+ font-size: 1rem;
98
+ line-height: 1.5rem;
99
+ color: #b6b6b6;
100
+ font-weight: 800;
101
+ }
102
+
103
+ /* Autofill styles */
104
+ &:-webkit-autofill {
105
+ background: var(--gray-500) !important;
106
+ color: var(--black) !important;
107
+ -webkit-text-fill-color: var(--black) !important;
108
+ transition: background-color 5000s ease-in-out 0s; /* Prevent flashing */
109
+ }
110
+
111
+ &:-webkit-autofill::first-line {
112
+ font-family: "Montserrat", sans-serif;
113
+ font-size: 1rem;
114
+ font-weight: 500;
115
+ }
116
+ }
117
+
118
+ &:focus-within {
119
+ .form-control {
120
+ border: 2px solid
121
+ ${(props) =>
122
+ props.isInvalid
123
+ ? "var(--red)"
124
+ : props.$bordercolor || "var(--yellow-500)"};
125
+ }
126
+ }
127
+
128
+ .flag-dropdown {
129
+ background: white;
130
+ border: none;
131
+ padding: 0.5rem;
132
+ margin: 0.25rem;
133
+ cursor: pointer;
134
+
135
+ &:focus-within {
136
+ outline: none;
137
+ }
138
+ }
139
+ `;
140
+
141
+ export const FormErrorMessage = styled.small`
142
+ font-size: 0.75rem;
143
+ line-height: 1.125rem;
144
+ color: var(--red);
145
+ margin-top: 0.25rem;
146
+ display: block;
147
+ `;
148
+
149
+ export const FormControl = styled.div.withConfig({
150
+ shouldForwardProp: (prop) => !["isInvalid", "$bordercolor"].includes(prop),
151
+ })<{ isInvalid?: boolean; $bordercolor?: string }>`
152
+ ${FormLabel} {
153
+ ${(props) =>
154
+ props.isInvalid &&
155
+ css`
156
+ color: var(--red);
157
+ `};
158
+ }
159
+
160
+ ${Input} {
161
+ ${(props) =>
162
+ props.isInvalid &&
163
+ css`
164
+ border: 1px solid var(--red);
165
+
166
+ &:not(:focus)::placeholder {
167
+ color: var(--red);
168
+ font-weight: 600;
169
+ }
170
+ `};
171
+
172
+ &:focus {
173
+ ${(props) =>
174
+ props.isInvalid &&
175
+ css`
176
+ border: 1px solid var(--red);
177
+ `};
178
+ }
179
+ }
180
+
181
+ ${FormPhoneInput} {
182
+ ${(props) =>
183
+ props.isInvalid &&
184
+ css`
185
+ border: 1px solid var(--red);
186
+
187
+ &:not(:focus)::placeholder {
188
+ color: var(--red);
189
+ font-weight: 600;
190
+ }
191
+ `};
192
+
193
+ &:focus {
194
+ ${(props) =>
195
+ props.isInvalid &&
196
+ css`
197
+ border: 1px solid var(--red);
198
+ `};
199
+ }
200
+ }
201
+ `;
@@ -0,0 +1,15 @@
1
+ import { useState } from "react";
2
+
3
+ export function useError() {
4
+ const [error, setError] = useState<Error | null>(null);
5
+
6
+ const handleError = (err: unknown) => {
7
+ const errorObj = err instanceof Error ? err : new Error(String(err));
8
+ setError(errorObj);
9
+ console.error(errorObj);
10
+ };
11
+
12
+ const clearError = () => setError(null);
13
+
14
+ return { error, handleError, clearError };
15
+ }