Compare commits

...

66 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
nabijonovdavronbek619@gmail.com
91fe13f9bf or gitea 2026-02-16 09:31:18 +05:00
nabijonovdavronbek619@gmail.com
7bd4dbf10f yandex statistics 2026-02-11 17:14:57 +05:00
nabijonovdavronbek619@gmail.com
4ea825557b yandex metrika 2026-02-11 09:40:09 +05:00
nabijonovdavronbek619@gmail.com
410a35aa4c yandex metrika 2026-02-11 09:38:57 +05:00
nabijonovdavronbek619@gmail.com
4ee9ae3acb for gitea 2026-02-10 20:58:22 +05:00
nabijonovdavronbek619@gmail.com
a7b665b50c filter added 2026-02-10 20:55:27 +05:00
nabijonovdavronbek619@gmail.com
6fbe23109c vreadCrumb added 2026-02-10 16:41:40 +05:00
nabijonovdavronbek619@gmail.com
071685b52c initial loading image updated 2026-02-10 13:58:55 +05:00
nabijonovdavronbek619@gmail.com
f688a01afd main catalog card color changed 2026-02-10 13:57:25 +05:00
nabijonovdavronbek619@gmail.com
1ab5c6b741 chnage linear graident colors 2026-02-10 13:44:44 +05:00
nabijonovdavronbek619@gmail.com
cd86d6397e chnage created by , commented instagram 2026-02-10 10:12:33 +05:00
nabijonovdavronbek619@gmail.com
dcdfce4d79 contact form updated , added telegram instagram icon buttons 2026-02-09 19:21:22 +05:00
nabijonovdavronbek619@gmail.com
625e21394f favicaon io 2026-02-09 17:00:08 +05:00
nabijonovdavronbek619@gmail.com
4c2dc6a0f5 metadata 2026-02-09 16:42:46 +05:00
nabijonovdavronbek619@gmail.com
a9161b16b9 global 2026-02-09 16:13:53 +05:00
nabijonovdavronbek619@gmail.com
6a598ebfd3 product page and detail page updated 2026-02-09 11:24:32 +05:00
nabijonovdavronbek619@gmail.com
2706dc387f connected to detail page and detail page form modal for get product price 2026-02-07 14:38:27 +05:00
nabijonovdavronbek619@gmail.com
6a89bc1acc detail page connected to backend , modal form for one product connected to backend 2026-02-07 14:36:11 +05:00
115 changed files with 6496 additions and 1285 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,3 +1,4 @@
import { Breadcrumb } from "@/components/breadCrumb";
import Catalog from "@/components/pages/home/blog/catalog";
import { ProductBanner } from "@/components/pages/products";
import { MainSubCategory } from "@/components/pages/subCategory";
@@ -6,8 +7,11 @@ export default function Page() {
return (
<div className="bg-[#1e1d1c] pb-30">
<ProductBanner />
<div className="max-w-300 mx-auto w-full pt-20">
<MainSubCategory />
<div className="max-w-300 mx-auto w-full pt-5">
<div className="pb-8">
<Breadcrumb />
</div>
<Catalog />
</div>
</div>
);

View File

@@ -0,0 +1,89 @@
"use client";
import { Features, RightSide, SliderComp } from "@/components/pages/products";
import { useProductPageInfo } from "@/zustand/useProduct";
import { useQuery } from "@tanstack/react-query";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { LoadingSkeleton } from "@/components/pages/products/slug/loading";
import { EmptyState } from "@/components/pages/products/slug/empty";
import { Breadcrumb } from "@/components/breadCrumb";
import { useSearchParams } from "next/dist/client/components/navigation";
// Types
interface ProductImage {
id: number;
product: number;
image: string;
is_main: boolean;
order: number;
}
interface ProductDetail {
id: number;
name: string;
articular: string;
status: string;
description: string;
size: number;
price: string;
features: string[];
images: ProductImage[];
}
export default function SlugPage() {
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,
});
// Loading State
if (isLoading) {
return <LoadingSkeleton />;
}
// Empty State
if (!product) {
return <EmptyState />;
}
// Extract images
const productImages = product.images?.map((img) => img.image) || [];
const mainImage =
product.images?.find((img) => img.is_main)?.image || productImages[0] || "";
const features = product.features.map((item: any) => item.name);
return (
<div className="min-h-screen bg-[#1e1d1c] px-4 md:px-8 pb-35">
<div className="max-w-7xl mx-auto">
<div className="min-[400px]:pt-35 pt-45 pb-10">
<Breadcrumb />
</div>
{/* Main Product Section */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 mb-12">
{/* Left - Image Slider */}
<SliderComp imgs={productImages} />
{/* Right - Product Info */}
<RightSide
id={product.id}
title={product.name}
articular={product.articular}
status={product.status}
description={product.description}
price={product.price}
image={mainImage}
/>
</div>
{/* Features Section */}
{product.features && product.features.length > 0 && (
<Features features={features} />
)}
</div>
</div>
);
}

View File

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

View File

