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
This commit is contained in:
AmirReza Jamali
2026-02-08 13:35:07 +03:30
parent 5fc6469752
commit 7b6cd4d965
9 changed files with 4834 additions and 108 deletions
+86 -44
View File
@@ -1,8 +1,10 @@
"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 { useState } from "react";
import { useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "react-toastify";
import InputGroup from "../_components/InputGroup";
@@ -29,8 +31,42 @@ const InquiresForm = ({
handleSubmit,
formState: { errors },
reset,
setError,
} = useForm<TInquiresForm>();
const defaultFields = useMemo(
() => [
{
name: "full_name",
label: "Full Name",
placeholder: "",
required: false,
},
{
name: "company_name",
label: "Company Name",
placeholder: "",
required: false,
},
{
name: "work_email",
label: "Work Email",
placeholder: "",
required: false,
},
{
name: "phone_number",
label: "Phone Number",
placeholder: "",
required: false,
},
],
[]
);
const fields = contactData?.fields || defaultFields;
const formFieldNames = useMemo(() => fields.map((f) => f.name), [fields]);
const onSubmit = async (data: TInquiresForm) => {
setIsLoading(true);
try {
@@ -38,9 +74,7 @@ const InquiresForm = ({
toast.success(response.message);
reset();
} catch (error) {
console.log(error);
toast.error("Something went wrong");
throw error;
handleApiError(error, setError, toast.error, formFieldNames);
} finally {
setIsLoading(false);
}
@@ -60,32 +94,56 @@ const InquiresForm = ({
4: "lg:col-span-4",
}[columnsPerRow];
const defaultFields = [
{ name: "full_name", label: "Full Name", placeholder: "", required: false },
{
name: "company_name",
label: "Company Name",
placeholder: "",
required: false,
},
{
name: "work_email",
label: "Work Email",
placeholder: "",
required: false,
},
{
name: "phone_number",
label: "Phone Number",
placeholder: "",
required: false,
},
];
const fields = contactData?.fields || defaultFields;
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";
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",
},
});
}
return createFieldValidation(field.label, field.required, {
maxLength: {
value: 200,
message: `${field.label} must not exceed 200 characters`,
},
});
};
return (
<form
className={`w-full px-4 lg:px-16 py-10 grid ${gridColsClass} gap-4`}
@@ -95,8 +153,6 @@ const InquiresForm = ({
field.name.includes("email") || field.name === "work_email"
? "email"
: "text";
const isPhoneField =
field.name.includes("phone") || field.name === "phone_number";
return (
<InputGroup
@@ -107,21 +163,7 @@ const InquiresForm = ({
register={register}
type={inputType}
error={errors[field.name]?.message}
validation={
isPhoneField
? {
pattern: {
value: /^[+\d]+$/,
message: "Please enter a valid phone number",
},
required: field.required
? "This field is required"
: undefined,
}
: field.required
? { required: "This field is required" }
: undefined
}
validation={getFieldValidation(field)}
/>
);
})}