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,53 @@
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import { useTranslations } from "next-intl";
import { InnerNavbar } from "./innerNavbar";
export function AboutBanner() {
const t = useTranslations();
return (
<section className="relative w-full lg:h-[70vh] min-[350px]:h-[90vh] h-screen min-h-100 overflow-hidden pt-10">
{/* Background Image */}
<div
className="absolute inset-0 z-0"
style={{
backgroundImage: "url(/images/about/aboutBanner.jpg)",
backgroundSize: "cover",
backgroundPosition: "center",
}}
/>
{/* Gradient Overlay - Bottom-left to top-right */}
<div
className="absolute inset-0 z-10"
style={{
background: `linear-gradient(to top right, #d2610a 0%, #1e1d1ce3 30%, #1e1d1ce3 100%)`,
}}
/>
<div className="max-w-250 w-full mx-auto px-4">
{/* <div className="relative z-20 pt-50 sm:pt-30 pb-10">
<InnerNavbar />
</div> */}
<div className="relative z-20 h-full flex max-lg:flex-col items-start justify-between gap-5 pt-40">
<div className="spacw-y-4 ">
<div className="flex items-center gap-3">
<DotAnimatsiya />
<span className="font-almarai text-sm text-white font-semibold tracking-wide">
{t("about.banner.title")}
</span>
</div>
<p
className="font-unbounded uppercase bg-linear-to-br from-white via-white to-black
text-transparent bg-clip-text text-4xl sm:text-5xl lg:text-6xl font-bold leading-tight text-pretty"
>
{t("about.banner.subtitle")}
</p>
</div>
<div className="font-almarai lg:w-[40%] text-gray-300 sm:mt-20">
{t("about.banner.description")}
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,81 @@
"use client";
import { motion } from "framer-motion";
import { ShieldCheck } from "lucide-react";
import { useTranslations } from "next-intl";
import { NormativeCard } from "./normativeCard";
const fadeUp = (delay = 0) => ({
initial: { opacity: 0, y: 36 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.65, ease: [0.22, 1, 0.36, 1] as any, delay },
});
const fadeUpView = (delay = 0) => ({
initial: { opacity: 0, y: 48 },
whileInView: { opacity: 1, y: 0 },
transition: { duration: 0.65, ease: [0.22, 1, 0.36, 1] as any, delay },
viewport: { once: true },
});
export default function NormativBazaPage() {
const t = useTranslations();
return (
<div className="bg-[#0f0e0d] text-white min-h-screen pt-10 pb-20">
{/* ── Hero ── */}
<section className="relative w-full px-2">
{/* Content */}
<div className="relative z-10 flex flex-col justify-end h-full max-w-6xl mx-auto">
<motion.span
{...fadeUp(0)}
className="text-xs font-black uppercase tracking-[0.22em] text-red-600 mb-4"
>
{t("about.normativBaza.hero.label")}
</motion.span>
<motion.h1
{...fadeUp(0.1)}
className="text-4xl md:text-5xl lg:text-7xl font-black uppercase leading-[0.95] tracking-tight text-white"
>
{t("about.normativBaza.hero.title1")}
<br />
<span className="text-red-700">
{t("about.normativBaza.hero.title2")}
</span>
</motion.h1>
<motion.p
{...fadeUp(0.22)}
className="mt-6 max-w-xl text-sm md:text-base text-gray-300 leading-relaxed"
>
{t("about.normativBaza.hero.description")}
</motion.p>
{/* Decorative line */}
<motion.div
initial={{ scaleX: 0, originX: 0 }}
animate={{ scaleX: 1 }}
transition={{ delay: 0.4, duration: 0.8, ease: [0.22, 1, 0.36, 1] }}
className="mt-10 w-24 h-px bg-red-700"
/>
</div>
</section>
<NormativeCard />
{/* ── Bottom quote band ── */}
<motion.section
{...fadeUpView(0)}
className="border-t border-white/5 max-w-6xl mx-auto px-6 py-10 flex flex-col md:flex-row items-start md:items-center gap-6 justify-between"
>
<p className="text-xl md:text-2xl font-bold text-white/80 max-w-lg leading-snug">
{t("about.normativBaza.bottomText")}
</p>
<div className="shrink-0 w-16 h-16 rounded-2xl bg-red-400/10 border border-red-400/20 flex items-center justify-center">
<ShieldCheck size={28} className="text-red-600" />
</div>
</motion.section>
</div>
);
}

View File

@@ -0,0 +1,49 @@
"use client";
import { Download } from "lucide-react";
interface DownloadCardProps {
title: string;
fileType?: string;
fileSize: string;
fileUrl: string;
}
export default function DownloadCard({
title,
fileType = "PDF",
fileSize,
fileUrl,
}: DownloadCardProps) {
return (
<a
href={fileUrl}
download
className="min-h-40 h-full group relative w-full max-w-md border border-white/10 bg-[#171616b8] transition rounded-lg p-4 flex flex-col gap-4 items-start justify-between"
>
{/* Background glow effect */}
<div className="absolute inset-0 bg-linear-to-t from-red-600/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
{/* Decorative corner accent */}
<div className="absolute top-0 right-0 w-24 h-24 bg-linear-to-br from-red-500/20 to-transparent rounded-bl-full opacity-0 group-hover:opacity-00 transition-opacity duration-500" />
{/* Top section */}
<div className="flex justify-between items-start">
<h3 className="text-xl font-unbounded font-bold group-hover:text-red-500 text-white leading-tight transition-colors duration-300">
{title}
</h3>
<span className="text-sm font-medium text-white">{fileType}</span>
</div>
{/* Bottom section */}
<div className="flex w-full justify-between items-center">
<span className="text-sm text-gray-200">{fileSize}</span>
<Download
size={20}
className="text-gray-600 transition group-hover:text-red-700 duration-300"
/>
</div>
</a>
);
}

View File

@@ -0,0 +1,73 @@
"use client";
import { useTranslations } from "next-intl";
import DownloadCard from "./card";
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import PaginationLite from "@/components/paginationUI";
import { DownloadCardSkeleton } from "./loading/guidLoading";
export function Guides() {
const t = useTranslations();
const [currentPage, setCurrentPage] = useState(1);
const guides = [
{
file: "/varnix.pdf",
name: t("about.notePPPage.varnix"),
file_type: "PDF",
file_size: "368.51 KB",
},
{
file: "/ppFlanes.pdf",
name: t("about.notePPPage.ppFlanes"),
file_type: "PDF",
file_size: "368.51 KB",
},
{
file: "/ppFiting.pdf",
name: t("about.notePPPage.ppFiting"),
file_type: "PDF",
file_size: "368.51 KB",
},
];
const { data, isLoading } = useQuery({
queryKey: ["guides"],
queryFn: () => httpClient(endPoints.guides),
select: (res) => ({
results: res.data?.data?.results,
current_page: res.data?.data?.current_page,
total_pages: res.data?.data?.total_pages,
}),
});
const guidedata = data?.results ?? guides;
return (
<div className="space-y-4">
<div className="grid lg:grid-cols-3 min-[580px]:grid-cols-2 grid-cols-1 gap-4 max-w-7xl mx-auto py-5">
{isLoading ? (
<DownloadCardSkeleton />
) : (
guidedata.map((guide: any, index: number) => (
<DownloadCard
key={index}
title={guide.name}
fileType={guide.file_type}
fileSize={guide.file_size}
fileUrl={guide.file}
/>
))
)}
</div>
{data?.total_pages > 1 && (
<PaginationLite
currentPage={currentPage}
totalPages={data?.total_pages}
onChange={setCurrentPage}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,24 @@
export function DownloadCardSkeleton() {
return (
<div
className="min-h-40 h-full relative w-full max-w-md border border-white/10 bg-[#171616b8] rounded-lg p-4 flex flex-col gap-4 items-start justify-between"
>
{/* Top section */}
<div className="flex justify-between items-start w-full gap-4">
{/* Title */}
<div className="space-y-2 flex-1">
<div className="h-4 w-3/4 rounded bg-white/8 animate-pulse" />
<div className="h-4 w-1/2 rounded bg-white/8 animate-pulse" />
</div>
{/* File type badge */}
<div className="h-4 w-10 rounded bg-white/8 animate-pulse shrink-0" />
</div>
{/* Bottom section */}
<div className="flex w-full justify-between items-center">
<div className="h-3 w-16 rounded bg-white/8 animate-pulse" />
<div className="w-5 h-5 rounded bg-white/8 animate-pulse" />
</div>
</div>
);
}

View File

@@ -0,0 +1,44 @@
export function CertCardSkeleton({ count = 6 }: { count?: number }) {
return (
<>
<article className="flex flex-col rounded-2xl overflow-hidden sm:p-5 p-2 bg-[#161514] border border-white/5 w-full">
{/* Badge row */}
<div className="flex flex-col justify-between flex-1 min-w-0 py-1 gap-4">
<div className="space-y-2">
<div className="flex items-center gap-2 flex-wrap">
{/* Award badge */}
<div className="flex items-center gap-1.5">
<div className="w-2.5 h-2.5 rounded-sm bg-red-900/40 animate-pulse" />
<div className="h-2.5 w-20 rounded-full bg-red-900/40 animate-pulse" />
</div>
{/* Category pill */}
<div className="h-4 w-16 rounded-full border border-white/10 bg-white/5 animate-pulse" />
</div>
{/* Title lines */}
<div className="space-y-1.5 pt-1">
<div className="h-3.5 w-full rounded bg-white/8 animate-pulse" />
<div className="h-3.5 w-3/4 rounded bg-white/8 animate-pulse" />
</div>
</div>
</div>
{/* Divider */}
<div className="mx-4 h-px bg-white/5 sm:my-5 my-2" />
{/* Document list */}
<ul className="flex flex-col gap-2.5">
{Array.from({ length: 3 }).map((_, di) => (
<li key={di} className="flex items-start gap-2.5">
<span className="mt-1 flex-none w-1.5 h-1.5 rounded-full bg-red-900/40 animate-pulse" />
<div
className="h-3 rounded bg-white/8 animate-pulse"
style={{ width: `${75 - di * 10}%` }}
/>
</li>
))}
</ul>
</article>
</>
);
}

View File

@@ -0,0 +1,95 @@
"use client";
import { motion } from "framer-motion";
import { useTranslations } from "next-intl";
import { Award } from "lucide-react";
import { normativeData } from "@/lib/demoData";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import PaginationLite from "@/components/paginationUI";
import { CertCardSkeleton } from "./loading/loading";
export function NormativeCard() {
const t = useTranslations();
const [currentPage, setCurrentPage] = useState(1);
const { data, isLoading } = useQuery({
queryKey: ["normativeData"],
queryFn: () => httpClient(endPoints.normative),
select: (res) => ({
results: res.data?.data?.results,
current_page: res.data?.data?.current_page,
total_pages: res.data?.data?.total_pages,
}),
});
const generallyData = data?.results || normativeData;
if (isLoading) return <CertCardSkeleton />;
return (
<div className="space-y-4">
<div className="flex flex-col gap-8 py-10 max-w-6xl mx-auto px-2">
{generallyData.map((c: any, i: number) => (
<motion.article
key={i}
initial={{ opacity: 0, y: 28 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.55, delay: i * 0.1 }}
viewport={{ once: true }}
className="group flex flex-col rounded-2xl overflow-hidden sm:p-5 p-2 bg-[#161514] border border-white/5 hover:border-red-600/20 transition-colors duration-300 w-full"
>
{/* Meta + actions */}
<div className="flex flex-col justify-between flex-1 min-w-0 py-1 gap-4">
<div className="space-y-2">
{/* Badge row */}
<div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-1.5">
<Award size={11} className="text-red-600 shrink-0" />
<span className="text-[10px] font-black uppercase tracking-widest text-red-600">
{t("about.certificatePage.card.badge")}
</span>
</div>
<span className="text-[10px] font-bold uppercase tracking-wider text-white/20 border border-white/10 px-2 py-0.5 rounded-full">
{c.artikul}
</span>
</div>
{/* Title */}
<h3 className="font-bold text-sm md:text-base text-white leading-snug">
{t(c.title)}
</h3>
</div>
</div>
{/* Divider */}
<div className="mx-4 h-px bg-white/5 sm:my-5 my-2" />
{/* Documents list */}
<div className="overflow-hidden">
<ul className="flex flex-col gap-2.5">
{c.features.map((doc: any, di: number) => (
<li key={di} className="flex items-start gap-2.5">
<span className="mt-1 flex-none w-1.5 h-1.5 rounded-full bg-red-600/60" />
<p className="text-xs text-gray-400 leading-relaxed">
{t(doc?.name)}
</p>
</li>
))}
</ul>
</div>
</motion.article>
))}
</div>
{data?.total_pages > 1 && (
<PaginationLite
currentPage={currentPage}
totalPages={data?.total_pages}
onChange={setCurrentPage}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,64 @@
"use client";
import { motion } from "framer-motion";
import { useTranslations } from "next-intl";
import { certs } from "@/lib/demoData";
import { Award } from "lucide-react";
export function CertCard({ c, i }: { c: (typeof certs)[0]; i: number }) {
const t = useTranslations();
return (
<motion.article
initial={{ opacity: 0, y: 28 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.55, delay: i * 0.1 }}
viewport={{ once: true }}
className="group flex flex-col rounded-2xl overflow-hidden sm:p-5 p-2 bg-[#161514] border border-white/5 hover:border-red-600/20 transition-colors duration-300 w-full"
>
{/* Right: meta + actions for gitea */}
<div className="flex flex-col justify-between flex-1 min-w-0 py-1 gap-4">
<div className="space-y-2">
{/* Badge row */}
<div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-1.5">
<Award size={11} className="text-red-600 shrink-0" />
<span className="text-[10px] font-black uppercase tracking-widest text-red-600">
{t("about.certificatePage.card.badge")}
</span>
</div>
<span className="text-[10px] font-bold uppercase tracking-wider text-white/20 border border-white/10 px-2 py-0.5 rounded-full">
{c.artikul}
</span>
</div>
{/* Title */}
<h3 className="font-bold text-sm md:text-base text-white leading-snug">
{c.title}
</h3>
</div>
</div>
{/* ── Divider ── */}
<div className="mx-4 h-px bg-white/5 sm:my-5 my-2" />
{/* Collapsible document list */}
<div className="overflow-hidden">
<ul className="flex flex-col gap-2.5">
{c.features.length > 0 &&
c.features.map((doc: any, di: number) => {
const { name } = doc;
return (
<li key={di} className="flex items-start gap-2.5">
<span className="mt-1 flex-none w-1.5 h-1.5 rounded-full bg-red-600/60" />
<p className="text-xs text-gray-400 leading-relaxed">
{name || ""}
</p>
</li>
);
})}
</ul>
</div>
</motion.article>
);
}

View File

@@ -0,0 +1,33 @@
import { Phone } from "lucide-react";
import Image from "next/image";
export function AboutLine() {
return (
<div className="bg-black py-10 px-4">
<div className="max-w-280 w-full mx-auto relative py-10 flex items-center justify-between ">
<div className="text-white flex flex-col items-start justify-start gap-5 ">
<h2 className="lg:text-5xl sm:text-3xl text-2xl max-w-[80%] w-full font-semibold">
Ready for Action 24/7: Contact Us at
</h2>
<p className="flex items-center justify-center gap-4 font-semibold sm:text-xl text-lg">
<span
className="shrink-0 w-10 h-10 bg-red-600 rounded-full flex items-center justify-center
shadow-[0_0_0px_4px_rgba(239,68,68,0.1)]"
>
<Phone className="text-white w-5 h-5" />
</span>
+123-456-7890
</p>
</div>
<Image
src="/images/about/fire-box.png"
alt="image"
width={60}
height={60}
priority
className=" sm:flex hidden object-contain w-80 h-80 absolute -bottom-25 right-0"
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,5 @@
export { AboutBanner } from "./aboutBanner";
export { Story } from "./story";
export { AboutLine } from "./aboutLine";
export { WhyChooseUs } from "./whyChooseUs";
export { InnerNavbar } from "./innerNavbar";

View File

@@ -0,0 +1,52 @@
"use client";
import { useLocale, useTranslations } from "next-intl";
import Link from "next/link";
import { usePathname } from "next/navigation";
export function InnerNavbar() {
const t = useTranslations();
const locale = useLocale();
const pathname = usePathname();
const tabs = [
{ name: t("about.subPages.baza"), value: "baza" },
{ name: t("about.subPages.certificate"), value: "sertificate" },
{ name: t("about.subPages.notePP"), value: "notePP" },
{ name: t("about.subPages.noteTrailer"), value: "noteTrailer" },
{ name: t("about.subPages.noteFlans"), value: "noteFlans" },
];
return (
<nav className="w-full border-b border-gray-100 bg-[#1e1d1c] sticky top-0 z-30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center gap-1 overflow-x-auto">
{tabs.map((tab) => {
const href = `/${locale}/about/${tab.value}`;
const isActive = pathname === href || pathname.endsWith(`/about/${tab.value}`);
return (
<Link
key={tab.value}
href={href}
className={[
"relative shrink-0 px-4 py-4 text-sm font-semibold transition-colors duration-200 whitespace-nowrap",
"after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:rounded-full after:transition-all after:duration-200",
isActive
? "text-red-600"
: "text-gray-300 after:bg-transparent hover:text-red-600",
].join(" ")}
>
{tab.name}
</Link>
);
})}
</div>
</div>
</nav>
);
}
// for hide scrollbar in inner navbar, add this class to the parent container of InnerNavbar
// scrollbar-none [-ms-overflow-style:none] [scrollbar-width:none]

View File

@@ -0,0 +1,103 @@
import { useTranslations } from "next-intl";
import Image from "next/image";
export function Story() {
const t = useTranslations();
return (
<div className="pb-0 relative z-10 max-[350px]:pb-30 ">
<div className="max-w-260 mx-auto px-4">
<section className="relative -top-20 rounded-xl w-full lg:h-[70vh] h-[80vh] min-h-150 sm:overflow-hidden shadow-2xl flex flex-col items-start justify-between">
{/* Background Image */}
<div
className="absolute inset-0 z-0 rounded-xl"
style={{
backgroundImage: "url(/images/img13.jpg)",
backgroundSize: "cover",
backgroundPosition: "center",
}}
/>
{/* Gradient Overlay */}
<div
className="absolute inset-0 z-10 rounded-xl"
style={{
background: `linear-gradient(to bottom right, black 5%, transparent 70%, transparent 90%)`,
}}
/>
{/* Content */}
<div className="relative z-20 p-8 lg:p-12 max-w-130">
<h2 className="uppercase font-unbounded text-white text-4xl lg:text-5xl font-bold mb-6">
{t("about.story.title")}
</h2>
<p className="font-almarai text-gray-300 mb-4">
{t("about.story.description")}
</p>
<button className="font-almarai text-white flex items-center gap-2 hover:text-red-500 transition">
{t("about.story.readMore")}
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</button>
</div>
{/* bottom cards */}
<div className="flex max-md:flex-col items-end justify-center md:gap-10 gap-5 z-20 relative bottom-0 mx-auto max-md:pb-5 max-sm:px-2">
<div
className="flex sm:flex-row flex-col sm:items-center items-start
md:rounded-t-xl max-md:rounded-xl max-w-md w-full sm:p-7 p-2 bg-linear-to-br
from-[#f21b1b] to-[#830909] sm:gap-5 gap-2"
>
<span className="sm:rounded-xl rounded-lg bg-black sm:p-3 p-1 max-sm:w-15 max-sm:h-15">
<Image
src="/images/about/fireforce-vision.png"
alt="image icon"
width={100}
height={100}
className="object-cover"
/>
</span>
<div className="space-y-1 text-white">
<h2 className="font-unbounded text-xl font-semibold">
{t("about.story.vision.title")}
</h2>
<p className="font-almarai">
{t("about.story.vision.description")}
</p>
</div>
</div>
<div className="flex sm:flex-row flex-col sm:items-center items-start md:rounded-t-xl max-md:rounded-xl max-w-md w-full sm:p-4 p-2 bg-black sm:gap-5 gap-2">
<span className="sm:rounded-xl rounded-lg bg-[#1e1d1c] sm:p-3 p-1 max-sm:w-15 max-sm:h-15">
<Image
src="/images/about/fireforce-mission.png"
alt="image icon"
width={100}
height={100}
className=""
/>
</span>
<div className="space-y-1 text-white">
<h2 className="font-unbounded text-xl font-semibold">
{t("about.story.mission.title")}
</h2>
<p className="font-almarai">
{t("about.story.mission.description")}
</p>
</div>
</div>
</div>
</section>
</div>
</div>
);
}

View File

@@ -0,0 +1,102 @@
import Image from "next/image";
import { Check } from "lucide-react";
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import { useLocale, useTranslations } from "next-intl";
import Link from "next/link";
export function WhyChooseUs() {
const t = useTranslations();
const locale = useLocale();
const features = [
{ title: t("about.whyChoose.features.fastResponse") },
{ title: t("about.whyChoose.features.ready24") },
{ title: t("about.whyChoose.features.experienced") },
{ title: t("about.whyChoose.features.quality") },
];
return (
<section className=" bg-[#1e1d1c] px-4 py-30 pb-40 md:px-8 lg:px-12">
<div className="mx-auto grid max-w-7xl gap-8 lg:grid-cols-2 lg:gap-16">
{/* Left Content */}
<div className="flex flex-col justify-center space-y-8">
{/* Header */}
<div>
<div className="mb-4 flex items-center gap-2">
<DotAnimatsiya />
<span className="font-almarai text-sm font-semibold tracking-wider text-white">
{t("about.whyChoose.title")}
</span>
</div>
<h2
className="font-unbounded uppercase bg-linear-to-br from-white via-white to-black
text-transparent bg-clip-text text-3xl font-bold sm:text-4xl lg:text-5xl"
>
{t("about.whyChoose.subtitle")}
</h2>
</div>
{/* Description */}
<p className="font-almarai text-base leading-relaxed text-gray-400 sm:text-lg">
{t("about.whyChoose.description")}
</p>
{/* Features Grid */}
<div className="grid grid-cols-2 gap-4 sm:gap-6">
{features.map((feature, index) => (
<div key={index} className="flex items-center gap-3">
<div className="shrink-0">
<Check className="h-5 w-5 text-red-600 sm:h-6 sm:w-6" />
</div>
<span className="font-almarai text-sm font-medium text-white sm:text-base">
{feature.title}
</span>
</div>
))}
</div>
{/* CTA Button */}
<div>
<Link
href={`/${locale}/contact`}
className="font-almarai shadow-[0px_0px_2px_8px_#ff01015c] rounded-full bg-red-600 px-6 py-3 font-bold text-white transition-all hover:bg-red-700 sm:px-8 sm:py-4"
>
{t("about.whyChoose.contact")}
</Link>
</div>
</div>
{/* Right Image Section */}
<div className="relative flex items-center justify-center">
<div className="relative h-96 md:w-[80%] w-full sm:h-125 lg:h-150">
{/* Main Image */}
<Image
src="/images/img8.jpg"
alt="Best Award Firefighter"
fill
className="rounded-3xl object-cover object-top"
/>
{/* Overlay Gradient */}
<div className="absolute inset-0 rounded-3xl bg-linear-to-t from-black via-transparent to-black/0" />
{/* Award Badge */}
<div className="absolute bottom-6 left-1/2 flex -translate-x-1/2 items-center gap-3 ">
<Image
src="/images/home/emblem.png"
alt="Award"
width={70}
height={70}
className="h-24 w-24 sm:h-20 sm:w-20"
/>
<div>
<p className="font-almarai text-sm font-bold text-white sm:text-base">
{t("about.whyChoose.award")}
</p>
</div>
</div>
</div>
</div>
</div>
</section>
);
}

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

View File

@@ -0,0 +1,93 @@
"use client";
import Link from "next/link";
import FAQAccordion, { FAQItem } from "./faqAccardion";
import { useTranslations } from "next-intl";
import { useQuery } from "@tanstack/react-query";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useEffect, useState } from "react";
export function Togle() {
const t = useTranslations();
const faqItems: FAQItem[] = [
{
id: 1,
question: t("faq.question1.question"),
answer: t("faq.question1.answer"),
},
{
id: 2,
question: t("faq.question2.question"),
answer: t("faq.question2.answer"),
},
{
id: 3,
question: t("faq.question3.question"),
answer: t("faq.question3.answer"),
},
{
id: 4,
question: t("faq.question4.question"),
answer: t("faq.question4.answer"),
},
{
id: 5,
question: t("faq.question5.question"),
answer: t("faq.question5.answer"),
},
];
const [faq, setFaq] = useState<any>(faqItems);
const { data } = useQuery({
queryKey: ["faq"],
queryFn: () => httpClient(endPoints.faq),
select: (data) => data?.data?.results,
});
useEffect(() => {
data && setFaq(data);
}, [data]);
return (
<div className="min-h-screen bg-[#1e1d1c]">
<main className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
{/* Header Section */}
<div className="mb-16 flex items-start justify-between gap-4 lg:gap-12">
<div className="flex items-center md:col-span-1">
<h1
className="font-unbounded uppercase bg-linear-to-br from-white via-white/50 to-black
text-transparent bg-clip-text text-3xl font-bold leading-tight sm:text-4xl md:text-5xl lg:text-6xl"
>
{t("faq.banner.topic")}
</h1>
</div>
{/* FAQ Section */}
<div className="max-w-250 w-full">
<FAQAccordion items={faq} />
</div>
</div>
{/* ASK QUESTION */}
<div className="space-y-8 w-full mt-10 ">
<h1
className="font-unbounded uppercase text-center bg-linear-to-br from-white via-white/50 to-black
text-transparent bg-clip-text text-3xl font-bold leading-tight sm:text-4xl md:text-5xl lg:text-6xl"
>
{t("faq.ask.question")}
</h1>
<p className="font-almarai text-center text-white/80 text-lg">
{t("faq.ask.subtitle")}
</p>
<div className="w-full flex items-center justify-center">
<Link
href={"/contact"}
className="font-almarai mx-auto shadow-[0px_0px_2px_8px_#ff01015c] bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-8 rounded-full transition duration-300 transform hover:scale-105 w-fit"
>
{t("faq.ask.btn")}
</Link>
</div>
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,46 @@
"use client";
import * as Accordion from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";
export interface FAQItem {
id: number;
question: string;
answer: string;
}
interface FAQAccordionProps {
items: FAQItem[];
}
export default function FAQAccordion({ items }: FAQAccordionProps) {
return (
<Accordion.Root type="single" collapsible className="w-full">
{items.map((item, index) => (
<Accordion.Item
key={item.id}
value={String(item.id)}
className="border-b border-slate-700 py-6"
>
<Accordion.Trigger className="group flex w-full items-center justify-between text-left">
<h3 className="font-almarai text-lg font-bold uppercase tracking-wide text-white transition-colors duration-300 group-hover:cursor-pointer md:text-xl">
{item.question}
</h3>
<div className="ml-4 shrink-0">
<ChevronDown
className="h-6 w-6 text-red-600 transition-transform duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</div>
</Accordion.Trigger>
<Accordion.Content className="overflow-hidden pt-4 text-gray-400 animate-in fade-in slide-in-from-top-2 duration-300 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=closed]:slide-out-to-top-2">
<p className="font-almarai leading-relaxed text-sm md:text-base">
{item.answer}
</p>
</Accordion.Content>
</Accordion.Item>
))}
</Accordion.Root>
);
}

View File

@@ -0,0 +1,46 @@
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import { useTranslations } from "next-intl";
export function FaqBanner() {
const t = useTranslations();
return (
<section className="relative w-full h-[55vh] min-h-100 overflow-hidden pt-10">
{/* Background Image */}
<div
className="absolute inset-0 z-0"
style={{
backgroundImage: "url(/images/faqBanner.jpg)",
backgroundSize: "cover",
backgroundPosition: "center",
}}
/>
{/* Gradient Overlay - Bottom-left to top-right */}
<div
className="absolute inset-0 z-10"
style={{
background: `linear-gradient(to top right, #d2610a 0%, #1e1d1ce3 30%, #1e1d1ce3 100%)`,
}}
/>
<div className="max-w-250 w-full mx-auto px-4">
<div className="relative z-20 h-full flex max-lg:flex-col items-start justify-between gap-5 pt-30">
<div className="spacw-y-4 ">
<div className="flex items-center gap-3">
<DotAnimatsiya />
<span className="font-almarai text-sm text-white font-semibold tracking-wide">
{t("faq.banner.title")}
</span>
</div>
<p
className="font-unbounded uppercase bg-linear-to-br from-white via-white to-black
text-transparent bg-clip-text text-4xl sm:text-5xl lg:text-6xl font-bold leading-tight text-pretty"
>
{t("faq.banner.subtitle")}
</p>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,2 @@
export { FaqBanner } from "./faqBanner";
export { Togle } from "./answearQuestions";

View File

@@ -0,0 +1,121 @@
"use client";
import React from "react";
import Image from "next/image";
import { Flame, Building2, Ambulance } from "lucide-react";
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import { useLocale, useTranslations } from "next-intl";
import Link from "next/link";
interface ServiceItem {
icon: React.ReactNode;
title: string;
description: string;
}
export function AboutUs() {
const t = useTranslations();
const locale = useLocale();
const services: ServiceItem[] = [
{
icon: <Flame width={40} height={40} className="text-red-500" />,
title: t("home.about.prevention.title"),
description: t("home.about.prevention.description"),
},
{
icon: <Building2 width={40} height={40} className="text-red-500" />,
title: t("home.about.accidents.title"),
description: t("home.about.accidents.description"),
},
{
icon: <Ambulance width={40} height={40} className="text-red-500" />,
title: t("home.about.service.title"),
description: t("home.about.service.description"),
},
];
return (
<section className="bg-[#1e1d1c] py-16 px-4 sm:py-20 sm:px-6 lg:px-8">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 items-center">
{/* Left Content */}
<div className="space-y-8">
{/* Header */}
<div className="space-y-6">
<div className="flex items-center gap-3">
<DotAnimatsiya />
<span className="font-almarai text-white font-bold text-sm tracking-wide">
{t("home.about.title")}
</span>
</div>
<h2
className="font-unbounded uppercase text-4xl bg-linear-to-br from-white via-white to-black
text-transparent bg-clip-text sm:text-5xl lg:text-6xl font-bold leading-tight"
>
{t("home.about.subtitle")}
</h2>
</div>
{/* Services */}
<div className="space-y-6">
{services.map((service, index) => (
<div key={index} className="flex items-center gap-4">
<div className="rounded-lg p-3 inline-block bg-[#282828]">
{service.icon}
</div>
<div>
<h3 className="uppercase font-unbounded text-white font-bold text-base sm:text-lg mb-2">
{service.title}
</h3>
<p className="font-almarai text-gray-400 text-sm sm:text-base leading-relaxed">
{service.description}
</p>
</div>
</div>
))}
</div>
{/* Button */}
<div>
<Link href={`/${locale}/about`} className="font-almarai bg-red-600 hover:bg-red-700 text-white font-bold px-8 py-3 rounded-full transition-colors duration-300 shadow-[0px_0px_2px_8px_#ff01015c]">
{t("home.about.title")}
</Link>
</div>
</div>
{/* Right Image */}
<div className="relative flex justify-center lg:justify-end">
<div className="relative w-full max-w-md">
{/* Main Image */}
<div className="relative rounded-lg overflow-hidden bg-gray-900">
<Image
src="/images/img8.jpg"
alt="Firefighter at ready"
width={400}
height={600}
className="w-full h-150 object-cover"
priority
/>
</div>
{/* Badge Overlay */}
<div className="absolute bottom-4 right-4 sm:bottom-6 sm:left-6 flex items-center justify-start gap-3">
<Image
src="/images/home/emblem.png"
alt="Best Award Badge"
width={100}
height={100}
className="w-20 h-20 sm:w-24 sm:h-34 drop-shadow-lg"
/>
<div className="text-center">
<p className="text-white font-bold text-sm sm:text-base">
{t("home.about.award")}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,28 @@
import { BannerSlider } from "./slider";
export function Banner() {
return (
<section className="relative w-full lg:h-[86vh] h-screen min-h-150 overflow-hidden min-[450px]:pt-10 pt-20">
{/* Background Image */}
<div
className="absolute inset-0 z-0"
style={{
backgroundImage: "url(/images/home/banner.jpg)",
backgroundSize: "cover",
backgroundPosition: "center",
}}
/>
{/* Gradient Overlay - Bottom-left to top-right */}
<div
className="absolute inset-0 z-10"
style={{
background: `linear-gradient(to top right, #c75c08 0%, #1e1d1ce3 28%, #1e1d1ce3 100%)`,
}}
/>
{/* Content Container */}
<BannerSlider />
</section>
);
}

View File

@@ -0,0 +1,68 @@
export function BannerSliderSkeleton() {
return (
<div className="max-w-7xl mx-auto relative z-30 h-full mt-20 flex items-center justify-center">
{/* Fake nav buttons */}
<div className="w-10 h-10 absolute z-10 left-[5%] top-[40vh] rounded-full bg-gray-700/50 lg:flex hidden animate-pulse" />
<div className="w-10 h-10 absolute z-10 right-[5%] top-[40vh] rounded-full bg-gray-700/50 lg:flex hidden animate-pulse" />
{/* Slide content */}
<div className="relative z-20 h-full flex items-center lg:mt-0 sm:mt-[10vh] mt-[5vh] w-full">
<div className="max-w-400 mx-auto px-4 sm:px-6 lg:px-8 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 items-center h-full">
{/* Mobile text skeleton (hidden on lg) */}
<div className="lg:hidden space-y-6">
{/* Badge */}
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-gray-600 animate-pulse" />
<div className="h-4 w-32 rounded-full bg-gray-600 animate-pulse" />
</div>
{/* Heading */}
<div className="space-y-3">
<div className="h-8 w-4/5 rounded-lg bg-gray-600 animate-pulse" />
<div className="h-8 w-3/5 rounded-lg bg-gray-600 animate-pulse" />
</div>
{/* Description */}
<div className="space-y-2">
<div className="h-4 w-full rounded bg-gray-700 animate-pulse" />
<div className="h-4 w-11/12 rounded bg-gray-700 animate-pulse" />
<div className="h-4 w-3/4 rounded bg-gray-700 animate-pulse" />
</div>
{/* CTA */}
<div className="h-12 w-40 rounded-full bg-red-900/40 animate-pulse" />
</div>
{/* Image skeleton */}
<div className="flex items-end justify-center">
<div className="lg:w-[375px] w-[250px] lg:h-[375px] h-[250px] max-[300px]:w-[80vw] rounded-xl bg-gray-700/50 animate-pulse shimmer" />
</div>
{/* Desktop text skeleton (hidden on mobile) */}
<div className="lg:inline-block hidden space-y-6 mb-20">
{/* Badge */}
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-gray-600 animate-pulse" />
<div className="h-4 w-36 rounded-full bg-gray-600 animate-pulse" />
</div>
{/* Heading */}
<div className="space-y-3">
<div className="h-10 w-4/5 rounded-lg bg-gray-600 animate-pulse" />
<div className="h-10 w-3/5 rounded-lg bg-gray-600 animate-pulse" />
<div className="h-10 w-2/5 rounded-lg bg-gray-600 animate-pulse" />
</div>
{/* Description */}
<div className="space-y-2 max-w-md">
<div className="h-4 w-full rounded bg-gray-700 animate-pulse" />
<div className="h-4 w-11/12 rounded bg-gray-700 animate-pulse" />
<div className="h-4 w-3/4 rounded bg-gray-700 animate-pulse" />
</div>
{/* CTA */}
<div className="h-12 w-40 rounded-full bg-red-900/40 animate-pulse" />
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,163 @@
"use client";
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Autoplay } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation";
import { ChevronLeft, ChevronRight } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { useLocale, useTranslations } from "next-intl";
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import { useQuery } from "@tanstack/react-query";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { BannerType } from "@/lib/types";
import { BannerSliderSkeleton } from "./loading";
// The custom CSS selectors for navigation
const navigationPrevEl = ".hero-swiper-prev";
const navigationNextEl = ".hero-swiper-next";
export function BannerSlider() {
const t = useTranslations();
const locale = useLocale();
const { data, isLoading } = useQuery({
queryKey: ["banner"],
queryFn: () => httpClient(endPoints.banner),
select: (data: any): BannerType[] => data?.data?.results,
});
const BANNER_DATA = [
{
image: "/images/homeBanner3.png",
title: t("home.banner.title2"),
description: t("home.banner.description"),
},
{
image: "/images/homeBanner4.png",
title: t("home.banner.title2"),
description: t("home.banner.description"),
},
];
const bannerData = data ?? BANNER_DATA;
if (isLoading) return <BannerSliderSkeleton />;
return (
<div className="max-w-7xl mx-auto relative z-30 h-full mt-20 flex items-center justify-center ">
{/* Custom buttons */}
<button
className={`${navigationPrevEl.replace(
".",
"",
)} w-10 h-10 absolute z-10 left-0 top-[40vh] rounded-full p-0 bg-primary text-center text-white lg:flex hidden items-center justify-center hover:bg-red-600 hover:cursor-pointer transition`}
>
<ChevronLeft size={30} />
</button>
<button
className={`${navigationNextEl.replace(
".",
"",
)} w-10 h-10 absolute z-10 right-0 top-[40vh] rounded-full bg-primary text-center text-white lg:flex hidden items-center justify-center hover:bg-red-600 hover:cursor-pointer transition `}
>
<ChevronRight size={30} />
</button>
<Swiper
modules={[Navigation, Autoplay]}
slidesPerView={1}
spaceBetween={30}
loop={true}
navigation={{
// Pass the class selectors here
prevEl: navigationPrevEl,
nextEl: navigationNextEl,
}}
autoplay={{
delay: 5000,
disableOnInteraction: false,
}}
>
{bannerData.map((item, index) => (
<SwiperSlide key={index}>
<div className="relative z-20 h-full flex items-center lg:mt-0 sm:mt-[10vh] mt-[5vh]">
<div className="max-w-400 mx-auto px-4 sm:px-6 lg:px-8 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 items-center h-full">
{/* Right side - Text Content */}
<div className="lg:hidden inline-block space-y-6 text-white">
{/* Badge */}
<div className="flex items-center gap-2 w-fit">
<DotAnimatsiya />
<span className="text-sm font-semibold tracking-wide font-almarai">
{t("home.banner.title1")}
</span>
</div>
{/* Main Heading */}
<h1
className="bg-linear-to-br from-white via-white to-black
text-transparent bg-clip-text text-4xl sm:text-5xl lg:text-6xl font-bold leading-tight text-pretty font-unbounded"
>
{item.title}
</h1>
{/* Description */}
<p className="text-base sm:text-lg text-gray-300 leading-relaxed max-w-md">
{item.description}
</p>
{/* CTA Button */}
<button className="shadow-[0px_0px_2px_8px_#ff01015c] bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-8 rounded-full transition duration-300 transform hover:scale-105 w-fit">
{t("home.banner.cta")}
</button>
</div>
{/* Left side - Firefighters Image */}
<div className="flex items-end justify-center ">
<img
src={item.image}
alt="Firefighters"
loading="lazy"
className="lg:w-150 w-100 lg:h-150 max-[300px]:w-[80vw] object-contain object-right rounded-xl drop-shadow-2xl"
/>
</div>
{/* Right side - Text Content */}
<div className="lg:inline-block hidden space-y-6 mb-20">
{/* Badge */}
<div className="flex items-center gap-2 w-fit">
<DotAnimatsiya />
<span className="text-sm font-semibold text-white tracking-wide font-almarai">
{t("home.banner.title1")}
</span>
</div>
{/* Main Heading */}
<h1
className="font-unbounded uppercase text-4xl bg-linear-to-br from-white via-white to-black
text-transparent bg-clip-text sm:text-5xl lg:text-6xl font-bold leading-tight text-pretty"
>
{t("home.banner.title2")}
</h1>
{/* Description */}
<p className="font-almarai text-base sm:text-lg text-gray-300 leading-relaxed max-w-md">
{t("home.banner.description")}
</p>
{/* CTA Button */}
<Link
href={`/${locale}/contact`}
className="font-almarai shadow-[0px_0px_2px_8px_#ff01015c] bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-8 rounded-full transition duration-300 transform hover:scale-105 w-fit"
>
{t("home.banner.cta")}
</Link>
</div>
</div>
</div>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
);
}

View File

@@ -0,0 +1,32 @@
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import { useTranslations } from "next-intl";
import Catalog from "./catalog";
export function Blog() {
const t = useTranslations();
return (
<section className="bg-[#1f1f1f] py-30">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-12 text-center">
<div className="mb-4 flex items-center justify-center gap-2">
<DotAnimatsiya />
<span className="font-almarai text-sm font-semibold tracking-wider text-white uppercase">
{t("products.banner.title")}
</span>
</div>
<h2
className="font-unbounded bg-linear-to-br from-white py-2 via-white to-black
text-transparent bg-clip-text text-4xl font-bold tracking-tight md:text-5xl lg:text-6xl"
>
{t("products.ourproducts")}
</h2>
</div>
{/* Blog Cards Grid */}
<Catalog />
</div>
</section>
);
}

View File

@@ -0,0 +1,56 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import CatalogCard from "../../products/catalog";
import CatalogCardSkeleton from "@/components/loadingSkleton";
import EmptyData from "@/components/EmptyData";
import { getRouteLang } from "@/request/getLang";
import { CategoryType } from "@/lib/types";
import { useTranslations } from "next-intl";
export default function Catalog() {
const language = getRouteLang();
const { data, isLoading } = useQuery({
queryKey: ["category", language],
queryFn: () => httpClient(endPoints.category.all),
select: (data): CategoryType[] => data?.data?.results,
});
const t = useTranslations();
if (isLoading) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(3)].map((_, index) => (
<CatalogCardSkeleton key={index} />
))}
</div>
);
}
// Ma'lumot yo'q holati
if (!data || data.length === 0) {
return (
<EmptyData
title={t("products.noData.title")}
description={t("products.noData.description")}
icon="shopping"
/>
);
}
return (
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3 place-items-center">
{data.map((item, index) => (
<CatalogCard
key={index}
id={item.id}
title={item.name}
description={item.description}
image={item.image}
have_sub_category={item.have_sub_category}
/>
))}
</div>
);
}

View File

@@ -0,0 +1,8 @@
export { Banner } from "./banner/banner";
export { Statistics } from "./statistics";
export { AboutUs } from "./about";
export { Video } from "./video";
export { OurService } from "./ourService";
export { Testimonial } from "./testimonal";
export { Line } from "./line";
export { Blog } from "./blog/blog";

View File

@@ -0,0 +1,35 @@
import { Phone } from "lucide-react";
import { useTranslations } from "next-intl";
import Image from "next/image";
export function Line() {
const t = useTranslations();
return (
<div className="bg-black py-10 px-4">
<div className="max-w-280 w-full mx-auto relative py-10 flex items-center justify-between ">
<div className="text-white flex flex-col items-start justify-start gap-5 ">
<h2 className="font-almarai lg:text-5xl sm:text-3xl text-2xl max-w-[80%] w-full font-semibold">
{t("home.contactLine.text")}
</h2>
<p className="flex items-center justify-center gap-4 font-semibold sm:text-xl text-lg">
<span
className="shrink-0 w-10 h-10 bg-red-600 rounded-full flex items-center justify-center
shadow-[0_0_0px_4px_rgba(239,68,68,0.1)]"
>
<Phone className="text-white w-5 h-5" />
</span>
+998-55-055-21-21
</p>
</div>
<Image
src="/images/home/balon.png"
alt="image"
width={60}
height={60}
priority
className=" sm:flex hidden object-contain w-80 h-80 absolute -bottom-25 right-0"
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,205 @@
"use client";
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useQuery } from "@tanstack/react-query";
import { ChevronRight } from "lucide-react";
import { useLocale, useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
import { motion } from "framer-motion";
import { ServicesLoading } from "../services/loading";
import { EmptyServices } from "../services/empty";
import { useServiceDetail } from "@/zustand/useService";
import { cardVariants, containerVariants } from "@/lib/animations";
export function OurService() {
const t = useTranslations();
const locale = useLocale();
const setServiceId = useServiceDetail((state) => state.setServiceId);
// get request
const { data, isLoading, isError } = useQuery({
queryKey: ["firesafety"],
queryFn: () => httpClient(endPoints.services.all),
select: (data) => data?.data?.data?.results.slice(0, 4),
});
return (
<div className="bg-[#1e1d1c] py-10 md:py-16 lg:py-20 mb-30">
<div className="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8">
{/* Header for github */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="space-y-4 md:space-y-6"
>
<div className="font-almarai flex items-center justify-center gap-2 text-base sm:text-lg md:text-xl text-white font-bold">
<DotAnimatsiya />
{t("home.services.title")}
</div>
<h1 className="uppercase font-unbounded text-2xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl tracking-wider lg:tracking-[5px] font-bold bg-linear-to-br from-white via-white to-gray-400 text-transparent bg-clip-text text-center w-full">
{t("home.services.subtitle")}
</h1>
<p className="font-almarai text-center text-sm sm:text-base md:text-lg text-gray-400 max-w-4xl mx-auto px-4">
{t("home.services.description")}
</p>
</motion.div>
{/* Conditional Rendering */}
{isLoading ? (
<div className="my-10">
<ServicesLoading />
</div>
) : !data || (Array.isArray(data) && data.length === 0) ? (
<div className="my-10">
<EmptyServices />
</div>
) : (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* cards */}
<div className="max-w-250 w-full mx-auto overflow-hidden flex sm:flex-row flex-col items-center gap-5 my-10">
<motion.div
variants={cardVariants}
className="sm:w-[55%] overflow-hidden w-full"
onClick={() => setServiceId(data[0].id)}
>
<Link
href={`/${locale}/services/detail`}
className="overflow-hidden block hover:cursor-pointer relative space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300"
>
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{data[0].title}
</p>
<p className="font-almarai text-gray-400 max-w-80 w-full">
{data[0].subtitle}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
<Image
src={data[0].main_image}
alt="images"
width={200}
height={100}
className="object-contain sm:absolute bottom-0 -right-2 z-10"
/>
</Link>
</motion.div>
<Link
href={`/${locale}/services/detail`}
className="sm:w-[45%] w-full"
>
<motion.div
onClick={() => setServiceId(data[1].id)}
variants={cardVariants}
>
<div className="hover:cursor-pointer relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{data[1].title}
</p>
<p className="font-almarai text-gray-400 max-w-70 w-full">
{data[1].subtitle}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
<Image
src={data[1].main_image}
alt="images"
width={200}
height={100}
className="object-contain sm:absolute -bottom-4 -right-4 z-10"
/>
</div>
</motion.div>
</Link>
</div>
<div className="max-w-250 flex sm:flex-row flex-col items-start justify-between gap-5 mt- w-full mx-auto">
<motion.div
variants={cardVariants}
onClick={() => setServiceId(data[2].id)}
className="sm:w-[40%] w-full -mt-5"
>
<Link
href={`/${locale}/services/detail`}
className="block hover:cursor-pointer relative rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300"
>
<Image
src={data[2].main_image}
alt="images"
width={300}
height={200}
className="object-contain mt-5"
/>
<div className="space-y-4 py-6 px-8">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{data[2].title}
</p>
<p className="font-almarai text-gray-400 max-w-80 w-full">
{data[2].subtitle}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
</div>
</Link>
</motion.div>
<div className="sm:w-[60%] w-full">
<motion.div
onClick={() => setServiceId(data[3].id)}
variants={cardVariants}
>
<Link href={`/${locale}/services/detail`}>
<div className="hover:cursor-pointer relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{data[3].title}
</p>
<p className="font-almarai text-gray-400 max-w-70 w-full">
{data[3].subtitle}
</p>
<button className="font-almarai sm:mt-37 mt-0 text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
{t("home.services.learnmore")}{" "}
<ChevronRight size={20} />
</button>
<Image
src={data[3].main_image}
alt="images"
width={200}
height={100}
className="object-contain sm:absolute -bottom-20 -right-4 max-sm:-mb-20 z-10"
/>
</div>
</Link>
</motion.div>
<motion.div
variants={cardVariants}
className="py-6 px-8 rounded-xl mt-5 w-full p-5 bg-[linear-gradient(to_top_right,#000000,#190b00,#542604,#8f4308)] flex sm:flex-row flex-col gap-5 items-center justify-between hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300"
>
<h2 className="font-unbounded sm:text-3xl text-xl font-semibold font-armanai text-white">
{t("home.services.viewMoreServices")}
</h2>
<Link
href={`/${locale}/services`}
className="font-almarai shadow-[0px_0px_2px_6px_#a60404ad] bg-red-600 hover:bg-red-700 text-white font-bold sm:py-3 sm:px-8 px-8 py-2 rounded-full transition duration-300 transform hover:scale-105 w-fit"
>
{t("home.services.viewMore")}
</Link>
</motion.div>
</div>
</div>
</motion.div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,85 @@
"use client";
import { Counter } from "@/components/Counter";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useQuery } from "@tanstack/react-query";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
interface Statistics {
id: number;
number: number;
hint: string;
description: string;
}
export function Statistics() {
const t = useTranslations();
const stats = [
{
id: 1,
number: 25,
hint: "+",
description: t("home.statistics.experience"),
},
{
id: 2,
number: 450,
hint: "+",
description: t("home.statistics.projectsCompleted"),
},
{
id: 3,
number: 99,
hint: "+",
description: t("home.statistics.trainedSpecialists"),
},
{
id: 4,
number: 93,
hint: "%",
description: t("home.statistics.trustedClients"),
},
];
const [stat, setStat] = useState<Statistics[]>(stats);
const { data } = useQuery({
queryKey: ["statistics"],
queryFn: () => httpClient(endPoints.statistics),
select: (data) => data?.data?.results,
});
useEffect(() => {
data && setStat(data);
}, [data]);
return (
<section className="w-full bg-black">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 lg:gap-12">
{stat.map((stat, index) => (
<div
key={index}
className="flex flex-col items-center justify-center py-10 sm:py-20 lg:py-15 border-b-red-600 border-b"
>
{/* Number and Symbol */}
<div className="flex items-baseline gap-2 font-almarai">
<span className="text-4xl sm:text-5xl lg:text-6xl font-bold text-white">
<Counter countNum={Number(stat.number)} />
</span>
<span className="text-4xl sm:text-5xl lg:text-6xl font-bold text-red-600">
{stat.hint}
</span>
</div>
{/* Label */}
<p className="font-almarai text-sm sm:text-base text-gray-300 mt-4 text-center font-medium">
{stat.description}
</p>
</div>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,144 @@
"use client";
import { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay } from "swiper/modules";
import Image from "next/image";
import "swiper/css";
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import { useTranslations } from "next-intl";
function StarRating({ rating }: { rating: number }) {
return (
<div className="flex gap-1">
{[...Array(rating)].map((_, i) => (
<svg
key={i}
className="w-4 h-4 md:w-5 md:h-5 text-yellow-400 fill-current"
viewBox="0 0 20 20"
>
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z" />
</svg>
))}
</div>
);
}
export function Testimonial() {
const t = useTranslations();
const testimonials = [
{
id: 1,
quote:t("home.testimonials.clients.john.text"),
name: t("home.testimonials.clients.john.name"),
role: t("home.testimonials.clients.john.position"),
avatar: "/images/home/avatar.jpg",
rating: 5,
},
{
id: 2,
quote:t("home.testimonials.clients.sarah.text"),
name: t("home.testimonials.clients.sarah.name"),
role: t("home.testimonials.clients.sarah.position"),
avatar: "/images/home/avatar.jpg",
rating: 5,
},
{
id: 3,
quote:t("home.testimonials.clients.michael.text"),
name: t("home.testimonials.clients.michael.name"),
role: t("home.testimonials.clients.michael.position"),
avatar: "/images/home/avatar.jpg",
rating: 5,
},
];
return (
<section className="w-full bg-[#1a1a1a]">
<div className="flex flex-col lg:flex-row min-h-100 lg:min-h-125">
{/* Left Side - Firefighter Image */}
<div className="relative w-full lg:w-1/2 h-75 sm:h-100 lg:h-auto">
<Image
src="/images/img10.jpg"
alt="Professional firefighter in protective gear"
fill
className="object-cover object-top"
priority
/>
</div>
{/* Right Side - Testimonial Content with Background */}
<div className="relative w-full lg:w-1/2 min-h-87.5 sm:min-h-100 lg:min-h-125">
{/* Background Image */}
<div className="absolute inset-0">
<Image
src="/images/home/fikrBack.jpg"
alt="Firefighters in action"
fill
className="object-cover"
/>
{/* Gradient Overlay - Direction to the right */}
<div className="absolute inset-0 bg-linear-to-r from-[#1a1a1a] via-[#1a1a1a]/80 to-transparent" />
</div>
{/* Content */}
<div className="relative z-10 h-full flex flex-col items-start justify-center px-6 sm:px-10 lg:px-12 xl:px-16 py-10 lg:py-0">
{/* Header */}
<div className="w-full max-w-xl mx-auto mb-5">
<div className="flex items-center gap-2">
<DotAnimatsiya />
<span className="font-unbounded text-white font-semibold text-sm tracking-wider">
{t("home.testimonials.title")}
</span>
</div>
</div>
<Swiper
modules={[Autoplay]}
autoplay={{
delay: 5000,
disableOnInteraction: false,
}}
loop={true}
pagination={false}
navigation={false}
className="w-full max-w-xl"
>
{testimonials.map((testimonial) => (
<SwiperSlide key={testimonial.id}>
<div className="space-y-6">
{/* Quote */}
<p className="font-almarai text-white text-base sm:text-lg lg:text-xl leading-relaxed">
"{testimonial.quote}"
</p>
{/* Author Info */}
<div className="flex items-center gap-3">
<div className="relative w-10 h-10 sm:w-12 sm:h-12 rounded-full overflow-hidden">
<Image
src={testimonial.avatar || "/placeholder.svg"}
alt={testimonial.name}
fill
className="object-cover"
/>
</div>
<div>
<h4 className="font-unbounded text-white font-bold text-sm sm:text-base">
{testimonial.name}
</h4>
<p className="font-almarai text-red-600 text-xs sm:text-sm">
{testimonial.role}
</p>
</div>
</div>
{/* Star Rating */}
<StarRating rating={testimonial.rating} />
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,32 @@
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import HomeMarquee from "@/components/HomeMarquee";
import { Play } from "lucide-react";
import { useTranslations } from "next-intl";
export function Video() {
const t = useTranslations();
return (
<>
<div
className="bg-[#1e1d1c] bg-fixed max-sm:bg-center"
style={{ backgroundImage: "url(/images/img7.jpg)" }}
>
<div className="flex flex-col items-center justify-center bg-linear-to-t from-[#1e1d1c] to-[#1e1d1c02] h-80 w-full relative">
<div className="flex items-center gap-2 w-fit text-white ">
<DotAnimatsiya />
<span className="text-sm font-semibold tracking-wide font-almarai">
{t("rasmlar")}
</span>
</div>
<h1 className="font-unbounded uppercase text-4xl bg-linear-to-br from-white via-white to-black
text-transparent bg-clip-text sm:text-5xl lg:text-6xl font-bold leading-tight text-pretty">
{t("fotogalereya")}
</h1>
</div>
</div>
<div>
<HomeMarquee />
</div>
</>
);
}

View File

@@ -0,0 +1,49 @@
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import { useTranslations } from "next-intl";
export function ProductBanner() {
const t = useTranslations();
return (
<section className="relative w-full min-[400px]:h-[60vh] h-[75vh] min-h-100 overflow-hidden pt-10">
{/* Background Image */}
<div
className="absolute inset-0 z-0"
style={{
backgroundImage: "url(/images/img4.jpg)",
backgroundSize: "cover",
backgroundPosition: "center",
}}
/>
{/* Gradient Overlay - Bottom-left to top-right */}
<div
className="absolute inset-0 z-10"
style={{
background: `linear-gradient(to top right, #d2610a 0%, #1e1d1ce3 30%, #1e1d1ce3 100%)`,
}}
/>
<div className="max-w-250 w-full mx-auto px-4">
<div className="relative z-20 h-full flex max-lg:flex-col items-start justify-between gap-5 pt-30">
<div className="spacw-y-4 ">
<div className="flex items-center gap-2 w-fit">
<DotAnimatsiya />
<span className="font-almarai text-white text-sm font-semibold tracking-wide">
{t("products.banner.title")}
</span>
</div>
<p
className="font-unbounded uppercase bg-linear-to-br from-white via-white to-black
text-transparent bg-clip-text text-3xl sm:text-4xl lg:text-5xl font-bold leading-tight text-pretty"
>
{t("products.banner.subtitle")}
</p>
</div>
<div className="font-almarai lg:w-[40%] text-gray-300 lg:mt-20 md:mt-10 sm:mt-5 ">
{t("products.banner.description")}
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,155 @@
// import { useTranslations } from "next-intl";
// import Image from "next/image";
// import Link from "next/link";
// interface CatalogProps {
// image: string;
// title: string;
// slug: string;
// description: string;
// id:string;
// }
// export default function CatalogCard({
// image,
// title,
// slug,
// description,
// id,
// }: CatalogProps) {
// const t = useTranslations();
// return (
// <Link
// href={`/products?category=${id}`}
// className="group h-118 flex flex-col items-center justify-start" // Added 'group' here
// >
// <div className="h-full flex flex-col justify-between group-hover:scale-105 transition ease-in-out">
// <p className="text-white text-2xl font-unbounded font-semibold text-center transition-colors">
// {title}
// </p>
// <p className="text-white/50 font-almarai font-medium text-center">
// {t(`${description}`)}
// </p>
// <Image
// src={image}
// alt="image"
// width={400}
// height={90}
// className="h-90! rounded-xl object-contain bg-[#444242] transition-colors duration-300" // Added smooth transition
// />
// </div>
// </Link>
// );
// }
// bg-[#444242]
"use client";
import { useLocale, useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
import { ArrowUpRight } from "lucide-react";
import { useCategory } from "@/zustand/useCategory";
import { useSubCategory } from "@/zustand/useSubCategory";
interface CatalogProps {
id: number;
image: string;
title: string;
description: string;
have_sub_category: boolean;
}
export default function CatalogCard({
image,
title,
description,
id,
have_sub_category,
}: CatalogProps) {
const t = useTranslations();
const locale = useLocale();
const setCategory = useCategory((state) => state.setCategory);
const clearSubCategory = useSubCategory((state) => state.clearSubCategory);
const item = {
image: image,
name: title,
description: description,
id: id,
have_sub_category: have_sub_category,
};
const updateZustands = () => {
setCategory(item);
clearSubCategory();
};
const navigateLink = have_sub_category
? `/${locale}/catalog_page/subCategory?category=${id}`
: `/${locale}/catalog_page/products?category=${id}`;
return (
<Link
href={navigateLink}
onClick={updateZustands}
className="group relative h-112.5 w-full overflow-hidden rounded-2xl bg-[#171616] bg-linear-to-br from-[#2a2a2a] to-black border hover:border-red-700 border-white/10 transition-all duration-500 hover:-translate-y-1"
>
{/* Background glow effect */}
<div className="absolute inset-0 bg-linear-to-t from-red-600/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
{/* Decorative corner accent */}
<div className="absolute top-0 right-0 w-24 h-24 bg-linear-to-br from-red-500/20 to-transparent rounded-bl-full opacity-0 group-hover:opacity-00 transition-opacity duration-500" />
{/* Content container */}
<div className="relative h-full flex flex-col p-6">
{/* Title section */}
<div className="mb-4">
<div className="flex items-start justify-between mb-2">
<h3 className="text-2xl font-unbounded font-bold text-white leading-tight transition-colors duration-300">
{title}
</h3>
<div className="shrink-0 w-8 h-8 rounded-full bg-white/10 flex items-center justify-center group-hover:bg-red-500 transition-all duration-300 group-hover:scale-110">
<ArrowUpRight className="w-4 h-4 text-white" strokeWidth={2.5} />
</div>
</div>
{/* Description */}
<p className="text-sm font-almarai text-white/60 line-clamp-2 group-hover:text-white/80 transition-colors duration-300">
{description}
</p>
</div>
{/* Image container with elegant frame */}
<div className="relative flex-1 rounded-xl overflow-hidden bg-linear-to-br from-[#444242] to-gray-900/50 border border-white/5 group-hover:border-white/20 transition-all duration-500">
{/* Animated gradient overlay */}
<div className="absolute inset-0 bg-linear-to-t from-black/60 via-transparent to-transparent z-10" />
{/* Image */}
<div className="relative w-full h-full">
<Image
src={image}
alt={title}
fill
className="object-contain p-4 transition-transform duration-700 group-hover:scale-105"
/>
</div>
{/* Hover shimmer effect */}
{/* <div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000" />
</div> */}
</div>
{/* Bottom accent bar */}
<div className="mt-4 h-1 w-0 bg-linear-to-r from-red-500 to-red-600 group-hover:w-full transition-all duration-500 rounded-full" />
</div>
{/* Subtle noise texture overlay */}
<div
className="absolute inset-0 pointer-events-none opacity-[0.03] mix-blend-overlay"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' /%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' /%3E%3C/svg%3E")`,
}}
/>
</Link>
);
}

View File

@@ -0,0 +1,150 @@
"use client";
import { useTranslations } from "next-intl";
import { ChevronDown, ChevronUp } from "lucide-react";
import { useCatalogHook } from "./useCataloghook";
import { useCatalog } from "@/zustand/useCatalog";
import { AnimatePresence, motion } from "framer-motion";
export function CatalogSection() {
const t = useTranslations();
const setParentID = useCatalog((state) => state.setParentID);
const parentID = useCatalog((state) => state.parentID);
const {
catalogSection,
catalogsectionChild,
setParent,
childLoading,
openDropdowns,
setOpenDropdowns,
} = useCatalogHook();
return (
<div className="p-2 border-y flex flex-col overflow-x-auto gap-2 lg:overflow-x-hidden items-start justify-start">
{/* ── Top-level categories ─────────────────────────────────────── */}
<div className="flex gap-4 items-center">
{catalogSection?.map((item: any) => (
<div key={item.id} className="flex gap-2">
<div
onClick={() => {
setParent(item.id);
setOpenDropdowns((prev) => (prev === item.id ? null : item.id));
}}
className="flex items-center gap-2"
>
<p
className={`whitespace-nowrap font-medium hover:cursor-pointer hover:text-red-500 transition-colors duration-150 ${
openDropdowns === item.id ? "text-red-500" : ""
}`}
>
{item.name}
</p>
{item.children.length > 0 && (
<motion.span
// Chevron rotates smoothly instead of swapping icons
animate={{ rotate: openDropdowns === item.id ? 180 : 0 }}
transition={{ duration: 0.25, ease: "easeInOut" }}
className={`flex h-5 w-5 items-center justify-center rounded ${
openDropdowns === item.id ? "text-red-500" : "text-gray-400"
}`}
aria-label="Dropdown icon"
>
{/*
* Single icon that rotates — replaces the ChevronUp/Down swap.
* Logic unchanged: openDropdowns === item.id still drives it.
*/}
<ChevronDown className="h-4 w-4" strokeWidth={2.5} />
</motion.span>
)}
</div>
</div>
))}
</div>
{/* ── Sub-category dropdown — animated open/close ───────────────── */}
{/*
* AnimatePresence watches its children mount/unmount.
* The `key` on the motion.div is the open dropdown's id so that
* when you switch categories, the old panel exits and new one enters.
* All original conditional logic (catalogsectionChild, childLoading,
* .length > 0, t("subcategory_not_found")) is untouched.
*/}
<AnimatePresence mode="wait">
{catalogsectionChild && openDropdowns !== null && (
<motion.div
key={openDropdowns} // re-mounts animation when category changes
initial={{ opacity: 0, height: 0, y: -6 }}
animate={{ opacity: 1, height: "auto", y: 0 }}
exit={{ opacity: 0, height: 0, y: -6 }}
transition={{
duration: 0.28,
ease: [0.25, 0.46, 0.45, 0.94], // smooth ease-out cubic
}}
style={{ overflow: "hidden" }} // required for height: 0 → auto
className="flex items-center gap-2 border-gray-400"
>
{childLoading ? (
// Loading skeleton — staggered pulse instead of plain text
<div className="flex gap-2 py-2">
{[1, 2, 3, 4].map((i) => (
<motion.div
key={i}
className="h-5 rounded bg-gray-700"
style={{ width: 72 + i * 12 }}
animate={{ opacity: [0.4, 0.8, 0.4] }}
transition={{
duration: 1.2,
repeat: Infinity,
delay: i * 0.1,
ease: "easeInOut",
}}
/>
))}
</div>
) : catalogsectionChild.length > 0 ? (
<motion.div
className="flex items-center gap-0"
// Stagger each child item so they fan in one by one
variants={{
show: { transition: { staggerChildren: 0.04 } },
hidden: {},
}}
initial="hidden"
animate="show"
>
{catalogsectionChild.map((subItem: any) => (
<motion.div
key={subItem.id}
variants={{
hidden: { opacity: 0, x: -8 },
show: { opacity: 1, x: 0 },
}}
transition={{ duration: 0.2, ease: "easeOut" }}
onClick={() => setParentID(subItem.id)}
className="border-l px-3 my-2 hover:cursor-pointer hover:text-red-500 text-white flex items-center justify-center"
>
<p
className={`text-sm whitespace-nowrap transition-colors duration-150 ${
parentID === subItem.id ? "text-red-500" : ""
}`}
>
{subItem.name}
</p>
</motion.div>
))}
</motion.div>
) : (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-sm text-gray-300 py-2"
>
{t("subcategory_not_found")}
</motion.p>
)}
</motion.div>
)}
</AnimatePresence>
</div>
);
}

View File

@@ -0,0 +1,87 @@
"use client";
import { useTranslations } from "next-intl";
import { useCategoryHook } from "./useCategory";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
export function Category() {
const t = useTranslations();
const {
categoryBack,
handleCategoryClick,
openDropdowns,
category,
subCategory,
handleSubCategoryClick,
subCategoryBack,
subCategoryLoading,
} = useCategoryHook();
return (
<div className="lg:space-y-2 space-x-6 lg:p-2 flex lg:flex-col overflow-x-auto lg:overflow-x-hidden items-start justify-start w-full">
{categoryBack?.map((item: any) => (
<div key={item.id} className="w-full">
{/* Main Category */}
<div onClick={() => handleCategoryClick(item)} className="">
<p
className={`whitespace-nowrap font-medium hover:cursor-pointer hover:text-red-500 ${category.id === item.id ? "text-red-500" : ""}`}
>
{item.name}
</p>
{item.have_sub_category && (
<span
className={`flex h-5 w-5 items-center justify-center rounded transition ${
openDropdowns[item.id] ? "text-red-500" : "text-gray-400"
}`}
aria-label="Dropdown icon"
>
{openDropdowns[item.id] ? (
<ChevronUp className="h-4 w-4" strokeWidth={2.5} />
) : (
<ChevronDown className="h-4 w-4" strokeWidth={2.5} />
)}
</span>
)}
</div>
{/* ⭐ YANGI: SubCategory Dropdown */}
{item.have_sub_category && openDropdowns[item.id] && (
<div className="space-y-2 border-l-2 border-gray-400 pl-3">
{subCategoryLoading ? (
<p className="text-sm text-gray-300">Yuklanmoqda...</p>
) : subCategoryBack && subCategoryBack.length > 0 ? (
subCategoryBack.map((subItem: any) => (
<div
key={subItem.id}
onClick={() => handleSubCategoryClick(subItem)}
className="hover:cursor-pointer flex items-center gap-2 hover:bg-gray-600 p-1.5 rounded transition-colors"
>
<span
className={`flex h-4 w-4 items-center justify-center rounded border-2 transition ${
subCategory.id === subItem.id
? "border-red-600 bg-red-600"
: "border-gray-400 bg-transparent"
}`}
aria-label="SubCategory checkbox"
>
{subCategory.id === subItem.id && (
<Check
className="h-2.5 w-2.5 text-white"
strokeWidth={3}
/>
)}
</span>
<p className="text-sm whitespace-nowrap">{subItem.name}</p>
</div>
))
) : (
<p className="text-sm text-gray-300">
{t("subcategory_not_found")}
</p>
)}
</div>
)}
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,11 @@
import { Category } from "./category";
import { CatalogSection } from "./catalog";
export default function Filter() {
return (
<div className="space-y-1 lg:px-0 mb-2 px-3 w-full text-white ">
<Category />
<CatalogSection />
</div>
);
}

View File

@@ -0,0 +1,34 @@
"use client";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
export const useCatalogHook = () => {
const [openDropdowns, setOpenDropdowns] = useState<number | undefined>(0);
const [parent, setParent] = useState(0);
const { data: catalogsectionChild, isLoading: childLoading } = useQuery({
queryKey: ["catalogsection/", parent],
queryFn: () => httpClient(endPoints.filter.child({ id: parent || 0 })),
select: (data) => data?.data?.data?.results,
enabled: !!openDropdowns,
});
const { data: catalogSection } = useQuery({
queryKey: ["catalogsection"],
queryFn: () => httpClient(endPoints.filter.catalogSection),
select: (data) => data?.data?.data?.results,
});
console.log({ catalogSection });
console.log({ catalogsectionChild });
return {
catalogSection,
catalogsectionChild,
setParent,
openDropdowns,
setOpenDropdowns,
childLoading,
};
};

View File

@@ -0,0 +1,78 @@
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useCategory } from "@/zustand/useCategory";
import { useSubCategory } from "@/zustand/useSubCategory";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
export const useCategoryHook = () => {
const [openDropdowns, setOpenDropdowns] = useState<Record<number, boolean>>(
{},
);
const category = useCategory((state) => state.category);
const setCategory = useCategory((state) => state.setCategory);
const subCategory = useSubCategory((state) => state.subCategory);
const setSubCategory = useSubCategory((state) => state.setSubCategory);
const clearSubCategory = useSubCategory((state) => state.clearSubCategory);
// Category data
const { data: categoryBack } = useQuery({
queryKey: ["category"],
queryFn: () => httpClient(endPoints.category.all),
select: (data) => data?.data?.results,
});
const { data: subCategoryBack, isLoading: subCategoryLoading } = useQuery({
queryKey: ["subCategory", category.id],
queryFn: () => httpClient(endPoints.subCategory.byId(category.id)),
enabled:
!!category.id &&
category.have_sub_category === true &&
openDropdowns[category.id] === true,
select: (data) => data?.data?.results,
});
const handleCategoryClick = (item: any) => {
if (item.have_sub_category) {
// Agar subCategory bo'lsa, dropdown ochish/yopish
setOpenDropdowns((prev) => ({
...prev,
[item.id]: !prev[item.id],
}));
// Category'ni set qilish (filterlar yangilanishi uchun)
setCategory(item);
// SubCategory'ni tozalash (yangisini tanlash uchun)
if (!openDropdowns[item.id]) {
clearSubCategory();
}
} else {
// Agar subCategory bo'lmasa, to'g'ridan-to'g'ri category ni set qilish
setCategory(item);
clearSubCategory();
// Barcha dropdown'larni yopish
setOpenDropdowns({});
}
};
const handleSubCategoryClick = (item: any) => {
setSubCategory(item);
};
return {
category,
setCategory,
subCategory,
setSubCategory,
clearSubCategory,
categoryBack,
subCategoryBack,
subCategoryLoading,
handleCategoryClick,
handleSubCategoryClick,
openDropdowns,
};
};

View File

@@ -0,0 +1,5 @@
export { ProductBanner } from "./banner";
export { Products } from "./product/products";
export { SliderComp } from "./slug/slider";
export { RightSide } from "./slug/rightSide";
export { Features } from "./slug/features";

View File

@@ -0,0 +1,135 @@
"use client";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useQuery } from "@tanstack/react-query";
import ProductCard from "./productCard";
import { useCategory } from "@/zustand/useCategory";
import { useFilter } from "@/lib/filter-zustand";
import { useMemo, useState, useEffect } from "react";
import { useProductPageInfo } from "@/zustand/useProduct";
import { useSubCategory } from "@/zustand/useSubCategory";
import { useTranslations } from "next-intl";
import PaginationLite from "@/components/paginationUI";
import { useCatalog } from "@/zustand/useCatalog";
export default function MainProduct() {
const t = useTranslations();
const category = useCategory((s) => s.category);
const subCategory = useSubCategory((s) => s.subCategory);
const filter = useFilter((s) => s.filter);
const getFiltersByType = useFilter((s) => s.getFiltersByType);
const setProduct = useProductPageInfo((s) => s.setProducts);
const parentID = useCatalog((state) => state.parentID);
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
setCurrentPage(1);
}, [parentID]);
// ── Filter params ────────────────────────────────────────────────────────
const queryParams = useMemo(() => {
const catalog = getFiltersByType("catalog");
const size = getFiltersByType("size");
const catalogParams = catalog.map((i) => `catalog=${i.id}`).join("&");
const sizeParams = size.map((i) => `size=${i.id}`).join("&");
const allParams = [catalogParams, sizeParams].filter(Boolean).join("&");
setCurrentPage(1);
return allParams ? `&${allParams}` : "";
}, [filter]);
// ── Request URL ──────────────────────────────────────────────────────────
const requestLink = useMemo(() => {
const baseLink = category.have_sub_category
? endPoints.product.bySubCategory({ id: subCategory.id, currentPage })
: parentID
? endPoints.product.byCatalogSection({ id: parentID, currentPage })
: endPoints.product.byCategory({ id: category.id, currentPage });
return `${baseLink}${queryParams}`;
}, [
category.id,
category.have_sub_category,
subCategory.id,
currentPage,
parentID,
queryParams,
]);
// ── Query ────────────────────────────────────────────────────────────────
const { data, isLoading, error } = useQuery({
queryKey: [
"products",
category.id,
category.have_sub_category,
subCategory.id,
parentID,
queryParams,
currentPage,
],
queryFn: () => httpClient(requestLink),
placeholderData: (prev) => prev, // no flicker on pagination
select: (res) => ({
results: res?.data?.data?.results ?? [],
totalPages: res?.data?.data?.total_pages ?? 1,
}),
});
const results = data?.results ?? [];
const totalPages = data?.totalPages ?? 1;
// ── Render states ────────────────────────────────────────────────────────
if (isLoading && !data) {
return (
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
{[1, 2, 3].map((i) => (
<div key={i} className="h-96 bg-gray-800 animate-pulse rounded-2xl" />
))}
</div>
);
}
if (error) {
return (
<div className="text-center text-red-500 py-10">{t("loadingError")}</div>
);
}
if (!results.length) {
return (
<div className="text-center text-gray-400 py-10">
{t("productsNotFound")}
</div>
);
}
return (
<div className="space-y-4">
<div
className={`grid lg:grid-cols-4 sm:grid-cols-2 grid-cols-1 gap-5 transition-opacity ${
isLoading ? "opacity-50 pointer-events-none" : "opacity-100"
}`}
>
{results.map((item: any) => (
<ProductCard
key={item.id}
getProduct={() => setProduct(item)}
title={item.name}
image={item?.images?.[0]?.image || ""}
slug="special_product"
/>
))}
</div>
{totalPages > 1 && (
<PaginationLite
currentPage={currentPage}
totalPages={totalPages}
onChange={(p) => setCurrentPage(p)}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,161 @@
"use client";
import { useLocale, useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
interface ProductCardProps {
title: string;
image: string;
slug: string;
getProduct: () => void;
}
export default function ProductCard({
title,
image,
slug,
getProduct,
}: ProductCardProps) {
const locale = useLocale();
const t = useTranslations();
return (
<Link
href={`/${locale}/catalog_page/products/${slug}`}
onClick={getProduct}
className="group block"
>
<article className="
relative
bg-neutral-900
border border-zinc-800
rounded-xl
overflow-hidden
transition-all duration-300
hover:border-red-600/60
hover:shadow-[0_0_24px_0_rgba(220,38,38,0.15)]
max-sm:max-w-100 max-sm:mx-auto max-sm:w-full
h-95 flex flex-col
">
{/* Top accent line */}
<div className="
absolute top-0 left-0 right-0 h-0.5
bg-linear-to-r from-transparent via-red-600 to-transparent
opacity-0 group-hover:opacity-100
transition-opacity duration-300
z-10
" />
{/* Image Container */}
<div className="
relative
h-48 sm:h-56 md:h-64
bg-zinc-900
border-b border-zinc-800
overflow-hidden
">
{/* Background pattern */}
<div className="
absolute inset-0
bg-[radial-gradient(circle_at_center,_rgba(39,39,42,0.8)_0%,_rgba(9,9,11,1)_100%)]
" />
<Image
src={image || "/placeholder.svg"}
alt={title}
fill
className="
object-contain
p-4
transition-transform duration-500
group-hover:scale-105
drop-shadow-[0_4px_12px_rgba(0,0,0,0.5)]
"
sizes="(max-width: 640px) 90vw, (max-width: 1024px) 45vw, 30vw"
/>
{/* Hover overlay */}
<div className="
absolute inset-0
bg-red-600/5
opacity-0 group-hover:opacity-100
transition-opacity duration-300
" />
</div>
{/* Content */}
<div className="p-5">
{/* Decorative line */}
<div className="
w-8 h-0.5
bg-red-600
mb-3
transition-all duration-300
group-hover:w-16
" />
<h3 className="
text-sm sm:text-base
font-bold
text-zinc-100
line-clamp-3
leading-snug
tracking-wide
transition-colors duration-300
group-hover:text-white
">
{title}
</h3>
{/* Bottom row */}
<div className="
flex items-center justify-between
mt-4 pt-4
border-t border-zinc-800
">
<span className="
text-xs
text-zinc-500
tracking-widest
uppercase
font-medium
">
{ t("home.services.learnmore")}
</span>
{/* Arrow */}
<div className="
flex items-center justify-center
w-7 h-7
rounded-full
border border-zinc-700
text-zinc-500
transition-all duration-300
group-hover:border-red-600
group-hover:text-red-500
group-hover:bg-red-600/10
">
<svg
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2 6H10M10 6L7 3M10 6L7 9"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
</div>
</div>
</article>
</Link>
);
}

View File

@@ -0,0 +1,18 @@
import Filter from "../filter/filter";
import MainProduct from "./mianProduct";
export function Products() {
return (
<div className="bg-[#1e1d1c] pb-10 pt-5 px-2">
<div className="max-w-300 mx-auto w-full z-20 relative">
<div className="flex flex-col items-start gap-2">
{/* filter part */}
<Filter />
{/* main products */}
<MainProduct />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,36 @@
// Empty State Component
export function EmptyState() {
return (
<div className="min-h-screen bg-[#1e1d1c] flex items-center justify-center px-4">
<div className="text-center max-w-md">
<div className="w-24 h-24 mx-auto mb-6 bg-gray-800 rounded-full flex items-center justify-center">
<svg
className="w-12 h-12 text-gray-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
</div>
<h2 className="text-2xl font-bold text-white mb-2">
Mahsulot topilmadi
</h2>
<p className="text-gray-400 mb-6">
Siz qidirayotgan mahsulot mavjud emas yoki o'chirilgan
</p>
<a
href="/products"
className="inline-block bg-red-700 hover:bg-red-800 text-white font-bold py-3 px-6 rounded-lg transition"
>
Mahsulotlarga qaytish
</a>
</div>
</div>
);
}

View File

@@ -0,0 +1,44 @@
import { useTranslations } from "next-intl";
export function Features({ features }: { features: string[] }) {
const t = useTranslations();
if (!features || features.length === 0) {
return null;
}
return (
<div className="mt-12">
<h2 className="text-2xl md:text-3xl font-bold text-white mb-6">
{t("products.features")}
</h2>
<div className="rounded-xl overflow-hidden border border-gray-800 shadow-xl">
<table className="w-full">
<thead>
<tr className="bg-linear-to-r from-stone-800 to-black/10 border-b border-gray-800">
<th className="px-4 py-4 md:px-6 text-left text-sm md:text-base font-semibold text-white">
{t("products.feature")}
</th>
</tr>
</thead>
<tbody>
{features.map((feature, index) => (
<tr
key={index}
className={`border-b border-gray-800 last:border-b-0 transition-colors hover:bg-red-900/10 ${
index % 2 === 0 ? "bg-[#252525]" : "bg-[#1e1e1e]"
}`}
>
<td className="px-4 py-4 md:px-6 text-sm md:text-base text-gray-300">
<div className="flex items-start gap-3">
<span className="text-red-700 mt-1"></span>
<span>{feature}</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}

View File

@@ -0,0 +1,61 @@
// Loading Skeleton Component
export function LoadingSkeleton() {
return (
<div className="min-h-screen bg-[#1e1d1c] py-20 md:py-32 lg:py-40 px-4 md:px-8">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 mb-12">
{/* Image Skeleton */}
<div className="w-full h-96 md:h-125 bg-gray-800 rounded-lg animate-pulse" />
{/* Info Skeleton */}
<div className="flex flex-col justify-center space-y-4">
{/* Title */}
<div className="h-8 bg-gray-800 rounded animate-pulse w-3/4" />
{/* Articular */}
<div className="h-6 bg-gray-800 rounded animate-pulse w-1/2" />
{/* Status */}
<div className="h-8 bg-gray-800 rounded animate-pulse w-1/3" />
{/* Description */}
<div className="space-y-2">
<div className="h-4 bg-gray-800 rounded animate-pulse w-full" />
<div className="h-4 bg-gray-800 rounded animate-pulse w-5/6" />
<div className="h-4 bg-gray-800 rounded animate-pulse w-4/6" />
</div>
{/* Price */}
<div className="h-10 bg-gray-800 rounded animate-pulse w-32" />
{/* Buttons */}
<div className="flex gap-4">
<div className="h-12 bg-gray-800 rounded animate-pulse flex-1" />
</div>
{/* Social */}
<div className="flex gap-2">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div
key={i}
className="w-10 h-10 bg-gray-800 rounded-lg animate-pulse"
/>
))}
</div>
</div>
</div>
{/* Features Skeleton */}
<div className="space-y-2">
<div className="h-12 bg-gray-800 rounded-t-xl animate-pulse" />
{[1, 2, 3, 4].map((i) => (
<div
key={i}
className="h-16 bg-gray-800/50 rounded animate-pulse"
/>
))}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,154 @@
"use client";
import { usePriceModalStore } from "@/zustand/useProceModalStore";
import { Check, Instagram, Send, Share2 } from "lucide-react";
import { useTranslations } from "next-intl";
import { useParams } from "next/navigation";
import { useState } from "react";
interface RightSideProps {
id: number;
title: string;
articular: string;
status: string;
description: string;
price: string;
image: string;
}
export function RightSide({
title,
articular,
status,
description,
price,
id,
image,
}: RightSideProps) {
const openModal = usePriceModalStore((state) => state.openModal);
const t = useTranslations();
const params = useParams();
const locale = params.locale || "uz";
const [copied, setCopied] = useState(false);
const handleShare = async () => {
const productUrl = `${window.location.origin}/${locale}/catalog_page/products/special_product?productId=${id}`;
try {
// Modern Web Share API dan foydalanish (mobil qurilmalar uchun)
if (navigator.share) {
await navigator.share({
title: title,
text: `${title} - ${description.slice(0, 100)}...`,
url: productUrl,
});
} else {
// Desktop uchun clipboard ga copy qilish
await navigator.clipboard.writeText(productUrl);
setCopied(true);
// 2 soniyadan keyin "Copied" holatini o'chirish
setTimeout(() => {
setCopied(false);
}, 2000);
}
} catch (error) {
console.error("Share error:", error);
// Fallback - clipboard ga copy qilish
try {
await navigator.clipboard.writeText(productUrl);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 2000);
} catch (clipboardError) {
console.error("Clipboard error:", clipboardError);
}
}
};
const handleGetPrice = () => {
openModal({
id,
name: title,
image,
inStock: status === "Sotuvda mavjud",
});
};
// Status color logic
const isInStock = status === "Sotuvda mavjud";
const statusColor = isInStock
? "bg-green-600/20 text-green-400 border border-green-600/30"
: "bg-red-600/20 text-red-400 border border-red-600/30";
return (
<div className="flex flex-col justify-center space-y-6">
{/* Title */}
<h1 className="text-xl md:text-3xl font-unbounded font-bold text-white leading-tight">
{title}
</h1>
{/* Article ID */}
<div className="flex items-center gap-2 text-sm md:text-base">
<span className="text-gray-400">Artikul:</span>
<span className="text-white font-semibold">{articular}</span>
</div>
{/* Status Badge */}
<div>
<span
className={`inline-block px-4 py-2 rounded-lg text-sm font-semibold ${statusColor}`}
>
{status}
</span>
</div>
{/* Description */}
<div className="border-l-4 border-red-700 pl-4">
<p className="text-sm md:text-base text-gray-300 leading-relaxed">
{description}
</p>
</div>
{/* Price Section */}
<div className="bg-[#1716169f] rounded-xl p-5 space-y-6">
{/* Action Button */}
<button
onClick={handleGetPrice}
className="w-full bg-red-700 hover:bg-red-800 text-white font-bold py-4 px-6 rounded-lg transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-red-700/50"
>
{t("products.send")}
</button>
{/* Social Share */}
<div className="pt-4 border-t border-gray-800 flex items-center gap-5">
<button
onClick={handleShare}
className="flex items-center gap-3 mb-3 text-gray-400 hover:text-white transition-colors group"
>
{copied ? (
<>
<Check className="w-5 h-5 text-green-400" />
<span className="text-sm text-green-400">
{t("products.copied") || "Link nusxalandi!"}
</span>
</>
) : (
<>
<Share2 className="w-5 h-5 group-hover:scale-110 transition-transform" />
<span className="text-sm">{t("products.share")}:</span>
</>
)}
</button>
<a
href="https://t.me/ignum_tech"
className="p-2 rounded-md bg-white text-red-500 hover:text-white hover:bg-red-500"
>
<Send />
</a>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,158 @@
"use client";
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Pagination, Thumbs } from "swiper/modules";
import { useState } from "react";
import type { Swiper as SwiperType } from "swiper";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import "swiper/css/thumbs";
import Image from "next/image";
import { useTranslations } from "next-intl";
const navigationPrevEl = ".custom-swiper-prev";
const navigationNextEl = ".custom-swiper-next";
export function SliderComp({ imgs }: { imgs: string[] }) {
const [thumbsSwiper, setThumbsSwiper] = useState<SwiperType | null>(null);
const t = useTranslations();
// Agar rasm bo'lmasa
if (!imgs || imgs.length === 0) {
return (
<div className="w-full h-96 md:h-125 bg-gray-800 rounded-lg flex items-center justify-center">
<div className="text-center">
<svg
className="w-20 h-20 text-gray-600 mx-auto mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<p className="text-gray-500">{t("image_not_found")}</p>
</div>
</div>
);
}
return (
<div className="space-y-4">
{/* Main Slider */}
<div className="relative group">
<Swiper
modules={[Navigation, Pagination, Thumbs]}
thumbs={{
swiper:
thumbsSwiper && !thumbsSwiper.destroyed ? thumbsSwiper : null,
}}
navigation={{
prevEl: navigationPrevEl,
nextEl: navigationNextEl,
}}
pagination={{ clickable: true }}
loop={imgs.length > 1}
className="w-[90%] h-96 md:h-96 rounded-lg overflow-hidden shadow-xl"
>
{imgs.map((image, index) => (
<SwiperSlide
key={index}
className=" flex items-center justify-center"
>
<div className="relative w-full h-full p-4 md:p-8">
<Image
src={image}
alt={`Product image ${index + 1}`}
fill
className="object-contain"
sizes="(max-width: 768px) 100vw, 50vw"
priority={index === 0}
/>
</div>
</SwiperSlide>
))}
</Swiper>
{/* Navigation Buttons */}
{imgs.length > 1 && (
<>
<button
className={`${navigationPrevEl.replace(
".",
"",
)} absolute z-10 top-1/2 -translate-y-1/2 left-2 md:left-4 rounded-lg w-10 h-10 md:w-12 md:h-12 bg-red-700/90 hover:bg-red-800 text-white flex items-center justify-center transition opacity-0 group-hover:opacity-100 shadow-lg`}
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</button>
<button
className={`${navigationNextEl.replace(
".",
"",
)} absolute z-10 top-1/2 -translate-y-1/2 right-2 md:right-4 rounded-lg w-10 h-10 md:w-12 md:h-12 bg-red-700/90 hover:bg-red-800 text-white flex items-center justify-center transition opacity-0 group-hover:opacity-100 shadow-lg`}
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</button>
</>
)}
</div>
{/* Thumbnail Slider */}
{imgs.length > 1 && (
<Swiper
onSwiper={setThumbsSwiper}
spaceBetween={10}
slidesPerView={4}
breakpoints={{
640: { slidesPerView: 5 },
768: { slidesPerView: 6 },
}}
watchSlidesProgress
className="w-full"
>
{imgs.map((image, index) => (
<SwiperSlide key={index}>
<div className="relative h-20 md:h-24 bg-white rounded-lg overflow-hidden cursor-pointer border-2 border-transparent hover:border-red-700 transition">
<Image
src={image}
alt={`Thumbnail ${index + 1}`}
fill
className="object-contain p-2"
sizes="100px"
/>
</div>
</SwiperSlide>
))}
</Swiper>
)}
</div>
);
}

View File

@@ -0,0 +1,72 @@
import { useTranslations } from "next-intl";
import { motion } from "framer-motion";
// Empty State Component
export function EmptyServices() {
const t = useTranslations();
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="max-w-250 w-full mx-auto py-20"
>
<div className="bg-[#171616] bg-linear-to-br from-[#2a2a2a] to-black rounded-2xl p-10 md:p-16 text-center border border-gray-500">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
className="mb-8"
>
<div className="w-24 h-24 mx-auto bg-red-500/10 rounded-full flex items-center justify-center">
<svg
className="w-12 h-12 text-red-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
</div>
</motion.div>
<motion.h3
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
className="text-2xl md:text-3xl font-bold text-white font-unbounded mb-4"
>
{t("operationalSystems.noData.title") || "Xizmatlar topilmadi"}
</motion.h3>
<motion.p
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="text-gray-400 font-almarai text-base md:text-lg mb-8 max-w-md mx-auto"
>
{t("operationalSystems.noData.description") || "Hozircha hech qanday xizmat mavjud emas. Tez orada yangi xizmatlar qo'shiladi."}
</motion.p>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
>
<button
onClick={() => window.location.reload()}
className="font-almarai bg-red-600 hover:bg-red-700 text-white font-semibold py-3 px-8 rounded-full transition-all duration-300 transform hover:scale-105 shadow-lg hover:shadow-red-500/50"
>
{t("operationalSystems.noData.empty") || "Qayta yuklash"}
</button>
</motion.div>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,3 @@
export { ServiceBanner } from "./serviceBanner";
export { ServiceFaq } from "./serviceFaq";
export { ServicePageServices } from "./servicePageServices";

View File

@@ -0,0 +1,45 @@
import { motion } from "framer-motion";
// Loading Skeleton Component
function ServiceCardSkeleton() {
return (
<div className="animate-pulse space-y-4 py-6 px-8 rounded-xl bg-[#171616] bg-linear-to-br from-[#2a2a2a] to-black">
<div className="h-6 bg-[#171616] rounded w-3/4"></div>
<div className="h-4 bg-[#171616] rounded w-full"></div>
<div className="h-4 bg-[#171616] rounded w-5/6"></div>
<div className="h-8 bg-[#171616] rounded w-32"></div>
</div>
);
}
// Loading Component
export function ServicesLoading() {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="max-w-250 w-full mx-auto space-y-5"
>
{/* Top Cards Skeleton */}
<div className="flex sm:flex-row flex-col items-center gap-5">
<div className="sm:w-[55%] w-full">
<ServiceCardSkeleton />
</div>
<div className="sm:w-[45%] w-full">
<ServiceCardSkeleton />
</div>
</div>
{/* Bottom Cards Skeleton */}
<div className="flex sm:flex-row flex-col items-start justify-between gap-5">
<div className="sm:w-[40%] w-full">
<ServiceCardSkeleton />
</div>
<div className="sm:w-[60%] w-full space-y-5">
<ServiceCardSkeleton />
<div className="h-24 bg-[#171616] rounded-xl animate-pulse"></div>
</div>
</div>
</motion.div>
);
}

View File

@@ -0,0 +1,49 @@
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import { useTranslations } from "next-intl";
export function ServiceBanner() {
const t = useTranslations();
return (
<section className="relative w-full h-[60vh] min-h-100 overflow-hidden pt-10">
{/* Background Image */}
<div
className="absolute inset-0 z-0"
style={{
backgroundImage: "url(/images/services/serviceBanner.jpg)",
backgroundSize: "cover",
backgroundPosition: "center",
}}
/>
{/* Gradient Overlay - Bottom-left to top-right */}
<div
className="absolute inset-0 z-10"
style={{
background: `linear-gradient(to right top, rgb(157 73 9) 0%, rgb(33 32 31 / 89%) 40%, rgba(30, 29, 28, 0.89) 100%)`,
}}
/>
<div className="max-w-250 w-full mx-auto px-4">
<div className="relative z-20 h-full flex max-lg:flex-col items-start justify-between gap-5 pt-30">
<div className="spacw-y-4 ">
<div className="flex items-center gap-3">
<DotAnimatsiya />
<span className="font-almarai text-sm text-white font-semibold tracking-wide">
{t("services.banner.title")}
</span>
</div>
<p
className="font-unbounded uppercase bg-linear-to-br from-white via-white to-black
text-transparent bg-clip-text text-4xl sm:text-5xl lg:text-6xl font-bold leading-tight text-pretty"
>
{t("services.banner.subtitle")}
</p>
</div>
<div className="font-almarai lg:w-[40%] text-gray-300 mt-10 text-lg">
{t("services.banner.description")}
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,56 @@
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import FAQAccordion, { FAQItem } from "../faq/faqAccardion";
import { useLocale, useTranslations } from "next-intl";
export function ServiceFaq() {
const t = useTranslations();
const locale = useLocale();
const faqItems: FAQItem[] = [
{
id: 1,
question: t("faq.question1.question"),
answer: t("faq.question1.answer"),
},
{
id: 2,
question: t("faq.question2.question"),
answer: t("faq.question2.answer"),
},
{
id: 3,
question: t("faq.question3.question"),
answer: t("faq.question3.answer"),
},
{
id: 4,
question: t("faq.question4.question"),
answer: t("faq.question4.answer"),
},
{
id: 5,
question: t("faq.question5.question"),
answer: t("faq.question5.answer"),
},
];
return (
<div className="bg-[#1e1d1c] py-20 pb-50 space-y-8">
{/* header */}
<div className="space-y-4 w-full ">
<div className="flex items-center gap-3 justify-center text-white text-xl">
<DotAnimatsiya /> {locale === "ru" ? "ФАК" : "FAQ"}
</div>
<h1
className="text-center bg-linear-to-br from-white via-white/50 to-black
text-transparent bg-clip-text text-3xl font-bold uppercase leading-tight sm:text-4xl md:text-5xl lg:text-6xl"
>
{t("faq.banner.topic")}
</h1>
</div>
{/* FAQ Section */}
<div className="md:col-span-2 max-w-6xl mx-auto w-full px-2">
<FAQAccordion items={faqItems} />
</div>
</div>
);
}

View File

@@ -0,0 +1,220 @@
"use client";
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useQuery } from "@tanstack/react-query";
import { ChevronRight } from "lucide-react";
import { useLocale, useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
import { motion } from "framer-motion";
import { ServicesLoading } from "./loading";
import { EmptyServices } from "./empty";
import { useServiceDetail } from "@/zustand/useService";
import { cardVariants, containerVariants } from "@/lib/animations";
export function ServicePageServices() {
const t = useTranslations();
const locale = useLocale();
const setServiceId = useServiceDetail((state) => state.setServiceId);
// get request
const { data, isLoading, isError } = useQuery({
queryKey: ["firesafety"],
queryFn: () => httpClient(endPoints.services.all),
select: (data) => {
const serviceData = data?.data?.data?.results;
return serviceData.reduce(
(resultArray: any, item: any, index: number) => {
const chunkIndex = Math.floor(index / 4);
if (!resultArray[chunkIndex]) {
resultArray[chunkIndex] = []; // Yangi chunk boshlash
}
resultArray[chunkIndex].push(item);
return resultArray;
},
[] as any[][],
);
},
});
return (
<div className="bg-[#1e1d1c] py-10 md:py-16 lg:py-20 mb-15">
<div className="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8">
{/* Header for github */}
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
className="space-y-4 md:space-y-6"
>
<div className="font-almarai flex items-center justify-center gap-2 text-base sm:text-lg md:text-xl text-white font-bold">
<DotAnimatsiya />
{t("home.services.title")}
</div>
<h1 className="uppercase font-unbounded text-2xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl tracking-wider lg:tracking-[5px] font-bold bg-linear-to-br from-white via-white to-gray-400 text-transparent bg-clip-text text-center w-full">
{t("home.services.subtitle")}
</h1>
<p className="font-almarai text-center text-sm sm:text-base md:text-lg text-gray-400 max-w-4xl mx-auto px-4">
{t("home.services.description")}
</p>
</motion.div>
{/* Conditional Rendering */}
{isLoading ? (
<div className="my-10">
<ServicesLoading />
</div>
) : !data || (Array.isArray(data) && data.length === 0) ? (
<div className="my-10">
<EmptyServices />
</div>
) : (
data.map((item: any, index: number) => (
<motion.div
key={index}
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* cards */}
<div className="max-w-250 w-full mx-auto overflow-hidden flex sm:flex-row flex-col items-center gap-3 my-4">
{item[0] && (
<motion.div
variants={cardVariants}
className="sm:w-[55%] overflow-hidden w-full"
onClick={() => setServiceId(item[0].id)}
>
<Link
href={`/${locale}/services/detail`}
className="overflow-hidden block hover:cursor-pointer relative space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300"
>
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{item[0]?.title}
</p>
<p className="font-almarai text-gray-400 max-w-80 w-full">
{item[0]?.subtitle}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
{t("home.services.learnmore")}{" "}
<ChevronRight size={20} />
</button>
<Image
src={item[0]?.main_image}
alt="images"
width={200}
height={100}
className="object-contain sm:absolute bottom-0 -right-2 z-10"
/>
</Link>
</motion.div>
)}
{item[1] && (
<Link
href={`/${locale}/services/detail`}
className="sm:w-[45%] w-full"
>
<motion.div
onClick={() => setServiceId(item[1]?.id)}
variants={cardVariants}
>
<div className="hover:cursor-pointer relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{item[1]?.title}
</p>
<p className="font-almarai text-gray-400 max-w-70 w-full">
{item[1]?.subtitle}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
{t("home.services.learnmore")}{" "}
<ChevronRight size={20} />
</button>
<Image
src={item[1]?.main_image}
alt="images"
width={200}
height={100}
className="object-contain sm:absolute -bottom-4 -right-4 z-10"
/>
</div>
</motion.div>
</Link>
)}
</div>
<div className="max-w-250 flex sm:flex-row flex-col items-start justify-between gap-3 w-full mx-auto">
{item[2] && (
<motion.div
variants={cardVariants}
onClick={() => setServiceId(item[2]?.id)}
className="sm:w-[40%] w-full -mt-5"
>
<Link
href={`/${locale}/services/detail`}
className="block hover:cursor-pointer relative rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300"
>
<Image
src={item[2]?.main_image}
alt="images"
width={250}
height={200}
className="object-contain mt-5"
/>
<div className="space-y-1 pb-3 px-5">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{item[2]?.title}
</p>
<p className="font-almarai text-gray-400 max-w-80 w-full">
{item[2]?.subtitle}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
{t("home.services.learnmore")}{" "}
<ChevronRight size={20} />
</button>
</div>
</Link>
</motion.div>
)}
<div className="sm:w-[60%] w-full">
{item[3] && (
<motion.div
onClick={() => setServiceId(item[3]?.id)}
variants={cardVariants}
>
<Link href={`/${locale}/services/detail`}>
<div className="hover:cursor-pointer relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{item[3]?.title}
</p>
<p className="font-almarai text-gray-400 max-w-70 w-full">
{item[3]?.subtitle}
</p>
<button className="font-almarai sm:mt-40 mt-0 text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
{t("home.services.learnmore")}{" "}
<ChevronRight size={20} />
</button>
<Image
src={item[3]?.main_image}
alt="images"
width={200}
height={100}
className="object-contain sm:absolute -bottom-20 -right-4 max-sm:-mb-20 z-10"
/>
</div>
</Link>
</motion.div>
)}
</div>
</div>
</motion.div>
))
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,58 @@
"use client";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useQuery } from "@tanstack/react-query";
import { useCategory } from "@/zustand/useCategory";
import Card from "./card";
import { useTranslations } from "next-intl";
export function MainSubCategory() {
const category = useCategory((state) => state.category);
const t = useTranslations();
const { data, isLoading, error } = useQuery({
queryKey: ["subCategory"],
queryFn: () => httpClient(endPoints.subCategory.byId(category.id)),
select: (data) => data?.data?.results,
});
if (isLoading) {
return (
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
{[1, 2, 3].map((i) => (
<div key={i} className="h-96 bg-gray-800 animate-pulse rounded-2xl" />
))}
</div>
);
}
if (error) {
return (
<div className="text-center text-red-500 py-10">
{t("loading_error")}
</div>
);
}
if (!data || data.length === 0) {
return (
<div className="text-center text-gray-400 py-10">
{t("products_not_found")}
</div>
);
}
return (
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
{data.map((item: any) => (
<Card
key={item.id}
title={item.name}
image={item.image}
slug={item.slug}
id={item.id}
category={item.category}
/>
))}
</div>
);
}

View File

@@ -0,0 +1,60 @@
import { useSubCategory } from "@/zustand/useSubCategory";
import { useLocale } from "next-intl";
import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
interface ProductCardProps {
id: number;
category: number;
title: string;
image: string;
slug: string;
}
export default function Card({
title,
image,
slug,
id,
category,
}: ProductCardProps) {
const locale = useLocale();
const router = useRouter();
const setSubCategory = useSubCategory((state) => state.setSubCategory);
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
setSubCategory({
id,
name: title,
image,
category,
});
router.push(`/${locale}/catalog_page/products`);
};
return (
<Link href="#" onClick={handleClick}>
<article className="group transition-all duration-300 hover:cursor-pointer max-sm:max-w-100 max-sm:mx-auto max-sm:w-full">
{/* Image Container */}
<div className="relative rounded-2xl h-45 sm:h-55 md:h-65 lg:w-[95%] w-[90%] mx-auto overflow-hidden">
<Image
src={image || "/placeholder.svg"}
alt={title}
fill
className="object-contain transition-transform duration-300 group-hover:scale-105"
sizes="(max-width: 640px) 90vw, (max-width: 1024px) 45vw, 30vw"
/>
</div>
{/* Content Container */}
<div className="p-6 sm:p-4">
<h3 className="text-lg text-center font-unbounded font-bold text-white mb-4 line-clamp-3 group-hover:text-red-400 transition-colors duration-300">
{title}
</h3>
</div>
</article>
</Link>
);
}

View File

@@ -0,0 +1 @@
export { MainSubCategory } from "./body";