Compare commits
65 Commits
6a89bc1acc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
332ff87c58 | ||
|
|
79436a9b9d | ||
|
|
f157c56b93 | ||
|
|
d03a340afb | ||
|
|
aba11a939a | ||
|
|
06ac90c391 | ||
|
|
f396125acf | ||
|
|
809438735f | ||
|
|
b838025ab0 | ||
|
|
cd7d6bb208 | ||
|
|
9cc151a796 | ||
|
|
dad1070807 | ||
|
|
a6c1e4644a | ||
|
|
2b8e86e305 | ||
|
|
1e12790e5f | ||
|
|
41ae5e4c49 | ||
|
|
2babb32e6a | ||
|
|
03ea2d51e4 | ||
|
|
aca4103213 | ||
|
|
68277d4b4c | ||
|
|
e62286effa | ||
|
|
a0f8ef76d7 | ||
|
|
11a18b52ce | ||
|
|
960010ba7b | ||
|
|
a7682a8178 | ||
|
|
8aa5ead09c | ||
|
|
bfc9b85026 | ||
|
|
1104c55bea | ||
|
|
361faf5709 | ||
|
|
9858216ae6 | ||
|
|
7d4e45d524 | ||
|
|
61013d119f | ||
|
|
4737c091be | ||
|
|
9d406d0998 | ||
|
|
d8faba0fb5 | ||
|
|
ed4363e523 | ||
|
|
6e55416fe4 | ||
|
|
afae7da68c | ||
|
|
9608ed23ac | ||
|
|
c1e70491f8 | ||
|
|
8eb434643c | ||
|
|
137dc3e7c2 | ||
|
|
974d31c096 | ||
|
|
123e6324e4 | ||
|
|
c01520399a | ||
|
|
9f46e7c244 | ||
|
|
259af77384 | ||
|
|
1d34ea1d47 | ||
|
|
91fe13f9bf | ||
|
|
7bd4dbf10f | ||
|
|
4ea825557b | ||
|
|
410a35aa4c | ||
|
|
4ee9ae3acb | ||
|
|
a7b665b50c | ||
|
|
6fbe23109c | ||
|
|
071685b52c | ||
|
|
f688a01afd | ||
|
|
1ab5c6b741 | ||
|
|
cd86d6397e | ||
|
|
dcdfce4d79 | ||
|
|
625e21394f | ||
|
|
4c2dc6a0f5 | ||
|
|
a9161b16b9 | ||
|
|
6a598ebfd3 | ||
|
|
2706dc387f |
10
app/[locale]/about/baza/page.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import NormativBazaPage from "@/components/pages/about/aboutDetail/baza";
|
||||||
|
import { Statistics } from "@/components/pages/home";
|
||||||
|
|
||||||
|
export default function Baza() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<NormativBazaPage />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
15
app/[locale]/about/layout.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { AboutBanner } from "@/components/pages/about";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function AboutLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AboutBanner />
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
app/[locale]/about/notePP/page.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { Guides } from "@/components/pages/about/aboutDetail/guides";
|
||||||
|
|
||||||
|
export default function NotePPPage() {
|
||||||
|
const t = useTranslations();
|
||||||
|
return (
|
||||||
|
<main className="min-h-[30vh] bg-[#0f0e0d] pt-5 text-white pb-40">
|
||||||
|
<div className="bg-black sm:w-[95%] w-[98%] mx-auto p-5">
|
||||||
|
<h1
|
||||||
|
className="my-15 text-center font-unbounded uppercase bg-linear-to-br from-white via-white/70 to-black
|
||||||
|
text-transparent bg-clip-text text-3xl font-bold sm:text-4xl"
|
||||||
|
>
|
||||||
|
{t("about.notePPPage.title")}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<Guides />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { AboutBanner, Story, WhyChooseUs } from "@/components/pages/about";
|
import { Story, WhyChooseUs } from "@/components/pages/about";
|
||||||
import { Statistics } from "@/components/pages/home";
|
import { Statistics } from "@/components/pages/home";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div className="mb-0">
|
<div className="mb-0">
|
||||||
<AboutBanner />
|
|
||||||
<Story />
|
<Story />
|
||||||
<Statistics/>
|
<Statistics/>
|
||||||
<WhyChooseUs/>
|
<WhyChooseUs/>
|
||||||
|
|||||||
110
app/[locale]/about/sertificate/page.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
"use client";
|
||||||
|
import { CertCardSkeleton } from "@/components/pages/about/aboutDetail/loading/loading";
|
||||||
|
import { CertCard } from "@/components/pages/about/aboutDetail/sertificateCard";
|
||||||
|
import PaginationLite from "@/components/paginationUI";
|
||||||
|
import { certs } from "@/lib/demoData";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { Award } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function SertificatePage() {
|
||||||
|
const t = useTranslations();
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ["sertificate"],
|
||||||
|
queryFn: () => httpClient(endPoints.sertificate),
|
||||||
|
select: (res) => ({
|
||||||
|
results: res.data?.data?.results,
|
||||||
|
current_page: res.data?.data?.current_page,
|
||||||
|
total_pages: res.data?.data?.total_pages,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const generallydata = data?.results || certs;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-[#0f0e0d] text-white pb-44 overflow-x-hidden">
|
||||||
|
{/* ── Hero ── */}
|
||||||
|
<section className="max-w-6xl mx-auto px-6 pt-14 pb-10">
|
||||||
|
<motion.span
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="text-[11px] font-black uppercase tracking-[0.22em] text-red-600"
|
||||||
|
>
|
||||||
|
{t("about.certificatePage.hero.label")}
|
||||||
|
</motion.span>
|
||||||
|
|
||||||
|
<motion.h1
|
||||||
|
initial={{ opacity: 0, y: 24 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.05 } as any}
|
||||||
|
className="mt-3 text-5xl md:text-7xl font-black uppercase tracking-tight leading-[0.92]"
|
||||||
|
>
|
||||||
|
{t("about.certificatePage.hero.title1")}{" "}
|
||||||
|
<span className="text-red-600">
|
||||||
|
{t("about.certificatePage.hero.title2")}
|
||||||
|
</span>
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.15 } as any}
|
||||||
|
className="mt-5 max-w-lg text-sm md:text-base text-gray-300 leading-relaxed"
|
||||||
|
>
|
||||||
|
{t("about.certificatePage.hero.description")}
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ scaleX: 0 }}
|
||||||
|
animate={{ scaleX: 1 }}
|
||||||
|
transition={{ delay: 0.3, duration: 0.7 } as any}
|
||||||
|
style={{ originX: 0 }}
|
||||||
|
className="mt-8 w-20 h-px bg-red-600"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ── Count strip ── */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="max-w-6xl mx-auto px-6 mb-10 flex items-center gap-5 border-y border-white/5 py-5"
|
||||||
|
>
|
||||||
|
<Award size={15} className="text-red-600" />
|
||||||
|
<span className="text-6xl font-black text-white/10">
|
||||||
|
{certs.length}
|
||||||
|
</span>
|
||||||
|
<p className="text-sm text-gray-400 leading-relaxed max-w-xs">
|
||||||
|
{t("about.certificatePage.count.description")}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* ── Cards ── */}
|
||||||
|
<section className="max-w-4xl mx-auto px-6 flex flex-col gap-4">
|
||||||
|
{isLoading ? (
|
||||||
|
<CertCardSkeleton />
|
||||||
|
) : (
|
||||||
|
generallydata.map((c: any, i: number) => (
|
||||||
|
<CertCard key={c.id} c={c} i={i} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/*pagination*/}
|
||||||
|
{data?.total_pages > 1 && (
|
||||||
|
<PaginationLite
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={data?.total_pages}
|
||||||
|
onChange={setCurrentPage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Breadcrumb } from "@/components/breadCrumb";
|
||||||
import Catalog from "@/components/pages/home/blog/catalog";
|
import Catalog from "@/components/pages/home/blog/catalog";
|
||||||
import { ProductBanner } from "@/components/pages/products";
|
import { ProductBanner } from "@/components/pages/products";
|
||||||
import { MainSubCategory } from "@/components/pages/subCategory";
|
import { MainSubCategory } from "@/components/pages/subCategory";
|
||||||
@@ -6,8 +7,11 @@ export default function Page() {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-[#1e1d1c] pb-30">
|
<div className="bg-[#1e1d1c] pb-30">
|
||||||
<ProductBanner />
|
<ProductBanner />
|
||||||
<div className="max-w-300 mx-auto w-full pt-20">
|
<div className="max-w-300 mx-auto w-full pt-5">
|
||||||
<MainSubCategory />
|
<div className="pb-8">
|
||||||
|
<Breadcrumb />
|
||||||
|
</div>
|
||||||
|
<Catalog />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +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 { AlertCircle } from "lucide-react";
|
|
||||||
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 { useSearchParams } from "next/dist/client/components/navigation";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
interface ProductImage {
|
interface ProductImage {
|
||||||
@@ -32,17 +32,14 @@ interface ProductDetail {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SlugPage() {
|
export default function SlugPage() {
|
||||||
const productZustand = useProductPageInfo((state) => state.product);
|
|
||||||
|
|
||||||
|
const productZustand = useProductPageInfo((state) => state.product);
|
||||||
const { data: product, isLoading } = useQuery({
|
const { data: product, isLoading } = useQuery({
|
||||||
queryKey: ["product", productZustand.id],
|
queryKey: ["product", productZustand.id],
|
||||||
queryFn: () => httpClient(endPoints.product.detail(productZustand.id)),
|
queryFn: () => httpClient(endPoints.product.detail(productZustand.id)),
|
||||||
select: (data) => data?.data?.data as ProductDetail,
|
select: (data) => data?.data?.data as ProductDetail,
|
||||||
enabled: !!productZustand.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(()=>console.log("product detail: ",product))
|
|
||||||
|
|
||||||
// Loading State
|
// Loading State
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <LoadingSkeleton />;
|
return <LoadingSkeleton />;
|
||||||
@@ -55,12 +52,16 @@ export default function SlugPage() {
|
|||||||
|
|
||||||
// Extract images
|
// Extract images
|
||||||
const productImages = product.images?.map((img) => img.image) || [];
|
const productImages = product.images?.map((img) => img.image) || [];
|
||||||
const mainImage = product.images?.find((img) => img.is_main)?.image || productImages[0] || "";
|
const mainImage =
|
||||||
const features = product.features.map((item:any)=>item.name)
|
product.images?.find((img) => img.is_main)?.image || productImages[0] || "";
|
||||||
|
const features = product.features.map((item: any) => item.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#1e1d1c] py-20 md:py-32 lg:py-40 px-4 md:px-8">
|
<div className="min-h-screen bg-[#1e1d1c] px-4 md:px-8 pb-35">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="min-[400px]:pt-35 pt-45 pb-10">
|
||||||
|
<Breadcrumb />
|
||||||
|
</div>
|
||||||
{/* Main Product Section */}
|
{/* Main Product Section */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 mb-12">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 mb-12">
|
||||||
{/* Left - Image Slider */}
|
{/* Left - Image Slider */}
|
||||||
17
app/[locale]/catalog_page/products/page.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
"use client";
|
||||||
|
import { Breadcrumb } from "@/components/breadCrumb";
|
||||||
|
import { ProductBanner, Products } from "@/components/pages/products";
|
||||||
|
import { useSubCategory } from "@/zustand/useSubCategory";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const subCategory = useSubCategory((state) => state.subCategory);
|
||||||
|
return (
|
||||||
|
<div className="bg-[#1e1d1c] pb-30">
|
||||||
|
<ProductBanner />
|
||||||
|
<div className="max-w-300 w-full mx-auto pt-4">
|
||||||
|
<Breadcrumb customLabels={{ subCategory: subCategory.name }} />
|
||||||
|
</div>
|
||||||
|
<Products />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Breadcrumb } from "@/components/breadCrumb";
|
||||||
import { ProductBanner } from "@/components/pages/products";
|
import { ProductBanner } from "@/components/pages/products";
|
||||||
import { MainSubCategory } from "@/components/pages/subCategory";
|
import { MainSubCategory } from "@/components/pages/subCategory";
|
||||||
|
|
||||||
@@ -5,7 +6,10 @@ export default function Page() {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-[#1e1d1c] pb-30">
|
<div className="bg-[#1e1d1c] pb-30">
|
||||||
<ProductBanner />
|
<ProductBanner />
|
||||||
<div className="py-20">
|
<div className="pb-20">
|
||||||
|
<div className="max-w-350 mx-auto w-full py-10">
|
||||||
|
<Breadcrumb />
|
||||||
|
</div>
|
||||||
<MainSubCategory />
|
<MainSubCategory />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -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 />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { ProductBanner, Products } from "@/components/pages/products";
|
|
||||||
import FilterCatalog from "@/components/pages/products/filter/catalog/filterCatalog";
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return (
|
|
||||||
<div className="bg-[#1e1d1c] pb-30">
|
|
||||||
<ProductBanner />
|
|
||||||
{/* <FilterCatalog /> */}
|
|
||||||
<Products />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
5
app/[locale]/services/detail/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { InitialLoading } from "@/components/initialLoading/initialLoading";
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <InitialLoading />;
|
||||||
|
}
|
||||||
164
app/[locale]/services/detail/page.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import {
|
||||||
|
SystemFeature,
|
||||||
|
} from "@/lib/api/demoapi/operationalSystems";
|
||||||
|
import { Breadcrumb } from "@/components/breadCrumb";
|
||||||
|
import { useServiceDetail } from "@/zustand/useService";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import { cardVariants, containerVariants } from "@/lib/animations";
|
||||||
|
|
||||||
|
export default function OperationalSystemsPage() {
|
||||||
|
const t = useTranslations("operationalSystems");
|
||||||
|
const serviceId = useServiceDetail((state) => state.serviceId);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
isLoading: loading,
|
||||||
|
isError: error,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["firesafety", serviceId],
|
||||||
|
queryFn: () => httpClient(endPoints.services.detail(serviceId || 0)),
|
||||||
|
select: (data) => data?.data?.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Demo data - fallback ma'lumotlar
|
||||||
|
const demoData: SystemFeature[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
title: t("systems.sprinkler.title"),
|
||||||
|
shortDesc: t("systems.sprinkler.short-desc"),
|
||||||
|
description: t("systems.sprinkler.description"),
|
||||||
|
features: [
|
||||||
|
t("systems.sprinkler.features.0"),
|
||||||
|
t("systems.sprinkler.features.1"),
|
||||||
|
t("systems.sprinkler.features.2"),
|
||||||
|
t("systems.sprinkler.features.3"),
|
||||||
|
],
|
||||||
|
image: "/images/services/sprinkler.jpg",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Loading state
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-black">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="text-center space-y-6"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
|
||||||
|
className="w-16 h-16 border-4 border-red-500 border-t-transparent rounded-full mx-auto"
|
||||||
|
/>
|
||||||
|
<p className="text-white text-xl font-medium font-unbounded">
|
||||||
|
{t("loading")}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error state with retry
|
||||||
|
if (error && !data) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-black px-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="text-center space-y-6 max-w-md"
|
||||||
|
>
|
||||||
|
<div className="text-7xl">⚠️</div>
|
||||||
|
<p className="text-white text-xl font-medium">{t("error")}</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pt-20 md:pt-30 pb-35 max-w-6xl mx-auto w-full px-4">
|
||||||
|
<div className="mb-5">
|
||||||
|
<Breadcrumb />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
{!data || data.legth === 0 ? (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="text-center py-20"
|
||||||
|
>
|
||||||
|
<p className="text-gray-400 text-xl font-unbounded">{t("noData")}</p>
|
||||||
|
</motion.div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
className="space-y-12 md:space-y-20"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
variants={cardVariants}
|
||||||
|
whileHover={{ y: -8 }}
|
||||||
|
className={`flex flex-col gap-8 md:gap-12 items-center`}
|
||||||
|
>
|
||||||
|
{/* Image Section */}
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.03 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="w-full relative h-64 md:h-80 lg:h-96 rounded-xl overflow-hidden"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={data?.detail_image}
|
||||||
|
alt={data.title}
|
||||||
|
fill
|
||||||
|
className="object-cover w-full h-auto"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-linear-to-t from-black/10 to-transparent" />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Content Section */}
|
||||||
|
<div className="w-full space-y-6">
|
||||||
|
<motion.h2
|
||||||
|
initial={{ opacity: 0, x: data?.id % 2 === 0 ? -20 : 20 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="text-2xl md:text-3xl lg:text-4xl font-bold text-white font-almarai"
|
||||||
|
>
|
||||||
|
{data.title}
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, x: data?.id % 2 === 0 ? -20 : 20 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.1 }}
|
||||||
|
className="text-gray-300 text-sm md:text-base leading-relaxed font-unbounded"
|
||||||
|
>
|
||||||
|
{data.subtitle}
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, x: data?.id % 2 === 0 ? -20 : 20 }}
|
||||||
|
whileInView={{ opacity: 1, x: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.1 }}
|
||||||
|
className="text-gray-300 text-sm md:text-base leading-relaxed font-unbounded"
|
||||||
|
>
|
||||||
|
{data?.description}
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
app/[locale]/services/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { InitialLoading } from "@/components/initialLoading/initialLoading";
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <InitialLoading />;
|
||||||
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
import { OurService, Video } from "@/components/pages/home";
|
import { Video } from "@/components/pages/home";
|
||||||
import { ServiceBanner, ServiceFaq } from "@/components/pages/services";
|
import {
|
||||||
|
ServiceBanner,
|
||||||
|
ServiceFaq,
|
||||||
|
ServicePageServices,
|
||||||
|
} from "@/components/pages/services";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
<ServiceBanner />
|
<ServiceBanner />
|
||||||
<OurService />
|
<ServicePageServices />
|
||||||
<Video />
|
<Video />
|
||||||
<ServiceFaq />
|
<ServiceFaq />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
BIN
app/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -157,3 +157,9 @@ body {
|
|||||||
.delay-300 {
|
.delay-300 {
|
||||||
animation-delay: 300ms;
|
animation-delay: 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leo{
|
||||||
|
color: #979797;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* jvjjjjvjvj */
|
||||||
147
app/layout.tsx
@@ -6,8 +6,7 @@ import { NextIntlClientProvider } from "next-intl";
|
|||||||
import { getMessages } from "next-intl/server";
|
import { getMessages } from "next-intl/server";
|
||||||
import { InitialLoading } from "@/components/initialLoading/initialLoading";
|
import { InitialLoading } from "@/components/initialLoading/initialLoading";
|
||||||
import { Providers } from "@/components/provider";
|
import { Providers } from "@/components/provider";
|
||||||
|
import Script from "next/script";
|
||||||
("info@ignum-tech.com");
|
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -19,48 +18,148 @@ const geistMono = Geist_Mono({
|
|||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
// openGraphData tipini aniq belgilaymiz
|
||||||
title: "FireForce - Emergency Services",
|
const openGraphData: Record<
|
||||||
|
"uz" | "ru" | "en",
|
||||||
|
{ title: string; description: string; locale: string }
|
||||||
|
> = {
|
||||||
|
uz: {
|
||||||
|
title: "Ignum Technologies - Professional Fire Safety Systems",
|
||||||
description:
|
description:
|
||||||
"FireForce - Your trusted emergency response team bringing calm amidst chaos",
|
"Tijorat va uy-joy ob’ektlari uchun yong‘in aniqlash, bostirish va signalizatsiya tizimlarini o‘z ichiga olgan to‘liq yong‘in himoyasi yechimlari.",
|
||||||
generator: "v0.app",
|
locale: "uz_UZ",
|
||||||
|
},
|
||||||
|
ru: {
|
||||||
|
title:
|
||||||
|
"Ignum Technologies - Профессиональные системы пожарной безопасности",
|
||||||
|
description:
|
||||||
|
"Полные решения по пожарной защите, включая системы обнаружения, тушения и сигнализации для коммерческих и жилых объектов.",
|
||||||
|
locale: "ru_RU",
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
title: "Ignum Technologies - Professional Fire Safety Systems",
|
||||||
|
description:
|
||||||
|
"Comprehensive fire protection solutions including detection, suppression, and alarm systems for commercial and residential properties.",
|
||||||
|
locale: "en_US",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: {
|
||||||
|
default: "Ignum Technologies - Fire Safety Systems Installation & Sales",
|
||||||
|
template: "%s | Ignum Technologies",
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
"Ignum Technologies specializes in professional fire safety systems installation and sales. Protect your property with cutting-edge fire detection, suppression, and alarm systems from certified experts.",
|
||||||
|
keywords: [
|
||||||
|
"fire safety systems",
|
||||||
|
"fire alarm installation",
|
||||||
|
"fire suppression systems",
|
||||||
|
"fire detection",
|
||||||
|
"Ignum Technologies",
|
||||||
|
"fire safety equipment",
|
||||||
|
"fire protection services",
|
||||||
|
"commercial fire systems",
|
||||||
|
"residential fire safety",
|
||||||
|
],
|
||||||
|
authors: [{ name: "Ignum Technologies" }],
|
||||||
|
creator: "Ignum Technologies",
|
||||||
|
publisher: "Ignum Technologies",
|
||||||
|
formatDetection: { email: false, address: false, telephone: false },
|
||||||
|
metadataBase: new URL("https://ignum-tech.com"),
|
||||||
|
alternates: { canonical: "/" },
|
||||||
icons: {
|
icons: {
|
||||||
icon: [
|
icon: [
|
||||||
{
|
{ url: "/icon-light-32x32.png", media: "(prefers-color-scheme: light)" },
|
||||||
url: "/icon-light-32x32.png",
|
{ url: "/icon-dark-32x32.png", media: "(prefers-color-scheme: dark)" },
|
||||||
media: "(prefers-color-scheme: light)",
|
{ url: "/icon.svg", type: "image/svg+xml" },
|
||||||
},
|
|
||||||
{
|
|
||||||
url: "/icon-dark-32x32.png",
|
|
||||||
media: "(prefers-color-scheme: dark)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: "/icon.svg",
|
|
||||||
type: "image/svg+xml",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
apple: "/apple-icon.png",
|
apple: "/apple-icon.png",
|
||||||
},
|
},
|
||||||
|
verification: { google: "your-google-verification-code" },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
params,
|
params,
|
||||||
}: Readonly<{
|
}: Readonly<{ children: React.ReactNode; params: any }>) {
|
||||||
children: React.ReactNode;
|
|
||||||
params: any;
|
|
||||||
}>) {
|
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const messages: any = await getMessages();
|
const messages: any = await getMessages();
|
||||||
|
|
||||||
|
// Locale ga mos Open Graph ma'lumotini tanlaymiz
|
||||||
|
const og = openGraphData[locale as "uz" | "ru" | "en"] || openGraphData["uz"];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale} suppressHydrationWarning>
|
<html lang={locale} suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<meta name="theme-color" content="#FF4500" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Ignum Tech" />
|
||||||
|
|
||||||
|
{/* Open Graph */}
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:locale" content={og.locale} />
|
||||||
|
<meta property="og:url" content="https://ignum-tech.com" />
|
||||||
|
<meta property="og:site_name" content="Ignum Technologies" />
|
||||||
|
<meta property="og:title" content={og.title} />
|
||||||
|
<meta property="og:description" content={og.description} />
|
||||||
|
<meta property="og:image" content="/og-image.jpg" />
|
||||||
|
<meta property="og:image:width" content="1200" />
|
||||||
|
<meta property="og:image:height" content="630" />
|
||||||
|
<meta
|
||||||
|
property="og:image:alt"
|
||||||
|
content="Ignum Technologies - Fire Safety Systems"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Twitter */}
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content={og.title} />
|
||||||
|
<meta name="twitter:description" content={og.description} />
|
||||||
|
<meta name="twitter:image" content="/twitter-image.jpg" />
|
||||||
|
<meta name="twitter:creator" content="@ignumtech" />
|
||||||
|
|
||||||
|
{/* Yandex Metrika */}
|
||||||
|
<Script
|
||||||
|
id="yandex-metrika"
|
||||||
|
strategy="afterInteractive"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};
|
||||||
|
m[i].l=1*new Date();
|
||||||
|
for (var j = 0; j < document.scripts.length; j++) {if (document.scripts[j].src === r) { return; }}
|
||||||
|
k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})
|
||||||
|
(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");
|
||||||
|
|
||||||
|
ym(106736850, "init", {
|
||||||
|
clickmap:true,
|
||||||
|
trackLinks:true,
|
||||||
|
accurateTrackBounce:true,
|
||||||
|
webvisor:true,
|
||||||
|
ecommerce:"dataLayer"
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<noscript>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
src="https://mc.yandex.ru/watch/106736850"
|
||||||
|
style={{ position: "absolute", left: "-9999px" }}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
</head>
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
<InitialLoading />
|
{/* <InitialLoading />
|
||||||
<NextIntlClientProvider messages={messages} locale={locale}>
|
<NextIntlClientProvider messages={messages} locale={locale}>
|
||||||
<Providers>{children}</Providers>
|
<Providers>{children}</Providers>
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider> */}
|
||||||
|
{children}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 />;
|
||||||
}
|
}
|
||||||
|
|||||||
195
components/breadCrumb.tsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
"use client";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { ChevronRight, Home } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useCategory } from "@/zustand/useCategory";
|
||||||
|
import { useSubCategory } from "@/zustand/useSubCategory";
|
||||||
|
import { useProductPageInfo } from "@/zustand/useProduct";
|
||||||
|
|
||||||
|
interface BreadcrumbProps {
|
||||||
|
customLabels?: Record<string, string>;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Breadcrumb({
|
||||||
|
customLabels = {},
|
||||||
|
className = "",
|
||||||
|
}: BreadcrumbProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const t = useTranslations();
|
||||||
|
const category = useCategory((state) => state.category);
|
||||||
|
const subCategory = useSubCategory((state) => state.subCategory);
|
||||||
|
const product = useProductPageInfo((state) => state.product);
|
||||||
|
|
||||||
|
// Pathdan segments olish
|
||||||
|
const segments = pathname.split("/").filter((segment) => segment !== "");
|
||||||
|
|
||||||
|
// Agar locale bo'lsa, uni olib tashlash (uz, en, ru)
|
||||||
|
const locales = ["uz", "en", "ru"];
|
||||||
|
const filteredSegments = segments.filter(
|
||||||
|
(segment) => !locales.includes(segment),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Agar faqat home page bo'lsa, breadcrumb ko'rsatmaslik
|
||||||
|
if (filteredSegments.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Breadcrumb items yaratish
|
||||||
|
const breadcrumbItems: Array<{
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
isLast: boolean;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
// Home qo'shish (har doim birinchi)
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: t("breadcrumb.home") || "Home",
|
||||||
|
href: "/",
|
||||||
|
isLast: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Locale olish
|
||||||
|
const locale = segments.find((seg) => locales.includes(seg)) || "";
|
||||||
|
const localePrefix = locale ? `/${locale}` : "";
|
||||||
|
|
||||||
|
// Segmentlarni tahlil qilish
|
||||||
|
filteredSegments.forEach((segment, index) => {
|
||||||
|
const isLast = index === filteredSegments.length - 1;
|
||||||
|
|
||||||
|
if (segment === "catalog_page") {
|
||||||
|
// Catalog_page - asosiy kategoriyalar sahifasi
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: t("breadcrumb.catalog_page") || "Katalog",
|
||||||
|
href: `${localePrefix}/catalog_page`,
|
||||||
|
isLast: isLast,
|
||||||
|
});
|
||||||
|
} else if (segment === "subCategory") {
|
||||||
|
// SubCategory - kategoriya nomi ko'rsatiladi
|
||||||
|
if (category?.name) {
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: category.name,
|
||||||
|
href: `${localePrefix}/catalog_page/subCategory`,
|
||||||
|
isLast: isLast,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (segment === "products") {
|
||||||
|
if (subCategory?.name) {
|
||||||
|
// Agar subCategory orqali kelgan bo'lsa
|
||||||
|
// 1. Kategoriya
|
||||||
|
if (category?.name) {
|
||||||
|
const categoryInBreadcrumb = breadcrumbItems.find(
|
||||||
|
(item) => item.label === category.name,
|
||||||
|
);
|
||||||
|
if (!categoryInBreadcrumb) {
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: category.name,
|
||||||
|
href: `${localePrefix}/catalog_page/subCategory`,
|
||||||
|
isLast: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. SubKategoriya
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: subCategory.name,
|
||||||
|
href: `${localePrefix}/catalog_page/subCategory`,
|
||||||
|
isLast: false,
|
||||||
|
});
|
||||||
|
} else if (category?.name) {
|
||||||
|
// To'g'ridan-to'g'ri kategoriyadan products ga kelgan
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: category.name,
|
||||||
|
href: `${localePrefix}/catalog_page`,
|
||||||
|
isLast: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (segment.startsWith("[") && segment.endsWith("]")) {
|
||||||
|
// Dynamic route (masalan, [slug])
|
||||||
|
// Custom label yoki default
|
||||||
|
const slugValue = segment.replace(/\[|\]/g, "");
|
||||||
|
const label = customLabels[slugValue] || slugValue;
|
||||||
|
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: label,
|
||||||
|
href: `${localePrefix}/${filteredSegments.slice(0, index + 1).join("/")}`,
|
||||||
|
isLast: isLast,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Boshqa segmentlar
|
||||||
|
const label = getLabel(segment);
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: label,
|
||||||
|
href: `${localePrefix}/${filteredSegments.slice(0, index + 1).join("/")}`,
|
||||||
|
isLast: isLast,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default label translator
|
||||||
|
function getLabel(segment: string): string {
|
||||||
|
// Agar custom label berilgan bo'lsa
|
||||||
|
if (customLabels[segment]) {
|
||||||
|
return customLabels[segment];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agar translation mavjud bo'lsa
|
||||||
|
try {
|
||||||
|
if (segment === "special_product") {
|
||||||
|
return product.name;
|
||||||
|
}
|
||||||
|
if(segment === 'detail') return '';
|
||||||
|
return t(`breadcrumb.${segment}`);
|
||||||
|
} catch {
|
||||||
|
// Aks holda, segment nomini formatlash
|
||||||
|
return segment
|
||||||
|
.replace(/-/g, " ")
|
||||||
|
.replace(/_/g, " ")
|
||||||
|
.split(" ")
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav aria-label="Breadcrumb" className={`py-4 ${className} sm:px-5 px-2`}>
|
||||||
|
<ol className="flex items-center flex-wrap gap-2 sm:text-xl text-lg">
|
||||||
|
{breadcrumbItems.map((item, index) => (
|
||||||
|
<li
|
||||||
|
key={`${item.label}-${index}`}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{index > 0 && (
|
||||||
|
<ChevronRight className="w-4 h-4 text-gray-200" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{index === 0 ? (
|
||||||
|
// Home link with icon
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
className="flex items-center gap-1.5 text-gray-200 hover:text-red-600 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
<Home className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
) : item.isLast ? (
|
||||||
|
// Last item (current page)
|
||||||
|
<span className=" text-gray-200 font-medium line-clamp-1">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
// Regular link
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
className="text-gray-200 hover:text-red-600 transition-colors duration-200 line-clamp-1"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,20 +23,23 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
}
|
|
||||||
|
|
||||||
.initial-svg {
|
|
||||||
width: 250px;
|
width: 250px;
|
||||||
height: 250px;
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initial-loading-content svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
animation: initialFloat 2s ease-in-out infinite, initialScale 1.5s ease-in-out infinite;
|
animation: initialFloat 2s ease-in-out infinite, initialScale 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.initial-path {
|
/* SVG path'larga stil - tiniq va aniq */
|
||||||
fill: url(#initial-gradient);
|
.logo-path {
|
||||||
filter: url(#initial-glow);
|
fill: url(#neon-gradient);
|
||||||
stroke: #ffffff;
|
stroke: #ffffff;
|
||||||
stroke-width: 2;
|
stroke-width: 0.3;
|
||||||
animation: pathPulse 1.5s ease-in-out infinite;
|
filter: drop-shadow(0 0 3px rgba(255, 255, 255, 0.3));
|
||||||
|
animation: pathPulse 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading dots animation */
|
/* Loading dots animation */
|
||||||
@@ -53,8 +56,9 @@
|
|||||||
.loading-dots span {
|
.loading-dots span {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
background: #ff0000;
|
background: linear-gradient(135deg, #e0e0e0 0%, #ffffff 50%, #c0c0c0 100%);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 5px rgba(255, 255, 255, 0.3);
|
||||||
animation: dotBounce 1.4s ease-in-out infinite;
|
animation: dotBounce 1.4s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +80,7 @@
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
transform: translateY(-30px);
|
transform: translateY(-15px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,18 +89,18 @@
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
transform: scale(1.1);
|
transform: scale(1.03);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pathPulse {
|
@keyframes pathPulse {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
stroke-width: 2;
|
filter: drop-shadow(0 0 3px rgba(255, 255, 255, 0.3));
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
opacity: 0.7;
|
opacity: 0.95;
|
||||||
stroke-width: 3;
|
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.4));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,3 +119,23 @@
|
|||||||
body:has(.initial-loading:not(.fade-out)) {
|
body:has(.initial-loading:not(.fade-out)) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.initial-loading-content {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.initial-loading-content {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-dots span {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,18 +3,18 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Mail, Phone, MapPin } from "lucide-react";
|
import { Mail, Phone, MapPin, Instagram, Send } from "lucide-react";
|
||||||
import { useLocale, useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import axios from "axios";
|
|
||||||
import httpClient from "@/request/api";
|
import httpClient from "@/request/api";
|
||||||
import { endPoints } from "@/request/links";
|
import { endPoints } from "@/request/links";
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
const [errors, setErrors] = useState<any>({});
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [subscribed, setSubscribed] = useState(false);
|
const [subscribed, setSubscribed] = useState(false);
|
||||||
@@ -34,10 +34,36 @@ export function Footer() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formatPhoneNumber = (value: string) => {
|
||||||
|
const numbers = value.replace(/\D/g, "");
|
||||||
|
if (!numbers.startsWith("998")) {
|
||||||
|
return "+998 ";
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted = "+998 ";
|
||||||
|
const rest = numbers.slice(3);
|
||||||
|
if (rest.length > 0) formatted += rest.slice(0, 2);
|
||||||
|
if (rest.length > 2) formatted += " " + rest.slice(2, 5);
|
||||||
|
if (rest.length > 5) formatted += " " + rest.slice(5, 7);
|
||||||
|
if (rest.length > 7) formatted += " " + rest.slice(7, 9);
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const formatted = formatPhoneNumber(e.target.value);
|
||||||
|
setEmail(formatted);
|
||||||
|
if (errors.address) {
|
||||||
|
setErrors({ ...errors, address: "" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubscribe = (e: React.FormEvent) => {
|
const handleSubscribe = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (email) {
|
if (email) {
|
||||||
formRequest.mutate({ number: email });
|
// Telefon raqamni tozalash (faqat raqamlar)
|
||||||
|
const cleanPhone = email.replace(/\D/g, "");
|
||||||
|
formRequest.mutate({ number: Number(cleanPhone.slice(3)) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,10 +75,10 @@ export function Footer() {
|
|||||||
"linear-gradient(to top right, #452811 0%, #000000 20%, #000000 40%, #000000 60%, #000000 80%, #000000 100%)",
|
"linear-gradient(to top right, #452811 0%, #000000 20%, #000000 40%, #000000 60%, #000000 80%, #000000 100%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Newsletter Section for gitea */}
|
{/* Newsletter Section */}
|
||||||
<div className=" absolute w-full -top-40 px-4 py-12 md:py-16">
|
<div className=" absolute w-full -top-40 px-4 py-12 md:py-16">
|
||||||
<div className="mx-auto max-w-6xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
<div className="rounded-2xl bg-[#fa1d1d] px-6 py-8 md:flex lg:flex-row flex-col max-lg:gap-5 md:items-center lg:justify-between justify-center md:px-10 md:py-12">
|
<div className="rounded-2xl bg-red-600 px-6 py-8 md:flex lg:flex-row flex-col max-lg:gap-5 md:items-center lg:justify-between justify-center md:px-10 md:py-12">
|
||||||
<div className="mb-8 md:mb-0">
|
<div className="mb-8 md:mb-0">
|
||||||
<h2 className="font-unbounded text-2xl font-bold text-white md:text-3xl">
|
<h2 className="font-unbounded text-2xl font-bold text-white md:text-3xl">
|
||||||
{t("contactTitle")}
|
{t("contactTitle")}
|
||||||
@@ -67,14 +93,16 @@ export function Footer() {
|
|||||||
className="flex sm:flex-row flex-col w-full gap-2 md:w-auto"
|
className="flex sm:flex-row flex-col w-full gap-2 md:w-auto"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="tel"
|
||||||
placeholder={t("enterPhone")}
|
id="phone"
|
||||||
|
name="phone"
|
||||||
value={email}
|
value={email}
|
||||||
minLength={9}
|
onChange={handlePhoneChange}
|
||||||
maxLength={13}
|
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
errors.email ? "border-red-500" : "border-transparent"
|
||||||
className="font-almarai flex-1 rounded-full bg-white px-6 py-3 text-gray-800 placeholder-gray-400 focus:outline-none md:w-64"
|
}`}
|
||||||
required
|
placeholder="+998 90 123 45 67"
|
||||||
|
maxLength={17}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -107,6 +135,20 @@ export function Footer() {
|
|||||||
<p className="font-almarai text-sm leading-relaxed text-gray-300">
|
<p className="font-almarai text-sm leading-relaxed text-gray-300">
|
||||||
{t("footer.description")}
|
{t("footer.description")}
|
||||||
</p>
|
</p>
|
||||||
|
<div className="flex items-center gap-5 mt-5">
|
||||||
|
{/* <a
|
||||||
|
href=""
|
||||||
|
className="p-2 rounded-md bg-gray-700 hover:bg-red-500"
|
||||||
|
>
|
||||||
|
<Instagram />
|
||||||
|
</a> */}
|
||||||
|
<a
|
||||||
|
href="https://t.me/ignum_tech"
|
||||||
|
className="p-2 rounded-md bg-gray-700 hover:bg-red-500"
|
||||||
|
>
|
||||||
|
<Send />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Links */}
|
{/* Quick Links */}
|
||||||
@@ -187,9 +229,7 @@ export function Footer() {
|
|||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-3">
|
<li className="flex items-start gap-3">
|
||||||
<MapPin className="mt-1 h-5 w-5 shrink-0 text-white" />
|
<MapPin className="mt-1 h-5 w-5 shrink-0 text-white" />
|
||||||
<span>
|
<span>{t("footer.address")}</span>
|
||||||
{t("footer.address")}
|
|
||||||
</span>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,18 +238,30 @@ export function Footer() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Copyright Section */}
|
{/* Copyright Section */}
|
||||||
<div className="border-t border-gray-800 px-4 py-8">
|
<div className="border-t border-gray-800 px-4 py-6">
|
||||||
<div className="mx-auto max-w-6xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
<div className="font-almarai flex flex-col justify-between gap-4 text-sm text-gray-400 md:flex-row md:items-center">
|
<div className="font-almarai flex flex-col justify-end items-end w-full gap-4 text-lg text-gray-400 md:flex-row md:items-center">
|
||||||
<div>Copyright © 2025 Ignum Company.</div>
|
{locale === "uz" ? (
|
||||||
<div className="flex gap-6">
|
<div className="flex gap-2 w-full justify-center items-end ">
|
||||||
<a href="#terms" className="hover:text-white">
|
<a
|
||||||
Terms & Conditions
|
href="http://felix-its.uz/"
|
||||||
|
className="hover:text-red-600 hover:cursor-pointer text-blue-300 underline"
|
||||||
|
>
|
||||||
|
Felix-its.uz
|
||||||
</a>
|
</a>
|
||||||
<a href="#privacy" className="hover:text-white">
|
<p>- Jamoasi tomonidan ishlab chiqilgan</p>
|
||||||
Privacy Policy
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex w-full justify-center items-end gap-2">
|
||||||
|
{locale === "ru" ? <p>Разработано -</p> : <p>Created by - </p>}
|
||||||
|
<a
|
||||||
|
href="http://felix-its.uz/"
|
||||||
|
className="hover:text-red-600 hover:cursor-pointer text-blue-300 underline"
|
||||||
|
>
|
||||||
|
Felix-its.uz
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,15 +5,34 @@ import { ChevronDown, Phone, Menu, X } from "lucide-react";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import LanguageSelectRadix from "../languageSwitcher";
|
import LanguageSelectRadix from "../languageSwitcher";
|
||||||
import { useLocale, useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import NavbarLogo from "./navbarLogo/navbarLogo";
|
import UpHeader from "./upHeader";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "../ui/dropdown-menu";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import { NavbarItem } from "@/lib/types";
|
||||||
|
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
const [scrolled, setScrolled] = useState(false);
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
|
||||||
|
const { data: navbarItems } = useQuery({
|
||||||
|
queryKey: ["navbaritem",locale],
|
||||||
|
queryFn: () => httpClient(endPoints.navbar),
|
||||||
|
select: (data: any) => ({
|
||||||
|
results: data?.data?.data?.results,
|
||||||
|
total_items: data?.data?.data?.total_items,
|
||||||
|
total_pages: data?.data?.data?.total_pages,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
setScrolled(window.scrollY > 50);
|
setScrolled(window.scrollY > 50);
|
||||||
@@ -36,57 +55,73 @@ export function Navbar() {
|
|||||||
<nav
|
<nav
|
||||||
className={`fixed top-0 left-0 right-0 z-50 border-b border-gray-400/50 ${scrolled && "bg-black"} transition`}
|
className={`fixed top-0 left-0 right-0 z-50 border-b border-gray-400/50 ${scrolled && "bg-black"} transition`}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
className={`overflow-hidden transition-all duration-500 ease-in-out ${
|
||||||
|
scrolled
|
||||||
|
? "max-h-0 opacity-0 -translate-y-2"
|
||||||
|
: "max-h-20 opacity-100 translate-y-0"
|
||||||
|
}`}
|
||||||
|
style={{ transform: scrolled ? "translateY(-8px)" : "translateY(0)" }}
|
||||||
|
>
|
||||||
|
<UpHeader />
|
||||||
|
</div>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex justify-between items-center h-20">
|
<div className="flex justify-between items-center h-20">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link href={`/${locale}/home`} className="hover:cursor-pointer">
|
<Link href={`/${locale}/home`} className="hover:cursor-pointer">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className=" flex items-center justify-center">
|
<div className=" flex items-center justify-center">
|
||||||
<Image src={'/images/IGNUM/PNG/1.@6x.png'} alt="logo image" width={80} height={80} />
|
<Image
|
||||||
|
src={"/images/IGNUM/PNG/1.@6x.png"}
|
||||||
|
alt="logo image"
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Desktop Navigation Menu */}
|
{/* Desktop Navigation Menu */}
|
||||||
<div className="hidden h-full lg:flex items-center gap-8">
|
<div className="hidden h-full lg:flex items-center gap-8">
|
||||||
|
{navbarItems?.results ? (
|
||||||
|
navbarItems.results.map((item: NavbarItem) => (
|
||||||
|
<DropdownMenu key={item.id}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Link
|
||||||
|
key={item.id}
|
||||||
|
href={`/${locale}/${item.url}`}
|
||||||
|
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
{item.children.length > 0 && (
|
||||||
|
<ChevronDown size={12} className="ml-1" />
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
{item.children.length > 0 && (
|
||||||
|
<DropdownMenuContent className="space-y-2">
|
||||||
|
{item.children.map((child: NavbarItem) => (
|
||||||
|
<DropdownMenuItem asChild key={child.id}>
|
||||||
|
<Link
|
||||||
|
href={`/${locale}/${child.url}`}
|
||||||
|
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
||||||
|
>
|
||||||
|
{child.name}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
)}
|
||||||
|
</DropdownMenu>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
<Link
|
<Link
|
||||||
href={`/${locale}/home`}
|
href={`/${locale}/home`}
|
||||||
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
||||||
>
|
>
|
||||||
{t("navbar.home")}
|
{t("navbar.home")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
)}
|
||||||
href={`/${locale}/about`}
|
|
||||||
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
|
||||||
>
|
|
||||||
{t("navbar.about")}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/faq`}
|
|
||||||
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
|
||||||
>
|
|
||||||
{t("navbar.faq")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/services`}
|
|
||||||
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
|
||||||
>
|
|
||||||
{t("navbar.services")}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/catalog_page`}
|
|
||||||
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
|
||||||
>
|
|
||||||
{t("navbar.products")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/contact`}
|
|
||||||
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
|
||||||
>
|
|
||||||
{t("navbar.contact")}
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-5">
|
<div className="flex items-center gap-5">
|
||||||
@@ -103,7 +138,8 @@ export function Navbar() {
|
|||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-white text-sm font-bold">
|
<div className="text-white text-sm font-bold">
|
||||||
<div>+998-98-099-21-21</div> <div>+998-77-372-21-21</div>
|
<div>+998-55-055-21-21</div>
|
||||||
|
<div>+998-77-372-21-21</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,51 +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">
|
||||||
|
{navbarItems?.results ? (
|
||||||
|
navbarItems.results.map((item: NavbarItem) => (
|
||||||
|
<DropdownMenu key={item.id}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Link
|
||||||
|
key={item.id}
|
||||||
|
href={`/${locale}/${item.url}`}
|
||||||
|
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
{item.children.length > 0 && (
|
||||||
|
<ChevronDown size={12} className="ml-1" />
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
{item.children.length > 0 && (
|
||||||
|
<DropdownMenuContent className="space-y-2">
|
||||||
|
{item.children.map((child: NavbarItem) => (
|
||||||
|
<Link
|
||||||
|
key={child.id}
|
||||||
|
href={`/${locale}/${child.url}`}
|
||||||
|
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
||||||
|
>
|
||||||
|
{child.name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
)}
|
||||||
|
</DropdownMenu>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
<Link
|
<Link
|
||||||
href={`/${locale}/home`}
|
href={`/${locale}/home`}
|
||||||
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
|
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
|
||||||
>
|
>
|
||||||
{t("navbar.home")}
|
{t("navbar.home")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
)}
|
||||||
href={`/${locale}/about`}
|
|
||||||
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
|
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
|
||||||
>
|
|
||||||
{t("navbar.about")}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* Mobile Pages Dropdown */}
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/faq`}
|
|
||||||
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
|
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
|
||||||
>
|
|
||||||
{t("navbar.faq")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/services`}
|
|
||||||
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
|
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
|
||||||
>
|
|
||||||
{t("navbar.services")}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/catalog_page`}
|
|
||||||
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
|
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
|
||||||
>
|
|
||||||
{t("navbar.products")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/contact`}
|
|
||||||
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
|
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
|
||||||
>
|
|
||||||
{t("navbar.contact")}
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
51
components/layout/upHeader.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { Download, Mail, Phone, Send } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
const downloadCatalog = () => {
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = "/catalog.pdf";
|
||||||
|
link.download = "catalog.pdf";
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function UpHeader() {
|
||||||
|
const t = useTranslations();
|
||||||
|
return (
|
||||||
|
<div className="w-full border-b border-gray-400/50">
|
||||||
|
<div className="max-w-7xl mx-auto py-2 px-4 sm:px-6 lg:px-8 flex max-[450px]:flex-col justify-between gap-2 items-center">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p className="text-white font-medium">{t("navbar.connect")}:</p>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<a
|
||||||
|
href="mailto:support@fireforce.com"
|
||||||
|
className="p-1 rounded-sm text-white hover:text-white hover:border-red-700 hover:bg-red-700 transition"
|
||||||
|
>
|
||||||
|
<Mail size={20} />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="tel:+998773722121"
|
||||||
|
className="p-1 rounded-sm text-white hover:text-white hover:border-red-700 hover:bg-red-700 transition"
|
||||||
|
>
|
||||||
|
<Phone size={20} />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://t.me/ignum_tech"
|
||||||
|
className="p-1 rounded-sm text-white hover:text-white hover:border-red-700 hover:bg-red-700 transition"
|
||||||
|
>
|
||||||
|
<Send size={20} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={downloadCatalog}
|
||||||
|
className="py-1 px-4 flex items-center gap-2 hover:bg-red-700 hover:cursor-pointer hover:border-red-700 text-white font-medium border border-white rounded-md transition"
|
||||||
|
>
|
||||||
|
<Download size={18} />
|
||||||
|
{t("navbar.catalog")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { InnerNavbar } from "./innerNavbar";
|
||||||
|
|
||||||
export function AboutBanner() {
|
export function AboutBanner() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
return (
|
return (
|
||||||
<section className="relative w-full lg:h-[60vh] h-screen min-h-100 overflow-hidden pt-10">
|
<section className="relative w-full lg:h-[70vh] min-[350px]:h-[90vh] h-screen min-h-100 overflow-hidden pt-10">
|
||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 z-0"
|
className="absolute inset-0 z-0"
|
||||||
@@ -24,7 +25,10 @@ export function AboutBanner() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="max-w-250 w-full mx-auto px-4">
|
<div className="max-w-250 w-full mx-auto px-4">
|
||||||
<div className="relative z-20 h-full flex max-lg:flex-col items-start justify-between gap-5 pt-30">
|
{/* <div className="relative z-20 pt-50 sm:pt-30 pb-10">
|
||||||
|
<InnerNavbar />
|
||||||
|
</div> */}
|
||||||
|
<div className="relative z-20 h-full flex max-lg:flex-col items-start justify-between gap-5 pt-40">
|
||||||
<div className="spacw-y-4 ">
|
<div className="spacw-y-4 ">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<DotAnimatsiya />
|
<DotAnimatsiya />
|
||||||
@@ -39,7 +43,7 @@ export function AboutBanner() {
|
|||||||
{t("about.banner.subtitle")}
|
{t("about.banner.subtitle")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="font-almarai lg:w-[40%] text-gray-300 mt-20">
|
<div className="font-almarai lg:w-[40%] text-gray-300 sm:mt-20">
|
||||||
{t("about.banner.description")}
|
{t("about.banner.description")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
81
components/pages/about/aboutDetail/baza.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { ShieldCheck } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { NormativeCard } from "./normativeCard";
|
||||||
|
|
||||||
|
const fadeUp = (delay = 0) => ({
|
||||||
|
initial: { opacity: 0, y: 36 },
|
||||||
|
animate: { opacity: 1, y: 0 },
|
||||||
|
transition: { duration: 0.65, ease: [0.22, 1, 0.36, 1] as any, delay },
|
||||||
|
});
|
||||||
|
|
||||||
|
const fadeUpView = (delay = 0) => ({
|
||||||
|
initial: { opacity: 0, y: 48 },
|
||||||
|
whileInView: { opacity: 1, y: 0 },
|
||||||
|
transition: { duration: 0.65, ease: [0.22, 1, 0.36, 1] as any, delay },
|
||||||
|
viewport: { once: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function NormativBazaPage() {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-[#0f0e0d] text-white min-h-screen pt-10 pb-20">
|
||||||
|
{/* ── Hero ── */}
|
||||||
|
<section className="relative w-full px-2">
|
||||||
|
{/* Content */}
|
||||||
|
<div className="relative z-10 flex flex-col justify-end h-full max-w-6xl mx-auto">
|
||||||
|
<motion.span
|
||||||
|
{...fadeUp(0)}
|
||||||
|
className="text-xs font-black uppercase tracking-[0.22em] text-red-600 mb-4"
|
||||||
|
>
|
||||||
|
{t("about.normativBaza.hero.label")}
|
||||||
|
</motion.span>
|
||||||
|
|
||||||
|
<motion.h1
|
||||||
|
{...fadeUp(0.1)}
|
||||||
|
className="text-4xl md:text-5xl lg:text-7xl font-black uppercase leading-[0.95] tracking-tight text-white"
|
||||||
|
>
|
||||||
|
{t("about.normativBaza.hero.title1")}
|
||||||
|
<br />
|
||||||
|
<span className="text-red-700">
|
||||||
|
{t("about.normativBaza.hero.title2")}
|
||||||
|
</span>
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
{...fadeUp(0.22)}
|
||||||
|
className="mt-6 max-w-xl text-sm md:text-base text-gray-300 leading-relaxed"
|
||||||
|
>
|
||||||
|
{t("about.normativBaza.hero.description")}
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
{/* Decorative line */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ scaleX: 0, originX: 0 }}
|
||||||
|
animate={{ scaleX: 1 }}
|
||||||
|
transition={{ delay: 0.4, duration: 0.8, ease: [0.22, 1, 0.36, 1] }}
|
||||||
|
className="mt-10 w-24 h-px bg-red-700"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<NormativeCard />
|
||||||
|
|
||||||
|
{/* ── Bottom quote band ── */}
|
||||||
|
<motion.section
|
||||||
|
{...fadeUpView(0)}
|
||||||
|
className="border-t border-white/5 max-w-6xl mx-auto px-6 py-10 flex flex-col md:flex-row items-start md:items-center gap-6 justify-between"
|
||||||
|
>
|
||||||
|
<p className="text-xl md:text-2xl font-bold text-white/80 max-w-lg leading-snug">
|
||||||
|
{t("about.normativBaza.bottomText")}
|
||||||
|
</p>
|
||||||
|
<div className="shrink-0 w-16 h-16 rounded-2xl bg-red-400/10 border border-red-400/20 flex items-center justify-center">
|
||||||
|
<ShieldCheck size={28} className="text-red-600" />
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
components/pages/about/aboutDetail/card.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Download } from "lucide-react";
|
||||||
|
|
||||||
|
interface DownloadCardProps {
|
||||||
|
title: string;
|
||||||
|
fileType?: string;
|
||||||
|
fileSize: string;
|
||||||
|
fileUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DownloadCard({
|
||||||
|
title,
|
||||||
|
fileType = "PDF",
|
||||||
|
fileSize,
|
||||||
|
fileUrl,
|
||||||
|
}: DownloadCardProps) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={fileUrl}
|
||||||
|
download
|
||||||
|
className="min-h-40 h-full group relative w-full max-w-md border border-white/10 bg-[#171616b8] transition rounded-lg p-4 flex flex-col gap-4 items-start justify-between"
|
||||||
|
>
|
||||||
|
{/* Background glow effect */}
|
||||||
|
<div className="absolute inset-0 bg-linear-to-t from-red-600/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||||
|
|
||||||
|
{/* Decorative corner accent */}
|
||||||
|
<div className="absolute top-0 right-0 w-24 h-24 bg-linear-to-br from-red-500/20 to-transparent rounded-bl-full opacity-0 group-hover:opacity-00 transition-opacity duration-500" />
|
||||||
|
{/* Top section */}
|
||||||
|
<div className="flex justify-between items-start">
|
||||||
|
<h3 className="text-xl font-unbounded font-bold group-hover:text-red-500 text-white leading-tight transition-colors duration-300">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<span className="text-sm font-medium text-white">{fileType}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom section */}
|
||||||
|
<div className="flex w-full justify-between items-center">
|
||||||
|
<span className="text-sm text-gray-200">{fileSize}</span>
|
||||||
|
|
||||||
|
<Download
|
||||||
|
size={20}
|
||||||
|
className="text-gray-600 transition group-hover:text-red-700 duration-300"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
73
components/pages/about/aboutDetail/guides.tsx
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"use client";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import DownloadCard from "./card";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import PaginationLite from "@/components/paginationUI";
|
||||||
|
import { DownloadCardSkeleton } from "./loading/guidLoading";
|
||||||
|
|
||||||
|
export function Guides() {
|
||||||
|
const t = useTranslations();
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const guides = [
|
||||||
|
{
|
||||||
|
file: "/varnix.pdf",
|
||||||
|
name: t("about.notePPPage.varnix"),
|
||||||
|
file_type: "PDF",
|
||||||
|
file_size: "368.51 KB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "/ppFlanes.pdf",
|
||||||
|
name: t("about.notePPPage.ppFlanes"),
|
||||||
|
file_type: "PDF",
|
||||||
|
file_size: "368.51 KB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: "/ppFiting.pdf",
|
||||||
|
name: t("about.notePPPage.ppFiting"),
|
||||||
|
file_type: "PDF",
|
||||||
|
file_size: "368.51 KB",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ["guides"],
|
||||||
|
queryFn: () => httpClient(endPoints.guides),
|
||||||
|
select: (res) => ({
|
||||||
|
results: res.data?.data?.results,
|
||||||
|
current_page: res.data?.data?.current_page,
|
||||||
|
total_pages: res.data?.data?.total_pages,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const guidedata = data?.results ?? guides;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid lg:grid-cols-3 min-[580px]:grid-cols-2 grid-cols-1 gap-4 max-w-7xl mx-auto py-5">
|
||||||
|
{isLoading ? (
|
||||||
|
<DownloadCardSkeleton />
|
||||||
|
) : (
|
||||||
|
guidedata.map((guide: any, index: number) => (
|
||||||
|
<DownloadCard
|
||||||
|
key={index}
|
||||||
|
title={guide.name}
|
||||||
|
fileType={guide.file_type}
|
||||||
|
fileSize={guide.file_size}
|
||||||
|
fileUrl={guide.file}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{data?.total_pages > 1 && (
|
||||||
|
<PaginationLite
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={data?.total_pages}
|
||||||
|
onChange={setCurrentPage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
components/pages/about/aboutDetail/loading/guidLoading.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
44
components/pages/about/aboutDetail/loading/loading.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
95
components/pages/about/aboutDetail/normativeCard.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { Award } from "lucide-react";
|
||||||
|
import { normativeData } from "@/lib/demoData";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useState } from "react";
|
||||||
|
import PaginationLite from "@/components/paginationUI";
|
||||||
|
import { CertCardSkeleton } from "./loading/loading";
|
||||||
|
|
||||||
|
export function NormativeCard() {
|
||||||
|
const t = useTranslations();
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ["normativeData"],
|
||||||
|
queryFn: () => httpClient(endPoints.normative),
|
||||||
|
select: (res) => ({
|
||||||
|
results: res.data?.data?.results,
|
||||||
|
current_page: res.data?.data?.current_page,
|
||||||
|
total_pages: res.data?.data?.total_pages,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const generallyData = data?.results || normativeData;
|
||||||
|
|
||||||
|
if (isLoading) return <CertCardSkeleton />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-col gap-8 py-10 max-w-6xl mx-auto px-2">
|
||||||
|
{generallyData.map((c: any, i: number) => (
|
||||||
|
<motion.article
|
||||||
|
key={i}
|
||||||
|
initial={{ opacity: 0, y: 28 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.55, delay: i * 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="group flex flex-col rounded-2xl overflow-hidden sm:p-5 p-2 bg-[#161514] border border-white/5 hover:border-red-600/20 transition-colors duration-300 w-full"
|
||||||
|
>
|
||||||
|
{/* Meta + actions */}
|
||||||
|
<div className="flex flex-col justify-between flex-1 min-w-0 py-1 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{/* Badge row */}
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Award size={11} className="text-red-600 shrink-0" />
|
||||||
|
<span className="text-[10px] font-black uppercase tracking-widest text-red-600">
|
||||||
|
{t("about.certificatePage.card.badge")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] font-bold uppercase tracking-wider text-white/20 border border-white/10 px-2 py-0.5 rounded-full">
|
||||||
|
{c.artikul}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h3 className="font-bold text-sm md:text-base text-white leading-snug">
|
||||||
|
{t(c.title)}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Divider */}
|
||||||
|
<div className="mx-4 h-px bg-white/5 sm:my-5 my-2" />
|
||||||
|
|
||||||
|
{/* Documents list */}
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<ul className="flex flex-col gap-2.5">
|
||||||
|
{c.features.map((doc: any, di: number) => (
|
||||||
|
<li key={di} className="flex items-start gap-2.5">
|
||||||
|
<span className="mt-1 flex-none w-1.5 h-1.5 rounded-full bg-red-600/60" />
|
||||||
|
<p className="text-xs text-gray-400 leading-relaxed">
|
||||||
|
{t(doc?.name)}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</motion.article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{data?.total_pages > 1 && (
|
||||||
|
<PaginationLite
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={data?.total_pages}
|
||||||
|
onChange={setCurrentPage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
components/pages/about/aboutDetail/sertificateCard.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { certs } from "@/lib/demoData";
|
||||||
|
import { Award } from "lucide-react";
|
||||||
|
|
||||||
|
export function CertCard({ c, i }: { c: (typeof certs)[0]; i: number }) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.article
|
||||||
|
initial={{ opacity: 0, y: 28 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.55, delay: i * 0.1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="group flex flex-col rounded-2xl overflow-hidden sm:p-5 p-2 bg-[#161514] border border-white/5 hover:border-red-600/20 transition-colors duration-300 w-full"
|
||||||
|
>
|
||||||
|
{/* Right: meta + actions for gitea */}
|
||||||
|
<div className="flex flex-col justify-between flex-1 min-w-0 py-1 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
{/* Badge row */}
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Award size={11} className="text-red-600 shrink-0" />
|
||||||
|
<span className="text-[10px] font-black uppercase tracking-widest text-red-600">
|
||||||
|
{t("about.certificatePage.card.badge")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] font-bold uppercase tracking-wider text-white/20 border border-white/10 px-2 py-0.5 rounded-full">
|
||||||
|
{c.artikul}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h3 className="font-bold text-sm md:text-base text-white leading-snug">
|
||||||
|
{c.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ── Divider ── */}
|
||||||
|
<div className="mx-4 h-px bg-white/5 sm:my-5 my-2" />
|
||||||
|
|
||||||
|
{/* Collapsible document list */}
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<ul className="flex flex-col gap-2.5">
|
||||||
|
{c.features.length > 0 &&
|
||||||
|
c.features.map((doc: any, di: number) => {
|
||||||
|
const { name } = doc;
|
||||||
|
return (
|
||||||
|
<li key={di} className="flex items-start gap-2.5">
|
||||||
|
<span className="mt-1 flex-none w-1.5 h-1.5 rounded-full bg-red-600/60" />
|
||||||
|
<p className="text-xs text-gray-400 leading-relaxed">
|
||||||
|
{name || ""}
|
||||||
|
</p>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</motion.article>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,3 +2,4 @@ export { AboutBanner } from "./aboutBanner";
|
|||||||
export { Story } from "./story";
|
export { Story } from "./story";
|
||||||
export { AboutLine } from "./aboutLine";
|
export { AboutLine } from "./aboutLine";
|
||||||
export { WhyChooseUs } from "./whyChooseUs";
|
export { WhyChooseUs } from "./whyChooseUs";
|
||||||
|
export { InnerNavbar } from "./innerNavbar";
|
||||||
52
components/pages/about/innerNavbar.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
export function InnerNavbar() {
|
||||||
|
const t = useTranslations();
|
||||||
|
const locale = useLocale();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ name: t("about.subPages.baza"), value: "baza" },
|
||||||
|
{ name: t("about.subPages.certificate"), value: "sertificate" },
|
||||||
|
{ name: t("about.subPages.notePP"), value: "notePP" },
|
||||||
|
{ name: t("about.subPages.noteTrailer"), value: "noteTrailer" },
|
||||||
|
{ name: t("about.subPages.noteFlans"), value: "noteFlans" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="w-full border-b border-gray-100 bg-[#1e1d1c] sticky top-0 z-30">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="flex items-center gap-1 overflow-x-auto">
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const href = `/${locale}/about/${tab.value}`;
|
||||||
|
const isActive = pathname === href || pathname.endsWith(`/about/${tab.value}`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={tab.value}
|
||||||
|
href={href}
|
||||||
|
className={[
|
||||||
|
"relative shrink-0 px-4 py-4 text-sm font-semibold transition-colors duration-200 whitespace-nowrap",
|
||||||
|
"after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:rounded-full after:transition-all after:duration-200",
|
||||||
|
isActive
|
||||||
|
? "text-red-600"
|
||||||
|
: "text-gray-300 after:bg-transparent hover:text-red-600",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{tab.name}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// for hide scrollbar in inner navbar, add this class to the parent container of InnerNavbar
|
||||||
|
// scrollbar-none [-ms-overflow-style:none] [scrollbar-width:none]
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ export function Story() {
|
|||||||
return (
|
return (
|
||||||
<div className="pb-0 relative z-10 max-[350px]:pb-30 ">
|
<div className="pb-0 relative z-10 max-[350px]:pb-30 ">
|
||||||
<div className="max-w-260 mx-auto px-4">
|
<div className="max-w-260 mx-auto px-4">
|
||||||
<section className="relative -top-30 rounded-xl w-full lg:h-[70vh] h-[80vh] min-h-150 sm:overflow-hidden shadow-2xl flex flex-col items-start justify-between">
|
<section className="relative -top-20 rounded-xl w-full lg:h-[70vh] h-[80vh] min-h-150 sm:overflow-hidden shadow-2xl flex flex-col items-start justify-between">
|
||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 z-0 rounded-xl"
|
className="absolute inset-0 z-0 rounded-xl"
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
||||||
import { useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export function WhyChooseUs() {
|
export function WhyChooseUs() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const locale = useLocale();
|
||||||
const features = [
|
const features = [
|
||||||
{ title: t("about.whyChoose.features.fastResponse") },
|
{ title: t("about.whyChoose.features.fastResponse") },
|
||||||
{ title: t("about.whyChoose.features.ready24") },
|
{ title: t("about.whyChoose.features.ready24") },
|
||||||
@@ -56,9 +56,12 @@ export function WhyChooseUs() {
|
|||||||
|
|
||||||
{/* CTA Button */}
|
{/* CTA Button */}
|
||||||
<div>
|
<div>
|
||||||
<button className="font-almarai shadow-[0px_0px_2px_8px_#ff01015c] rounded-full bg-red-600 px-6 py-3 font-bold text-white transition-all hover:bg-red-700 sm:px-8 sm:py-4">
|
<Link
|
||||||
|
href={`/${locale}/contact`}
|
||||||
|
className="font-almarai shadow-[0px_0px_2px_8px_#ff01015c] rounded-full bg-red-600 px-6 py-3 font-bold text-white transition-all hover:bg-red-700 sm:px-8 sm:py-4"
|
||||||
|
>
|
||||||
{t("about.whyChoose.contact")}
|
{t("about.whyChoose.contact")}
|
||||||
</button>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ export default function Form() {
|
|||||||
|
|
||||||
const formRequest = useMutation({
|
const formRequest = useMutation({
|
||||||
mutationKey: [],
|
mutationKey: [],
|
||||||
mutationFn: (data: FormData) => httpClient.post(endPoints.post.contact, data),
|
mutationFn: (data: FormData) =>
|
||||||
|
httpClient.post(endPoints.post.contact, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setSubmitStatus("success");
|
setSubmitStatus("success");
|
||||||
setFormData({
|
setFormData({
|
||||||
@@ -78,10 +79,13 @@ export default function Form() {
|
|||||||
if (!formData.surname.trim()) {
|
if (!formData.surname.trim()) {
|
||||||
newErrors.surname = "Last name is required";
|
newErrors.surname = "Last name is required";
|
||||||
}
|
}
|
||||||
if (!formData.address.trim()) {
|
const phoneNumbers = formData.address.replace(/\D/g, "");
|
||||||
newErrors.address = "address is required";
|
if (phoneNumbers.length !== 12) {
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.address)) {
|
newErrors.address =
|
||||||
newErrors.address = "Please enter a valid address";
|
t("validation.phoneRequired") || "To'liq telefon raqam kiriting";
|
||||||
|
} else if (!phoneNumbers.startsWith("998")) {
|
||||||
|
newErrors.address =
|
||||||
|
t("validation.phoneInvalid") || "Noto'g'ri telefon raqam";
|
||||||
}
|
}
|
||||||
if (!formData.theme.trim()) {
|
if (!formData.theme.trim()) {
|
||||||
newErrors.theme = "theme is required";
|
newErrors.theme = "theme is required";
|
||||||
@@ -97,6 +101,30 @@ export default function Form() {
|
|||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatPhoneNumber = (value: string) => {
|
||||||
|
const numbers = value.replace(/\D/g, "");
|
||||||
|
if (!numbers.startsWith("998")) {
|
||||||
|
return "+998 ";
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted = "+998 ";
|
||||||
|
const rest = numbers.slice(3);
|
||||||
|
if (rest.length > 0) formatted += rest.slice(0, 2);
|
||||||
|
if (rest.length > 2) formatted += " " + rest.slice(2, 5);
|
||||||
|
if (rest.length > 5) formatted += " " + rest.slice(5, 7);
|
||||||
|
if (rest.length > 7) formatted += " " + rest.slice(7, 9);
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const formatted = formatPhoneNumber(e.target.value);
|
||||||
|
setFormData({ ...formData, address: formatted });
|
||||||
|
if (errors.address) {
|
||||||
|
setErrors({ ...errors, address: "" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
) => {
|
) => {
|
||||||
@@ -169,19 +197,19 @@ export default function Form() {
|
|||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="address"
|
type="tel"
|
||||||
name="address"
|
id="phone"
|
||||||
placeholder={t("contact.form.placeholders.email")}
|
name="phone"
|
||||||
value={formData.address}
|
value={formData.address}
|
||||||
onChange={handleChange}
|
onChange={handlePhoneChange}
|
||||||
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||||
errors.address ? "border-red-500" : "border-transparent"
|
errors.address ? "border-red-500" : "border-transparent"
|
||||||
}`}
|
}`}
|
||||||
|
placeholder="+998 90 123 45 67"
|
||||||
|
maxLength={17}
|
||||||
/>
|
/>
|
||||||
{errors.address && (
|
{errors.address && (
|
||||||
<p className="font-almarai mt-1 text-xs text-red-500">
|
<p className="mt-1 text-sm text-red-500">{errors.address}</p>
|
||||||
{errors.address}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function Contact() {
|
|||||||
className="absolute inset-0"
|
className="absolute inset-0"
|
||||||
style={{
|
style={{
|
||||||
background:
|
background:
|
||||||
"radial-gradient(ellipse at bottom center, #d2610a 0%, #1e1d1ce9 70% , #1e1d1ce9 70%)",
|
"radial-gradient(at center bottom, rgb(144 74 20) 0%, rgba(30, 29, 28, 0.914) 50%, rgba(30, 29, 28, 0.914) 70%)",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Flame, Building2, Ambulance } from "lucide-react";
|
import { Flame, Building2, Ambulance } from "lucide-react";
|
||||||
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
||||||
import { useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
interface ServiceItem {
|
interface ServiceItem {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
@@ -16,6 +15,7 @@ interface ServiceItem {
|
|||||||
|
|
||||||
export function AboutUs() {
|
export function AboutUs() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const locale = useLocale();
|
||||||
const services: ServiceItem[] = [
|
const services: ServiceItem[] = [
|
||||||
{
|
{
|
||||||
icon: <Flame width={40} height={40} className="text-red-500" />,
|
icon: <Flame width={40} height={40} className="text-red-500" />,
|
||||||
@@ -76,9 +76,9 @@ export function AboutUs() {
|
|||||||
|
|
||||||
{/* Button */}
|
{/* Button */}
|
||||||
<div>
|
<div>
|
||||||
<Button className="font-almarai bg-red-600 hover:bg-red-700 text-white font-bold px-8 py-3 rounded-full transition-colors duration-300 shadow-[0px_0px_2px_8px_#ff01015c]">
|
<Link href={`/${locale}/about`} className="font-almarai bg-red-600 hover:bg-red-700 text-white font-bold px-8 py-3 rounded-full transition-colors duration-300 shadow-[0px_0px_2px_8px_#ff01015c]">
|
||||||
{t("home.about.title")}
|
{t("home.about.title")}
|
||||||
</Button>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
import { useTranslations } from "next-intl";
|
|
||||||
import DotAnimatsiya from "../../dot/DotAnimatsiya";
|
|
||||||
|
|
||||||
export function Banner() {
|
|
||||||
const t = useTranslations();
|
|
||||||
return (
|
|
||||||
<section className="relative w-full lg:h-[86vh] h-screen min-h-150 overflow-hidden pt-20">
|
|
||||||
{/* Background Image */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 z-0"
|
|
||||||
style={{
|
|
||||||
backgroundImage: "url(/images/home/banner.jpg)",
|
|
||||||
backgroundSize: "cover",
|
|
||||||
backgroundPosition: "center",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Gradient Overlay - Bottom-left to top-right */}
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 z-10"
|
|
||||||
style={{
|
|
||||||
background: `linear-gradient(to top right, #c75c08 0%, #1e1d1ce3 28%, #1e1d1ce3 100%)`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Content Container */}
|
|
||||||
<div className="relative z-20 h-full flex items-center lg:mt-0 sm:mt-[10vh] mt-[5vh]">
|
|
||||||
<div className="max-w-400 mx-auto px-4 sm:px-6 lg:px-8 w-full">
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 items-center h-full">
|
|
||||||
{/* Right side - Text Content */}
|
|
||||||
<div className="lg:hidden inline-block space-y-6 text-white">
|
|
||||||
{/* Badge */}
|
|
||||||
<div className="flex items-center gap-2 w-fit">
|
|
||||||
<DotAnimatsiya />
|
|
||||||
<span className="text-sm font-semibold tracking-wide font-almarai">
|
|
||||||
{t("home.banner.title1")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Heading */}
|
|
||||||
<h1
|
|
||||||
className="bg-linear-to-br from-white via-white to-black
|
|
||||||
text-transparent bg-clip-text text-4xl sm:text-5xl lg:text-6xl font-bold leading-tight text-pretty font-unbounded"
|
|
||||||
>
|
|
||||||
{t("home.banner.title2")}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<p className="text-base sm:text-lg text-gray-300 leading-relaxed max-w-md">
|
|
||||||
{t("home.banner.description")}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* CTA Button */}
|
|
||||||
<button className="shadow-[0px_0px_2px_8px_#ff01015c] bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-8 rounded-full transition duration-300 transform hover:scale-105 w-fit">
|
|
||||||
{t("home.banner.cta")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Left side - Firefighters Image */}
|
|
||||||
<div className="flex items-end justify-center ">
|
|
||||||
<img
|
|
||||||
src="/images/homeBanner3.png"
|
|
||||||
alt="Firefighters"
|
|
||||||
loading="lazy"
|
|
||||||
className="lg:w-150 w-100 lg:h-150 max-[300px]:w-[80vw] object-contain object-right rounded-xl drop-shadow-2xl"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right side - Text Content */}
|
|
||||||
<div className="lg:inline-block hidden space-y-6 mb-20">
|
|
||||||
{/* Badge */}
|
|
||||||
<div className="flex items-center gap-2 w-fit">
|
|
||||||
<DotAnimatsiya />
|
|
||||||
<span className="text-sm font-semibold text-white tracking-wide font-almarai">
|
|
||||||
{t("home.banner.title1")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Heading */}
|
|
||||||
<h1 className="font-unbounded uppercase text-4xl bg-linear-to-br from-white via-white to-black
|
|
||||||
text-transparent bg-clip-text sm:text-5xl lg:text-6xl font-bold leading-tight text-pretty">
|
|
||||||
{t("home.banner.title2")}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<p className="font-almarai text-base sm:text-lg text-gray-300 leading-relaxed max-w-md">
|
|
||||||
{t("home.banner.description")}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* CTA Button */}
|
|
||||||
<button className="font-almarai shadow-[0px_0px_2px_8px_#ff01015c] bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-8 rounded-full transition duration-300 transform hover:scale-105 w-fit">
|
|
||||||
{t("home.banner.cta")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
28
components/pages/home/banner/banner.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { BannerSlider } from "./slider";
|
||||||
|
|
||||||
|
export function Banner() {
|
||||||
|
return (
|
||||||
|
<section className="relative w-full lg:h-[86vh] h-screen min-h-150 overflow-hidden min-[450px]:pt-10 pt-20">
|
||||||
|
{/* Background Image */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-0"
|
||||||
|
style={{
|
||||||
|
backgroundImage: "url(/images/home/banner.jpg)",
|
||||||
|
backgroundSize: "cover",
|
||||||
|
backgroundPosition: "center",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Gradient Overlay - Bottom-left to top-right */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 z-10"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to top right, #c75c08 0%, #1e1d1ce3 28%, #1e1d1ce3 100%)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Content Container */}
|
||||||
|
<BannerSlider />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
68
components/pages/home/banner/loading.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
163
components/pages/home/banner/slider.tsx
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
|
import { Navigation, Autoplay } from "swiper/modules";
|
||||||
|
import "swiper/css";
|
||||||
|
import "swiper/css/navigation";
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
|
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import { BannerType } from "@/lib/types";
|
||||||
|
import { BannerSliderSkeleton } from "./loading";
|
||||||
|
// The custom CSS selectors for navigation
|
||||||
|
const navigationPrevEl = ".hero-swiper-prev";
|
||||||
|
const navigationNextEl = ".hero-swiper-next";
|
||||||
|
|
||||||
|
export function BannerSlider() {
|
||||||
|
const t = useTranslations();
|
||||||
|
const locale = useLocale();
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ["banner"],
|
||||||
|
queryFn: () => httpClient(endPoints.banner),
|
||||||
|
select: (data: any): BannerType[] => data?.data?.results,
|
||||||
|
});
|
||||||
|
const BANNER_DATA = [
|
||||||
|
{
|
||||||
|
image: "/images/homeBanner3.png",
|
||||||
|
title: t("home.banner.title2"),
|
||||||
|
description: t("home.banner.description"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: "/images/homeBanner4.png",
|
||||||
|
title: t("home.banner.title2"),
|
||||||
|
description: t("home.banner.description"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const bannerData = data ?? BANNER_DATA;
|
||||||
|
|
||||||
|
if (isLoading) return <BannerSliderSkeleton />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto relative z-30 h-full mt-20 flex items-center justify-center ">
|
||||||
|
{/* Custom buttons */}
|
||||||
|
<button
|
||||||
|
className={`${navigationPrevEl.replace(
|
||||||
|
".",
|
||||||
|
"",
|
||||||
|
)} w-10 h-10 absolute z-10 left-0 top-[40vh] rounded-full p-0 bg-primary text-center text-white lg:flex hidden items-center justify-center hover:bg-red-600 hover:cursor-pointer transition`}
|
||||||
|
>
|
||||||
|
<ChevronLeft size={30} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`${navigationNextEl.replace(
|
||||||
|
".",
|
||||||
|
"",
|
||||||
|
)} w-10 h-10 absolute z-10 right-0 top-[40vh] rounded-full bg-primary text-center text-white lg:flex hidden items-center justify-center hover:bg-red-600 hover:cursor-pointer transition `}
|
||||||
|
>
|
||||||
|
<ChevronRight size={30} />
|
||||||
|
</button>
|
||||||
|
<Swiper
|
||||||
|
modules={[Navigation, Autoplay]}
|
||||||
|
slidesPerView={1}
|
||||||
|
spaceBetween={30}
|
||||||
|
loop={true}
|
||||||
|
navigation={{
|
||||||
|
// Pass the class selectors here
|
||||||
|
prevEl: navigationPrevEl,
|
||||||
|
nextEl: navigationNextEl,
|
||||||
|
}}
|
||||||
|
autoplay={{
|
||||||
|
delay: 5000,
|
||||||
|
disableOnInteraction: false,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{bannerData.map((item, index) => (
|
||||||
|
<SwiperSlide key={index}>
|
||||||
|
<div className="relative z-20 h-full flex items-center lg:mt-0 sm:mt-[10vh] mt-[5vh]">
|
||||||
|
<div className="max-w-400 mx-auto px-4 sm:px-6 lg:px-8 w-full">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 items-center h-full">
|
||||||
|
{/* Right side - Text Content */}
|
||||||
|
<div className="lg:hidden inline-block space-y-6 text-white">
|
||||||
|
{/* Badge */}
|
||||||
|
<div className="flex items-center gap-2 w-fit">
|
||||||
|
<DotAnimatsiya />
|
||||||
|
<span className="text-sm font-semibold tracking-wide font-almarai">
|
||||||
|
{t("home.banner.title1")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Heading */}
|
||||||
|
<h1
|
||||||
|
className="bg-linear-to-br from-white via-white to-black
|
||||||
|
text-transparent bg-clip-text text-4xl sm:text-5xl lg:text-6xl font-bold leading-tight text-pretty font-unbounded"
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-base sm:text-lg text-gray-300 leading-relaxed max-w-md">
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* CTA Button */}
|
||||||
|
<button className="shadow-[0px_0px_2px_8px_#ff01015c] bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-8 rounded-full transition duration-300 transform hover:scale-105 w-fit">
|
||||||
|
{t("home.banner.cta")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Left side - Firefighters Image */}
|
||||||
|
<div className="flex items-end justify-center ">
|
||||||
|
<img
|
||||||
|
src={item.image}
|
||||||
|
alt="Firefighters"
|
||||||
|
loading="lazy"
|
||||||
|
className="lg:w-150 w-100 lg:h-150 max-[300px]:w-[80vw] object-contain object-right rounded-xl drop-shadow-2xl"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right side - Text Content */}
|
||||||
|
<div className="lg:inline-block hidden space-y-6 mb-20">
|
||||||
|
{/* Badge */}
|
||||||
|
<div className="flex items-center gap-2 w-fit">
|
||||||
|
<DotAnimatsiya />
|
||||||
|
<span className="text-sm font-semibold text-white tracking-wide font-almarai">
|
||||||
|
{t("home.banner.title1")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Heading */}
|
||||||
|
<h1
|
||||||
|
className="font-unbounded uppercase text-4xl bg-linear-to-br from-white via-white to-black
|
||||||
|
text-transparent bg-clip-text sm:text-5xl lg:text-6xl font-bold leading-tight text-pretty"
|
||||||
|
>
|
||||||
|
{t("home.banner.title2")}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="font-almarai text-base sm:text-lg text-gray-300 leading-relaxed max-w-md">
|
||||||
|
{t("home.banner.description")}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* CTA Button */}
|
||||||
|
<Link
|
||||||
|
href={`/${locale}/contact`}
|
||||||
|
className="font-almarai shadow-[0px_0px_2px_8px_#ff01015c] bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-8 rounded-full transition duration-300 transform hover:scale-105 w-fit"
|
||||||
|
>
|
||||||
|
{t("home.banner.cta")}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
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 { useEffect } from "react";
|
|
||||||
import CatalogCard from "../../products/catalog";
|
import CatalogCard from "../../products/catalog";
|
||||||
import CatalogCardSkeleton from "@/components/loadingSkleton";
|
import CatalogCardSkeleton from "@/components/loadingSkleton";
|
||||||
import EmptyData from "@/components/EmptyData";
|
import EmptyData from "@/components/EmptyData";
|
||||||
import { getRouteLang } from "@/request/getLang";
|
import { getRouteLang } from "@/request/getLang";
|
||||||
import { CategoryType } from "@/lib/types";
|
import { CategoryType } from "@/lib/types";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function Catalog() {
|
export default function Catalog() {
|
||||||
const language = getRouteLang();
|
const language = getRouteLang();
|
||||||
@@ -16,6 +16,7 @@ export default function Catalog() {
|
|||||||
queryFn: () => httpClient(endPoints.category.all),
|
queryFn: () => httpClient(endPoints.category.all),
|
||||||
select: (data): CategoryType[] => data?.data?.results,
|
select: (data): CategoryType[] => data?.data?.results,
|
||||||
});
|
});
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -31,8 +32,8 @@ export default function Catalog() {
|
|||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<EmptyData
|
<EmptyData
|
||||||
title="Katalog topilmadi"
|
title={t("products.noData.title")}
|
||||||
description="Hozircha kategoriyalar mavjud emas. Keyinroq qaytib keling."
|
description={t("products.noData.description")}
|
||||||
icon="shopping"
|
icon="shopping"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export { Banner } from "./banner";
|
export { Banner } from "./banner/banner";
|
||||||
export { Statistics } from "./statistics";
|
export { Statistics } from "./statistics";
|
||||||
export { AboutUs } from "./about";
|
export { AboutUs } from "./about";
|
||||||
export { Video } from "./video";
|
export { Video } from "./video";
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ export function Line() {
|
|||||||
>
|
>
|
||||||
<Phone className="text-white w-5 h-5" />
|
<Phone className="text-white w-5 h-5" />
|
||||||
</span>
|
</span>
|
||||||
+123-456-7890
|
+998-55-055-21-21
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Image
|
<Image
|
||||||
src="/images/home/fireHydrant.png"
|
src="/images/home/balon.png"
|
||||||
alt="image"
|
alt="image"
|
||||||
width={60}
|
width={60}
|
||||||
height={60}
|
height={60}
|
||||||
|
|||||||
@@ -1,16 +1,40 @@
|
|||||||
|
"use client";
|
||||||
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { ChevronRight } from "lucide-react";
|
import { ChevronRight } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { ServicesLoading } from "../services/loading";
|
||||||
|
import { EmptyServices } from "../services/empty";
|
||||||
|
import { useServiceDetail } from "@/zustand/useService";
|
||||||
|
import { cardVariants, containerVariants } from "@/lib/animations";
|
||||||
|
|
||||||
export function OurService() {
|
export function OurService() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const locale = useLocale();
|
||||||
|
const setServiceId = useServiceDetail((state) => state.setServiceId);
|
||||||
|
|
||||||
|
// get request
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ["firesafety"],
|
||||||
|
queryFn: () => httpClient(endPoints.services.all),
|
||||||
|
select: (data) => data?.data?.data?.results.slice(0, 4),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#1e1d1c] py-10 md:py-16 lg:py-20 mb-30">
|
<div className="bg-[#1e1d1c] py-10 md:py-16 lg:py-20 mb-30">
|
||||||
<div className="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
{/* Header */}
|
{/* Header for github */}
|
||||||
<div className="space-y-4 md:space-y-6">
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="space-y-4 md:space-y-6"
|
||||||
|
>
|
||||||
<div className="font-almarai flex items-center justify-center gap-2 text-base sm:text-lg md:text-xl text-white font-bold">
|
<div className="font-almarai flex items-center justify-center gap-2 text-base sm:text-lg md:text-xl text-white font-bold">
|
||||||
<DotAnimatsiya />
|
<DotAnimatsiya />
|
||||||
{t("home.services.title")}
|
{t("home.services.title")}
|
||||||
@@ -21,52 +45,95 @@ export function OurService() {
|
|||||||
<p className="font-almarai text-center text-sm sm:text-base md:text-lg text-gray-400 max-w-4xl mx-auto px-4">
|
<p className="font-almarai text-center text-sm sm:text-base md:text-lg text-gray-400 max-w-4xl mx-auto px-4">
|
||||||
{t("home.services.description")}
|
{t("home.services.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Conditional Rendering */}
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="my-10">
|
||||||
|
<ServicesLoading />
|
||||||
|
</div>
|
||||||
|
) : !data || (Array.isArray(data) && data.length === 0) ? (
|
||||||
|
<div className="my-10">
|
||||||
|
<EmptyServices />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<motion.div
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
>
|
||||||
{/* cards */}
|
{/* cards */}
|
||||||
<div className="max-w-250 w-full mx-auto flex sm:flex-row flex-col items-center gap-5 my-10">
|
<div className="max-w-250 w-full mx-auto overflow-hidden flex sm:flex-row flex-col items-center gap-5 my-10">
|
||||||
<div className="relative space-y-4 py-6 px-8 rounded-xl sm:w-[55%] w-full bg-[linear-gradient(to_bottom_right,#000000,#000000,#000000,#d2610a)]">
|
<motion.div
|
||||||
|
variants={cardVariants}
|
||||||
|
className="sm:w-[55%] overflow-hidden w-full"
|
||||||
|
onClick={() => setServiceId(data[0].id)}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={`/${locale}/services/detail`}
|
||||||
|
className="overflow-hidden block hover:cursor-pointer relative space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300"
|
||||||
|
>
|
||||||
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
||||||
{t("home.services.services.operation.title")}
|
{data[0].title}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-almarai text-gray-400 max-w-80 w-full">
|
<p className="font-almarai text-gray-400 max-w-80 w-full">
|
||||||
{t("home.services.services.operation.description")}
|
{data[0].subtitle}
|
||||||
</p>
|
</p>
|
||||||
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm">
|
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
|
||||||
{t("home.services.learnmore")} <ChevronRight size={20} />
|
{t("home.services.learnmore")} <ChevronRight size={20} />
|
||||||
</button>
|
</button>
|
||||||
<Image
|
<Image
|
||||||
src="/images/home/gruop.png"
|
src={data[0].main_image}
|
||||||
alt="images"
|
alt="images"
|
||||||
width={200}
|
width={200}
|
||||||
height={100}
|
height={100}
|
||||||
className="object-contain sm:absolute bottom-0 right-2 z-10"
|
className="object-contain sm:absolute bottom-0 -right-2 z-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</Link>
|
||||||
<div className="relative overflow-hidden space-y-4 py-6 px-8 rounded-xl sm:w-[45%] w-full bg-[linear-gradient(to_bottom_right,#000000,#000000,#000000,#d2610a)]">
|
</motion.div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={`/${locale}/services/detail`}
|
||||||
|
className="sm:w-[45%] w-full"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
onClick={() => setServiceId(data[1].id)}
|
||||||
|
variants={cardVariants}
|
||||||
|
>
|
||||||
|
<div className="hover:cursor-pointer relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300">
|
||||||
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
||||||
{t("home.services.services.suppression.title")}
|
{data[1].title}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-almarai text-gray-400 max-w-70 w-full">
|
<p className="font-almarai text-gray-400 max-w-70 w-full">
|
||||||
{t("home.services.services.suppression.description")}
|
{data[1].subtitle}
|
||||||
</p>
|
</p>
|
||||||
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm">
|
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
|
||||||
{t("home.services.learnmore")} <ChevronRight size={20} />
|
{t("home.services.learnmore")} <ChevronRight size={20} />
|
||||||
</button>
|
</button>
|
||||||
<Image
|
<Image
|
||||||
src="/images/home/redShlang.png"
|
src={data[1].main_image}
|
||||||
alt="images"
|
alt="images"
|
||||||
width={200}
|
width={200}
|
||||||
height={100}
|
height={100}
|
||||||
className="object-contain sm:absolute -bottom-4 -right-4 z-10"
|
className="object-contain sm:absolute -bottom-4 -right-4 z-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-250 flex sm:flex-row flex-col items-start justify-between gap-5 mt-5 w-full mx-auto">
|
<div className="max-w-250 flex sm:flex-row flex-col items-start justify-between gap-5 mt- w-full mx-auto">
|
||||||
<div className="relative rounded-xl sm:w-[40%] w-full bg-[linear-gradient(to_bottom_right,#d2610a,#000000,#000000)]">
|
<motion.div
|
||||||
|
variants={cardVariants}
|
||||||
|
onClick={() => setServiceId(data[2].id)}
|
||||||
|
className="sm:w-[40%] w-full -mt-5"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={`/${locale}/services/detail`}
|
||||||
|
className="block hover:cursor-pointer relative rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300"
|
||||||
|
>
|
||||||
<Image
|
<Image
|
||||||
src="/images/home/ambulance.png"
|
src={data[2].main_image}
|
||||||
alt="images"
|
alt="images"
|
||||||
width={300}
|
width={300}
|
||||||
height={200}
|
height={200}
|
||||||
@@ -74,48 +141,64 @@ export function OurService() {
|
|||||||
/>
|
/>
|
||||||
<div className="space-y-4 py-6 px-8">
|
<div className="space-y-4 py-6 px-8">
|
||||||
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
||||||
{t("home.services.services.safety.title")}
|
{data[2].title}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-almarai text-gray-400 max-w-80 w-full">
|
<p className="font-almarai text-gray-400 max-w-80 w-full">
|
||||||
{t("home.services.services.safety.description")}
|
{data[2].subtitle}
|
||||||
</p>
|
</p>
|
||||||
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm">
|
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
|
||||||
{t("home.services.learnmore")} <ChevronRight size={20} />
|
{t("home.services.learnmore")} <ChevronRight size={20} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
<div className="sm:w-[60%] w-full">
|
<div className="sm:w-[60%] w-full">
|
||||||
<div className="relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#000000,#000000,#d2610a)]">
|
<motion.div
|
||||||
|
onClick={() => setServiceId(data[3].id)}
|
||||||
|
variants={cardVariants}
|
||||||
|
>
|
||||||
|
<Link href={`/${locale}/services/detail`}>
|
||||||
|
<div className="hover:cursor-pointer relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300">
|
||||||
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
||||||
{t("home.services.services.monitoring.title")}
|
{data[3].title}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-almarai text-gray-400 max-w-70 w-full">
|
<p className="font-almarai text-gray-400 max-w-70 w-full">
|
||||||
{t("home.services.services.monitoring.description")}
|
{data[3].subtitle}
|
||||||
</p>
|
</p>
|
||||||
<button className="font-almarai sm:mt-38 mt-0 text-[#dc2626] font-semibold flex items-center gap-2 text-sm">
|
<button className="font-almarai sm:mt-37 mt-0 text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
|
||||||
{t("home.services.learnmore")} <ChevronRight size={20} />
|
{t("home.services.learnmore")}{" "}
|
||||||
|
<ChevronRight size={20} />
|
||||||
</button>
|
</button>
|
||||||
<Image
|
<Image
|
||||||
src="/images/home/balon.png"
|
src={data[3].main_image}
|
||||||
alt="images"
|
alt="images"
|
||||||
width={200}
|
width={200}
|
||||||
height={100}
|
height={100}
|
||||||
className="object-contain sm:absolute -bottom-20 -right-4 max-sm:-mb-20 z-10"
|
className="object-contain sm:absolute -bottom-20 -right-4 max-sm:-mb-20 z-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="py-8 px-8 rounded-xl mt-5 w-full p-5 bg-[linear-gradient(to_top_right,#000000,#000000,#d2610a)] flex sm:flex-row flex-col gap-5 items-center justify-between">
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
variants={cardVariants}
|
||||||
|
className="py-6 px-8 rounded-xl mt-5 w-full p-5 bg-[linear-gradient(to_top_right,#000000,#190b00,#542604,#8f4308)] flex sm:flex-row flex-col gap-5 items-center justify-between hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300"
|
||||||
|
>
|
||||||
<h2 className="font-unbounded sm:text-3xl text-xl font-semibold font-armanai text-white">
|
<h2 className="font-unbounded sm:text-3xl text-xl font-semibold font-armanai text-white">
|
||||||
{t("home.services.viewMoreServices")}
|
{t("home.services.viewMoreServices")}
|
||||||
</h2>
|
</h2>
|
||||||
<Link
|
<Link
|
||||||
href="/services"
|
href={`/${locale}/services`}
|
||||||
className="font-almarai shadow-[0px_0px_2px_6px_#a60404ad] bg-red-600 hover:bg-red-700 text-white font-bold sm:py-3 sm:px-8 px-8 py-2 rounded-full transition duration-300 transform hover:scale-105 w-fit"
|
className="font-almarai shadow-[0px_0px_2px_6px_#a60404ad] bg-red-600 hover:bg-red-700 text-white font-bold sm:py-3 sm:px-8 px-8 py-2 rounded-full transition duration-300 transform hover:scale-105 w-fit"
|
||||||
>
|
>
|
||||||
{t("home.services.viewMore")}
|
{t("home.services.viewMore")}
|
||||||
</Link>
|
</Link>
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
527
components/pages/payment/index.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -84,14 +84,14 @@ export default function CatalogCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const navigateLink = have_sub_category
|
const navigateLink = have_sub_category
|
||||||
? `/${locale}/catalog_page?category=${id}`
|
? `/${locale}/catalog_page/subCategory?category=${id}`
|
||||||
: `/${locale}/products?category=${id}`;
|
: `/${locale}/catalog_page/products?category=${id}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={navigateLink}
|
href={navigateLink}
|
||||||
onClick={updateZustands}
|
onClick={updateZustands}
|
||||||
className="group relative h-112.5 w-full overflow-hidden rounded-2xl bg-[#17161679] from-[#444242] to-black border hover:border-red-700 border-white/10 transition-all duration-500 hover:-translate-y-1"
|
className="group relative h-112.5 w-full overflow-hidden rounded-2xl bg-[#171616] bg-linear-to-br from-[#2a2a2a] to-black border hover:border-red-700 border-white/10 transition-all duration-500 hover:-translate-y-1"
|
||||||
>
|
>
|
||||||
{/* Background glow effect */}
|
{/* 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" />
|
<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" />
|
||||||
|
|||||||
151
components/pages/products/filter/catalog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import EmptyData from "@/components/EmptyData";
|
|
||||||
import { CategoryType } from "@/lib/types";
|
|
||||||
import httpClient from "@/request/api";
|
|
||||||
import { getRouteLang } from "@/request/getLang";
|
|
||||||
import { endPoints } from "@/request/links";
|
|
||||||
import { useCategory } from "@/store/useCategory";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import CatalogCardSkeletonSmall from "./loading";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { ArrowUpRight } from "lucide-react";
|
|
||||||
|
|
||||||
export default function FilterCatalog() {
|
|
||||||
const language = getRouteLang();
|
|
||||||
const setCategory = useCategory((state) => state.setCategory);
|
|
||||||
const { data, isLoading } = useQuery({
|
|
||||||
queryKey: ["category", language],
|
|
||||||
queryFn: () => httpClient(endPoints.category.all),
|
|
||||||
select: (data): CategoryType[] => data?.data?.results,
|
|
||||||
});
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("product catalog data: ", data);
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
||||||
{[...Array(3)].map((_, index) => (
|
|
||||||
<CatalogCardSkeletonSmall key={index} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ma'lumot yo'q holati
|
|
||||||
if (!data || data.length === 0) {
|
|
||||||
return (
|
|
||||||
<EmptyData
|
|
||||||
title="Katalog topilmadi"
|
|
||||||
description="Hozircha kategoriyalar mavjud emas. Keyinroq qaytib keling."
|
|
||||||
icon="shopping"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-200 w-full mx-auto space-x-5 px-5 flex items-center justify-around my-10 -mt-30 pb-5 relative z-20 sm:overflow-x-hidden overflow-x-scroll">
|
|
||||||
{data?.map((item) => (
|
|
||||||
<div
|
|
||||||
onClick={() => setCategory(item)}
|
|
||||||
className="shrink-0 group relative w-55 h-60 overflow-hidden rounded-2xl bg-[#17161679] border border-white/10 transition-all duration-500 hover:-translate-y-1 hover:border-red-700 cursor-pointer"
|
|
||||||
>
|
|
||||||
{/* Background glow effect */}
|
|
||||||
<div className="absolute inset-0 bg-linear-to-t from-red-600/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
|
||||||
|
|
||||||
{/* Decorative corner accent */}
|
|
||||||
<div className="absolute top-0 right-0 w-16 h-16 bg-linear-to-br from-red-500/20 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
|
||||||
|
|
||||||
{/* Content container */}
|
|
||||||
<div className="relative h-full flex flex-col p-4">
|
|
||||||
{/* Title section */}
|
|
||||||
<div className="mb-3">
|
|
||||||
<div className="flex items-start justify-between gap-2">
|
|
||||||
<h3 className="text-lg font-unbounded font-bold text-white leading-tight transition-colors duration-300">
|
|
||||||
{item.name}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="shrink-0 w-6 h-6 rounded-full bg-white/10 flex items-center justify-center group-hover:bg-red-500 transition-all duration-300 group-hover:scale-110">
|
|
||||||
<ArrowUpRight
|
|
||||||
className="w-3.5 h-3.5 text-white"
|
|
||||||
strokeWidth={2.5}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Image container */}
|
|
||||||
<div className="relative flex-1 rounded-xl overflow-hidden bg-linear-to-br from-[#444242] to-gray-900/50 border border-white/5 group-hover:border-white/20 transition-all duration-500">
|
|
||||||
<div className="absolute inset-0 bg-linear-to-t from-black/60 via-transparent to-transparent z-10" />
|
|
||||||
|
|
||||||
<div className="relative w-full h-full">
|
|
||||||
<Image
|
|
||||||
src={item.image}
|
|
||||||
alt={item.name}
|
|
||||||
fill
|
|
||||||
className="object-contain p-3 transition-transform duration-700 group-hover:scale-105"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
87
components/pages/products/filter/category.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,149 +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 } from "lucide-react";
|
|
||||||
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 subCategory = useSubCategory((state) => state.subCategory);
|
|
||||||
|
|
||||||
const [dataExpanded, setDataExpanded] = useState<boolean>(false);
|
|
||||||
const [numberExpanded, setNumberExpanded] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const [catalogData, setCatalogData] = useState<
|
|
||||||
{ id: number; name: string; type: string }[]
|
|
||||||
>(result[0].items);
|
|
||||||
const [sizeData, setSizeData] = useState<
|
|
||||||
{ id: number; name: string; type: string }[]
|
|
||||||
>(result[1].items);
|
|
||||||
|
|
||||||
const { data: catalog } = useQuery({
|
|
||||||
queryKey: ["catalog"],
|
|
||||||
queryFn: () => httpClient(endPoints.filter.catalogCategoryId(category.id)),
|
|
||||||
select: (data) => {
|
|
||||||
const catalogData = data?.data?.results;
|
|
||||||
return catalogData.map((item: any) => ({
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
type: "catalog",
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data: size } = useQuery({
|
|
||||||
queryKey: ["size"],
|
|
||||||
queryFn: () => httpClient(endPoints.filter.sizeCategoryId(category.id)),
|
|
||||||
select: (data) => {
|
|
||||||
const sizedata = data?.data?.results;
|
|
||||||
return sizedata.map((item: any) => ({
|
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
type: "size",
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
catalog && setCatalogData(catalog);
|
|
||||||
size && setSizeData(size);
|
|
||||||
}, [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);
|
|
||||||
|
|
||||||
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 ">
|
||||||
{/* Bo'lim filtri */}
|
<Category />
|
||||||
{visibleSectionData && (
|
<CatalogSection />
|
||||||
<div className="bg-gray-500 rounded-lg w-full">
|
|
||||||
<p className="bg-red-500 text-white p-2 font-semibold font-almarai text-lg rounded-t-lg">
|
|
||||||
Bo'lim
|
|
||||||
</p>
|
|
||||||
<div className="lg:space-y-3 space-x-6 lg:p-2 p-5 flex lg:flex-col overflow-x-auto lg:overflow-x-hidden items-start justify-start w-full">
|
|
||||||
{visibleSectionData.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
onClick={() => toggleFilter(item)}
|
|
||||||
className="hover:cursor-pointer flex items-center gap-2 w-auto shrink-0"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition ${
|
|
||||||
hasData(item.name)
|
|
||||||
? "border-red-600 bg-red-600"
|
|
||||||
: "border-gray-400 bg-transparent"
|
|
||||||
}`}
|
|
||||||
aria-label="Filter checkbox"
|
|
||||||
>
|
|
||||||
{hasData(item.name) && (
|
|
||||||
<Check className="h-3 w-3 text-white" strokeWidth={3} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<p className="whitespace-nowrap">{item.name}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className="lg:flex hidden p-2 text-lg underline hover:text-red-300 transition"
|
|
||||||
onClick={() => setDataExpanded(!dataExpanded)}
|
|
||||||
>
|
|
||||||
{dataExpanded ? "Yashirish" : "Ko'proq ko'rish"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* O'lcham filtri */}
|
|
||||||
{visibleSectionNumber && (
|
|
||||||
<div className="bg-gray-500 rounded-lg">
|
|
||||||
<p className="bg-red-500 text-white p-2 font-semibold font-almarai text-lg rounded-t-lg">
|
|
||||||
O'lcham
|
|
||||||
</p>
|
|
||||||
<div className="lg:space-y-3 space-x-6 lg:p-2 p-5 flex lg:flex-col overflow-x-auto lg:overflow-x-hidden items-start justify-start w-full">
|
|
||||||
{visibleSectionNumber.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
onClick={() => toggleFilter(item)}
|
|
||||||
className="hover:cursor-pointer flex items-center gap-2 w-auto shrink-0"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition ${
|
|
||||||
hasData(item.name)
|
|
||||||
? "border-red-600 bg-red-600"
|
|
||||||
: "border-gray-400 bg-transparent"
|
|
||||||
}`}
|
|
||||||
aria-label="Filter checkbox"
|
|
||||||
>
|
|
||||||
{hasData(item.name) && (
|
|
||||||
<Check className="h-3 w-3 text-white" strokeWidth={3} />
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<p>{item.name}</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setNumberExpanded(!numberExpanded)}
|
|
||||||
className="lg:flex hidden p-2 text-lg underline hover:text-red-300 transition"
|
|
||||||
>
|
|
||||||
{numberExpanded ? "Yashirish" : "Ko'proq ko'rish"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useFilter } from "@/lib/filter-zustand";
|
|
||||||
import { X } from "lucide-react";
|
|
||||||
|
|
||||||
export default function FilterInfo() {
|
|
||||||
const filtered = useFilter((state) => state.filter);
|
|
||||||
const resetFilter = useFilter((state) => state.resetFilter);
|
|
||||||
const togleFilter = useFilter((state) => state.toggleFilter);
|
|
||||||
if (filtered.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="fixed bottom-13 left-5 z-10 bg-gray-500 p-3 rounded-lg space-y-3 max-w-70 w-full">
|
|
||||||
<p className="text-white ">Found: 20</p>
|
|
||||||
<div className="flex gap-1 flex-wrap">
|
|
||||||
{filtered &&
|
|
||||||
filtered.map((item) => (
|
|
||||||
<div
|
|
||||||
key={item.id}
|
|
||||||
className="flex items-center gap-1 p-1 rounded-lg bg-gray-700 text-white text-sm "
|
|
||||||
>
|
|
||||||
<button onClick={() => togleFilter(item)}>
|
|
||||||
<X size={16} />
|
|
||||||
</button>
|
|
||||||
{item.name}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button onClick={resetFilter} className="text-white underline ">
|
|
||||||
Clear all
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
34
components/pages/products/filter/useCataloghook.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
78
components/pages/products/filter/useCategory.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,59 +1,87 @@
|
|||||||
"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 { useEffect, useMemo } from "react";
|
import { useMemo, useState, useEffect } from "react";
|
||||||
import { useProductPageInfo } from "@/store/useProduct";
|
import { useProductPageInfo } from "@/zustand/useProduct";
|
||||||
|
import { useSubCategory } from "@/zustand/useSubCategory";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import PaginationLite from "@/components/paginationUI";
|
||||||
|
import { useCatalog } from "@/zustand/useCatalog";
|
||||||
|
|
||||||
export default function MainProduct() {
|
export default function MainProduct() {
|
||||||
const category = useCategory((state) => state.category);
|
const t = useTranslations();
|
||||||
const filter = useFilter((state) => state.filter);
|
const category = useCategory((s) => s.category);
|
||||||
const getFiltersByType = useFilter((state) => state.getFiltersByType);
|
const subCategory = useSubCategory((s) => s.subCategory);
|
||||||
const setProduct = useProductPageInfo((state) => state.setProducts);
|
const filter = useFilter((s) => s.filter);
|
||||||
|
const getFiltersByType = useFilter((s) => s.getFiltersByType);
|
||||||
|
const setProduct = useProductPageInfo((s) => s.setProducts);
|
||||||
|
|
||||||
// Query params yaratish
|
const parentID = useCatalog((state) => state.parentID);
|
||||||
|
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, [parentID]);
|
||||||
|
|
||||||
|
// ── Filter params ────────────────────────────────────────────────────────
|
||||||
const queryParams = useMemo(() => {
|
const queryParams = useMemo(() => {
|
||||||
const catalog = getFiltersByType("catalog");
|
const catalog = getFiltersByType("catalog");
|
||||||
const size = getFiltersByType("size");
|
const size = getFiltersByType("size");
|
||||||
|
const catalogParams = catalog.map((i) => `catalog=${i.id}`).join("&");
|
||||||
// Har bir filter uchun query string yaratish
|
const sizeParams = size.map((i) => `size=${i.id}`).join("&");
|
||||||
const catalogParams = catalog.map((item) => `catalog=${item.id}`).join("&");
|
|
||||||
const sizeParams = size.map((item) => `size=${item.id}`).join("&");
|
|
||||||
|
|
||||||
// Barcha paramslarni birlashtirish
|
|
||||||
const allParams = [catalogParams, sizeParams].filter(Boolean).join("&");
|
const allParams = [catalogParams, sizeParams].filter(Boolean).join("&");
|
||||||
|
setCurrentPage(1);
|
||||||
return allParams ? `&${allParams}` : "";
|
return allParams ? `&${allParams}` : "";
|
||||||
}, [filter, getFiltersByType]);
|
}, [filter]);
|
||||||
|
|
||||||
// Request link yaratish
|
// ── Request URL ──────────────────────────────────────────────────────────
|
||||||
const requestLink = useMemo(() => {
|
const requestLink = useMemo(() => {
|
||||||
const baseLink = category.have_sub_category
|
const baseLink = category.have_sub_category
|
||||||
? endPoints.subCategory.byId(category.id)
|
? endPoints.product.bySubCategory({ id: subCategory.id, currentPage })
|
||||||
: endPoints.product.byCategory(category.id || 0);
|
: parentID
|
||||||
|
? endPoints.product.byCatalogSection({ id: parentID, currentPage })
|
||||||
|
: endPoints.product.byCategory({ id: category.id, currentPage });
|
||||||
|
|
||||||
// Query params qo'shish
|
|
||||||
return `${baseLink}${queryParams}`;
|
return `${baseLink}${queryParams}`;
|
||||||
}, [category.id, category.have_sub_category, queryParams]);
|
}, [
|
||||||
|
category.id,
|
||||||
|
category.have_sub_category,
|
||||||
|
subCategory.id,
|
||||||
|
currentPage,
|
||||||
|
parentID,
|
||||||
|
queryParams,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ── Query ────────────────────────────────────────────────────────────────
|
||||||
const { data, isLoading, error } = useQuery({
|
const { data, isLoading, error } = useQuery({
|
||||||
queryKey: ["products", category.id, queryParams],
|
queryKey: [
|
||||||
|
"products",
|
||||||
|
category.id,
|
||||||
|
category.have_sub_category,
|
||||||
|
subCategory.id,
|
||||||
|
parentID,
|
||||||
|
queryParams,
|
||||||
|
currentPage,
|
||||||
|
],
|
||||||
queryFn: () => httpClient(requestLink),
|
queryFn: () => httpClient(requestLink),
|
||||||
select: (data) => {
|
placeholderData: (prev) => prev, // no flicker on pagination
|
||||||
const product = data?.data?.data?.results;
|
select: (res) => ({
|
||||||
return product.map((item: any) => ({
|
results: res?.data?.data?.results ?? [],
|
||||||
id: item.id,
|
totalPages: res?.data?.data?.total_pages ?? 1,
|
||||||
name: item.name,
|
}),
|
||||||
image: item.images[0].image,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const results = data?.results ?? [];
|
||||||
|
const totalPages = data?.totalPages ?? 1;
|
||||||
|
|
||||||
if (isLoading) {
|
// ── Render states ────────────────────────────────────────────────────────
|
||||||
|
if (isLoading && !data) {
|
||||||
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) => (
|
||||||
@@ -65,31 +93,43 @@ export default function MainProduct() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center text-red-500 py-10">
|
<div className="text-center text-red-500 py-10">{t("loadingError")}</div>
|
||||||
Ma'lumotlarni yuklashda xatolik yuz berdi
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data || data.length === 0) {
|
if (!results.length) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center text-gray-400 py-10">
|
<div className="text-center text-gray-400 py-10">
|
||||||
Mahsulotlar topilmadi
|
{t("productsNotFound")}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
<div className="space-y-4">
|
||||||
{data.map((item: any) => (
|
<div
|
||||||
|
className={`grid lg:grid-cols-4 sm:grid-cols-2 grid-cols-1 gap-5 transition-opacity ${
|
||||||
|
isLoading ? "opacity-50 pointer-events-none" : "opacity-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{results.map((item: any) => (
|
||||||
<ProductCard
|
<ProductCard
|
||||||
key={item.id} // ✅ index o'rniga id ishlatish
|
key={item.id}
|
||||||
getProduct={() => setProduct(item)}
|
getProduct={() => setProduct(item)}
|
||||||
title={item.name}
|
title={item.name}
|
||||||
image={item.image}
|
image={item?.images?.[0]?.image || ""}
|
||||||
slug={item.slug}
|
slug="special_product"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<PaginationLite
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onChange={(p) => setCurrentPage(p)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useLocale } from "next-intl";
|
"use client";
|
||||||
|
|
||||||
|
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";
|
||||||
|
|
||||||
@@ -16,26 +18,142 @@ export default function ProductCard({
|
|||||||
getProduct,
|
getProduct,
|
||||||
}: ProductCardProps) {
|
}: ProductCardProps) {
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/${locale}/products/${slug}`} onClick={getProduct}>
|
<Link
|
||||||
<article className="group transition-all duration-300 hover:cursor-pointer max-sm:max-w-100 max-sm:mx-auto max-sm:w-full">
|
href={`/${locale}/catalog_page/products/${slug}`}
|
||||||
|
onClick={getProduct}
|
||||||
|
className="group block"
|
||||||
|
>
|
||||||
|
<article className="
|
||||||
|
relative
|
||||||
|
bg-neutral-900
|
||||||
|
border border-zinc-800
|
||||||
|
rounded-xl
|
||||||
|
overflow-hidden
|
||||||
|
transition-all duration-300
|
||||||
|
hover:border-red-600/60
|
||||||
|
hover:shadow-[0_0_24px_0_rgba(220,38,38,0.15)]
|
||||||
|
max-sm:max-w-100 max-sm:mx-auto max-sm:w-full
|
||||||
|
h-95 flex flex-col
|
||||||
|
">
|
||||||
|
|
||||||
|
{/* Top accent line */}
|
||||||
|
<div className="
|
||||||
|
absolute top-0 left-0 right-0 h-0.5
|
||||||
|
bg-linear-to-r from-transparent via-red-600 to-transparent
|
||||||
|
opacity-0 group-hover:opacity-100
|
||||||
|
transition-opacity duration-300
|
||||||
|
z-10
|
||||||
|
" />
|
||||||
|
|
||||||
{/* Image Container */}
|
{/* Image Container */}
|
||||||
<div className="relative rounded-2xl h-45 sm:h-55 md:h-65 lg:w-[95%] w-[90%] mx-auto overflow-hidden">
|
<div className="
|
||||||
|
relative
|
||||||
|
h-48 sm:h-56 md:h-64
|
||||||
|
bg-zinc-900
|
||||||
|
border-b border-zinc-800
|
||||||
|
overflow-hidden
|
||||||
|
">
|
||||||
|
{/* Background pattern */}
|
||||||
|
<div className="
|
||||||
|
absolute inset-0
|
||||||
|
bg-[radial-gradient(circle_at_center,_rgba(39,39,42,0.8)_0%,_rgba(9,9,11,1)_100%)]
|
||||||
|
" />
|
||||||
|
|
||||||
<Image
|
<Image
|
||||||
src={image || "/placeholder.svg"}
|
src={image || "/placeholder.svg"}
|
||||||
alt={title}
|
alt={title}
|
||||||
fill
|
fill
|
||||||
className="object-contain transition-transform duration-300 group-hover:scale-105"
|
className="
|
||||||
|
object-contain
|
||||||
|
p-4
|
||||||
|
transition-transform duration-500
|
||||||
|
group-hover:scale-105
|
||||||
|
drop-shadow-[0_4px_12px_rgba(0,0,0,0.5)]
|
||||||
|
"
|
||||||
sizes="(max-width: 640px) 90vw, (max-width: 1024px) 45vw, 30vw"
|
sizes="(max-width: 640px) 90vw, (max-width: 1024px) 45vw, 30vw"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Hover overlay */}
|
||||||
|
<div className="
|
||||||
|
absolute inset-0
|
||||||
|
bg-red-600/5
|
||||||
|
opacity-0 group-hover:opacity-100
|
||||||
|
transition-opacity duration-300
|
||||||
|
" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content Container */}
|
{/* Content */}
|
||||||
<div className="p-6 sm:p-4">
|
<div className="p-5">
|
||||||
<h3 className="text-lg text-center sm:text-xl md:text-2xl font-bold text-white mb-4 line-clamp-3 group-hover:text-red-400 transition-colors duration-300">
|
{/* Decorative line */}
|
||||||
|
<div className="
|
||||||
|
w-8 h-0.5
|
||||||
|
bg-red-600
|
||||||
|
mb-3
|
||||||
|
transition-all duration-300
|
||||||
|
group-hover:w-16
|
||||||
|
" />
|
||||||
|
|
||||||
|
<h3 className="
|
||||||
|
text-sm sm:text-base
|
||||||
|
font-bold
|
||||||
|
text-zinc-100
|
||||||
|
line-clamp-3
|
||||||
|
leading-snug
|
||||||
|
tracking-wide
|
||||||
|
transition-colors duration-300
|
||||||
|
group-hover:text-white
|
||||||
|
">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
{/* Bottom row */}
|
||||||
|
<div className="
|
||||||
|
flex items-center justify-between
|
||||||
|
mt-4 pt-4
|
||||||
|
border-t border-zinc-800
|
||||||
|
">
|
||||||
|
<span className="
|
||||||
|
text-xs
|
||||||
|
text-zinc-500
|
||||||
|
tracking-widest
|
||||||
|
uppercase
|
||||||
|
font-medium
|
||||||
|
">
|
||||||
|
{ t("home.services.learnmore")}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Arrow */}
|
||||||
|
<div className="
|
||||||
|
flex items-center justify-center
|
||||||
|
w-7 h-7
|
||||||
|
rounded-full
|
||||||
|
border border-zinc-700
|
||||||
|
text-zinc-500
|
||||||
|
transition-all duration-300
|
||||||
|
group-hover:border-red-600
|
||||||
|
group-hover:text-red-500
|
||||||
|
group-hover:bg-red-600/10
|
||||||
|
">
|
||||||
|
<svg
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 12 12"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2 6H10M10 6L7 3M10 6L7 9"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -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] py-10">
|
<div className="bg-[#1e1d1c] pb-10 pt-5 px-2">
|
||||||
<div className="max-w-300 mx-auto w-full z-20 relative">
|
<div className="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>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export function Features({ features }: { features: string[] }) {
|
export function Features({ features }: { features: string[] }) {
|
||||||
|
const t = useTranslations();
|
||||||
if (!features || features.length === 0) {
|
if (!features || features.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -6,14 +9,14 @@ export function Features({ features }: { features: string[] }) {
|
|||||||
return (
|
return (
|
||||||
<div className="mt-12">
|
<div className="mt-12">
|
||||||
<h2 className="text-2xl md:text-3xl font-bold text-white mb-6">
|
<h2 className="text-2xl md:text-3xl font-bold text-white mb-6">
|
||||||
Xususiyatlar
|
{t("products.features")}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="rounded-xl overflow-hidden border border-gray-800 shadow-xl">
|
<div className="rounded-xl overflow-hidden border border-gray-800 shadow-xl">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-linear-to-r from-gray-900 to-black border-b border-gray-800">
|
<tr className="bg-linear-to-r from-stone-800 to-black/10 border-b border-gray-800">
|
||||||
<th className="px-4 py-4 md:px-6 text-left text-sm md:text-base font-semibold text-white">
|
<th className="px-4 py-4 md:px-6 text-left text-sm md:text-base font-semibold text-white">
|
||||||
Xususiyat
|
{t("products.feature")}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { usePriceModalStore } from "@/store/useProceModalStore";
|
"use client";
|
||||||
import { Facebook, Share2 } from "lucide-react";
|
|
||||||
|
|
||||||
const socialLinks = [
|
import { usePriceModalStore } from "@/zustand/useProceModalStore";
|
||||||
{ name: "telegram", icon: "✈️", color: "#0088cc" },
|
import { Check, Instagram, Send, Share2 } from "lucide-react";
|
||||||
{ name: "facebook", icon: <Facebook size={18} />, color: "#1877F2" },
|
import { useTranslations } from "next-intl";
|
||||||
{ name: "whatsapp", icon: "💬", color: "#25D366" },
|
import { useParams } from "next/navigation";
|
||||||
{ name: "twitter", icon: "𝕏", color: "#1DA1F2" },
|
import { useState } from "react";
|
||||||
];
|
|
||||||
|
|
||||||
interface RightSideProps {
|
interface RightSideProps {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -28,6 +26,46 @@ export function RightSide({
|
|||||||
image,
|
image,
|
||||||
}: RightSideProps) {
|
}: RightSideProps) {
|
||||||
const openModal = usePriceModalStore((state) => state.openModal);
|
const openModal = usePriceModalStore((state) => state.openModal);
|
||||||
|
const t = useTranslations();
|
||||||
|
const params = useParams();
|
||||||
|
const locale = params.locale || "uz";
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleShare = async () => {
|
||||||
|
const productUrl = `${window.location.origin}/${locale}/catalog_page/products/special_product?productId=${id}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Modern Web Share API dan foydalanish (mobil qurilmalar uchun)
|
||||||
|
if (navigator.share) {
|
||||||
|
await navigator.share({
|
||||||
|
title: title,
|
||||||
|
text: `${title} - ${description.slice(0, 100)}...`,
|
||||||
|
url: productUrl,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Desktop uchun clipboard ga copy qilish
|
||||||
|
await navigator.clipboard.writeText(productUrl);
|
||||||
|
setCopied(true);
|
||||||
|
|
||||||
|
// 2 soniyadan keyin "Copied" holatini o'chirish
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Share error:", error);
|
||||||
|
// Fallback - clipboard ga copy qilish
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(productUrl);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 2000);
|
||||||
|
} catch (clipboardError) {
|
||||||
|
console.error("Clipboard error:", clipboardError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleGetPrice = () => {
|
const handleGetPrice = () => {
|
||||||
openModal({
|
openModal({
|
||||||
@@ -47,7 +85,7 @@ export function RightSide({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center space-y-6">
|
<div className="flex flex-col justify-center space-y-6">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h1 className="text-2xl md:text-3xl lg:text-4xl font-bold text-white leading-tight">
|
<h1 className="text-xl md:text-3xl font-unbounded font-bold text-white leading-tight">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -59,7 +97,9 @@ export function RightSide({
|
|||||||
|
|
||||||
{/* Status Badge */}
|
{/* Status Badge */}
|
||||||
<div>
|
<div>
|
||||||
<span className={`inline-block px-4 py-2 rounded-lg text-sm font-semibold ${statusColor}`}>
|
<span
|
||||||
|
className={`inline-block px-4 py-2 rounded-lg text-sm font-semibold ${statusColor}`}
|
||||||
|
>
|
||||||
{status}
|
{status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -72,42 +112,41 @@ export function RightSide({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Price Section */}
|
{/* Price Section */}
|
||||||
<div className="bg-[#1716169f] rounded-xl p-6 space-y-6">
|
<div className="bg-[#1716169f] rounded-xl p-5 space-y-6">
|
||||||
{/* Price */}
|
|
||||||
<div>
|
|
||||||
<p className="text-gray-400 text-sm mb-2">Narx:</p>
|
|
||||||
<h2 className="text-3xl md:text-4xl font-bold text-red-700">
|
|
||||||
${price}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Button */}
|
{/* Action Button */}
|
||||||
<button
|
<button
|
||||||
onClick={handleGetPrice}
|
onClick={handleGetPrice}
|
||||||
className="w-full bg-red-700 hover:bg-red-800 text-white font-bold py-4 px-6 rounded-lg transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-red-700/50"
|
className="w-full bg-red-700 hover:bg-red-800 text-white font-bold py-4 px-6 rounded-lg transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-red-700/50"
|
||||||
>
|
>
|
||||||
Xabar yuborish
|
{t("products.send")}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Social Share */}
|
{/* Social Share */}
|
||||||
<div className="pt-4 border-t border-gray-800">
|
<div className="pt-4 border-t border-gray-800 flex items-center gap-5">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<button
|
||||||
<Share2 className="w-5 h-5 text-gray-400" />
|
onClick={handleShare}
|
||||||
<span className="text-sm text-gray-400">Ulashish:</span>
|
className="flex items-center gap-3 mb-3 text-gray-400 hover:text-white transition-colors group"
|
||||||
</div>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{socialLinks.map((social) => (
|
|
||||||
<a
|
|
||||||
key={social.name}
|
|
||||||
href="#"
|
|
||||||
className="w-10 h-10 rounded-lg flex items-center justify-center text-white text-sm font-bold transition-all duration-300 hover:scale-110 hover:shadow-lg"
|
|
||||||
style={{ backgroundColor: social.color }}
|
|
||||||
title={social.name}
|
|
||||||
>
|
>
|
||||||
{social.icon}
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-5 h-5 text-green-400" />
|
||||||
|
<span className="text-sm text-green-400">
|
||||||
|
{t("products.copied") || "Link nusxalandi!"}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Share2 className="w-5 h-5 group-hover:scale-110 transition-transform" />
|
||||||
|
<span className="text-sm">{t("products.share")}:</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href="https://t.me/ignum_tech"
|
||||||
|
className="p-2 rounded-md bg-white text-red-500 hover:text-white hover:bg-red-500"
|
||||||
|
>
|
||||||
|
<Send />
|
||||||
</a>
|
</a>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import "swiper/css/navigation";
|
|||||||
import "swiper/css/pagination";
|
import "swiper/css/pagination";
|
||||||
import "swiper/css/thumbs";
|
import "swiper/css/thumbs";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
const navigationPrevEl = ".custom-swiper-prev";
|
const navigationPrevEl = ".custom-swiper-prev";
|
||||||
const navigationNextEl = ".custom-swiper-next";
|
const navigationNextEl = ".custom-swiper-next";
|
||||||
|
|
||||||
export function SliderComp({ imgs }: { imgs: string[] }) {
|
export function SliderComp({ imgs }: { imgs: string[] }) {
|
||||||
const [thumbsSwiper, setThumbsSwiper] = useState<SwiperType | null>(null);
|
const [thumbsSwiper, setThumbsSwiper] = useState<SwiperType | null>(null);
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
// Agar rasm bo'lmasa
|
// Agar rasm bo'lmasa
|
||||||
if (!imgs || imgs.length === 0) {
|
if (!imgs || imgs.length === 0) {
|
||||||
@@ -33,7 +35,7 @@ export function SliderComp({ imgs }: { imgs: string[] }) {
|
|||||||
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<p className="text-gray-500">Rasm mavjud emas</p>
|
<p className="text-gray-500">{t("image_not_found")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -45,7 +47,10 @@ export function SliderComp({ imgs }: { imgs: string[] }) {
|
|||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
<Swiper
|
<Swiper
|
||||||
modules={[Navigation, Pagination, Thumbs]}
|
modules={[Navigation, Pagination, Thumbs]}
|
||||||
thumbs={{ swiper: thumbsSwiper && !thumbsSwiper.destroyed ? thumbsSwiper : null }}
|
thumbs={{
|
||||||
|
swiper:
|
||||||
|
thumbsSwiper && !thumbsSwiper.destroyed ? thumbsSwiper : null,
|
||||||
|
}}
|
||||||
navigation={{
|
navigation={{
|
||||||
prevEl: navigationPrevEl,
|
prevEl: navigationPrevEl,
|
||||||
nextEl: navigationNextEl,
|
nextEl: navigationNextEl,
|
||||||
@@ -79,21 +84,41 @@ export function SliderComp({ imgs }: { imgs: string[] }) {
|
|||||||
<button
|
<button
|
||||||
className={`${navigationPrevEl.replace(
|
className={`${navigationPrevEl.replace(
|
||||||
".",
|
".",
|
||||||
""
|
"",
|
||||||
)} absolute z-10 top-1/2 -translate-y-1/2 left-2 md:left-4 rounded-lg w-10 h-10 md:w-12 md:h-12 bg-red-700/90 hover:bg-red-800 text-white flex items-center justify-center transition opacity-0 group-hover:opacity-100 shadow-lg`}
|
)} absolute z-10 top-1/2 -translate-y-1/2 left-2 md:left-4 rounded-lg w-10 h-10 md:w-12 md:h-12 bg-red-700/90 hover:bg-red-800 text-white flex items-center justify-center transition opacity-0 group-hover:opacity-100 shadow-lg`}
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
className="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M15 19l-7-7 7-7"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`${navigationNextEl.replace(
|
className={`${navigationNextEl.replace(
|
||||||
".",
|
".",
|
||||||
""
|
"",
|
||||||
)} absolute z-10 top-1/2 -translate-y-1/2 right-2 md:right-4 rounded-lg w-10 h-10 md:w-12 md:h-12 bg-red-700/90 hover:bg-red-800 text-white flex items-center justify-center transition opacity-0 group-hover:opacity-100 shadow-lg`}
|
)} absolute z-10 top-1/2 -translate-y-1/2 right-2 md:right-4 rounded-lg w-10 h-10 md:w-12 md:h-12 bg-red-700/90 hover:bg-red-800 text-white flex items-center justify-center transition opacity-0 group-hover:opacity-100 shadow-lg`}
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
className="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
|
|||||||
72
components/pages/services/empty.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
// Empty State Component
|
||||||
|
export function EmptyServices() {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="max-w-250 w-full mx-auto py-20"
|
||||||
|
>
|
||||||
|
<div className="bg-[#171616] bg-linear-to-br from-[#2a2a2a] to-black rounded-2xl p-10 md:p-16 text-center border border-gray-500">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
transition={{ delay: 0.2, type: "spring", stiffness: 200 }}
|
||||||
|
className="mb-8"
|
||||||
|
>
|
||||||
|
<div className="w-24 h-24 mx-auto bg-red-500/10 rounded-full flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
className="w-12 h-12 text-red-500"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.h3
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.3 }}
|
||||||
|
className="text-2xl md:text-3xl font-bold text-white font-unbounded mb-4"
|
||||||
|
>
|
||||||
|
{t("operationalSystems.noData.title") || "Xizmatlar topilmadi"}
|
||||||
|
</motion.h3>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.4 }}
|
||||||
|
className="text-gray-400 font-almarai text-base md:text-lg mb-8 max-w-md mx-auto"
|
||||||
|
>
|
||||||
|
{t("operationalSystems.noData.description") || "Hozircha hech qanday xizmat mavjud emas. Tez orada yangi xizmatlar qo'shiladi."}
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.5 }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="font-almarai bg-red-600 hover:bg-red-700 text-white font-semibold py-3 px-8 rounded-full transition-all duration-300 transform hover:scale-105 shadow-lg hover:shadow-red-500/50"
|
||||||
|
>
|
||||||
|
{t("operationalSystems.noData.empty") || "Qayta yuklash"}
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
export { ServiceBanner } from "./serviceBanner";
|
export { ServiceBanner } from "./serviceBanner";
|
||||||
export { ServiceFaq } from "./serviceFaq";
|
export { ServiceFaq } from "./serviceFaq";
|
||||||
|
export { ServicePageServices } from "./servicePageServices";
|
||||||
|
|||||||
45
components/pages/services/loading.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
// Loading Skeleton Component
|
||||||
|
function ServiceCardSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="animate-pulse space-y-4 py-6 px-8 rounded-xl bg-[#171616] bg-linear-to-br from-[#2a2a2a] to-black">
|
||||||
|
<div className="h-6 bg-[#171616] rounded w-3/4"></div>
|
||||||
|
<div className="h-4 bg-[#171616] rounded w-full"></div>
|
||||||
|
<div className="h-4 bg-[#171616] rounded w-5/6"></div>
|
||||||
|
<div className="h-8 bg-[#171616] rounded w-32"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading Component
|
||||||
|
export function ServicesLoading() {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
className="max-w-250 w-full mx-auto space-y-5"
|
||||||
|
>
|
||||||
|
{/* Top Cards Skeleton */}
|
||||||
|
<div className="flex sm:flex-row flex-col items-center gap-5">
|
||||||
|
<div className="sm:w-[55%] w-full">
|
||||||
|
<ServiceCardSkeleton />
|
||||||
|
</div>
|
||||||
|
<div className="sm:w-[45%] w-full">
|
||||||
|
<ServiceCardSkeleton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Cards Skeleton */}
|
||||||
|
<div className="flex sm:flex-row flex-col items-start justify-between gap-5">
|
||||||
|
<div className="sm:w-[40%] w-full">
|
||||||
|
<ServiceCardSkeleton />
|
||||||
|
</div>
|
||||||
|
<div className="sm:w-[60%] w-full space-y-5">
|
||||||
|
<ServiceCardSkeleton />
|
||||||
|
<div className="h-24 bg-[#171616] rounded-xl animate-pulse"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ export function ServiceBanner() {
|
|||||||
<div
|
<div
|
||||||
className="absolute inset-0 z-10"
|
className="absolute inset-0 z-10"
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(to top right, #d2610a 0%, #1e1d1ce3 30%, #1e1d1ce3 100%)`,
|
background: `linear-gradient(to right top, rgb(157 73 9) 0%, rgb(33 32 31 / 89%) 40%, rgba(30, 29, 28, 0.89) 100%)`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,32 +1,33 @@
|
|||||||
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
||||||
import FAQAccordion from "../faq/faqAccardion";
|
import FAQAccordion, { FAQItem } from "../faq/faqAccardion";
|
||||||
import { useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
|
|
||||||
export function ServiceFaq() {
|
export function ServiceFaq() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const faqItems = [
|
const locale = useLocale();
|
||||||
|
const faqItems: FAQItem[] = [
|
||||||
{
|
{
|
||||||
id: "faq-1",
|
id: 1,
|
||||||
question: t("faq.question1.question"),
|
question: t("faq.question1.question"),
|
||||||
answer: t("faq.question1.answer"),
|
answer: t("faq.question1.answer"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "faq-2",
|
id: 2,
|
||||||
question: t("faq.question2.question"),
|
question: t("faq.question2.question"),
|
||||||
answer: t("faq.question2.answer"),
|
answer: t("faq.question2.answer"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "faq-3",
|
id: 3,
|
||||||
question: t("faq.question3.question"),
|
question: t("faq.question3.question"),
|
||||||
answer: t("faq.question3.answer"),
|
answer: t("faq.question3.answer"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "faq-4",
|
id: 4,
|
||||||
question: t("faq.question4.question"),
|
question: t("faq.question4.question"),
|
||||||
answer: t("faq.question4.answer"),
|
answer: t("faq.question4.answer"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "faq-5",
|
id: 5,
|
||||||
question: t("faq.question5.question"),
|
question: t("faq.question5.question"),
|
||||||
answer: t("faq.question5.answer"),
|
answer: t("faq.question5.answer"),
|
||||||
},
|
},
|
||||||
@@ -36,18 +37,18 @@ export function ServiceFaq() {
|
|||||||
{/* header */}
|
{/* header */}
|
||||||
<div className="space-y-4 w-full ">
|
<div className="space-y-4 w-full ">
|
||||||
<div className="flex items-center gap-3 justify-center text-white text-xl">
|
<div className="flex items-center gap-3 justify-center text-white text-xl">
|
||||||
<DotAnimatsiya /> FAQ
|
<DotAnimatsiya /> {locale === "ru" ? "ФАК" : "FAQ"}
|
||||||
</div>
|
</div>
|
||||||
<h1
|
<h1
|
||||||
className="text-center bg-linear-to-br from-white via-white/50 to-black
|
className="text-center bg-linear-to-br from-white via-white/50 to-black
|
||||||
text-transparent bg-clip-text text-3xl font-bold uppercase leading-tight sm:text-4xl md:text-5xl lg:text-6xl"
|
text-transparent bg-clip-text text-3xl font-bold uppercase leading-tight sm:text-4xl md:text-5xl lg:text-6xl"
|
||||||
>
|
>
|
||||||
General Questions
|
{t("faq.banner.topic")}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* FAQ Section */}
|
{/* FAQ Section */}
|
||||||
<div className="md:col-span-2 max-w-6xl mx-auto w-full">
|
<div className="md:col-span-2 max-w-6xl mx-auto w-full px-2">
|
||||||
<FAQAccordion items={faqItems} />
|
<FAQAccordion items={faqItems} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
220
components/pages/services/servicePageServices.tsx
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
"use client";
|
||||||
|
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { ChevronRight } from "lucide-react";
|
||||||
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { ServicesLoading } from "./loading";
|
||||||
|
import { EmptyServices } from "./empty";
|
||||||
|
import { useServiceDetail } from "@/zustand/useService";
|
||||||
|
import { cardVariants, containerVariants } from "@/lib/animations";
|
||||||
|
|
||||||
|
export function ServicePageServices() {
|
||||||
|
const t = useTranslations();
|
||||||
|
const locale = useLocale();
|
||||||
|
const setServiceId = useServiceDetail((state) => state.setServiceId);
|
||||||
|
|
||||||
|
// get request
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ["firesafety"],
|
||||||
|
queryFn: () => httpClient(endPoints.services.all),
|
||||||
|
select: (data) => {
|
||||||
|
const serviceData = data?.data?.data?.results;
|
||||||
|
return serviceData.reduce(
|
||||||
|
(resultArray: any, item: any, index: number) => {
|
||||||
|
const chunkIndex = Math.floor(index / 4);
|
||||||
|
|
||||||
|
if (!resultArray[chunkIndex]) {
|
||||||
|
resultArray[chunkIndex] = []; // Yangi chunk boshlash
|
||||||
|
}
|
||||||
|
|
||||||
|
resultArray[chunkIndex].push(item);
|
||||||
|
|
||||||
|
return resultArray;
|
||||||
|
},
|
||||||
|
[] as any[][],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-[#1e1d1c] py-10 md:py-16 lg:py-20 mb-15">
|
||||||
|
<div className="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
{/* Header for github */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="space-y-4 md:space-y-6"
|
||||||
|
>
|
||||||
|
<div className="font-almarai flex items-center justify-center gap-2 text-base sm:text-lg md:text-xl text-white font-bold">
|
||||||
|
<DotAnimatsiya />
|
||||||
|
{t("home.services.title")}
|
||||||
|
</div>
|
||||||
|
<h1 className="uppercase font-unbounded text-2xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl tracking-wider lg:tracking-[5px] font-bold bg-linear-to-br from-white via-white to-gray-400 text-transparent bg-clip-text text-center w-full">
|
||||||
|
{t("home.services.subtitle")}
|
||||||
|
</h1>
|
||||||
|
<p className="font-almarai text-center text-sm sm:text-base md:text-lg text-gray-400 max-w-4xl mx-auto px-4">
|
||||||
|
{t("home.services.description")}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Conditional Rendering */}
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="my-10">
|
||||||
|
<ServicesLoading />
|
||||||
|
</div>
|
||||||
|
) : !data || (Array.isArray(data) && data.length === 0) ? (
|
||||||
|
<div className="my-10">
|
||||||
|
<EmptyServices />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
data.map((item: any, index: number) => (
|
||||||
|
<motion.div
|
||||||
|
key={index}
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
>
|
||||||
|
{/* cards */}
|
||||||
|
<div className="max-w-250 w-full mx-auto overflow-hidden flex sm:flex-row flex-col items-center gap-3 my-4">
|
||||||
|
{item[0] && (
|
||||||
|
<motion.div
|
||||||
|
variants={cardVariants}
|
||||||
|
className="sm:w-[55%] overflow-hidden w-full"
|
||||||
|
onClick={() => setServiceId(item[0].id)}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={`/${locale}/services/detail`}
|
||||||
|
className="overflow-hidden block hover:cursor-pointer relative space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300"
|
||||||
|
>
|
||||||
|
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
||||||
|
{item[0]?.title}
|
||||||
|
</p>
|
||||||
|
<p className="font-almarai text-gray-400 max-w-80 w-full">
|
||||||
|
{item[0]?.subtitle}
|
||||||
|
</p>
|
||||||
|
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
|
||||||
|
{t("home.services.learnmore")}{" "}
|
||||||
|
<ChevronRight size={20} />
|
||||||
|
</button>
|
||||||
|
<Image
|
||||||
|
src={item[0]?.main_image}
|
||||||
|
alt="images"
|
||||||
|
width={200}
|
||||||
|
height={100}
|
||||||
|
className="object-contain sm:absolute bottom-0 -right-2 z-10"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{item[1] && (
|
||||||
|
<Link
|
||||||
|
href={`/${locale}/services/detail`}
|
||||||
|
className="sm:w-[45%] w-full"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
onClick={() => setServiceId(item[1]?.id)}
|
||||||
|
variants={cardVariants}
|
||||||
|
>
|
||||||
|
<div className="hover:cursor-pointer relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300">
|
||||||
|
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
||||||
|
{item[1]?.title}
|
||||||
|
</p>
|
||||||
|
<p className="font-almarai text-gray-400 max-w-70 w-full">
|
||||||
|
{item[1]?.subtitle}
|
||||||
|
</p>
|
||||||
|
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
|
||||||
|
{t("home.services.learnmore")}{" "}
|
||||||
|
<ChevronRight size={20} />
|
||||||
|
</button>
|
||||||
|
<Image
|
||||||
|
src={item[1]?.main_image}
|
||||||
|
alt="images"
|
||||||
|
width={200}
|
||||||
|
height={100}
|
||||||
|
className="object-contain sm:absolute -bottom-4 -right-4 z-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-250 flex sm:flex-row flex-col items-start justify-between gap-3 w-full mx-auto">
|
||||||
|
{item[2] && (
|
||||||
|
<motion.div
|
||||||
|
variants={cardVariants}
|
||||||
|
onClick={() => setServiceId(item[2]?.id)}
|
||||||
|
className="sm:w-[40%] w-full -mt-5"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={`/${locale}/services/detail`}
|
||||||
|
className="block hover:cursor-pointer relative rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={item[2]?.main_image}
|
||||||
|
alt="images"
|
||||||
|
width={250}
|
||||||
|
height={200}
|
||||||
|
className="object-contain mt-5"
|
||||||
|
/>
|
||||||
|
<div className="space-y-1 pb-3 px-5">
|
||||||
|
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
||||||
|
{item[2]?.title}
|
||||||
|
</p>
|
||||||
|
<p className="font-almarai text-gray-400 max-w-80 w-full">
|
||||||
|
{item[2]?.subtitle}
|
||||||
|
</p>
|
||||||
|
<button className="font-almarai text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
|
||||||
|
{t("home.services.learnmore")}{" "}
|
||||||
|
<ChevronRight size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="sm:w-[60%] w-full">
|
||||||
|
{item[3] && (
|
||||||
|
<motion.div
|
||||||
|
onClick={() => setServiceId(item[3]?.id)}
|
||||||
|
variants={cardVariants}
|
||||||
|
>
|
||||||
|
<Link href={`/${locale}/services/detail`}>
|
||||||
|
<div className="hover:cursor-pointer relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)] hover:shadow-2xl hover:shadow-red-500/20 transition-all duration-300">
|
||||||
|
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
||||||
|
{item[3]?.title}
|
||||||
|
</p>
|
||||||
|
<p className="font-almarai text-gray-400 max-w-70 w-full">
|
||||||
|
{item[3]?.subtitle}
|
||||||
|
</p>
|
||||||
|
<button className="font-almarai sm:mt-40 mt-0 text-[#dc2626] font-semibold flex items-center gap-2 text-sm hover:gap-3 transition-all">
|
||||||
|
{t("home.services.learnmore")}{" "}
|
||||||
|
<ChevronRight size={20} />
|
||||||
|
</button>
|
||||||
|
<Image
|
||||||
|
src={item[3]?.main_image}
|
||||||
|
alt="images"
|
||||||
|
width={200}
|
||||||
|
height={100}
|
||||||
|
className="object-contain sm:absolute -bottom-20 -right-4 max-sm:-mb-20 z-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,11 +3,13 @@
|
|||||||
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";
|
||||||
|
|
||||||
export function MainSubCategory() {
|
export function MainSubCategory() {
|
||||||
const category = useCategory((state) => state.category);
|
const category = useCategory((state) => state.category);
|
||||||
|
const t = useTranslations();
|
||||||
const { data, isLoading, error } = useQuery({
|
const { data, isLoading, error } = useQuery({
|
||||||
queryKey: ["subCategory"],
|
queryKey: ["subCategory"],
|
||||||
queryFn: () => httpClient(endPoints.subCategory.byId(category.id)),
|
queryFn: () => httpClient(endPoints.subCategory.byId(category.id)),
|
||||||
@@ -27,7 +29,7 @@ export function MainSubCategory() {
|
|||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center text-red-500 py-10">
|
<div className="text-center text-red-500 py-10">
|
||||||
Ma'lumotlarni yuklashda xatolik yuz berdi
|
{t("loading_error")}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -35,7 +37,7 @@ export function MainSubCategory() {
|
|||||||
if (!data || data.length === 0) {
|
if (!data || data.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center text-gray-400 py-10">
|
<div className="text-center text-gray-400 py-10">
|
||||||
Mahsulotlar topilmadi
|
{t("products_not_found")}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -43,7 +45,7 @@ export function MainSubCategory() {
|
|||||||
<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">
|
||||||
{data.map((item: any) => (
|
{data.map((item: any) => (
|
||||||
<Card
|
<Card
|
||||||
key={item.id} // ✅ index o'rniga id ishlatish
|
key={item.id}
|
||||||
title={item.name}
|
title={item.name}
|
||||||
image={item.image}
|
image={item.image}
|
||||||
slug={item.slug}
|
slug={item.slug}
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -32,7 +32,7 @@ export default function Card({
|
|||||||
image,
|
image,
|
||||||
category,
|
category,
|
||||||
});
|
});
|
||||||
router.push(`/${locale}/products`);
|
router.push(`/${locale}/catalog_page/products`);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Link href="#" onClick={handleClick}>
|
<Link href="#" onClick={handleClick}>
|
||||||
@@ -50,7 +50,7 @@ export default function Card({
|
|||||||
|
|
||||||
{/* Content Container */}
|
{/* Content Container */}
|
||||||
<div className="p-6 sm:p-4">
|
<div className="p-6 sm:p-4">
|
||||||
<h3 className="text-lg text-center sm:text-xl md:text-2xl font-bold text-white mb-4 line-clamp-3 group-hover:text-red-400 transition-colors duration-300">
|
<h3 className="text-lg text-center font-unbounded font-bold text-white mb-4 line-clamp-3 group-hover:text-red-400 transition-colors duration-300">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
99
components/paginationUI.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
} from "@/components/ui/pagination";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
onChange: (page: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PaginationLite({
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
onChange,
|
||||||
|
}: Props) {
|
||||||
|
const getPages = () => {
|
||||||
|
const visibleCount = 7; // maximum number of items to show (including ellipses)
|
||||||
|
const pages: (number | string)[] = [];
|
||||||
|
|
||||||
|
// If total pages are within visible limit, show all
|
||||||
|
if (totalPages <= visibleCount) {
|
||||||
|
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If current page is near the beginning: show 1..5, ellipsis, last
|
||||||
|
if (currentPage <= 4) {
|
||||||
|
for (let i = 1; i <= 5; i++) pages.push(i);
|
||||||
|
pages.push("…", totalPages);
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If current page is near the end: show first, ellipsis, last-4..last
|
||||||
|
if (currentPage >= totalPages - 3) {
|
||||||
|
pages.push(1, "…");
|
||||||
|
for (let i = totalPages - 4; i <= totalPages; i++) pages.push(i);
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middle case: first, ellipsis, current-1, current, current+1, ellipsis, last
|
||||||
|
pages.push(
|
||||||
|
1,
|
||||||
|
"…",
|
||||||
|
currentPage - 1,
|
||||||
|
currentPage,
|
||||||
|
currentPage + 1,
|
||||||
|
"…",
|
||||||
|
totalPages,
|
||||||
|
);
|
||||||
|
return pages;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pages = getPages();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pagination className="w-full flex justify-center items-center mx-auto">
|
||||||
|
<PaginationContent className="w-full px-auto items-center flex justify-center ">
|
||||||
|
{totalPages !== 1 && (
|
||||||
|
<PaginationPrevious
|
||||||
|
className="hover:cursor-pointer text-white hover:text-white"
|
||||||
|
onClick={() => onChange(Math.max(1, currentPage - 1))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pages */}
|
||||||
|
{pages.map((p, idx) =>
|
||||||
|
p === "…" ? (
|
||||||
|
<PaginationItem key={idx} className="px-2 text-white">
|
||||||
|
…
|
||||||
|
</PaginationItem>
|
||||||
|
) : (
|
||||||
|
<PaginationItem key={idx}>
|
||||||
|
<PaginationLink
|
||||||
|
isActive={p === currentPage}
|
||||||
|
className={`hover:cursor-pointer ${p === currentPage ? "text-black scale-110 text-[20px] font-medium" : "text-white"} hover:text-white border`}
|
||||||
|
onClick={() => onChange(Number(p))}
|
||||||
|
>
|
||||||
|
{p}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
{totalPages !== 1 && (
|
||||||
|
<PaginationNext
|
||||||
|
className="hover:cursor-pointer text-white hover:text-white"
|
||||||
|
onClick={() => onChange(Math.min(totalPages, currentPage + 1))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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";
|
||||||
@@ -12,7 +12,7 @@ import { toast } from "react-toastify";
|
|||||||
interface FormType {
|
interface FormType {
|
||||||
name: string;
|
name: string;
|
||||||
product: number;
|
product: number;
|
||||||
phone: number; // ✅ String bo'lishi kerak
|
number: number; // ✅ String bo'lishi kerak
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PriceModal() {
|
export function PriceModal() {
|
||||||
@@ -21,12 +21,12 @@ export function PriceModal() {
|
|||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
phone: "+998 ",
|
number: "+998 ",
|
||||||
});
|
});
|
||||||
|
|
||||||
const [errors, setErrors] = useState({
|
const [errors, setErrors] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
phone: "",
|
number: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const formRequest = useMutation({
|
const formRequest = useMutation({
|
||||||
@@ -35,7 +35,7 @@ export function PriceModal() {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: "",
|
name: "",
|
||||||
phone: "+998 ",
|
number: "+998 ",
|
||||||
});
|
});
|
||||||
toast.success(t("success") || "Muvaffaqiyatli yuborildi!");
|
toast.success(t("success") || "Muvaffaqiyatli yuborildi!");
|
||||||
closeModal();
|
closeModal();
|
||||||
@@ -46,16 +46,16 @@ export function PriceModal() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset form when modal closes
|
// Reset form when modal closes for github
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: "",
|
name: "",
|
||||||
phone: "+998 ",
|
number: "+998 ",
|
||||||
});
|
});
|
||||||
setErrors({
|
setErrors({
|
||||||
name: "",
|
name: "",
|
||||||
phone: "",
|
number: "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
@@ -90,9 +90,9 @@ export function PriceModal() {
|
|||||||
|
|
||||||
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const formatted = formatPhoneNumber(e.target.value);
|
const formatted = formatPhoneNumber(e.target.value);
|
||||||
setFormData({ ...formData, phone: formatted });
|
setFormData({ ...formData, number: formatted });
|
||||||
if (errors.phone) {
|
if (errors.number) {
|
||||||
setErrors({ ...errors, phone: "" });
|
setErrors({ ...errors, number: "" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,7 +107,7 @@ export function PriceModal() {
|
|||||||
const validateForm = () => {
|
const validateForm = () => {
|
||||||
const newErrors = {
|
const newErrors = {
|
||||||
name: "",
|
name: "",
|
||||||
phone: "",
|
number: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Name validation
|
// Name validation
|
||||||
@@ -116,17 +116,17 @@ export function PriceModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Phone validation
|
// Phone validation
|
||||||
const phoneNumbers = formData.phone.replace(/\D/g, "");
|
const phoneNumbers = formData.number.replace(/\D/g, "");
|
||||||
if (phoneNumbers.length !== 12) {
|
if (phoneNumbers.length !== 12) {
|
||||||
newErrors.phone =
|
newErrors.number =
|
||||||
t("validation.phoneRequired") || "To'liq telefon raqam kiriting";
|
t("validation.phoneRequired") || "To'liq telefon raqam kiriting";
|
||||||
} else if (!phoneNumbers.startsWith("998")) {
|
} else if (!phoneNumbers.startsWith("998")) {
|
||||||
newErrors.phone =
|
newErrors.number =
|
||||||
t("validation.phoneInvalid") || "Noto'g'ri telefon raqam";
|
t("validation.phoneInvalid") || "Noto'g'ri telefon raqam";
|
||||||
}
|
}
|
||||||
|
|
||||||
setErrors(newErrors);
|
setErrors(newErrors);
|
||||||
return !newErrors.name && !newErrors.phone;
|
return !newErrors.name && !newErrors.number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
@@ -137,15 +137,13 @@ export function PriceModal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Telefon raqamni tozalash (faqat raqamlar)
|
// Telefon raqamni tozalash (faqat raqamlar)
|
||||||
const cleanPhone = formData.phone.replace(/\D/g, "");
|
const cleanPhone = formData.number.replace(/\D/g, "");
|
||||||
|
|
||||||
const sendedData: FormType = {
|
const sendedData: FormType = {
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
phone: Number(cleanPhone), // ✅ String sifatida yuborish
|
number: Number(cleanPhone.slice(3)), // ✅ String sifatida yuborish
|
||||||
product: product?.id || 0,
|
product: product?.id || 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Sended data:", sendedData);
|
|
||||||
formRequest.mutate(sendedData);
|
formRequest.mutate(sendedData);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -236,16 +234,16 @@ export function PriceModal() {
|
|||||||
type="tel"
|
type="tel"
|
||||||
id="phone"
|
id="phone"
|
||||||
name="phone"
|
name="phone"
|
||||||
value={formData.phone}
|
value={formData.number}
|
||||||
onChange={handlePhoneChange}
|
onChange={handlePhoneChange}
|
||||||
className={`w-full px-4 py-3 bg-[#1e1e1e] border ${
|
className={`w-full px-4 py-3 bg-[#1e1e1e] border ${
|
||||||
errors.phone ? "border-red-500" : "border-gray-700"
|
errors.number ? "border-red-500" : "border-gray-700"
|
||||||
} rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-red-700 transition`}
|
} rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-red-700 transition`}
|
||||||
placeholder="+998 90 123 45 67"
|
placeholder="+998 90 123 45 67"
|
||||||
maxLength={17}
|
maxLength={17}
|
||||||
/>
|
/>
|
||||||
{errors.phone && (
|
{errors.number && (
|
||||||
<p className="mt-1 text-sm text-red-500">{errors.phone}</p>
|
<p className="mt-1 text-sm text-red-500">{errors.number}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function Providers({ children }: { children: ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<BackAnimatsiya />
|
{/* <BackAnimatsiya /> */}
|
||||||
<Navbar />
|
<Navbar />
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
|
|||||||
<ol
|
<ol
|
||||||
data-slot="breadcrumb-list"
|
data-slot="breadcrumb-list"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5',
|
'text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm wrap-break-words sm:gap-2.5',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
'use client'
|
"use client"
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from "react"
|
||||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
|
||||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
|
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
function DropdownMenu({
|
function DropdownMenu({
|
||||||
...props
|
...props
|
||||||
@@ -16,7 +16,7 @@ function DropdownMenuPortal({
|
|||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
<DropdownMenuPrimitive.Portal {...props} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,8 +42,8 @@ function DropdownMenuContent({
|
|||||||
data-slot="dropdown-menu-content"
|
data-slot="dropdown-menu-content"
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
|
"border-[#1e1d1c] text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md bg-[#1e1d1c] border p-1 shadow-md",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -62,11 +62,11 @@ function DropdownMenuGroup({
|
|||||||
function DropdownMenuItem({
|
function DropdownMenuItem({
|
||||||
className,
|
className,
|
||||||
inset,
|
inset,
|
||||||
variant = 'default',
|
variant = "default",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||||
inset?: boolean
|
inset?: boolean
|
||||||
variant?: 'default' | 'destructive'
|
variant?: "default" | "destructive"
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
@@ -75,7 +75,7 @@ function DropdownMenuItem({
|
|||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -93,7 +93,7 @@ function DropdownMenuCheckboxItem({
|
|||||||
data-slot="dropdown-menu-checkbox-item"
|
data-slot="dropdown-menu-checkbox-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -129,7 +129,7 @@ function DropdownMenuRadioItem({
|
|||||||
data-slot="dropdown-menu-radio-item"
|
data-slot="dropdown-menu-radio-item"
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -155,8 +155,8 @@ function DropdownMenuLabel({
|
|||||||
data-slot="dropdown-menu-label"
|
data-slot="dropdown-menu-label"
|
||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-2 py-1.5 text-sm font-medium data-[inset]:pl-8',
|
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -170,7 +170,7 @@ function DropdownMenuSeparator({
|
|||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuPrimitive.Separator
|
||||||
data-slot="dropdown-menu-separator"
|
data-slot="dropdown-menu-separator"
|
||||||
className={cn('bg-border -mx-1 my-1 h-px', className)}
|
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -179,13 +179,13 @@ function DropdownMenuSeparator({
|
|||||||
function DropdownMenuShortcut({
|
function DropdownMenuShortcut({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'span'>) {
|
}: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="dropdown-menu-shortcut"
|
data-slot="dropdown-menu-shortcut"
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground ml-auto text-xs tracking-widest',
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -212,7 +212,7 @@ function DropdownMenuSubTrigger({
|
|||||||
data-inset={inset}
|
data-inset={inset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
@@ -230,8 +230,8 @@ function DropdownMenuSubContent({
|
|||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
data-slot="dropdown-menu-sub-content"
|
data-slot="dropdown-menu-sub-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
import * as React from 'react'
|
import * as React from "react"
|
||||||
import {
|
import {
|
||||||
ChevronLeftIcon,
|
ChevronLeftIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
MoreHorizontalIcon,
|
MoreHorizontalIcon,
|
||||||
} from 'lucide-react'
|
} from "lucide-react"
|
||||||
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from "@/lib/utils"
|
||||||
import { Button, buttonVariants } from '@/components/ui/button'
|
import { Button, buttonVariants } from "@/components/ui/button"
|
||||||
|
|
||||||
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
|
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
role="navigation"
|
role="navigation"
|
||||||
aria-label="pagination"
|
aria-label="pagination"
|
||||||
data-slot="pagination"
|
data-slot="pagination"
|
||||||
className={cn('mx-auto flex w-full justify-center', className)}
|
className={cn("mx-auto flex w-full justify-center", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -23,42 +23,42 @@ function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
|
|||||||
function PaginationContent({
|
function PaginationContent({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'ul'>) {
|
}: React.ComponentProps<"ul">) {
|
||||||
return (
|
return (
|
||||||
<ul
|
<ul
|
||||||
data-slot="pagination-content"
|
data-slot="pagination-content"
|
||||||
className={cn('flex flex-row items-center gap-1', className)}
|
className={cn("flex flex-row items-center gap-1", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
|
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||||
return <li data-slot="pagination-item" {...props} />
|
return <li data-slot="pagination-item" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaginationLinkProps = {
|
type PaginationLinkProps = {
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
|
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||||
React.ComponentProps<'a'>
|
React.ComponentProps<"a">
|
||||||
|
|
||||||
function PaginationLink({
|
function PaginationLink({
|
||||||
className,
|
className,
|
||||||
isActive,
|
isActive,
|
||||||
size = 'icon',
|
size = "icon",
|
||||||
...props
|
...props
|
||||||
}: PaginationLinkProps) {
|
}: PaginationLinkProps) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
aria-current={isActive ? 'page' : undefined}
|
aria-current={isActive ? "page" : undefined}
|
||||||
data-slot="pagination-link"
|
data-slot="pagination-link"
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
className={cn(
|
className={cn(
|
||||||
buttonVariants({
|
buttonVariants({
|
||||||
variant: isActive ? 'outline' : 'ghost',
|
variant: isActive ? "outline" : "ghost",
|
||||||
size,
|
size,
|
||||||
}),
|
}),
|
||||||
className,
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -73,11 +73,10 @@ function PaginationPrevious({
|
|||||||
<PaginationLink
|
<PaginationLink
|
||||||
aria-label="Go to previous page"
|
aria-label="Go to previous page"
|
||||||
size="default"
|
size="default"
|
||||||
className={cn('gap-1 px-2.5 sm:pl-2.5', className)}
|
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon />
|
<ChevronLeftIcon />
|
||||||
<span className="hidden sm:block">Previous</span>
|
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -90,10 +89,9 @@ function PaginationNext({
|
|||||||
<PaginationLink
|
<PaginationLink
|
||||||
aria-label="Go to next page"
|
aria-label="Go to next page"
|
||||||
size="default"
|
size="default"
|
||||||
className={cn('gap-1 px-2.5 sm:pr-2.5', className)}
|
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<span className="hidden sm:block">Next</span>
|
|
||||||
<ChevronRightIcon />
|
<ChevronRightIcon />
|
||||||
</PaginationLink>
|
</PaginationLink>
|
||||||
)
|
)
|
||||||
@@ -102,12 +100,12 @@ function PaginationNext({
|
|||||||
function PaginationEllipsis({
|
function PaginationEllipsis({
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<'span'>) {
|
}: React.ComponentProps<"span">) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
aria-hidden
|
aria-hidden
|
||||||
data-slot="pagination-ellipsis"
|
data-slot="pagination-ellipsis"
|
||||||
className={cn('flex size-9 items-center justify-center', className)}
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<MoreHorizontalIcon className="size-4" />
|
<MoreHorizontalIcon className="size-4" />
|
||||||
|
|||||||
33
lib/animations.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const itemVariants = {
|
||||||
|
hidden: { opacity: 0, y: 30 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.6,
|
||||||
|
ease: [0.22, 1, 0.36, 1] as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cardVariants = {
|
||||||
|
hidden: { opacity: 0, scale: 0.95 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.5,
|
||||||
|
ease: [0.22, 1, 0.36, 1] as const,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
27
lib/api/demoapi/operationalSystems.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://api.example.com';
|
||||||
|
|
||||||
|
export interface SystemFeature {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
shortDesc?:string;
|
||||||
|
description: string;
|
||||||
|
features: string[];
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOperationalSystems = async (): Promise<SystemFeature[]> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${API_BASE_URL}/operational-systems`, {
|
||||||
|
timeout: 10000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching operational systems:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,3 +1,47 @@
|
|||||||
|
import { title } from "process";
|
||||||
|
|
||||||
|
export const certs = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
src: "/images/about/sertificate.webp",
|
||||||
|
title: "Пожаростойкие армированные трубы SLT BLOCKFIRE PP-R-GF",
|
||||||
|
year: "2024",
|
||||||
|
artikul: "PP-R-GF",
|
||||||
|
features: [
|
||||||
|
"СТО 22.21.29-015-17207509-2022 (версия 2) — согласован МЧС России в качестве нормативного документа по пожарной безопасности.",
|
||||||
|
"СТО 22.21.29-021-17207509-2024 «Автоматическая противопожарная защита многоярусных стеллажных конструкций» — согласован МЧС России №ГУ-исх-66586 от 05.07.2024.",
|
||||||
|
"Протоколы испытаний по ГОСТ Р 58832 ИЛ НИЦ ПТ и СП ФГБУ ВНИИПО МЧС России № 2249/2.1-2022 от 03.03.2022, №2683/2.1-2023 от 06.10.2023.",
|
||||||
|
"Свидетельство о государственной регистрации № RU.77.01.34.013.E.001631.07.20 от 07.07.2020.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
src: "/images/about/sertificate.webp",
|
||||||
|
title: "Пожаростойкие однослойные трубы SLT BLOCKFIRE PP-R",
|
||||||
|
year: "2023",
|
||||||
|
artikul: "PP-R",
|
||||||
|
features: [
|
||||||
|
"СТО 22.21.29-015-17207509-2022 (версия 2) — согласован МЧС России в качестве нормативного документа по пожарной безопасности.",
|
||||||
|
"СТО 22.21.29-021-17207509-2024 «Автоматическая противопожарная защита многоярусных стеллажных конструкций» — согласован МЧС России №ГУ-исх-66586 от 05.07.2024.",
|
||||||
|
"Протоколы испытаний ИЛ НИЦ ПТ и СП ФГБУ ВНИИПО МЧС России № 2249/2.1-2022 от 03.03.2022, №2683/2.1-2023 от 06.10.2023.",
|
||||||
|
"Свидетельство о государственной регистрации № RU.77.01.34.008.E.001638.07.20 от 08.07.2020.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
src: "/images/about/sertificate.webp",
|
||||||
|
title: "Пожаростойкие фитинги SLT BLOCKFIRE PP-R",
|
||||||
|
year: "2023",
|
||||||
|
artikul: "Фитинги",
|
||||||
|
features: [
|
||||||
|
"СТО 22.21.29-015-17207509-2022 (версия 2) — согласован МЧС России в качестве нормативного документа по пожарной безопасности.",
|
||||||
|
"СТО 22.21.29-021-17207509-2024 «Автоматическая противопожарная защита многоярусных стеллажных конструкций» — согласован МЧС России №ГУ-исх-66586 от 05.07.2024.",
|
||||||
|
"Протоколы испытаний по ГОСТ Р 58832 ИЛ НИЦ ПТ и СП ФГБУ ВНИИПО МЧС России № 2249/2.1-2022 от 03.03.2022, №2683/2.1-2023 от 06.10.2023.",
|
||||||
|
"Свидетельство о государственной регистрации № RU.77.01.34.013.E.001630.07.20 от 07.07.2020.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const DATA = [
|
export const DATA = [
|
||||||
{
|
{
|
||||||
name: "P-0834405",
|
name: "P-0834405",
|
||||||
@@ -238,3 +282,25 @@ export const result = [
|
|||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const normativeData = [
|
||||||
|
{
|
||||||
|
title: "certs.slt_blockfire.title",
|
||||||
|
artikul: "SLT BLOCKFIRE",
|
||||||
|
features: [
|
||||||
|
"certs.slt_blockfire.doc1",
|
||||||
|
"certs.slt_blockfire.doc2",
|
||||||
|
"certs.slt_blockfire.doc3",
|
||||||
|
"certs.slt_blockfire.doc4",
|
||||||
|
"certs.slt_blockfire.doc5",
|
||||||
|
"certs.slt_blockfire.doc6",
|
||||||
|
"certs.slt_blockfire.doc7",
|
||||||
|
"certs.slt_blockfire.doc8",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "certs.slt_aqua.title",
|
||||||
|
artikul: "SLT AQUA",
|
||||||
|
features: ["certs.slt_aqua.doc1"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
23
lib/types.ts
@@ -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[];
|
||||||
|
}
|
||||||
201
messages/en.json
@@ -120,6 +120,64 @@
|
|||||||
},
|
},
|
||||||
"contact": "CONTACT US",
|
"contact": "CONTACT US",
|
||||||
"award": "Best Fire Protection Award 2025"
|
"award": "Best Fire Protection Award 2025"
|
||||||
|
},
|
||||||
|
"subPages": {
|
||||||
|
"baza": "Regulatory base",
|
||||||
|
"certificate": "Certificates",
|
||||||
|
"notePP": "Guides"
|
||||||
|
},
|
||||||
|
"normativBaza": {
|
||||||
|
"hero": {
|
||||||
|
"label": "Documents & Standards",
|
||||||
|
"title1": "Regulatory",
|
||||||
|
"title2": "Framework",
|
||||||
|
"description": "Our company installs and supplies fire protection equipment and systems in accordance with current regulatory and legal standards."
|
||||||
|
},
|
||||||
|
"sectionLabel": "Main Directions",
|
||||||
|
"cards": {
|
||||||
|
"card1": {
|
||||||
|
"title": "State Standards",
|
||||||
|
"text": "Equipment and installation works fully comply with national fire safety standards."
|
||||||
|
},
|
||||||
|
"card2": {
|
||||||
|
"title": "Technical Regulations",
|
||||||
|
"text": "All fire protection systems are designed and installed in compliance with current technical regulations."
|
||||||
|
},
|
||||||
|
"card3": {
|
||||||
|
"title": "Safety Requirements",
|
||||||
|
"text": "Each project is individually analyzed, taking into account the safety level of the facility."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bottomText": "All works are carried out in accordance with state standards and safety requirements."
|
||||||
|
},
|
||||||
|
"certificatePage": {
|
||||||
|
"hero": {
|
||||||
|
"label": "Official Approvals",
|
||||||
|
"title1": "Certifi",
|
||||||
|
"title2": "cates",
|
||||||
|
"description": "Official certificates and documents confirming the quality of our products and installation services."
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"suffix": "certificates",
|
||||||
|
"description": "Number of official certificates and approval documents obtained by our company during its operations."
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"badge": "Official Document",
|
||||||
|
"view": "View",
|
||||||
|
"download": "Download"
|
||||||
|
},
|
||||||
|
"certificates": {
|
||||||
|
"cert1": {
|
||||||
|
"title": "Certificate 1",
|
||||||
|
"desc": "Official certificate authorizing installation and supply of fire protection systems."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notePPPage": {
|
||||||
|
"title": "Installation Instructions",
|
||||||
|
"varnix": "Installation instructions for welded saddles 2025",
|
||||||
|
"ppFlanes": "Installation instructions for PP flanges",
|
||||||
|
"ppFiting": "Installation instructions for PP pipes and fittings"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
@@ -136,16 +194,20 @@
|
|||||||
"subject": "Subject",
|
"subject": "Subject",
|
||||||
"message": "Leave us a message"
|
"message": "Leave us a message"
|
||||||
},
|
},
|
||||||
"privacy": "You agree to our friendly privacy policy",
|
"privacy": "You agree to our Privacy Policy.",
|
||||||
"send": "SEND MESSAGE",
|
"send": "SEND MESSAGE",
|
||||||
"email": "EMAIL",
|
"email": "EMAIL",
|
||||||
"emailAddress": "info@ignum-tech.com",
|
"emailAddress": "info@ignum-tech.com",
|
||||||
"location": "Our Location",
|
"location": "Our Location",
|
||||||
"address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy",
|
"address": "39, 3rd Niyozbek Yoli Street, Yunusabad District, Tashkent, Uzbekistan",
|
||||||
"phone": "Phone"
|
"phone": "Phone"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"products": {
|
"products": {
|
||||||
|
"noData": {
|
||||||
|
"title": "Catalog Not Found",
|
||||||
|
"description": "There are currently no categories available. Please check back later."
|
||||||
|
},
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"blockdescription": "Polypropylene pipes and fittings for automatic fire suppression systems and internal fire water supply",
|
"blockdescription": "Polypropylene pipes and fittings for automatic fire suppression systems and internal fire water supply",
|
||||||
"cadescription": "Equipment for automatic fire suppression",
|
"cadescription": "Equipment for automatic fire suppression",
|
||||||
@@ -156,7 +218,13 @@
|
|||||||
"subtitle": "Ignum Technology Ready",
|
"subtitle": "Ignum Technology Ready",
|
||||||
"description": "We not only supply equipment but become a successful partner for every client."
|
"description": "We not only supply equipment but become a successful partner for every client."
|
||||||
},
|
},
|
||||||
"ourproducts": "Our Products"
|
"ourproducts": "Our Products",
|
||||||
|
"price": "Price",
|
||||||
|
"send": "Send Message",
|
||||||
|
"share": "Share",
|
||||||
|
"features": "Features",
|
||||||
|
"feature": "Feature",
|
||||||
|
"copied": "Link copied!"
|
||||||
},
|
},
|
||||||
"faq": {
|
"faq": {
|
||||||
"banner": {
|
"banner": {
|
||||||
@@ -199,13 +267,15 @@
|
|||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"home": "HOME",
|
"home": "HOME",
|
||||||
"about": "ABOUT",
|
"about": "ABOUT COMPANY",
|
||||||
"pages": "PAGES",
|
"pages": "PAGES",
|
||||||
"services": "SERVICES",
|
"services": "SERVICES",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
"products": "PRODUCTS",
|
"products": "PRODUCTS",
|
||||||
"contact": "CONTACT",
|
"contact": "CONTACT",
|
||||||
"emergency": "Emergency Call!"
|
"emergency": "Emergency Call!",
|
||||||
|
"catalog": "Catalog",
|
||||||
|
"connect": "Connect with us"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"description": "We provide professional services for the installation of fire safety systems and the sale of certified protective equipment.",
|
"description": "We provide professional services for the installation of fire safety systems and the sale of certified protective equipment.",
|
||||||
@@ -222,16 +292,57 @@
|
|||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"help": "Help"
|
"help": "Help"
|
||||||
},
|
},
|
||||||
"address":"Tashkent city, Yunusabad district, 3rd dead-end of Niyozbek Yoli street, house 39"
|
"address": "Tashkent city, Yunusabad district, 3rd dead-end of Niyozbek Yoli street, house 39",
|
||||||
|
"create": "Created by {name}",
|
||||||
|
"terms": "Terms & Conditions",
|
||||||
|
"privacy": "Privacy Policy"
|
||||||
|
},
|
||||||
|
"operationalSystems": {
|
||||||
|
"title": "Operating Systems",
|
||||||
|
"subtitle": "Automatic fire detection and extinguishing systems. Latest technological achievements.",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"error": "An error occurred. Please try again.",
|
||||||
|
"noData": {
|
||||||
|
"empty": "No data found",
|
||||||
|
"title": "No Services Found",
|
||||||
|
"description": "There are currently no services available. New services will be added soon."
|
||||||
|
},
|
||||||
|
"retry": "Retry",
|
||||||
|
"features": "Features",
|
||||||
|
"systems": {
|
||||||
|
"sprinkler": {
|
||||||
|
"title": "Sprinkler Fire Suppression System",
|
||||||
|
"short-desc": "Automatic fire detection and extinguishing systems. The latest achievements in technology.",
|
||||||
|
"description": "The sprinkler fire suppression system controls and extinguishes fires at an early stage through automatic water spraying. The system activates automatically when temperature rises and operates only in the fire detection area.",
|
||||||
|
"features": [
|
||||||
|
"Automatic activation mechanism",
|
||||||
|
"Covers only the affected area with water",
|
||||||
|
"Suitable for industrial and commercial buildings",
|
||||||
|
"Reliable and widely used system"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gas": {
|
||||||
|
"title": "Gas Fire Suppression System",
|
||||||
|
"description": "The gas fire suppression system extinguishes fires using special inert or chemical gases. This system is used in places where water cannot be used — server rooms, data centers, and areas with electrical equipment.",
|
||||||
|
"features": [
|
||||||
|
"Does not damage electrical equipment",
|
||||||
|
"Fast and effective suppression",
|
||||||
|
"Leaves no residue",
|
||||||
|
"Ideal for server and IT rooms"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"foam": {
|
||||||
|
"title": "Foam Fire Suppression System",
|
||||||
|
"description": "The foam fire suppression system effectively extinguishes fires involving flammable liquids and petroleum products. The foam covers the combustible material, blocks oxygen access, and quickly suppresses the flames.",
|
||||||
|
"features": [
|
||||||
|
"Effective for flammable liquids",
|
||||||
|
"Used in oil depots and warehouses",
|
||||||
|
"Quickly isolates fire",
|
||||||
|
"High safety level"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"rasmlar": "Images",
|
|
||||||
"fotogalereya": "Photo Gallery",
|
|
||||||
"contactTitle": "Send us your phone number",
|
|
||||||
"contactSubTitle": "Our staff will contact you",
|
|
||||||
"enterPhone": "Enter your phone number",
|
|
||||||
"send": "Sent",
|
|
||||||
"error":"Error!",
|
|
||||||
"succes":"sent!",
|
|
||||||
"priceModal": {
|
"priceModal": {
|
||||||
"title": "Get Price",
|
"title": "Get Price",
|
||||||
"product": {
|
"product": {
|
||||||
@@ -255,5 +366,67 @@
|
|||||||
},
|
},
|
||||||
"success": "Your request has been sent successfully!",
|
"success": "Your request has been sent successfully!",
|
||||||
"error": "An error occurred. Please try again."
|
"error": "An error occurred. Please try again."
|
||||||
|
},
|
||||||
|
"breadcrumb": {
|
||||||
|
"home": "Home",
|
||||||
|
"about": "About Us",
|
||||||
|
"services": "Services",
|
||||||
|
"products": "Products",
|
||||||
|
"contact": "Contact",
|
||||||
|
"blog": "Blog",
|
||||||
|
"catalog_page": "Products",
|
||||||
|
"fire-safety": "Fire Safety",
|
||||||
|
"fire-alarm": "Fire Alarm",
|
||||||
|
"fire-suppression": "Fire Suppression",
|
||||||
|
"installation": "Installation",
|
||||||
|
"maintenance": "Maintenance"
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"category": "Categories",
|
||||||
|
"catalog": "Section",
|
||||||
|
"size": "Sizes"
|
||||||
|
},
|
||||||
|
"certs": {
|
||||||
|
"slt_blockfire": {
|
||||||
|
"title": "Design and Installation of SLT BLOCKFIRE Plastic Pipes",
|
||||||
|
"doc1": "STO 22.21.29-015-17207509-2022, approved by Uzbekistan MES",
|
||||||
|
"doc2": "Fire resistance tests in FGBU VNIIPO Uzbekistan labs",
|
||||||
|
"doc3": "Certification test reports №14143/1 06.09.2018",
|
||||||
|
"doc4": "Fire resistance research reports 29.06.2022 and 11.01.2023",
|
||||||
|
"doc5": "Test protocols for SLT BLOCKFIRE pipes and fittings №2249/2.1-2022",
|
||||||
|
"doc6": "Test protocols for SLT BLOCKFIRE pipes and fittings №2683/2.1-2023",
|
||||||
|
"doc7": "Test protocols for SLT BLOCKFIRE pipes and fittings №134/18-07.2024/12-1/Д-3556",
|
||||||
|
"doc8": "Fire resistance tests for AUP-S-M №131/26-12.2023/12-1/Д-3190"
|
||||||
|
},
|
||||||
|
"slt_aqua": {
|
||||||
|
"title": "SLT AQUA Automatic Fire Protection System",
|
||||||
|
"doc1": "STO 22.21.29-021-17207509-2023, approved by Uzbekistan MES"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"aboutCerts": {
|
||||||
|
"certificatePage": {
|
||||||
|
"card": {
|
||||||
|
"badge": "certificate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rasmlar": "Images",
|
||||||
|
"fotogalereya": "Photo Gallery",
|
||||||
|
"contactTitle": "Send us your phone number",
|
||||||
|
"contactSubTitle": "Our staff will contact you",
|
||||||
|
"enterPhone": "Enter your phone number",
|
||||||
|
"send": "Sent",
|
||||||
|
"error": "Error!",
|
||||||
|
"succes": "sent!",
|
||||||
|
"loadingError": "An error occurred while loading data",
|
||||||
|
"productsNotFound": "Products not found",
|
||||||
|
"subcategory_not_found": "Subcategory not found",
|
||||||
|
"section": "Section",
|
||||||
|
"clear_all": "Clear all",
|
||||||
|
"image_not_found": "Image not available",
|
||||||
|
"loading_error": "An error occurred while loading data",
|
||||||
|
"products_not_found": "Products not found",
|
||||||
|
"hide": "Hide",
|
||||||
|
"show_more": "Show more",
|
||||||
|
"category": "Categories"
|
||||||
}
|
}
|
||||||
|
|||||||
207
messages/ru.json
@@ -120,6 +120,64 @@
|
|||||||
},
|
},
|
||||||
"contact": "СВЯЗАТЬСЯ С НАМИ",
|
"contact": "СВЯЗАТЬСЯ С НАМИ",
|
||||||
"award": "Лучшая Пожарная Защита 2025"
|
"award": "Лучшая Пожарная Защита 2025"
|
||||||
|
},
|
||||||
|
"subPages": {
|
||||||
|
"baza": "Нормативная база",
|
||||||
|
"certificate": "Сертификаты",
|
||||||
|
"notePP": "Инструкция"
|
||||||
|
},
|
||||||
|
"normativBaza": {
|
||||||
|
"hero": {
|
||||||
|
"label": "Документы и стандарты",
|
||||||
|
"title1": "Нормативная",
|
||||||
|
"title2": "База",
|
||||||
|
"description": "Наша компания осуществляет установку и продажу противопожарных средств и систем на основании действующих нормативно-правовых документов."
|
||||||
|
},
|
||||||
|
"sectionLabel": "Основные направления",
|
||||||
|
"cards": {
|
||||||
|
"card1": {
|
||||||
|
"title": "Государственные стандарты",
|
||||||
|
"text": "Оборудование и монтажные работы полностью соответствуют национальным стандартам пожарной безопасности."
|
||||||
|
},
|
||||||
|
"card2": {
|
||||||
|
"title": "Технические регламенты",
|
||||||
|
"text": "При проектировании и установке противопожарных систем соблюдаются действующие технические регламенты."
|
||||||
|
},
|
||||||
|
"card3": {
|
||||||
|
"title": "Требования безопасности",
|
||||||
|
"text": "Каждый проект анализируется индивидуально с учетом уровня безопасности объекта."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bottomText": "Все работы выполняются в соответствии с государственными стандартами и требованиями безопасности."
|
||||||
|
},
|
||||||
|
"certificatePage": {
|
||||||
|
"hero": {
|
||||||
|
"label": "Официальные подтверждения",
|
||||||
|
"title1": "Сертифи",
|
||||||
|
"title2": "каты",
|
||||||
|
"description": "Официальные сертификаты и документы, подтверждающие качество нашей продукции и монтажных работ."
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"suffix": "сертификатов",
|
||||||
|
"description": "Количество официальных сертификатов и подтверждающих документов, полученных компанией за время деятельности."
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"badge": "Официальный документ",
|
||||||
|
"view": "Просмотреть",
|
||||||
|
"download": "Скачать"
|
||||||
|
},
|
||||||
|
"certificates": {
|
||||||
|
"cert1": {
|
||||||
|
"title": "Сертификат 1",
|
||||||
|
"desc": "Официальный сертификат, подтверждающий право на установку и поставку систем пожарной безопасности."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notePPPage": {
|
||||||
|
"title": "Инструкция по монтажу",
|
||||||
|
"varnix": "Инструкция по монтажу вварных седел 2025",
|
||||||
|
"ppFlanes": "Инструкция по монтажу фланцев из ПП",
|
||||||
|
"ppFiting": "Инструкция по монтажу ПП труб и фитингов"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
@@ -136,16 +194,20 @@
|
|||||||
"subject": "Тема",
|
"subject": "Тема",
|
||||||
"message": "Оставьте нам сообщение"
|
"message": "Оставьте нам сообщение"
|
||||||
},
|
},
|
||||||
"privacy": "Вы соглашаетесь с нашей дружественной политикой конфиденциальности",
|
"privacy": "Вы соглашаетесь с нашей Политикой конфиденциальности.",
|
||||||
"send": "ОТПРАВИТЬ СООБЩЕНИЕ",
|
"send": "ОТПРАВИТЬ СООБЩЕНИЕ",
|
||||||
"email": "ЭЛЕКТРОННАЯ ПОЧТА",
|
"email": "ЭЛЕКТРОННАЯ ПОЧТА",
|
||||||
"emailAddress": "info@ignum-tech.com",
|
"emailAddress": "info@ignum-tech.com",
|
||||||
"location": "Наше Местоположение",
|
"location": "Наше Местоположение",
|
||||||
"address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy",
|
"address": "г. Ташкент, Юнусабадский район, 3-й проезд Ниёзбек йули, дом 39",
|
||||||
"phone": "Телефон"
|
"phone": "Телефон"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"products": {
|
"products": {
|
||||||
|
"noData": {
|
||||||
|
"title": "Каталог не найден",
|
||||||
|
"description": "В настоящее время категории отсутствуют. Пожалуйста, зайдите позже."
|
||||||
|
},
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"blockdescription": "Полипропиленовые трубы и фитинги для систем автоматического пожаротушения и внутреннего противопожарного водопровода",
|
"blockdescription": "Полипропиленовые трубы и фитинги для систем автоматического пожаротушения и внутреннего противопожарного водопровода",
|
||||||
"cadescription": "Оборудование для автоматического пожаротушения",
|
"cadescription": "Оборудование для автоматического пожаротушения",
|
||||||
@@ -156,11 +218,17 @@
|
|||||||
"subtitle": "Технология Ignum Готова",
|
"subtitle": "Технология Ignum Готова",
|
||||||
"description": "Мы не просто поставляем оборудование, мы становимся успешным партнером для каждого клиента."
|
"description": "Мы не просто поставляем оборудование, мы становимся успешным партнером для каждого клиента."
|
||||||
},
|
},
|
||||||
"ourproducts": "Наши продукты"
|
"ourproducts": "Наши продукты",
|
||||||
|
"price": "Цена",
|
||||||
|
"send": "Отправить сообщение",
|
||||||
|
"share": "Поделиться",
|
||||||
|
"features": "Характеристики",
|
||||||
|
"feature": "Характеристика",
|
||||||
|
"copied": "Ссылка скопирована!"
|
||||||
},
|
},
|
||||||
"faq": {
|
"faq": {
|
||||||
"banner": {
|
"banner": {
|
||||||
"title": "FAQ",
|
"title": "ЧЗВ",
|
||||||
"subtitle": "Общие Вопросы",
|
"subtitle": "Общие Вопросы",
|
||||||
"topic": "О РАБОТЕ"
|
"topic": "О РАБОТЕ"
|
||||||
},
|
},
|
||||||
@@ -199,13 +267,15 @@
|
|||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"home": "ГЛАВНАЯ",
|
"home": "ГЛАВНАЯ",
|
||||||
"about": "О НАС",
|
"about": "О компании",
|
||||||
"pages": "СТРАНИЦЫ",
|
"pages": "СТРАНИЦЫ",
|
||||||
"services": "УСЛУГИ",
|
"services": "УСЛУГИ",
|
||||||
"faq": "FAQ",
|
"faq": "ЧЗВ",
|
||||||
"products": "ПРОДУКТЫ",
|
"products": "ПРОДУКТЫ",
|
||||||
"contact": "КОНТАКТЫ",
|
"contact": "КОНТАКТЫ",
|
||||||
"emergency": "Экстренный Вызов!"
|
"emergency": "Экстренный Вызов!",
|
||||||
|
"catalog": "Каталог",
|
||||||
|
"connect": "Связаться с Нами"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"description": "Мы предоставляем профессиональные услуги по установке систем пожарной безопасности и продаже сертифицированных средств защиты.",
|
"description": "Мы предоставляем профессиональные услуги по установке систем пожарной безопасности и продаже сертифицированных средств защиты.",
|
||||||
@@ -215,23 +285,64 @@
|
|||||||
"about": "О нас",
|
"about": "О нас",
|
||||||
"services": "Услуги",
|
"services": "Услуги",
|
||||||
"products": "Продукты",
|
"products": "Продукты",
|
||||||
"faq": "FAQ"
|
"faq": "ЧЗВ"
|
||||||
},
|
},
|
||||||
"support": {
|
"support": {
|
||||||
"title": "ПОДДЕРЖКА",
|
"title": "ПОДДЕРЖКА",
|
||||||
"contact": "Контакты",
|
"contact": "Контакты",
|
||||||
"help": "Помощь"
|
"help": "Помощь"
|
||||||
},
|
},
|
||||||
"address":"г. Ташкент, Юнусабадский район, 3-й тупик улицы Ниязбек йўли, дом 39"
|
"address": "г. Ташкент, Юнусабадский район, 3-й тупик улицы Ниязбек йўли, дом 39",
|
||||||
|
"create": "Разработано",
|
||||||
|
"terms": "Условия использования",
|
||||||
|
"privacy": "Политика конфиденциальности"
|
||||||
|
},
|
||||||
|
"operationalSystems": {
|
||||||
|
"title": "Операционные системы",
|
||||||
|
"subtitle": "Системы автоматического обнаружения и тушения пожаров. Последние достижения технологий.",
|
||||||
|
"loading": "Загрузка...",
|
||||||
|
"error": "Произошла ошибка. Пожалуйста, попробуйте снова.",
|
||||||
|
"noData": {
|
||||||
|
"empty": "Данные не найдены",
|
||||||
|
"title": "Услуги не найдены",
|
||||||
|
"description": "В настоящее время нет доступных услуг. Новые услуги будут добавлены в ближайшее время."
|
||||||
|
},
|
||||||
|
"retry": "Повторить",
|
||||||
|
"features": "Характеристики",
|
||||||
|
"systems": {
|
||||||
|
"sprinkler": {
|
||||||
|
"title": "Спринклерная система пожаротушения",
|
||||||
|
"short-desc": "Системы автоматического обнаружения и тушения пожаров. Новейшие достижения технологий.",
|
||||||
|
"description": "Спринклерная система пожаротушения контролирует и тушит пожар на начальной стадии путем автоматического распыления воды. Система автоматически активируется при повышении температуры и работает только в зоне обнаружения пожара.",
|
||||||
|
"features": [
|
||||||
|
"Автоматический механизм активации",
|
||||||
|
"Покрывает водой только поврежденную зону",
|
||||||
|
"Подходит для промышленных и торговых зданий",
|
||||||
|
"Надежная и широко применяемая система"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gas": {
|
||||||
|
"title": "Газовая система пожаротушения",
|
||||||
|
"description": "Газовая система пожаротушения тушит пожар с помощью специальных инертных или химических газов. Эта система применяется в местах, где нельзя использовать воду — серверные комнаты, дата-центры и помещения с электрооборудованием.",
|
||||||
|
"features": [
|
||||||
|
"Не наносит вреда электрооборудованию",
|
||||||
|
"Быстрое и эффективное тушение",
|
||||||
|
"Не оставляет остатков",
|
||||||
|
"Идеально для серверных и IT помещений"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"foam": {
|
||||||
|
"title": "Пенная система пожаротушения",
|
||||||
|
"description": "Пенная система пожаротушения эффективно тушит пожары, связанные с горючими жидкостями и нефтепродуктами. Пена покрывает горючее вещество, перекрывает доступ кислорода и быстро подавляет огонь.",
|
||||||
|
"features": [
|
||||||
|
"Эффективна для горючих жидкостей",
|
||||||
|
"Применяется на нефтебазах и складах",
|
||||||
|
"Быстро изолирует пожар",
|
||||||
|
"Высокий уровень безопасности"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"rasmlar": "Изображения",
|
|
||||||
"fotogalereya": "Фотогалерея",
|
|
||||||
"contactTitle": "Отправьте нам свой номер",
|
|
||||||
"contactSubTitle": "Наши сотрудники свяжутся с вами",
|
|
||||||
"enterPhone": "Введите ваш номер телефона",
|
|
||||||
"send": "Отправить",
|
|
||||||
"error": "Ошибка!",
|
|
||||||
"succes": "Отправлено!",
|
|
||||||
"priceModal": {
|
"priceModal": {
|
||||||
"title": "Узнать цену",
|
"title": "Узнать цену",
|
||||||
"product": {
|
"product": {
|
||||||
@@ -255,5 +366,67 @@
|
|||||||
},
|
},
|
||||||
"success": "Ваш запрос успешно отправлен!",
|
"success": "Ваш запрос успешно отправлен!",
|
||||||
"error": "Произошла ошибка. Попробуйте снова."
|
"error": "Произошла ошибка. Попробуйте снова."
|
||||||
|
},
|
||||||
|
"breadcrumb": {
|
||||||
|
"home": "Главная",
|
||||||
|
"about": "О нас",
|
||||||
|
"services": "Услуги",
|
||||||
|
"products": "Продукция",
|
||||||
|
"contact": "Контакты",
|
||||||
|
"blog": "Блог",
|
||||||
|
"catalog_page": "Товары",
|
||||||
|
"fire-safety": "Пожарная безопасность",
|
||||||
|
"fire-alarm": "Пожарная сигнализация",
|
||||||
|
"fire-suppression": "Пожаротушение",
|
||||||
|
"installation": "Монтаж",
|
||||||
|
"maintenance": "Техническое обслуживание"
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"category": "Категории",
|
||||||
|
"catalog": "Раздел",
|
||||||
|
"size": "Размеры"
|
||||||
|
},
|
||||||
|
"certs": {
|
||||||
|
"slt_blockfire": {
|
||||||
|
"title": "Проектирование и монтаж пластиковых труб SLT BLOCKFIRE",
|
||||||
|
"doc1": "СТО 22.21.29-015-17207509-2022, утверждено МЧС Узбекистана",
|
||||||
|
"doc2": "Испытания на огнестойкость в лабораториях ФГБУ ВНИИПО Узбекистан",
|
||||||
|
"doc3": "Отчеты о сертификационных испытаниях №14143/1 06.09.2018",
|
||||||
|
"doc4": "Отчеты по исследованиям огнестойкости 29.06.2022 и 11.01.2023",
|
||||||
|
"doc5": "Протокол испытаний труб и фитингов SLT BLOCKFIRE №2249/2.1-2022",
|
||||||
|
"doc6": "Протокол испытаний труб и фитингов SLT BLOCKFIRE №2683/2.1-2023",
|
||||||
|
"doc7": "Протокол испытаний труб и фитингов SLT BLOCKFIRE №134/18-07.2024/12-1/Д-3556",
|
||||||
|
"doc8": "Протокол испытаний огнестойкости AUP-S-M №131/26-12.2023/12-1/Д-3190"
|
||||||
|
},
|
||||||
|
"slt_aqua": {
|
||||||
|
"title": "Автоматическая противопожарная защита SLT AQUA",
|
||||||
|
"doc1": "СТО 22.21.29-021-17207509-2023, утверждено МЧС Узбекистана"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"aboutCerts": {
|
||||||
|
"certificatePage": {
|
||||||
|
"card": {
|
||||||
|
"badge": "сертификат"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rasmlar": "Изображения",
|
||||||
|
"fotogalereya": "Фотогалерея",
|
||||||
|
"contactTitle": "Отправьте нам свой номер",
|
||||||
|
"contactSubTitle": "Наши сотрудники свяжутся с вами",
|
||||||
|
"enterPhone": "Введите ваш номер телефона",
|
||||||
|
"send": "Отправить",
|
||||||
|
"error": "Ошибка!",
|
||||||
|
"succes": "Отправлено!",
|
||||||
|
"loadingError": "Произошла ошибка при загрузке данных",
|
||||||
|
"productsNotFound": "Товары не найдены",
|
||||||
|
"subcategory_not_found": "Подкатегория не найдена",
|
||||||
|
"section": "Раздел",
|
||||||
|
"clear_all": "Очистить всё",
|
||||||
|
"image_not_found": "Изображение отсутствует",
|
||||||
|
"loading_error": "Произошла ошибка при загрузке данных",
|
||||||
|
"products_not_found": "Товары не найдены",
|
||||||
|
"hide": "Скрыть",
|
||||||
|
"show_more": "Показать больше",
|
||||||
|
"category": "Категории"
|
||||||
}
|
}
|
||||||
|
|||||||
199
messages/uz.json
@@ -120,6 +120,64 @@
|
|||||||
},
|
},
|
||||||
"contact": "BIZ BILAN BOG'LANISH",
|
"contact": "BIZ BILAN BOG'LANISH",
|
||||||
"award": "Eng Yaxshi Yong'in Himoyasi 2025"
|
"award": "Eng Yaxshi Yong'in Himoyasi 2025"
|
||||||
|
},
|
||||||
|
"subPages": {
|
||||||
|
"baza": "Normativ baza",
|
||||||
|
"certificate": "Sertifikatlar",
|
||||||
|
"notePP": "Qo'llanmalar"
|
||||||
|
},
|
||||||
|
"normativBaza": {
|
||||||
|
"hero": {
|
||||||
|
"label": "Hujjatlar va standartlar",
|
||||||
|
"title1": "Normativ",
|
||||||
|
"title2": "Baza",
|
||||||
|
"description": "Kompaniyamiz yong'inga qarshi vositalar va tizimlarni o'rnatish hamda sotish faoliyatini amaldagi normativ-huquqiy hujjatlar asosida olib boradi."
|
||||||
|
},
|
||||||
|
"sectionLabel": "Asosiy yo'nalishlar",
|
||||||
|
"cards": {
|
||||||
|
"card1": {
|
||||||
|
"title": "Davlat Standartlari",
|
||||||
|
"text": "Yong'in xavfsizligi bo'yicha milliy standartlarga to'liq mos keluvchi uskunalar va montaj ishlari."
|
||||||
|
},
|
||||||
|
"card2": {
|
||||||
|
"title": "Texnik Reglamentlar",
|
||||||
|
"text": "Yong'inga qarshi tizimlarni loyihalash va o'rnatishda amaldagi texnik reglamentlarga rioya qilinadi."
|
||||||
|
},
|
||||||
|
"card3": {
|
||||||
|
"title": "Xavfsizlik Talablari",
|
||||||
|
"text": "Har bir loyiha individual tahlil qilinadi va obyektning xavfsizlik darajasi hisobga olinadi."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bottomText": "Barcha ishlar davlat standartlari va xavfsizlik talablariga muvofiq amalga oshiriladi."
|
||||||
|
},
|
||||||
|
"certificatePage": {
|
||||||
|
"hero": {
|
||||||
|
"label": "Rasmiy tasdiqlar",
|
||||||
|
"title1": "Sertifi",
|
||||||
|
"title2": "katlar",
|
||||||
|
"description": "Bizning mahsulotlar va o'rnatish ishlari sifatini tasdiqlovchi rasmiy sertifikatlar va hujjatlar."
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"suffix": "ta sertifikat",
|
||||||
|
"description": "Kompaniyamiz faoliyati davomida olingan rasmiy sertifikat va tasdiqlash hujjatlari soni."
|
||||||
|
},
|
||||||
|
"card": {
|
||||||
|
"badge": "Rasmiy hujjat",
|
||||||
|
"view": "Ko'rish",
|
||||||
|
"download": "Yuklab olish"
|
||||||
|
},
|
||||||
|
"certificates": {
|
||||||
|
"cert1": {
|
||||||
|
"title": "Sertifikat 1",
|
||||||
|
"desc": "Yong'in xavfsizligi tizimlarini o'rnatish va yetkazib berish faoliyatini amalga oshirish uchun berilgan rasmiy sertifikat."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notePPPage": {
|
||||||
|
"title": "Montaj bo‘yicha ko‘rsatma",
|
||||||
|
"varnix": "Payvandlanadigan sedelkalarni o‘rnatish bo‘yicha yo‘riqnoma",
|
||||||
|
"ppFlanes": "PP flaneslarni o‘rnatish bo‘yicha yo‘riqnoma",
|
||||||
|
"ppFiting": "PP quvurlar va fitinglarni o‘rnatish bo‘yicha yo‘riqnoma"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"contact": {
|
"contact": {
|
||||||
@@ -136,7 +194,7 @@
|
|||||||
"subject": "Mavzu",
|
"subject": "Mavzu",
|
||||||
"message": "Xabaringizni qoldiring"
|
"message": "Xabaringizni qoldiring"
|
||||||
},
|
},
|
||||||
"privacy": "Bizning maxfiylik siyosatimizga rozilik bildirasiz",
|
"privacy": "Siz bizning Maxfiylik siyosatimizga rozilik bildirasiz.",
|
||||||
"send": "XABAR YUBORISH",
|
"send": "XABAR YUBORISH",
|
||||||
"email": "ELEKTRON POCHTA",
|
"email": "ELEKTRON POCHTA",
|
||||||
"emailAddress": "info@ignum-tech.com",
|
"emailAddress": "info@ignum-tech.com",
|
||||||
@@ -146,6 +204,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"products": {
|
"products": {
|
||||||
|
"noData": {
|
||||||
|
"title": "Katalog topilmadi",
|
||||||
|
"description": "Hozircha kategoriyalar mavjud emas. Keyinroq urinib ko'ring"
|
||||||
|
},
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"blockdescription": "Avtomatik yong‘in o‘chirish tizimlari va ichki yong‘inga qarshi suv ta’minoti uchun polipropilen quvurlar va fitinglar",
|
"blockdescription": "Avtomatik yong‘in o‘chirish tizimlari va ichki yong‘inga qarshi suv ta’minoti uchun polipropilen quvurlar va fitinglar",
|
||||||
"cadescription": "Avtomatik yong‘in o‘chirish uchun uskunalar",
|
"cadescription": "Avtomatik yong‘in o‘chirish uchun uskunalar",
|
||||||
@@ -156,7 +218,13 @@
|
|||||||
"subtitle": "Ignum Texnologiyasi Tayyor",
|
"subtitle": "Ignum Texnologiyasi Tayyor",
|
||||||
"description": "Biz nafaqat uskunalar yetkazib beramiz, balki har bir mijozning muvaffaqiyatli hamkoriga aylanamiz."
|
"description": "Biz nafaqat uskunalar yetkazib beramiz, balki har bir mijozning muvaffaqiyatli hamkoriga aylanamiz."
|
||||||
},
|
},
|
||||||
"ourproducts": "Bizning mahsulotlarimiz"
|
"ourproducts": "Bizning mahsulotlarimiz",
|
||||||
|
"price": "Narx",
|
||||||
|
"send": "Xabar yuborish",
|
||||||
|
"share": "Ulashish",
|
||||||
|
"features": "Xususiyatlar",
|
||||||
|
"feature": "Xususiyat",
|
||||||
|
"copied": "Link nusxalandi!"
|
||||||
},
|
},
|
||||||
"faq": {
|
"faq": {
|
||||||
"banner": {
|
"banner": {
|
||||||
@@ -199,13 +267,15 @@
|
|||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"home": "ASOSIY",
|
"home": "ASOSIY",
|
||||||
"about": "BIZ HAQIMIZDA",
|
"about": "KAMPANIYA HAQIDA",
|
||||||
"pages": "SAHIFALAR",
|
"pages": "SAHIFALAR",
|
||||||
"services": "XIZMATLAR",
|
"services": "XIZMATLAR",
|
||||||
"faq": "FAQ",
|
"faq": "FAQ",
|
||||||
"products": "MAHSULOTLAR",
|
"products": "MAHSULOTLAR",
|
||||||
"contact": "ALOQA",
|
"contact": "ALOQA",
|
||||||
"emergency": "Favqulodda Qo'ng'iroq!"
|
"emergency": "Favqulodda Qo'ng'iroq!",
|
||||||
|
"catalog": "Katalog",
|
||||||
|
"connect": "Biz bilan bog'laning"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"description": "Biz yong‘in xavfsizligi tizimlarini o‘rnatish va sertifikatlangan himoya vositalari savdosi bo‘yicha professional xizmatlar ko‘rsatamiz.",
|
"description": "Biz yong‘in xavfsizligi tizimlarini o‘rnatish va sertifikatlangan himoya vositalari savdosi bo‘yicha professional xizmatlar ko‘rsatamiz.",
|
||||||
@@ -222,16 +292,57 @@
|
|||||||
"contact": "Aloqa",
|
"contact": "Aloqa",
|
||||||
"help": "Yordam"
|
"help": "Yordam"
|
||||||
},
|
},
|
||||||
"address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy"
|
"address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy",
|
||||||
|
"create": "{name} - Jamoasi tomonidan ishlab chiqilgan",
|
||||||
|
"terms": "Foydalanish shartlari",
|
||||||
|
"privacy": "Maxfiylik siyosati"
|
||||||
|
},
|
||||||
|
"operationalSystems": {
|
||||||
|
"title": "Operatsion tizimlar",
|
||||||
|
"subtitle": "Yong'inni avtomatik aniqlash va o'chirish tizimlari. Texnologiyaning eng so'nggi yutuqlari.",
|
||||||
|
"loading": "Yuklanmoqda...",
|
||||||
|
"error": "Xatolik yuz berdi. Iltimos, qaytadan urinib ko'ring.",
|
||||||
|
"noData": {
|
||||||
|
"empty": "Ma'lumotlar topilmadi",
|
||||||
|
"title": "Xizmatlar topilmadi",
|
||||||
|
"description": "Hozircha hech qanday xizmat mavjud emas. Tez orada yangi xizmatlar qo'shiladi."
|
||||||
|
},
|
||||||
|
"retry": "Qayta urinish",
|
||||||
|
"features": "Hususiyatlari",
|
||||||
|
"systems": {
|
||||||
|
"sprinkler": {
|
||||||
|
"title": "Sprinklerli yong'in o'chirish tizimi",
|
||||||
|
"short-desc": "Yong'inni avtomatik aniqlash va o'chirish tizimlari. Texnologiyaning eng so'nggi yutuqlari.",
|
||||||
|
"description": "Sprinklerli yong'in o'chirish tizimi avtomatik suv purkash orqali yong'inni dastlabki bosqichida nazorat qiladi va o'chiradi. Tizim harorat oshganda avtomatik ishga tushadi va faqat yong'in aniqlangan hududda faoliyat ko'rsatadi.",
|
||||||
|
"features": [
|
||||||
|
"Avtomatik ishga tushish mexanizmi",
|
||||||
|
"Faqat zararlangan hududni suv bilan qoplaydi",
|
||||||
|
"Sanoat va savdo binolari uchun mos",
|
||||||
|
"Ishonchli va keng qo'llaniladigan tizim"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"gas": {
|
||||||
|
"title": "Gazli yong'in o'chirish tizimi",
|
||||||
|
"description": "Gazli yong'in o'chirish tizimi maxsus inert yoki kimyoviy gazlar yordamida yong'inni o'chiradi. Bu tizim suv ishlatish mumkin bo'lmagan joylarda — server xonalari, data markazlar va elektr jihozlari mavjud hududlarda qo'llaniladi.",
|
||||||
|
"features": [
|
||||||
|
"Elektr jihozlariga zarar yetkazmaydi",
|
||||||
|
"Tez va samarali o'chirish",
|
||||||
|
"Qoldiq modda qoldirmaydi",
|
||||||
|
"Server va IT xonalari uchun ideal"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"foam": {
|
||||||
|
"title": "Ko'pikli yong'in o'chirish tizimi",
|
||||||
|
"description": "Ko'pikli yong'in o'chirish tizimi yonuvchi suyuqliklar va neft mahsulotlari bilan bog'liq yong'inlarni samarali o'chiradi. Ko'pik yonuvchi modda ustini qoplab, kislorod kirishini to'sadi va olovni tezda bostiradi.",
|
||||||
|
"features": [
|
||||||
|
"Yonuvchi suyuqliklar uchun samarali",
|
||||||
|
"Neft bazalari va omborlarda qo'llaniladi",
|
||||||
|
"Yong'inni tez izolyatsiya qiladi",
|
||||||
|
"Yuqori xavfsizlik darajasi"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"rasmlar": "Rasmlar",
|
|
||||||
"fotogalereya": "Fotogalereya",
|
|
||||||
"contactTitle": "Bizga raqamingizni yuboring",
|
|
||||||
"contactSubTitle": "Xodimlarimiz siz bilan bog'lanishadi",
|
|
||||||
"enterPhone": "Telefon raqamingiz kiriting",
|
|
||||||
"send": "Yuborish",
|
|
||||||
"error": "Xatolik!",
|
|
||||||
"succes": "Yuborildi!",
|
|
||||||
"priceModal": {
|
"priceModal": {
|
||||||
"title": "Narxni bilish",
|
"title": "Narxni bilish",
|
||||||
"product": {
|
"product": {
|
||||||
@@ -255,5 +366,67 @@
|
|||||||
},
|
},
|
||||||
"success": "So‘rovingiz muvaffaqiyatli yuborildi!",
|
"success": "So‘rovingiz muvaffaqiyatli yuborildi!",
|
||||||
"error": "Xatolik yuz berdi. Iltimos, qayta urinib ko‘ring."
|
"error": "Xatolik yuz berdi. Iltimos, qayta urinib ko‘ring."
|
||||||
|
},
|
||||||
|
"breadcrumb": {
|
||||||
|
"home": "Bosh sahifa",
|
||||||
|
"about": "Biz haqimizda",
|
||||||
|
"services": "Xizmatlar",
|
||||||
|
"catalog_page": "Mahsulotlar",
|
||||||
|
"subCategory": "{subCategory}",
|
||||||
|
"contact": "Bog'lanish",
|
||||||
|
"blog": "Blog",
|
||||||
|
"fire-safety": "Yong'in xavfsizligi",
|
||||||
|
"fire-alarm": "Yong'in signalizatsiyasi",
|
||||||
|
"fire-suppression": "Yong'in o'chirish",
|
||||||
|
"installation": "O'rnatish",
|
||||||
|
"maintenance": "Texnik xizmat"
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"category": "Kategoriyalar",
|
||||||
|
"catalog": "Bo'lim",
|
||||||
|
"size": "O'lchamlar"
|
||||||
|
},
|
||||||
|
"certs": {
|
||||||
|
"slt_blockfire": {
|
||||||
|
"title": "SLT BLOCKFIRE plastik quvurlarini loyihalash va o‘rnatish",
|
||||||
|
"doc1": "STO 22.21.29-015-17207509-2022, Moslashtirilgan O‘zbekiston MChS tomonidan",
|
||||||
|
"doc2": "O‘tga chidamlilik laboratoriya sinovlari FGBU VNIIPO O‘zbekiston",
|
||||||
|
"doc3": "Sertifikatsion sinov hisobotlari №14143/1 06.09.2018",
|
||||||
|
"doc4": "O‘tga chidamlilik tadqiqotlari hisobotlari 29.06.2022 va 11.01.2023",
|
||||||
|
"doc5": "SLT BLOCKFIRE quvurlari va fittinglar protokollari №2249/2.1-2022",
|
||||||
|
"doc6": "SLT BLOCKFIRE quvurlari va fittinglar protokollari №2683/2.1-2023",
|
||||||
|
"doc7": "SLT BLOCKFIRE quvurlari va fittinglar protokollari №134/18-07.2024/12-1/Д-3556",
|
||||||
|
"doc8": "O‘tga chidamlilik AUP-S-M sinovlari protokoli №131/26-12.2023/12-1/Д-3190"
|
||||||
|
},
|
||||||
|
"slt_aqua": {
|
||||||
|
"title": "SLT AQUA avtomatik yong‘inga qarshi himoya tizimi",
|
||||||
|
"doc1": "STO 22.21.29-021-17207509-2023, O‘zbekiston MChS tomonidan tasdiqlangan"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"aboutCerts": {
|
||||||
|
"certificatePage": {
|
||||||
|
"card": {
|
||||||
|
"badge": "sertifikat"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rasmlar": "Rasmlar",
|
||||||
|
"fotogalereya": "Fotogalereya",
|
||||||
|
"contactTitle": "Bizga raqamingizni yuboring",
|
||||||
|
"contactSubTitle": "Xodimlarimiz siz bilan bog'lanishadi",
|
||||||
|
"enterPhone": "Telefon raqamingiz kiriting",
|
||||||
|
"send": "Yuborish",
|
||||||
|
"error": "Xatolik!",
|
||||||
|
"succes": "Yuborildi!",
|
||||||
|
"loadingError": "Ma'lumotlarni yuklashda xatolik yuz berdi",
|
||||||
|
"productsNotFound": "Mahsulotlar topilmadi",
|
||||||
|
"subcategory_not_found": "Subkategoriya topilmadi",
|
||||||
|
"section": "Bo'lim",
|
||||||
|
"clear_all": "Barchasini tozalash",
|
||||||
|
"image_not_found": "Rasm mavjud emas",
|
||||||
|
"loading_error": "Ma'lumotlarni yuklashda xatolik yuz berdi",
|
||||||
|
"products_not_found": "Mahsulotlar topilmadi",
|
||||||
|
"hide":"Yashirish",
|
||||||
|
"show_more":"Ko'proq ko'rish",
|
||||||
|
"category": "Kategoriyalar"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
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",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal flow - just pass locale in headers
|
return response;
|
||||||
const response = NextResponse.next();
|
|
||||||
const requestHeaders = new Headers(request.headers);
|
|
||||||
requestHeaders.set("x-locale", localeFromPath);
|
|
||||||
requestHeaders.set("x-pathname", pathname);
|
|
||||||
|
|
||||||
return NextResponse.next({
|
|
||||||
request: {
|
|
||||||
headers: requestHeaders,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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|.*\\..*).*)',
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"start": "next start"
|
"start": "next start -p 3909"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/intl-localematcher": "^0.8.0",
|
"@formatjs/intl-localematcher": "^0.8.0",
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
"next": "16.0.10",
|
"next": "16.0.10",
|
||||||
"next-intl": "^4.7.0",
|
"next-intl": "^4.7.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"radix-ui": "^1.4.3",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
"react-day-picker": "9.8.0",
|
"react-day-picker": "9.8.0",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
|
|||||||
1470
pnpm-lock.yaml
generated
BIN
public/catalog.pdf
Normal file
BIN
public/images/about/flages.png
Normal file
|
After Width: | Height: | Size: 478 KiB |
BIN
public/images/about/gaza.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
public/images/about/pp.avif
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
public/images/about/sertificate.webp
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/images/homeBanner4.png
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
public/images/services/foam.jpg
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
public/images/services/gss.webp
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
public/images/services/sprinkler.jpg
Normal file
|
After Width: | Height: | Size: 33 KiB |
21
public/manifest.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "Ignum Technologies - Fire Safety Systems",
|
||||||
|
"short_name": "Ignum Tech",
|
||||||
|
"description": "Professional fire safety systems installation and sales",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#FF4500",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/og-image.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/og-image.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 353 KiB |