updated compoennt file structure
This commit is contained in:
301
components/pages/contact/form.tsx
Normal file
301
components/pages/contact/form.tsx
Normal file
@@ -0,0 +1,301 @@
|
||||
"use client";
|
||||
|
||||
import { Check } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import axios from "axios";
|
||||
import { toast } from "react-toastify";
|
||||
import httpClient from "@/request/api";
|
||||
import { endPoints } from "@/request/links";
|
||||
|
||||
interface FormData {
|
||||
name: string;
|
||||
surname: string;
|
||||
address: string;
|
||||
theme: string;
|
||||
message: string;
|
||||
agreeToPolicy: boolean;
|
||||
}
|
||||
|
||||
interface FormErrors {
|
||||
name?: string;
|
||||
surname?: string;
|
||||
address?: string;
|
||||
theme?: string;
|
||||
message?: string;
|
||||
agreeToPolicy?: string;
|
||||
}
|
||||
|
||||
export default function Form() {
|
||||
const t = useTranslations();
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
name: "",
|
||||
surname: "",
|
||||
address: "",
|
||||
theme: "",
|
||||
message: "",
|
||||
agreeToPolicy: false,
|
||||
});
|
||||
const [errors, setErrors] = useState<FormErrors>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitStatus, setSubmitStatus] = useState<
|
||||
"idle" | "success" | "error"
|
||||
>("idle");
|
||||
|
||||
const formRequest = useMutation({
|
||||
mutationKey: [],
|
||||
mutationFn: (data: FormData) =>
|
||||
httpClient.post(endPoints.post.contact, data),
|
||||
onSuccess: () => {
|
||||
setSubmitStatus("success");
|
||||
setFormData({
|
||||
name: "",
|
||||
surname: "",
|
||||
address: "",
|
||||
theme: "",
|
||||
message: "",
|
||||
agreeToPolicy: false,
|
||||
});
|
||||
setIsSubmitting(false);
|
||||
toast.success(t("succes"));
|
||||
setTimeout(() => setSubmitStatus("idle"), 3000);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log("error: ", error);
|
||||
setIsSubmitting(false);
|
||||
setSubmitStatus("error");
|
||||
toast.error(t("error"));
|
||||
setTimeout(() => setSubmitStatus("idle"), 3000);
|
||||
},
|
||||
});
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: FormErrors = {};
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = "First name is required";
|
||||
}
|
||||
if (!formData.surname.trim()) {
|
||||
newErrors.surname = "Last name is required";
|
||||
}
|
||||
const phoneNumbers = formData.address.replace(/\D/g, "");
|
||||
if (phoneNumbers.length !== 12) {
|
||||
newErrors.address =
|
||||
t("validation.phoneRequired") || "To'liq telefon raqam kiriting";
|
||||
} else if (!phoneNumbers.startsWith("998")) {
|
||||
newErrors.address =
|
||||
t("validation.phoneInvalid") || "Noto'g'ri telefon raqam";
|
||||
}
|
||||
if (!formData.theme.trim()) {
|
||||
newErrors.theme = "theme is required";
|
||||
}
|
||||
if (!formData.message.trim()) {
|
||||
newErrors.message = "Message is required";
|
||||
}
|
||||
if (!formData.agreeToPolicy) {
|
||||
newErrors.agreeToPolicy = "You must agree to the privacy policy";
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const formatPhoneNumber = (value: string) => {
|
||||
const numbers = value.replace(/\D/g, "");
|
||||
if (!numbers.startsWith("998")) {
|
||||
return "+998 ";
|
||||
}
|
||||
|
||||
let formatted = "+998 ";
|
||||
const rest = numbers.slice(3);
|
||||
if (rest.length > 0) formatted += rest.slice(0, 2);
|
||||
if (rest.length > 2) formatted += " " + rest.slice(2, 5);
|
||||
if (rest.length > 5) formatted += " " + rest.slice(5, 7);
|
||||
if (rest.length > 7) formatted += " " + rest.slice(7, 9);
|
||||
|
||||
return formatted;
|
||||
};
|
||||
|
||||
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const formatted = formatPhoneNumber(e.target.value);
|
||||
setFormData({ ...formData, address: formatted });
|
||||
if (errors.address) {
|
||||
setErrors({ ...errors, address: "" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||
) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
if (errors[name as keyof FormErrors]) {
|
||||
setErrors((prev) => ({ ...prev, [name]: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckboxChange = () => {
|
||||
setFormData((prev) => ({ ...prev, agreeToPolicy: !prev.agreeToPolicy }));
|
||||
if (errors.agreeToPolicy) {
|
||||
setErrors((prev) => ({ ...prev, agreeToPolicy: undefined }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
setSubmitStatus("idle");
|
||||
formRequest.mutate(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* First Row - Names */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder={t("contact.form.placeholders.firstName")}
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm
|
||||
text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||
errors.name ? "border-red-500" : "border-transparent"
|
||||
}`}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="font-almarai mt-1 text-xs text-red-500">
|
||||
{errors.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="surname"
|
||||
placeholder={t("contact.form.placeholders.lastName")}
|
||||
value={formData.surname}
|
||||
onChange={handleChange}
|
||||
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||
errors.surname ? "border-red-500" : "border-transparent"
|
||||
}`}
|
||||
/>
|
||||
{errors.surname && (
|
||||
<p className="font-almarai mt-1 text-xs text-red-500">
|
||||
{errors.surname}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Second Row - address & theme */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value={formData.address}
|
||||
onChange={handlePhoneChange}
|
||||
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||
errors.address ? "border-red-500" : "border-transparent"
|
||||
}`}
|
||||
placeholder="+998 90 123 45 67"
|
||||
maxLength={17}
|
||||
/>
|
||||
{errors.address && (
|
||||
<p className="mt-1 text-sm text-red-500">{errors.address}</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="theme"
|
||||
placeholder={t("contact.form.placeholders.subject")}
|
||||
value={formData.theme}
|
||||
onChange={handleChange}
|
||||
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||
errors.theme ? "border-red-500" : "border-transparent"
|
||||
}`}
|
||||
/>
|
||||
{errors.theme && (
|
||||
<p className="font-almarai mt-1 text-xs text-red-500">
|
||||
{errors.theme}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Message Textarea */}
|
||||
<div>
|
||||
<textarea
|
||||
name="message"
|
||||
placeholder={t("contact.form.placeholders.message")}
|
||||
rows={8}
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
className={`font-almarai w-full resize-none rounded-md border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||
errors.message ? "border-red-500" : "border-transparent"
|
||||
}`}
|
||||
/>
|
||||
{errors.message && (
|
||||
<p className="font-almarai mt-1 text-xs text-red-500">
|
||||
{errors.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Checkbox and Submit */}
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCheckboxChange}
|
||||
className={` flex h-5 w-5 items-center justify-center rounded border-2 transition ${
|
||||
formData.agreeToPolicy
|
||||
? "border-red-600 bg-red-600"
|
||||
: "border-gray-400 bg-transparent"
|
||||
}`}
|
||||
aria-label="Agree to privacy policy"
|
||||
>
|
||||
{formData.agreeToPolicy && (
|
||||
<Check className="h-3 w-3 text-white" strokeWidth={3} />
|
||||
)}
|
||||
</button>
|
||||
<span className="font-almarai text-sm text-gray-300">
|
||||
{t("contact.form.privacy")}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="font-almarai shadow-[0px_0px_2px_8px_#ff01015c] rounded-full bg-red-600 px-8 py-3 text-sm font-semibold uppercase tracking-wider text-white transition hover:cursor-pointer hover:scale-90 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting ? "Sending..." : t("contact.form.send")}
|
||||
</button>
|
||||
</div>
|
||||
{errors.agreeToPolicy && (
|
||||
<p className="font-almarai text-xs text-red-500">
|
||||
{errors.agreeToPolicy}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Status Messages */}
|
||||
{submitStatus === "success" && (
|
||||
<p className="font-almarai text-center text-sm text-green-400">
|
||||
Message sent successfully!
|
||||
</p>
|
||||
)}
|
||||
{submitStatus === "error" && (
|
||||
<p className="font-almarai text-center text-sm text-red-400">
|
||||
Failed to send message. Please try again.
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user