feat(form): enhance and refactor contact form to inquiries form

This commit refactors the contact form into a more robust inquiries form, relocating it from `/contact-us` to a new `/inquires` route.

Key changes include:
- Renamed and moved the component from `ContactUs` to `Inquires`.
- Updated form fields: removed "Company Name", added "Message", and renamed "Work Email" and "Phone Number" for clarity.
- Implemented comprehensive client-side validation for all fields using `react-hook-form`.
- Added a loading state to the submit button to provide user feedback during submission.
- The form now automatically resets upon successful submission.
- Introduced new reusable `TextareaGroup` and `Loading` components to support the enhanced form functionality.
This commit is contained in:
AmirReza Jamali
2025-10-27 11:33:18 +03:30
parent ca911091f4
commit 645b397396
6 changed files with 284 additions and 40 deletions
+2 -2
View File
@@ -1,9 +1,9 @@
import ContactUs from "../_contact-us/page"; import Inquires from "../_inquires/page";
const FooterForm = () => { const FooterForm = () => {
return ( return (
<div className="absolute w-full -top-[420px] lg:-top-72 z-10"> <div className="absolute w-full -top-[420px] lg:-top-72 z-10">
<ContactUs /> <Inquires />
</div> </div>
); );
}; };
+20
View File
@@ -0,0 +1,20 @@
function Loading({ size = "md" }: { size?: "sm" | "md" | "lg" }) {
const sizeClasses = {
sm: "w-4 h-4 border-2",
md: "w-8 h-8 border-3",
lg: "w-12 h-12 border-4",
};
return (
<div className="inline-block">
<div
className={`${sizeClasses[size]} border-gray-200 border-t-blue-600 rounded-full animate-spin`}
role="status"
aria-label="Loading">
<span className="sr-only">Loading...</span>
</div>
</div>
);
}
export default Loading;
+61
View File
@@ -0,0 +1,61 @@
"use client";
import { useState } from "react";
import { RegisterOptions, UseFormRegister } from "react-hook-form";
function TextareaGroup({
id,
label,
register,
validation,
error,
}: {
label: string;
id: string;
register: UseFormRegister<any>;
validation?: RegisterOptions;
error?: string;
}) {
const [isFocused, setIsFocused] = useState(false);
const [hasValue, setHasValue] = useState(false);
const { onChange, ...rest } = register(id, validation);
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setHasValue(e.target.value !== "");
onChange(e);
};
return (
<div>
<div className="relative w-full my-3">
<textarea
className={`border ${
error
? "border-red-500"
: isFocused
? "border-(--primary)"
: "border-gray-200"
} p-2 py-3 rounded-lg w-full outline-none resize-vertical min-h-[100px]`}
id={id}
{...rest}
onChange={handleChange}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>
<label
htmlFor={id}
className={`absolute left-3 transition-all duration-200 pointer-events-none bg-white px-1 ${
isFocused || hasValue
? "-top-2 text-xs text-gray-600"
: "top-3 text-base text-gray-400"
}`}>
{label}
<span className="text-red-400">*</span>
</label>
</div>
{error && <p className="text-red-500 text-sm my-3">{error}</p>}
</div>
);
}
export default TextareaGroup;
+91 -34
View File
@@ -1,42 +1,47 @@
"use client"; "use client";
import api from "@/service/api"; import api from "@/service/api";
import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import InputGroup from "../_components/InputGroup"; import InputGroup from "../_components/InputGroup";
import Loading from "../_components/Loading";
import TextareaGroup from "../_components/Textarea";
type TContactUsForm = { type TContactUsForm = {
full_name: string; full_name: string;
company_name: string; email: string;
work_email: string; phone: string;
phone_number: string; message: string;
}; };
interface IProps {
type ContactUsFormProps = {
columnsPerRow?: 1 | 2 | 3 | 4; columnsPerRow?: 1 | 2 | 3 | 4;
buttonText?: string; buttonText?: string;
}; }
const ContactUsForm = ({ const ContactUsForm = ({
columnsPerRow = 2,
buttonText = "Submit", buttonText = "Submit",
}: ContactUsFormProps) => { columnsPerRow = 1,
}: IProps) => {
const [isLoading, setIsLoading] = useState(false);
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
reset,
} = useForm<TContactUsForm>(); } = useForm<TContactUsForm>();
const onSubmit = async (data: TContactUsForm) => { const onSubmit = async (data: TContactUsForm) => {
setIsLoading(() => true);
try { try {
const response = (await api.post("inquiries", { ...data })).data; const response = (await api.post("contacts", { ...data })).data;
toast.success(response.message); toast.success(response.message);
reset();
} catch (error) { } catch (error) {
console.log(error); console.log(error);
toast.error("Something went wrong"); toast.error("Something went wrong");
throw error; throw error;
} finally {
setIsLoading(() => false);
} }
}; };
const gridColsClass = { const gridColsClass = {
1: "lg:grid-cols-1", 1: "lg:grid-cols-1",
2: "lg:grid-cols-2", 2: "lg:grid-cols-2",
@@ -59,36 +64,88 @@ const ContactUsForm = ({
id="full_name" id="full_name"
label="Full Name" label="Full Name"
register={register} register={register}
type="text"
/>
<InputGroup
id="company_name"
label="Company Name"
register={register}
type="text"
/>
<InputGroup
id="work_email"
label="Work Email"
register={register}
type="email"
/>
<InputGroup
id="phone_number"
label="Phone Number"
register={register}
type="text"
error={errors.phone_number?.message}
validation={{ validation={{
required: {
message: "This field is required",
value: true,
},
minLength: {
value: 2,
message: "Please enter at least 2 characters",
},
maxLength: {
value: 100,
message: "Please enter at most 100 characters",
},
}}
type="text"
error={errors.full_name?.message}
/>
<InputGroup
id="email"
label="Email"
register={register}
validation={{
required: {
message: "This field is required",
value: true,
},
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: "Please enter a valid email address",
},
}}
type="email"
error={errors.email?.message}
/>
<InputGroup
id="phone"
label="Phone"
register={register}
validation={{
required: {
message: "This field is required",
value: true,
},
minLength: {
value: 10,
message: "Please enter at least 10 characters",
},
maxLength: {
value: 20,
message: "Please enter at most 20 characters",
},
pattern: { pattern: {
value: /^[+\d]+$/, value: /^[+\d]+$/,
message: "Please enter a valid phone number", message: "Please enter a valid phone number",
}, },
}} }}
type="text"
error={errors.phone?.message}
/>
<TextareaGroup
id="message"
label="Message"
register={register}
error={errors.message?.message}
validation={{
required: {
value: true,
message: "This field is required",
},
maxLength: {
value: 1000,
message: "Please enter at most 1000 characters",
},
minLength: {
value: 10,
message: "Please enter at least 10 characters",
},
}}
/> />
<div className={`${colSpanClass} flex justify-end`}> <div className={`${colSpanClass} flex justify-end`}>
<button className="bg-(--primary) w-fit text-white rounded-full py-4 px-6"> <button className="bg-(--primary) cursor-pointer w-fit text-white rounded-full py-4 px-6">
{buttonText} {isLoading ? <Loading /> : buttonText}
</button> </button>
</div> </div>
</form> </form>
+106
View File
@@ -0,0 +1,106 @@
"use client";
import api from "@/service/api";
import { 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 = {
full_name: string;
company_name: string;
work_email: string;
phone_number: string;
};
type InquiresFormProps = {
columnsPerRow?: 1 | 2 | 3 | 4;
buttonText?: string;
};
const InquiresForm = ({
columnsPerRow = 2,
buttonText = "Submit",
}: InquiresFormProps) => {
const [isLoading, setIsLoading] = useState(false);
const {
register,
handleSubmit,
formState: { errors },
reset,
} = useForm<TInquiresForm>();
const onSubmit = async (data: TInquiresForm) => {
setIsLoading(true);
try {
const response = (await api.post("inquiries", { ...data })).data;
toast.success(response.message);
reset();
} catch (error) {
console.log(error);
toast.error("Something went wrong");
throw error;
} 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}
type="text"
/>
<InputGroup
id="company_name"
label="Company Name"
register={register}
type="text"
/>
<InputGroup
id="work_email"
label="Work Email"
register={register}
type="email"
/>
<InputGroup
id="phone_number"
label="Phone Number"
register={register}
type="text"
error={errors.phone_number?.message}
validation={{
pattern: {
value: /^[+\d]+$/,
message: "Please enter a valid phone number",
},
}}
/>
<div className={`${colSpanClass} flex justify-end`}>
<button className="bg-(--primary) cursor-pointer w-fit text-white rounded-full py-4 px-6">
{isLoading ? <Loading /> : buttonText}
</button>
</div>
</form>
);
};
export default InquiresForm;
@@ -1,6 +1,6 @@
import ContactUsForm from "./form"; import InquiresForm from "./form";
const ContactUs = () => { const Inquires = () => {
return ( return (
<div className="flex flex-col items-center mt-16 px-8 "> <div className="flex flex-col items-center mt-16 px-8 ">
<div className="lg:w-2/3 m-auto flex flex-col gap-4 mb-12"> <div className="lg:w-2/3 m-auto flex flex-col gap-4 mb-12">
@@ -11,10 +11,10 @@ const ContactUs = () => {
</p> </p>
</div> </div>
<section className="border border-gray-300 rounded-4xl w-full lg:max-w-2/3 flex flex-col items-center bg-white"> <section className="border border-gray-300 rounded-4xl w-full lg:max-w-2/3 flex flex-col items-center bg-white">
<ContactUsForm /> <InquiresForm />
</section> </section>
</div> </div>
); );
}; };
export default ContactUs; export default Inquires;