Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 1 addition & 5 deletions src/components/ButtonPrimary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ export function PrimaryButton({ children, isDisabled = false, onClick }: Primary
<button
disabled={isDisabled}
onClick={onClick}
className="w-full py-5 px-12 text-white bg-primary-blue-base font-bold text-[27px] rounded-[20px] cursor-pointer outline-none flex justify-center items-center gap-4 h-20
hover:bg-primary-blue-300 transition-colors duration-300
active:bg-primary-blue-500
disabled:border-3 disabled:border-primary-gray-100 disabled:text-primary-gray-100 disabled:cursor-not-allowed disabled:hover:bg-primary-blue-base
">
className="w-full h-full bg-[#0069A8] hover:bg-[#052F4A] disabled:bg-[#E5E7EB] disabled:text-[#99A1AF] transition-colors duration-300 text-white font-medium text-sm py-2 rounded-md cursor-pointer shadow-sm">
{children}
</button>
)
Expand Down
6 changes: 1 addition & 5 deletions src/components/ButtonSecondary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@ export function SecondaryButton({ children, isDisabled = false, onClick, isActiv
<button
disabled={isDisabled}
onClick={onClick}
className={`w-full py-5 px-5 lg:px-12 border-2 border-primary-blue-base text-primary-blue-base font-bold text-[27px] rounded-[20px] cursor-pointer outline-none h-20 flex items-center justify-center
hover:bg-primary-blue-100 hover:text-primary-blue-300 transition-colors duration-300
active:text-primary-blue-500
disabled:border-3 disabled:border-primary-gray-100 disabled:text-primary-gray-300 disabled:cursor-not-allowed disabled:hover:bg-transparent
${activeStyle}
className={`w-full bg-transparent hover:bg-[#F3F4F6] transition-colors duration-300 text-[#6B7280] font-medium text-sm py-2 rounded-md cursor-pointer
`}>
{children}
</button>
Expand Down
113 changes: 83 additions & 30 deletions src/components/Input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { formatCPF } from "@/utils/formatCpf";
import { formatPhone } from "@/utils/formatPhone";
import { useState } from "react";
import { Controller, type Control, type FieldErrors, type FieldValues, type Path } from "react-hook-form";
import { BiCheck } from "react-icons/bi";
import { IoEyeOutline } from "react-icons/io5";
import { LuEyeClosed } from "react-icons/lu";
import { VscError } from "react-icons/vsc";

interface InputProps<T extends FieldValues> {
name: Path<T>;
Expand All @@ -13,21 +15,36 @@ interface InputProps<T extends FieldValues> {
placeholderText: string;
isOptional?: boolean;
mask?: "cpf" | "phone";
rules?: PasswordRule[];
}

interface PasswordRule {
label: string;
valid: boolean;
}


export function Input<T extends FieldValues>({
name,
control,
errors,
typeInput = 'text',
placeholderText,
isOptional = false,
mask
mask,
rules = []
}: InputProps<T>) {
const [showPassword, setShowPassword] = useState(false);
const isPassword = typeInput === 'password';
const formattedType = isPassword ? (showPassword ? 'text' : 'password') : typeInput;

const hasInvalidRule = rules.some((rule) => !rule.valid);

const isPasswordValid =
isPassword &&
rules.length > 0 &&
!hasInvalidRule;

type MaskType = "cpf" | "phone" | "none";

function applyMask(value: string, mask?: MaskType) {
Expand Down Expand Up @@ -58,40 +75,76 @@ export function Input<T extends FieldValues>({
render={({ field }) => (
<div className="flex flex-col gap-1.5">
<div className="relative">
<input
{...field}
name={name}
type={formattedType}
placeholder={isOptional ? placeholderText : `${placeholderText} *`}
onChange={(e) => handleInputChange(e.target.value, field.onChange, mask)}
className={`w-full p-5 bg-primary-gray-100 text-primary-gray-base font-normal placeholder:text-[27px] text-[27px] rounded-[20px] outline-none border-2 h-20
placeholder:text-primary-gray-base
focus:border-primary-blue-300
${errors?.[name] ? 'border-[#CF1A0F]' : 'border-transparent'}
`}
/>

{isPassword && (
<button
type="button"
data-testid="toggle-password"
onClick={() => setShowPassword((prev) => !prev)}
className="absolute right-5 top-1/2 -translate-y-1/2 cursor-pointer"
>
{showPassword ? (
<LuEyeClosed size={24} color="#66666E" />
) : (
<IoEyeOutline size={24} color="#66666E" />
)}
</button>
)}
<label className="text-[#171717]">
{placeholderText}
{!isOptional && <span className="text-[#F73B3B]">*</span>}
</label>
<div className="relative mt-2">
<input
{...field}
name={name}
type={formattedType}
placeholder={isOptional ? placeholderText : `${placeholderText} *`}
onChange={(e) =>
handleInputChange(e.target.value, field.onChange, mask)
}
className={`w-full p-5 font-normal rounded-[10px] outline-none h-[52px]
placeholder:text-[#99A1AF]

${errors?.[name]
? "border border-[#C10007] bg-[#FEF2F2] text-[#C10007]"
: isPasswordValid
? "border border-[#008235] bg-[#F0FDF4]"
: "border border-[#E5E7EB] bg-[#F3F4F6] focus:border-[#0069A8]"
}
`}
/>

{isPassword && (
<button
type="button"
data-testid="toggle-password"
onClick={() => setShowPassword((prev) => !prev)}
className="absolute right-5 top-1/2 -translate-y-1/2 cursor-pointer"
>
{showPassword ? (
<LuEyeClosed size={24} color="#66666E" />
) : (
<IoEyeOutline size={24} color="#66666E" />
)}
</button>
)}
</div>
</div>

{errors?.[name] && (
<span className="text-[#CF1A0F] font-normal text-base">
{errors?.[name] && (!isPassword || rules.length === 0) && (
<span className="text-[#C10007] font-normal text-base">
{errors[name]?.message as string}
</span>
)}

{isPassword && hasInvalidRule && (
<div className="flex flex-col gap-2 text-[19px]">
<span className="text-[#C10007]">Sua senha deve conter:</span>
{rules.map((rule) => (
<div key={rule.label} className="flex items-center gap-2">
{rule.valid
? <BiCheck className="text-primary-blue-700 w-5 h-5" />
: <VscError className="text-[#C10007] w-5 h-5" />
}
<span className="text-[#111111D9] text-sm">
{rule.label}
</span>
</div>
))}
</div>
)}

{isPasswordValid && (
<span className="text-[#008235] font-medium text-base">
Senha forte
</span>
)}
</div>
)}
/>
Expand Down
90 changes: 34 additions & 56 deletions src/components/StudentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import { toast } from "react-toastify";
import { TOAST_STYLES } from "@/pages/ToastStyleContainer";
import { SignUpStudentSchema, type SignUpStudentSchemaType } from "@/schemas/SignUp";
import { PrimaryButton } from "./ButtonPrimary";
import { BiCheck } from "react-icons/bi";
import { IoClose } from "react-icons/io5";
import { ButtonLoader } from "./ButtonLoader";
import { toStudentPayload } from "@/adapters/auth/toStudentPayload";
import { getPasswordRules } from "@/utils/passwordRules";

export function StudentForm() {
const { errors, handleSubmit, reset, control, watch } = useFormValidation(SignUpStudentSchema, {
Expand Down Expand Up @@ -50,17 +49,8 @@ export function StudentForm() {
}
}, [isError, isSuccess, navigate, reset]);

function usePasswordRules(password: string) {
return [
{ label: "Mais de 10 caracteres", valid: password.length > 10 },
{ label: "Pelo menos 1 número", valid: /\d/.test(password) },
{ label: "Letras maiúsculas e minúsculas", valid: /[a-z]/.test(password) && /[A-Z]/.test(password) },
{ label: "Pelo menos 1 caractere especial", valid: /[!@#$%^&*(),.?":{}|<>]/.test(password) },
];
}

const passwordValue = watch("password", "");
const rules = usePasswordRules(passwordValue);
const rules = getPasswordRules(passwordValue);

const onSubmit = (formData: SignUpStudentSchemaType) => {
console.log("Formulário submetido", formData);
Expand All @@ -86,66 +76,54 @@ export function StudentForm() {
placeholderText="E-mail"
/>

<Input<SignUpStudentSchemaType>
name="cpf"
control={control}
errors={errors}
placeholderText="CPF"
mask="cpf"
/>

<Input<SignUpStudentSchemaType>
name="phone"
control={control}
errors={errors}
placeholderText="Celular"
mask="phone"
isOptional
/>
<div className="grid grid-cols-2 gap-6">
<Input<SignUpStudentSchemaType>
name="cpf"
control={control}
errors={errors}
placeholderText="CPF"
mask="cpf"
/>

<Input<SignUpStudentSchemaType>
name="phone"
control={control}
errors={errors}
placeholderText="Telefone"
mask="phone"
isOptional
/>
</div>

<Input<SignUpStudentSchemaType>
name="password"
control={control}
errors={errors}
placeholderText="Crie sua senha"
placeholderText="Senha"
typeInput="password"
rules={rules}
/>

<Input<SignUpStudentSchemaType>
name="confirmPassword"
control={control}
errors={errors}
placeholderText="Confirme sua senha"
placeholderText="Confirmar senha"
typeInput="password"
/>

<div className="flex flex-col gap-2 text-[19px]">
<span className="text-primary-blue-700">Sua senha deve conter:</span>
{rules.map((rule) => (
<div key={rule.label} className="flex items-center gap-2">
{rule.valid
? <BiCheck className="text-primary-blue-700 w-5 h-5" />
: <IoClose className="text-[#CF1A0F] w-5 h-5" />
}
<span className={rule.valid ? "text-primary-blue-700" : "text-[#CF1A0F]"}>
{rule.label}
</span>
</div>
))}

<span className="text-primary-blue-700">*campos obrigatórios</span>
<div className="w-full h-[52px]">
<PrimaryButton isDisabled={isPending || !rules.every(rule => rule.valid) || watch("password") !== watch("confirmPassword")}>
{isPending ? (
<>
Carregando
<ButtonLoader />
</>
) : (
"Criar conta"
)}
</PrimaryButton>
</div>

<PrimaryButton isDisabled={isPending}>
{isPending ? (
<>
Carregando
<ButtonLoader />
</>
) : (
"Criar conta"
)}
</PrimaryButton>
</div>


Expand Down
Loading