updated compoennt file structure

This commit is contained in:
nabijonovdavronbek619@gmail.com
2026-03-09 13:03:17 +05:00
parent aba11a939a
commit d03a340afb
72 changed files with 25 additions and 28 deletions

View File

@@ -0,0 +1,21 @@
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import { useTranslations } from "next-intl";
import React from "react";
export default function ContactHeader() {
const t = useTranslations();
return (
<div className="mb-8 text-center">
<h2
className="uppercase font-unbounded bg-linear-to-br from-white via-white to-black
text-transparent bg-clip-text text-4xl font-bold tracking-wide md:text-5xl"
>
{t("contact.banner.subtitle")}
</h2>
<p className="font-almarai mt-3 text-sm text-gray-300 italic">
{t("contact.banner.description")}
</p>
</div>
);
}

View 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>
);
}

View File

@@ -0,0 +1 @@
export { Contact } from "./main";

View File

@@ -0,0 +1,89 @@
import Image from "next/image";
import { Mail, MapPin, Phone } from "lucide-react";
import ContactHeader from "./contactHeader";
import Form from "./form";
import { useTranslations } from "next-intl";
import Maps from "./maps";
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
export function Contact() {
const t = useTranslations();
const contactInfo = [
{
icon: Mail,
title: t("contact.form.email"),
detail: t("contact.form.emailAddress"),
},
{
icon: MapPin,
title: t("contact.form.location"),
detail: t("contact.form.address"),
},
{
icon: Phone,
title: t("contact.form.phone"),
detail: "+998-77-372-21-21",
},
];
return (
<section className="relative min-h-175 w-full py-30 md:py-40">
{/* Background Image */}
<div className="absolute inset-0 z-0">
<Image
src="/images/img5.webp"
alt="Contact background"
fill
className="object-cover"
priority
/>
{/* Radial Gradient Overlay */}
<div
className="absolute inset-0"
style={{
background:
"radial-gradient(at center bottom, rgb(144 74 20) 0%, rgba(30, 29, 28, 0.914) 50%, rgba(30, 29, 28, 0.914) 70%)",
}}
/>
</div>
{/* Content */}
<div className="relative z-10 ">
<div className="flex items-center justify-center w-full">
<div className="mb-4 flex items-center justify-center gap-2">
<DotAnimatsiya />
<span className="font-almarai text-sm font-semibold tracking-wider text-white">
{t("contact.banner.title")}
</span>
</div>
</div>
<Maps />
<div className="relative z-10 mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 space-y-5">
<ContactHeader />
<Form />
{/* Contact Info */}
<div className="mt-12 grid grid-cols-1 gap-8 md:grid-cols-3">
{contactInfo.map((info) => (
<div
key={info.title}
className="flex flex-col items-center text-center"
>
<div className="mb-3 flex h-14 w-14 items-center justify-center rounded-xl bg-[#2c2b2a]">
<info.icon className="h-6 w-6 text-red-600" />
</div>
<h3 className="font-almarai text-sm font-bold tracking-wider text-white">
{info.title}
</h3>
<p className="font-almarai mt-1 text-sm text-gray-400">
{info.detail}
</p>
</div>
))}
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,31 @@
export default function Maps() {
return (
<section className="w-full px-4 py-10">
<div className="mx-auto max-w-7xl grid grid-cols-1 md:grid-cols-2 gap-10">
{/* Yandex Map */}
<div className="h-[300px] sm:h-[400px] md:h-[500px] rounded-2xl overflow-hidden shadow-lg border hover:shadow-xl transition">
<iframe
src="https://yandex.uz/map-widget/v1/?ll=69.288118%2C41.323186&mode=search&oid=56350803620&ol=biz&z=16.97"
className="w-full h-full"
frameBorder="0"
allowFullScreen
loading="lazy"
/>
</div>
{/* Google Map */}
<div className="h-[300px] sm:h-[400px] md:h-[500px] rounded-2xl overflow-hidden shadow-lg border hover:shadow-xl transition">
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2996.342450769356!2d69.28561627695484!3d41.323166199974985!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x38aef56f76215ce7%3A0xfd64c6a930fb1bbb!2sIGNUM!5e0!3m2!1sru!2s!4v1770203090924"
className="w-full h-full"
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
/>
</div>
</div>
</section>
);
}