"use client"; import { handleApiError } from "@/lib/apiErrorHandler"; import { sanitizeText } from "@/lib/sanitize"; import { createFieldValidation } from "@/lib/validation"; import api from "@/service/api"; import { CmsContact } from "@/types/TCms"; import HCaptcha from "@hcaptcha/react-hcaptcha"; import { useMemo, 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"; type TInquiresForm = { [key: string]: string; }; type InquiresFormProps = { columnsPerRow?: 1 | 2 | 3 | 4; buttonText?: string; contactData?: CmsContact; }; const InquiresForm = ({ columnsPerRow = 2, buttonText, contactData, }: InquiresFormProps) => { const [isLoading, setIsLoading] = useState(false); const [hcaptchaToken, setHcaptchaToken] = useState(null); const captchaRef = useRef(null); const { register, handleSubmit, formState: { errors, isValid }, reset, setError, trigger, } = useForm({ mode: "onChange" }); const defaultFields = useMemo( () => [ { name: "full_name", label: "Full Name", placeholder: "", required: true, }, { name: "company_name", label: "Company Name", placeholder: "", required: true, }, { name: "work_email", label: "Work Email", placeholder: "", required: true, }, { name: "phone_number", label: "Phone Number", placeholder: "", required: true, }, ], [], ); const fields = contactData?.fields || defaultFields; const formFieldNames = useMemo(() => fields.map((f) => f.name), [fields]); const onCaptchaVerify = (token: string) => { setHcaptchaToken(token); }; const onCaptchaExpire = () => { setHcaptchaToken(null); }; const onSubmit = async (data: TInquiresForm) => { const isFormValid = await trigger(); if (!isFormValid) { return; } if (!hcaptchaToken) { toast.error("Please complete the captcha verification"); return; } setIsLoading(true); try { const response = ( await api.post("inquiries", { ...data, hcaptcha_token: hcaptchaToken }) ).data; toast.success(response.message); reset(); setHcaptchaToken(null); captchaRef.current?.resetCaptcha(); } catch (error) { handleApiError(error, setError, toast.error, formFieldNames); } 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]; const submitText = sanitizeText(contactData?.submit_button_text) || buttonText || "Submit"; /** * Creates validation rules based on field type with XSS protection */ const getFieldValidation = (field: { name: string; label: string; required?: boolean; }) => { const isEmailField = field.name.includes("email") || field.name === "work_email"; const isPhoneField = field.name.includes("phone") || field.name === "phone_number"; const isStrictTextField = field.name === "full_name" || field.name === "company_name"; if (isPhoneField) { return createFieldValidation(field.label, field.required, { pattern: { value: /^[+\d\s()-]+$/, message: "Please enter a valid phone number (digits, +, spaces, parentheses, and dashes only)", }, minLength: field.required ? { value: 10, message: "Phone number must be at least 10 digits" } : undefined, maxLength: { value: 20, message: "Phone number must not exceed 20 characters", }, }); } if (isEmailField) { return createFieldValidation(field.label, field.required, { pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: "Please enter a valid email address", }, }); } if (isStrictTextField) { return createFieldValidation(field.label, field.required, { minLength: field.required ? { value: 2, message: `${field.label} must be at least 2 characters`, } : undefined, maxLength: { value: 200, message: `${field.label} must not exceed 200 characters`, }, pattern: { value: /^[\p{L}\p{M}\p{N}\s'.,()&/_-]+$/u, message: `${field.label} may only include letters, numbers, spaces, and common punctuation (no HTML or code)`, }, }); } return createFieldValidation(field.label, field.required, { maxLength: { value: 200, message: `${field.label} must not exceed 200 characters`, }, }); }; return (
{fields.map((field) => { const inputType = field.name.includes("email") || field.name === "work_email" ? "email" : "text"; return ( ); })}
); }; export default InquiresForm;