@@ -1,3 +1,4 @@
import { Breadcrumb } from "@/components/breadCrumb";
import { ProductBanner } from "@/components/pages/products";
import { MainSubCategory } from "@/components/pages/subCategory";
@@ -5,7 +6,10 @@ export default function Page() {
return (
<div className="bg-[#1e1d1c] pb-30">
<ProductBanner />
<div className="py-20">
<div className="pb-20">
<div className="max-w-350 mx-auto w-full py-10">
<Breadcrumb />
</div>
<MainSubCategory />
</div>
</div>

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

@@ -1,41 +0,0 @@
"use client";
import { DATA } from "@/lib/demoData";
import { Features, RightSide, SliderComp } from "@/components/pages/products";
export default function SlugPage() {
const statusColor =
DATA[0].status === "full"
? "text-green-500"
: DATA[0].status === "empty"
? "text-red-600"
: "text-yellow-800";
const statusText =
DATA[0].status === "full"
? "Sotuvda mavjud"
: DATA[0].status === "empty"
? "Sotuvda qolmagan"
: "Buyurtma asosida";
return (
<div className="min-h-screen bg-[#1e1d1c] py-40 px-4 md:px-8">
<div className="max-w-6xl mx-auto">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12">
<SliderComp imgs={DATA[0].images} />
<RightSide
id={1}
title={DATA[0].title}
name={DATA[0].name}
statusColor={statusColor}
statusText={statusText}
description={DATA[0].description}
image={DATA[0].images[0]}
/>
</div>
<Features features={DATA[0].features} />
</div>
</div>
);
}

View File

@@ -1,12 +0,0 @@
import { ProductBanner, Products } from "@/components/pages/products";
import FilterCatalog from "@/components/pages/products/filter/catalog/filterCatalog";
export default function Page() {
return (
<div className="bg-[#1e1d1c] pb-30">
<ProductBanner />
{/* <FilterCatalog /> */}
<Products />
</div>
);
}

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>

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -157,3 +157,9 @@ body {
.delay-300 {
animation-delay: 300ms;
}
.leo{
color: #979797;
}
/* jvjjjjvjvj */

View File

@@ -6,8 +6,7 @@ import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
import { InitialLoading } from "@/components/initialLoading/initialLoading";
import { Providers } from "@/components/provider";
("info@ignum-tech.com");
import Script from "next/script";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -19,48 +18,148 @@ 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: "FireForce - Emergency Services",
title: {
default: "Ignum Technologies - Fire Safety Systems Installation & Sales",
template: "%s | Ignum Technologies",
},
description:
"FireForce - Your trusted emergency response team bringing calm amidst chaos",
generator: "v0.app",
"Ignum Technologies specializes in professional fire safety systems installation and sales. Protect your property with cutting-edge fire detection, suppression, and alarm systems from certified experts.",
keywords: [
"fire safety systems",
"fire alarm installation",
"fire suppression systems",
"fire detection",
"Ignum Technologies",
"fire safety equipment",
"fire protection services",
"commercial fire systems",
"residential fire safety",
],
authors: [{ name: "Ignum Technologies" }],
creator: "Ignum Technologies",
publisher: "Ignum Technologies",
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" },
};
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>
<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"
dangerouslySetInnerHTML={{
__html: `
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
m[i].l=1*new Date();
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
ym(106736850, "init", {
clickmap:true,
trackLinks:true,
accurateTrackBounce:true,
webvisor:true,
ecommerce:"dataLayer"
});
`,
}}
/>
<noscript>
<div>
<img
src="https://mc.yandex.ru/watch/106736850"
style={{ position: "absolute", left: "-9999px" }}
alt=""
/>
</div>
</noscript>
</head>
<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 />;
}

195
components/breadCrumb.tsx Normal file
View File

@@ -0,0 +1,195 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ChevronRight, Home } from "lucide-react";
import { useTranslations } from "next-intl";
import { useCategory } from "@/zustand/useCategory";
import { useSubCategory } from "@/zustand/useSubCategory";
import { useProductPageInfo } from "@/zustand/useProduct";
interface BreadcrumbProps {
customLabels?: Record<string, string>;
className?: string;
}
export function Breadcrumb({
customLabels = {},
className = "",
}: BreadcrumbProps) {
const pathname = usePathname();
const t = useTranslations();
const category = useCategory((state) => state.category);
const subCategory = useSubCategory((state) => state.subCategory);
const product = useProductPageInfo((state) => state.product);
// Pathdan segments olish
const segments = pathname.split("/").filter((segment) => segment !== "");
// Agar locale bo'lsa, uni olib tashlash (uz, en, ru)
const locales = ["uz", "en", "ru"];
const filteredSegments = segments.filter(
(segment) => !locales.includes(segment),
);
// Agar faqat home page bo'lsa, breadcrumb ko'rsatmaslik
if (filteredSegments.length === 0) {
return null;
}
// Breadcrumb items yaratish
const breadcrumbItems: Array<{
label: string;
href: string;
isLast: boolean;
}> = [];
// Home qo'shish (har doim birinchi)
breadcrumbItems.push({
label: t("breadcrumb.home") || "Home",
href: "/",
isLast: false,
});
// Locale olish
const locale = segments.find((seg) => locales.includes(seg)) || "";
const localePrefix = locale ? `/${locale}` : "";
// Segmentlarni tahlil qilish
filteredSegments.forEach((segment, index) => {
const isLast = index === filteredSegments.length - 1;
if (segment === "catalog_page") {
// Catalog_page - asosiy kategoriyalar sahifasi
breadcrumbItems.push({
label: t("breadcrumb.catalog_page") || "Katalog",
href: `${localePrefix}/catalog_page`,
isLast: isLast,
});
} else if (segment === "subCategory") {
// SubCategory - kategoriya nomi ko'rsatiladi
if (category?.name) {
breadcrumbItems.push({
label: category.name,
href: `${localePrefix}/catalog_page/subCategory`,
isLast: isLast,
});
}
} else if (segment === "products") {
if (subCategory?.name) {
// Agar subCategory orqali kelgan bo'lsa
// 1. Kategoriya
if (category?.name) {
const categoryInBreadcrumb = breadcrumbItems.find(
(item) => item.label === category.name,
);
if (!categoryInBreadcrumb) {
breadcrumbItems.push({
label: category.name,
href: `${localePrefix}/catalog_page/subCategory`,
isLast: false,
});
}
}
// 2. SubKategoriya
breadcrumbItems.push({
label: subCategory.name,
href: `${localePrefix}/catalog_page/subCategory`,
isLast: false,
});
} else if (category?.name) {
// To'g'ridan-to'g'ri kategoriyadan products ga kelgan
breadcrumbItems.push({
label: category.name,
href: `${localePrefix}/catalog_page`,
isLast: false,
});
}
} else if (segment.startsWith("[") && segment.endsWith("]")) {
// Dynamic route (masalan, [slug])
// Custom label yoki default
const slugValue = segment.replace(/\[|\]/g, "");
const label = customLabels[slugValue] || slugValue;
breadcrumbItems.push({
label: label,
href: `${localePrefix}/${filteredSegments.slice(0, index + 1).join("/")}`,
isLast: isLast,
});
} else {
// Boshqa segmentlar
const label = getLabel(segment);
breadcrumbItems.push({
label: label,
href: `${localePrefix}/${filteredSegments.slice(0, index + 1).join("/")}`,
isLast: isLast,
});
}
});
// Default label translator
function getLabel(segment: string): string {
// Agar custom label berilgan bo'lsa
if (customLabels[segment]) {
return customLabels[segment];
}
// Agar translation mavjud bo'lsa
try {
if (segment === "special_product") {
return product.name;
}
if(segment === 'detail') return '';
return t(`breadcrumb.${segment}`);
} catch {
// Aks holda, segment nomini formatlash
return segment
.replace(/-/g, " ")
.replace(/_/g, " ")
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}
}
return (
<nav aria-label="Breadcrumb" className={`py-4 ${className} sm:px-5 px-2`}>
<ol className="flex items-center flex-wrap gap-2 sm:text-xl text-lg">
{breadcrumbItems.map((item, index) => (
<li
key={`${item.label}-${index}`}
className="flex items-center gap-2"
>
{index > 0 && (
<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-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-200 font-medium line-clamp-1">
{item.label}
</span>
) : (
// Regular link
<Link
href={item.href}
className="text-gray-200 hover:text-red-600 transition-colors duration-200 line-clamp-1"
>
{item.label}
</Link>
)}
</li>
))}
</ol>
</nav>
);
}

View File

