Compare commits

...

17 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
53 changed files with 1493 additions and 868 deletions

View File

@@ -1,34 +1,8 @@
"use client";
import Image from "next/image";
import { motion } from "framer-motion";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Guides } from "@/components/pages/about/aboutDetail/guides"; import { Guides } from "@/components/pages/about/aboutDetail/guides";
const ease = [0.22, 1, 0.36, 1] as [number, number, number, number];
export default function NotePPPage() { export default function NotePPPage() {
const t = useTranslations(); const t = useTranslations();
const guides = [
{
image: "/images/about/pp.avif",
title: t("about.notePPPage.hero.title"),
description: t("about.notePPPage.hero.description"),
eyebrow: t("about.notePPPage.hero.eyebrow"),
titleLine1: t("about.notePPPage.hero.titleLine1"),
titleLine2: t("about.notePPPage.hero.titleLine2"),
},
{
image: "/images/about/pp.avif",
title: t("about.noteTrailerPage.hero.title"),
description: t("about.noteTrailerPage.hero.description"),
eyebrow: t("about.noteTrailerPage.hero.eyebrow"),
titleLine1: t("about.noteTrailerPage.hero.titleLine1"),
titleLine2: t("about.noteTrailerPage.hero.titleLine2"),
},
];
return ( return (
<main className="min-h-[30vh] bg-[#0f0e0d] pt-5 text-white pb-40"> <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"> <div className="bg-black sm:w-[95%] w-[98%] mx-auto p-5">

View File

@@ -1,12 +1,30 @@
"use client" "use client";
import { CertCardSkeleton } from "@/components/pages/about/aboutDetail/loading/loading";
import { CertCard } from "@/components/pages/about/aboutDetail/sertificateCard"; import { CertCard } from "@/components/pages/about/aboutDetail/sertificateCard";
import PaginationLite from "@/components/paginationUI";
import { certs } from "@/lib/demoData"; 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 { motion } from "framer-motion";
import { Award } from "lucide-react"; import { Award } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useState } from "react";
export default function SertificatePage() { export default function SertificatePage() {
const t = useTranslations(); 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 ( return (
<main className="min-h-screen bg-[#0f0e0d] text-white pb-44 overflow-x-hidden"> <main className="min-h-screen bg-[#0f0e0d] text-white pb-44 overflow-x-hidden">
@@ -70,10 +88,23 @@ export default function SertificatePage() {
{/* ── Cards ── */} {/* ── Cards ── */}
<section className="max-w-4xl mx-auto px-6 flex flex-col gap-4"> <section className="max-w-4xl mx-auto px-6 flex flex-col gap-4">
{certs.map((c, i) => ( {isLoading ? (
<CertCard key={c.id} c={c} i={i} /> <CertCardSkeleton />
))} ) : (
generallydata.map((c: any, i: number) => (
<CertCard key={c.id} c={c} i={i} />
))
)}
</section> </section>
{/*pagination*/}
{data?.total_pages > 1 && (
<PaginationLite
currentPage={currentPage}
totalPages={data?.total_pages}
onChange={setCurrentPage}
/>
)}
</main> </main>
); );
} }

View File

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

View File

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

@@ -7,7 +7,7 @@ import {
SystemFeature, SystemFeature,
} from "@/lib/api/demoapi/operationalSystems"; } from "@/lib/api/demoapi/operationalSystems";
import { Breadcrumb } from "@/components/breadCrumb"; import { Breadcrumb } from "@/components/breadCrumb";
import { useServiceDetail } from "@/store/useService"; import { useServiceDetail } from "@/zustand/useService";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import httpClient from "@/request/api"; import httpClient from "@/request/api";
import { endPoints } from "@/request/links"; import { endPoints } from "@/request/links";

View File

@@ -155,10 +155,11 @@ export default async function RootLayout({
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased`}
> >
<InitialLoading /> {/* <InitialLoading />
<NextIntlClientProvider messages={messages} locale={locale}> <NextIntlClientProvider messages={messages} locale={locale}>
<Providers>{children}</Providers> <Providers>{children}</Providers>
</NextIntlClientProvider> </NextIntlClientProvider> */}
{children}
</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 />;
} }

View File

@@ -3,9 +3,9 @@ import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { ChevronRight, Home } from "lucide-react"; import { ChevronRight, Home } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useCategory } from "@/store/useCategory"; import { useCategory } from "@/zustand/useCategory";
import { useSubCategory } from "@/store/useSubCategory"; import { useSubCategory } from "@/zustand/useSubCategory";
import { useProductPageInfo } from "@/store/useProduct"; import { useProductPageInfo } from "@/zustand/useProduct";
interface BreadcrumbProps { interface BreadcrumbProps {
customLabels?: Record<string, string>; customLabels?: Record<string, string>;
@@ -161,28 +161,28 @@ export function Breadcrumb({
className="flex items-center gap-2" className="flex items-center gap-2"
> >
{index > 0 && ( {index > 0 && (
<ChevronRight className="w-4 h-4 text-gray-400 dark:text-gray-600" /> <ChevronRight className="w-4 h-4 text-gray-200" />
)} )}
{index === 0 ? ( {index === 0 ? (
// Home link with icon // Home link with icon
<Link <Link
href={item.href} href={item.href}
className="flex items-center gap-1.5 text-gray-600 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-500 transition-colors duration-200" className="flex items-center gap-1.5 text-gray-200 hover:text-red-600 transition-colors duration-200"
> >
<Home className="w-4 h-4" /> <Home className="w-4 h-4" />
<span className="hidden sm:inline">{item.label}</span> <span className="hidden sm:inline">{item.label}</span>
</Link> </Link>
) : item.isLast ? ( ) : item.isLast ? (
// Last item (current page) // Last item (current page)
<span className="text-gray-900 dark:text-white font-medium line-clamp-1"> <span className=" text-gray-200 font-medium line-clamp-1">
{item.label} {item.label}
</span> </span>
) : ( ) : (
// Regular link // Regular link
<Link <Link
href={item.href} href={item.href}
className="text-gray-600 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-500 transition-colors duration-200 line-clamp-1" className="text-gray-200 hover:text-red-600 transition-colors duration-200 line-clamp-1"
> >
{item.label} {item.label}
</Link> </Link>

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);
}; };
@@ -55,7 +60,7 @@ export default function LanguageSelectRadix() {
<button <button
onClick={() => setIsOpen(!isOpen)} onClick={() => setIsOpen(!isOpen)}
disabled={isPending} disabled={isPending}
className="inline-flex items-center justify-between gap-2 px-2 py-1 border border-gray-300 hover:border-red-600 className="inline-flex items-center justify-between gap-2 px-2 py-1 border border-gray-300 hover:border-red-600
rounded-lg text-white text-sm font-medium shadow-sm hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all" rounded-lg text-white text-sm font-medium shadow-sm hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
aria-label="Tilni tanlash" aria-label="Tilni tanlash"
aria-expanded={isOpen} aria-expanded={isOpen}

View File

@@ -12,6 +12,10 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "../ui/dropdown-menu"; } 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();
@@ -19,12 +23,15 @@ export function Navbar() {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false); const [scrolled, setScrolled] = useState(false);
const tabs = [ const { data: navbarItems } = useQuery({
{ name: t("navbar.about"), value: "" }, queryKey: ["navbaritem",locale],
{ name: t("about.subPages.baza"), value: "baza" }, queryFn: () => httpClient(endPoints.navbar),
{ name: t("about.subPages.certificate"), value: "sertificate" }, select: (data: any) => ({
{ name: t("about.subPages.notePP"), value: "notePP" }, 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 = () => {
@@ -76,61 +83,45 @@ export function Navbar() {
{/* 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">
<Link {navbarItems?.results ? (
href={`/${locale}/home`} navbarItems.results.map((item: NavbarItem) => (
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition" <DropdownMenu key={item.id}>
> <DropdownMenuTrigger asChild>
{t("navbar.home")}
</Link>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<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")}
<ChevronDown size={12} className="ml-1" />
</Link>
</DropdownMenuTrigger>
<DropdownMenuContent>
{tabs.map((tab) => (
<DropdownMenuItem asChild key={tab.value}>
<Link <Link
href={`/${locale}/about/${tab.value}`} key={item.id}
className="font-unbounded w-full uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition" 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"
> >
{tab.name} {item.name}
{item.children.length > 0 && (
<ChevronDown size={12} className="ml-1" />
)}
</Link> </Link>
</DropdownMenuItem> </DropdownMenuTrigger>
))} {item.children.length > 0 && (
</DropdownMenuContent> <DropdownMenuContent className="space-y-2">
</DropdownMenu> {item.children.map((child: NavbarItem) => (
<DropdownMenuItem asChild key={child.id}>
<Link <Link
href={`/${locale}/faq`} 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" className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
> >
{locale === "ru" ? "ЧЗВ" : "FAQ"} {child.name}
</Link> </Link>
<Link </DropdownMenuItem>
href={`/${locale}/services`} ))}
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition" </DropdownMenuContent>
> )}
{t("navbar.services")} </DropdownMenu>
</Link> ))
) : (
<Link <Link
href={`/${locale}/catalog_page`} 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.products")} {t("navbar.home")}
</Link> </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">
@@ -207,67 +198,44 @@ 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">
<Link {navbarItems?.results ? (
href={`/${locale}/home`} navbarItems.results.map((item: NavbarItem) => (
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2" <DropdownMenu key={item.id}>
onClick={() => setIsMobileMenuOpen(false)} <DropdownMenuTrigger asChild>
>
{t("navbar.home")}
</Link>
<DropdownMenu>
<DropdownMenuTrigger>
<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")}
<ChevronDown size={12} className="ml-1" />
</Link>
</DropdownMenuTrigger>
<DropdownMenuContent>
{tabs.map((tab) => (
<DropdownMenuItem>
<Link <Link
href={`/${locale}/about/${tab.value}`} key={item.id}
className="font-unbounded w-full uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition" 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"
> >
{tab.name} {item.name}
{item.children.length > 0 && (
<ChevronDown size={12} className="ml-1" />
)}
</Link> </Link>
</DropdownMenuItem> </DropdownMenuTrigger>
))} {item.children.length > 0 && (
</DropdownMenuContent> <DropdownMenuContent className="space-y-2">
</DropdownMenu> {item.children.map((child: NavbarItem) => (
<Link
{/* Mobile Pages Dropdown */} key={child.id}
<Link href={`/${locale}/${child.url}`}
href={`/${locale}/faq`} className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2" >
onClick={() => setIsMobileMenuOpen(false)} {child.name}
> </Link>
{locale === "ru" ? "ЧЗВ" : "FAQ"} ))}
</Link> </DropdownMenuContent>
<Link )}
href={`/${locale}/services`} </DropdownMenu>
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2" ))
onClick={() => setIsMobileMenuOpen(false)} ) : (
> <Link
{t("navbar.services")} href={`/${locale}/home`}
</Link> className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
>
<Link {t("navbar.home")}
href={`/${locale}/catalog_page`} </Link>
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> </div>
</> </>

View File

@@ -1,7 +1,7 @@
"use client"; "use client";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { ShieldCheck, BookOpen, Flame } from "lucide-react"; import { ShieldCheck } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { NormativeCard } from "./normativeCard"; import { NormativeCard } from "./normativeCard";
@@ -21,27 +21,6 @@ const fadeUpView = (delay = 0) => ({
export default function NormativBazaPage() { export default function NormativBazaPage() {
const t = useTranslations(); const t = useTranslations();
const cards = [
{
icon: BookOpen,
title: t("about.normativBaza.cards.card1.title"),
text: t("about.normativBaza.cards.card1.text"),
number: "01",
},
{
icon: Flame,
title: t("about.normativBaza.cards.card2.title"),
text: t("about.normativBaza.cards.card2.text"),
number: "02",
},
{
icon: ShieldCheck,
title: t("about.normativBaza.cards.card3.title"),
text: t("about.normativBaza.cards.card3.text"),
number: "03",
},
];
return ( return (
<div className="bg-[#0f0e0d] text-white min-h-screen pt-10 pb-20"> <div className="bg-[#0f0e0d] text-white min-h-screen pt-10 pb-20">
{/* ── Hero ── */} {/* ── Hero ── */}

View File

@@ -1,40 +1,73 @@
"use client";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import DownloadCard from "./card"; 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() { export function Guides() {
const t = useTranslations(); const t = useTranslations();
const [currentPage, setCurrentPage] = useState(1);
const guides = [ const guides = [
{ {
fileUrl: "/varnix.pdf", file: "/varnix.pdf",
fileName: t("about.notePPPage.varnix"), name: t("about.notePPPage.varnix"),
fileType: "PDF", file_type: "PDF",
fileSize: "368.51 KB", file_size: "368.51 KB",
}, },
{ {
fileUrl: "/ppFlanes.pdf", file: "/ppFlanes.pdf",
fileName: t("about.notePPPage.ppFlanes"), name: t("about.notePPPage.ppFlanes"),
fileType: "PDF", file_type: "PDF",
fileSize: "368.51 KB", file_size: "368.51 KB",
}, },
{ {
fileUrl: "/ppFiting.pdf", file: "/ppFiting.pdf",
fileName: t("about.notePPPage.ppFiting"), name: t("about.notePPPage.ppFiting"),
fileType: "PDF", file_type: "PDF",
fileSize: "368.51 KB", 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 ( return (
<div className="grid lg:grid-cols-3 min-[580px]:grid-cols-2 grid-cols-1 gap-4 max-w-7xl mx-auto py-5"> <div className="space-y-4">
{guides.map((guide, index) => ( <div className="grid lg:grid-cols-3 min-[580px]:grid-cols-2 grid-cols-1 gap-4 max-w-7xl mx-auto py-5">
<DownloadCard {isLoading ? (
key={index} <DownloadCardSkeleton />
title={guide.fileName} ) : (
fileType={guide.fileType} guidedata.map((guide: any, index: number) => (
fileSize={guide.fileSize} <DownloadCard
fileUrl={guide.fileUrl} 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> </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

@@ -4,62 +4,92 @@ import { motion } from "framer-motion";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { Award } from "lucide-react"; import { Award } from "lucide-react";
import { normativeData } from "@/lib/demoData"; 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() { export function NormativeCard() {
const t = useTranslations(); 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 ( return (
<div className="flex flex-col gap-8 py-10 max-w-6xl mx-auto px-2"> <div className="space-y-4">
{normativeData.map((c, i) => ( <div className="flex flex-col gap-8 py-10 max-w-6xl mx-auto px-2">
<motion.article {generallyData.map((c: any, i: number) => (
key={i} <motion.article
initial={{ opacity: 0, y: 28 }} key={i}
whileInView={{ opacity: 1, y: 0 }} initial={{ opacity: 0, y: 28 }}
transition={{ duration: 0.55, delay: i * 0.1 }} whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }} transition={{ duration: 0.55, delay: i * 0.1 }}
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" 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"> {/* Meta + actions */}
<div className="space-y-2"> <div className="flex flex-col justify-between flex-1 min-w-0 py-1 gap-4">
{/* Badge row */} <div className="space-y-2">
<div className="flex items-center gap-2 flex-wrap"> {/* Badge row */}
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-2 flex-wrap">
<Award size={11} className="text-red-600 shrink-0" /> <div className="flex items-center gap-1.5">
<span className="text-[10px] font-black uppercase tracking-widest text-red-600"> <Award size={11} className="text-red-600 shrink-0" />
{t("about.certificatePage.card.badge")} <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> </span>
</div> </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.category} {/* Title */}
</span> <h3 className="font-bold text-sm md:text-base text-white leading-snug">
{t(c.title)}
</h3>
</div> </div>
{/* Title */}
<h3 className="font-bold text-sm md:text-base text-white leading-snug">
{t(c.titleKey)}
</h3>
</div> </div>
</div>
{/* Divider */} {/* Divider */}
<div className="mx-4 h-px bg-white/5 sm:my-5 my-2" /> <div className="mx-4 h-px bg-white/5 sm:my-5 my-2" />
{/* Documents list */} {/* Documents list */}
<div className="overflow-hidden"> <div className="overflow-hidden">
<ul className="flex flex-col gap-2.5"> <ul className="flex flex-col gap-2.5">
{c.documents.map((doc, di) => ( {c.features.map((doc: any, di: number) => (
<li key={di} className="flex items-start gap-2.5"> <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" /> <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"> <p className="text-xs text-gray-400 leading-relaxed">
{t(doc)} {t(doc?.name)}
</p> </p>
</li> </li>
))} ))}
</ul> </ul>
</div> </div>
</motion.article> </motion.article>
))} ))}
</div>
{data?.total_pages > 1 && (
<PaginationLite
currentPage={currentPage}
totalPages={data?.total_pages}
onChange={setCurrentPage}
/>
)}
</div> </div>
); );
} }

View File

@@ -28,7 +28,7 @@ export function CertCard({ c, i }: { c: (typeof certs)[0]; i: number }) {
</span> </span>
</div> </div>
<span className="text-[10px] font-bold uppercase tracking-wider text-white/20 border border-white/10 px-2 py-0.5 rounded-full"> <span className="text-[10px] font-bold uppercase tracking-wider text-white/20 border border-white/10 px-2 py-0.5 rounded-full">
{c.category} {c.artikul}
</span> </span>
</div> </div>
@@ -45,12 +45,18 @@ export function CertCard({ c, i }: { c: (typeof certs)[0]; i: number }) {
{/* Collapsible document list */} {/* Collapsible document list */}
<div className="overflow-hidden"> <div className="overflow-hidden">
<ul className="flex flex-col gap-2.5"> <ul className="flex flex-col gap-2.5">
{c.documents.map((doc, di) => ( {c.features.length > 0 &&
<li key={di} className="flex items-start gap-2.5"> c.features.map((doc: any, di: number) => {
<span className="mt-1 flex-none w-1.5 h-1.5 rounded-full bg-red-600/60" /> const { name } = doc;
<p className="text-xs text-gray-400 leading-relaxed">{doc}</p> return (
</li> <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> </ul>
</div> </div>
</motion.article> </motion.article>

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

@@ -9,6 +9,11 @@ import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { useLocale, useTranslations } from "next-intl"; import { useLocale, useTranslations } from "next-intl";
import DotAnimatsiya from "@/components/dot/DotAnimatsiya"; 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 // The custom CSS selectors for navigation
const navigationPrevEl = ".hero-swiper-prev"; const navigationPrevEl = ".hero-swiper-prev";
const navigationNextEl = ".hero-swiper-next"; const navigationNextEl = ".hero-swiper-next";
@@ -16,6 +21,11 @@ const navigationNextEl = ".hero-swiper-next";
export function BannerSlider() { export function BannerSlider() {
const t = useTranslations(); const t = useTranslations();
const locale = useLocale(); const locale = useLocale();
const { data, isLoading } = useQuery({
queryKey: ["banner"],
queryFn: () => httpClient(endPoints.banner),
select: (data: any): BannerType[] => data?.data?.results,
});
const BANNER_DATA = [ const BANNER_DATA = [
{ {
image: "/images/homeBanner3.png", image: "/images/homeBanner3.png",
@@ -28,14 +38,19 @@ export function BannerSlider() {
description: t("home.banner.description"), description: t("home.banner.description"),
}, },
]; ];
const bannerData = data ?? BANNER_DATA;
if (isLoading) return <BannerSliderSkeleton />;
return ( return (
<div className="relative z-30 h-full mt-20"> <div className="max-w-7xl mx-auto relative z-30 h-full mt-20 flex items-center justify-center ">
{/* Custom buttons */} {/* Custom buttons */}
<button <button
className={`${navigationPrevEl.replace( className={`${navigationPrevEl.replace(
".", ".",
"", "",
)} w-10 h-10 absolute z-10 left-[10%] top-50 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`} )} 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} /> <ChevronLeft size={30} />
</button> </button>
@@ -43,7 +58,7 @@ export function BannerSlider() {
className={`${navigationNextEl.replace( className={`${navigationNextEl.replace(
".", ".",
"", "",
)} w-10 h-10 absolute z-10 right-[10%] top-50 rounded-full bg-primary text-center text-white lg:flex hidden items-center justify-center hover:bg-red-600 hover:cursor-pointer transition `} )} 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} /> <ChevronRight size={30} />
</button> </button>
@@ -62,7 +77,7 @@ export function BannerSlider() {
disableOnInteraction: false, disableOnInteraction: false,
}} }}
> >
{BANNER_DATA.map((item, index) => ( {bannerData.map((item, index) => (
<SwiperSlide key={index}> <SwiperSlide key={index}>
<div className="relative z-20 h-full flex items-center lg:mt-0 sm:mt-[10vh] mt-[5vh]"> <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="max-w-400 mx-auto px-4 sm:px-6 lg:px-8 w-full">

View File

@@ -10,7 +10,7 @@ import Link from "next/link";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { ServicesLoading } from "../services/loading"; import { ServicesLoading } from "../services/loading";
import { EmptyServices } from "../services/empty"; import { EmptyServices } from "../services/empty";
import { useServiceDetail } from "@/store/useService"; import { useServiceDetail } from "@/zustand/useService";
import { cardVariants, containerVariants } from "@/lib/animations"; import { cardVariants, containerVariants } from "@/lib/animations";
export function OurService() { export function OurService() {

View File

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

View File

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

View File

@@ -48,8 +48,8 @@ import { useLocale, useTranslations } from "next-intl";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { ArrowUpRight } from "lucide-react"; import { ArrowUpRight } from "lucide-react";
import { useCategory } from "@/store/useCategory"; import { useCategory } from "@/zustand/useCategory";
import { useSubCategory } from "@/store/useSubCategory"; import { useSubCategory } from "@/zustand/useSubCategory";
interface CatalogProps { interface CatalogProps {
id: number; id: number;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,17 @@
"use client"; "use client";
import httpClient from "@/request/api"; import httpClient from "@/request/api";
import { endPoints } from "@/request/links"; import { endPoints } from "@/request/links";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import ProductCard from "./productCard"; import ProductCard from "./productCard";
import { useCategory } from "@/store/useCategory"; import { useCategory } from "@/zustand/useCategory";
import { useFilter } from "@/lib/filter-zustand"; import { useFilter } from "@/lib/filter-zustand";
import { useMemo, useState } from "react"; import { useMemo, useState, useEffect } from "react";
import { useProductPageInfo } from "@/store/useProduct"; import { useProductPageInfo } from "@/zustand/useProduct";
import { useSubCategory } from "@/store/useSubCategory"; import { useSubCategory } from "@/zustand/useSubCategory";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import PaginationLite from "@/components/paginationUI"; import PaginationLite from "@/components/paginationUI";
import { useCatalog } from "@/zustand/useCatalog";
export default function MainProduct() { export default function MainProduct() {
const t = useTranslations(); const t = useTranslations();
@@ -19,8 +21,15 @@ export default function MainProduct() {
const getFiltersByType = useFilter((s) => s.getFiltersByType); const getFiltersByType = useFilter((s) => s.getFiltersByType);
const setProduct = useProductPageInfo((s) => s.setProducts); const setProduct = useProductPageInfo((s) => s.setProducts);
const parentID = useCatalog((state) => state.parentID);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
setCurrentPage(1);
}, [parentID]);
// ── Filter params ────────────────────────────────────────────────────────
const queryParams = useMemo(() => { const queryParams = useMemo(() => {
const catalog = getFiltersByType("catalog"); const catalog = getFiltersByType("catalog");
const size = getFiltersByType("size"); const size = getFiltersByType("size");
@@ -31,45 +40,48 @@ export default function MainProduct() {
return allParams ? `&${allParams}` : ""; return allParams ? `&${allParams}` : "";
}, [filter]); }, [filter]);
// ── Request URL ──────────────────────────────────────────────────────────
const requestLink = useMemo(() => { const requestLink = useMemo(() => {
const baseLink = category.have_sub_category const baseLink = category.have_sub_category
? endPoints.product.bySubCategory({ id: subCategory.id, currentPage }) ? endPoints.product.bySubCategory({ id: subCategory.id, currentPage })
: endPoints.product.byCategory({ id: category.id, currentPage }); : parentID
? endPoints.product.byCatalogSection({ id: parentID, currentPage })
: endPoints.product.byCategory({ id: category.id, currentPage });
return `${baseLink}${queryParams}`; return `${baseLink}${queryParams}`;
}, [ }, [
category.id, category.id,
category.have_sub_category, category.have_sub_category,
queryParams,
subCategory.id, subCategory.id,
currentPage, currentPage,
parentID,
queryParams,
]); ]);
// ── Query ────────────────────────────────────────────────────────────────
const { data, isLoading, error } = useQuery({ const { data, isLoading, error } = useQuery({
queryKey: [ queryKey: [
"products", "products",
subCategory.id,
category.id, category.id,
category.have_sub_category,
subCategory.id,
parentID,
queryParams, queryParams,
currentPage, currentPage,
], ],
queryFn: () => httpClient(requestLink), queryFn: () => httpClient(requestLink),
placeholderData: (prev) => prev, // ✅ pagination da flicker yo'q placeholderData: (prev) => prev, // no flicker on pagination
select: (res) => ({ select: (res) => ({
results: res?.data?.data?.results ?? [], results: res?.data?.data?.results ?? [],
totalPages: res?.data?.data?.total_pages ?? 1, totalPages: res?.data?.data?.total_pages ?? 1,
}), }),
}); });
// ✅ To'g'ridan select dan olamiz — useEffect + let emas const results = data?.results ?? [];
const results = useMemo(() => { const totalPages = data?.totalPages ?? 1;
return data?.results ?? [];
}, [data]);
const totalPages = useMemo(() => {
return data?.totalPages ?? 1;
}, [data]);
// ── Render states ────────────────────────────────────────────────────────
if (isLoading && !data) { if (isLoading && !data) {
// ✅ placeholderData bor — faqat birinchi yuklanishda
return ( return (
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5"> <div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
{[1, 2, 3].map((i) => ( {[1, 2, 3].map((i) => (
@@ -94,10 +106,11 @@ export default function MainProduct() {
} }
return ( return (
<div > <div className="space-y-4">
{/* ✅ isLoading da overlay — list o'rnini saqlab, ustidan opacity */}
<div <div
className={`grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5 transition-opacity ${isLoading ? "opacity-50 pointer-events-none" : "opacity-100"}`} 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) => ( {results.map((item: any) => (
<ProductCard <ProductCard

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useLocale } 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";
@@ -18,6 +18,7 @@ export default function ProductCard({
getProduct, getProduct,
}: ProductCardProps) { }: ProductCardProps) {
const locale = useLocale(); const locale = useLocale();
const t = useTranslations();
return ( return (
<Link <Link
@@ -121,7 +122,7 @@ export default function ProductCard({
uppercase uppercase
font-medium font-medium
"> ">
Batafsil { t("home.services.learnmore")}
</span> </span>
{/* Arrow */} {/* Arrow */}

View File

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

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { usePriceModalStore } from "@/store/useProceModalStore"; import { usePriceModalStore } from "@/zustand/useProceModalStore";
import { Check, Instagram, Send, Share2 } from "lucide-react"; import { Check, Instagram, Send, Share2 } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";

View File

@@ -10,7 +10,7 @@ import Link from "next/link";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import { ServicesLoading } from "./loading"; import { ServicesLoading } from "./loading";
import { EmptyServices } from "./empty"; import { EmptyServices } from "./empty";
import { useServiceDetail } from "@/store/useService"; import { useServiceDetail } from "@/zustand/useService";
import { cardVariants, containerVariants } from "@/lib/animations"; import { cardVariants, containerVariants } from "@/lib/animations";
export function ServicePageServices() { export function ServicePageServices() {
@@ -41,8 +41,6 @@ export function ServicePageServices() {
}, },
}); });
console.log("service page services: ", data);
return ( return (
<div className="bg-[#1e1d1c] py-10 md:py-16 lg:py-20 mb-15"> <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"> <div className="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8">

View File

@@ -3,7 +3,7 @@
import httpClient from "@/request/api"; import httpClient from "@/request/api";
import { endPoints } from "@/request/links"; import { endPoints } from "@/request/links";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useCategory } from "@/store/useCategory"; import { useCategory } from "@/zustand/useCategory";
import Card from "./card"; import Card from "./card";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";

View File

@@ -1,4 +1,4 @@
import { useSubCategory } from "@/store/useSubCategory"; import { useSubCategory } from "@/zustand/useSubCategory";
import { useLocale } from "next-intl"; import { useLocale } from "next-intl";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";

View File

@@ -78,8 +78,8 @@ export default function PaginationLite({
) : ( ) : (
<PaginationItem key={idx}> <PaginationItem key={idx}>
<PaginationLink <PaginationLink
className="hover:cursor-pointer text-white hover:text-white"
isActive={p === currentPage} 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))} onClick={() => onChange(Number(p))}
> >
{p} {p}

View File

@@ -3,7 +3,7 @@ import { useTranslations } from "next-intl";
import Image from "next/image"; import Image from "next/image";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { X } from "lucide-react"; import { X } from "lucide-react";
import { usePriceModalStore } from "@/store/useProceModalStore"; import { usePriceModalStore } from "@/zustand/useProceModalStore";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import httpClient from "@/request/api"; import httpClient from "@/request/api";
import { endPoints } from "@/request/links"; import { endPoints } from "@/request/links";

View File

@@ -1,11 +1,13 @@
import { title } from "process";
export const certs = [ export const certs = [
{ {
id: 1, id: 1,
src: "/images/about/sertificate.webp", src: "/images/about/sertificate.webp",
title: "Пожаростойкие армированные трубы SLT BLOCKFIRE PP-R-GF", title: "Пожаростойкие армированные трубы SLT BLOCKFIRE PP-R-GF",
year: "2024", year: "2024",
category: "PP-R-GF", artikul: "PP-R-GF",
documents: [ features: [
"СТО 22.21.29-015-17207509-2022 (версия 2) — согласован МЧС России в качестве нормативного документа по пожарной безопасности.", "СТО 22.21.29-015-17207509-2022 (версия 2) — согласован МЧС России в качестве нормативного документа по пожарной безопасности.",
"СТО 22.21.29-021-17207509-2024 «Автоматическая противопожарная защита многоярусных стеллажных конструкций» — согласован МЧС России №ГУ-исх-66586 от 05.07.2024.", "СТО 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.", "Протоколы испытаний по ГОСТ Р 58832 ИЛ НИЦ ПТ и СП ФГБУ ВНИИПО МЧС России № 2249/2.1-2022 от 03.03.2022, №2683/2.1-2023 от 06.10.2023.",
@@ -17,8 +19,8 @@ export const certs = [
src: "/images/about/sertificate.webp", src: "/images/about/sertificate.webp",
title: "Пожаростойкие однослойные трубы SLT BLOCKFIRE PP-R", title: "Пожаростойкие однослойные трубы SLT BLOCKFIRE PP-R",
year: "2023", year: "2023",
category: "PP-R", artikul: "PP-R",
documents: [ features: [
"СТО 22.21.29-015-17207509-2022 (версия 2) — согласован МЧС России в качестве нормативного документа по пожарной безопасности.", "СТО 22.21.29-015-17207509-2022 (версия 2) — согласован МЧС России в качестве нормативного документа по пожарной безопасности.",
"СТО 22.21.29-021-17207509-2024 «Автоматическая противопожарная защита многоярусных стеллажных конструкций» — согласован МЧС России №ГУ-исх-66586 от 05.07.2024.", "СТО 22.21.29-021-17207509-2024 «Автоматическая противопожарная защита многоярусных стеллажных конструкций» — согласован МЧС России №ГУ-исх-66586 от 05.07.2024.",
"Протоколы испытаний ИЛ НИЦ ПТ и СП ФГБУ ВНИИПО МЧС России № 2249/2.1-2022 от 03.03.2022, №2683/2.1-2023 от 06.10.2023.", "Протоколы испытаний ИЛ НИЦ ПТ и СП ФГБУ ВНИИПО МЧС России № 2249/2.1-2022 от 03.03.2022, №2683/2.1-2023 от 06.10.2023.",
@@ -30,8 +32,8 @@ export const certs = [
src: "/images/about/sertificate.webp", src: "/images/about/sertificate.webp",
title: "Пожаростойкие фитинги SLT BLOCKFIRE PP-R", title: "Пожаростойкие фитинги SLT BLOCKFIRE PP-R",
year: "2023", year: "2023",
category: "Фитинги", artikul: "Фитинги",
documents: [ features: [
"СТО 22.21.29-015-17207509-2022 (версия 2) — согласован МЧС России в качестве нормативного документа по пожарной безопасности.", "СТО 22.21.29-015-17207509-2022 (версия 2) — согласован МЧС России в качестве нормативного документа по пожарной безопасности.",
"СТО 22.21.29-021-17207509-2024 «Автоматическая противопожарная защита многоярусных стеллажных конструкций» — согласован МЧС России №ГУ-исх-66586 от 05.07.2024.", "СТО 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.", "Протоколы испытаний по ГОСТ Р 58832 ИЛ НИЦ ПТ и СП ФГБУ ВНИИПО МЧС России № 2249/2.1-2022 от 03.03.2022, №2683/2.1-2023 от 06.10.2023.",
@@ -45,12 +47,12 @@ export const DATA = [
name: "P-0834405", name: "P-0834405",
title: "Elektr yong'in detektori-Ypres ver.3", title: "Elektr yong'in detektori-Ypres ver.3",
status: "full", status: "full",
description: `Xavfsizlik va yong'in qo'l detektori barcha turdagi qabul qilish va nazorat qilish moslamalari bilan ishlash uchun mo'ljallangan. description: `Xavfsizlik va yong'in qo'l detektori barcha turdagi qabul qilish va nazorat qilish moslamalari bilan ishlash uchun mo'ljallangan.
Detektor GOST R 53325-2012 bo'yicha a sinfiga mos keladi va himoya qopqog'ini ochib, qo'zg'aysan elementini - ishning markazidagi tugmachani Detektor GOST R 53325-2012 bo'yicha a sinfiga mos keladi va himoya qopqog'ini ochib, qo'zg'aysan elementini - ishning markazidagi tugmachani
bosgandan so'ng signal signalini hosil qiladi.Detektor korpusining kichik qalinligi uni shikastlanishdan himoya qiladi, tashqi ko'rinishini bosgandan so'ng signal signalini hosil qiladi.Detektor korpusining kichik qalinligi uni shikastlanishdan himoya qiladi, tashqi ko'rinishini
yaxshilaydi va faqat o'rnatish uchun qo'shimcha variantni ishlatishga imkon beradi. Elektr detektori-IPR ver.3 faqat HP signal kabellarida ishlashni ta'minlaydi. yaxshilaydi va faqat o'rnatish uchun qo'shimcha variantni ishlatishga imkon beradi. Elektr detektori-IPR ver.3 faqat HP signal kabellarida ishlashni ta'minlaydi.
Detektorning navbatchilik rejimi qizil rangdagi ko'rinishlar bilan ko'rsatiladi, "yong'in" rejimi - qizil indikatorni doimiy ravishda yoqish orqali. Detektorning navbatchilik rejimi qizil rangdagi ko'rinishlar bilan ko'rsatiladi, "yong'in" rejimi - qizil indikatorni doimiy ravishda yoqish orqali.
Tugmani bosgandan va mahkamlagandan so'ng uni dastlabki holatiga qulay tarzda qaytarish( asbob bilan, maxsus kalitsiz); etkazib berish to'plamida shaffof himoya Tugmani bosgandan va mahkamlagandan so'ng uni dastlabki holatiga qulay tarzda qaytarish( asbob bilan, maxsus kalitsiz); etkazib berish to'plamida shaffof himoya
qopqog'ining mavjudligi; tizimga texnik xizmat ko'rsatishni soddalashtirish uchun muhrlash qobiliyati.`, qopqog'ining mavjudligi; tizimga texnik xizmat ko'rsatishni soddalashtirish uchun muhrlash qobiliyati.`,
features: [ features: [
"Signal pallasida besleme zo'riqishida-9 + 28V;", "Signal pallasida besleme zo'riqishida-9 + 28V;",
@@ -283,9 +285,9 @@ export const result = [
export const normativeData = [ export const normativeData = [
{ {
titleKey: "certs.slt_blockfire.title", title: "certs.slt_blockfire.title",
category: "SLT BLOCKFIRE", artikul: "SLT BLOCKFIRE",
documents: [ features: [
"certs.slt_blockfire.doc1", "certs.slt_blockfire.doc1",
"certs.slt_blockfire.doc2", "certs.slt_blockfire.doc2",
"certs.slt_blockfire.doc3", "certs.slt_blockfire.doc3",
@@ -293,14 +295,12 @@ export const normativeData = [
"certs.slt_blockfire.doc5", "certs.slt_blockfire.doc5",
"certs.slt_blockfire.doc6", "certs.slt_blockfire.doc6",
"certs.slt_blockfire.doc7", "certs.slt_blockfire.doc7",
"certs.slt_blockfire.doc8" "certs.slt_blockfire.doc8",
] ],
}, },
{ {
titleKey: "certs.slt_aqua.title", title: "certs.slt_aqua.title",
category: "SLT AQUA", artikul: "SLT AQUA",
documents: [ features: ["certs.slt_aqua.doc1"],
"certs.slt_aqua.doc1" },
]
}
]; ];

View File

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

View File

@@ -425,5 +425,8 @@
"clear_all": "Clear all", "clear_all": "Clear all",
"image_not_found": "Image not available", "image_not_found": "Image not available",
"loading_error": "An error occurred while loading data", "loading_error": "An error occurred while loading data",
"products_not_found": "Products not found" "products_not_found": "Products not found",
"hide": "Hide",
"show_more": "Show more",
"category": "Categories"
} }

View File

@@ -124,7 +124,7 @@
"subPages": { "subPages": {
"baza": "Нормативная база", "baza": "Нормативная база",
"certificate": "Сертификаты", "certificate": "Сертификаты",
"notePP": "Руководства" "notePP": "Инструкция"
}, },
"normativBaza": { "normativBaza": {
"hero": { "hero": {
@@ -425,5 +425,8 @@
"clear_all": "Очистить всё", "clear_all": "Очистить всё",
"image_not_found": "Изображение отсутствует", "image_not_found": "Изображение отсутствует",
"loading_error": "Произошла ошибка при загрузке данных", "loading_error": "Произошла ошибка при загрузке данных",
"products_not_found": "Товары не найдены" "products_not_found": "Товары не найдены",
"hide": "Скрыть",
"show_more": "Показать больше",
"category": "Категории"
} }

View File

@@ -425,5 +425,8 @@
"clear_all": "Barchasini tozalash", "clear_all": "Barchasini tozalash",
"image_not_found": "Rasm mavjud emas", "image_not_found": "Rasm mavjud emas",
"loading_error": "Ma'lumotlarni yuklashda xatolik yuz berdi", "loading_error": "Ma'lumotlarni yuklashda xatolik yuz berdi",
"products_not_found": "Mahsulotlar topilmadi" "products_not_found": "Mahsulotlar topilmadi",
"hide":"Yashirish",
"show_more":"Ko'proq ko'rish",
"category": "Kategoriyalar"
} }

View File

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

View File

@@ -16,8 +16,8 @@ export const endPoints = {
}, },
product: { product: {
byCategory: ({ id, currentPage }: ProductTypes) => { byCategory: ({ id, currentPage }: ProductTypes) => {
let link = "product/"; let link = "product/?page_size=12";
if (id) link += `?category=${id}`; if (id) link += `&category=${id}`;
if (currentPage) link += `&page=${currentPage}`; if (currentPage) link += `&page=${currentPage}`;
return link; return link;
@@ -29,12 +29,24 @@ export const endPoints = {
return link; return link;
}, },
byCatalogSection: ({ id, currentPage }: ProductTypes) => {
let link = "product";
if (id) link += `?catalog_section=${id}`;
if (currentPage) link += `&page=${currentPage}`;
return link;
},
detail: (id: number) => `product/${id}/`, detail: (id: number) => `product/${id}/`,
}, },
faq: "faq/", faq: "faq/",
gallery: "gallery/?page_size=500", gallery: "gallery/?page_size=500",
contact: "contact/", contact: "contact/",
statistics: "statistics/", statistics: "statistics/",
banner: "banner/?page_size=500",
navbar: "navigationitem/?page_size=500",
sertificate: "document/?type=certificate",
normative: "document/?type=normative",
guides: "guide/",
filter: { filter: {
size: "size/", size: "size/",
sizePageItems: "size/?page_size=500", sizePageItems: "size/?page_size=500",
@@ -42,6 +54,12 @@ export const endPoints = {
catalog: "catalog/", catalog: "catalog/",
catalogPageItems: "catalog/?page_size=500", catalogPageItems: "catalog/?page_size=500",
catalogCategoryId: (id: number) => `catalog/?category=${id}&page_size=500`, catalogCategoryId: (id: number) => `catalog/?category=${id}&page_size=500`,
child: ({ id }: { id?: number }) => {
const link = "catalogsection/?page_size=500";
if (id) return `${link}&parent=${id}`;
return link;
},
catalogSection: "catalogsection/?page_size=500",
}, },
post: { post: {
sendNumber: "callBack/", sendNumber: "callBack/",

13
zustand/useCatalog.ts Normal file
View File

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