Compare commits

..

48 Commits

Author SHA1 Message Date
nabijonovdavronbek619@gmail.com
332ff87c58 payment not found 2026-03-10 17:42:02 +05:00
nabijonovdavronbek619@gmail.com
79436a9b9d payment not found 2026-03-10 17:23:20 +05:00
nabijonovdavronbek619@gmail.com
f157c56b93 added filtering by catalo parent 2026-03-10 12:16:26 +05:00
nabijonovdavronbek619@gmail.com
d03a340afb updated compoennt file structure 2026-03-09 13:03:17 +05:00
nabijonovdavronbek619@gmail.com
aba11a939a get product request fixed from catalog_selection bug 2026-03-08 12:41:11 +05:00
nabijonovdavronbek619@gmail.com
06ac90c391 get product request fixed from catalog_selection bug 2026-03-08 12:39:16 +05:00
nabijonovdavronbek619@gmail.com
f396125acf add additional filter to product page 2026-03-07 21:16:56 +05:00
nabijonovdavronbek619@gmail.com
809438735f file name and location updayed for better be 2026-03-07 16:31:18 +05:00
nabijonovdavronbek619@gmail.com
b838025ab0 clear filter 2026-03-06 20:54:22 +05:00
nabijonovdavronbek619@gmail.com
cd7d6bb208 chceck api working 2026-03-05 17:25:45 +05:00
nabijonovdavronbek619@gmail.com
9cc151a796 baza , notepp, sertificate pages connected to backend 2026-03-05 17:14:58 +05:00
nabijonovdavronbek619@gmail.com
dad1070807 show case slider connected to backend 2026-03-05 10:50:24 +05:00
nabijonovdavronbek619@gmail.com
a6c1e4644a navbar component conneceted to backend 2026-03-05 10:29:05 +05:00
nabijonovdavronbek619@gmail.com
2b8e86e305 last push(I hope) 2026-03-04 12:29:07 +05:00
nabijonovdavronbek619@gmail.com
1e12790e5f translation updated 2026-03-03 15:22:30 +05:00
nabijonovdavronbek619@gmail.com
41ae5e4c49 show case banner updated 2026-03-03 14:54:42 +05:00
nabijonovdavronbek619@gmail.com
2babb32e6a pagination page_size updated , breadcrumb color changed , sub link text changed (ru) on navbar 2026-03-03 14:46:35 +05:00
nabijonovdavronbek619@gmail.com
03ea2d51e4 complated: Guides , sertificate , baza. added dropdown to about navigation button on navbar 2026-03-03 11:28:05 +05:00
nabijonovdavronbek619@gmail.com
aca4103213 baza page complated 2026-03-03 11:22:49 +05:00
nabijonovdavronbek619@gmail.com
68277d4b4c sertificate , guides pages done 2026-03-03 10:46:27 +05:00
nabijonovdavronbek619@gmail.com
e62286effa sertifiacte page updated 2026-03-02 20:14:15 +05:00
nabijonovdavronbek619@gmail.com
a0f8ef76d7 remove middlewere 2026-03-02 17:27:38 +05:00
nabijonovdavronbek619@gmail.com
11a18b52ce remove middlewere 2026-03-02 17:25:05 +05:00
nabijonovdavronbek619@gmail.com
960010ba7b remove middlewere 2026-03-02 17:23:35 +05:00
nabijonovdavronbek619@gmail.com
a7682a8178 navbar updated mobile 2026-03-02 15:09:43 +05:00
nabijonovdavronbek619@gmail.com
8aa5ead09c navbar updated 2026-03-02 15:06:46 +05:00
nabijonovdavronbek619@gmail.com
bfc9b85026 all page is done 2026-03-02 13:04:37 +05:00
nabijonovdavronbek619@gmail.com
1104c55bea all page is done 2026-03-02 12:53:29 +05:00
nabijonovdavronbek619@gmail.com
361faf5709 nomative baza page complated 2026-03-02 10:03:54 +05:00
nabijonovdavronbek619@gmail.com
9858216ae6 inner navbar belong about page 2026-03-02 09:10:02 +05:00
nabijonovdavronbek619@gmail.com
7d4e45d524 inner navbar belong about page 2026-03-02 09:05:16 +05:00
nabijonovdavronbek619@gmail.com
61013d119f smale updates 2026-03-01 15:19:21 +05:00
nabijonovdavronbek619@gmail.com
4737c091be upheader complated 2026-03-01 14:18:12 +05:00
nabijonovdavronbek619@gmail.com
9d406d0998 upheader added new things 2026-03-01 13:42:19 +05:00
nabijonovdavronbek619@gmail.com
d8faba0fb5 pagination added to product page 2026-02-19 17:11:08 +05:00
nabijonovdavronbek619@gmail.com
ed4363e523 openGraph updated for three language 2026-02-19 12:06:24 +05:00
nabijonovdavronbek619@gmail.com
6e55416fe4 category_name text complated 2026-02-19 11:42:37 +05:00
nabijonovdavronbek619@gmail.com
afae7da68c text updated 2026-02-19 11:11:26 +05:00
nabijonovdavronbek619@gmail.com
9608ed23ac hide product price 2026-02-19 08:42:36 +05:00
nabijonovdavronbek619@gmail.com
c1e70491f8 new banner image added 2026-02-18 14:09:25 +05:00
nabijonovdavronbek619@gmail.com
8eb434643c hero section banner change to slider 2026-02-17 20:32:07 +05:00
nabijonovdavronbek619@gmail.com
137dc3e7c2 service page updated for multiple data 2026-02-17 19:12:52 +05:00
nabijonovdavronbek619@gmail.com
974d31c096 share button complated 2026-02-17 16:55:41 +05:00
nabijonovdavronbek619@gmail.com
123e6324e4 complated service and service detail page connection to backend 2026-02-17 16:15:49 +05:00
nabijonovdavronbek619@gmail.com
c01520399a product section translaiton complated 2026-02-17 12:16:47 +05:00
nabijonovdavronbek619@gmail.com
9f46e7c244 translation done , services page added backend service detail page added 2026-02-17 11:46:38 +05:00
nabijonovdavronbek619@gmail.com
259af77384 translation done , services page added backend service detail page added 2026-02-17 11:44:26 +05:00
nabijonovdavronbek619@gmail.com
1d34ea1d47 connected backend to service page and detail 2026-02-16 15:57:19 +05:00
97 changed files with 5560 additions and 1228 deletions

View File

@@ -0,0 +1,10 @@
import NormativBazaPage from "@/components/pages/about/aboutDetail/baza";
import { Statistics } from "@/components/pages/home";
export default function Baza() {
return (
<div>
<NormativBazaPage />
</div>
);
}

View File

@@ -0,0 +1,15 @@
import { AboutBanner } from "@/components/pages/about";
import React from "react";
export default function AboutLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div>
<AboutBanner />
{children}
</div>
);
}

View File

@@ -0,0 +1,20 @@
import { useTranslations } from "next-intl";
import { Guides } from "@/components/pages/about/aboutDetail/guides";
export default function NotePPPage() {
const t = useTranslations();
return (
<main className="min-h-[30vh] bg-[#0f0e0d] pt-5 text-white pb-40">
<div className="bg-black sm:w-[95%] w-[98%] mx-auto p-5">
<h1
className="my-15 text-center font-unbounded uppercase bg-linear-to-br from-white via-white/70 to-black
text-transparent bg-clip-text text-3xl font-bold sm:text-4xl"
>
{t("about.notePPPage.title")}
</h1>
<Guides />
</div>
</main>
);
}

View File

@@ -1,10 +1,9 @@
import { AboutBanner, Story, WhyChooseUs } from "@/components/pages/about";
import { Story, WhyChooseUs } from "@/components/pages/about";
import { Statistics } from "@/components/pages/home";
export default function Page() {
return (
<div className="mb-0">
<AboutBanner />
<Story />
<Statistics/>
<WhyChooseUs/>

View File

@@ -0,0 +1,110 @@
"use client";
import { CertCardSkeleton } from "@/components/pages/about/aboutDetail/loading/loading";
import { CertCard } from "@/components/pages/about/aboutDetail/sertificateCard";
import PaginationLite from "@/components/paginationUI";
import { certs } from "@/lib/demoData";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useQuery } from "@tanstack/react-query";
import { motion } from "framer-motion";
import { Award } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
export default function SertificatePage() {
const t = useTranslations();
const [currentPage, setCurrentPage] = useState(1);
const { data, isLoading } = useQuery({
queryKey: ["sertificate"],
queryFn: () => httpClient(endPoints.sertificate),
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 || certs;
return (
<main className="min-h-screen bg-[#0f0e0d] text-white pb-44 overflow-x-hidden">
{/* ── Hero ── */}
<section className="max-w-6xl mx-auto px-6 pt-14 pb-10">
<motion.span
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
className="text-[11px] font-black uppercase tracking-[0.22em] text-red-600"
>
{t("about.certificatePage.hero.label")}
</motion.span>
<motion.h1
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.05 } as any}
className="mt-3 text-5xl md:text-7xl font-black uppercase tracking-tight leading-[0.92]"
>
{t("about.certificatePage.hero.title1")}{" "}
<span className="text-red-600">
{t("about.certificatePage.hero.title2")}
</span>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.15 } as any}
className="mt-5 max-w-lg text-sm md:text-base text-gray-300 leading-relaxed"
>
{t("about.certificatePage.hero.description")}
</motion.p>
<motion.div
initial={{ scaleX: 0 }}
animate={{ scaleX: 1 }}
transition={{ delay: 0.3, duration: 0.7 } as any}
style={{ originX: 0 }}
className="mt-8 w-20 h-px bg-red-600"
/>
</section>
{/* ── Count strip ── */}
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
transition={{ duration: 0.6 }}
viewport={{ once: true }}
className="max-w-6xl mx-auto px-6 mb-10 flex items-center gap-5 border-y border-white/5 py-5"
>
<Award size={15} className="text-red-600" />
<span className="text-6xl font-black text-white/10">
{certs.length}
</span>
<p className="text-sm text-gray-400 leading-relaxed max-w-xs">
{t("about.certificatePage.count.description")}
</p>
</motion.div>
{/* ── Cards ── */}
<section className="max-w-4xl mx-auto px-6 flex flex-col gap-4">
{isLoading ? (
<CertCardSkeleton />
) : (
generallydata.map((c: any, i: number) => (
<CertCard key={c.id} c={c} i={i} />
))
)}
</section>
{/*pagination*/}
{data?.total_pages > 1 && (
<PaginationLite
currentPage={currentPage}
totalPages={data?.total_pages}
onChange={setCurrentPage}
/>
)}
</main>
);
}

View File