@@ -23,20 +23,23 @@
flex-direction: column;
align-items: center;
gap: 2rem;
}
.initial-svg {
width: 250px;
height: 250px;
}
.initial-loading-content svg {
width: 100%;
height: 100%;
animation: initialFloat 2s ease-in-out infinite, initialScale 1.5s ease-in-out infinite;
}
.initial-path {
fill: url(#initial-gradient);
filter: url(#initial-glow);
/* SVG path'larga stil - tiniq va aniq */
.logo-path {
fill: url(#neon-gradient);
stroke: #ffffff;
stroke-width: 2;
animation: pathPulse 1.5s ease-in-out infinite;
stroke-width: 0.3;
filter: drop-shadow(0 0 3px rgba(255, 255, 255, 0.3));
animation: pathPulse 2s ease-in-out infinite;
}
/* Loading dots animation */
@@ -53,8 +56,9 @@
.loading-dots span {
width: 12px;
height: 12px;
background: #ff0000;
background: linear-gradient(135deg, #e0e0e0 0%, #ffffff 50%, #c0c0c0 100%);
border-radius: 50%;
box-shadow: 0 0 5px rgba(255, 255, 255, 0.3);
animation: dotBounce 1.4s ease-in-out infinite;
}
@@ -76,7 +80,7 @@
transform: translateY(0);
}
50% {
transform: translateY(-30px);
transform: translateY(-15px);
}
}
@@ -85,18 +89,18 @@
transform: scale(1);
}
50% {
transform: scale(1.1);
transform: scale(1.03);
}
}
@keyframes pathPulse {
0%, 100% {
opacity: 1;
stroke-width: 2;
filter: drop-shadow(0 0 3px rgba(255, 255, 255, 0.3));
}
50% {
opacity: 0.7;
stroke-width: 3;
opacity: 0.95;
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.4));
}
}
@@ -115,3 +119,23 @@
body:has(.initial-loading:not(.fade-out)) {
overflow: hidden;
}
/* Responsive */
@media (max-width: 768px) {
.initial-loading-content {
width: 200px;
height: 200px;
}
}
@media (max-width: 480px) {
.initial-loading-content {
width: 150px;
height: 150px;
}
.loading-dots span {
width: 10px;
height: 10px;
}
}

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

@@ -3,18 +3,18 @@
import React from "react";
import { useState } from "react";
import { Mail, Phone, MapPin } from "lucide-react";
import { Mail, Phone, MapPin, Instagram, Send } from "lucide-react";
import { useLocale, useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
import { useMutation } from "@tanstack/react-query";
import { toast } from "react-toastify";
import axios from "axios";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
export function Footer() {
const locale = useLocale();
const [errors, setErrors] = useState<any>({});
const t = useTranslations();
const [email, setEmail] = useState("");
const [subscribed, setSubscribed] = useState(false);
@@ -34,10 +34,36 @@ export function Footer() {
},
});
const formatPhoneNumber = (value: string) => {
const numbers = value.replace(/\D/g, "");
if (!numbers.startsWith("998")) {
return "+998 ";
}
let formatted = "+998 ";
const rest = numbers.slice(3);
if (rest.length > 0) formatted += rest.slice(0, 2);
if (rest.length > 2) formatted += " " + rest.slice(2, 5);
if (rest.length > 5) formatted += " " + rest.slice(5, 7);
if (rest.length > 7) formatted += " " + rest.slice(7, 9);
return formatted;
};
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const formatted = formatPhoneNumber(e.target.value);
setEmail(formatted);
if (errors.address) {
setErrors({ ...errors, address: "" });
}
};
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)) });
}
};
@@ -49,10 +75,10 @@ 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-[#fa1d1d] 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">
<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">
<div className="mb-8 md:mb-0">
<h2 className="font-unbounded text-2xl font-bold text-white md:text-3xl">
{t("contactTitle")}
@@ -67,14 +93,16 @@ export function Footer() {
className="flex sm:flex-row flex-col w-full gap-2 md:w-auto"
>
<input
type="text"
placeholder={t("enterPhone")}
type="tel"
id="phone"
name="phone"
value={email}
minLength={9}
maxLength={13}
onChange={(e) => setEmail(e.target.value)}
className="font-almarai flex-1 rounded-full bg-white px-6 py-3 text-gray-800 placeholder-gray-400 focus:outline-none md:w-64"
required
onChange={handlePhoneChange}
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
errors.email ? "border-red-500" : "border-transparent"
}`}
placeholder="+998 90 123 45 67"
maxLength={17}
/>
<button
type="submit"
@@ -107,6 +135,20 @@ export function Footer() {
<p className="font-almarai text-sm leading-relaxed text-gray-300">
{t("footer.description")}
</p>
<div className="flex items-center gap-5 mt-5">
{/* <a
href=""
className="p-2 rounded-md bg-gray-700 hover:bg-red-500"
>
<Instagram />
</a> */}
<a
href="https://t.me/ignum_tech"
className="p-2 rounded-md bg-gray-700 hover:bg-red-500"
>
<Send />
</a>
</div>
</div>
{/* Quick Links */}
@@ -187,9 +229,7 @@ export function Footer() {
</li>
<li className="flex items-start gap-3">
<MapPin className="mt-1 h-5 w-5 shrink-0 text-white" />
<span>
{t("footer.address")}
</span>
<span>{t("footer.address")}</span>
</li>
</ul>
</div>
@@ -198,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-between gap-4 text-sm text-gray-400 md:flex-row md:items-center">
<div>Copyright © 2025 Ignum Company.</div>
<div className="flex gap-6">
<a href="#terms" className="hover:text-white">
Terms & Conditions
</a>
<a href="#privacy" className="hover:text-white">
Privacy Policy
</a>
</div>
<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>
<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>

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">
<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>
{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>
)}
</div>
<div className="flex items-center gap-5">
@@ -103,7 +138,8 @@ export function Navbar() {
</span>
<div>
<div className="text-white text-sm font-bold">
<div>+998-98-099-21-21</div> <div>+998-77-372-21-21</div>
<div>+998-55-055-21-21</div>
<div>+998-77-372-21-21</div>
</div>
</div>
</div>
@@ -162,51 +198,44 @@ export function Navbar() {
{/* Mobile Menu Links */}
<div className="flex flex-col p-6 gap-4">
<Link
href={`/${locale}/home`}
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
onClick={() => setIsMobileMenuOpen(false)}
>
{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>
{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-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{t("navbar.home")}
</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

@@ -45,7 +45,8 @@ export default function Form() {
const formRequest = useMutation({
mutationKey: [],
mutationFn: (data: FormData) => httpClient.post(endPoints.post.contact, data),
mutationFn: (data: FormData) =>
httpClient.post(endPoints.post.contact, data),
onSuccess: () => {
setSubmitStatus("success");
setFormData({
@@ -78,10 +79,13 @@ export default function Form() {
if (!formData.surname.trim()) {
newErrors.surname = "Last name is required";
}
if (!formData.address.trim()) {
newErrors.address = "address is required";
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.address)) {
newErrors.address = "Please enter a valid address";
const phoneNumbers = formData.address.replace(/\D/g, "");
if (phoneNumbers.length !== 12) {
newErrors.address =
t("validation.phoneRequired") || "To'liq telefon raqam kiriting";
} else if (!phoneNumbers.startsWith("998")) {
newErrors.address =
t("validation.phoneInvalid") || "Noto'g'ri telefon raqam";
}
if (!formData.theme.trim()) {
newErrors.theme = "theme is required";
@@ -97,6 +101,30 @@ export default function Form() {
return Object.keys(newErrors).length === 0;
};
const formatPhoneNumber = (value: string) => {
const numbers = value.replace(/\D/g, "");
if (!numbers.startsWith("998")) {
return "+998 ";
}
let formatted = "+998 ";
const rest = numbers.slice(3);
if (rest.length > 0) formatted += rest.slice(0, 2);
if (rest.length > 2) formatted += " " + rest.slice(2, 5);
if (rest.length > 5) formatted += " " + rest.slice(5, 7);
if (rest.length > 7) formatted += " " + rest.slice(7, 9);
return formatted;
};
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const formatted = formatPhoneNumber(e.target.value);
setFormData({ ...formData, address: formatted });
if (errors.address) {
setErrors({ ...errors, address: "" });
}
};
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
@@ -169,19 +197,19 @@ export default function Form() {
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<input
type="address"
name="address"
placeholder={t("contact.form.placeholders.email")}
type="tel"
id="phone"
name="phone"
value={formData.address}
onChange={handleChange}
onChange={handlePhoneChange}
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
errors.address ? "border-red-500" : "border-transparent"
}`}
placeholder="+998 90 123 45 67"
maxLength={17}
/>
{errors.address && (
<p className="font-almarai mt-1 text-xs text-red-500">
{errors.address}
</p>
<p className="mt-1 text-sm text-red-500">{errors.address}</p>
)}
</div>
<div>

