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 = () => {
|
||||
return (
|
||||
<div className="absolute w-full -top-[420px] lg:-top-72 z-10">
|
||||
<ContactUs />
|
||||
<Inquires />
|
||||
</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";
|
||||
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";
|
||||
import TextareaGroup from "../_components/Textarea";
|
||||
|
||||
type TContactUsForm = {
|
||||
full_name: string;
|
||||
company_name: string;
|
||||
work_email: string;
|
||||
phone_number: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type ContactUsFormProps = {
|
||||
interface IProps {
|
||||
columnsPerRow?: 1 | 2 | 3 | 4;
|
||||
buttonText?: string;
|
||||
};
|
||||
|
||||
}
|
||||
const ContactUsForm = ({
|
||||
columnsPerRow = 2,
|
||||
buttonText = "Submit",
|
||||
}: ContactUsFormProps) => {
|
||||
columnsPerRow = 1,
|
||||
}: IProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
reset,
|
||||
} = useForm<TContactUsForm>();
|
||||
|
||||
const onSubmit = async (data: TContactUsForm) => {
|
||||
setIsLoading(() => true);
|
||||
try {
|
||||
const response = (await api.post("inquiries", { ...data })).data;
|
||||
const response = (await api.post("contacts", { ...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",
|
||||
@@ -59,36 +64,88 @@ const ContactUsForm = ({
|
||||
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={{
|
||||
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: {
|
||||
value: /^[+\d]+$/,
|
||||
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`}>
|
||||
<button className="bg-(--primary) w-fit text-white rounded-full py-4 px-6">
|
||||
{buttonText}
|
||||
<button className="bg-(--primary) cursor-pointer w-fit text-white rounded-full py-4 px-6">
|
||||
{isLoading ? <Loading /> : buttonText}
|
||||
</button>
|
||||
</div>
|
||||
</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 (
|
||||
<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">
|
||||
@@ -11,10 +11,10 @@ const ContactUs = () => {
|
||||
</p>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContactUs;
|
||||
export default Inquires;
|
||||
Reference in New Issue
Block a user