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:
+86
-44
@@ -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)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user