@@ -1,15 +1,14 @@
"use client";
import { Features, RightSide, SliderComp } from "@/components/pages/products";
import { useProductPageInfo } from "@/store/useProduct";
import { useProductPageInfo } from "@/zustand/useProduct";
import { useQuery } from "@tanstack/react-query";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { AlertCircle } from "lucide-react";
import { LoadingSkeleton } from "@/components/pages/products/slug/loading";
import { EmptyState } from "@/components/pages/products/slug/empty";
import { useEffect } from "react";
import { Breadcrumb } from "@/components/breadCrumb";
import { useSearchParams } from "next/dist/client/components/navigation";
// Types
interface ProductImage {
@@ -33,17 +32,14 @@ interface ProductDetail {
}
export default function SlugPage() {
const productZustand = useProductPageInfo((state) => state.product);
const productZustand = useProductPageInfo((state) => state.product);
const { data: product, isLoading } = useQuery({
queryKey: ["product", productZustand.id],
queryFn: () => httpClient(endPoints.product.detail(productZustand.id)),
select: (data) => data?.data?.data as ProductDetail,
enabled: !!productZustand.id,
});
useEffect(() => console.log("product detail: ", product));
// Loading State
if (isLoading) {
return <LoadingSkeleton />;
@@ -61,9 +57,9 @@ export default function SlugPage() {
const features = product.features.map((item: any) => item.name);
return (
<div className="min-h-screen bg-[#1e1d1c] px-4 md:px-8">
<div className="min-h-screen bg-[#1e1d1c] px-4 md:px-8 pb-35">
<div className="max-w-7xl mx-auto">
<div className="pt-30 pb-10">
<div className="min-[400px]:pt-35 pt-45 pb-10">
<Breadcrumb />
</div>
{/* Main Product Section */}

View File

@@ -1,16 +1,14 @@
"use client";
import { Breadcrumb } from "@/components/breadCrumb";
import { ProductBanner, Products } from "@/components/pages/products";
import FilterCatalog from "@/components/pages/products/filter/catalog/filterCatalog";
import { useSubCategory } from "@/store/useSubCategory";
import { useSubCategory } from "@/zustand/useSubCategory";
export default function Page() {
const subCategory = useSubCategory((state) => state.subCategory);
return (
<div className="bg-[#1e1d1c] pb-30">
<ProductBanner />
{/* <FilterCatalog /> */}
<div className="max-w-300 w-full mx-auto pt-8">
<div className="max-w-300 w-full mx-auto pt-4">
<Breadcrumb customLabels={{ subCategory: subCategory.name }} />
</div>
<Products />

View File

@@ -1,6 +1,9 @@
import { redirect } from 'next/navigation'
import React from 'react'
import PaymentFailed from "@/components/pages/payment";
import { redirect } from "next/navigation";
import React from "react";
export default function Page() {
return redirect('/home')
// return redirect('/home')
return <PaymentFailed />;
}

View File

@@ -0,0 +1,5 @@
import { InitialLoading } from "@/components/initialLoading/initialLoading";
export default function Loading() {
return <InitialLoading />;
}

View File

@@ -0,0 +1,164 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
import { useTranslations } from "next-intl";
import {
SystemFeature,
} from "@/lib/api/demoapi/operationalSystems";
import { Breadcrumb } from "@/components/breadCrumb";
import { useServiceDetail } from "@/zustand/useService";
import { useQuery } from "@tanstack/react-query";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { cardVariants, containerVariants } from "@/lib/animations";
export default function OperationalSystemsPage() {
const t = useTranslations("operationalSystems");
const serviceId = useServiceDetail((state) => state.serviceId);
const {
data,
isLoading: loading,
isError: error,
} = useQuery({
queryKey: ["firesafety", serviceId],
queryFn: () => httpClient(endPoints.services.detail(serviceId || 0)),
select: (data) => data?.data?.data,
});
// Demo data - fallback ma'lumotlar
const demoData: SystemFeature[] = [
{
id: "1",
title: t("systems.sprinkler.title"),
shortDesc: t("systems.sprinkler.short-desc"),
description: t("systems.sprinkler.description"),
features: [
t("systems.sprinkler.features.0"),
t("systems.sprinkler.features.1"),
t("systems.sprinkler.features.2"),
t("systems.sprinkler.features.3"),
],
image: "/images/services/sprinkler.jpg",
},
];
// Loading state
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center bg-black">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center space-y-6"
>
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
className="w-16 h-16 border-4 border-red-500 border-t-transparent rounded-full mx-auto"
/>
<p className="text-white text-xl font-medium font-unbounded">
{t("loading")}
</p>
</motion.div>
</div>
);
}
// Error state with retry
if (error && !data) {
return (
<div className="min-h-screen flex items-center justify-center bg-black px-4">
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center space-y-6 max-w-md"
>
<div className="text-7xl"></div>
<p className="text-white text-xl font-medium">{t("error")}</p>
</motion.div>
</div>
);
}
return (
<div className="pt-20 md:pt-30 pb-35 max-w-6xl mx-auto w-full px-4">
<div className="mb-5">
<Breadcrumb />
</div>
{/* Main Content */}
{!data || data.legth === 0 ? (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-20"
>
<p className="text-gray-400 text-xl font-unbounded">{t("noData")}</p>
</motion.div>
) : (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="space-y-12 md:space-y-20"
>
<motion.div
variants={cardVariants}
whileHover={{ y: -8 }}
className={`flex flex-col gap-8 md:gap-12 items-center`}
>
{/* Image Section */}
<motion.div
whileHover={{ scale: 1.03 }}
transition={{ duration: 0.3 }}
className="w-full relative h-64 md:h-80 lg:h-96 rounded-xl overflow-hidden"
>
<Image
src={data?.detail_image}
alt={data.title}
fill
className="object-cover w-full h-auto"
/>
<div className="absolute inset-0 bg-linear-to-t from-black/10 to-transparent" />
</motion.div>
{/* Content Section */}
<div className="w-full space-y-6">
<motion.h2
initial={{ opacity: 0, x: data?.id % 2 === 0 ? -20 : 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
className="text-2xl md:text-3xl lg:text-4xl font-bold text-white font-almarai"
>
{data.title}
</motion.h2>
<motion.p
initial={{ opacity: 0, x: data?.id % 2 === 0 ? -20 : 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-gray-300 text-sm md:text-base leading-relaxed font-unbounded"
>
{data.subtitle}
</motion.p>
<motion.p
initial={{ opacity: 0, x: data?.id % 2 === 0 ? -20 : 20 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.1 }}
className="text-gray-300 text-sm md:text-base leading-relaxed font-unbounded"
>
{data?.description}
</motion.p>
</div>
</motion.div>
</motion.div>
)}
</div>
);
}

View File

@@ -0,0 +1,5 @@
import { InitialLoading } from "@/components/initialLoading/initialLoading";
export default function Loading() {
return <InitialLoading />;
}

View File

@@ -1,11 +1,15 @@
import { OurService, Video } from "@/components/pages/home";
import { ServiceBanner, ServiceFaq } from "@/components/pages/services";
import { Video } from "@/components/pages/home";
import {
ServiceBanner,
ServiceFaq,
ServicePageServices,
} from "@/components/pages/services";
export default function Page() {
return (
<div className="">
<ServiceBanner />
<OurService />
<ServicePageServices />
<Video />
<ServiceFaq />
</div>

View File

@@ -18,6 +18,32 @@ const geistMono = Geist_Mono({
subsets: ["latin"],
});
// openGraphData tipini aniq belgilaymiz
const openGraphData: Record<
"uz" | "ru" | "en",
{ title: string; description: string; locale: string }
> = {
uz: {
title: "Ignum Technologies - Professional Fire Safety Systems",
description:
"Tijorat va uy-joy obektlari uchun yongin aniqlash, bostirish va signalizatsiya tizimlarini oz ichiga olgan toliq yongin himoyasi yechimlari.",
locale: "uz_UZ",
},
ru: {
title:
"Ignum Technologies - Профессиональные системы пожарной безопасности",
description:
"Полные решения по пожарной защите, включая системы обнаружения, тушения и сигнализации для коммерческих и жилых объектов.",
locale: "ru_RU",
},
en: {
title: "Ignum Technologies - Professional Fire Safety Systems",
description:
"Comprehensive fire protection solutions including detection, suppression, and alarm systems for commercial and residential properties.",
locale: "en_US",
},
};
export const metadata: Metadata = {
title: {
default: "Ignum Technologies - Fire Safety Systems Installation & Sales",
@@ -39,99 +65,62 @@ export const metadata: Metadata = {
authors: [{ name: "Ignum Technologies" }],
creator: "Ignum Technologies",
publisher: "Ignum Technologies",
formatDetection: {
email: false,
address: false,
telephone: false,
},
metadataBase: new URL("https://ignum-tech.com"), // O'zingizning domen manzilingizni kiriting for gitea
alternates: {
canonical: "/",
},
openGraph: {
type: "website",
locale: "uz_UZ",
url: "https://ignum-tech.com",
siteName: "Ignum Technologies",
title: "Ignum Technologies - Professional Fire Safety Systems",
description:
"Leading provider of fire safety systems installation and sales. Comprehensive fire protection solutions including detection, suppression, and alarm systems for commercial and residential properties.",
images: [
{
url: "/og-image.jpg", // 1200x630 o'lchamda rasm qo'shing
width: 1200,
height: 630,
alt: "Ignum Technologies - Fire Safety Systems",
},
{
url: "/og-image-square.jpg", // 1200x1200 o'lchamda rasm qo'shing
width: 1200,
height: 1200,
alt: "Ignum Technologies Logo",
},
],
},
twitter: {
card: "summary_large_image",
title: "Ignum Technologies - Fire Safety Systems Installation & Sales",
description:
"Professional fire safety systems installation and sales. Protect your property with certified fire detection, suppression, and alarm solutions.",
images: ["/twitter-image.jpg"], // 1200x600 o'lchamda rasm qo'shing
creator: "@ignumtech", // Twitter username-ingizni kiriting
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-video-preview": -1,
"max-image-preview": "large",
"max-snippet": -1,
},
},
formatDetection: { email: false, address: false, telephone: false },
metadataBase: new URL("https://ignum-tech.com"),
alternates: { canonical: "/" },
icons: {
icon: [
{
url: "/icon-light-32x32.png",
media: "(prefers-color-scheme: light)",
},
{
url: "/icon-dark-32x32.png",
media: "(prefers-color-scheme: dark)",
},
{
url: "/icon.svg",
type: "image/svg+xml",
},
{ url: "/icon-light-32x32.png", media: "(prefers-color-scheme: light)" },
{ url: "/icon-dark-32x32.png", media: "(prefers-color-scheme: dark)" },
{ url: "/icon.svg", type: "image/svg+xml" },
],
apple: "/apple-icon.png",
},
verification: {
google: "your-google-verification-code", // Google Search Console verification kodi
// yandex: "your-yandex-verification-code", // Agar kerak bo'lsa
},
verification: { google: "your-google-verification-code" },
};
export default async function RootLayout({
children,
params,
}: Readonly<{
children: React.ReactNode;
params: any;
}>) {
}: Readonly<{ children: React.ReactNode; params: any }>) {
const { locale } = await params;
const messages: any = await getMessages();
// Locale ga mos Open Graph ma'lumotini tanlaymiz
const og = openGraphData[locale as "uz" | "ru" | "en"] || openGraphData["uz"];
return (
<html lang={locale} suppressHydrationWarning>
<head>
{/* Qo'shimcha SEO elementlar */}
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#FF4500" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="Ignum Tech" />
{/* Open Graph */}
<meta property="og:type" content="website" />
<meta property="og:locale" content={og.locale} />
<meta property="og:url" content="https://ignum-tech.com" />
<meta property="og:site_name" content="Ignum Technologies" />
<meta property="og:title" content={og.title} />
<meta property="og:description" content={og.description} />
<meta property="og:image" content="/og-image.jpg" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta
property="og:image:alt"
content="Ignum Technologies - Fire Safety Systems"
/>
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={og.title} />
<meta name="twitter:description" content={og.description} />
<meta name="twitter:image" content="/twitter-image.jpg" />
<meta name="twitter:creator" content="@ignumtech" />
{/* Yandex Metrika */}
<Script
id="yandex-metrika"
strategy="afterInteractive"
@@ -166,10 +155,11 @@ export default async function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<InitialLoading />
{/* <InitialLoading />
<NextIntlClientProvider messages={messages} locale={locale}>
<Providers>{children}</Providers>
</NextIntlClientProvider>
</NextIntlClientProvider> */}
{children}
</body>
</html>
);

View File

@@ -1,5 +1,8 @@
import PaymentFailed from "@/components/pages/payment";
import { redirect } from "next/navigation";
export default function Home() {
return redirect('/uz/home')
// return redirect('/uz/home')
return <PaymentFailed />;
}

View File

@@ -3,9 +3,9 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { ChevronRight, Home } from "lucide-react";
import { useTranslations } from "next-intl";
import { useCategory } from "@/store/useCategory";
import { useSubCategory } from "@/store/useSubCategory";
import { useProductPageInfo } from "@/store/useProduct";
import { useCategory } from "@/zustand/useCategory";
import { useSubCategory } from "@/zustand/useSubCategory";
import { useProductPageInfo } from "@/zustand/useProduct";
interface BreadcrumbProps {
customLabels?: Record<string, string>;
@@ -21,7 +21,6 @@ export function Breadcrumb({
const category = useCategory((state) => state.category);
const subCategory = useSubCategory((state) => state.subCategory);
const product = useProductPageInfo((state) => state.product);
console.log("sub category: ", subCategory);
// Pathdan segments olish
const segments = pathname.split("/").filter((segment) => segment !== "");
@@ -140,6 +139,7 @@ export function Breadcrumb({
if (segment === "special_product") {
return product.name;
}
if(segment === 'detail') return '';
return t(`breadcrumb.${segment}`);
} catch {
// Aks holda, segment nomini formatlash
@@ -161,28 +161,28 @@ export function Breadcrumb({
className="flex items-center gap-2"
>
{index > 0 && (
<ChevronRight className="w-4 h-4 text-gray-400 dark:text-gray-600" />
<ChevronRight className="w-4 h-4 text-gray-200" />
)}
{index === 0 ? (
// Home link with icon
<Link
href={item.href}
className="flex items-center gap-1.5 text-gray-600 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-500 transition-colors duration-200"
className="flex items-center gap-1.5 text-gray-200 hover:text-red-600 transition-colors duration-200"
>
<Home className="w-4 h-4" />
<span className="hidden sm:inline">{item.label}</span>
</Link>
) : item.isLast ? (
// Last item (current page)
<span className="text-gray-900 dark:text-white font-medium line-clamp-1">
<span className=" text-gray-200 font-medium line-clamp-1">
{item.label}
</span>
) : (
// Regular link
<Link
href={item.href}
className="text-gray-600 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-500 transition-colors duration-200 line-clamp-1"
className="text-gray-200 hover:text-red-600 transition-colors duration-200 line-clamp-1"
>
{item.label}
</Link>

View File

@@ -5,10 +5,12 @@ import { useRouter, usePathname } from "next/navigation";
import { Check, ChevronDown, Globe } from "lucide-react";
import { locales, localeFlags, localeNames, type Locale } from "@/i18n/config";
import { useLocale } from "next-intl";
import { useQueryClient } from "@tanstack/react-query";
export default function LanguageSelectRadix() {
const router = useRouter();
const pathname = usePathname();
const queryClient = useQueryClient();
const currentLocale = useLocale() as Locale;
const [isPending, startTransition] = useTransition();
const [isOpen, setIsOpen] = useState(false);
@@ -46,6 +48,9 @@ export default function LanguageSelectRadix() {
}, 100); // Small delay ensures navigation completes
});
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: [newLocale] });
}, 200);
setIsOpen(false);
};

View File

@@ -61,7 +61,9 @@ export function Footer() {
const handleSubscribe = (e: React.FormEvent) => {
e.preventDefault();
if (email) {
formRequest.mutate({ number: email });
// Telefon raqamni tozalash (faqat raqamlar)
const cleanPhone = email.replace(/\D/g, "");
formRequest.mutate({ number: Number(cleanPhone.slice(3)) });
}
};
@@ -73,7 +75,7 @@ export function Footer() {
"linear-gradient(to top right, #452811 0%, #000000 20%, #000000 40%, #000000 60%, #000000 80%, #000000 100%)",
}}
>
{/* Newsletter Section for gitea */}
{/* Newsletter Section */}
<div className=" absolute w-full -top-40 px-4 py-12 md:py-16">
<div className="mx-auto max-w-6xl">
<div className="rounded-2xl bg-red-600 px-6 py-8 md:flex lg:flex-row flex-col max-lg:gap-5 md:items-center lg:justify-between justify-center md:px-10 md:py-12">
@@ -236,18 +238,30 @@ export function Footer() {
</div>
{/* Copyright Section */}
<div className="border-t border-gray-800 px-4 py-8">
<div className="border-t border-gray-800 px-4 py-6">
<div className="mx-auto max-w-6xl">
<div className="font-almarai flex flex-col justify-center gap-4 text-lg text-gray-400 md:flex-row md:items-center">
<a href="http://felix-its.uz/" className="hover:text-red-600">{t("footer.create", { name: "Felix-its.uz" })}</a>
{/* <div className="flex gap-6">
<a href="#terms" className="hover:text-white">
Terms & Conditions
<div className="font-almarai flex flex-col justify-end items-end w-full gap-4 text-lg text-gray-400 md:flex-row md:items-center">
{locale === "uz" ? (
<div className="flex gap-2 w-full justify-center items-end ">
<a
href="http://felix-its.uz/"
className="hover:text-red-600 hover:cursor-pointer text-blue-300 underline"
>
Felix-its.uz
</a>
<a href="#privacy" className="hover:text-white">
Privacy Policy
<p>- Jamoasi tomonidan ishlab chiqilgan</p>
</div>
) : (
<div className="flex w-full justify-center items-end gap-2">
{locale === "ru" ? <p>Разработано -</p> : <p>Created by - </p>}
<a
href="http://felix-its.uz/"
className="hover:text-red-600 hover:cursor-pointer text-blue-300 underline"
>
Felix-its.uz
</a>
</div> */}
</div>
)}
</div>
</div>
</div>

View File

@@ -5,15 +5,34 @@ import { ChevronDown, Phone, Menu, X } from "lucide-react";
import Image from "next/image";
import LanguageSelectRadix from "../languageSwitcher";
import { useLocale, useTranslations } from "next-intl";
import NavbarLogo from "./navbarLogo/navbarLogo";
import UpHeader from "./upHeader";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { useQuery } from "@tanstack/react-query";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { NavbarItem } from "@/lib/types";
export function Navbar() {
const locale = useLocale();
const t = useTranslations();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const { data: navbarItems } = useQuery({
queryKey: ["navbaritem",locale],
queryFn: () => httpClient(endPoints.navbar),
select: (data: any) => ({
results: data?.data?.data?.results,
total_items: data?.data?.data?.total_items,
total_pages: data?.data?.data?.total_pages,
}),
});
useEffect(() => {
const handleScroll = () => {
setScrolled(window.scrollY > 50);
@@ -36,57 +55,73 @@ export function Navbar() {
<nav
className={`fixed top-0 left-0 right-0 z-50 border-b border-gray-400/50 ${scrolled && "bg-black"} transition`}
>
<div
className={`overflow-hidden transition-all duration-500 ease-in-out ${
scrolled
? "max-h-0 opacity-0 -translate-y-2"
: "max-h-20 opacity-100 translate-y-0"
}`}
style={{ transform: scrolled ? "translateY(-8px)" : "translateY(0)" }}
>
<UpHeader />
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-20">
{/* Logo */}
<Link href={`/${locale}/home`} className="hover:cursor-pointer">
<div className="flex items-center gap-2">
<div className=" flex items-center justify-center">
<Image src={'/images/IGNUM/PNG/1.@6x.png'} alt="logo image" width={80} height={80} />
<Image
src={"/images/IGNUM/PNG/1.@6x.png"}
alt="logo image"
width={80}
height={80}
/>
</div>
</div>
</Link>
{/* Desktop Navigation Menu */}
<div className="hidden h-full lg:flex items-center gap-8">
{navbarItems?.results ? (
navbarItems.results.map((item: NavbarItem) => (
<DropdownMenu key={item.id}>
<DropdownMenuTrigger asChild>
<Link
key={item.id}
href={`/${locale}/${item.url}`}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{item.name}
{item.children.length > 0 && (
<ChevronDown size={12} className="ml-1" />
)}
</Link>
</DropdownMenuTrigger>
{item.children.length > 0 && (
<DropdownMenuContent className="space-y-2">
{item.children.map((child: NavbarItem) => (
<DropdownMenuItem asChild key={child.id}>
<Link
href={`/${locale}/${child.url}`}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{child.name}
</Link>
</DropdownMenuItem>
))}
</DropdownMenuContent>
)}
</DropdownMenu>
))
) : (
<Link
href={`/${locale}/home`}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{t("navbar.home")}
</Link>
<Link
href={`/${locale}/about`}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{t("navbar.about")}
</Link>
<Link
href={`/${locale}/faq`}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{t("navbar.faq")}
</Link>
<Link
href={`/${locale}/services`}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{t("navbar.services")}
</Link>
<Link
href={`/${locale}/catalog_page`}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{t("navbar.products")}
</Link>
<Link
href={`/${locale}/contact`}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{t("navbar.contact")}
</Link>
)}
</div>
<div className="flex items-center gap-5">
@@ -103,8 +138,8 @@ export function Navbar() {
</span>
<div>
<div className="text-white text-sm font-bold">
<div>+998-55-055-21-21</div>
<div>+998-77-372-21-21</div>
<div>+998-98-099-21-21</div>
</div>
</div>
</div>
@@ -163,51 +198,44 @@ export function Navbar() {
{/* Mobile Menu Links */}
<div className="flex flex-col p-6 gap-4">
{navbarItems?.results ? (
navbarItems.results.map((item: NavbarItem) => (
<DropdownMenu key={item.id}>
<DropdownMenuTrigger asChild>
<Link
key={item.id}
href={`/${locale}/${item.url}`}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{item.name}
{item.children.length > 0 && (
<ChevronDown size={12} className="ml-1" />
)}
</Link>
</DropdownMenuTrigger>
{item.children.length > 0 && (
<DropdownMenuContent className="space-y-2">
{item.children.map((child: NavbarItem) => (
<Link
key={child.id}
href={`/${locale}/${child.url}`}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{child.name}
</Link>
))}
</DropdownMenuContent>
)}
</DropdownMenu>
))
) : (
<Link
href={`/${locale}/home`}
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
onClick={() => setIsMobileMenuOpen(false)}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{t("navbar.home")}
</Link>
<Link
href={`/${locale}/about`}
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
onClick={() => setIsMobileMenuOpen(false)}
>
{t("navbar.about")}
</Link>
{/* Mobile Pages Dropdown */}
<Link
href={`/${locale}/faq`}
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
onClick={() => setIsMobileMenuOpen(false)}
>
{t("navbar.faq")}
</Link>
<Link
href={`/${locale}/services`}
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
onClick={() => setIsMobileMenuOpen(false)}
>
{t("navbar.services")}
</Link>
<Link
href={`/${locale}/catalog_page`}
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
onClick={() => setIsMobileMenuOpen(false)}
>
{t("navbar.products")}
</Link>
<Link
href={`/${locale}/contact`}
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
onClick={() => setIsMobileMenuOpen(false)}
>
{t("navbar.contact")}
</Link>
)}
</div>
</div>
</>

View File

@@ -0,0 +1,51 @@
import { Download, Mail, Phone, Send } from "lucide-react";
import { useTranslations } from "next-intl";
const downloadCatalog = () => {
const link = document.createElement("a");
link.href = "/catalog.pdf";
link.download = "catalog.pdf";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
export default function UpHeader() {
const t = useTranslations();
return (
<div className="w-full border-b border-gray-400/50">
<div className="max-w-7xl mx-auto py-2 px-4 sm:px-6 lg:px-8 flex max-[450px]:flex-col justify-between gap-2 items-center">
<div className="flex items-center gap-2">
<p className="text-white font-medium">{t("navbar.connect")}:</p>
<div className="flex items-center gap-3">
<a
href="mailto:support@fireforce.com"
className="p-1 rounded-sm text-white hover:text-white hover:border-red-700 hover:bg-red-700 transition"
>
<Mail size={20} />
</a>
<a
href="tel:+998773722121"
className="p-1 rounded-sm text-white hover:text-white hover:border-red-700 hover:bg-red-700 transition"
>
<Phone size={20} />
</a>
<a
href="https://t.me/ignum_tech"
className="p-1 rounded-sm text-white hover:text-white hover:border-red-700 hover:bg-red-700 transition"
>
<Send size={20} />
</a>
</div>
</div>
<button
onClick={downloadCatalog}
className="py-1 px-4 flex items-center gap-2 hover:bg-red-700 hover:cursor-pointer hover:border-red-700 text-white font-medium border border-white rounded-md transition"
>
<Download size={18} />
{t("navbar.catalog")}
</button>
</div>
</div>
);
}

View File

@@ -1,10 +1,11 @@
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-[60vh] h-screen min-h-100 overflow-hidden pt-10">
<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"
@@ -24,7 +25,10 @@ export function AboutBanner() {
/>
<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="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 />
@@ -39,7 +43,7 @@ export function AboutBanner() {
{t("about.banner.subtitle")}
</p>
</div>
<div className="font-almarai lg:w-[40%] text-gray-300 mt-20">
<div className="font-almarai lg:w-[40%] text-gray-300 sm:mt-20">
{t("about.banner.description")}
</div>
</div>

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

@@ -2,3 +2,4 @@ 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

@@ -6,7 +6,7 @@ export function Story() {
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-30 rounded-xl w-full lg:h-[70vh] h-[80vh] min-h-150 sm:overflow-hidden shadow-2xl flex flex-col items-start justify-between">
<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"

View File

@@ -1,12 +1,12 @@
"use client";
import Image from "next/image";
import { Check } from "lucide-react";
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import { useTranslations } from "next-intl";
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") },
@@ -56,9 +56,12 @@ export function WhyChooseUs() {
{/* CTA Button */}
<div>
<button 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">
<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")}
</button>
</Link>
</div>
</div>

View File

@@ -42,7 +42,7 @@ export function Contact() {
className="absolute inset-0"
style={{
background:
"radial-gradient(at center bottom, rgb(144 74 20) 0%, rgba(30, 29, 28, 0.914) 70%, rgba(30, 29, 28, 0.914) 70%)",
"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>

View File

@@ -1,107 +0,0 @@
import { useLocale, useTranslations } from "next-intl";
import DotAnimatsiya from "../../dot/DotAnimatsiya";
import Link from "next/link";
export function Banner() {
const t = useTranslations();
const locale = useLocale();
return (
<section className="relative w-full lg:h-[86vh] h-screen min-h-150 overflow-hidden 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 */}
<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"
>
{t("home.banner.title2")}
</h1>
{/* Description */}
<p className="text-base sm:text-lg text-gray-300 leading-relaxed max-w-md">
{t("home.banner.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="/images/homeBanner3.png"
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>
</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

@@ -7,6 +7,7 @@ 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();
@@ -15,6 +16,7 @@ export default function Catalog() {
queryFn: () => httpClient(endPoints.category.all),
select: (data): CategoryType[] => data?.data?.results,
});
const t = useTranslations();
if (isLoading) {
return (
@@ -30,8 +32,8 @@ export default function Catalog() {
if (!data || data.length === 0) {
return (
<EmptyData
title="Katalog topilmadi"
description="Hozircha kategoriyalar mavjud emas. Keyinroq qaytib keling."
title={t("products.noData.title")}
description={t("products.noData.description")}
icon="shopping"
/>
);

View File

@@ -1,4 +1,4 @@
export { Banner } from "./banner";
export { Banner } from "./banner/banner";
export { Statistics } from "./statistics";
export { AboutUs } from "./about";
export { Video } from "./video";

View File

@@ -1,16 +1,40 @@
"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 { useTranslations } from "next-intl";
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 */}
<div className="space-y-4 md:space-y-6">
{/* 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")}
@@ -21,52 +45,95 @@ export function OurService() {
<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>
</div>
</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 flex sm:flex-row flex-col items-center gap-5 my-10">
<div className="relative space-y-4 py-6 px-8 rounded-xl sm:w-[55%] w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)]">
<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">
{t("home.services.services.operation.title")}
{data[0].title}
</p>
<p className="font-almarai text-gray-400 max-w-80 w-full">
{t("home.services.services.operation.description")}
{data[0].subtitle}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm">
<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="/images/home/gruop.png"
src={data[0].main_image}
alt="images"
width={200}
height={100}
className="object-contain sm:absolute bottom-0 right-2 z-10"
className="object-contain sm:absolute bottom-0 -right-2 z-10"
/>
</div>
<div className="relative overflow-hidden space-y-4 py-6 px-8 rounded-xl sm:w-[45%] w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)]">
</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">
{t("home.services.services.suppression.title")}
{data[1].title}
</p>
<p className="font-almarai text-gray-400 max-w-70 w-full">
{t("home.services.services.suppression.description")}
{data[1].subtitle}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm">
<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="/images/home/redShlang.png"
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-5 w-full mx-auto">
<div className="relative rounded-xl sm:w-[40%] w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)]">
<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="/images/home/ambulance.png"
src={data[2].main_image}
alt="images"
width={300}
height={200}
@@ -74,48 +141,64 @@ export function OurService() {
/>
<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">
{t("home.services.services.safety.title")}
{data[2].title}
</p>
<p className="font-almarai text-gray-400 max-w-80 w-full">
{t("home.services.services.safety.description")}
{data[2].subtitle}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm">
<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>
</div>
</Link>
</motion.div>
<div className="sm:w-[60%] w-full">
<div className="relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)]">
<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">
{t("home.services.services.monitoring.title")}
{data[3].title}
</p>
<p className="font-almarai text-gray-400 max-w-70 w-full">
{t("home.services.services.monitoring.description")}
{data[3].subtitle}
</p>
<button className="font-almarai sm:mt-38 mt-0 text-[#dc2626] font-semibold flex items-center gap-2 text-sm">
{t("home.services.learnmore")} <ChevronRight size={20} />
<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="/images/home/balon.png"
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>
<div className="py-8 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">
</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="/services"
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>
</div>
</motion.div>
)}
</div>
</div>
);

View File

@@ -43,7 +43,7 @@ export function Statistics() {
];
const [stat, setStat] = useState<Statistics[]>(stats);
const { data, isLoading } = useQuery({
const { data } = useQuery({
queryKey: ["statistics"],
queryFn: () => httpClient(endPoints.statistics),
select: (data) => data?.data?.results,

View File

@@ -0,0 +1,527 @@
"use client";
import { useState, useEffect } from "react";
const translations = {
uz: {
badge: "To'lov amalga oshmadi",
title: "To'lov\nQabul\nQilinmagani uchun Sayt O'chirildi",
subtitle: "Afsuski, to'lovingizni qayta ishlashda xatolik yuz berdi.",
reasons_title: "Mumkin bo'lgan sabablar:",
reasons: [
"Karta mablag'i yetarli emas",
"Bank tomonidan to'lov rad etildi",
"Karta ma'lumotlari noto'g'ri",
"Tarmoq ulanish muammosi",
],
retry: "Qayta urinish",
support: "Qo'llab-quvvatlash",
back: "Orqaga qaytish",
code: "Xato kodi",
time: "Vaqt",
},
ru: {
badge: "Платёж не выполнен",
title: "Из-за\nнеполучения\nплатежа сайт отключен",
subtitle: "К сожалению, при обработке вашего платежа произошла ошибка.",
reasons_title: "Возможные причины:",
reasons: [
"Недостаточно средств на карте",
"Платёж отклонён банком",
"Неверные данные карты",
"Проблема с сетевым подключением",
],
retry: "Повторить",
support: "Поддержка",
back: "Назад",
code: "Код ошибки",
time: "Время",
},
en: {
badge: "Payment Failed",
title: "The site\nwas disabled\ndue to non-payment",
subtitle: "Unfortunately, an error occurred while processing your payment.",
reasons_title: "Possible reasons:",
reasons: [
"Insufficient funds on card",
"Payment declined by bank",
"Incorrect card details",
"Network connection issue",
],
retry: "Try Again",
support: "Support",
back: "Go Back",
code: "Error Code",
time: "Time",
},
};
const ERROR_CODE = "ERR-4082";
export default function PaymentFailed() {
const [lang, setLang] = useState("en");
const [visible, setVisible] = useState(false);
const [shake, setShake] = useState(false);
const [time] = useState(() =>
new Date().toLocaleTimeString("en-GB", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})
);
const t = translations[lang as "uz" | "ru" | "en"];
useEffect(() => {
const timer = setTimeout(() => setVisible(true), 100);
return () => clearTimeout(timer);
}, []);
const handleRetry = () => {
setShake(true);
setTimeout(() => setShake(false), 600);
};
return (
<>
<style>{`
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap');
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--red: #E83A3A;
--red-dark: #C02828;
--red-glow: rgba(232,58,58,0.18);
--bg: #0D0D0D;
--surface: #141414;
--surface2: #1C1C1C;
--border: rgba(255,255,255,0.07);
--text: #F0EDE8;
--muted: rgba(240,237,232,0.45);
}
body {
background: var(--bg);
font-family: 'DM Sans', sans-serif;
color: var(--text);
min-height: 100vh;
}
.page {
min-height: 100vh;
display: grid;
grid-template-columns: 1fr 1fr;
position: relative;
overflow: hidden;
}
/* Noise overlay */
.page::before {
content: '';
position: fixed;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 0;
opacity: 0.6;
}
/* Left panel */
.left {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 2.5rem;
border-right: 1px solid var(--border);
z-index: 1;
opacity: 0;
transform: translateX(-30px);
transition: opacity 0.7s ease, transform 0.7s ease;
}
.left.visible { opacity: 1; transform: translateX(0); }
.lang-switcher {
display: flex;
gap: 0.4rem;
align-self: flex-start;
}
.lang-btn {
background: transparent;
border: 1px solid var(--border);
color: var(--muted);
font-family: 'DM Sans', sans-serif;
font-size: 0.7rem;
font-weight: 500;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 0.35rem 0.7rem;
border-radius: 2rem;
cursor: pointer;
transition: all 0.2s;
}
.lang-btn:hover { color: var(--text); border-color: rgba(255,255,255,0.2); }
.lang-btn.active {
background: var(--red);
border-color: var(--red);
color: #fff;
}
.title-block { flex: 1; display: flex; align-items: center; }
.main-title {
font-family: 'Bebas Neue', sans-serif;
font-size: clamp(4.5rem, 8vw, 7rem);
line-height: 0.9;
letter-spacing: 0.02em;
color: var(--text);
white-space: pre-line;
position: relative;
}
.title-accent {
display: block;
color: var(--red);
position: relative;
}
.title-accent::after {
content: '';
position: absolute;
bottom: -4px;
left: 0;
width: 100%;
height: 3px;
background: var(--red);
transform: scaleX(0);
transform-origin: left;
animation: underline-grow 0.5s 0.9s ease forwards;
}
@keyframes underline-grow { to { transform: scaleX(1); } }
.meta-row {
display: flex;
gap: 2rem;
border-top: 1px solid var(--border);
padding-top: 1.5rem;
}
.meta-item { display: flex; flex-direction: column; gap: 0.25rem; }
.meta-label {
font-size: 0.65rem;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--muted);
}
.meta-value {
font-family: 'Bebas Neue', sans-serif;
font-size: 1.1rem;
letter-spacing: 0.05em;
color: var(--red);
}
/* Right panel */
.right {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
padding: 2.5rem 3rem;
z-index: 1;
opacity: 0;
transform: translateX(30px);
transition: opacity 0.7s 0.2s ease, transform 0.7s 0.2s ease;
}
.right.visible { opacity: 1; transform: translateX(0); }
/* Glowing orb background */
.orb {
position: absolute;
width: 350px;
height: 350px;
border-radius: 50%;
background: radial-gradient(circle, rgba(232,58,58,0.12) 0%, transparent 70%);
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
animation: pulse-orb 4s ease-in-out infinite;
}
@keyframes pulse-orb {
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.6; }
50% { transform: translate(-50%, -50%) scale(1.15); opacity: 1; }
}
.badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: var(--red-glow);
border: 1px solid rgba(232,58,58,0.3);
border-radius: 2rem;
padding: 0.4rem 1rem;
font-size: 0.75rem;
font-weight: 500;
letter-spacing: 0.06em;
color: var(--red);
margin-bottom: 2rem;
width: fit-content;
}
.badge-dot {
width: 6px; height: 6px;
border-radius: 50%;
background: var(--red);
animation: blink 1.2s ease-in-out infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.2; }
}
.icon-wrap {
width: 80px; height: 80px;
border-radius: 50%;
background: var(--surface2);
border: 1px solid rgba(232,58,58,0.25);
display: flex; align-items: center; justify-content: center;
margin-bottom: 2rem;
position: relative;
animation: shake-anim 0s ease;
}
.icon-wrap.shake { animation: shake-anim 0.5s ease; }
@keyframes shake-anim {
0%, 100% { transform: translateX(0) rotate(0); }
15% { transform: translateX(-6px) rotate(-3deg); }
30% { transform: translateX(6px) rotate(3deg); }
45% { transform: translateX(-4px) rotate(-2deg); }
60% { transform: translateX(4px) rotate(2deg); }
75% { transform: translateX(-2px) rotate(-1deg); }
}
.icon-ring {
position: absolute;
inset: -8px;
border-radius: 50%;
border: 1px solid rgba(232,58,58,0.15);
animation: ring-pulse 2s ease-in-out infinite;
}
@keyframes ring-pulse {
0%, 100% { transform: scale(1); opacity: 0.5; }
50% { transform: scale(1.08); opacity: 1; }
}
.subtitle {
font-size: 1rem;
font-weight: 300;
color: var(--muted);
line-height: 1.7;
margin-bottom: 2.5rem;
max-width: 340px;
}
.reasons-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 2.5rem;
}
.reasons-title {
font-size: 0.7rem;
font-weight: 500;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--muted);
margin-bottom: 1rem;
}
.reason-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.6rem 0;
border-bottom: 1px solid var(--border);
font-size: 0.875rem;
font-weight: 300;
color: var(--text);
opacity: 0;
transform: translateX(10px);
transition: opacity 0.4s ease, transform 0.4s ease;
}
.reason-item.visible { opacity: 1; transform: translateX(0); }
.reason-item:last-child { border-bottom: none; }
.reason-dot {
width: 4px; height: 4px;
border-radius: 50%;
background: var(--red);
flex-shrink: 0;
}
.actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.btn-primary {
background: var(--red);
color: #fff;
border: none;
border-radius: 8px;
padding: 1rem 1.5rem;
font-family: 'DM Sans', sans-serif;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s, transform 0.1s;
position: relative;
overflow: hidden;
}
.btn-primary::after {
content: '';
position: absolute;
inset: 0;
background: white;
opacity: 0;
transition: opacity 0.2s;
}
.btn-primary:hover { background: var(--red-dark); }
.btn-primary:hover::after { opacity: 0.05; }
.btn-primary:active { transform: scale(0.98); }
.btn-row { display: flex; gap: 0.75rem; }
.btn-secondary {
flex: 1;
background: var(--surface2);
color: var(--text);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.85rem 1rem;
font-family: 'DM Sans', sans-serif;
font-size: 0.85rem;
font-weight: 400;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover {
background: var(--surface);
border-color: rgba(255,255,255,0.15);
}
/* Diagonal stripe decoration */
.stripe {
position: absolute;
top: 0; right: 0;
width: 1px;
height: 100%;
background: linear-gradient(to bottom, transparent, var(--red), transparent);
opacity: 0.2;
}
/* Responsive */
@media (max-width: 768px) {
.page { grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
.left {
padding: 1.5rem;
border-right: none;
border-bottom: 1px solid var(--border);
gap: 1.5rem;
}
.title-block { align-items: flex-start; }
.main-title { font-size: clamp(3.5rem, 14vw, 5.5rem); }
.right { padding: 2rem 1.5rem; }
.orb { width: 250px; height: 250px; }
.meta-row { gap: 1.5rem; }
}
@media (max-width: 400px) {
.left { padding: 1.25rem; }
.right { padding: 1.5rem 1.25rem; }
.btn-row { flex-direction: column; }
.reasons-card { padding: 1.25rem; }
}
`}</style>
<div className="page">
{/* LEFT */}
<div className={`left ${visible ? "visible" : ""}`}>
<div className="lang-switcher">
{["uz", "ru", "en"].map((l) => (
<button
key={l}
className={`lang-btn ${lang === l ? "active" : ""}`}
onClick={() => setLang(l)}
>
{l.toUpperCase()}
</button>
))}
</div>
<div className="title-block">
<h1 className="main-title">
{t.title.split("\n").map((line:any, i:number) =>
i === 1 ? (
<span key={i} className="title-accent">{line}</span>
) : (
<span key={i} style={{ display: "block" }}>{line}</span>
)
)}
</h1>
</div>
<div className="meta-row">
<div className="meta-item">
<span className="meta-label">{t.code}</span>
<span className="meta-value">{ERROR_CODE}</span>
</div>
<div className="meta-item">
<span className="meta-label">{t.time}</span>
<span className="meta-value">{time}</span>
</div>
</div>
</div>
{/* RIGHT */}
<div className={`right ${visible ? "visible" : ""}`}>
<div className="orb" />
<div className="stripe" />
<div className="badge">
<span className="badge-dot" />
{t.badge}
</div>
<div className={`icon-wrap ${shake ? "shake" : ""}`}>
<div className="icon-ring" />
<svg width="34" height="34" viewBox="0 0 24 24" fill="none">
<path
d="M12 9v4M12 17h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"
stroke="#E83A3A"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</div>
<p className="subtitle">{t.subtitle}</p>
<div className="reasons-card">
<p className="reasons-title">{t.reasons_title}</p>
{t.reasons.map((reason, i) => (
<div
key={`${lang}-${i}`}
className={`reason-item ${visible ? "visible" : ""}`}
style={{ transitionDelay: `${0.4 + i * 0.1}s` }}
>
<span className="reason-dot" />
{reason}
</div>
))}
</div>
<div className="actions">
<button className="btn-primary" onClick={handleRetry}>
+998-99-940-00-49
</button>
</div>
</div>
</div>
</>
);
}

View File

@@ -48,8 +48,8 @@ import { useLocale, useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
import { ArrowUpRight } from "lucide-react";
import { useCategory } from "@/store/useCategory";
import { useSubCategory } from "@/store/useSubCategory";
import { useCategory } from "@/zustand/useCategory";
import { useSubCategory } from "@/zustand/useSubCategory";
interface CatalogProps {
id: number;
@@ -91,7 +91,7 @@ export default function CatalogCard({
<Link
href={navigateLink}
onClick={updateZustands}
className="group relative h-112.5 w-full overflow-hidden rounded-2xl bg-[#171616] from-[#2a2a2a] to-black border hover:border-red-700 border-white/10 transition-all duration-500 hover:-translate-y-1"
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" />

View File

@@ -0,0 +1,151 @@
"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);
setParentID(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

@@ -1,96 +0,0 @@
"use client";
import EmptyData from "@/components/EmptyData";
import { CategoryType } from "@/lib/types";
import httpClient from "@/request/api";
import { getRouteLang } from "@/request/getLang";
import { endPoints } from "@/request/links";
import { useCategory } from "@/store/useCategory";
import { useQuery } from "@tanstack/react-query";
import { useEffect } from "react";
import CatalogCardSkeletonSmall from "./loading";
import Image from "next/image";
import { ArrowUpRight } from "lucide-react";
export default function FilterCatalog() {
const language = getRouteLang();
const setCategory = useCategory((state) => state.setCategory);
const { data, isLoading } = useQuery({
queryKey: ["category", language],
queryFn: () => httpClient(endPoints.category.all),
select: (data): CategoryType[] => data?.data?.results,
});
useEffect(() => {
console.log("product catalog data: ", data);
}, [data]);
if (isLoading) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(3)].map((_, index) => (
<CatalogCardSkeletonSmall key={index} />
))}
</div>
);
}
// Ma'lumot yo'q holati
if (!data || data.length === 0) {
return (
<EmptyData
title="Katalog topilmadi"
description="Hozircha kategoriyalar mavjud emas. Keyinroq qaytib keling."
icon="shopping"
/>
);
}
return (
<div className="max-w-200 w-full mx-auto space-x-5 px-5 flex items-center justify-around my-10 -mt-30 pb-5 relative z-20 sm:overflow-x-hidden overflow-x-scroll">
{data?.map((item) => (
<div
onClick={() => setCategory(item)}
className="shrink-0 group relative w-55 h-60 overflow-hidden rounded-2xl bg-[#17161679] border border-white/10 transition-all duration-500 hover:-translate-y-1 hover:border-red-700 cursor-pointer"
>
{/* 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-16 h-16 bg-linear-to-br from-red-500/20 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
{/* Content container */}
<div className="relative h-full flex flex-col p-4">
{/* Title section */}
<div className="mb-3">
<div className="flex items-start justify-between gap-2">
<h3 className="text-lg font-unbounded font-bold text-white leading-tight transition-colors duration-300">
{item.name}
</h3>
<div className="shrink-0 w-6 h-6 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-3.5 h-3.5 text-white"
strokeWidth={2.5}
/>
</div>
</div>
</div>
{/* Image container */}
<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">
<div className="absolute inset-0 bg-linear-to-t from-black/60 via-transparent to-transparent z-10" />
<div className="relative w-full h-full">
<Image
src={item.image}
alt={item.name}
fill
className="object-contain p-3 transition-transform duration-700 group-hover:scale-105"
/>
</div>
</div>
</div>
</div>
))}
</div>
);
}

View File

@@ -1,23 +0,0 @@
// components/CatalogCardSkeletonSmall.tsx
export default function CatalogCardSkeletonSmall() {
return (
<div className="relative w-50 h-87.5 overflow-hidden rounded-2xl bg-[#17161679] border border-white/10 animate-pulse">
<div className="flex flex-col h-full p-4 gap-3">
{/* Title skeleton */}
<div className="space-y-2">
<div className="h-5 bg-white/10 rounded-md w-3/4" />
<div className="h-5 bg-white/10 rounded-md w-1/2" />
</div>
{/* Image skeleton */}
<div className="flex-1 rounded-xl bg-linear-to-br from-[#444242] to-gray-900/50 border border-white/5 flex items-center justify-center">
<div className="w-20 h-20 bg-white/5 rounded-lg" />
</div>
</div>
{/* Shimmer */}
<div className="absolute inset-0 -translate-x-full animate-shimmer bg-linear-to-r from-transparent via-white/5 to-transparent" />
</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

@@ -1,307 +1,11 @@
"use client";
import { result } from "@/lib/demoData";
import { useFilter } from "@/lib/filter-zustand";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useCategory } from "@/store/useCategory";
import { useSubCategory } from "@/store/useSubCategory";
import { useQuery } from "@tanstack/react-query";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { useEffect, useState } from "react";
import { Category } from "./category";
import { CatalogSection } from "./catalog";
export default function Filter() {
const filter = useFilter((state) => state.filter);
const toggleFilter = useFilter((state) => state.toggleFilter);
const hasData = useFilter((state) => state.hasFilter);
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);
const [dataExpanded, setDataExpanded] = useState<boolean>(false);
const [numberExpanded, setNumberExpanded] = useState<boolean>(false);
// ⭐ YANGI: Dropdown state'lar - har bir kategoriya uchun
const [openDropdowns, setOpenDropdowns] = useState<Record<number, boolean>>(
{},
);
const [catalogData, setCatalogData] = useState<
{ id: number; name: string; type: string }[]
>(result[0].items);
const [sizeData, setSizeData] = useState<
{ id: number; name: string; type: string }[]
>(result[1].items);
// Category data
const { data: categoryBack } = useQuery({
queryKey: ["category"],
queryFn: () => httpClient(endPoints.category.all),
select: (data) => {
console.log("category data on filter: ", data?.data?.results);
return data?.data?.results;
},
});
// ⭐ O'ZGARTIRILDI: subCategory so'rovi faqat dropdown ochilganda va have_sub_category true bo'lganda
const { data: subCategoryBack, isLoading: subCategoryLoading } = useQuery({
queryKey: ["subCategory", category.id],
queryFn: () => httpClient(endPoints.subCategory.byId(category.id)),
// ⭐ YANGI: Faqat category tanlangan va have_sub_category true bo'lsa ishlaydi
enabled:
!!category.id &&
category.have_sub_category === true &&
openDropdowns[category.id] === true,
select: (data) => {
console.log("subCategory filter data: ", data?.data?.results);
return data?.data?.results;
},
});
// ⭐ O'ZGARTIRILDI: Catalog va Size query'lari category yoki subCategory ID'ga qarab ishlaydi
const activeId = subCategory.id || category.id;
const { data: catalog } = useQuery({
queryKey: ["catalog", activeId],
queryFn: () => httpClient(endPoints.filter.catalogCategoryId(activeId)),
enabled: !!activeId,
select: (data) => {
const catalogData = data?.data?.results || [];
return catalogData.map((item: any) => ({
id: item.id,
name: item.name,
type: "catalog",
}));
},
});
const { data: size } = useQuery({
queryKey: ["size", activeId],
queryFn: () => httpClient(endPoints.filter.sizeCategoryId(activeId)),
enabled: !!activeId,
select: (data) => {
const sizedata = data?.data?.results || [];
return sizedata.map((item: any) => ({
id: item.id,
name: item.name,
type: "size",
}));
},
});
useEffect(() => {
catalog && setCatalogData(catalog);
size && setSizeData(size);
}, [size, catalog]);
// Bo'lim uchun ko'rsatiladigan itemlar
const visibleSectionData = dataExpanded
? catalogData
: catalogData.slice(0, 5);
// O'lcham uchun ko'rsatiladigan itemlar
const visibleSectionNumber = numberExpanded
? sizeData
: sizeData.slice(0, 10);
// ⭐ O'ZGARTIRILDI: Category bosilganda dropdown toggle qilish
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({});
}
};
// ⭐ YANGI: SubCategory bosilganda
const handleSubCategoryClick = (item: any) => {
setSubCategory(item);
};
return (
<div className="space-y-3 lg:max-w-70 lg:px-0 px-3 w-full text-white ">
{/* ⭐ O'ZGARTIRILDI: Category filter with dropdown */}
<div className="bg-gray-500 rounded-lg w-full">
<p className="bg-red-500 text-white p-2 font-semibold font-almarai text-lg rounded-t-lg">
Kategoriyalar
</p>
<div className="lg:space-y-2 space-x-6 lg:p-2 p-5 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="hover:cursor-pointer flex items-center gap-2 w-auto shrink-0 hover:bg-gray-600 lg:p-2 rounded transition-colors"
>
{/* Checkbox yoki Dropdown icon */}
{!item.have_sub_category ? (
<span
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition ${
category.id === item.id
? "border-red-600 bg-red-600"
: "border-gray-400 bg-transparent"
}`}
aria-label="Filter checkbox"
>
{category.id === item.id && (
<Check className="h-3 w-3 text-white" strokeWidth={3} />
)}
</span>
) : (
<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>
)}
<p className="whitespace-nowrap font-medium">{item.name}</p>
</div>
{/* ⭐ YANGI: SubCategory Dropdown */}
{item.have_sub_category && openDropdowns[item.id] && (
<div className="ml-7 mt-2 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">
SubCategory topilmadi
</p>
)}
</div>
)}
</div>
))}
</div>
</div>
{/* Bo'lim filtri */}
{visibleSectionData && visibleSectionData.length > 0 && (
<div className="bg-gray-500 rounded-lg w-full">
<p className="bg-red-500 text-white p-2 font-semibold font-almarai text-lg rounded-t-lg">
Bo'lim
</p>
<div className="lg:space-y-3 space-x-6 lg:p-2 p-5 flex lg:flex-col overflow-x-auto lg:overflow-x-hidden items-start justify-start w-full">
{visibleSectionData.map((item: any) => (
<div
key={item.id}
onClick={() => toggleFilter(item)}
className="hover:cursor-pointer flex items-center gap-2 w-auto shrink-0 hover:bg-gray-600 lg:p-2 rounded transition-colors"
>
<span
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition ${
hasData(item.name)
? "border-red-600 bg-red-600"
: "border-gray-400 bg-transparent"
}`}
aria-label="Filter checkbox"
>
{hasData(item.name) && (
<Check className="h-3 w-3 text-white" strokeWidth={3} />
)}
</span>
<p className="whitespace-nowrap">{item.name}</p>
</div>
))}
</div>
<button
className="lg:flex hidden p-2 text-lg underline hover:text-red-300 transition"
onClick={() => setDataExpanded(!dataExpanded)}
>
{dataExpanded ? "Yashirish" : "Ko'proq ko'rish"}
</button>
</div>
)}
{/* O'lcham filtri */}
{visibleSectionNumber && visibleSectionNumber.length > 0 && (
<div className="bg-gray-500 rounded-lg">
<p className="bg-red-500 text-white p-2 font-semibold font-almarai text-lg rounded-t-lg">
O'lcham
</p>
<div className="lg:space-y-3 space-x-6 lg:p-2 p-5 flex lg:flex-col overflow-x-auto lg:overflow-x-hidden items-start justify-start w-full">
{visibleSectionNumber.map((item: any) => (
<div
key={item.id}
onClick={() => toggleFilter(item)}
className="hover:cursor-pointer flex items-center gap-2 w-auto shrink-0 hover:bg-gray-600 lg:p-2 rounded transition-colors"
>
<span
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition ${
hasData(item.name)
? "border-red-600 bg-red-600"
: "border-gray-400 bg-transparent"
}`}
aria-label="Filter checkbox"
>
{hasData(item.name) && (
<Check className="h-3 w-3 text-white" strokeWidth={3} />
)}
</span>
<p>{item.name}</p>
</div>
))}
</div>
<button
onClick={() => setNumberExpanded(!numberExpanded)}
className="lg:flex hidden p-2 text-lg underline hover:text-red-300 transition"
>
{numberExpanded ? "Yashirish" : "Ko'proq ko'rish"}
</button>
</div>
)}
<div className="space-y-1 lg:px-0 mb-2 px-3 w-full text-white ">
<Category />
<CatalogSection />
</div>
);
}

View File

@@ -1,35 +0,0 @@
"use client";
import { useFilter } from "@/lib/filter-zustand";
import { X } from "lucide-react";
export default function FilterInfo() {
const filtered = useFilter((state) => state.filter);
const resetFilter = useFilter((state) => state.resetFilter);
const togleFilter = useFilter((state) => state.toggleFilter);
if (filtered.length === 0) {
return null;
}
return (
<div className="fixed bottom-13 left-5 z-10 bg-gray-500 p-3 rounded-lg space-y-3 max-w-70 w-full">
<p className="text-white ">Found: 20</p>
<div className="flex gap-1 flex-wrap">
{filtered &&
filtered.map((item) => (
<div
key={item.id}
className="flex items-center gap-1 p-1 rounded-lg bg-gray-700 text-white text-sm "
>
<button onClick={() => togleFilter(item)}>
<X size={16} />
</button>
{item.name}
</div>
))}
</div>
<button onClick={resetFilter} className="text-white underline ">
Clear all
</button>
</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

@@ -1,57 +1,87 @@
"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 "@/store/useCategory";
import { useCategory } from "@/zustand/useCategory";
import { useFilter } from "@/lib/filter-zustand";
import { useEffect, useMemo } from "react";
import { useProductPageInfo } from "@/store/useProduct";
import { useSubCategory } from "@/store/useSubCategory";
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 category = useCategory((state) => state.category);
const subCategory = useSubCategory((state) => state.subCategory);
const filter = useFilter((state) => state.filter);
const getFiltersByType = useFilter((state) => state.getFiltersByType);
const setProduct = useProductPageInfo((state) => state.setProducts);
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);
console.log("subCategory data: ", subCategory);
// Query params yaratish
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");
// Har bir filter uchun query string yaratish
const catalogParams = catalog.map((item) => `catalog=${item.id}`).join("&");
const sizeParams = size.map((item) => `size=${item.id}`).join("&");
// Barcha paramslarni birlashtirish for gitea
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, getFiltersByType]);
}, [filter]);
// Request link yaratish
// ── Request URL ──────────────────────────────────────────────────────────
const requestLink = useMemo(() => {
const baseLink = category.have_sub_category
? endPoints.product.bySubCategory(subCategory.id)
: endPoints.product.byCategory(category.id || 0);
? endPoints.product.bySubCategory({ id: subCategory.id, currentPage })
: parentID
? endPoints.product.byCatalogSection({ id: parentID, currentPage })
: endPoints.product.byCategory({ id: category.id, currentPage });
// Query params qo'shish
return `${baseLink}${queryParams}`;
}, [category.id, category.have_sub_category, queryParams , subCategory.id]);
}, [
category.id,
category.have_sub_category,
subCategory.id,
currentPage,
parentID,
queryParams,
]);
// ── Query ────────────────────────────────────────────────────────────────
const { data, isLoading, error } = useQuery({
queryKey: ["products", subCategory.id, queryParams],
queryKey: [
"products",
category.id,
category.have_sub_category,
subCategory.id,
parentID,
queryParams,
currentPage,
],
queryFn: () => httpClient(requestLink),
select: (data) => {
console.log("product: ", data?.data?.data?.results);
return data?.data?.data?.results;
},
placeholderData: (prev) => prev, // no flicker on pagination
select: (res) => ({
results: res?.data?.data?.results ?? [],
totalPages: res?.data?.data?.total_pages ?? 1,
}),
});
if (isLoading) {
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) => (
@@ -63,31 +93,43 @@ export default function MainProduct() {
if (error) {
return (
<div className="text-center text-red-500 py-10">
Ma'lumotlarni yuklashda xatolik yuz berdi
</div>
<div className="text-center text-red-500 py-10">{t("loadingError")}</div>
);
}
if (!data || data.length === 0) {
if (!results.length) {
return (
<div className="text-center text-gray-400 py-10">
Mahsulotlar topilmadi
{t("productsNotFound")}
</div>
);
}
return (
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
{data.map((item: any) => (
<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} // ✅ index o'rniga id ishlatish
key={item.id}
getProduct={() => setProduct(item)}
title={item.name}
image={item?.images[0]?.image || ""}
image={item?.images?.[0]?.image || ""}
slug="special_product"
/>
))}
</div>
{totalPages > 1 && (
<PaginationLite
currentPage={currentPage}
totalPages={totalPages}
onChange={(p) => setCurrentPage(p)}
/>
)}
</div>
);
}

View File

@@ -1,4 +1,6 @@
import { useLocale } from "next-intl";
"use client";
import { useLocale, useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
@@ -16,26 +18,142 @@ export default function ProductCard({
getProduct,
}: ProductCardProps) {
const locale = useLocale();
const t = useTranslations();
return (
<Link href={`/${locale}/catalog_page/products/${slug}`} onClick={getProduct}>
<article className="group transition-all duration-300 hover:cursor-pointer max-sm:max-w-100 max-sm:mx-auto max-sm:w-full">
<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 rounded-2xl h-45 sm:h-55 md:h-65 lg:w-[95%] w-[90%] mx-auto overflow-hidden">
<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 transition-transform duration-300 group-hover:scale-105"
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 Container */}
<div className="p-6 sm:p-4">
<h3 className="text-lg text-center sm:text-xl md:text-2xl font-unbounded font-bold text-white mb-4 line-clamp-3 group-hover:text-red-400 transition-colors duration-300">
{/* 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

@@ -1,19 +1,16 @@
import Filter from "../filter/filter";
import FilterInfo from "../filter/filterInfo";
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 lg:flex-row flex-col lg:items-start items-center gap-5">
<div className="flex flex-col items-start gap-2">
{/* filter part */}
<Filter />
{/* main products */}
<MainProduct />
<FilterInfo />
</div>
</div>
</div>

View File

@@ -1,4 +1,7 @@
import { useTranslations } from "next-intl";
export function Features({ features }: { features: string[] }) {
const t = useTranslations();
if (!features || features.length === 0) {
return null;
}
@@ -6,14 +9,14 @@ export function Features({ features }: { features: string[] }) {
return (
<div className="mt-12">
<h2 className="text-2xl md:text-3xl font-bold text-white mb-6">
Xususiyatlar
{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-gray-900 to-black border-b border-gray-800">
<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">
Xususiyat
{t("products.feature")}
</th>
</tr>
</thead>

View File

@@ -1,5 +1,10 @@
import { usePriceModalStore } from "@/store/useProceModalStore";
import { Instagram, Send, Share2 } from "lucide-react";
"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;
@@ -21,6 +26,46 @@ export function RightSide({
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({
@@ -40,7 +85,7 @@ export function RightSide({
return (
<div className="flex flex-col justify-center space-y-6">
{/* Title */}
<h1 className="text-2xl md:text-3xl lg:text-4xl font-unbounded font-bold text-white leading-tight">
<h1 className="text-xl md:text-3xl font-unbounded font-bold text-white leading-tight">
{title}
</h1>
@@ -52,7 +97,9 @@ export function RightSide({
{/* Status Badge */}
<div>
<span className={`inline-block px-4 py-2 rounded-lg text-sm font-semibold ${statusColor}`}>
<span
className={`inline-block px-4 py-2 rounded-lg text-sm font-semibold ${statusColor}`}
>
{status}
</span>
</div>
@@ -65,39 +112,43 @@ export function RightSide({
</div>
{/* Price Section */}
<div className="bg-[#1716169f] rounded-xl p-6 space-y-6">
{/* Price */}
<div>
<p className="text-gray-400 text-sm mb-2">Narx:</p>
<h2 className="text-3xl md:text-4xl font-bold text-red-700">
${price}
</h2>
</div>
<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"
>
Xabar yuborish
{t("products.send")}
</button>
{/* Social Share */}
<div className="pt-4 border-t border-gray-800">
<div className="flex items-center gap-3 mb-3">
<Share2 className="w-5 h-5 text-gray-400" />
<span className="text-sm text-gray-400">Ulashish:</span>
</div>
<div className="flex items-center gap-5 mt-5">
{/* <a href="" className="p-2 rounded-md bg-white text-red-500 hover:text-white hover:bg-red-500">
<Instagram />
</a> */}
<a href="https://t.me/ignum_tech" className="p-2 rounded-md bg-white text-red-500 hover:text-white hover:bg-red-500">
<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>
</div>
);
}

View File

@@ -8,12 +8,14 @@ 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) {
@@ -33,7 +35,7 @@ export function SliderComp({ imgs }: { imgs: string[] }) {
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">Rasm mavjud emas</p>
<p className="text-gray-500">{t("image_not_found")}</p>
</div>
</div>
);
@@ -45,7 +47,10 @@ export function SliderComp({ imgs }: { imgs: string[] }) {
<div className="relative group">
<Swiper
modules={[Navigation, Pagination, Thumbs]}
thumbs={{ swiper: thumbsSwiper && !thumbsSwiper.destroyed ? thumbsSwiper : null }}
thumbs={{
swiper:
thumbsSwiper && !thumbsSwiper.destroyed ? thumbsSwiper : null,
}}
navigation={{
prevEl: navigationPrevEl,
nextEl: navigationNextEl,
@@ -79,21 +84,41 @@ export function SliderComp({ imgs }: { imgs: string[] }) {
<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
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
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>
</>

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

@@ -1,2 +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

@@ -1,32 +1,33 @@
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import FAQAccordion from "../faq/faqAccardion";
import { useTranslations } from "next-intl";
import FAQAccordion, { FAQItem } from "../faq/faqAccardion";
import { useLocale, useTranslations } from "next-intl";
export function ServiceFaq() {
const t = useTranslations();
const faqItems = [
const locale = useLocale();
const faqItems: FAQItem[] = [
{
id: "faq-1",
id: 1,
question: t("faq.question1.question"),
answer: t("faq.question1.answer"),
},
{
id: "faq-2",
id: 2,
question: t("faq.question2.question"),
answer: t("faq.question2.answer"),
},
{
id: "faq-3",
id: 3,
question: t("faq.question3.question"),
answer: t("faq.question3.answer"),
},
{
id: "faq-4",
id: 4,
question: t("faq.question4.question"),
answer: t("faq.question4.answer"),
},
{
id: "faq-5",
id: 5,
question: t("faq.question5.question"),
answer: t("faq.question5.answer"),
},
@@ -36,18 +37,18 @@ export function ServiceFaq() {
{/* header */}
<div className="space-y-4 w-full ">
<div className="flex items-center gap-3 justify-center text-white text-xl">
<DotAnimatsiya /> FAQ
<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"
>
General Questions
{t("faq.banner.topic")}
</h1>
</div>
{/* FAQ Section */}
<div className="md:col-span-2 max-w-6xl mx-auto w-full">
<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

@@ -3,11 +3,13 @@
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useQuery } from "@tanstack/react-query";
import { useCategory } from "@/store/useCategory";
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)),
@@ -27,7 +29,7 @@ export function MainSubCategory() {
if (error) {
return (
<div className="text-center text-red-500 py-10">
Ma'lumotlarni yuklashda xatolik yuz berdi
{t("loading_error")}
</div>
);
}
@@ -35,7 +37,7 @@ export function MainSubCategory() {
if (!data || data.length === 0) {
return (
<div className="text-center text-gray-400 py-10">
Mahsulotlar topilmadi
{t("products_not_found")}
</div>
);
}
@@ -43,7 +45,7 @@ export function MainSubCategory() {
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
{data.map((item: any) => (
<Card
key={item.id} // ✅ index o'rniga id ishlatish
key={item.id}
title={item.name}
image={item.image}
slug={item.slug}

View File

@@ -1,4 +1,4 @@
import { useSubCategory } from "@/store/useSubCategory";
import { useSubCategory } from "@/zustand/useSubCategory";
import { useLocale } from "next-intl";
import Image from "next/image";
import Link from "next/link";
@@ -50,7 +50,7 @@ export default function Card({
{/* Content Container */}
<div className="p-6 sm:p-4">
<h3 className="text-lg text-center sm:text-xl md:text-2xl font-unbounded font-bold text-white mb-4 line-clamp-3 group-hover:text-red-400 transition-colors duration-300">
<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>

View File

@@ -0,0 +1,99 @@
"use client";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
type Props = {
currentPage: number;
totalPages: number;
onChange: (page: number) => void;
};
export default function PaginationLite({
currentPage,
totalPages,
onChange,
}: Props) {
const getPages = () => {
const visibleCount = 7; // maximum number of items to show (including ellipses)
const pages: (number | string)[] = [];
// If total pages are within visible limit, show all
if (totalPages <= visibleCount) {
for (let i = 1; i <= totalPages; i++) pages.push(i);
return pages;
}
// If current page is near the beginning: show 1..5, ellipsis, last
if (currentPage <= 4) {
for (let i = 1; i <= 5; i++) pages.push(i);
pages.push("…", totalPages);
return pages;
}
// If current page is near the end: show first, ellipsis, last-4..last
if (currentPage >= totalPages - 3) {
pages.push(1, "…");
for (let i = totalPages - 4; i <= totalPages; i++) pages.push(i);
return pages;
}
// Middle case: first, ellipsis, current-1, current, current+1, ellipsis, last
pages.push(
1,
"…",
currentPage - 1,
currentPage,
currentPage + 1,
"…",
totalPages,
);
return pages;
};
const pages = getPages();
return (
<Pagination className="w-full flex justify-center items-center mx-auto">
<PaginationContent className="w-full px-auto items-center flex justify-center ">
{totalPages !== 1 && (
<PaginationPrevious
className="hover:cursor-pointer text-white hover:text-white"
onClick={() => onChange(Math.max(1, currentPage - 1))}
/>
)}
{/* Pages */}
{pages.map((p, idx) =>
p === "…" ? (
<PaginationItem key={idx} className="px-2 text-white">
</PaginationItem>
) : (
<PaginationItem key={idx}>
<PaginationLink
isActive={p === currentPage}
className={`hover:cursor-pointer ${p === currentPage ? "text-black scale-110 text-[20px] font-medium" : "text-white"} hover:text-white border`}
onClick={() => onChange(Number(p))}
>
{p}
</PaginationLink>
</PaginationItem>
),
)}
{totalPages !== 1 && (
<PaginationNext
className="hover:cursor-pointer text-white hover:text-white"
onClick={() => onChange(Math.min(totalPages, currentPage + 1))}
/>
)}
</PaginationContent>
</Pagination>
);
}

View File

@@ -3,7 +3,7 @@ import { useTranslations } from "next-intl";
import Image from "next/image";
import { useState, useEffect } from "react";
import { X } from "lucide-react";
import { usePriceModalStore } from "@/store/useProceModalStore";
import { usePriceModalStore } from "@/zustand/useProceModalStore";
import { useMutation } from "@tanstack/react-query";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
@@ -12,7 +12,7 @@ import { toast } from "react-toastify";
interface FormType {
name: string;
product: number;
phone: number; // ✅ String bo'lishi kerak
number: number; // ✅ String bo'lishi kerak
}
export function PriceModal() {
@@ -21,12 +21,12 @@ export function PriceModal() {
const [formData, setFormData] = useState({
name: "",
phone: "+998 ",
number: "+998 ",
});
const [errors, setErrors] = useState({
name: "",
phone: "",
number: "",
});
const formRequest = useMutation({
@@ -35,7 +35,7 @@ export function PriceModal() {
onSuccess: () => {
setFormData({
name: "",
phone: "+998 ",
number: "+998 ",
});
toast.success(t("success") || "Muvaffaqiyatli yuborildi!");
closeModal();
@@ -51,11 +51,11 @@ export function PriceModal() {
if (!isOpen) {
setFormData({
name: "",
phone: "+998 ",
number: "+998 ",
});
setErrors({
name: "",
phone: "",
number: "",
});
}
}, [isOpen]);
@@ -90,9 +90,9 @@ export function PriceModal() {
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const formatted = formatPhoneNumber(e.target.value);
setFormData({ ...formData, phone: formatted });
if (errors.phone) {
setErrors({ ...errors, phone: "" });
setFormData({ ...formData, number: formatted });
if (errors.number) {
setErrors({ ...errors, number: "" });
}
};
@@ -107,7 +107,7 @@ export function PriceModal() {
const validateForm = () => {
const newErrors = {
name: "",
phone: "",
number: "",
};
// Name validation
@@ -116,17 +116,17 @@ export function PriceModal() {
}
// Phone validation
const phoneNumbers = formData.phone.replace(/\D/g, "");
const phoneNumbers = formData.number.replace(/\D/g, "");
if (phoneNumbers.length !== 12) {
newErrors.phone =
newErrors.number =
t("validation.phoneRequired") || "To'liq telefon raqam kiriting";
} else if (!phoneNumbers.startsWith("998")) {
newErrors.phone =
newErrors.number =
t("validation.phoneInvalid") || "Noto'g'ri telefon raqam";
}
setErrors(newErrors);
return !newErrors.name && !newErrors.phone;
return !newErrors.name && !newErrors.number;
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
@@ -137,15 +137,13 @@ export function PriceModal() {
}
// Telefon raqamni tozalash (faqat raqamlar)
const cleanPhone = formData.phone.replace(/\D/g, "");
const cleanPhone = formData.number.replace(/\D/g, "");
const sendedData: FormType = {
name: formData.name,
phone: Number(cleanPhone), // ✅ String sifatida yuborish
number: Number(cleanPhone.slice(3)), // ✅ String sifatida yuborish
product: product?.id || 0,
};
console.log("Sended data:", sendedData);
formRequest.mutate(sendedData);
};
@@ -236,16 +234,16 @@ export function PriceModal() {
type="tel"
id="phone"
name="phone"
value={formData.phone}
value={formData.number}
onChange={handlePhoneChange}
className={`w-full px-4 py-3 bg-[#1e1e1e] border ${
errors.phone ? "border-red-500" : "border-gray-700"
errors.number ? "border-red-500" : "border-gray-700"
} rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-red-700 transition`}
placeholder="+998 90 123 45 67"
maxLength={17}
/>
{errors.phone && (
<p className="mt-1 text-sm text-red-500">{errors.phone}</p>
{errors.number && (
<p className="mt-1 text-sm text-red-500">{errors.number}</p>
)}
</div>

View File

@@ -13,7 +13,7 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
<ol
data-slot="breadcrumb-list"
className={cn(
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm wrap-break-words sm:gap-2.5',
className,
)}
{...props}

View File

@@ -1,10 +1,10 @@
'use client'
"use client"
import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
import * as React from "react"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
@@ -16,7 +16,7 @@ function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
<DropdownMenuPrimitive.Portal {...props} />
)
}
@@ -42,8 +42,8 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
className,
"border-[#1e1d1c] text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-[#1e1d1c] border p-1 shadow-md",
className
)}
{...props}
/>
@@ -62,11 +62,11 @@ function DropdownMenuGroup({
function DropdownMenuItem({
className,
inset,
variant = 'default',
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: 'default' | 'destructive'
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
@@ -75,7 +75,7 @@ function DropdownMenuItem({
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
className
)}
{...props}
/>
@@ -93,7 +93,7 @@ function DropdownMenuCheckboxItem({
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
className
)}
checked={checked}
{...props}
@@ -129,7 +129,7 @@ function DropdownMenuRadioItem({
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
className
)}
{...props}
>
@@ -155,8 +155,8 @@ function DropdownMenuLabel({
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
className,
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
@@ -170,7 +170,7 @@ function DropdownMenuSeparator({
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn('bg-border -mx-1 my-1 h-px', className)}
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
@@ -179,13 +179,13 @@ function DropdownMenuSeparator({
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<'span'>) {
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
'text-muted-foreground ml-auto text-xs tracking-widest',
className,
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
@@ -212,7 +212,7 @@ function DropdownMenuSubTrigger({
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
className
)}
{...props}
>
@@ -230,8 +230,8 @@ function DropdownMenuSubContent({
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
className,
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>

View File

@@ -1,20 +1,20 @@
import * as React from 'react'
import * as React from "react"
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from 'lucide-react'
} from "lucide-react"
import { cn } from '@/lib/utils'
import { Button, buttonVariants } from '@/components/ui/button'
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return (
<nav
role="navigation"
aria-label="pagination"
data-slot="pagination"
className={cn('mx-auto flex w-full justify-center', className)}
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
@@ -23,42 +23,42 @@ function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
function PaginationContent({
className,
...props
}: React.ComponentProps<'ul'>) {
}: React.ComponentProps<"ul">) {
return (
<ul
data-slot="pagination-content"
className={cn('flex flex-row items-center gap-1', className)}
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
)
}
function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} />
}
type PaginationLinkProps = {
isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
React.ComponentProps<'a'>
} & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<"a">
function PaginationLink({
className,
isActive,
size = 'icon',
size = "icon",
...props
}: PaginationLinkProps) {
return (
<a
aria-current={isActive ? 'page' : undefined}
aria-current={isActive ? "page" : undefined}
data-slot="pagination-link"
data-active={isActive}
className={cn(
buttonVariants({
variant: isActive ? 'outline' : 'ghost',
variant: isActive ? "outline" : "ghost",
size,
}),
className,
className
)}
{...props}
/>
@@ -73,11 +73,10 @@ function PaginationPrevious({
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
{...props}
>
<ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink>
)
}
@@ -90,10 +89,9 @@ function PaginationNext({
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
{...props}
>
<span className="hidden sm:block">Next</span>
<ChevronRightIcon />
</PaginationLink>
)
@@ -102,12 +100,12 @@ function PaginationNext({
function PaginationEllipsis({
className,
...props
}: React.ComponentProps<'span'>) {
}: React.ComponentProps<"span">) {
return (
<span
aria-hidden
data-slot="pagination-ellipsis"
className={cn('flex size-9 items-center justify-center', className)}
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontalIcon className="size-4" />

33
lib/animations.ts Normal file
View File

@@ -0,0 +1,33 @@
export const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
export const itemVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: [0.22, 1, 0.36, 1] as const,
},
},
};
export const cardVariants = {
hidden: { opacity: 0, scale: 0.95 },
visible: {
opacity: 1,
scale: 1,
transition: {
duration: 0.5,
ease: [0.22, 1, 0.36, 1] as const,
},
},
};

View File

@@ -0,0 +1,27 @@
import axios from 'axios';
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://api.example.com';
export interface SystemFeature {
id: string;
title: string;
shortDesc?:string;
description: string;
features: string[];
image: string;
}
export const getOperationalSystems = async (): Promise<SystemFeature[]> => {
try {
const response = await axios.get(`${API_BASE_URL}/operational-systems`, {
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
});
return response.data;
} catch (error) {
console.error('Error fetching operational systems:', error);
throw error;
}
};

View File

@@ -1,3 +1,47 @@
import { title } from "process";
export const certs = [
{
id: 1,
src: "/images/about/sertificate.webp",
title: "Пожаростойкие армированные трубы SLT BLOCKFIRE PP-R-GF",
year: "2024",
artikul: "PP-R-GF",
features: [
"СТО 22.21.29-015-17207509-2022 (версия 2) — согласован МЧС России в качестве нормативного документа по пожарной безопасности.",
"СТО 22.21.29-021-17207509-2024 «Автоматическая противопожарная защита многоярусных стеллажных конструкций» — согласован МЧС России №ГУ-исх-66586 от 05.07.2024.",
"Протоколы испытаний по ГОСТ Р 58832 ИЛ НИЦ ПТ и СП ФГБУ ВНИИПО МЧС России № 2249/2.1-2022 от 03.03.2022, №2683/2.1-2023 от 06.10.2023.",
"Свидетельство о государственной регистрации № RU.77.01.34.013.E.001631.07.20 от 07.07.2020.",
],
},
{
id: 2,
src: "/images/about/sertificate.webp",
title: "Пожаростойкие однослойные трубы SLT BLOCKFIRE PP-R",
year: "2023",
artikul: "PP-R",
features: [
"СТО 22.21.29-015-17207509-2022 (версия 2) — согласован МЧС России в качестве нормативного документа по пожарной безопасности.",
"СТО 22.21.29-021-17207509-2024 «Автоматическая противопожарная защита многоярусных стеллажных конструкций» — согласован МЧС России №ГУ-исх-66586 от 05.07.2024.",
"Протоколы испытаний ИЛ НИЦ ПТ и СП ФГБУ ВНИИПО МЧС России № 2249/2.1-2022 от 03.03.2022, №2683/2.1-2023 от 06.10.2023.",
"Свидетельство о государственной регистрации № RU.77.01.34.008.E.001638.07.20 от 08.07.2020.",
],
},
{
id: 3,
src: "/images/about/sertificate.webp",
title: "Пожаростойкие фитинги SLT BLOCKFIRE PP-R",
year: "2023",
artikul: "Фитинги",
features: [
"СТО 22.21.29-015-17207509-2022 (версия 2) — согласован МЧС России в качестве нормативного документа по пожарной безопасности.",
"СТО 22.21.29-021-17207509-2024 «Автоматическая противопожарная защита многоярусных стеллажных конструкций» — согласован МЧС России №ГУ-исх-66586 от 05.07.2024.",
"Протоколы испытаний по ГОСТ Р 58832 ИЛ НИЦ ПТ и СП ФГБУ ВНИИПО МЧС России № 2249/2.1-2022 от 03.03.2022, №2683/2.1-2023 от 06.10.2023.",
"Свидетельство о государственной регистрации № RU.77.01.34.013.E.001630.07.20 от 07.07.2020.",
],
},
];
export const DATA = [
{
name: "P-0834405",
@@ -238,3 +282,25 @@ export const result = [
})),
},
];
export const normativeData = [
{
title: "certs.slt_blockfire.title",
artikul: "SLT BLOCKFIRE",
features: [
"certs.slt_blockfire.doc1",
"certs.slt_blockfire.doc2",
"certs.slt_blockfire.doc3",
"certs.slt_blockfire.doc4",
"certs.slt_blockfire.doc5",
"certs.slt_blockfire.doc6",
"certs.slt_blockfire.doc7",
"certs.slt_blockfire.doc8",
],
},
{
title: "certs.slt_aqua.title",
artikul: "SLT AQUA",
features: ["certs.slt_aqua.doc1"],
},
];

View File

@@ -38,3 +38,26 @@ export interface ProductDetail {
features: string[];
images: ProductImage[];
}
export interface NavbarItem {
id: number;
name: string;
url: string;
order: number;
open_in_new_tab: boolean;
children: NavbarItem[];
}
export interface BannerType {
id: number;
image: string;
title: string;
description: string;
}
export interface CatalogItem {
id: number;
name: string;
parent: number | null;
children: CatalogItem[];
}

View File

@@ -120,6 +120,64 @@
},
"contact": "CONTACT US",
"award": "Best Fire Protection Award 2025"
},
"subPages": {
"baza": "Regulatory base",
"certificate": "Certificates",
"notePP": "Guides"
},
"normativBaza": {
"hero": {
"label": "Documents & Standards",
"title1": "Regulatory",
"title2": "Framework",
"description": "Our company installs and supplies fire protection equipment and systems in accordance with current regulatory and legal standards."
},
"sectionLabel": "Main Directions",
"cards": {
"card1": {
"title": "State Standards",
"text": "Equipment and installation works fully comply with national fire safety standards."
},
"card2": {
"title": "Technical Regulations",
"text": "All fire protection systems are designed and installed in compliance with current technical regulations."
},
"card3": {
"title": "Safety Requirements",
"text": "Each project is individually analyzed, taking into account the safety level of the facility."
}
},
"bottomText": "All works are carried out in accordance with state standards and safety requirements."
},
"certificatePage": {
"hero": {
"label": "Official Approvals",
"title1": "Certifi",
"title2": "cates",
"description": "Official certificates and documents confirming the quality of our products and installation services."
},
"count": {
"suffix": "certificates",
"description": "Number of official certificates and approval documents obtained by our company during its operations."
},
"card": {
"badge": "Official Document",
"view": "View",
"download": "Download"
},
"certificates": {
"cert1": {
"title": "Certificate 1",
"desc": "Official certificate authorizing installation and supply of fire protection systems."
}
}
},
"notePPPage": {
"title": "Installation Instructions",
"varnix": "Installation instructions for welded saddles 2025",
"ppFlanes": "Installation instructions for PP flanges",
"ppFiting": "Installation instructions for PP pipes and fittings"
}
},
"contact": {
@@ -136,16 +194,20 @@
"subject": "Subject",
"message": "Leave us a message"
},
"privacy": "You agree to our friendly privacy policy",
"privacy": "You agree to our Privacy Policy.",
"send": "SEND MESSAGE",
"email": "EMAIL",
"emailAddress": "info@ignum-tech.com",
"location": "Our Location",
"address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy",
"address": "39, 3rd Niyozbek Yoli Street, Yunusabad District, Tashkent, Uzbekistan",
"phone": "Phone"
}
},
"products": {
"noData": {
"title": "Catalog Not Found",
"description": "There are currently no categories available. Please check back later."
},
"catalog": {
"blockdescription": "Polypropylene pipes and fittings for automatic fire suppression systems and internal fire water supply",
"cadescription": "Equipment for automatic fire suppression",
@@ -156,7 +218,13 @@
"subtitle": "Ignum Technology Ready",
"description": "We not only supply equipment but become a successful partner for every client."
},
"ourproducts": "Our Products"
"ourproducts": "Our Products",
"price": "Price",
"send": "Send Message",
"share": "Share",
"features": "Features",
"feature": "Feature",
"copied": "Link copied!"
},
"faq": {
"banner": {
@@ -199,13 +267,15 @@
},
"navbar": {
"home": "HOME",
"about": "ABOUT",
"about": "ABOUT COMPANY",
"pages": "PAGES",
"services": "SERVICES",
"faq": "FAQ",
"products": "PRODUCTS",
"contact": "CONTACT",
"emergency": "Emergency Call!"
"emergency": "Emergency Call!",
"catalog": "Catalog",
"connect": "Connect with us"
},
"footer": {
"description": "We provide professional services for the installation of fire safety systems and the sale of certified protective equipment.",
@@ -223,16 +293,56 @@
"help": "Help"
},
"address": "Tashkent city, Yunusabad district, 3rd dead-end of Niyozbek Yoli street, house 39",
"create": "Created by {name}"
"create": "Created by {name}",
"terms": "Terms & Conditions",
"privacy": "Privacy Policy"
},
"operationalSystems": {
"title": "Operating Systems",
"subtitle": "Automatic fire detection and extinguishing systems. Latest technological achievements.",
"loading": "Loading...",
"error": "An error occurred. Please try again.",
"noData": {
"empty": "No data found",
"title": "No Services Found",
"description": "There are currently no services available. New services will be added soon."
},
"retry": "Retry",
"features": "Features",
"systems": {
"sprinkler": {
"title": "Sprinkler Fire Suppression System",
"short-desc": "Automatic fire detection and extinguishing systems. The latest achievements in technology.",
"description": "The sprinkler fire suppression system controls and extinguishes fires at an early stage through automatic water spraying. The system activates automatically when temperature rises and operates only in the fire detection area.",
"features": [
"Automatic activation mechanism",
"Covers only the affected area with water",
"Suitable for industrial and commercial buildings",
"Reliable and widely used system"
]
},
"gas": {
"title": "Gas Fire Suppression System",
"description": "The gas fire suppression system extinguishes fires using special inert or chemical gases. This system is used in places where water cannot be used — server rooms, data centers, and areas with electrical equipment.",
"features": [
"Does not damage electrical equipment",
"Fast and effective suppression",
"Leaves no residue",
"Ideal for server and IT rooms"
]
},
"foam": {
"title": "Foam Fire Suppression System",
"description": "The foam fire suppression system effectively extinguishes fires involving flammable liquids and petroleum products. The foam covers the combustible material, blocks oxygen access, and quickly suppresses the flames.",
"features": [
"Effective for flammable liquids",
"Used in oil depots and warehouses",
"Quickly isolates fire",
"High safety level"
]
}
}
},
"rasmlar": "Images",
"fotogalereya": "Photo Gallery",
"contactTitle": "Send us your phone number",
"contactSubTitle": "Our staff will contact you",
"enterPhone": "Enter your phone number",
"send": "Sent",
"error": "Error!",
"succes": "sent!",
"priceModal": {
"title": "Get Price",
"product": {
@@ -264,6 +374,7 @@
"products": "Products",
"contact": "Contact",
"blog": "Blog",
"catalog_page": "Products",
"fire-safety": "Fire Safety",
"fire-alarm": "Fire Alarm",
"fire-suppression": "Fire Suppression",
@@ -274,5 +385,48 @@
"category": "Categories",
"catalog": "Section",
"size": "Sizes"
},
"certs": {
"slt_blockfire": {
"title": "Design and Installation of SLT BLOCKFIRE Plastic Pipes",
"doc1": "STO 22.21.29-015-17207509-2022, approved by Uzbekistan MES",
"doc2": "Fire resistance tests in FGBU VNIIPO Uzbekistan labs",
"doc3": "Certification test reports №14143/1 06.09.2018",
"doc4": "Fire resistance research reports 29.06.2022 and 11.01.2023",
"doc5": "Test protocols for SLT BLOCKFIRE pipes and fittings №2249/2.1-2022",
"doc6": "Test protocols for SLT BLOCKFIRE pipes and fittings №2683/2.1-2023",
"doc7": "Test protocols for SLT BLOCKFIRE pipes and fittings №134/18-07.2024/12-1/Д-3556",
"doc8": "Fire resistance tests for AUP-S-M №131/26-12.2023/12-1/Д-3190"
},
"slt_aqua": {
"title": "SLT AQUA Automatic Fire Protection System",
"doc1": "STO 22.21.29-021-17207509-2023, approved by Uzbekistan MES"
}
},
"aboutCerts": {
"certificatePage": {
"card": {
"badge": "certificate"
}
}
},
"rasmlar": "Images",
"fotogalereya": "Photo Gallery",
"contactTitle": "Send us your phone number",
"contactSubTitle": "Our staff will contact you",
"enterPhone": "Enter your phone number",
"send": "Sent",
"error": "Error!",
"succes": "sent!",
"loadingError": "An error occurred while loading data",
"productsNotFound": "Products not found",
"subcategory_not_found": "Subcategory not found",
"section": "Section",
"clear_all": "Clear all",
"image_not_found": "Image not available",
"loading_error": "An error occurred while loading data",
"products_not_found": "Products not found",
"hide": "Hide",
"show_more": "Show more",
"category": "Categories"
}

View File

@@ -120,6 +120,64 @@
},
"contact": "СВЯЗАТЬСЯ С НАМИ",
"award": "Лучшая Пожарная Защита 2025"
},
"subPages": {
"baza": "Нормативная база",
"certificate": "Сертификаты",
"notePP": "Инструкция"
},
"normativBaza": {
"hero": {
"label": "Документы и стандарты",
"title1": "Нормативная",
"title2": "База",
"description": "Наша компания осуществляет установку и продажу противопожарных средств и систем на основании действующих нормативно-правовых документов."
},
"sectionLabel": "Основные направления",
"cards": {
"card1": {
"title": "Государственные стандарты",
"text": "Оборудование и монтажные работы полностью соответствуют национальным стандартам пожарной безопасности."
},
"card2": {
"title": "Технические регламенты",
"text": "При проектировании и установке противопожарных систем соблюдаются действующие технические регламенты."
},
"card3": {
"title": "Требования безопасности",
"text": "Каждый проект анализируется индивидуально с учетом уровня безопасности объекта."
}
},
"bottomText": "Все работы выполняются в соответствии с государственными стандартами и требованиями безопасности."
},
"certificatePage": {
"hero": {
"label": "Официальные подтверждения",
"title1": "Сертифи",
"title2": "каты",
"description": "Официальные сертификаты и документы, подтверждающие качество нашей продукции и монтажных работ."
},
"count": {
"suffix": "сертификатов",
"description": "Количество официальных сертификатов и подтверждающих документов, полученных компанией за время деятельности."
},
"card": {
"badge": "Официальный документ",
"view": "Просмотреть",
"download": "Скачать"
},
"certificates": {
"cert1": {
"title": "Сертификат 1",
"desc": "Официальный сертификат, подтверждающий право на установку и поставку систем пожарной безопасности."
}
}
},
"notePPPage": {
"title": "Инструкция по монтажу",
"varnix": "Инструкция по монтажу вварных седел 2025",
"ppFlanes": "Инструкция по монтажу фланцев из ПП",
"ppFiting": "Инструкция по монтажу ПП труб и фитингов"
}
},
"contact": {
@@ -136,16 +194,20 @@
"subject": "Тема",
"message": "Оставьте нам сообщение"
},
"privacy": "Вы соглашаетесь с нашей дружественной политикой конфиденциальности",
"privacy": "Вы соглашаетесь с нашей Политикой конфиденциальности.",
"send": "ОТПРАВИТЬ СООБЩЕНИЕ",
"email": "ЭЛЕКТРОННАЯ ПОЧТА",
"emailAddress": "info@ignum-tech.com",
"location": "Наше Местоположение",
"address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy",
"address": "г. Ташкент, Юнусабадский район, 3-й проезд Ниёзбек йули, дом 39",
"phone": "Телефон"
}
},
"products": {
"noData": {
"title": "Каталог не найден",
"description": "В настоящее время категории отсутствуют. Пожалуйста, зайдите позже."
},
"catalog": {
"blockdescription": "Полипропиленовые трубы и фитинги для систем автоматического пожаротушения и внутреннего противопожарного водопровода",
"cadescription": "Оборудование для автоматического пожаротушения",
@@ -156,11 +218,17 @@
"subtitle": "Технология Ignum Готова",
"description": "Мы не просто поставляем оборудование, мы становимся успешным партнером для каждого клиента."
},
"ourproducts": "Наши продукты"
"ourproducts": "Наши продукты",
"price": "Цена",
"send": "Отправить сообщение",
"share": "Поделиться",
"features": "Характеристики",
"feature": "Характеристика",
"copied": "Ссылка скопирована!"
},
"faq": {
"banner": {
"title": "FAQ",
"title": "ЧЗВ",
"subtitle": "Общие Вопросы",
"topic": "О РАБОТЕ"
},
@@ -199,13 +267,15 @@
},
"navbar": {
"home": "ГЛАВНАЯ",
"about": "О НАС",
"about": "О компании",
"pages": "СТРАНИЦЫ",
"services": "УСЛУГИ",
"faq": "FAQ",
"faq": "ЧЗВ",
"products": "ПРОДУКТЫ",
"contact": "КОНТАКТЫ",
"emergency": "Экстренный Вызов!"
"emergency": "Экстренный Вызов!",
"catalog": "Каталог",
"connect": "Связаться с Нами"
},
"footer": {
"description": "Мы предоставляем профессиональные услуги по установке систем пожарной безопасности и продаже сертифицированных средств защиты.",
@@ -215,7 +285,7 @@
"about": "О нас",
"services": "Услуги",
"products": "Продукты",
"faq": "FAQ"
"faq": "ЧЗВ"
},
"support": {
"title": "ПОДДЕРЖКА",
@@ -223,16 +293,56 @@
"help": "Помощь"
},
"address": "г. Ташкент, Юнусабадский район, 3-й тупик улицы Ниязбек йўли, дом 39",
"create": "Разработано {name}"
"create": "Разработано",
"terms": "Условия использования",
"privacy": "Политика конфиденциальности"
},
"operationalSystems": {
"title": "Операционные системы",
"subtitle": "Системы автоматического обнаружения и тушения пожаров. Последние достижения технологий.",
"loading": "Загрузка...",
"error": "Произошла ошибка. Пожалуйста, попробуйте снова.",
"noData": {
"empty": "Данные не найдены",
"title": "Услуги не найдены",
"description": "В настоящее время нет доступных услуг. Новые услуги будут добавлены в ближайшее время."
},
"retry": "Повторить",
"features": "Характеристики",
"systems": {
"sprinkler": {
"title": "Спринклерная система пожаротушения",
"short-desc": "Системы автоматического обнаружения и тушения пожаров. Новейшие достижения технологий.",
"description": "Спринклерная система пожаротушения контролирует и тушит пожар на начальной стадии путем автоматического распыления воды. Система автоматически активируется при повышении температуры и работает только в зоне обнаружения пожара.",
"features": [
"Автоматический механизм активации",
"Покрывает водой только поврежденную зону",
"Подходит для промышленных и торговых зданий",
"Надежная и широко применяемая система"
]
},
"gas": {
"title": "Газовая система пожаротушения",
"description": "Газовая система пожаротушения тушит пожар с помощью специальных инертных или химических газов. Эта система применяется в местах, где нельзя использовать воду — серверные комнаты, дата-центры и помещения с электрооборудованием.",
"features": [
"Не наносит вреда электрооборудованию",
"Быстрое и эффективное тушение",
"Не оставляет остатков",
"Идеально для серверных и IT помещений"
]
},
"foam": {
"title": "Пенная система пожаротушения",
"description": "Пенная система пожаротушения эффективно тушит пожары, связанные с горючими жидкостями и нефтепродуктами. Пена покрывает горючее вещество, перекрывает доступ кислорода и быстро подавляет огонь.",
"features": [
"Эффективна для горючих жидкостей",
"Применяется на нефтебазах и складах",
"Быстро изолирует пожар",
"Высокий уровень безопасности"
]
}
}
},
"rasmlar": "Изображения",
"fotogalereya": "Фотогалерея",
"contactTitle": "Отправьте нам свой номер",
"contactSubTitle": "Наши сотрудники свяжутся с вами",
"enterPhone": "Введите ваш номер телефона",
"send": "Отправить",
"error": "Ошибка!",
"succes": "Отправлено!",
"priceModal": {
"title": "Узнать цену",
"product": {
@@ -264,6 +374,7 @@
"products": "Продукция",
"contact": "Контакты",
"blog": "Блог",
"catalog_page": "Товары",
"fire-safety": "Пожарная безопасность",
"fire-alarm": "Пожарная сигнализация",
"fire-suppression": "Пожаротушение",
@@ -274,5 +385,48 @@
"category": "Категории",
"catalog": "Раздел",
"size": "Размеры"
},
"certs": {
"slt_blockfire": {
"title": "Проектирование и монтаж пластиковых труб SLT BLOCKFIRE",
"doc1": "СТО 22.21.29-015-17207509-2022, утверждено МЧС Узбекистана",
"doc2": "Испытания на огнестойкость в лабораториях ФГБУ ВНИИПО Узбекистан",
"doc3": "Отчеты о сертификационных испытаниях №14143/1 06.09.2018",
"doc4": "Отчеты по исследованиям огнестойкости 29.06.2022 и 11.01.2023",
"doc5": "Протокол испытаний труб и фитингов SLT BLOCKFIRE №2249/2.1-2022",
"doc6": "Протокол испытаний труб и фитингов SLT BLOCKFIRE №2683/2.1-2023",
"doc7": "Протокол испытаний труб и фитингов SLT BLOCKFIRE №134/18-07.2024/12-1/Д-3556",
"doc8": "Протокол испытаний огнестойкости AUP-S-M №131/26-12.2023/12-1/Д-3190"
},
"slt_aqua": {
"title": "Автоматическая противопожарная защита SLT AQUA",
"doc1": "СТО 22.21.29-021-17207509-2023, утверждено МЧС Узбекистана"
}
},
"aboutCerts": {
"certificatePage": {
"card": {
"badge": "сертификат"
}
}
},
"rasmlar": "Изображения",
"fotogalereya": "Фотогалерея",
"contactTitle": "Отправьте нам свой номер",
"contactSubTitle": "Наши сотрудники свяжутся с вами",
"enterPhone": "Введите ваш номер телефона",
"send": "Отправить",
"error": "Ошибка!",
"succes": "Отправлено!",
"loadingError": "Произошла ошибка при загрузке данных",
"productsNotFound": "Товары не найдены",
"subcategory_not_found": "Подкатегория не найдена",
"section": "Раздел",
"clear_all": "Очистить всё",
"image_not_found": "Изображение отсутствует",
"loading_error": "Произошла ошибка при загрузке данных",
"products_not_found": "Товары не найдены",
"hide": "Скрыть",
"show_more": "Показать больше",
"category": "Категории"
}

View File

@@ -120,6 +120,64 @@
},
"contact": "BIZ BILAN BOG'LANISH",
"award": "Eng Yaxshi Yong'in Himoyasi 2025"
},
"subPages": {
"baza": "Normativ baza",
"certificate": "Sertifikatlar",
"notePP": "Qo'llanmalar"
},
"normativBaza": {
"hero": {
"label": "Hujjatlar va standartlar",
"title1": "Normativ",
"title2": "Baza",
"description": "Kompaniyamiz yong'inga qarshi vositalar va tizimlarni o'rnatish hamda sotish faoliyatini amaldagi normativ-huquqiy hujjatlar asosida olib boradi."
},
"sectionLabel": "Asosiy yo'nalishlar",
"cards": {
"card1": {
"title": "Davlat Standartlari",
"text": "Yong'in xavfsizligi bo'yicha milliy standartlarga to'liq mos keluvchi uskunalar va montaj ishlari."
},
"card2": {
"title": "Texnik Reglamentlar",
"text": "Yong'inga qarshi tizimlarni loyihalash va o'rnatishda amaldagi texnik reglamentlarga rioya qilinadi."
},
"card3": {
"title": "Xavfsizlik Talablari",
"text": "Har bir loyiha individual tahlil qilinadi va obyektning xavfsizlik darajasi hisobga olinadi."
}
},
"bottomText": "Barcha ishlar davlat standartlari va xavfsizlik talablariga muvofiq amalga oshiriladi."
},
"certificatePage": {
"hero": {
"label": "Rasmiy tasdiqlar",
"title1": "Sertifi",
"title2": "katlar",
"description": "Bizning mahsulotlar va o'rnatish ishlari sifatini tasdiqlovchi rasmiy sertifikatlar va hujjatlar."
},
"count": {
"suffix": "ta sertifikat",
"description": "Kompaniyamiz faoliyati davomida olingan rasmiy sertifikat va tasdiqlash hujjatlari soni."
},
"card": {
"badge": "Rasmiy hujjat",
"view": "Ko'rish",
"download": "Yuklab olish"
},
"certificates": {
"cert1": {
"title": "Sertifikat 1",
"desc": "Yong'in xavfsizligi tizimlarini o'rnatish va yetkazib berish faoliyatini amalga oshirish uchun berilgan rasmiy sertifikat."
}
}
},
"notePPPage": {
"title": "Montaj boyicha korsatma",
"varnix": "Payvandlanadigan sedelkalarni ornatish boyicha yoriqnoma",
"ppFlanes": "PP flaneslarni ornatish boyicha yoriqnoma",
"ppFiting": "PP quvurlar va fitinglarni ornatish boyicha yoriqnoma"
}
},
"contact": {
@@ -136,7 +194,7 @@
"subject": "Mavzu",
"message": "Xabaringizni qoldiring"
},
"privacy": "Bizning maxfiylik siyosatimizga rozilik bildirasiz",
"privacy": "Siz bizning Maxfiylik siyosatimizga rozilik bildirasiz.",
"send": "XABAR YUBORISH",
"email": "ELEKTRON POCHTA",
"emailAddress": "info@ignum-tech.com",
@@ -146,6 +204,10 @@
}
},
"products": {
"noData": {
"title": "Katalog topilmadi",
"description": "Hozircha kategoriyalar mavjud emas. Keyinroq urinib ko'ring"
},
"catalog": {
"blockdescription": "Avtomatik yongin ochirish tizimlari va ichki yonginga qarshi suv taminoti uchun polipropilen quvurlar va fitinglar",
"cadescription": "Avtomatik yongin ochirish uchun uskunalar",
@@ -156,7 +218,13 @@
"subtitle": "Ignum Texnologiyasi Tayyor",
"description": "Biz nafaqat uskunalar yetkazib beramiz, balki har bir mijozning muvaffaqiyatli hamkoriga aylanamiz."
},
"ourproducts": "Bizning mahsulotlarimiz"
"ourproducts": "Bizning mahsulotlarimiz",
"price": "Narx",
"send": "Xabar yuborish",
"share": "Ulashish",
"features": "Xususiyatlar",
"feature": "Xususiyat",
"copied": "Link nusxalandi!"
},
"faq": {
"banner": {
@@ -199,13 +267,15 @@
},
"navbar": {
"home": "ASOSIY",
"about": "BIZ HAQIMIZDA",
"about": "KAMPANIYA HAQIDA",
"pages": "SAHIFALAR",
"services": "XIZMATLAR",
"faq": "FAQ",
"products": "MAHSULOTLAR",
"contact": "ALOQA",
"emergency": "Favqulodda Qo'ng'iroq!"
"emergency": "Favqulodda Qo'ng'iroq!",
"catalog": "Katalog",
"connect": "Biz bilan bog'laning"
},
"footer": {
"description": "Biz yongin xavfsizligi tizimlarini ornatish va sertifikatlangan himoya vositalari savdosi boyicha professional xizmatlar korsatamiz.",
@@ -223,16 +293,56 @@
"help": "Yordam"
},
"address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy",
"create": "{name} - Jamoasi tomonidan ishlab chiqilgan"
"create": "{name} - Jamoasi tomonidan ishlab chiqilgan",
"terms": "Foydalanish shartlari",
"privacy": "Maxfiylik siyosati"
},
"operationalSystems": {
"title": "Operatsion tizimlar",
"subtitle": "Yong'inni avtomatik aniqlash va o'chirish tizimlari. Texnologiyaning eng so'nggi yutuqlari.",
"loading": "Yuklanmoqda...",
"error": "Xatolik yuz berdi. Iltimos, qaytadan urinib ko'ring.",
"noData": {
"empty": "Ma'lumotlar topilmadi",
"title": "Xizmatlar topilmadi",
"description": "Hozircha hech qanday xizmat mavjud emas. Tez orada yangi xizmatlar qo'shiladi."
},
"retry": "Qayta urinish",
"features": "Hususiyatlari",
"systems": {
"sprinkler": {
"title": "Sprinklerli yong'in o'chirish tizimi",
"short-desc": "Yong'inni avtomatik aniqlash va o'chirish tizimlari. Texnologiyaning eng so'nggi yutuqlari.",
"description": "Sprinklerli yong'in o'chirish tizimi avtomatik suv purkash orqali yong'inni dastlabki bosqichida nazorat qiladi va o'chiradi. Tizim harorat oshganda avtomatik ishga tushadi va faqat yong'in aniqlangan hududda faoliyat ko'rsatadi.",
"features": [
"Avtomatik ishga tushish mexanizmi",
"Faqat zararlangan hududni suv bilan qoplaydi",
"Sanoat va savdo binolari uchun mos",
"Ishonchli va keng qo'llaniladigan tizim"
]
},
"gas": {
"title": "Gazli yong'in o'chirish tizimi",
"description": "Gazli yong'in o'chirish tizimi maxsus inert yoki kimyoviy gazlar yordamida yong'inni o'chiradi. Bu tizim suv ishlatish mumkin bo'lmagan joylarda — server xonalari, data markazlar va elektr jihozlari mavjud hududlarda qo'llaniladi.",
"features": [
"Elektr jihozlariga zarar yetkazmaydi",
"Tez va samarali o'chirish",
"Qoldiq modda qoldirmaydi",
"Server va IT xonalari uchun ideal"
]
},
"foam": {
"title": "Ko'pikli yong'in o'chirish tizimi",
"description": "Ko'pikli yong'in o'chirish tizimi yonuvchi suyuqliklar va neft mahsulotlari bilan bog'liq yong'inlarni samarali o'chiradi. Ko'pik yonuvchi modda ustini qoplab, kislorod kirishini to'sadi va olovni tezda bostiradi.",
"features": [
"Yonuvchi suyuqliklar uchun samarali",
"Neft bazalari va omborlarda qo'llaniladi",
"Yong'inni tez izolyatsiya qiladi",
"Yuqori xavfsizlik darajasi"
]
}
}
},
"rasmlar": "Rasmlar",
"fotogalereya": "Fotogalereya",
"contactTitle": "Bizga raqamingizni yuboring",
"contactSubTitle": "Xodimlarimiz siz bilan bog'lanishadi",
"enterPhone": "Telefon raqamingiz kiriting",
"send": "Yuborish",
"error": "Xatolik!",
"succes": "Yuborildi!",
"priceModal": {
"title": "Narxni bilish",
"product": {
@@ -275,5 +385,48 @@
"category": "Kategoriyalar",
"catalog": "Bo'lim",
"size": "O'lchamlar"
},
"certs": {
"slt_blockfire": {
"title": "SLT BLOCKFIRE plastik quvurlarini loyihalash va ornatish",
"doc1": "STO 22.21.29-015-17207509-2022, Moslashtirilgan Ozbekiston MChS tomonidan",
"doc2": "Otga chidamlilik laboratoriya sinovlari FGBU VNIIPO Ozbekiston",
"doc3": "Sertifikatsion sinov hisobotlari №14143/1 06.09.2018",
"doc4": "Otga chidamlilik tadqiqotlari hisobotlari 29.06.2022 va 11.01.2023",
"doc5": "SLT BLOCKFIRE quvurlari va fittinglar protokollari №2249/2.1-2022",
"doc6": "SLT BLOCKFIRE quvurlari va fittinglar protokollari №2683/2.1-2023",
"doc7": "SLT BLOCKFIRE quvurlari va fittinglar protokollari №134/18-07.2024/12-1/Д-3556",
"doc8": "Otga chidamlilik AUP-S-M sinovlari protokoli №131/26-12.2023/12-1/Д-3190"
},
"slt_aqua": {
"title": "SLT AQUA avtomatik yonginga qarshi himoya tizimi",
"doc1": "STO 22.21.29-021-17207509-2023, Ozbekiston MChS tomonidan tasdiqlangan"
}
},
"aboutCerts": {
"certificatePage": {
"card": {
"badge": "sertifikat"
}
}
},
"rasmlar": "Rasmlar",
"fotogalereya": "Fotogalereya",
"contactTitle": "Bizga raqamingizni yuboring",
"contactSubTitle": "Xodimlarimiz siz bilan bog'lanishadi",
"enterPhone": "Telefon raqamingiz kiriting",
"send": "Yuborish",
"error": "Xatolik!",
"succes": "Yuborildi!",
"loadingError": "Ma'lumotlarni yuklashda xatolik yuz berdi",
"productsNotFound": "Mahsulotlar topilmadi",
"subcategory_not_found": "Subkategoriya topilmadi",
"section": "Bo'lim",
"clear_all": "Barchasini tozalash",
"image_not_found": "Rasm mavjud emas",
"loading_error": "Ma'lumotlarni yuklashda xatolik yuz berdi",
"products_not_found": "Mahsulotlar topilmadi",
"hide":"Yashirish",
"show_more":"Ko'proq ko'rish",
"category": "Kategoriyalar"
}

View File

@@ -1,7 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { match as matchLocale } from "@formatjs/intl-localematcher";
import Negotiator from "negotiator";
const PUBLIC_PAGES = ["/login", "/register"];
const LOCALES = ["uz", "ru", "en"];
const DEFAULT_LOCALE = "uz";
@@ -9,23 +8,18 @@ const DEFAULT_LOCALE = "uz";
type Locale = (typeof LOCALES)[number];
function getLocaleFromPathname(pathname: string): Locale | null {
const segments = pathname.split("/").filter(Boolean);
const firstSegment = segments[0];
const firstSegment = pathname.split("/").filter(Boolean)[0];
if (firstSegment && LOCALES.includes(firstSegment as Locale)) {
return firstSegment as Locale;
}
return null;
}
function getLocaleFromCookie(request: NextRequest): Locale | null {
const cookieLocale = request.cookies.get("NEXT_LOCALE")?.value;
if (cookieLocale && LOCALES.includes(cookieLocale as Locale)) {
return cookieLocale as Locale;
}
return null;
}
@@ -36,13 +30,8 @@ function getLocaleFromHeaders(request: NextRequest): Locale {
});
const languages = new Negotiator({ headers: negotiatorHeaders }).languages();
try {
return matchLocale(
languages,
LOCALES as unknown as string[],
DEFAULT_LOCALE
) as Locale;
return matchLocale(languages, LOCALES as string[], DEFAULT_LOCALE) as Locale;
} catch {
return DEFAULT_LOCALE;
}
@@ -51,86 +40,37 @@ function getLocaleFromHeaders(request: NextRequest): Locale {
export function middleware(request: NextRequest) {
const { pathname, search } = request.nextUrl;
// Skip public files and API routes
if (
pathname.includes(".") ||
pathname.startsWith("/api") ||
pathname.startsWith("/_next")
) {
return NextResponse.next();
}
// 1. Check URL locale
// 1. If URL has a locale, pass it through with headers (+ sync cookie if needed)
const localeFromPath = getLocaleFromPathname(pathname);
// 2. Check cookie locale
const localeFromCookie = getLocaleFromCookie(request);
const preferredLocale = localeFromPath ?? localeFromCookie ?? getLocaleFromHeaders(request);
// 3. Check browser locale
const localeFromBrowser = getLocaleFromHeaders(request);
// Priority: URL > Cookie > Browser
const preferredLocale =
localeFromPath || localeFromCookie || localeFromBrowser;
// Faqat kerakli sahifalarni redirect qilamiz
const isPublicPage = PUBLIC_PAGES.some((page) => pathname === page);
if (isPublicPage) {
const url = request.nextUrl.clone();
url.pathname = `/${DEFAULT_LOCALE}/verify-otp`;
url.search = search; // ?code=1111&phone=...
return NextResponse.redirect(url);
}
// If URL has no locale, redirect with preferred locale
// 2. No locale in URL → redirect to preferred locale
if (!localeFromPath) {
const newUrl = new URL(`/${preferredLocale}/${pathname}`, request.url);
const newUrl = new URL(`/${preferredLocale}${pathname}`, request.url);
newUrl.search = search;
return NextResponse.redirect(newUrl);
}
// If URL locale differs from cookie, update cookie
if (localeFromPath !== localeFromCookie) {
const response = NextResponse.next();
// ✅ Set cookie on server side
response.cookies.set("NEXT_LOCALE", localeFromPath, {
path: "/",
maxAge: 31536000, // 1 year
sameSite: "lax",
});
// ✅ Pass locale to request headers for server components
// 3. Build response with locale headers for server components
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-locale", localeFromPath);
requestHeaders.set("x-pathname", pathname);
return NextResponse.next({
request: {
headers: requestHeaders,
},
const response = NextResponse.next({ request: { headers: requestHeaders } });
// 4. Sync cookie if it differs from URL locale
if (localeFromPath !== localeFromCookie) {
response.cookies.set("NEXT_LOCALE", localeFromPath, {
path: "/",
maxAge: 31_536_000, // 1 year
sameSite: "lax",
});
}
// Normal flow - just pass locale in headers
const response = NextResponse.next();
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-locale", localeFromPath);
requestHeaders.set("x-pathname", pathname);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
return response;
}
export const config = {
matcher: [
// Match all pathnames except for
// - … if they start with `/api`, `/_next` or `/_vercel`
// - … the ones containing a dot (e.g. `favicon.ico`)
'/((?!api|_next|_vercel|.*\\..*).*)',
],
matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"],
};

View File

@@ -6,7 +6,7 @@
"build": "next build",
"dev": "next dev",
"lint": "eslint .",
"start": "next start"
"start": "next start -p 3909"
},
"dependencies": {
"@formatjs/intl-localematcher": "^0.8.0",
@@ -55,6 +55,7 @@
"next": "16.0.10",
"next-intl": "^4.7.0",
"next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "19.2.0",
"react-day-picker": "9.8.0",
"react-dom": "19.2.0",

1470
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
public/catalog.pdf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
public/images/about/pp.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
public/pp_fiting.pdf Normal file

Binary file not shown.

BIN
public/pp_flanes.pdf Normal file

Binary file not shown.

BIN
public/varnix.pdf Normal file

Binary file not shown.

View File

@@ -1,3 +1,8 @@
interface ProductTypes {
id: number;
currentPage: number;
}
export const endPoints = {
category: {
all: "category/",
@@ -5,16 +10,43 @@ export const endPoints = {
subCategory: {
byId: (id: number) => `subCategory/?category=${id}`,
},
services: {
all: "firesafety/?page_size=500",
detail: (id: number) => `firesafety/${id}`,
},
product: {
byCategory: (categoryId: number) => `product/?category=${categoryId}`,
bySubCategory: (subCategoryId: number) =>
`product/?subCategory=${subCategoryId}`,
byCategory: ({ id, currentPage }: ProductTypes) => {
let link = "product/?page_size=12";
if (id) link += `&category=${id}`;
if (currentPage) link += `&page=${currentPage}`;
return link;
},
bySubCategory: ({ id, currentPage }: ProductTypes) => {
let link = "product/";
if (id) link += `?subCategory=${id}`;
if (currentPage) link += `&page=${currentPage}`;
return link;
},
byCatalogSection: ({ id, currentPage }: ProductTypes) => {
let link = "product";
if (id) link += `?catalog_section=${id}`;
if (currentPage) link += `&page=${currentPage}`;
return link;
},
detail: (id: number) => `product/${id}/`,
},
faq: "faq/",
gallery: "gallery/?page_size=500",
contact: "contact/",
statistics: "statistics/",
banner: "banner/?page_size=500",
navbar: "navigationitem/?page_size=500",
sertificate: "document/?type=certificate",
normative: "document/?type=normative",
guides: "guide/",
filter: {
size: "size/",
sizePageItems: "size/?page_size=500",
@@ -22,6 +54,12 @@ export const endPoints = {
catalog: "catalog/",
catalogPageItems: "catalog/?page_size=500",
catalogCategoryId: (id: number) => `catalog/?category=${id}&page_size=500`,
child: ({ id }: { id?: number }) => {
const link = "catalogsection/?page_size=500";
if (id) return `${link}&parent=${id}`;
return link;
},
catalogSection: "catalogsection/?page_size=500",
},
post: {
sendNumber: "callBack/",

13
zustand/useCatalog.ts Normal file
View File

@@ -0,0 +1,13 @@
import { create } from "zustand";
type CatalogType = {
parentID: number;
setParentID: (id: number) => void;
};
export const useCatalog = create<CatalogType>((set) => {
return {
parentID: 0,
setParentID: (id: number) => set({ parentID: id }),
};
});

14
zustand/useService.ts Normal file
View File

@@ -0,0 +1,14 @@
import { create } from "zustand";
interface ServiceIdZustandType {
serviceId: number;
setServiceId: (serviceId: number) => void;
}
export const useServiceDetail = create<ServiceIdZustandType>((set) => ({
serviceId: 0,
setServiceId: (data: number) =>
set({
serviceId: data,
}),
}));