Files
tm-landing/app/_contact-us/form.tsx
T
AmirReza Jamali 7b6cd4d965 feat(validation): Implement form validation and error handling utilities
- Add validation rules for form fields with XSS protection
- Create API error handler to parse and manage server validation errors
- Integrate validation rules into contact and inquiries forms
- Enhance error handling with user feedback for form submissions
- Introduce unit tests for validation and error handling functionalities
2026-02-08 13:35:07 +03:30

136 lines
3.6 KiB
TypeScript

"use client";
import { handleApiError } from "@/lib/apiErrorHandler";
import { validationRules } from "@/lib/validation";
import api from "@/service/api";
import HCaptcha from "@hcaptcha/react-hcaptcha";
import { useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "react-toastify";
import InputGroup from "../_components/InputGroup";
import Loading from "../_components/Loading";
import TextareaGroup from "../_components/Textarea";
type TContactUsForm = {
full_name: string;
email: string;
phone: string;
message: string;
};
const FORM_FIELDS = ["full_name", "email", "phone", "message"];
interface IProps {
columnsPerRow?: 1 | 2 | 3 | 4;
buttonText?: string;
}
const ContactUsForm = ({
buttonText = "Submit",
columnsPerRow = 1,
}: IProps) => {
const [isLoading, setIsLoading] = useState(false);
const [hcaptchaToken, setHcaptchaToken] = useState<string | null>(null);
const captchaRef = useRef<HCaptcha>(null);
const {
register,
handleSubmit,
formState: { errors },
reset,
setError,
} = useForm<TContactUsForm>();
const onCaptchaVerify = (token: string) => {
setHcaptchaToken(token);
};
const onCaptchaExpire = () => {
setHcaptchaToken(null);
};
const onSubmit = async (data: TContactUsForm) => {
if (!hcaptchaToken) {
toast.error("Please complete the captcha verification");
return;
}
setIsLoading(() => true);
try {
const response = (
await api.post("contacts", { ...data, hcaptcha_token: hcaptchaToken })
).data;
toast.success(response.message);
reset();
setHcaptchaToken(null);
captchaRef.current?.resetCaptcha();
} catch (error) {
handleApiError(error, setError, toast.error, FORM_FIELDS);
} finally {
setIsLoading(() => false);
}
};
const gridColsClass = {
1: "lg:grid-cols-1",
2: "lg:grid-cols-2",
3: "lg:grid-cols-3",
4: "lg:grid-cols-4",
}[columnsPerRow];
const colSpanClass = {
1: "lg:col-span-1",
2: "lg:col-span-2",
3: "lg:col-span-3",
4: "lg:col-span-4",
}[columnsPerRow];
return (
<form
className={`w-full px-4 lg:px-16 py-10 grid ${gridColsClass} gap-4`}
onSubmit={handleSubmit(onSubmit)}>
<InputGroup
id="full_name"
label="Full Name"
register={register}
validation={validationRules.fullName}
type="text"
error={errors.full_name?.message}
/>
<InputGroup
id="email"
label="Email"
register={register}
validation={validationRules.email}
type="email"
error={errors.email?.message}
/>
<InputGroup
id="phone"
label="Phone"
register={register}
validation={validationRules.phone}
type="text"
error={errors.phone?.message}
/>
<TextareaGroup
id="message"
label="Message"
register={register}
error={errors.message?.message}
validation={validationRules.message}
/>
<div className={`${colSpanClass}`}>
<HCaptcha
sitekey={process.env.NEXT_PUBLIC_HCAPTCHA_SITE_KEY || ""}
onVerify={onCaptchaVerify}
onExpire={onCaptchaExpire}
ref={captchaRef}
/>
</div>
<div className={`${colSpanClass} flex justify-end`}>
<button className="bg-(--primary) cursor-pointer w-fit text-white rounded-full py-4 px-6 flex justify-center items-center text-center">
{isLoading ? <Loading /> : buttonText}
</button>
</div>
</form>
);
};
export default ContactUsForm;