Compare commits

..

82 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
nabijonovdavronbek619@gmail.com
74f1d7a9fd unuse tools removed 2026-02-07 11:28:49 +05:00
nabijonovdavronbek619@gmail.com
dbe8399086 add catalog filter card 2026-02-07 11:27:13 +05:00
nabijonovdavronbek619@gmail.com
66bf104cb7 add maps to contact page catalog_page added , email addres updated , remove page drop down 2026-02-06 23:03:46 +05:00
nabijonovdavronbek619@gmail.com
873bbb82a9 get products by filter connected 2026-02-06 21:47:56 +05:00
nabijonovdavronbek619@gmail.com
d4a242b169 catalog connected to backend 2026-02-06 18:57:47 +05:00
nabijonovdavronbek619@gmail.com
e99df29b81 subcategory zustand fixed and created product zustand for get product detail information 2026-02-05 20:08:13 +05:00
nabijonovdavronbek619@gmail.com
34cb524626 catalog part connected to backend , added empty data and loading component 2026-02-05 19:56:23 +05:00
nabijonovdavronbek619@gmail.com
3cf5e0efcf filter connected to backend 2026-02-05 15:11:36 +05:00
nabijonovdavronbek619@gmail.com
d7e1990cc9 faq , statistics , gallary parts connected to backend 2026-02-05 12:21:33 +05:00
nabijonovdavronbek619@gmail.com
87f304225e toast part design update 2026-02-05 11:28:19 +05:00
nabijonovdavronbek619@gmail.com
3c862ea104 connetcted to backend: form request 2026-02-05 11:02:57 +05:00
nabijonovdavronbek619@gmail.com
ca3e28779e loading and catalog card 2026-02-02 18:22:53 +05:00
nabijonovdavronbek619@gmail.com
63b363b142 priceContact added 2026-02-01 19:19:46 +05:00
nabijonovdavronbek619@gmail.com
96acd12d9c catalog part is done 2026-01-30 20:01:56 +05:00
nabijonovdavronbek619@gmail.com
b1095f2c12 filter 2026-01-30 11:24:07 +05:00
nabijonovdavronbek619@gmail.com
f439f9bbdf background animation and navbar logo animation 2026-01-29 17:08:51 +05:00
162 changed files with 8628 additions and 1230 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"; import { Statistics } from "@/components/pages/home";
export default function Page() { export default function Page() {
return ( return (
<div className="mb-0"> <div className="mb-0">
<AboutBanner />
<Story /> <Story />
<Statistics/> <Statistics/>
<WhyChooseUs/> <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

@@ -0,0 +1,18 @@
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";
export default function Page() {
return (
<div className="bg-[#1e1d1c] pb-30">
<ProductBanner />
<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

@@ -0,0 +1,17 @@
import { Breadcrumb } from "@/components/breadCrumb";
import { ProductBanner } from "@/components/pages/products";
import { MainSubCategory } from "@/components/pages/subCategory";
export default function Page() {
return (
<div className="bg-[#1e1d1c] pb-30">
<ProductBanner />
<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() { export default function Page() {
return redirect('/home') // return redirect('/home')
return <PaymentFailed />;
} }

View File

@@ -1,39 +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
title={DATA[0].title}
name={DATA[0].name}
statusColor={statusColor}
statusText={statusText}
description={DATA[0].description}
/>
</div>
<Features features={DATA[0].features} />
</div>
</div>
);
}

View File

@@ -1,10 +0,0 @@
import { ProductBanner, Products } from "@/components/pages/products";
export default function Page() {
return (
<div className="bg-[#1e1d1c] pb-30">
<ProductBanner />
<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 { Video } from "@/components/pages/home";
import { ServiceBanner, ServiceFaq } from "@/components/pages/services"; import {
ServiceBanner,
ServiceFaq,
ServicePageServices,
} from "@/components/pages/services";
export default function Page() { export default function Page() {
return ( return (
<div className=""> <div className="">
<ServiceBanner /> <ServiceBanner />
<OurService /> <ServicePageServices />
<Video /> <Video />
<ServiceFaq /> <ServiceFaq />
</div> </div>

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -135,4 +135,31 @@ body {
background: #1e1d1c; background: #1e1d1c;
} }
.loio {
color: #8b1515, #c91d1d;
}
/* globals.css ga qo'shing */
@keyframes shimmer {
100% {
transform: translateX(100%);
}
}
.animate-shimmer {
animation: shimmer 2s infinite;
}
.delay-150 {
animation-delay: 150ms;
}
.delay-300 {
animation-delay: 300ms;
}
.leo{
color: #979797;
}
/* jvjjjjvjvj */

View File

@@ -1,12 +1,12 @@
import React from "react"; import React from "react";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import { Analytics } from "@vercel/analytics/next";
import "./globals.css"; import "./globals.css";
import { Footer, Navbar } from "@/components/layout";
import { NextIntlClientProvider } from "next-intl"; import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server"; import { getMessages } from "next-intl/server";
import BackAnimatsiya from "@/components/backAnimatsiya/backAnimatsiya"; import { InitialLoading } from "@/components/initialLoading/initialLoading";
import { Providers } from "@/components/provider";
import Script from "next/script";
const geistSans = Geist({ const geistSans = Geist({
variable: "--font-geist-sans", variable: "--font-geist-sans",
@@ -18,51 +18,148 @@ const geistMono = Geist_Mono({
subsets: ["latin"], subsets: ["latin"],
}); });
export const metadata: Metadata = { // openGraphData tipini aniq belgilaymiz
title: "FireForce - Emergency Services", const openGraphData: Record<
"uz" | "ru" | "en",
{ title: string; description: string; locale: string }
> = {
uz: {
title: "Ignum Technologies - Professional Fire Safety Systems",
description: description:
"FireForce - Your trusted emergency response team bringing calm amidst chaos", "Tijorat va uy-joy obektlari uchun yongin aniqlash, bostirish va signalizatsiya tizimlarini oz ichiga olgan toliq yongin himoyasi yechimlari.",
generator: "v0.app", locale: "uz_UZ",
},
ru: {
title:
"Ignum Technologies - Профессиональные системы пожарной безопасности",
description:
"Полные решения по пожарной защите, включая системы обнаружения, тушения и сигнализации для коммерческих и жилых объектов.",
locale: "ru_RU",
},
en: {
title: "Ignum Technologies - Professional Fire Safety Systems",
description:
"Comprehensive fire protection solutions including detection, suppression, and alarm systems for commercial and residential properties.",
locale: "en_US",
},
};
export const metadata: Metadata = {
title: {
default: "Ignum Technologies - Fire Safety Systems Installation & Sales",
template: "%s | Ignum Technologies",
},
description:
"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: { icons: {
icon: [ icon: [
{ { url: "/icon-light-32x32.png", media: "(prefers-color-scheme: light)" },
url: "/icon-light-32x32.png", { url: "/icon-dark-32x32.png", media: "(prefers-color-scheme: dark)" },
media: "(prefers-color-scheme: light)", { url: "/icon.svg", type: "image/svg+xml" },
},
{
url: "/icon-dark-32x32.png",
media: "(prefers-color-scheme: dark)",
},
{
url: "/icon.svg",
type: "image/svg+xml",
},
], ],
apple: "/apple-icon.png", apple: "/apple-icon.png",
}, },
verification: { google: "your-google-verification-code" },
}; };
export default async function RootLayout({ export default async function RootLayout({
children, children,
params, params,
}: Readonly<{ }: Readonly<{ children: React.ReactNode; params: any }>) {
children: React.ReactNode;
params: any;
}>) {
const { locale } = await params; const { locale } = await params;
const messages: any = await getMessages(); const messages: any = await getMessages();
// Locale ga mos Open Graph ma'lumotini tanlaymiz
const og = openGraphData[locale as "uz" | "ru" | "en"] || openGraphData["uz"];
return ( return (
<html lang={locale} suppressHydrationWarning> <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 <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased`}
> >
{/* <InitialLoading />
<NextIntlClientProvider messages={messages} locale={locale}> <NextIntlClientProvider messages={messages} locale={locale}>
<BackAnimatsiya /> <Providers>{children}</Providers>
<Navbar /> </NextIntlClientProvider> */}
{children} {children}
<Footer />
<Analytics />
</NextIntlClientProvider>
</body> </body>
</html> </html>
); );

View File

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

48
components/EmptyData.tsx Normal file
View File

@@ -0,0 +1,48 @@
// components/EmptyData.tsx
import { PackageOpen, ShoppingBag } from "lucide-react";
import { useTranslations } from "next-intl";
interface EmptyDataProps {
title?: string;
description?: string;
icon?: "package" | "shopping";
}
export default function EmptyData({
title,
description,
icon = "package",
}: EmptyDataProps) {
const t = useTranslations();
const Icon = icon === "package" ? PackageOpen : ShoppingBag;
return (
<div className="flex flex-col items-center justify-center min-h-100 py-12 px-4">
{/* Animated background circles */}
<div className="relative flex items-center justify-center ">
{/* Icon */}
<div className="relative z-10 w-20 h-20 mx-auto mb-6 rounded-full bg-linear-to-br from-[#444242] to-gray-900 border border-white/10 flex items-center justify-center">
<Icon className="w-10 h-10 text-white/40" strokeWidth={1.5} />
</div>
</div>
{/* Text content */}
<div className="text-center space-y-3 max-w-md">
<h3 className="text-2xl font-unbounded font-bold text-white">
{title || t("no_data_title")}
</h3>
<p className="text-white/50 font-almarai">
{description || t("no_data_description")}
</p>
</div>
{/* Decorative elements */}
<div className="mt-8 flex gap-2">
<div className="w-2 h-2 rounded-full bg-red-500/30 animate-pulse" />
<div className="w-2 h-2 rounded-full bg-red-500/30 animate-pulse delay-150" />
<div className="w-2 h-2 rounded-full bg-red-500/30 animate-pulse delay-300" />
</div>
</div>
);
}

View File

@@ -1,51 +1,46 @@
"use client";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useQuery } from "@tanstack/react-query";
import Image from "next/image"; import Image from "next/image";
import { useEffect, useState } from "react";
import Marquee from "react-fast-marquee"; import Marquee from "react-fast-marquee";
const images = [
"/images/img2.webp",
"/images/img3.jpg",
"/images/img6.jpg",
"/images/img11.jpeg",
"/images/img12.png",
];
export default function HomeMarquee() { export default function HomeMarquee() {
const [marqImg, setMarqImg] = useState<string[]>(images);
const { data } = useQuery({
queryKey: ["gallery"],
queryFn: () => httpClient(endPoints.gallery),
select: (data) => {
const galary = data?.data?.results;
return galary.map((item: any) => item.image) || [];
},
});
useEffect(() => {
data && setMarqImg(data);
}, [data]);
return ( return (
<div className="bg-[#1e1d1c] py-5"> <div className="bg-[#1e1d1c] py-5">
<Marquee> <Marquee>
{marqImg.map((item) => (
<div className="relative sm:w-125 w-70 sm:h-62.5 h-40 mx-2 overflow-hidden rounded-xl"> <div className="relative sm:w-125 w-70 sm:h-62.5 h-40 mx-2 overflow-hidden rounded-xl">
<Image <Image
src="/images/img2.webp" src={item}
alt="images" alt="images"
fill fill
priority priority
className="object-cover" className="object-cover"
/> />
</div> </div>
<div className="relative sm:w-125 w-70 sm:h-62.5 h-40 mx-2 overflow-hidden rounded-xl"> ))}
<Image
src="/images/img3.jpg"
alt="images"
fill
className="object-cover"
/>
</div>
<div className="relative sm:w-125 w-70 sm:h-62.5 h-40 mx-2 overflow-hidden rounded-xl">
<Image
src="/images/img6.jpg"
alt="images"
fill
className="object-cover"
/>
</div>
<div className="relative sm:w-125 w-70 sm:h-62.5 h-40 mx-2 overflow-hidden rounded-xl">
<Image
src="/images/img11.jpeg"
alt="images"
fill
className="object-cover"
/>
</div>
<div className="relative sm:w-125 w-70 sm:h-62.5 h-40 mx-2 overflow-hidden rounded-xl">
<Image
src="/images/img12.png"
alt="images"
fill
className="object-cover"
/>
</div>
</Marquee> </Marquee>
</div> </div>
); );

View File

@@ -2,7 +2,7 @@ import "./back.css";
export default function BackAnimatsiya() { export default function BackAnimatsiya() {
return ( return (
<div className="fixed inset-0 w-full h-full flex items-center justify-center pointer-events-none z-0 opacity-100"> <div className="fixed inset-0 w-full h-full flex items-center justify-center pointer-events-none z-0 opacity-10">
<svg <svg
id="Layer_2" id="Layer_2"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

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

@@ -0,0 +1,141 @@
.initial-loading {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #0c0c0c 0%, #151313 100%);
z-index: 99999;
display: flex;
align-items: center;
justify-content: center;
opacity: 1;
transition: opacity 0.6s ease-out;
}
.initial-loading.fade-out {
opacity: 0;
pointer-events: none;
}
.initial-loading-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
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;
}
/* SVG path'larga stil - tiniq va aniq */
.logo-path {
fill: url(#neon-gradient);
stroke: #ffffff;
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 */
.initial-loading-text {
text-align: center;
}
.loading-dots {
display: flex;
gap: 8px;
justify-content: center;
}
.loading-dots span {
width: 12px;
height: 12px;
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;
}
.loading-dots span:nth-child(1) {
animation-delay: 0s;
}
.loading-dots span:nth-child(2) {
animation-delay: 0.2s;
}
.loading-dots span:nth-child(3) {
animation-delay: 0.4s;
}
/* Animations */
@keyframes initialFloat {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-15px);
}
}
@keyframes initialScale {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.03);
}
}
@keyframes pathPulse {
0%, 100% {
opacity: 1;
filter: drop-shadow(0 0 3px rgba(255, 255, 255, 0.3));
}
50% {
opacity: 0.95;
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.4));
}
}
@keyframes dotBounce {
0%, 80%, 100% {
transform: scale(0);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
/* Smooth scroll prevention */
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

@@ -0,0 +1,106 @@
"use client";
import { useState, useEffect } from "react";
import "./initialLoading.css";
export function InitialLoading() {
const [isLoading, setIsLoading] = useState(true);
const [isVisible, setIsVisible] = useState(true);
useEffect(() => {
// Faqat birinchi yuklanishda ishga tushadi
const hasVisited = sessionStorage.getItem("hasVisited");
if (hasVisited) {
// Agar oldin tashrif buyurilgan bo'lsa, darhol yashirish
setIsLoading(false);
setIsVisible(false);
return;
}
// Birinchi tashrif
const loadingTimer = setTimeout(() => {
setIsLoading(false);
// Fade out animatsiyasi
const hideTimer = setTimeout(() => {
setIsVisible(false);
sessionStorage.setItem("hasVisited", "true");
}, 1000); // Fade out duration
return () => clearTimeout(hideTimer);
}, 1500); // Loading duration
return () => clearTimeout(loadingTimer);
}, []);
// Agar ko'rinmas bo'lsa, hech narsa render qilmaymiz
if (!isVisible) return null;
return (
<div className={`initial-loading ${!isLoading ? "fade-out" : ""}`}>
<div className="initial-loading-content">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 423.22 424.82"
className="w-full h-full"
>
<defs>
{/* Qizil neon gradient for github*/}
<linearGradient id="neon-gradient" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#979797" />
<stop offset="50%" stopColor="#e4e4e4" />
<stop offset="100%" stopColor="#9a9a9a" />
</linearGradient>
{/* Neon glow filter */}
<filter id="neon-glow">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<g>
<polygon
className="logo-path"
points="352.86 365.08 347.11 365.08 347.11 381 289.38 381 289.38 365.08 283.63 365.08 283.63 381 204.78 381 204.78 365.08 199.03 365.08 199.03 381 141.32 381 141.32 365.08 135.57 365.08 135.57 381 77.87 381 77.87 365.08 71.86 365.08 71.85 381 17.21 381 17.21 386.74 72.11 386.74 72.11 424.82 74.04 424.82 74.04 386.74 350.94 386.74 350.94 424.82 352.86 424.82 352.86 386.74 406.02 386.74 406.02 381 352.86 381 352.86 365.08"
/>
<path
className="logo-path"
d="m72.11,157.74v73.55l-41.42,23.89h3.83l37.58-21.68-.04,21.68h5.79v-24.99l57.7-33.27v58.26h5.75v-57.15l57.71-33.29v90.44h5.75v-90.43l39.43,22.77,39.43,22.77v44.89h5.74v-41.58l57.73,33.32v8.25h5.75v-95.41h-1.92v82.75l-61.56-35.55v-98.77l59.59-34.35-.97-1.66-58.62,33.81V0h-1.92v42.86h-88.43v115.23l-5.2,3-14.21,8.16-27.25,15.74-11.05,6.37v-73.53l36.14-20.85-.96-1.66-35.19,20.29v-35.03h-1.92v36.13l-65.36,37.7v-37.25h-1.92v38.35l-53.08,30.6.96,1.67,52.12-30.07ZM204.78,48.58h78.85v60.71l-78.85,45.48V48.58Zm0,108.41l78.85-45.49v92.14l-78.85-45.54v-1.11Zm-126.91,1.92l57.7-33.32v69.11l-57.7,33.26v-69.05Z"
/>
<g>
<rect className="logo-path" y="277.99" width="12.06" height="64.23" />
<path
className="logo-path"
d="m88.82,342.36c3.93-.65,7.51-1.73,10.75-3.23,3.24-1.5,6.01-3.46,8.32-5.89l1.31,8.97h7.76v-35.25h-39.45v9.72h27.58v1.03c0,3.12-.94,5.84-2.8,8.18-1.87,2.34-4.81,4.13-8.83,5.38-4.02,1.25-9.3,1.87-15.85,1.87-5.49,0-10.28-.73-14.4-2.2-4.11-1.46-7.32-3.8-9.63-7.01-2.31-3.21-3.46-7.46-3.46-12.76v-2.24c0-3.93.75-7.28,2.24-10.05,1.5-2.77,3.58-5.03,6.26-6.78,2.68-1.74,5.8-3.02,9.35-3.83,3.55-.81,7.39-1.21,11.5-1.21,3.37,0,6.56.2,9.58.61,3.02.41,5.75,1.11,8.18,2.1,2.43,1,4.35,2.38,5.75,4.16,1.4,1.78,2.1,3.97,2.1,6.59h11.78c0-4.05-.89-7.56-2.66-10.52-1.78-2.96-4.33-5.41-7.67-7.34-3.33-1.93-7.32-3.38-11.97-4.35-4.64-.97-9.83-1.45-15.57-1.45-8.6,0-15.99,1.25-22.16,3.74-6.17,2.49-10.89,6.22-14.16,11.17-3.27,4.95-4.91,11.08-4.91,18.37,0,11.16,3.23,19.48,9.68,24.96,6.45,5.49,16.16,8.23,29.12,8.23,4.24,0,8.32-.33,12.25-.98Z"
/>
<path
className="logo-path"
d="m160.8,299.31c1.68,1.81,3.3,3.49,4.86,5.05l37.96,37.86h11.12v-64.23h-11.59v37.02c0,1.31.03,2.9.09,4.77.06,1.87.12,3.43.19,4.68h-.75c-.75-.87-1.67-1.87-2.76-2.99-1.09-1.12-2.15-2.23-3.18-3.32-1.03-1.09-1.92-1.98-2.66-2.66l-37.86-37.49h-11.5v64.23h11.59v-36.37c0-2.31-.02-4.46-.05-6.45-.03-1.99-.08-3.46-.14-4.39h.65c1,1.06,2.34,2.49,4.02,4.3Z"
/>
<path
className="logo-path"
d="m242.6,277.99v35.34c0,6.11,1.26,11.42,3.79,15.94,2.52,4.52,6.36,7.99,11.5,10.42,5.14,2.43,11.64,3.65,19.49,3.65s14.33-1.21,19.45-3.65c5.11-2.43,8.93-5.9,11.45-10.42,2.52-4.52,3.79-9.83,3.79-15.94v-35.34h-12.06v34.97c0,6.48-1.96,11.47-5.89,14.96-3.93,3.49-9.5,5.23-16.73,5.23s-12.89-1.74-16.78-5.23c-3.9-3.49-5.84-8.48-5.84-14.96v-34.97h-12.15Z"
/>
<path
className="logo-path"
d="m404.99,277.99l-17.2,37.02c-.5,1.12-1.11,2.51-1.82,4.16-.72,1.65-1.42,3.32-2.1,5-.69,1.68-1.31,3.18-1.87,4.49h-.56c-.62-1.5-1.3-3.12-2.01-4.86-.72-1.74-1.42-3.44-2.1-5.1-.69-1.65-1.25-2.94-1.68-3.88l-17.2-36.83h-18.51v64.23h11.59v-40.29c0-1.31-.02-2.67-.05-4.07-.03-1.4-.06-2.76-.09-4.07-.03-1.31-.08-2.4-.14-3.27h.75c.31.81.67,1.79,1.07,2.94.4,1.15.89,2.37,1.45,3.65.56,1.28,1.12,2.51,1.68,3.69l19.26,41.42h11.87l19.17-41.42c.43-.94.92-2.02,1.45-3.27.53-1.25,1.04-2.49,1.54-3.74.5-1.25.9-2.34,1.22-3.27h.75c-.06,1-.13,2.18-.19,3.55-.06,1.37-.11,2.74-.14,4.11-.03,1.37-.05,2.62-.05,3.74v40.29h12.15v-64.23h-18.23Z"
/>
</g>
</g>
</svg>
{/* Loading dots */}
<div className="initial-loading-text">
<div className="loading-dots">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
);
}

View File

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

View File

@@ -3,23 +3,67 @@
import React from "react"; import React from "react";
import { useState } 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 { useLocale, useTranslations } from "next-intl";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useMutation } from "@tanstack/react-query";
import { toast } from "react-toastify";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
export function Footer() { export function Footer() {
const locale = useLocale(); const locale = useLocale();
const [errors, setErrors] = useState<any>({});
const t = useTranslations(); const t = useTranslations();
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [subscribed, setSubscribed] = useState(false); const [subscribed, setSubscribed] = useState(false);
const handleSubscribe = (e: React.FormEvent) => { const formRequest = useMutation({
e.preventDefault(); mutationKey: [],
if (email) { mutationFn: (data: any) => httpClient.post(endPoints.post.sendNumber, data),
onSuccess: () => {
toast.success(t("succes"));
setSubscribed(true); setSubscribed(true);
setEmail(""); setEmail("");
setTimeout(() => setSubscribed(false), 3000); setTimeout(() => setSubscribed(false), 3000);
},
onError: (error) => {
console.log("error: ", error);
toast.error(t("error"));
},
});
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) {
// Telefon raqamni tozalash (faqat raqamlar)
const cleanPhone = email.replace(/\D/g, "");
formRequest.mutate({ number: Number(cleanPhone.slice(3)) });
} }
}; };
@@ -31,10 +75,10 @@ export function Footer() {
"linear-gradient(to top right, #452811 0%, #000000 20%, #000000 40%, #000000 60%, #000000 80%, #000000 100%)", "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=" absolute w-full -top-40 px-4 py-12 md:py-16">
<div className="mx-auto max-w-6xl"> <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"> <div className="mb-8 md:mb-0">
<h2 className="font-unbounded text-2xl font-bold text-white md:text-3xl"> <h2 className="font-unbounded text-2xl font-bold text-white md:text-3xl">
{t("contactTitle")} {t("contactTitle")}
@@ -49,12 +93,16 @@ export function Footer() {
className="flex sm:flex-row flex-col w-full gap-2 md:w-auto" className="flex sm:flex-row flex-col w-full gap-2 md:w-auto"
> >
<input <input
type="email" type="tel"
placeholder={t("enterPhone")} id="phone"
name="phone"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={handlePhoneChange}
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" 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 ${
required errors.email ? "border-red-500" : "border-transparent"
}`}
placeholder="+998 90 123 45 67"
maxLength={17}
/> />
<button <button
type="submit" type="submit"
@@ -87,6 +135,20 @@ export function Footer() {
<p className="font-almarai text-sm leading-relaxed text-gray-300"> <p className="font-almarai text-sm leading-relaxed text-gray-300">
{t("footer.description")} {t("footer.description")}
</p> </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> </div>
{/* Quick Links */} {/* Quick Links */}
@@ -162,12 +224,12 @@ export function Footer() {
href="mailto:support@fireforce.com" href="mailto:support@fireforce.com"
className="hover:text-[#fa1d1d]" className="hover:text-[#fa1d1d]"
> >
support@fireforce.com info@ignum-tech.com
</a> </a>
</li> </li>
<li className="flex items-start gap-3"> <li className="flex items-start gap-3">
<MapPin className="mt-1 h-5 w-5 shrink-0 text-white" /> <MapPin className="mt-1 h-5 w-5 shrink-0 text-white" />
<span>Jl. Dr. Ir Soekarno No. 99x Tabanan - Bali</span> <span>{t("footer.address")}</span>
</li> </li>
</ul> </ul>
</div> </div>
@@ -176,20 +238,30 @@ export function Footer() {
</div> </div>
{/* Copyright Section */} {/* 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="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 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">
<div> {locale === "uz" ? (
Copyright © 2025 Ignum Company. <div className="flex gap-2 w-full justify-center items-end ">
</div> <a
<div className="flex gap-6"> href="http://felix-its.uz/"
<a href="#terms" className="hover:text-white"> className="hover:text-red-600 hover:cursor-pointer text-blue-300 underline"
Terms & Conditions >
Felix-its.uz
</a> </a>
<a href="#privacy" className="hover:text-white"> <p>- Jamoasi tomonidan ishlab chiqilgan</p>
Privacy Policy </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> </a>
</div> </div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -5,15 +5,34 @@ import { ChevronDown, Phone, Menu, X } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import LanguageSelectRadix from "../languageSwitcher"; import LanguageSelectRadix from "../languageSwitcher";
import { useLocale, useTranslations } from "next-intl"; 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() { export function Navbar() {
const locale = useLocale(); const locale = useLocale();
const t = useTranslations(); const t = useTranslations();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = 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(() => { useEffect(() => {
const handleScroll = () => { const handleScroll = () => {
setScrolled(window.scrollY > 50); setScrolled(window.scrollY > 50);
@@ -36,89 +55,73 @@ export function Navbar() {
<nav <nav
className={`fixed top-0 left-0 right-0 z-50 border-b border-gray-400/50 ${scrolled && "bg-black"} transition`} 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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-20"> <div className="flex justify-between items-center h-20">
{/* Logo */} {/* Logo */}
<Link href={`/${locale}/home`} className="hover:cursor-pointer"> <Link href={`/${locale}/home`} className="hover:cursor-pointer">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className=" flex items-center justify-center"> <div className=" flex items-center justify-center">
<NavbarLogo/> <Image
src={"/images/IGNUM/PNG/1.@6x.png"}
alt="logo image"
width={80}
height={80}
/>
</div> </div>
</div> </div>
</Link> </Link>
{/* Desktop Navigation Menu */} {/* Desktop Navigation Menu */}
<div className="hidden h-full lg:flex items-center gap-8"> <div className="hidden h-full lg:flex items-center gap-8">
{navbarItems?.results ? (
navbarItems.results.map((item: NavbarItem) => (
<DropdownMenu key={item.id}>
<DropdownMenuTrigger asChild>
<Link
key={item.id}
href={`/${locale}/${item.url}`}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{item.name}
{item.children.length > 0 && (
<ChevronDown size={12} className="ml-1" />
)}
</Link>
</DropdownMenuTrigger>
{item.children.length > 0 && (
<DropdownMenuContent className="space-y-2">
{item.children.map((child: NavbarItem) => (
<DropdownMenuItem asChild key={child.id}>
<Link
href={`/${locale}/${child.url}`}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{child.name}
</Link>
</DropdownMenuItem>
))}
</DropdownMenuContent>
)}
</DropdownMenu>
))
) : (
<Link <Link
href={`/${locale}/home`} 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" 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")} {t("navbar.home")}
</Link> </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>
{/* Pages Dropdown */}
<div className="relative group h-full">
<button
className="font-unbounded uppercase text-white text-sm h-full font-semibold hover:text-red-500
transition-colors flex items-center gap-1"
>
{t("navbar.pages")}
<ChevronDown
size={16}
className="transition-transform group-hover:rotate-180"
/>
</button>
{/* Dropdown Menu */}
<div
className="absolute top-full left-0 w-40 bg-white rounded-b-md shadow-lg
font-semibold opacity-0 invisible group-hover:opacity-100
group-hover:visible transition-all duration-300
transform translate-y-2 group-hover:translate-y-0
pointer-events-none group-hover:pointer-events-auto overflow-hidden"
>
<Link
href={`/${locale}/faq`}
className="font-unbounded uppercase block px-4 py-2 text-black text-sm hover:bg-red-600
hover:text-white transition-colors"
>
{t("navbar.faq")}
</Link>
<Link
href={`/${locale}/services`}
className="font-unbounded uppercase block px-4 py-2 text-black text-sm hover:bg-red-600
hover:text-white transition-colors"
>
{t("navbar.services")}
</Link>
{/* <Link
href={`/${locale}/blog`}
className="font-unbounded uppercase block px-4 py-2 text-black text-sm hover:bg-red-600
hover:text-white transition-colors rounded-b-md"
>
Blog
</Link> */}
</div>
</div>
<Link
href={`/${locale}/products`}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{t("navbar.products")}
</Link>
<Link
href={`/${locale}/contact`}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{t("navbar.contact")}
</Link>
</div> </div>
<div className="flex items-center gap-5"> <div className="flex items-center gap-5">
@@ -135,7 +138,8 @@ export function Navbar() {
</span> </span>
<div> <div>
<div className="text-white text-sm font-bold"> <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> </div>
</div> </div>
@@ -194,68 +198,45 @@ export function Navbar() {
{/* Mobile Menu Links */} {/* Mobile Menu Links */}
<div className="flex flex-col p-6 gap-4"> <div className="flex flex-col p-6 gap-4">
{navbarItems?.results ? (
navbarItems.results.map((item: NavbarItem) => (
<DropdownMenu key={item.id}>
<DropdownMenuTrigger asChild>
<Link
key={item.id}
href={`/${locale}/${item.url}`}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{item.name}
{item.children.length > 0 && (
<ChevronDown size={12} className="ml-1" />
)}
</Link>
</DropdownMenuTrigger>
{item.children.length > 0 && (
<DropdownMenuContent className="space-y-2">
{item.children.map((child: NavbarItem) => (
<Link
key={child.id}
href={`/${locale}/${child.url}`}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
{child.name}
</Link>
))}
</DropdownMenuContent>
)}
</DropdownMenu>
))
) : (
<Link <Link
href={`/${locale}/home`} href={`/${locale}/home`}
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2" className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
onClick={() => setIsMobileMenuOpen(false)}
> >
{t("navbar.home")} {t("navbar.home")}
</Link> </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 */}
<div>
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition flex items-center gap-1 py-2 w-full"
>
{t("navbar.pages")}
<ChevronDown
size={16}
className={`transition-transform ${isDropdownOpen ? "rotate-180" : ""}`}
/>
</button>
{isDropdownOpen && (
<div className="ml-4 mt-2 flex flex-col gap-2">
<Link
href={`/${locale}/faq`}
className="font-unbounded uppercase text-white/80 text-sm hover:text-red-500 transition py-2"
onClick={() => setIsMobileMenuOpen(false)}
>
{t("navbar.faq")}
</Link>
<Link
href={`/${locale}/services`}
className="font-unbounded uppercase text-white/80 text-sm hover:text-red-500 transition py-2"
onClick={() => setIsMobileMenuOpen(false)}
>
{t("navbar.services")}
</Link>
</div>
)} )}
</div> </div>
<Link
href={`/${locale}/products`}
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
onClick={() => setIsMobileMenuOpen(false)}
>
{t("navbar.products")}
</Link>
<Link
href={`/${locale}/contact`}
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
onClick={() => setIsMobileMenuOpen(false)}
>
{t("navbar.contact")}
</Link>
</div>
</div> </div>
</> </>
); );

View File

@@ -2,14 +2,14 @@ import "./logo.css";
export default function NavbarLogo() { export default function NavbarLogo() {
return ( return (
<div className="relative w-24 h-12"> <div className="relative w-24 h-20">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 423.22 424.82" viewBox="0 0 423.22 424.82"
className="w-full h-full" className="w-full h-full"
> >
<defs> <defs>
{/* Qizil neon gradient */} {/* Qizil neon gradient for github*/}
<linearGradient id="neon-gradient" x1="0%" y1="100%" x2="100%" y2="0%"> <linearGradient id="neon-gradient" x1="0%" y1="100%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#979797" /> <stop offset="0%" stopColor="#979797" />
<stop offset="50%" stopColor="#e4e4e4" /> <stop offset="50%" stopColor="#e4e4e4" />

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

@@ -0,0 +1,41 @@
// components/CatalogCardSkeleton.tsx
export default function CatalogCardSkeleton() {
return (
<div className="relative h-112.5 w-full overflow-hidden rounded-2xl bg-[#17161679] border border-white/10 animate-pulse">
{/* Content container */}
<div className="relative h-full flex flex-col p-6">
{/* Title section */}
<div className="mb-4">
<div className="flex items-start justify-between mb-2">
{/* Title skeleton */}
<div className="flex-1 space-y-2">
<div className="h-7 bg-white/10 rounded-md w-3/4" />
<div className="h-7 bg-white/10 rounded-md w-1/2" />
</div>
{/* Icon skeleton */}
<div className="shrink-0 w-8 h-8 rounded-full bg-white/10" />
</div>
{/* Description skeleton */}
<div className="space-y-2 mt-3">
<div className="h-4 bg-white/10 rounded w-full" />
<div className="h-4 bg-white/10 rounded w-4/5" />
</div>
</div>
{/* Image container skeleton */}
<div className="relative flex-1 rounded-xl overflow-hidden bg-linear-to-br from-[#444242] to-gray-900/50 border border-white/5">
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-32 h-32 bg-white/5 rounded-lg" />
</div>
</div>
{/* Bottom accent bar skeleton */}
<div className="mt-4 h-1 w-1/3 bg-white/10 rounded-full" />
</div>
{/* Shimmer effect */}
<div className="absolute inset-0 -translate-x-full animate-shimmer bg-linear-to-r from-transparent via-white/5 to-transparent" />
</div>
);
}

View File

@@ -3,19 +3,35 @@ import { usePathname } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import "./page-transition.css"; import "./page-transition.css";
export default function PageTransition({ children }: { children: React.ReactNode }) { export default function PageTransition({
children
}: {
children: React.ReactNode
}) {
const pathname = usePathname(); const pathname = usePathname();
const [displayPath, setDisplayPath] = useState(pathname);
const [isTransitioning, setIsTransitioning] = useState(false); const [isTransitioning, setIsTransitioning] = useState(false);
useEffect(() => { useEffect(() => {
if (pathname !== displayPath) {
// Start exit animation
setIsTransitioning(true); setIsTransitioning(true);
const timer = setTimeout(() => { const exitTimer = setTimeout(() => {
setIsTransitioning(false); // Update path (triggers content change)
}, 1500); // Animatsiya davomiyligi setDisplayPath(pathname);
return () => clearTimeout(timer); // Start enter animation
}, [pathname]); const enterTimer = setTimeout(() => {
setIsTransitioning(false);
}, 800); // Enter animation duration
return () => clearTimeout(enterTimer);
}, 800); // Exit animation duration
return () => clearTimeout(exitTimer);
}
}, [pathname, displayPath]);
return ( return (
<> <>
@@ -32,7 +48,6 @@ export default function PageTransition({ children }: { children: React.ReactNode
<stop offset="50%" stopColor="#ff4444" /> <stop offset="50%" stopColor="#ff4444" />
<stop offset="100%" stopColor="#ff0000" /> <stop offset="100%" stopColor="#ff0000" />
</linearGradient> </linearGradient>
<filter id="transition-glow"> <filter id="transition-glow">
<feGaussianBlur stdDeviation="5" result="coloredBlur"/> <feGaussianBlur stdDeviation="5" result="coloredBlur"/>
<feMerge> <feMerge>

View File

@@ -1,10 +1,11 @@
import DotAnimatsiya from "@/components/dot/DotAnimatsiya"; import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { InnerNavbar } from "./innerNavbar";
export function AboutBanner() { export function AboutBanner() {
const t = useTranslations(); const t = useTranslations();
return ( 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 */} {/* Background Image */}
<div <div
className="absolute inset-0 z-0" 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="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="spacw-y-4 ">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<DotAnimatsiya /> <DotAnimatsiya />
@@ -39,7 +43,7 @@ export function AboutBanner() {
{t("about.banner.subtitle")} {t("about.banner.subtitle")}
</p> </p>
</div> </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")} {t("about.banner.description")}
</div> </div>
</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 { Story } from "./story";
export { AboutLine } from "./aboutLine"; export { AboutLine } from "./aboutLine";
export { WhyChooseUs } from "./whyChooseUs"; 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 ( return (
<div className="pb-0 relative z-10 max-[350px]:pb-30 "> <div className="pb-0 relative z-10 max-[350px]:pb-30 ">
<div className="max-w-260 mx-auto px-4"> <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 */} {/* Background Image */}
<div <div
className="absolute inset-0 z-0 rounded-xl" className="absolute inset-0 z-0 rounded-xl"

View File

@@ -1,12 +1,12 @@
"use client";
import Image from "next/image"; import Image from "next/image";
import { Check } from "lucide-react"; import { Check } from "lucide-react";
import DotAnimatsiya from "@/components/dot/DotAnimatsiya"; 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() { export function WhyChooseUs() {
const t = useTranslations(); const t = useTranslations();
const locale = useLocale();
const features = [ const features = [
{ title: t("about.whyChoose.features.fastResponse") }, { title: t("about.whyChoose.features.fastResponse") },
{ title: t("about.whyChoose.features.ready24") }, { title: t("about.whyChoose.features.ready24") },
@@ -56,9 +56,12 @@ export function WhyChooseUs() {
{/* CTA Button */} {/* CTA Button */}
<div> <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")} {t("about.whyChoose.contact")}
</button> </Link>
</div> </div>
</div> </div>

View File

@@ -6,12 +6,7 @@ export default function ContactHeader() {
const t = useTranslations(); const t = useTranslations();
return ( return (
<div className="mb-8 text-center"> <div className="mb-8 text-center">
<div className="mb-4 flex items-center justify-center gap-2">
<DotAnimatsiya />
<span className="font-almarai text-sm font-semibold tracking-wider text-white">
{t("contact.banner.title")}
</span>
</div>
<h2 <h2
className="uppercase font-unbounded bg-linear-to-br from-white via-white to-black className="uppercase font-unbounded bg-linear-to-br from-white via-white to-black
text-transparent bg-clip-text text-4xl font-bold tracking-wide md:text-5xl" text-transparent bg-clip-text text-4xl font-bold tracking-wide md:text-5xl"

View File

@@ -3,21 +3,26 @@
import { Check } from "lucide-react"; import { Check } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import axios from "axios";
import { toast } from "react-toastify";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
interface FormData { interface FormData {
firstName: string; name: string;
lastName: string; surname: string;
email: string; address: string;
subject: string; theme: string;
message: string; message: string;
agreeToPolicy: boolean; agreeToPolicy: boolean;
} }
interface FormErrors { interface FormErrors {
firstName?: string; name?: string;
lastName?: string; surname?: string;
email?: string; address?: string;
subject?: string; theme?: string;
message?: string; message?: string;
agreeToPolicy?: string; agreeToPolicy?: string;
} }
@@ -25,10 +30,10 @@ interface FormErrors {
export default function Form() { export default function Form() {
const t = useTranslations(); const t = useTranslations();
const [formData, setFormData] = useState<FormData>({ const [formData, setFormData] = useState<FormData>({
firstName: "", name: "",
lastName: "", surname: "",
email: "", address: "",
subject: "", theme: "",
message: "", message: "",
agreeToPolicy: false, agreeToPolicy: false,
}); });
@@ -38,22 +43,52 @@ export default function Form() {
"idle" | "success" | "error" "idle" | "success" | "error"
>("idle"); >("idle");
const formRequest = useMutation({
mutationKey: [],
mutationFn: (data: FormData) =>
httpClient.post(endPoints.post.contact, data),
onSuccess: () => {
setSubmitStatus("success");
setFormData({
name: "",
surname: "",
address: "",
theme: "",
message: "",
agreeToPolicy: false,
});
setIsSubmitting(false);
toast.success(t("succes"));
setTimeout(() => setSubmitStatus("idle"), 3000);
},
onError: (error) => {
console.log("error: ", error);
setIsSubmitting(false);
setSubmitStatus("error");
toast.error(t("error"));
setTimeout(() => setSubmitStatus("idle"), 3000);
},
});
const validateForm = (): boolean => { const validateForm = (): boolean => {
const newErrors: FormErrors = {}; const newErrors: FormErrors = {};
if (!formData.firstName.trim()) { if (!formData.name.trim()) {
newErrors.firstName = "First name is required"; newErrors.name = "First name is required";
} }
if (!formData.lastName.trim()) { if (!formData.surname.trim()) {
newErrors.lastName = "Last name is required"; newErrors.surname = "Last name is required";
} }
if (!formData.email.trim()) { const phoneNumbers = formData.address.replace(/\D/g, "");
newErrors.email = "Email is required"; if (phoneNumbers.length !== 12) {
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { newErrors.address =
newErrors.email = "Please enter a valid email"; 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.subject.trim()) { if (!formData.theme.trim()) {
newErrors.subject = "Subject is required"; newErrors.theme = "theme is required";
} }
if (!formData.message.trim()) { if (!formData.message.trim()) {
newErrors.message = "Message is required"; newErrors.message = "Message is required";
@@ -66,6 +101,30 @@ export default function Form() {
return Object.keys(newErrors).length === 0; 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 = ( const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>, e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => { ) => {
@@ -90,32 +149,7 @@ export default function Form() {
setIsSubmitting(true); setIsSubmitting(true);
setSubmitStatus("idle"); setSubmitStatus("idle");
formRequest.mutate(formData);
try {
const response = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData),
});
if (response.ok) {
setSubmitStatus("success");
setFormData({
firstName: "",
lastName: "",
email: "",
subject: "",
message: "",
agreeToPolicy: false,
});
} else {
setSubmitStatus("error");
}
} catch {
setSubmitStatus("error");
} finally {
setIsSubmitting(false);
}
}; };
return ( return (
@@ -125,66 +159,74 @@ export default function Form() {
<div> <div>
<input <input
type="text" type="text"
name="firstName" name="name"
placeholder={t("contact.form.placeholders.firstName")} placeholder={t("contact.form.placeholders.firstName")}
value={formData.firstName} value={formData.name}
onChange={handleChange} onChange={handleChange}
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm 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 ${ text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
errors.firstName ? "border-red-500" : "border-transparent" errors.name ? "border-red-500" : "border-transparent"
}`} }`}
/> />
{errors.firstName && ( {errors.name && (
<p className="font-almarai mt-1 text-xs text-red-500">{errors.firstName}</p> <p className="font-almarai mt-1 text-xs text-red-500">
{errors.name}
</p>
)} )}
</div> </div>
<div> <div>
<input <input
type="text" type="text"
name="lastName" name="surname"
placeholder={t("contact.form.placeholders.lastName")} placeholder={t("contact.form.placeholders.lastName")}
value={formData.lastName} value={formData.surname}
onChange={handleChange} onChange={handleChange}
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${ 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.lastName ? "border-red-500" : "border-transparent" errors.surname ? "border-red-500" : "border-transparent"
}`} }`}
/> />
{errors.lastName && ( {errors.surname && (
<p className="font-almarai mt-1 text-xs text-red-500">{errors.lastName}</p> <p className="font-almarai mt-1 text-xs text-red-500">
{errors.surname}
</p>
)} )}
</div> </div>
</div> </div>
{/* Second Row - Email & Subject */} {/* Second Row - address & theme */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div> <div>
<input <input
type="email" type="tel"
name="email" id="phone"
placeholder={t("contact.form.placeholders.email")} name="phone"
value={formData.email} 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 ${ 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" errors.address ? "border-red-500" : "border-transparent"
}`} }`}
placeholder="+998 90 123 45 67"
maxLength={17}
/> />
{errors.email && ( {errors.address && (
<p className="font-almarai mt-1 text-xs text-red-500">{errors.email}</p> <p className="mt-1 text-sm text-red-500">{errors.address}</p>
)} )}
</div> </div>
<div> <div>
<input <input
type="text" type="text"
name="subject" name="theme"
placeholder={t("contact.form.placeholders.subject")} placeholder={t("contact.form.placeholders.subject")}
value={formData.subject} value={formData.theme}
onChange={handleChange} onChange={handleChange}
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${ 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.subject ? "border-red-500" : "border-transparent" errors.theme ? "border-red-500" : "border-transparent"
}`} }`}
/> />
{errors.subject && ( {errors.theme && (
<p className="font-almarai mt-1 text-xs text-red-500">{errors.subject}</p> <p className="font-almarai mt-1 text-xs text-red-500">
{errors.theme}
</p>
)} )}
</div> </div>
</div> </div>
@@ -202,7 +244,9 @@ export default function Form() {
}`} }`}
/> />
{errors.message && ( {errors.message && (
<p className="font-almarai mt-1 text-xs text-red-500">{errors.message}</p> <p className="font-almarai mt-1 text-xs text-red-500">
{errors.message}
</p>
)} )}
</div> </div>
@@ -236,7 +280,9 @@ export default function Form() {
</button> </button>
</div> </div>
{errors.agreeToPolicy && ( {errors.agreeToPolicy && (
<p className="font-almarai text-xs text-red-500">{errors.agreeToPolicy}</p> <p className="font-almarai text-xs text-red-500">
{errors.agreeToPolicy}
</p>
)} )}
{/* Status Messages */} {/* Status Messages */}

View File

@@ -1,8 +1,10 @@
import Image from "next/image"; import Image from "next/image";
import { Mail, MapPin, Phone, Check } from "lucide-react"; import { Mail, MapPin, Phone } from "lucide-react";
import ContactHeader from "./contactHeader"; import ContactHeader from "./contactHeader";
import Form from "./form"; import Form from "./form";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import Maps from "./maps";
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
export function Contact() { export function Contact() {
const t = useTranslations(); const t = useTranslations();
@@ -25,7 +27,7 @@ export function Contact() {
]; ];
return ( return (
<section className="relative min-h-175 w-full py-16 md:py-40"> <section className="relative min-h-175 w-full py-30 md:py-40">
{/* Background Image */} {/* Background Image */}
<div className="absolute inset-0 z-0"> <div className="absolute inset-0 z-0">
<Image <Image
@@ -40,13 +42,23 @@ export function Contact() {
className="absolute inset-0" className="absolute inset-0"
style={{ style={{
background: 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> </div>
{/* Content */} {/* Content */}
<div className="relative z-10 mx-auto max-w-4xl px-4 sm:px-6 lg:px-8"> <div className="relative z-10 ">
<div className="flex items-center justify-center w-full">
<div className="mb-4 flex items-center justify-center gap-2">
<DotAnimatsiya />
<span className="font-almarai text-sm font-semibold tracking-wider text-white">
{t("contact.banner.title")}
</span>
</div>
</div>
<Maps />
<div className="relative z-10 mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 space-y-5">
<ContactHeader /> <ContactHeader />
<Form /> <Form />
@@ -64,11 +76,14 @@ export function Contact() {
<h3 className="font-almarai text-sm font-bold tracking-wider text-white"> <h3 className="font-almarai text-sm font-bold tracking-wider text-white">
{info.title} {info.title}
</h3> </h3>
<p className="font-almarai mt-1 text-sm text-gray-400">{info.detail}</p> <p className="font-almarai mt-1 text-sm text-gray-400">
{info.detail}
</p>
</div> </div>
))} ))}
</div> </div>
</div> </div>
</div>
</section> </section>
); );
} }

View File

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

View File

@@ -1,37 +1,53 @@
"use client";
import Link from "next/link"; import Link from "next/link";
import FAQAccordion from "./faqAccardion"; import FAQAccordion, { FAQItem } from "./faqAccardion";
import { faqItems } from "@/lib/demoData";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useQuery } from "@tanstack/react-query";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useEffect, useState } from "react";
export function Togle() { export function Togle() {
const t = useTranslations(); const t = useTranslations();
const faqItems = [ const faqItems: FAQItem[] = [
{ {
id: "faq-1", id: 1,
question: t("faq.question1.question"), question: t("faq.question1.question"),
answer: t("faq.question1.answer"), answer: t("faq.question1.answer"),
}, },
{ {
id: "faq-2", id: 2,
question: t("faq.question2.question"), question: t("faq.question2.question"),
answer: t("faq.question2.answer"), answer: t("faq.question2.answer"),
}, },
{ {
id: "faq-3", id: 3,
question: t("faq.question3.question"), question: t("faq.question3.question"),
answer: t("faq.question3.answer"), answer: t("faq.question3.answer"),
}, },
{ {
id: "faq-4", id: 4,
question: t("faq.question4.question"), question: t("faq.question4.question"),
answer: t("faq.question4.answer"), answer: t("faq.question4.answer"),
}, },
{ {
id: "faq-5", id: 5,
question: t("faq.question5.question"), question: t("faq.question5.question"),
answer: t("faq.question5.answer"), answer: t("faq.question5.answer"),
}, },
]; ];
const [faq, setFaq] = useState<any>(faqItems);
const { data } = useQuery({
queryKey: ["faq"],
queryFn: () => httpClient(endPoints.faq),
select: (data) => data?.data?.results,
});
useEffect(() => {
data && setFaq(data);
}, [data]);
return ( return (
<div className="min-h-screen bg-[#1e1d1c]"> <div className="min-h-screen bg-[#1e1d1c]">
<main className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8"> <main className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
@@ -48,7 +64,7 @@ export function Togle() {
{/* FAQ Section */} {/* FAQ Section */}
<div className="max-w-250 w-full"> <div className="max-w-250 w-full">
<FAQAccordion items={faqItems} /> <FAQAccordion items={faq} />
</div> </div>
</div> </div>
{/* ASK QUESTION */} {/* ASK QUESTION */}

View File

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

View File

@@ -1,12 +1,11 @@
"use client"; "use client";
import React from "react"; import React from "react";
import { Button } from "@/components/ui/button";
import Image from "next/image"; import Image from "next/image";
import { Flame, Building2, Ambulance } from "lucide-react"; import { Flame, Building2, Ambulance } from "lucide-react";
import DotAnimatsiya from "@/components/dot/DotAnimatsiya"; import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import { useTranslations } from "next-intl"; import { useLocale, useTranslations } from "next-intl";
import Link from "next/link";
interface ServiceItem { interface ServiceItem {
icon: React.ReactNode; icon: React.ReactNode;
@@ -16,6 +15,7 @@ interface ServiceItem {
export function AboutUs() { export function AboutUs() {
const t = useTranslations(); const t = useTranslations();
const locale = useLocale();
const services: ServiceItem[] = [ const services: ServiceItem[] = [
{ {
icon: <Flame width={40} height={40} className="text-red-500" />, icon: <Flame width={40} height={40} className="text-red-500" />,
@@ -76,9 +76,9 @@ export function AboutUs() {
{/* Button */} {/* Button */}
<div> <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")} {t("home.about.title")}
</Button> </Link>
</div> </div>
</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/homeBanner.png"
alt="Firefighters"
loading="lazy"
className="lg:w-150 w-100 lg:h-150 max-[300px]:w-[80vw] object-cover 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

@@ -1,111 +0,0 @@
import Image from "next/image";
import { ChevronRight } from "lucide-react";
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import { useTranslations } from "next-intl";
import ProductCard from "../products/productCard";
export function Blog() {
const t = useTranslations();
const blogPosts = [
{
id: 1,
image: "/images/img14.webp",
category: "Tips & Trick",
title: t("home.blog.articles.article1"),
author: "John Doe",
date: "July 24, 2025",
},
{
id: 2,
image: "/images/img15.webp",
category: "Insight",
title: t("home.blog.articles.article2"),
author: "John Doe",
date: "July 24, 2025",
},
{
id: 3,
image: "/images/img16.webp",
category: "News",
title: t("home.blog.articles.article3"),
author: "John Doe",
date: "July 24, 2025",
},
];
return (
<section className="bg-[#1f1f1f] py-45">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="mb-12 text-center">
<div className="mb-4 flex items-center justify-center gap-2">
<DotAnimatsiya />
<span className="font-almarai text-sm font-semibold tracking-wider text-white uppercase">
{t("products.banner.title")}
</span>
</div>
<h2
className="font-unbounded bg-linear-to-br from-white py-2 via-white to-black
text-transparent bg-clip-text text-4xl font-bold tracking-tight md:text-5xl lg:text-6xl"
>
{t("products.ourproducts")}
</h2>
</div>
{/* Blog Cards Grid */}
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3 max-sm:place-items-center">
{/* {blogPosts.map((post) => (
<article key={post.id} className="group">
<div className="relative mb-6 aspect-4/2 md:aspect-4/3 overflow-hidden rounded-lg">
<Image
src={post.image || "/placeholder.svg"}
alt={post.title}
fill
className="object-cover transition-transform duration-300 group-hover:scale-105"
/>
<div className="absolute bottom-4 left-4">
<span className="font-almarai rounded bg-red-600 px-4 py-2 text-sm font-medium text-white">
{post.category}
</span>
</div>
</div>
<div>
<h3 className="font-unbounded uppercase mb-3 text-lg font-bold leading-tight tracking-wide text-white md:text-xl">
{post.title}
</h3>
<p className="font-almarai mb-4 text-sm text-gray-400">
<span className="text-gray-500">by </span>
<span className="text-white">{post.author}</span>
<span className="mx-2 text-gray-500">•</span>
<span className="text-gray-400">{post.date}</span>
</p>
<a
href="#"
className="font-almarai inline-flex items-center gap-1 text-sm font-semibold tracking-wider text-red-600 uppercase transition-colors hover:text-red-500"
>
{t("home.blog.readMore")}
<ChevronRight className="h-4 w-4" />
</a>
</div>
</article>
))} */}
{Array(3)
.fill(null)
.map((_, index) => (
<ProductCard
key={index}
title="Elektr yong'in detektori-Ypres ver.2"
name="P-0834404"
image="/images/products/products.webp"
slug="P_0834404"
status="full"
/>
))}
</div>
</div>
</section>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -4,7 +4,7 @@ import { useTranslations } from "next-intl";
export function ProductBanner() { export function ProductBanner() {
const t = useTranslations(); const t = useTranslations();
return ( return (
<section className="relative w-full h-[60vh] min-h-100 overflow-hidden pt-10"> <section className="relative w-full min-[400px]:h-[60vh] h-[75vh] min-h-100 overflow-hidden pt-10">
{/* Background Image */} {/* Background Image */}
<div <div
className="absolute inset-0 z-0" className="absolute inset-0 z-0"

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,70 +0,0 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
interface ProductCardProps {
title: string;
name: string;
image: string;
slug: string;
status: "full" | "empty" | "withOrder";
}
export default function ProductCard({
title,
name,
image,
slug,
status,
}: ProductCardProps) {
const statusColor =
status === "full"
? "text-green-500"
: status === "empty"
? "text-red-600"
: "text-yellow-800";
const statusText =
status === "full"
? "Sotuvda mavjud"
: status === "empty"
? "Sotuvda qolmagan"
: "Buyurtma asosida";
return (
<Link href={`/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 ">
{/* Image Container */}
<div className="relative rounded-2xl h-45 sm:h-55 md:h-65 lg:w-[95%] w-[90%] mx-auto overflow-hidden bg-white">
<Image
src={image || "/placeholder.svg"}
alt={title}
fill
className="object-contain transition-transform duration-300"
/>
</div>
{/* Content Container */}
<div className="p-6 sm:p-4">
{/* Title */}
<h3 className="text-lg sm:text-xl md:text-2xl font-bold text-white mb-4 line-clamp-3 group-hover:text-red-400 transition-colors duration-300">
{title}
</h3>
{/* Meta Information */}
<div className="flex flex-col items-start gap-0 text-gray-400 text-sm sm:text-base mb-6">
<span className="font-medium">{name}</span>
<span className={`font-medium ${statusColor}`}>{statusText}</span>
</div>
{/* Read More Link */}
<span className="inline-flex items-center gap-2 text-red-600 font-bold text-base sm:text-lg uppercase tracking-wide hover:gap-4 transition-all duration-300 group/link">
Read More
<ArrowRight className="w-5 h-5 transition-transform duration-300 group-hover/link:translate-x-1" />
</span>
</div>
</article>
</Link>
);
}

View File

@@ -1,24 +0,0 @@
import ProductCard from "./productCard";
export function Products() {
return (
<div className="bg-[#1e1d1c] py-20">
<div className="max-w-250 mx-auto w-full sm:-mt-50 -mt-30 z-20 relative">
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
{Array(9)
.fill(null)
.map((_, index) => (
<ProductCard
key={index}
title="Elektr yong'in detektori-Ypres ver.2"
name="P-0834404"
image="/images/products/products.webp"
slug="P_0834404"
status="full"
/>
))}
</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,10 +1,22 @@
import { useTranslations } from "next-intl";
export function Features({ features }: { features: string[] }) { export function Features({ features }: { features: string[] }) {
const t = useTranslations();
if (!features || features.length === 0) {
return null;
}
return ( return (
<table className="w-full rounded-xl overflow-hidden"> <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> <thead>
<tr className="border-b border-gray-700 bg-black"> <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"> <th className="px-4 py-4 md:px-6 text-left text-sm md:text-base font-semibold text-white">
Feature {t("products.feature")}
</th> </th>
</tr> </tr>
</thead> </thead>
@@ -12,16 +24,21 @@ export function Features({ features }: { features: string[] }) {
{features.map((feature, index) => ( {features.map((feature, index) => (
<tr <tr
key={index} key={index}
className={` border-gray-700 transition-colors hover:bg-opacity-80 ${ className={`border-b border-gray-800 last:border-b-0 transition-colors hover:bg-red-900/10 ${
index % 2 === 0 ? "bg-[#323232]" : "bg-black/20" index % 2 === 0 ? "bg-[#252525]" : "bg-[#1e1e1e]"
}`} }`}
> >
<td className="px-4 py-4 md:px-6 text-sm md:text-base text-white font-medium"> <td className="px-4 py-4 md:px-6 text-sm md:text-base text-gray-300">
{feature} <div className="flex items-start gap-3">
<span className="text-red-700 mt-1"></span>
<span>{feature}</span>
</div>
</td> </td>
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </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,110 +1,152 @@
"use client";
import { Facebook } from "lucide-react"; import { usePriceModalStore } from "@/zustand/useProceModalStore";
import { Check, Instagram, Send, Share2 } from "lucide-react";
const socialLinks = [ import { useTranslations } from "next-intl";
{ name: "telegram", icon: "✈️", color: "#0088cc" }, import { useParams } from "next/navigation";
{ name: "facebook", icon: <Facebook />, color: "#1877F2" }, import { useState } from "react";
{ name: "odnoklassniki", icon: "ok", color: "#ED7100" },
{ name: "vkontakte", icon: "VK", color: "#0077FF" },
{ name: "twitter", icon: "𝕏", color: "#1DA1F2" },
{ name: "whatsapp", icon: "W", color: "#25D366" },
];
interface RightSideProps { interface RightSideProps {
id: number;
title: string; title: string;
name: string; articular: string;
status: string;
description: string; description: string;
statusText: string; price: string;
statusColor: string; image: string;
} }
export function RightSide({ export function RightSide({
title, title,
name, articular,
status,
description, description,
statusColor, price,
statusText, id,
image,
}: RightSideProps) { }: RightSideProps) {
const openModal = usePriceModalStore((state) => state.openModal);
const t = useTranslations();
const params = useParams();
const locale = params.locale || "uz";
const [copied, setCopied] = useState(false);
const handleShare = async () => {
const productUrl = `${window.location.origin}/${locale}/catalog_page/products/special_product?productId=${id}`;
try {
// Modern Web Share API dan foydalanish (mobil qurilmalar uchun)
if (navigator.share) {
await navigator.share({
title: title,
text: `${title} - ${description.slice(0, 100)}...`,
url: productUrl,
});
} else {
// Desktop uchun clipboard ga copy qilish
await navigator.clipboard.writeText(productUrl);
setCopied(true);
// 2 soniyadan keyin "Copied" holatini o'chirish
setTimeout(() => {
setCopied(false);
}, 2000);
}
} catch (error) {
console.error("Share error:", error);
// Fallback - clipboard ga copy qilish
try {
await navigator.clipboard.writeText(productUrl);
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 2000);
} catch (clipboardError) {
console.error("Clipboard error:", clipboardError);
}
}
};
const handleGetPrice = () => {
openModal({
id,
name: title,
image,
inStock: status === "Sotuvda mavjud",
});
};
// Status color logic
const isInStock = status === "Sotuvda mavjud";
const statusColor = isInStock
? "bg-green-600/20 text-green-400 border border-green-600/30"
: "bg-red-600/20 text-red-400 border border-red-600/30";
return ( return (
<div className="flex flex-col justify-center"> <div className="flex flex-col justify-center space-y-6">
{/* Title */} {/* 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} {title}
</h1> </h1>
{/* Article ID */} {/* Article ID */}
<div className="mb-3"> <div className="flex items-center gap-2 text-sm md:text-base">
<p className="text-gray-400"> <span className="text-gray-400">Artikul:</span>
Artikul: <span className="text-white font-semibold">{articular}</span>
<span className="text-white font-semibold">{name}</span>
</p>
</div> </div>
{/* Status Badge */} {/* Status Badge */}
<div className="mb-2"> <div>
<span <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> </span>
</div> </div>
{/* description */} {/* Description */}
<div className="mb-2"> <div className="border-l-4 border-red-700 pl-4">
<p className="text-sm font-bold text-white mb-4 leading-tight"> <p className="text-sm md:text-base text-gray-300 leading-relaxed">
{description} {description}
</p> </p>
</div> </div>
{/* Price Section */} {/* Price Section */}
<div className="mb-8"> <div className="bg-[#1716169f] rounded-xl p-5 space-y-6">
<h2 className="text-2xl md:text-3xl font-bold text-red-700 mb-6"> {/* Action Button */}
17.00$
</h2>
{/* 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> */}
<button <button
onClick={() => {}} 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" className="w-full bg-red-700 hover:bg-red-800 text-white font-bold py-4 px-6 rounded-lg transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-red-700/50"
> >
Xabar yuborish {t("products.send")}
</button> </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 */} {/* Social Share */}
<div className="flex gap-3 items-center"> <div className="pt-4 border-t border-gray-800 flex items-center gap-5">
<div className="flex gap-2"> <button
{socialLinks.map((social) => ( onClick={handleShare}
<a className="flex items-center gap-3 mb-3 text-gray-400 hover:text-white transition-colors group"
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} {copied ? (
<>
<Check className="w-5 h-5 text-green-400" />
<span className="text-sm text-green-400">
{t("products.copied") || "Link nusxalandi!"}
</span>
</>
) : (
<>
<Share2 className="w-5 h-5 group-hover:scale-110 transition-transform" />
<span className="text-sm">{t("products.share")}:</span>
</>
)}
</button>
<a
href="https://t.me/ignum_tech"
className="p-2 rounded-md bg-white text-red-500 hover:text-white hover:bg-red-500"
>
<Send />
</a> </a>
))}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,60 +1,158 @@
"use client"; "use client";
import { Swiper, SwiperSlide } from "swiper/react"; 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";
import "swiper/css/navigation"; 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 navigationPrevEl = ".custom-swiper-prev";
const navigationNextEl = ".custom-swiper-next"; const navigationNextEl = ".custom-swiper-next";
export function SliderComp({ imgs }: { imgs: string[] }) { 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 ( return (
<div> <div className="w-full h-96 md:h-125 bg-gray-800 rounded-lg flex items-center justify-center">
<div className="flex items-center justify-center relative"> <div className="text-center">
<svg
className="w-20 h-20 text-gray-600 mx-auto mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
<p className="text-gray-500">{t("image_not_found")}</p>
</div>
</div>
);
}
return (
<div className="space-y-4">
{/* Main Slider */}
<div className="relative group">
<Swiper <Swiper
modules={[Navigation]} modules={[Navigation, Pagination, Thumbs]}
thumbs={{
swiper:
thumbsSwiper && !thumbsSwiper.destroyed ? thumbsSwiper : null,
}}
navigation={{ navigation={{
// Pass the class selectors here
prevEl: navigationPrevEl, prevEl: navigationPrevEl,
nextEl: navigationNextEl, nextEl: navigationNextEl,
}} }}
pagination={{ clickable: true }} 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) => ( {imgs.map((image, index) => (
<SwiperSlide <SwiperSlide
key={index} key={index}
className="bg-white flex items-center justify-center" className=" flex items-center justify-center"
> >
<img <div className="relative w-full h-full p-4 md:p-8">
src={image || "/placeholder.svg"} <Image
alt={`${DATA[0].title} - ${index + 1}`} src={image}
className="w-full h-full object-contain p-4" alt={`Product image ${index + 1}`}
fill
className="object-contain"
sizes="(max-width: 768px) 100vw, 50vw"
priority={index === 0}
/> />
</div>
</SwiperSlide> </SwiperSlide>
))} ))}
</Swiper> </Swiper>
{/* Custom buttons */}
{/* Navigation Buttons */}
{imgs.length > 1 && (
<>
<button <button
className={`${navigationPrevEl.replace( 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 )} 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`}
text-white flex items-center justify-center hover:cursor-pointer transition`}
> >
<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>
<button <button
className={`${navigationNextEl.replace( 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 `} )} 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> </button>
</>
)}
</div> </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> </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 { ServiceBanner } from "./serviceBanner";
export { ServiceFaq } from "./serviceFaq"; 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 <div
className="absolute inset-0 z-10" className="absolute inset-0 z-10"
style={{ 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,32 +1,33 @@
import DotAnimatsiya from "@/components/dot/DotAnimatsiya"; import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
import FAQAccordion from "../faq/faqAccardion"; import FAQAccordion, { FAQItem } from "../faq/faqAccardion";
import { useTranslations } from "next-intl"; import { useLocale, useTranslations } from "next-intl";
export function ServiceFaq() { export function ServiceFaq() {
const t = useTranslations(); const t = useTranslations();
const faqItems = [ const locale = useLocale();
const faqItems: FAQItem[] = [
{ {
id: "faq-1", id: 1,
question: t("faq.question1.question"), question: t("faq.question1.question"),
answer: t("faq.question1.answer"), answer: t("faq.question1.answer"),
}, },
{ {
id: "faq-2", id: 2,
question: t("faq.question2.question"), question: t("faq.question2.question"),
answer: t("faq.question2.answer"), answer: t("faq.question2.answer"),
}, },
{ {
id: "faq-3", id: 3,
question: t("faq.question3.question"), question: t("faq.question3.question"),
answer: t("faq.question3.answer"), answer: t("faq.question3.answer"),
}, },
{ {
id: "faq-4", id: 4,
question: t("faq.question4.question"), question: t("faq.question4.question"),
answer: t("faq.question4.answer"), answer: t("faq.question4.answer"),
}, },
{ {
id: "faq-5", id: 5,
question: t("faq.question5.question"), question: t("faq.question5.question"),
answer: t("faq.question5.answer"), answer: t("faq.question5.answer"),
}, },
@@ -36,18 +37,18 @@ export function ServiceFaq() {
{/* header */} {/* header */}
<div className="space-y-4 w-full "> <div className="space-y-4 w-full ">
<div className="flex items-center gap-3 justify-center text-white text-xl"> <div className="flex items-center gap-3 justify-center text-white text-xl">
<DotAnimatsiya /> FAQ <DotAnimatsiya /> {locale === "ru" ? "ФАК" : "FAQ"}
</div> </div>
<h1 <h1
className="text-center bg-linear-to-br from-white via-white/50 to-black 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" 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> </h1>
</div> </div>
{/* FAQ Section */} {/* 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} /> <FAQAccordion items={faqItems} />
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

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

265
components/priceContact.tsx Normal file
View File

@@ -0,0 +1,265 @@
"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 "@/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");
const { isOpen, product, closeModal } = usePriceModalStore();
const [formData, setFormData] = useState({
name: "",
number: "+998 ",
});
const [errors, setErrors] = useState({
name: "",
number: "",
});
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");
},
});
// Reset form when modal closes for github
useEffect(() => {
if (!isOpen) {
setFormData({
name: "",
number: "+998 ",
});
setErrors({
name: "",
number: "",
});
}
}, [isOpen]);
// Prevent body scroll when modal is open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "unset";
}
return () => {
document.body.style.overflow = "unset";
};
}, [isOpen]);
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, number: formatted });
if (errors.number) {
setErrors({ ...errors, number: "" });
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
if (errors[name as keyof typeof errors]) {
setErrors({ ...errors, [name]: "" });
}
};
const validateForm = () => {
const newErrors = {
name: "",
number: "",
};
// Name validation
if (!formData.name.trim()) {
newErrors.name = t("validation.nameRequired") || "Ism kiritilishi shart";
}
// Phone validation
const phoneNumbers = formData.number.replace(/\D/g, "");
if (phoneNumbers.length !== 12) {
newErrors.number =
t("validation.phoneRequired") || "To'liq telefon raqam kiriting";
} else if (!phoneNumbers.startsWith("998")) {
newErrors.number =
t("validation.phoneInvalid") || "Noto'g'ri telefon raqam";
}
setErrors(newErrors);
return !newErrors.name && !newErrors.number;
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!validateForm()) {
return;
}
// Telefon raqamni tozalash (faqat raqamlar)
const cleanPhone = formData.number.replace(/\D/g, "");
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;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={closeModal}
/>
{/* Modal */}
<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 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-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-[#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 || "/placeholder.svg"}
alt={product.name}
fill
className="object-contain p-2"
/>
</div>
<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-5">
{/* Name */}
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-300 mb-2"
>
{t("form.name") || "Ismingiz"}
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
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="mt-1 text-sm text-red-500">{errors.name}</p>
)}
</div>
{/* Phone */}
<div>
<label
htmlFor="phone"
className="block text-sm font-medium text-gray-300 mb-2"
>
{t("form.phone") || "Telefon raqam"}
</label>
<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>
{/* Submit Button */}
<button
type="submit"
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"
>
{formRequest.isPending
? t("form.submitting") || "Yuborilmoqda..."
: t("form.submit") || "Yuborish"}
</button>
</form>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,33 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import BackAnimatsiya from "../backAnimatsiya/backAnimatsiya";
import { Footer, Navbar } from "../layout";
import { PriceModal } from "../priceContact";
import { Analytics } from "@vercel/analytics/next";
import { ReactNode } from "react";
import { Bounce, Flip, ToastContainer, Zoom } from "react-toastify";
const queryClient = new QueryClient();
export function Providers({ children }: { children: ReactNode }) {
return (
<div>
<QueryClientProvider client={queryClient}>
{/* <BackAnimatsiya /> */}
<Navbar />
{children}
<Footer />
<PriceModal />
<Analytics />
<ToastContainer
position="top-center"
autoClose={3000}
hideProgressBar={true}
transition={Zoom}
theme="colored"
/>
</QueryClientProvider>
</div>
);
}

View File

@@ -13,7 +13,7 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
<ol <ol
data-slot="breadcrumb-list" data-slot="breadcrumb-list"
className={cn( 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, className,
)} )}
{...props} {...props}

View File

@@ -1,10 +1,10 @@
'use client' "use client"
import * as React from 'react' import * as React from "react"
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu' import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-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({ function DropdownMenu({
...props ...props
@@ -16,7 +16,7 @@ function DropdownMenuPortal({
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return ( return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> <DropdownMenuPrimitive.Portal {...props} />
) )
} }
@@ -42,8 +42,8 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content" data-slot="dropdown-menu-content"
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( 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', "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, className
)} )}
{...props} {...props}
/> />
@@ -62,11 +62,11 @@ function DropdownMenuGroup({
function DropdownMenuItem({ function DropdownMenuItem({
className, className,
inset, inset,
variant = 'default', variant = "default",
...props ...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & { }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean inset?: boolean
variant?: 'default' | 'destructive' variant?: "default" | "destructive"
}) { }) {
return ( return (
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
@@ -75,7 +75,7 @@ function DropdownMenuItem({
data-variant={variant} data-variant={variant}
className={cn( 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", "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} {...props}
/> />
@@ -93,7 +93,7 @@ function DropdownMenuCheckboxItem({
data-slot="dropdown-menu-checkbox-item" data-slot="dropdown-menu-checkbox-item"
className={cn( 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", "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} checked={checked}
{...props} {...props}
@@ -129,7 +129,7 @@ function DropdownMenuRadioItem({
data-slot="dropdown-menu-radio-item" data-slot="dropdown-menu-radio-item"
className={cn( 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", "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} {...props}
> >
@@ -155,8 +155,8 @@ function DropdownMenuLabel({
data-slot="dropdown-menu-label" data-slot="dropdown-menu-label"
data-inset={inset} data-inset={inset}
className={cn( className={cn(
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className, className
)} )}
{...props} {...props}
/> />
@@ -170,7 +170,7 @@ function DropdownMenuSeparator({
return ( return (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-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} {...props}
/> />
) )
@@ -179,13 +179,13 @@ function DropdownMenuSeparator({
function DropdownMenuShortcut({ function DropdownMenuShortcut({
className, className,
...props ...props
}: React.ComponentProps<'span'>) { }: React.ComponentProps<"span">) {
return ( return (
<span <span
data-slot="dropdown-menu-shortcut" data-slot="dropdown-menu-shortcut"
className={cn( className={cn(
'text-muted-foreground ml-auto text-xs tracking-widest', "text-muted-foreground ml-auto text-xs tracking-widest",
className, className
)} )}
{...props} {...props}
/> />
@@ -212,7 +212,7 @@ function DropdownMenuSubTrigger({
data-inset={inset} data-inset={inset}
className={cn( 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", "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} {...props}
> >
@@ -230,8 +230,8 @@ function DropdownMenuSubContent({
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content" data-slot="dropdown-menu-sub-content"
className={cn( 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', "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, className
)} )}
{...props} {...props}
/> />

View File

@@ -1,20 +1,20 @@
import * as React from 'react' import * as React from "react"
import { import {
ChevronLeftIcon, ChevronLeftIcon,
ChevronRightIcon, ChevronRightIcon,
MoreHorizontalIcon, MoreHorizontalIcon,
} from 'lucide-react' } from "lucide-react"
import { cn } from '@/lib/utils' import { cn } from "@/lib/utils"
import { Button, buttonVariants } from '@/components/ui/button' import { Button, buttonVariants } from "@/components/ui/button"
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) { function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
return ( return (
<nav <nav
role="navigation" role="navigation"
aria-label="pagination" aria-label="pagination"
data-slot="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} {...props}
/> />
) )
@@ -23,42 +23,42 @@ function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
function PaginationContent({ function PaginationContent({
className, className,
...props ...props
}: React.ComponentProps<'ul'>) { }: React.ComponentProps<"ul">) {
return ( return (
<ul <ul
data-slot="pagination-content" 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} {...props}
/> />
) )
} }
function PaginationItem({ ...props }: React.ComponentProps<'li'>) { function PaginationItem({ ...props }: React.ComponentProps<"li">) {
return <li data-slot="pagination-item" {...props} /> return <li data-slot="pagination-item" {...props} />
} }
type PaginationLinkProps = { type PaginationLinkProps = {
isActive?: boolean isActive?: boolean
} & Pick<React.ComponentProps<typeof Button>, 'size'> & } & Pick<React.ComponentProps<typeof Button>, "size"> &
React.ComponentProps<'a'> React.ComponentProps<"a">
function PaginationLink({ function PaginationLink({
className, className,
isActive, isActive,
size = 'icon', size = "icon",
...props ...props
}: PaginationLinkProps) { }: PaginationLinkProps) {
return ( return (
<a <a
aria-current={isActive ? 'page' : undefined} aria-current={isActive ? "page" : undefined}
data-slot="pagination-link" data-slot="pagination-link"
data-active={isActive} data-active={isActive}
className={cn( className={cn(
buttonVariants({ buttonVariants({
variant: isActive ? 'outline' : 'ghost', variant: isActive ? "outline" : "ghost",
size, size,
}), }),
className, className
)} )}
{...props} {...props}
/> />
@@ -73,11 +73,10 @@ function PaginationPrevious({
<PaginationLink <PaginationLink
aria-label="Go to previous page" aria-label="Go to previous page"
size="default" 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} {...props}
> >
<ChevronLeftIcon /> <ChevronLeftIcon />
<span className="hidden sm:block">Previous</span>
</PaginationLink> </PaginationLink>
) )
} }
@@ -90,10 +89,9 @@ function PaginationNext({
<PaginationLink <PaginationLink
aria-label="Go to next page" aria-label="Go to next page"
size="default" 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} {...props}
> >
<span className="hidden sm:block">Next</span>
<ChevronRightIcon /> <ChevronRightIcon />
</PaginationLink> </PaginationLink>
) )
@@ -102,12 +100,12 @@ function PaginationNext({
function PaginationEllipsis({ function PaginationEllipsis({
className, className,
...props ...props
}: React.ComponentProps<'span'>) { }: React.ComponentProps<"span">) {
return ( return (
<span <span
aria-hidden aria-hidden
data-slot="pagination-ellipsis" 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} {...props}
> >
<MoreHorizontalIcon className="size-4" /> <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,46 @@
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 = [ export const DATA = [
{ {
@@ -30,41 +73,234 @@ export const DATA = [
}, },
]; ];
export const faqItems = [ export const ProductCatalog = [
{ {
id: "faq-1", id: "slt",
question: "How do I become a firefighter?", slug: "slt_blockfire",
answer: title: "SLT Blockfire",
"Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel sit amet adipiscing sem neque.", description: "products.catalog.blockdescription",
image: "/images/products/category/slt.png",
}, },
{ {
id: "faq-2", id: "ca",
question: "What equipment do firefighters use?", slug: "ca_fire_mech",
answer: title: "CA-FIRE | MECH",
"Firefighters use specialized equipment including protective gear, breathing apparatus, fire hoses, ladders, and various rescue tools. Each piece of equipment is designed to keep firefighters safe while they perform their duties. Our team is trained extensively on all equipment and safety protocols.", description: "products.catalog.cadescription",
image: "/images/products/category/ca.png",
}, },
{ {
id: "faq-3", id: "lede",
question: "Do firefighters only fight fires?", slug: "lede",
answer: title: "LEDE",
"No, modern firefighters respond to a wide variety of emergencies including medical calls, vehicle accidents, hazardous material incidents, and rescue operations. They serve as first responders and are trained in emergency medical services to provide life-saving care to the community.", description: "products.catalog.lededescription",
}, image: "/images/products/category/lede.png",
{ },
id: "faq-4", ];
question: "What are the work hours for firefighters?",
answer: export const slt = [
"Firefighters typically work shifts that vary by department. Many operate on a schedule of 24 hours on duty followed by 48-72 hours off duty. This schedule allows for adequate rest and recovery while ensuring continuous emergency response coverage for the community.", {
}, id: 1,
{ slug: "slt_bir_qavatli_quvr",
id: "faq-5", name: "Bir qavatli PP-R quvuri (SDR 6) SLT BLOCKFIRE",
question: "How long is firefighter training?", description: "",
answer: image: "/images/products/slt/slt_blackfirebittalik-removebg-preview.png",
"Initial firefighter training typically takes 12-18 weeks of full-time instruction at a fire academy. After this, firefighters continue receiving ongoing training throughout their careers. Our department invests heavily in continuous education to maintain the highest standards of service.", },
}, {
{ id: 2,
id: "faq-6", slug: "slt_yonginga_qarshi_45_burilish",
question: "What is required to apply for the firefighter position?", name: "Yonginga qarshi polipropilenli 45° burilish",
answer: description: "",
"Candidates must be at least 18 years old, have a high school diploma or GED, possess a valid drivers license, and pass a background check and medical examination. Physical fitness is essential, and candidates must pass the Candidate Physical Ability Test (CPAT).", image: "/images/products/slt/slt_blackfireburilish-removebg-preview.png",
},
{
id: 3,
slug: "slt_otish_nuftasi",
name: "SLT BLOCKFIRE PP-R o'tish muftasi BxH",
description: "",
image: "/images/products/slt/slt_blackfireperexodnaya-removebg-preview.png",
},
];
export const ca = [
{
id: 1,
slug: "takozli_eshik_klapan",
name: "Takozli eshik klapanlari",
description: "",
image: "/images/products/ca/zadviji-removebg-preview.png",
},
{
id: 2,
slug: "takozli_eshik_klapan",
name: "Bosim Regulyatorlari",
description: "",
image: "/images/products/ca/regulyator-removebg-preview.png",
},
{
id: 3,
slug: "vites_kapalak_klapan",
name: "Vites qutisi bilan kapalak klapanlar",
description: "",
image: "/images/products/ca/zatvori-removebg-preview.png",
},
];
export const lede = [
{
id: 1,
slug: "tishli_tirsak",
name: "Tishli tirsak 3j",
description: "",
image: "/images/products/lede/atvot_rezbavoy-removebg-preview.png",
},
{
id: 2,
slug: "yivli_flanes",
name: "PN16 321 bo'lingan yivli birlashma flanesi",
description: "",
image: "/images/products/lede/flanes_nakidnoy-removebg-preview.png",
},
{
id: 2,
slug: "qisqartiruvchi_tee",
name: "130R o'yilgan qisqartiruvchi tee",
description: "",
image: "/images/products/lede/troynik_perexadnoy-removebg-preview.png",
},
];
const sectionData = [
"SLT-Aqua",
"Вварное седло",
"Кран шаровый",
"Муфты",
"Муфты комбинированные",
"Муфты переходные",
"Тройник комбинированный",
"Тройники",
"Трубы SDR 6",
"Трубы SDR 7,4",
"Угол 45",
"Угол 90",
"Угольник комбинированный",
"Фланцы+бурты",
];
const sectionNumber = [
"25",
'25х1/2"',
'25х3/4"',
"32",
"32x25x25",
"32x25x32",
'32х1"',
'32х1/2"',
"32х25",
'32х3/4"',
"40",
"40x25x40",
"40x32x40",
'40х1 1/4"',
'40х1 3/4"',
'40х1/2"',
"40х25",
"40х32",
"50",
"50x25x50",
"50x32x50",
"50x40x50",
'50х1 1/2"',
'50х1/2"',
"50х25",
"50х32",
"50х40",
"63",
"63x25x63",
"63x32x63",
"63x40x63",
"63x50x63",
'63х1/2"',
'63х2"',
"63х25",
"63х32",
"63х40",
"63х50",
"75",
"75x25x75",
"75x32x75",
"75x40x75",
"75x50x75",
"75x63x75",
'75х1/2"',
"75х32",
"75х40",
"75х50",
"75х63",
"90",
"90x40x90",
"90x50x90",
"90x63x90",
"90x75x90",
'90х1/2"',
"90х32",
"90х40",
"90х50",
"90х63",
"90х75",
"110",
"110x50x110",
"110x63x110",
"110x75x110",
"110x90x110",
'110х1/2"',
"110х25",
"110х32",
"110х40",
"110х50",
"110х63",
"110х75",
"110х90",
"125",
"160",
];
export const result = [
{
type: "section",
items: sectionData.map((name, index) => ({
id: index + 1,
name,
type: "catalog",
})),
},
{
type: "size",
items: sectionNumber.map((name, index) => ({
id: index + 1,
name,
type: "size",
})),
},
];
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"],
}, },
]; ];

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