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