View File

@@ -42,7 +42,7 @@ export function Contact() {
className="absolute inset-0"
style={{
background:
"radial-gradient(ellipse at bottom center, #d2610a 0%, #1e1d1ce9 70% , #1e1d1ce9 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,12 +1,11 @@
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import Image from "next/image";
import { Flame, Building2, Ambulance } 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";
interface ServiceItem {
icon: React.ReactNode;
@@ -16,6 +15,7 @@ interface ServiceItem {
export function AboutUs() {
const t = useTranslations();
const locale = useLocale();
const services: ServiceItem[] = [
{
icon: <Flame width={40} height={40} className="text-red-500" />,
@@ -76,9 +76,9 @@ export function AboutUs() {
{/* Button */}
<div>
<Button className="font-almarai bg-red-600 hover:bg-red-700 text-white font-bold px-8 py-3 rounded-full transition-colors duration-300 shadow-[0px_0px_2px_8px_#ff01015c]">
<Link href={`/${locale}/about`} className="font-almarai bg-red-600 hover:bg-red-700 text-white font-bold px-8 py-3 rounded-full transition-colors duration-300 shadow-[0px_0px_2px_8px_#ff01015c]">
{t("home.about.title")}
</Button>
</Link>
</div>
</div>

View File

@@ -1,100 +0,0 @@
import { useTranslations } from "next-intl";
import DotAnimatsiya from "../../dot/DotAnimatsiya";
export function Banner() {
const t = useTranslations();
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 */}
<button 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")}
</button>
</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

@@ -2,23 +2,21 @@
import { useQuery } from "@tanstack/react-query";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useEffect } from "react";
import CatalogCard from "../../products/catalog";
import CatalogCardSkeleton from "@/components/loadingSkleton";
import EmptyData from "@/components/EmptyData";
import { getRouteLang } from "@/request/getLang";
import { CategoryType } from "@/lib/types";
import { useTranslations } from "next-intl";
export default function Catalog() {
export default function Catalog() {
const language = getRouteLang();
const { data, isLoading } = useQuery({
queryKey: ["category", language],
queryFn: () => httpClient(endPoints.category.all),
select: (data): CategoryType[] => data?.data?.results,
});
useEffect(() => {
console.log("product catalog data: ", data);
}, [data]);
const t = useTranslations();
if (isLoading) {
return (
@@ -34,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

@@ -18,11 +18,11 @@ export function Line() {
>
<Phone className="text-white w-5 h-5" />
</span>
+123-456-7890
+998-55-055-21-21
</p>
</div>
<Image
src="/images/home/fireHydrant.png"
src="/images/home/balon.png"
alt="image"
width={60}
height={60}

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,101 +45,160 @@ 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>
{/* 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,#000000,#000000,#d2610a)]">
<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")}
</p>
<p className="font-almarai text-gray-400 max-w-80 w-full">
{t("home.services.services.operation.description")}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
<Image
src="/images/home/gruop.png"
alt="images"
width={200}
height={100}
className="object-contain sm:absolute bottom-0 right-2 z-10"
/>
{/* Conditional Rendering */}
{isLoading ? (
<div className="my-10">
<ServicesLoading />
</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,#000000,#000000,#d2610a)]">
<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")}
</p>
<p className="font-almarai text-gray-400 max-w-70 w-full">
{t("home.services.services.suppression.description")}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
<Image
src="/images/home/redShlang.png"
alt="images"
width={200}
height={100}
className="object-contain sm:absolute -bottom-4 -right-4 z-10"
/>
) : !data || (Array.isArray(data) && data.length === 0) ? (
<div className="my-10">
<EmptyServices />
</div>
</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,#d2610a,#000000,#000000)]">
<Image
src="/images/home/ambulance.png"
alt="images"
width={300}
height={200}
className="object-contain mt-5"
/>
<div className="space-y-4 py-6 px-8">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{t("home.services.services.safety.title")}
</p>
<p className="font-almarai text-gray-400 max-w-80 w-full">
{t("home.services.services.safety.description")}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
</div>
</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,#000000,#000000,#d2610a)]">
<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")}
</p>
<p className="font-almarai text-gray-400 max-w-70 w-full">
{t("home.services.services.monitoring.description")}
</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>
<Image
src="/images/home/balon.png"
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,#000000,#d2610a)] flex sm:flex-row flex-col gap-5 items-center justify-between">
<h2 className="font-unbounded sm:text-3xl text-xl font-semibold font-armanai text-white">
{t("home.services.viewMoreServices")}
</h2>
<Link
href="/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"
) : (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
>
{/* cards */}
<div className="max-w-250 w-full mx-auto overflow-hidden flex sm:flex-row flex-col items-center gap-5 my-10">
<motion.div
variants={cardVariants}
className="sm:w-[55%] overflow-hidden w-full"
onClick={() => setServiceId(data[0].id)}
>
{t("home.services.viewMore")}
<Link
href={`/${locale}/services/detail`}
className="overflow-hidden block hover:cursor-pointer relative space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300"
>
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{data[0].title}
</p>
<p className="font-almarai text-gray-400 max-w-80 w-full">
{data[0].subtitle}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
<Image
src={data[0].main_image}
alt="images"
width={200}
height={100}
className="object-contain sm:absolute bottom-0 -right-2 z-10"
/>
</Link>
</motion.div>
<Link
href={`/${locale}/services/detail`}
className="sm:w-[45%] w-full"
>
<motion.div
onClick={() => setServiceId(data[1].id)}
variants={cardVariants}
>
<div className="hover:cursor-pointer relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{data[1].title}
</p>
<p className="font-almarai text-gray-400 max-w-70 w-full">
{data[1].subtitle}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
<Image
src={data[1].main_image}
alt="images"
width={200}
height={100}
className="object-contain sm:absolute -bottom-4 -right-4 z-10"
/>
</div>
</motion.div>
</Link>
</div>
</div>
</div>
<div className="max-w-250 flex sm:flex-row flex-col items-start justify-between gap-5 mt- w-full mx-auto">
<motion.div
variants={cardVariants}
onClick={() => setServiceId(data[2].id)}
className="sm:w-[40%] w-full -mt-5"
>
<Link
href={`/${locale}/services/detail`}
className="block hover:cursor-pointer relative rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300"
>
<Image
src={data[2].main_image}
alt="images"
width={300}
height={200}
className="object-contain mt-5"
/>
<div className="space-y-4 py-6 px-8">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{data[2].title}
</p>
<p className="font-almarai text-gray-400 max-w-80 w-full">
{data[2].subtitle}
</p>
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
{t("home.services.learnmore")} <ChevronRight size={20} />
</button>
</div>
</Link>
</motion.div>
<div className="sm:w-[60%] w-full">
<motion.div
onClick={() => setServiceId(data[3].id)}
variants={cardVariants}
>
<Link href={`/${locale}/services/detail`}>
<div className="hover:cursor-pointer relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300">
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
{data[3].title}
</p>
<p className="font-almarai text-gray-400 max-w-70 w-full">
{data[3].subtitle}
</p>
<button className="font-almarai sm:mt-37 mt-0 text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
{t("home.services.learnmore")}{" "}
<ChevronRight size={20} />
</button>
<Image
src={data[3].main_image}
alt="images"
width={200}
height={100}
className="object-contain sm:absolute -bottom-20 -right-4 max-sm:-mb-20 z-10"
/>
</div>
</Link>
</motion.div>
<motion.div
variants={cardVariants}
className="py-6 px-8 rounded-xl mt-5 w-full p-5 bg-[linear-gradient(to_top_right,#000000,#190b00,#542604,#8f4308)] flex sm:flex-row flex-col gap-5 items-center justify-between hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300"
>
<h2 className="font-unbounded sm:text-3xl text-xl font-semibold font-armanai text-white">
{t("home.services.viewMoreServices")}
</h2>
<Link
href={`/${locale}/services`}
className="font-almarai shadow-[0px_0px_2px_6px_#a60404ad] bg-red-600 hover:bg-red-700 text-white font-bold sm:py-3 sm:px-8 px-8 py-2 rounded-full transition duration-300 transform hover:scale-105 w-fit"
>
{t("home.services.viewMore")}
</Link>
</motion.div>
</div>
</div>
</motion.div>
)}
</div>
</div>
);

View File

@@ -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;
@@ -84,14 +84,14 @@ export default function CatalogCard({
};
const navigateLink = have_sub_category
? `/${locale}/catalog_page?category=${id}`
: `/${locale}/products?category=${id}`;
? `/${locale}/catalog_page/subCategory?category=${id}`
: `/${locale}/catalog_page/products?category=${id}`;
return (
<Link
href={navigateLink}
onClick={updateZustands}
className="group relative h-112.5 w-full overflow-hidden rounded-2xl bg-[#17161679] from-[#444242] 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,151 +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 } 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 subCategory = useSubCategory((state) => state.subCategory);
const [dataExpanded, setDataExpanded] = useState<boolean>(false);
const [numberExpanded, setNumberExpanded] = useState<boolean>(false);
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);
const { data: catalog } = useQuery({
queryKey: ["catalog"],
queryFn: () => httpClient(endPoints.filter.catalogCategoryId(category.id)),
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"],
queryFn: () => httpClient(endPoints.filter.sizeCategoryId(category.id)),
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);
console.log("catalog: ", catalog, "size: ", 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);
console.log("filter: ", filter);
return (
<div className="space-y-3 lg:max-w-70 lg:px-0 px-3 w-full text-white">
{/* Bo'lim filtri */}
{visibleSectionData && (
<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) => (
<div
key={item.id}
onClick={() => toggleFilter(item)}
className="hover:cursor-pointer flex items-center gap-2 w-auto shrink-0"
>
<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 && (
<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) => (
<div
key={item.id}
onClick={() => toggleFilter(item)}
className="hover:cursor-pointer flex items-center gap-2 w-auto shrink-0"
>
<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,49 +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 { useMemo } from "react";
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 filter = useFilter((state) => state.filter);
const getFiltersByType = useFilter((state)=>state.getFiltersByType)
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);
// 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
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.subCategory.byId(category.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]);
}, [
category.id,
category.have_sub_category,
subCategory.id,
currentPage,
parentID,
queryParams,
]);
// ── Query ────────────────────────────────────────────────────────────────
const { data, isLoading, error } = useQuery({
queryKey: ["products", category.id , queryParams],
queryKey: [
"products",
category.id,
category.have_sub_category,
subCategory.id,
parentID,
queryParams,
currentPage,
],
queryFn: () => httpClient(requestLink),
select: (data) => 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) => (
@@ -55,30 +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) => (
<ProductCard
key={item.id} // ✅ index o'rniga id ishlatish
title={item.name}
image={item.image}
slug={item.slug}
<div className="space-y-4">
<div
className={`grid lg:grid-cols-4 sm:grid-cols-2 grid-cols-1 gap-5 transition-opacity ${
isLoading ? "opacity-50 pointer-events-none" : "opacity-100"
}`}
>
{results.map((item: any) => (
<ProductCard
key={item.id}
getProduct={() => setProduct(item)}
title={item.name}
image={item?.images?.[0]?.image || ""}
slug="special_product"
/>
))}
</div>
{totalPages > 1 && (
<PaginationLite
currentPage={currentPage}
totalPages={totalPages}
onChange={(p) => setCurrentPage(p)}
/>
))}
)}
</div>
);
}

View File

@@ -1,5 +1,6 @@
"use client";
import { useLocale } from "next-intl";
import { useLocale, useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
@@ -7,34 +8,152 @@ interface ProductCardProps {
title: string;
image: string;
slug: string;
getProduct: () => void;
}
export default function ProductCard({
title,
image,
slug,
getProduct,
}: ProductCardProps) {
const locale = useLocale();
const t = useTranslations();
return (
<Link href={`/${locale}/products/${slug}`}>
<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-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] py-10">
<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

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

View File

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

View File

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

View File

@@ -1,123 +1,152 @@
import { usePriceModalStore } from "@/store/useProceModalStore";
import { Facebook } from "lucide-react";
"use client";
const socialLinks = [
{ name: "telegram", icon: "✈️", color: "#0088cc" },
{ name: "facebook", icon: <Facebook />, color: "#1877F2" },
{ name: "odnoklassniki", icon: "ok", color: "#ED7100" },
{ name: "vkontakte", icon: "VK", color: "#0077FF" },
{ name: "twitter", icon: "𝕏", color: "#1DA1F2" },
{ name: "whatsapp", icon: "W", color: "#25D366" },
];
import { usePriceModalStore } from "@/zustand/useProceModalStore";
import { Check, Instagram, Send, Share2 } from "lucide-react";
import { useTranslations } from "next-intl";
import { useParams } from "next/navigation";
import { useState } from "react";
interface RightSideProps {
id: number;
title: string;
name: string;
articular: string;
status: string;
description: string;
statusText: string;
statusColor: string;
price: string;
image: string;
}
export function RightSide({
title,
name,
articular,
status,
description,
statusColor,
statusText,
price,
id,
image,
}: RightSideProps) {
const openModal = usePriceModalStore((state) => state.openModal);
const t = useTranslations();
const params = useParams();
const locale = params.locale || "uz";
const [copied, setCopied] = useState(false);
const handleShare = async () => {
const productUrl = `${window.location.origin}/${locale}/catalog_page/products/special_product?productId=${id}`;
try {
// Modern Web Share API dan foydalanish (mobil qurilmalar uchun)
if (navigator.share) {
await navigator.share({
title: title,
text: `${title} - ${description.slice(0, 100)}...`,
url: productUrl,
});
} else {
// Desktop uchun clipboard ga copy qilish
await navigator.clipboard.writeText(productUrl);
setCopied(true);
// 2 soniyadan keyin "Copied" holatini o'chirish
setTimeout(() => {
setCopied(false);
}, 2000);
}
} catch (error) {
console.error("Share error:", error);
// Fallback - clipboard ga copy qilish
try {
await navigator.clipboard.writeText(productUrl);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 2000);
} catch (clipboardError) {
console.error("Clipboard error:", clipboardError);
}
}
};
const handleGetPrice = () => {
openModal({
id: id,
id,
name: title,
image: image,
inStock: true,
image,
inStock: status === "Sotuvda mavjud",
});
};
// Status color logic
const isInStock = status === "Sotuvda mavjud";
const statusColor = isInStock
? "bg-green-600/20 text-green-400 border border-green-600/30"
: "bg-red-600/20 text-red-400 border border-red-600/30";
return (
<div className="flex flex-col justify-center">
<div className="flex flex-col justify-center space-y-6">
{/* Title */}
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold text-white mb-4 leading-tight">
<h1 className="text-xl md:text-3xl font-unbounded font-bold text-white leading-tight">
{title}
</h1>
{/* Article ID */}
<div className="mb-3">
<p className="text-gray-400">
Artikul:
<span className="text-white font-semibold">{name}</span>
</p>
<div className="flex items-center gap-2 text-sm md:text-base">
<span className="text-gray-400">Artikul:</span>
<span className="text-white font-semibold">{articular}</span>
</div>
{/* Status Badge */}
<div className="mb-2">
<div>
<span
className={`inline-block py-2 rounded text-sm font-semibold ${statusColor}`}
className={`inline-block px-4 py-2 rounded-lg text-sm font-semibold ${statusColor}`}
>
{statusText}
{status}
</span>
</div>
{/* description */}
<div className="mb-2">
<p className="text-sm font-bold text-white mb-4 leading-tight">
{/* Description */}
<div className="border-l-4 border-red-700 pl-4">
<p className="text-sm md:text-base text-gray-300 leading-relaxed">
{description}
</p>
</div>
{/* Price Section */}
<div className="mb-8">
<h2 className="text-2xl md:text-3xl font-bold text-red-700 mb-6">
17.00$
</h2>
<div className="bg-[#1716169f] rounded-xl p-5 space-y-6">
{/* Action Button */}
<button
onClick={handleGetPrice}
className="w-full bg-red-700 hover:bg-red-800 text-white font-bold py-4 px-6 rounded-lg transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-red-700/50"
>
{t("products.send")}
</button>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-4 mb-6">
{/* <button
onClick={onPriceClick}
className="flex-1 bg-red-700 hover:bg-red-800 text-white font-bold py-3 px-6 rounded-lg transition duration-300 transform hover:scale-105"
>
Narxni bilish
</button> */}
{/* Social Share */}
<div className="pt-4 border-t border-gray-800 flex items-center gap-5">
<button
onClick={handleGetPrice}
className="flex-1 border-2 border-red-700 text-red-700 hover:bg-red-50 font-bold py-3 px-6 rounded-lg transition duration-300"
onClick={handleShare}
className="flex items-center gap-3 mb-3 text-gray-400 hover:text-white transition-colors group"
>
Xabar yuborish
{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>
{/* <button
onClick={() => setIsFavorite(!isFavorite)}
className="p-3 border-2 border-gray-600 rounded-lg hover:border-red-700 transition duration-300"
title="Add to favorites"
>
<Heart
size={24}
className={
isFavorite ? "fill-red-700 text-red-700" : "text-gray-600"
}
/>
</button> */}
</div>
{/* Social Share Icons */}
<div className="flex gap-3 items-center">
<div className="flex gap-2">
{socialLinks.map((social) => (
<a
key={social.name}
href="#"
className="w-10 h-10 rounded-lg flex items-center justify-center text-white text-sm font-bold transition duration-300 hover:scale-110"
style={{ backgroundColor: social.color }}
title={social.name}
>
{social.icon}
</a>
))}
</div>
<a
href="https://t.me/ignum_tech"
className="p-2 rounded-md bg-white text-red-500 hover:text-white hover:bg-red-500"
>
<Send />
</a>
</div>
</div>
</div>

View File

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

View File

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

View File

@@ -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

@@ -19,7 +19,7 @@ export function ServiceBanner() {
<div
className="absolute inset-0 z-10"
style={{
background: `linear-gradient(to top right, #d2610a 0%, #1e1d1ce3 30%, #1e1d1ce3 100%)`,
background: `linear-gradient(to right top, rgb(157 73 9) 0%, rgb(33 32 31 / 89%) 40%, rgba(30, 29, 28, 0.89) 100%)`,
}}
/>

View File

@@ -1,53 +1,54 @@
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 = [
{
id: "faq-1",
question: t("faq.question1.question"),
answer: t("faq.question1.answer"),
},
{
id: "faq-2",
question: t("faq.question2.question"),
answer: t("faq.question2.answer"),
},
{
id: "faq-3",
question: t("faq.question3.question"),
answer: t("faq.question3.answer"),
},
{
id: "faq-4",
question: t("faq.question4.question"),
answer: t("faq.question4.answer"),
},
{
id: "faq-5",
question: t("faq.question5.question"),
answer: t("faq.question5.answer"),
},
];
const locale = useLocale();
const faqItems: FAQItem[] = [
{
id: 1,
question: t("faq.question1.question"),
answer: t("faq.question1.answer"),
},
{
id: 2,
question: t("faq.question2.question"),
answer: t("faq.question2.answer"),
},
{
id: 3,
question: t("faq.question3.question"),
answer: t("faq.question3.answer"),
},
{
id: 4,
question: t("faq.question4.question"),
answer: t("faq.question4.answer"),
},
{
id: 5,
question: t("faq.question5.question"),
answer: t("faq.question5.answer"),
},
];
return (
<div className="bg-[#1e1d1c] py-20 pb-50 space-y-8">
{/* header */}
<div className="space-y-4 w-full ">
<div className="flex items-center gap-3 justify-center text-white text-xl">
<DotAnimatsiya /> 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";
@@ -32,7 +32,7 @@ export default function Card({
image,
category,
});
router.push(`/${locale}/products`);
router.push(`/${locale}/catalog_page/products`);
};
return (
<Link href="#" onClick={handleClick}>
@@ -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-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

@@ -1,10 +1,19 @@
"use client";
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";
import { toast } from "react-toastify";
interface FormType {
name: string;
product: number;
number: number; // ✅ String bo'lishi kerak
}
export function PriceModal() {
const t = useTranslations("priceModal");
@@ -12,32 +21,42 @@ export function PriceModal() {
const [formData, setFormData] = useState({
name: "",
phone: "+998 ",
captcha: "",
number: "+998 ",
});
const [errors, setErrors] = useState({
name: "",
phone: "",
captcha: "",
number: "",
});
const [captchaCode, setCaptchaCode] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false);
const formRequest = useMutation({
mutationFn: (data: FormType) =>
httpClient.post(endPoints.post.productContact, data),
onSuccess: () => {
setFormData({
name: "",
number: "+998 ",
});
toast.success(t("success") || "Muvaffaqiyatli yuborildi!");
closeModal();
},
onError: (error) => {
console.error("Error:", error);
toast.error(t("error") || "Xatolik yuz berdi");
},
});
// Generate random captcha
useEffect(() => {
if (isOpen) {
const code = Math.random().toString(36).substring(2, 8).toUpperCase();
setCaptchaCode(code);
}
}, [isOpen]);
// Reset form when modal closes
// Reset form when modal closes for github
useEffect(() => {
if (!isOpen) {
setFormData({ name: "", phone: "+998 ", captcha: "" });
setErrors({ name: "", phone: "", captcha: "" });
setFormData({
name: "",
number: "+998 ",
});
setErrors({
name: "",
number: "",
});
}
}, [isOpen]);
@@ -61,7 +80,6 @@ export function PriceModal() {
let formatted = "+998 ";
const rest = numbers.slice(3);
if (rest.length > 0) formatted += rest.slice(0, 2);
if (rest.length > 2) formatted += " " + rest.slice(2, 5);
if (rest.length > 5) formatted += " " + rest.slice(5, 7);
@@ -72,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: "" });
}
};
@@ -89,52 +107,44 @@ export function PriceModal() {
const validateForm = () => {
const newErrors = {
name: "",
phone: "",
captcha: "",
number: "",
};
// Name validation
if (!formData.name.trim()) {
newErrors.name = t("validation.nameRequired");
newErrors.name = t("validation.nameRequired") || "Ism kiritilishi shart";
}
const phoneNumbers = formData.phone.replace(/\D/g, "");
// Phone validation
const phoneNumbers = formData.number.replace(/\D/g, "");
if (phoneNumbers.length !== 12) {
newErrors.phone = t("validation.phoneRequired");
newErrors.number =
t("validation.phoneRequired") || "To'liq telefon raqam kiriting";
} else if (!phoneNumbers.startsWith("998")) {
newErrors.phone = t("validation.phoneInvalid");
}
if (!formData.captcha.trim()) {
newErrors.captcha = t("validation.captchaRequired");
} else if (formData.captcha.toUpperCase() !== captchaCode) {
newErrors.captcha = t("validation.captchaRequired");
newErrors.number =
t("validation.phoneInvalid") || "Noto'g'ri telefon raqam";
}
setErrors(newErrors);
return !newErrors.name && !newErrors.phone && !newErrors.captcha;
return !newErrors.name && !newErrors.number;
};
const handleSubmit = async (e: React.FormEvent) => {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!validateForm()) {
return;
}
setIsSubmitting(true);
// Telefon raqamni tozalash (faqat raqamlar)
const cleanPhone = formData.number.replace(/\D/g, "");
try {
// API call logikangiz
await new Promise((resolve) => setTimeout(resolve, 1500));
// Success
alert(t("success"));
closeModal();
} catch (error) {
alert(t("error"));
} finally {
setIsSubmitting(false);
}
const sendedData: FormType = {
name: formData.name,
number: Number(cleanPhone.slice(3)), // ✅ String sifatida yuborish
product: product?.id || 0,
};
formRequest.mutate(sendedData);
};
if (!isOpen || !product) return null;
@@ -143,49 +153,58 @@ export function PriceModal() {
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50"
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={closeModal}
/>
{/* Modal */}
<div className="relative bg-white rounded-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto">
<div className="relative bg-[#2a2a2a] rounded-2xl shadow-2xl w-full max-w-md max-h-[90vh] overflow-y-auto">
{/* Close button */}
<button
onClick={closeModal}
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors z-10"
className="absolute right-4 top-4 text-gray-400 hover:text-white transition-colors z-10"
aria-label="Close modal"
>
<X size={24} />
</button>
{/* Content */}
<div className="p-8">
<h2 className="text-3xl font-bold mb-8">{t("title")}</h2>
<div className="p-6 md:p-8">
<h2 className="text-2xl md:text-3xl font-bold text-white mb-6">
{t("title") || "Narx so'rash"}
</h2>
{/* Product Info */}
<div className="bg-[#f5f0e8] rounded-lg p-6 mb-8 flex items-center gap-6">
<div className="relative w-24 h-24 shrink-0">
<div className="bg-[#1e1e1e] rounded-lg p-4 mb-6 flex items-center gap-4">
<div className="relative w-20 h-20 shrink-0 bg-white rounded-lg overflow-hidden">
<Image
src={product.image}
src={product.image || "/placeholder.svg"}
alt={product.name}
fill
className="object-contain"
className="object-contain p-2"
/>
</div>
<div>
<h3 className="text-xl font-semibold mb-2">{product.name}</h3>
<span className="text-green-600 font-medium">
{t("product.inStock")}
<div className="flex-1">
<h3 className="text-white font-semibold mb-1 line-clamp-2">
{product.name}
</h3>
<span className="text-green-400 text-sm">
{product.inStock
? t("product.inStock") || "Sotuvda mavjud"
: t("product.outOfStock") || "Sotuvda yo'q"}
</span>
</div>
</div>
{/* Form */}
<form onSubmit={handleSubmit} className="space-y-6">
<form onSubmit={handleSubmit} className="space-y-5">
{/* Name */}
<div>
<label htmlFor="name" className="block text-sm font-medium mb-2">
{t("form.name")}
<label
htmlFor="name"
className="block text-sm font-medium text-gray-300 mb-2"
>
{t("form.name") || "Ismingiz"}
</label>
<input
type="text"
@@ -193,80 +212,50 @@ export function PriceModal() {
name="name"
value={formData.name}
onChange={handleInputChange}
placeholder={t("form.namePlaceholder")}
className={`w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 ${
errors.name ? "border-red-500" : "border-gray-300"
}`}
className={`w-full px-4 py-3 bg-[#1e1e1e] border ${
errors.name ? "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={t("form.namePlaceholder") || "Ismingizni kiriting"}
/>
{errors.name && (
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
<p className="mt-1 text-sm text-red-500">{errors.name}</p>
)}
</div>
{/* Phone */}
<div>
<label htmlFor="phone" className="block text-sm font-medium mb-2">
{t("form.phone")}
<label
htmlFor="phone"
className="block text-sm font-medium text-gray-300 mb-2"
>
{t("form.phone") || "Telefon raqam"}
</label>
<div className="relative">
<input
type="tel"
id="phone"
name="phone"
value={formData.phone}
onChange={handlePhoneChange}
placeholder={t("form.phonePlaceholder")}
className={`w-full pl-2 pr-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 ${
errors.phone ? "border-red-500" : "border-gray-300"
}`}
/>
</div>
{errors.phone && (
<p className="text-red-500 text-sm mt-1">{errors.phone}</p>
<input
type="tel"
id="phone"
name="phone"
value={formData.number}
onChange={handlePhoneChange}
className={`w-full px-4 py-3 bg-[#1e1e1e] border ${
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.number && (
<p className="mt-1 text-sm text-red-500">{errors.number}</p>
)}
</div>
{/* Captcha */}
{/* <div>
<label htmlFor="captcha" className="block text-sm font-medium mb-2">
{t("form.captcha")}
</label>
<div className="flex gap-4">
<div className="relative w-40 h-12 bg-linear-to-r from-purple-200 via-pink-200 to-blue-200 rounded-lg flex items-center justify-center">
<span className="text-2xl font-bold tracking-widest select-none">
{captchaCode}
</span>
<div className="absolute inset-0 opacity-30">
<svg className="w-full h-full">
<line x1="0" y1="0" x2="100%" y2="100%" stroke="currentColor" strokeWidth="1" />
<line x1="100%" y1="0" x2="0" y2="100%" stroke="currentColor" strokeWidth="1" />
</svg>
</div>
</div>
<input
type="text"
id="captcha"
name="captcha"
value={formData.captcha}
onChange={handleInputChange}
placeholder={t("form.captchaPlaceholder")}
className={`flex-1 px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 ${
errors.captcha ? "border-red-500" : "border-gray-300"
}`}
/>
</div>
{errors.captcha && (
<p className="text-red-500 text-sm mt-1">{errors.captcha}</p>
)}
</div> */}
{/* Submit Button */}
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-[#8B1538] hover:bg-[#6d1028] text-white font-semibold py-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={formRequest.isPending}
className="w-full bg-red-700 hover:bg-red-800 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-bold py-3 px-6 rounded-lg transition-all duration-300 transform hover:scale-105 disabled:hover:scale-100"
>
{isSubmitting ? t("form.submitting") : t("form.submit")}
{formRequest.isPending
? t("form.submitting") || "Yuborilmoqda..."
: t("form.submit") || "Yuborish"}
</button>
</form>
</div>

View File

@@ -14,7 +14,7 @@ export function Providers({ children }: { children: ReactNode }) {
return (
<div>
<QueryClientProvider client={queryClient}>
<BackAnimatsiya />
{/* <BackAnimatsiya /> */}
<Navbar />
{children}
<Footer />

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.",
@@ -222,16 +292,57 @@
"contact": "Contact",
"help": "Help"
},
"address":"Tashkent city, Yunusabad district, 3rd dead-end of Niyozbek Yoli street, house 39"
"address": "Tashkent city, Yunusabad district, 3rd dead-end of Niyozbek Yoli street, house 39",
"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": {
@@ -255,5 +366,67 @@
},
"success": "Your request has been sent successfully!",
"error": "An error occurred. Please try again."
}
},
"breadcrumb": {
"home": "Home",
"about": "About Us",
"services": "Services",
"products": "Products",
"contact": "Contact",
"blog": "Blog",
"catalog_page": "Products",
"fire-safety": "Fire Safety",
"fire-alarm": "Fire Alarm",
"fire-suppression": "Fire Suppression",
"installation": "Installation",
"maintenance": "Maintenance"
},
"filter": {
"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,24 +285,65 @@
"about": "О нас",
"services": "Услуги",
"products": "Продукты",
"faq": "FAQ"
"faq": "ЧЗВ"
},
"support": {
"title": "ПОДДЕРЖКА",
"contact": "Контакты",
"help": "Помощь"
},
"address":"г. Ташкент, Юнусабадский район, 3-й тупик улицы Ниязбек йўли, дом 39"
"address": "г. Ташкент, Юнусабадский район, 3-й тупик улицы Ниязбек йўли, дом 39",
"create": "Разработано",
"terms": "Условия использования",
"privacy": "Политика конфиденциальности"
},
"rasmlar": "Изображения",
"fotogalereya": "Фотогалерея",
"contactTitle": "Отправьте нам свой номер",
"contactSubTitle": "Наши сотрудники свяжутся с вами",
"enterPhone": "Введите ваш номер телефона",
"send": "Отправить",
"error": "Ошибка!",
"succes": "Отправлено!",
"priceModal": {
"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": [
"Эффективна для горючих жидкостей",
"Применяется на нефтебазах и складах",
"Быстро изолирует пожар",
"Высокий уровень безопасности"
]
}
}
},
"priceModal": {
"title": "Узнать цену",
"product": {
"inStock": "В наличии"
@@ -255,5 +366,67 @@
},
"success": "Ваш запрос успешно отправлен!",
"error": "Произошла ошибка. Попробуйте снова."
}
},
"breadcrumb": {
"home": "Главная",
"about": "О нас",
"services": "Услуги",
"products": "Продукция",
"contact": "Контакты",
"blog": "Блог",
"catalog_page": "Товары",
"fire-safety": "Пожарная безопасность",
"fire-alarm": "Пожарная сигнализация",
"fire-suppression": "Пожаротушение",
"installation": "Монтаж",
"maintenance": "Техническое обслуживание"
},
"filter": {
"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.",
@@ -222,16 +292,57 @@
"contact": "Aloqa",
"help": "Yordam"
},
"address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy"
"address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy",
"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": {
@@ -255,5 +366,67 @@
},
"success": "Sorovingiz muvaffaqiyatli yuborildi!",
"error": "Xatolik yuz berdi. Iltimos, qayta urinib koring."
}
},
"breadcrumb": {
"home": "Bosh sahifa",
"about": "Biz haqimizda",
"services": "Xizmatlar",
"catalog_page": "Mahsulotlar",
"subCategory": "{subCategory}",
"contact": "Bog'lanish",
"blog": "Blog",
"fire-safety": "Yong'in xavfsizligi",
"fire-alarm": "Yong'in signalizatsiyasi",
"fire-suppression": "Yong'in o'chirish",
"installation": "O'rnatish",
"maintenance": "Texnik xizmat"
},
"filter": {
"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
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-locale", localeFromPath);
requestHeaders.set("x-pathname", pathname);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
}
// Normal flow - just pass locale in headers
const response = NextResponse.next();
// 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",
});
}
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

Some files were not shown because too many files have changed in this diff Show More