Compare commits
82 Commits
e71d774ccb
...
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 | ||
|
|
6a89bc1acc | ||
|
|
74f1d7a9fd | ||
|
|
dbe8399086 | ||
|
|
66bf104cb7 | ||
|
|
873bbb82a9 | ||
|
|
d4a242b169 | ||
|
|
e99df29b81 | ||
|
|
34cb524626 | ||
|
|
3cf5e0efcf | ||
|
|
d7e1990cc9 | ||
|
|
87f304225e | ||
|
|
3c862ea104 | ||
|
|
ca3e28779e | ||
|
|
63b363b142 | ||
|
|
96acd12d9c | ||
|
|
b1095f2c12 | ||
|
|
f439f9bbdf |
10
app/[locale]/about/baza/page.tsx
Normal file
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
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
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
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
app/[locale]/catalog_page/page.tsx
Normal file
18
app/[locale]/catalog_page/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Breadcrumb } from "@/components/breadCrumb";
|
||||||
|
import Catalog from "@/components/pages/home/blog/catalog";
|
||||||
|
import { ProductBanner } from "@/components/pages/products";
|
||||||
|
import { MainSubCategory } from "@/components/pages/subCategory";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#1e1d1c] pb-30">
|
||||||
|
<ProductBanner />
|
||||||
|
<div className="max-w-300 mx-auto w-full pt-5">
|
||||||
|
<div className="pb-8">
|
||||||
|
<Breadcrumb />
|
||||||
|
</div>
|
||||||
|
<Catalog />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
89
app/[locale]/catalog_page/products/[slug]/page.tsx
Normal file
89
app/[locale]/catalog_page/products/[slug]/page.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Features, RightSide, SliderComp } from "@/components/pages/products";
|
||||||
|
import { useProductPageInfo } from "@/zustand/useProduct";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import { LoadingSkeleton } from "@/components/pages/products/slug/loading";
|
||||||
|
import { EmptyState } from "@/components/pages/products/slug/empty";
|
||||||
|
import { Breadcrumb } from "@/components/breadCrumb";
|
||||||
|
import { useSearchParams } from "next/dist/client/components/navigation";
|
||||||
|
|
||||||
|
// Types
|
||||||
|
interface ProductImage {
|
||||||
|
id: number;
|
||||||
|
product: number;
|
||||||
|
image: string;
|
||||||
|
is_main: boolean;
|
||||||
|
order: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProductDetail {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
articular: string;
|
||||||
|
status: string;
|
||||||
|
description: string;
|
||||||
|
size: number;
|
||||||
|
price: string;
|
||||||
|
features: string[];
|
||||||
|
images: ProductImage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SlugPage() {
|
||||||
|
|
||||||
|
const productZustand = useProductPageInfo((state) => state.product);
|
||||||
|
const { data: product, isLoading } = useQuery({
|
||||||
|
queryKey: ["product", productZustand.id],
|
||||||
|
queryFn: () => httpClient(endPoints.product.detail(productZustand.id)),
|
||||||
|
select: (data) => data?.data?.data as ProductDetail,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Loading State
|
||||||
|
if (isLoading) {
|
||||||
|
return <LoadingSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty State
|
||||||
|
if (!product) {
|
||||||
|
return <EmptyState />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract images
|
||||||
|
const productImages = product.images?.map((img) => img.image) || [];
|
||||||
|
const mainImage =
|
||||||
|
product.images?.find((img) => img.is_main)?.image || productImages[0] || "";
|
||||||
|
const features = product.features.map((item: any) => item.name);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#1e1d1c] px-4 md:px-8 pb-35">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="min-[400px]:pt-35 pt-45 pb-10">
|
||||||
|
<Breadcrumb />
|
||||||
|
</div>
|
||||||
|
{/* Main Product Section */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 mb-12">
|
||||||
|
{/* Left - Image Slider */}
|
||||||
|
<SliderComp imgs={productImages} />
|
||||||
|
|
||||||
|
{/* Right - Product Info */}
|
||||||
|
<RightSide
|
||||||
|
id={product.id}
|
||||||
|
title={product.name}
|
||||||
|
articular={product.articular}
|
||||||
|
status={product.status}
|
||||||
|
description={product.description}
|
||||||
|
price={product.price}
|
||||||
|
image={mainImage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Features Section */}
|
||||||
|
{product.features && product.features.length > 0 && (
|
||||||
|
<Features features={features} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
app/[locale]/catalog_page/products/page.tsx
Normal file
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
app/[locale]/catalog_page/subCategory/page.tsx
Normal file
17
app/[locale]/catalog_page/subCategory/page.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Breadcrumb } from "@/components/breadCrumb";
|
||||||
|
import { ProductBanner } from "@/components/pages/products";
|
||||||
|
import { MainSubCategory } from "@/components/pages/subCategory";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#1e1d1c] pb-30">
|
||||||
|
<ProductBanner />
|
||||||
|
<div className="pb-20">
|
||||||
|
<div className="max-w-350 mx-auto w-full py-10">
|
||||||
|
<Breadcrumb />
|
||||||
|
</div>
|
||||||
|
<MainSubCategory />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,39 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { DATA } from "@/lib/demoData";
|
|
||||||
import { Features, RightSide, SliderComp } from "@/components/pages/products";
|
|
||||||
|
|
||||||
export default function SlugPage() {
|
|
||||||
const statusColor =
|
|
||||||
DATA[0].status === "full"
|
|
||||||
? "text-green-500"
|
|
||||||
: DATA[0].status === "empty"
|
|
||||||
? "text-red-600"
|
|
||||||
: "text-yellow-800";
|
|
||||||
|
|
||||||
const statusText =
|
|
||||||
DATA[0].status === "full"
|
|
||||||
? "Sotuvda mavjud"
|
|
||||||
: DATA[0].status === "empty"
|
|
||||||
? "Sotuvda qolmagan"
|
|
||||||
: "Buyurtma asosida";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-[#1e1d1c] py-40 px-4 md:px-8">
|
|
||||||
<div className="max-w-6xl mx-auto">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12">
|
|
||||||
<SliderComp imgs={DATA[0].images} />
|
|
||||||
|
|
||||||
<RightSide
|
|
||||||
title={DATA[0].title}
|
|
||||||
name={DATA[0].name}
|
|
||||||
statusColor={statusColor}
|
|
||||||
statusText={statusText}
|
|
||||||
description={DATA[0].description}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Features features={DATA[0].features} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { ProductBanner, Products } from "@/components/pages/products";
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return (
|
|
||||||
<div className="bg-[#1e1d1c] pb-30">
|
|
||||||
<ProductBanner />
|
|
||||||
<Products />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
5
app/[locale]/services/detail/loading.tsx
Normal file
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
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
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
BIN
app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -14,7 +14,7 @@
|
|||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
--font-roboto: "Roboto", sans-serif;
|
--font-roboto: "Roboto", sans-serif;
|
||||||
--font-almarai: "Almarai", sans-serif;
|
--font-almarai: "Almarai", sans-serif;
|
||||||
--font-unbounded:"Unbounded",sans-serif;
|
--font-unbounded: "Unbounded", sans-serif;
|
||||||
--color-sidebar-ring: var(--sidebar-ring);
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
@@ -135,4 +135,31 @@ body {
|
|||||||
background: #1e1d1c;
|
background: #1e1d1c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loio {
|
||||||
|
color: #8b1515, #c91d1d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* globals.css ga qo'shing */
|
||||||
|
@keyframes shimmer {
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-shimmer {
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-150 {
|
||||||
|
animation-delay: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-300 {
|
||||||
|
animation-delay: 300ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leo{
|
||||||
|
color: #979797;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* jvjjjjvjvj */
|
||||||
153
app/layout.tsx
153
app/layout.tsx
@@ -1,12 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import { Analytics } from "@vercel/analytics/next";
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { Footer, Navbar } from "@/components/layout";
|
|
||||||
import { NextIntlClientProvider } from "next-intl";
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
import { getMessages } from "next-intl/server";
|
import { getMessages } from "next-intl/server";
|
||||||
import BackAnimatsiya from "@/components/backAnimatsiya/backAnimatsiya";
|
import { InitialLoading } from "@/components/initialLoading/initialLoading";
|
||||||
|
import { Providers } from "@/components/provider";
|
||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -18,51 +18,148 @@ const geistMono = Geist_Mono({
|
|||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
// openGraphData tipini aniq belgilaymiz
|
||||||
title: "FireForce - Emergency Services",
|
const openGraphData: Record<
|
||||||
|
"uz" | "ru" | "en",
|
||||||
|
{ title: string; description: string; locale: string }
|
||||||
|
> = {
|
||||||
|
uz: {
|
||||||
|
title: "Ignum Technologies - Professional Fire Safety Systems",
|
||||||
description:
|
description:
|
||||||
"FireForce - Your trusted emergency response team bringing calm amidst chaos",
|
"Tijorat va uy-joy 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 />
|
||||||
<NextIntlClientProvider messages={messages} locale={locale}>
|
<NextIntlClientProvider messages={messages} locale={locale}>
|
||||||
<BackAnimatsiya />
|
<Providers>{children}</Providers>
|
||||||
<Navbar />
|
</NextIntlClientProvider> */}
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
|
||||||
<Analytics />
|
|
||||||
</NextIntlClientProvider>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
|
||||||
|
import PaymentFailed from "@/components/pages/payment";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return redirect('/uz/home')
|
// return redirect('/uz/home')
|
||||||
|
return <PaymentFailed />;
|
||||||
}
|
}
|
||||||
|
|||||||
48
components/EmptyData.tsx
Normal file
48
components/EmptyData.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
// components/EmptyData.tsx
|
||||||
|
import { PackageOpen, ShoppingBag } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
interface EmptyDataProps {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: "package" | "shopping";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EmptyData({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
icon = "package",
|
||||||
|
}: EmptyDataProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const Icon = icon === "package" ? PackageOpen : ShoppingBag;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-100 py-12 px-4">
|
||||||
|
{/* Animated background circles */}
|
||||||
|
<div className="relative flex items-center justify-center ">
|
||||||
|
{/* Icon */}
|
||||||
|
<div className="relative z-10 w-20 h-20 mx-auto mb-6 rounded-full bg-linear-to-br from-[#444242] to-gray-900 border border-white/10 flex items-center justify-center">
|
||||||
|
<Icon className="w-10 h-10 text-white/40" strokeWidth={1.5} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Text content */}
|
||||||
|
<div className="text-center space-y-3 max-w-md">
|
||||||
|
<h3 className="text-2xl font-unbounded font-bold text-white">
|
||||||
|
{title || t("no_data_title")}
|
||||||
|
</h3>
|
||||||
|
<p className="text-white/50 font-almarai">
|
||||||
|
{description || t("no_data_description")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Decorative elements */}
|
||||||
|
<div className="mt-8 flex gap-2">
|
||||||
|
<div className="w-2 h-2 rounded-full bg-red-500/30 animate-pulse" />
|
||||||
|
<div className="w-2 h-2 rounded-full bg-red-500/30 animate-pulse delay-150" />
|
||||||
|
<div className="w-2 h-2 rounded-full bg-red-500/30 animate-pulse delay-300" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,51 +1,46 @@
|
|||||||
|
"use client";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import Marquee from "react-fast-marquee";
|
import Marquee from "react-fast-marquee";
|
||||||
|
|
||||||
|
const images = [
|
||||||
|
"/images/img2.webp",
|
||||||
|
"/images/img3.jpg",
|
||||||
|
"/images/img6.jpg",
|
||||||
|
"/images/img11.jpeg",
|
||||||
|
"/images/img12.png",
|
||||||
|
];
|
||||||
|
|
||||||
export default function HomeMarquee() {
|
export default function HomeMarquee() {
|
||||||
|
const [marqImg, setMarqImg] = useState<string[]>(images);
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ["gallery"],
|
||||||
|
queryFn: () => httpClient(endPoints.gallery),
|
||||||
|
select: (data) => {
|
||||||
|
const galary = data?.data?.results;
|
||||||
|
return galary.map((item: any) => item.image) || [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
data && setMarqImg(data);
|
||||||
|
}, [data]);
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#1e1d1c] py-5">
|
<div className="bg-[#1e1d1c] py-5">
|
||||||
<Marquee>
|
<Marquee>
|
||||||
|
{marqImg.map((item) => (
|
||||||
<div className="relative sm:w-125 w-70 sm:h-62.5 h-40 mx-2 overflow-hidden rounded-xl">
|
<div className="relative sm:w-125 w-70 sm:h-62.5 h-40 mx-2 overflow-hidden rounded-xl">
|
||||||
<Image
|
<Image
|
||||||
src="/images/img2.webp"
|
src={item}
|
||||||
alt="images"
|
alt="images"
|
||||||
fill
|
fill
|
||||||
priority
|
priority
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative sm:w-125 w-70 sm:h-62.5 h-40 mx-2 overflow-hidden rounded-xl">
|
))}
|
||||||
<Image
|
|
||||||
src="/images/img3.jpg"
|
|
||||||
alt="images"
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="relative sm:w-125 w-70 sm:h-62.5 h-40 mx-2 overflow-hidden rounded-xl">
|
|
||||||
<Image
|
|
||||||
src="/images/img6.jpg"
|
|
||||||
alt="images"
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="relative sm:w-125 w-70 sm:h-62.5 h-40 mx-2 overflow-hidden rounded-xl">
|
|
||||||
<Image
|
|
||||||
src="/images/img11.jpeg"
|
|
||||||
alt="images"
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="relative sm:w-125 w-70 sm:h-62.5 h-40 mx-2 overflow-hidden rounded-xl">
|
|
||||||
<Image
|
|
||||||
src="/images/img12.png"
|
|
||||||
alt="images"
|
|
||||||
fill
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Marquee>
|
</Marquee>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import "./back.css";
|
|||||||
|
|
||||||
export default function BackAnimatsiya() {
|
export default function BackAnimatsiya() {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 w-full h-full flex items-center justify-center pointer-events-none z-0 opacity-100">
|
<div className="fixed inset-0 w-full h-full flex items-center justify-center pointer-events-none z-0 opacity-10">
|
||||||
<svg
|
<svg
|
||||||
id="Layer_2"
|
id="Layer_2"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
195
components/breadCrumb.tsx
Normal file
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
141
components/initialLoading/initialLoading.css
Normal file
141
components/initialLoading/initialLoading.css
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
.initial-loading {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #0c0c0c 0%, #151313 100%);
|
||||||
|
z-index: 99999;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initial-loading.fade-out {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initial-loading-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
width: 250px;
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initial-loading-content svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
animation: initialFloat 2s ease-in-out infinite, initialScale 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SVG path'larga stil - tiniq va aniq */
|
||||||
|
.logo-path {
|
||||||
|
fill: url(#neon-gradient);
|
||||||
|
stroke: #ffffff;
|
||||||
|
stroke-width: 0.3;
|
||||||
|
filter: drop-shadow(0 0 3px rgba(255, 255, 255, 0.3));
|
||||||
|
animation: pathPulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading dots animation */
|
||||||
|
.initial-loading-text {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-dots {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-dots span {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
background: linear-gradient(135deg, #e0e0e0 0%, #ffffff 50%, #c0c0c0 100%);
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 5px rgba(255, 255, 255, 0.3);
|
||||||
|
animation: dotBounce 1.4s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-dots span:nth-child(1) {
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-dots span:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-dots span:nth-child(3) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes initialFloat {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes initialScale {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.03);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pathPulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
filter: drop-shadow(0 0 3px rgba(255, 255, 255, 0.3));
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.95;
|
||||||
|
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dotBounce {
|
||||||
|
0%, 80%, 100% {
|
||||||
|
transform: scale(0);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth scroll prevention */
|
||||||
|
body:has(.initial-loading:not(.fade-out)) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.initial-loading-content {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.initial-loading-content {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-dots span {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
106
components/initialLoading/initialLoading.tsx
Normal file
106
components/initialLoading/initialLoading.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"use client";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import "./initialLoading.css";
|
||||||
|
|
||||||
|
export function InitialLoading() {
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isVisible, setIsVisible] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Faqat birinchi yuklanishda ishga tushadi
|
||||||
|
const hasVisited = sessionStorage.getItem("hasVisited");
|
||||||
|
|
||||||
|
if (hasVisited) {
|
||||||
|
// Agar oldin tashrif buyurilgan bo'lsa, darhol yashirish
|
||||||
|
setIsLoading(false);
|
||||||
|
setIsVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Birinchi tashrif
|
||||||
|
const loadingTimer = setTimeout(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
|
||||||
|
// Fade out animatsiyasi
|
||||||
|
const hideTimer = setTimeout(() => {
|
||||||
|
setIsVisible(false);
|
||||||
|
sessionStorage.setItem("hasVisited", "true");
|
||||||
|
}, 1000); // Fade out duration
|
||||||
|
|
||||||
|
return () => clearTimeout(hideTimer);
|
||||||
|
}, 1500); // Loading duration
|
||||||
|
|
||||||
|
return () => clearTimeout(loadingTimer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Agar ko'rinmas bo'lsa, hech narsa render qilmaymiz
|
||||||
|
if (!isVisible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`initial-loading ${!isLoading ? "fade-out" : ""}`}>
|
||||||
|
<div className="initial-loading-content">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 423.22 424.82"
|
||||||
|
className="w-full h-full"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
{/* Qizil neon gradient for github*/}
|
||||||
|
<linearGradient id="neon-gradient" x1="0%" y1="100%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stopColor="#979797" />
|
||||||
|
<stop offset="50%" stopColor="#e4e4e4" />
|
||||||
|
<stop offset="100%" stopColor="#9a9a9a" />
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
{/* Neon glow filter */}
|
||||||
|
<filter id="neon-glow">
|
||||||
|
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
|
||||||
|
<feMerge>
|
||||||
|
<feMergeNode in="coloredBlur"/>
|
||||||
|
<feMergeNode in="SourceGraphic"/>
|
||||||
|
</feMerge>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<g>
|
||||||
|
<polygon
|
||||||
|
className="logo-path"
|
||||||
|
points="352.86 365.08 347.11 365.08 347.11 381 289.38 381 289.38 365.08 283.63 365.08 283.63 381 204.78 381 204.78 365.08 199.03 365.08 199.03 381 141.32 381 141.32 365.08 135.57 365.08 135.57 381 77.87 381 77.87 365.08 71.86 365.08 71.85 381 17.21 381 17.21 386.74 72.11 386.74 72.11 424.82 74.04 424.82 74.04 386.74 350.94 386.74 350.94 424.82 352.86 424.82 352.86 386.74 406.02 386.74 406.02 381 352.86 381 352.86 365.08"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="logo-path"
|
||||||
|
d="m72.11,157.74v73.55l-41.42,23.89h3.83l37.58-21.68-.04,21.68h5.79v-24.99l57.7-33.27v58.26h5.75v-57.15l57.71-33.29v90.44h5.75v-90.43l39.43,22.77,39.43,22.77v44.89h5.74v-41.58l57.73,33.32v8.25h5.75v-95.41h-1.92v82.75l-61.56-35.55v-98.77l59.59-34.35-.97-1.66-58.62,33.81V0h-1.92v42.86h-88.43v115.23l-5.2,3-14.21,8.16-27.25,15.74-11.05,6.37v-73.53l36.14-20.85-.96-1.66-35.19,20.29v-35.03h-1.92v36.13l-65.36,37.7v-37.25h-1.92v38.35l-53.08,30.6.96,1.67,52.12-30.07ZM204.78,48.58h78.85v60.71l-78.85,45.48V48.58Zm0,108.41l78.85-45.49v92.14l-78.85-45.54v-1.11Zm-126.91,1.92l57.7-33.32v69.11l-57.7,33.26v-69.05Z"
|
||||||
|
/>
|
||||||
|
<g>
|
||||||
|
<rect className="logo-path" y="277.99" width="12.06" height="64.23" />
|
||||||
|
<path
|
||||||
|
className="logo-path"
|
||||||
|
d="m88.82,342.36c3.93-.65,7.51-1.73,10.75-3.23,3.24-1.5,6.01-3.46,8.32-5.89l1.31,8.97h7.76v-35.25h-39.45v9.72h27.58v1.03c0,3.12-.94,5.84-2.8,8.18-1.87,2.34-4.81,4.13-8.83,5.38-4.02,1.25-9.3,1.87-15.85,1.87-5.49,0-10.28-.73-14.4-2.2-4.11-1.46-7.32-3.8-9.63-7.01-2.31-3.21-3.46-7.46-3.46-12.76v-2.24c0-3.93.75-7.28,2.24-10.05,1.5-2.77,3.58-5.03,6.26-6.78,2.68-1.74,5.8-3.02,9.35-3.83,3.55-.81,7.39-1.21,11.5-1.21,3.37,0,6.56.2,9.58.61,3.02.41,5.75,1.11,8.18,2.1,2.43,1,4.35,2.38,5.75,4.16,1.4,1.78,2.1,3.97,2.1,6.59h11.78c0-4.05-.89-7.56-2.66-10.52-1.78-2.96-4.33-5.41-7.67-7.34-3.33-1.93-7.32-3.38-11.97-4.35-4.64-.97-9.83-1.45-15.57-1.45-8.6,0-15.99,1.25-22.16,3.74-6.17,2.49-10.89,6.22-14.16,11.17-3.27,4.95-4.91,11.08-4.91,18.37,0,11.16,3.23,19.48,9.68,24.96,6.45,5.49,16.16,8.23,29.12,8.23,4.24,0,8.32-.33,12.25-.98Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="logo-path"
|
||||||
|
d="m160.8,299.31c1.68,1.81,3.3,3.49,4.86,5.05l37.96,37.86h11.12v-64.23h-11.59v37.02c0,1.31.03,2.9.09,4.77.06,1.87.12,3.43.19,4.68h-.75c-.75-.87-1.67-1.87-2.76-2.99-1.09-1.12-2.15-2.23-3.18-3.32-1.03-1.09-1.92-1.98-2.66-2.66l-37.86-37.49h-11.5v64.23h11.59v-36.37c0-2.31-.02-4.46-.05-6.45-.03-1.99-.08-3.46-.14-4.39h.65c1,1.06,2.34,2.49,4.02,4.3Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="logo-path"
|
||||||
|
d="m242.6,277.99v35.34c0,6.11,1.26,11.42,3.79,15.94,2.52,4.52,6.36,7.99,11.5,10.42,5.14,2.43,11.64,3.65,19.49,3.65s14.33-1.21,19.45-3.65c5.11-2.43,8.93-5.9,11.45-10.42,2.52-4.52,3.79-9.83,3.79-15.94v-35.34h-12.06v34.97c0,6.48-1.96,11.47-5.89,14.96-3.93,3.49-9.5,5.23-16.73,5.23s-12.89-1.74-16.78-5.23c-3.9-3.49-5.84-8.48-5.84-14.96v-34.97h-12.15Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
className="logo-path"
|
||||||
|
d="m404.99,277.99l-17.2,37.02c-.5,1.12-1.11,2.51-1.82,4.16-.72,1.65-1.42,3.32-2.1,5-.69,1.68-1.31,3.18-1.87,4.49h-.56c-.62-1.5-1.3-3.12-2.01-4.86-.72-1.74-1.42-3.44-2.1-5.1-.69-1.65-1.25-2.94-1.68-3.88l-17.2-36.83h-18.51v64.23h11.59v-40.29c0-1.31-.02-2.67-.05-4.07-.03-1.4-.06-2.76-.09-4.07-.03-1.31-.08-2.4-.14-3.27h.75c.31.81.67,1.79,1.07,2.94.4,1.15.89,2.37,1.45,3.65.56,1.28,1.12,2.51,1.68,3.69l19.26,41.42h11.87l19.17-41.42c.43-.94.92-2.02,1.45-3.27.53-1.25,1.04-2.49,1.54-3.74.5-1.25.9-2.34,1.22-3.27h.75c-.06,1-.13,2.18-.19,3.55-.06,1.37-.11,2.74-.14,4.11-.03,1.37-.05,2.62-.05,3.74v40.29h12.15v-64.23h-18.23Z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Loading dots */}
|
||||||
|
<div className="initial-loading-text">
|
||||||
|
<div className="loading-dots">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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,23 +3,67 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Mail, Phone, MapPin } from "lucide-react";
|
import { Mail, Phone, MapPin, Instagram, Send } from "lucide-react";
|
||||||
import { useLocale, useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
const [errors, setErrors] = useState<any>({});
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [subscribed, setSubscribed] = useState(false);
|
const [subscribed, setSubscribed] = useState(false);
|
||||||
|
|
||||||
const handleSubscribe = (e: React.FormEvent) => {
|
const formRequest = useMutation({
|
||||||
e.preventDefault();
|
mutationKey: [],
|
||||||
if (email) {
|
mutationFn: (data: any) => httpClient.post(endPoints.post.sendNumber, data),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("succes"));
|
||||||
setSubscribed(true);
|
setSubscribed(true);
|
||||||
setEmail("");
|
setEmail("");
|
||||||
setTimeout(() => setSubscribed(false), 3000);
|
setTimeout(() => setSubscribed(false), 3000);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.log("error: ", error);
|
||||||
|
toast.error(t("error"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatPhoneNumber = (value: string) => {
|
||||||
|
const numbers = value.replace(/\D/g, "");
|
||||||
|
if (!numbers.startsWith("998")) {
|
||||||
|
return "+998 ";
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted = "+998 ";
|
||||||
|
const rest = numbers.slice(3);
|
||||||
|
if (rest.length > 0) formatted += rest.slice(0, 2);
|
||||||
|
if (rest.length > 2) formatted += " " + rest.slice(2, 5);
|
||||||
|
if (rest.length > 5) formatted += " " + rest.slice(5, 7);
|
||||||
|
if (rest.length > 7) formatted += " " + rest.slice(7, 9);
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const formatted = formatPhoneNumber(e.target.value);
|
||||||
|
setEmail(formatted);
|
||||||
|
if (errors.address) {
|
||||||
|
setErrors({ ...errors, address: "" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubscribe = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (email) {
|
||||||
|
// Telefon raqamni tozalash (faqat raqamlar)
|
||||||
|
const cleanPhone = email.replace(/\D/g, "");
|
||||||
|
formRequest.mutate({ number: Number(cleanPhone.slice(3)) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -31,10 +75,10 @@ export function Footer() {
|
|||||||
"linear-gradient(to top right, #452811 0%, #000000 20%, #000000 40%, #000000 60%, #000000 80%, #000000 100%)",
|
"linear-gradient(to top right, #452811 0%, #000000 20%, #000000 40%, #000000 60%, #000000 80%, #000000 100%)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Newsletter Section for gitea */}
|
{/* Newsletter Section */}
|
||||||
<div className=" absolute w-full -top-40 px-4 py-12 md:py-16">
|
<div className=" absolute w-full -top-40 px-4 py-12 md:py-16">
|
||||||
<div className="mx-auto max-w-6xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
<div className="rounded-2xl bg-[#fa1d1d] px-6 py-8 md:flex lg:flex-row flex-col max-lg:gap-5 md:items-center lg:justify-between justify-center md:px-10 md:py-12">
|
<div className="rounded-2xl bg-red-600 px-6 py-8 md:flex lg:flex-row flex-col max-lg:gap-5 md:items-center lg:justify-between justify-center md:px-10 md:py-12">
|
||||||
<div className="mb-8 md:mb-0">
|
<div className="mb-8 md:mb-0">
|
||||||
<h2 className="font-unbounded text-2xl font-bold text-white md:text-3xl">
|
<h2 className="font-unbounded text-2xl font-bold text-white md:text-3xl">
|
||||||
{t("contactTitle")}
|
{t("contactTitle")}
|
||||||
@@ -49,12 +93,16 @@ export function Footer() {
|
|||||||
className="flex sm:flex-row flex-col w-full gap-2 md:w-auto"
|
className="flex sm:flex-row flex-col w-full gap-2 md:w-auto"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="tel"
|
||||||
placeholder={t("enterPhone")}
|
id="phone"
|
||||||
|
name="phone"
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={handlePhoneChange}
|
||||||
className="font-almarai flex-1 rounded-full bg-white px-6 py-3 text-gray-800 placeholder-gray-400 focus:outline-none md:w-64"
|
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||||
required
|
errors.email ? "border-red-500" : "border-transparent"
|
||||||
|
}`}
|
||||||
|
placeholder="+998 90 123 45 67"
|
||||||
|
maxLength={17}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -87,6 +135,20 @@ export function Footer() {
|
|||||||
<p className="font-almarai text-sm leading-relaxed text-gray-300">
|
<p className="font-almarai text-sm leading-relaxed text-gray-300">
|
||||||
{t("footer.description")}
|
{t("footer.description")}
|
||||||
</p>
|
</p>
|
||||||
|
<div className="flex items-center gap-5 mt-5">
|
||||||
|
{/* <a
|
||||||
|
href=""
|
||||||
|
className="p-2 rounded-md bg-gray-700 hover:bg-red-500"
|
||||||
|
>
|
||||||
|
<Instagram />
|
||||||
|
</a> */}
|
||||||
|
<a
|
||||||
|
href="https://t.me/ignum_tech"
|
||||||
|
className="p-2 rounded-md bg-gray-700 hover:bg-red-500"
|
||||||
|
>
|
||||||
|
<Send />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Links */}
|
{/* Quick Links */}
|
||||||
@@ -162,12 +224,12 @@ export function Footer() {
|
|||||||
href="mailto:support@fireforce.com"
|
href="mailto:support@fireforce.com"
|
||||||
className="hover:text-[#fa1d1d]"
|
className="hover:text-[#fa1d1d]"
|
||||||
>
|
>
|
||||||
support@fireforce.com
|
info@ignum-tech.com
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-3">
|
<li className="flex items-start gap-3">
|
||||||
<MapPin className="mt-1 h-5 w-5 shrink-0 text-white" />
|
<MapPin className="mt-1 h-5 w-5 shrink-0 text-white" />
|
||||||
<span>Jl. Dr. Ir Soekarno No. 99x Tabanan - Bali</span>
|
<span>{t("footer.address")}</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,20 +238,30 @@ export function Footer() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Copyright Section */}
|
{/* Copyright Section */}
|
||||||
<div className="border-t border-gray-800 px-4 py-8">
|
<div className="border-t border-gray-800 px-4 py-6">
|
||||||
<div className="mx-auto max-w-6xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
<div className="font-almarai flex flex-col justify-between gap-4 text-sm text-gray-400 md:flex-row md:items-center">
|
<div className="font-almarai flex flex-col justify-end items-end w-full gap-4 text-lg text-gray-400 md:flex-row md:items-center">
|
||||||
<div>
|
{locale === "uz" ? (
|
||||||
Copyright © 2025 Ignum Company.
|
<div className="flex gap-2 w-full justify-center items-end ">
|
||||||
</div>
|
<a
|
||||||
<div className="flex gap-6">
|
href="http://felix-its.uz/"
|
||||||
<a href="#terms" className="hover:text-white">
|
className="hover:text-red-600 hover:cursor-pointer text-blue-300 underline"
|
||||||
Terms & Conditions
|
>
|
||||||
|
Felix-its.uz
|
||||||
</a>
|
</a>
|
||||||
<a href="#privacy" className="hover:text-white">
|
<p>- Jamoasi tomonidan ishlab chiqilgan</p>
|
||||||
Privacy Policy
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex w-full justify-center items-end gap-2">
|
||||||
|
{locale === "ru" ? <p>Разработано -</p> : <p>Created by - </p>}
|
||||||
|
<a
|
||||||
|
href="http://felix-its.uz/"
|
||||||
|
className="hover:text-red-600 hover:cursor-pointer text-blue-300 underline"
|
||||||
|
>
|
||||||
|
Felix-its.uz
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,15 +5,34 @@ import { ChevronDown, Phone, Menu, X } from "lucide-react";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import LanguageSelectRadix from "../languageSwitcher";
|
import LanguageSelectRadix from "../languageSwitcher";
|
||||||
import { useLocale, useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import NavbarLogo from "./navbarLogo/navbarLogo";
|
import UpHeader from "./upHeader";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "../ui/dropdown-menu";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import { NavbarItem } from "@/lib/types";
|
||||||
|
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
const [scrolled, setScrolled] = useState(false);
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
|
||||||
|
const { data: navbarItems } = useQuery({
|
||||||
|
queryKey: ["navbaritem",locale],
|
||||||
|
queryFn: () => httpClient(endPoints.navbar),
|
||||||
|
select: (data: any) => ({
|
||||||
|
results: data?.data?.data?.results,
|
||||||
|
total_items: data?.data?.data?.total_items,
|
||||||
|
total_pages: data?.data?.data?.total_pages,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
setScrolled(window.scrollY > 50);
|
setScrolled(window.scrollY > 50);
|
||||||
@@ -36,89 +55,73 @@ export function Navbar() {
|
|||||||
<nav
|
<nav
|
||||||
className={`fixed top-0 left-0 right-0 z-50 border-b border-gray-400/50 ${scrolled && "bg-black"} transition`}
|
className={`fixed top-0 left-0 right-0 z-50 border-b border-gray-400/50 ${scrolled && "bg-black"} transition`}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
className={`overflow-hidden transition-all duration-500 ease-in-out ${
|
||||||
|
scrolled
|
||||||
|
? "max-h-0 opacity-0 -translate-y-2"
|
||||||
|
: "max-h-20 opacity-100 translate-y-0"
|
||||||
|
}`}
|
||||||
|
style={{ transform: scrolled ? "translateY(-8px)" : "translateY(0)" }}
|
||||||
|
>
|
||||||
|
<UpHeader />
|
||||||
|
</div>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="flex justify-between items-center h-20">
|
<div className="flex justify-between items-center h-20">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link href={`/${locale}/home`} className="hover:cursor-pointer">
|
<Link href={`/${locale}/home`} className="hover:cursor-pointer">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className=" flex items-center justify-center">
|
<div className=" flex items-center justify-center">
|
||||||
<NavbarLogo/>
|
<Image
|
||||||
|
src={"/images/IGNUM/PNG/1.@6x.png"}
|
||||||
|
alt="logo image"
|
||||||
|
width={80}
|
||||||
|
height={80}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Desktop Navigation Menu */}
|
{/* Desktop Navigation Menu */}
|
||||||
<div className="hidden h-full lg:flex items-center gap-8">
|
<div className="hidden h-full lg:flex items-center gap-8">
|
||||||
|
{navbarItems?.results ? (
|
||||||
|
navbarItems.results.map((item: NavbarItem) => (
|
||||||
|
<DropdownMenu key={item.id}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Link
|
||||||
|
key={item.id}
|
||||||
|
href={`/${locale}/${item.url}`}
|
||||||
|
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
{item.children.length > 0 && (
|
||||||
|
<ChevronDown size={12} className="ml-1" />
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
{item.children.length > 0 && (
|
||||||
|
<DropdownMenuContent className="space-y-2">
|
||||||
|
{item.children.map((child: NavbarItem) => (
|
||||||
|
<DropdownMenuItem asChild key={child.id}>
|
||||||
|
<Link
|
||||||
|
href={`/${locale}/${child.url}`}
|
||||||
|
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
||||||
|
>
|
||||||
|
{child.name}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
)}
|
||||||
|
</DropdownMenu>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
<Link
|
<Link
|
||||||
href={`/${locale}/home`}
|
href={`/${locale}/home`}
|
||||||
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
||||||
>
|
>
|
||||||
{t("navbar.home")}
|
{t("navbar.home")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
)}
|
||||||
href={`/${locale}/about`}
|
|
||||||
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
|
||||||
>
|
|
||||||
{t("navbar.about")}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* Pages Dropdown */}
|
|
||||||
<div className="relative group h-full">
|
|
||||||
<button
|
|
||||||
className="font-unbounded uppercase text-white text-sm h-full font-semibold hover:text-red-500
|
|
||||||
transition-colors flex items-center gap-1"
|
|
||||||
>
|
|
||||||
{t("navbar.pages")}
|
|
||||||
<ChevronDown
|
|
||||||
size={16}
|
|
||||||
className="transition-transform group-hover:rotate-180"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* Dropdown Menu */}
|
|
||||||
<div
|
|
||||||
className="absolute top-full left-0 w-40 bg-white rounded-b-md shadow-lg
|
|
||||||
font-semibold opacity-0 invisible group-hover:opacity-100
|
|
||||||
group-hover:visible transition-all duration-300
|
|
||||||
transform translate-y-2 group-hover:translate-y-0
|
|
||||||
pointer-events-none group-hover:pointer-events-auto overflow-hidden"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/faq`}
|
|
||||||
className="font-unbounded uppercase block px-4 py-2 text-black text-sm hover:bg-red-600
|
|
||||||
hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
{t("navbar.faq")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/services`}
|
|
||||||
className="font-unbounded uppercase block px-4 py-2 text-black text-sm hover:bg-red-600
|
|
||||||
hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
{t("navbar.services")}
|
|
||||||
</Link>
|
|
||||||
{/* <Link
|
|
||||||
href={`/${locale}/blog`}
|
|
||||||
className="font-unbounded uppercase block px-4 py-2 text-black text-sm hover:bg-red-600
|
|
||||||
hover:text-white transition-colors rounded-b-md"
|
|
||||||
>
|
|
||||||
Blog
|
|
||||||
</Link> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/products`}
|
|
||||||
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
|
||||||
>
|
|
||||||
{t("navbar.products")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/contact`}
|
|
||||||
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
|
||||||
>
|
|
||||||
{t("navbar.contact")}
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-5">
|
<div className="flex items-center gap-5">
|
||||||
@@ -135,7 +138,8 @@ export function Navbar() {
|
|||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-white text-sm font-bold">
|
<div className="text-white text-sm font-bold">
|
||||||
<div>+998-98-099-21-21</div> <div>+998-77-372-21-21</div>
|
<div>+998-55-055-21-21</div>
|
||||||
|
<div>+998-77-372-21-21</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -194,68 +198,45 @@ export function Navbar() {
|
|||||||
|
|
||||||
{/* Mobile Menu Links */}
|
{/* Mobile Menu Links */}
|
||||||
<div className="flex flex-col p-6 gap-4">
|
<div className="flex flex-col p-6 gap-4">
|
||||||
|
{navbarItems?.results ? (
|
||||||
|
navbarItems.results.map((item: NavbarItem) => (
|
||||||
|
<DropdownMenu key={item.id}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Link
|
||||||
|
key={item.id}
|
||||||
|
href={`/${locale}/${item.url}`}
|
||||||
|
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
{item.children.length > 0 && (
|
||||||
|
<ChevronDown size={12} className="ml-1" />
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
{item.children.length > 0 && (
|
||||||
|
<DropdownMenuContent className="space-y-2">
|
||||||
|
{item.children.map((child: NavbarItem) => (
|
||||||
|
<Link
|
||||||
|
key={child.id}
|
||||||
|
href={`/${locale}/${child.url}`}
|
||||||
|
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
||||||
|
>
|
||||||
|
{child.name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
)}
|
||||||
|
</DropdownMenu>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
<Link
|
<Link
|
||||||
href={`/${locale}/home`}
|
href={`/${locale}/home`}
|
||||||
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
|
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
|
||||||
>
|
>
|
||||||
{t("navbar.home")}
|
{t("navbar.home")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
|
||||||
href={`/${locale}/about`}
|
|
||||||
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
|
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
|
||||||
>
|
|
||||||
{t("navbar.about")}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* Mobile Pages Dropdown */}
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
|
||||||
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition flex items-center gap-1 py-2 w-full"
|
|
||||||
>
|
|
||||||
{t("navbar.pages")}
|
|
||||||
<ChevronDown
|
|
||||||
size={16}
|
|
||||||
className={`transition-transform ${isDropdownOpen ? "rotate-180" : ""}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
{isDropdownOpen && (
|
|
||||||
<div className="ml-4 mt-2 flex flex-col gap-2">
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/faq`}
|
|
||||||
className="font-unbounded uppercase text-white/80 text-sm hover:text-red-500 transition py-2"
|
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
|
||||||
>
|
|
||||||
{t("navbar.faq")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/services`}
|
|
||||||
className="font-unbounded uppercase text-white/80 text-sm hover:text-red-500 transition py-2"
|
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
|
||||||
>
|
|
||||||
{t("navbar.services")}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/products`}
|
|
||||||
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
|
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
|
||||||
>
|
|
||||||
{t("navbar.products")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href={`/${locale}/contact`}
|
|
||||||
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
|
|
||||||
onClick={() => setIsMobileMenuOpen(false)}
|
|
||||||
>
|
|
||||||
{t("navbar.contact")}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import "./logo.css";
|
|||||||
|
|
||||||
export default function NavbarLogo() {
|
export default function NavbarLogo() {
|
||||||
return (
|
return (
|
||||||
<div className="relative w-24 h-12">
|
<div className="relative w-24 h-20">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 423.22 424.82"
|
viewBox="0 0 423.22 424.82"
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
{/* Qizil neon gradient */}
|
{/* Qizil neon gradient for github*/}
|
||||||
<linearGradient id="neon-gradient" x1="0%" y1="100%" x2="100%" y2="0%">
|
<linearGradient id="neon-gradient" x1="0%" y1="100%" x2="100%" y2="0%">
|
||||||
<stop offset="0%" stopColor="#979797" />
|
<stop offset="0%" stopColor="#979797" />
|
||||||
<stop offset="50%" stopColor="#e4e4e4" />
|
<stop offset="50%" stopColor="#e4e4e4" />
|
||||||
|
|||||||
51
components/layout/upHeader.tsx
Normal file
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
components/loadingSkleton.tsx
Normal file
41
components/loadingSkleton.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// components/CatalogCardSkeleton.tsx
|
||||||
|
export default function CatalogCardSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="relative h-112.5 w-full overflow-hidden rounded-2xl bg-[#17161679] border border-white/10 animate-pulse">
|
||||||
|
{/* Content container */}
|
||||||
|
<div className="relative h-full flex flex-col p-6">
|
||||||
|
{/* Title section */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
{/* Title skeleton */}
|
||||||
|
<div className="flex-1 space-y-2">
|
||||||
|
<div className="h-7 bg-white/10 rounded-md w-3/4" />
|
||||||
|
<div className="h-7 bg-white/10 rounded-md w-1/2" />
|
||||||
|
</div>
|
||||||
|
{/* Icon skeleton */}
|
||||||
|
<div className="shrink-0 w-8 h-8 rounded-full bg-white/10" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description skeleton */}
|
||||||
|
<div className="space-y-2 mt-3">
|
||||||
|
<div className="h-4 bg-white/10 rounded w-full" />
|
||||||
|
<div className="h-4 bg-white/10 rounded w-4/5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image container skeleton */}
|
||||||
|
<div className="relative flex-1 rounded-xl overflow-hidden bg-linear-to-br from-[#444242] to-gray-900/50 border border-white/5">
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<div className="w-32 h-32 bg-white/5 rounded-lg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom accent bar skeleton */}
|
||||||
|
<div className="mt-4 h-1 w-1/3 bg-white/10 rounded-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Shimmer effect */}
|
||||||
|
<div className="absolute inset-0 -translate-x-full animate-shimmer bg-linear-to-r from-transparent via-white/5 to-transparent" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,19 +3,35 @@ import { usePathname } from "next/navigation";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import "./page-transition.css";
|
import "./page-transition.css";
|
||||||
|
|
||||||
export default function PageTransition({ children }: { children: React.ReactNode }) {
|
export default function PageTransition({
|
||||||
|
children
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const [displayPath, setDisplayPath] = useState(pathname);
|
||||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (pathname !== displayPath) {
|
||||||
|
// Start exit animation
|
||||||
setIsTransitioning(true);
|
setIsTransitioning(true);
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const exitTimer = setTimeout(() => {
|
||||||
setIsTransitioning(false);
|
// Update path (triggers content change)
|
||||||
}, 1500); // Animatsiya davomiyligi
|
setDisplayPath(pathname);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
// Start enter animation
|
||||||
}, [pathname]);
|
const enterTimer = setTimeout(() => {
|
||||||
|
setIsTransitioning(false);
|
||||||
|
}, 800); // Enter animation duration
|
||||||
|
|
||||||
|
return () => clearTimeout(enterTimer);
|
||||||
|
}, 800); // Exit animation duration
|
||||||
|
|
||||||
|
return () => clearTimeout(exitTimer);
|
||||||
|
}
|
||||||
|
}, [pathname, displayPath]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -32,7 +48,6 @@ export default function PageTransition({ children }: { children: React.ReactNode
|
|||||||
<stop offset="50%" stopColor="#ff4444" />
|
<stop offset="50%" stopColor="#ff4444" />
|
||||||
<stop offset="100%" stopColor="#ff0000" />
|
<stop offset="100%" stopColor="#ff0000" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
|
|
||||||
<filter id="transition-glow">
|
<filter id="transition-glow">
|
||||||
<feGaussianBlur stdDeviation="5" result="coloredBlur"/>
|
<feGaussianBlur stdDeviation="5" result="coloredBlur"/>
|
||||||
<feMerge>
|
<feMerge>
|
||||||
|
|||||||
@@ -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
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
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
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
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
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
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
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
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>
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,7 @@ export default function ContactHeader() {
|
|||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
return (
|
return (
|
||||||
<div className="mb-8 text-center">
|
<div className="mb-8 text-center">
|
||||||
<div className="mb-4 flex items-center justify-center gap-2">
|
|
||||||
<DotAnimatsiya />
|
|
||||||
<span className="font-almarai text-sm font-semibold tracking-wider text-white">
|
|
||||||
{t("contact.banner.title")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h2
|
<h2
|
||||||
className="uppercase font-unbounded bg-linear-to-br from-white via-white to-black
|
className="uppercase font-unbounded bg-linear-to-br from-white via-white to-black
|
||||||
text-transparent bg-clip-text text-4xl font-bold tracking-wide md:text-5xl"
|
text-transparent bg-clip-text text-4xl font-bold tracking-wide md:text-5xl"
|
||||||
|
|||||||
@@ -3,21 +3,26 @@
|
|||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import axios from "axios";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
|
||||||
interface FormData {
|
interface FormData {
|
||||||
firstName: string;
|
name: string;
|
||||||
lastName: string;
|
surname: string;
|
||||||
email: string;
|
address: string;
|
||||||
subject: string;
|
theme: string;
|
||||||
message: string;
|
message: string;
|
||||||
agreeToPolicy: boolean;
|
agreeToPolicy: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormErrors {
|
interface FormErrors {
|
||||||
firstName?: string;
|
name?: string;
|
||||||
lastName?: string;
|
surname?: string;
|
||||||
email?: string;
|
address?: string;
|
||||||
subject?: string;
|
theme?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
agreeToPolicy?: string;
|
agreeToPolicy?: string;
|
||||||
}
|
}
|
||||||
@@ -25,10 +30,10 @@ interface FormErrors {
|
|||||||
export default function Form() {
|
export default function Form() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [formData, setFormData] = useState<FormData>({
|
const [formData, setFormData] = useState<FormData>({
|
||||||
firstName: "",
|
name: "",
|
||||||
lastName: "",
|
surname: "",
|
||||||
email: "",
|
address: "",
|
||||||
subject: "",
|
theme: "",
|
||||||
message: "",
|
message: "",
|
||||||
agreeToPolicy: false,
|
agreeToPolicy: false,
|
||||||
});
|
});
|
||||||
@@ -38,22 +43,52 @@ export default function Form() {
|
|||||||
"idle" | "success" | "error"
|
"idle" | "success" | "error"
|
||||||
>("idle");
|
>("idle");
|
||||||
|
|
||||||
|
const formRequest = useMutation({
|
||||||
|
mutationKey: [],
|
||||||
|
mutationFn: (data: FormData) =>
|
||||||
|
httpClient.post(endPoints.post.contact, data),
|
||||||
|
onSuccess: () => {
|
||||||
|
setSubmitStatus("success");
|
||||||
|
setFormData({
|
||||||
|
name: "",
|
||||||
|
surname: "",
|
||||||
|
address: "",
|
||||||
|
theme: "",
|
||||||
|
message: "",
|
||||||
|
agreeToPolicy: false,
|
||||||
|
});
|
||||||
|
setIsSubmitting(false);
|
||||||
|
toast.success(t("succes"));
|
||||||
|
setTimeout(() => setSubmitStatus("idle"), 3000);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.log("error: ", error);
|
||||||
|
setIsSubmitting(false);
|
||||||
|
setSubmitStatus("error");
|
||||||
|
toast.error(t("error"));
|
||||||
|
setTimeout(() => setSubmitStatus("idle"), 3000);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
const newErrors: FormErrors = {};
|
const newErrors: FormErrors = {};
|
||||||
|
|
||||||
if (!formData.firstName.trim()) {
|
if (!formData.name.trim()) {
|
||||||
newErrors.firstName = "First name is required";
|
newErrors.name = "First name is required";
|
||||||
}
|
}
|
||||||
if (!formData.lastName.trim()) {
|
if (!formData.surname.trim()) {
|
||||||
newErrors.lastName = "Last name is required";
|
newErrors.surname = "Last name is required";
|
||||||
}
|
}
|
||||||
if (!formData.email.trim()) {
|
const phoneNumbers = formData.address.replace(/\D/g, "");
|
||||||
newErrors.email = "Email is required";
|
if (phoneNumbers.length !== 12) {
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
newErrors.address =
|
||||||
newErrors.email = "Please enter a valid email";
|
t("validation.phoneRequired") || "To'liq telefon raqam kiriting";
|
||||||
|
} else if (!phoneNumbers.startsWith("998")) {
|
||||||
|
newErrors.address =
|
||||||
|
t("validation.phoneInvalid") || "Noto'g'ri telefon raqam";
|
||||||
}
|
}
|
||||||
if (!formData.subject.trim()) {
|
if (!formData.theme.trim()) {
|
||||||
newErrors.subject = "Subject is required";
|
newErrors.theme = "theme is required";
|
||||||
}
|
}
|
||||||
if (!formData.message.trim()) {
|
if (!formData.message.trim()) {
|
||||||
newErrors.message = "Message is required";
|
newErrors.message = "Message is required";
|
||||||
@@ -66,6 +101,30 @@ export default function Form() {
|
|||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatPhoneNumber = (value: string) => {
|
||||||
|
const numbers = value.replace(/\D/g, "");
|
||||||
|
if (!numbers.startsWith("998")) {
|
||||||
|
return "+998 ";
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted = "+998 ";
|
||||||
|
const rest = numbers.slice(3);
|
||||||
|
if (rest.length > 0) formatted += rest.slice(0, 2);
|
||||||
|
if (rest.length > 2) formatted += " " + rest.slice(2, 5);
|
||||||
|
if (rest.length > 5) formatted += " " + rest.slice(5, 7);
|
||||||
|
if (rest.length > 7) formatted += " " + rest.slice(7, 9);
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const formatted = formatPhoneNumber(e.target.value);
|
||||||
|
setFormData({ ...formData, address: formatted });
|
||||||
|
if (errors.address) {
|
||||||
|
setErrors({ ...errors, address: "" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
) => {
|
) => {
|
||||||
@@ -90,32 +149,7 @@ export default function Form() {
|
|||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
setSubmitStatus("idle");
|
setSubmitStatus("idle");
|
||||||
|
formRequest.mutate(formData);
|
||||||
try {
|
|
||||||
const response = await fetch("/api/contact", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(formData),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setSubmitStatus("success");
|
|
||||||
setFormData({
|
|
||||||
firstName: "",
|
|
||||||
lastName: "",
|
|
||||||
email: "",
|
|
||||||
subject: "",
|
|
||||||
message: "",
|
|
||||||
agreeToPolicy: false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setSubmitStatus("error");
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setSubmitStatus("error");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -125,66 +159,74 @@ export default function Form() {
|
|||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="firstName"
|
name="name"
|
||||||
placeholder={t("contact.form.placeholders.firstName")}
|
placeholder={t("contact.form.placeholders.firstName")}
|
||||||
value={formData.firstName}
|
value={formData.name}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm
|
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm
|
||||||
text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||||
errors.firstName ? "border-red-500" : "border-transparent"
|
errors.name ? "border-red-500" : "border-transparent"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{errors.firstName && (
|
{errors.name && (
|
||||||
<p className="font-almarai mt-1 text-xs text-red-500">{errors.firstName}</p>
|
<p className="font-almarai mt-1 text-xs text-red-500">
|
||||||
|
{errors.name}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="lastName"
|
name="surname"
|
||||||
placeholder={t("contact.form.placeholders.lastName")}
|
placeholder={t("contact.form.placeholders.lastName")}
|
||||||
value={formData.lastName}
|
value={formData.surname}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||||
errors.lastName ? "border-red-500" : "border-transparent"
|
errors.surname ? "border-red-500" : "border-transparent"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{errors.lastName && (
|
{errors.surname && (
|
||||||
<p className="font-almarai mt-1 text-xs text-red-500">{errors.lastName}</p>
|
<p className="font-almarai mt-1 text-xs text-red-500">
|
||||||
|
{errors.surname}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Second Row - Email & Subject */}
|
{/* Second Row - address & theme */}
|
||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="tel"
|
||||||
name="email"
|
id="phone"
|
||||||
placeholder={t("contact.form.placeholders.email")}
|
name="phone"
|
||||||
value={formData.email}
|
value={formData.address}
|
||||||
onChange={handleChange}
|
onChange={handlePhoneChange}
|
||||||
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||||
errors.email ? "border-red-500" : "border-transparent"
|
errors.address ? "border-red-500" : "border-transparent"
|
||||||
}`}
|
}`}
|
||||||
|
placeholder="+998 90 123 45 67"
|
||||||
|
maxLength={17}
|
||||||
/>
|
/>
|
||||||
{errors.email && (
|
{errors.address && (
|
||||||
<p className="font-almarai mt-1 text-xs text-red-500">{errors.email}</p>
|
<p className="mt-1 text-sm text-red-500">{errors.address}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="subject"
|
name="theme"
|
||||||
placeholder={t("contact.form.placeholders.subject")}
|
placeholder={t("contact.form.placeholders.subject")}
|
||||||
value={formData.subject}
|
value={formData.theme}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||||
errors.subject ? "border-red-500" : "border-transparent"
|
errors.theme ? "border-red-500" : "border-transparent"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{errors.subject && (
|
{errors.theme && (
|
||||||
<p className="font-almarai mt-1 text-xs text-red-500">{errors.subject}</p>
|
<p className="font-almarai mt-1 text-xs text-red-500">
|
||||||
|
{errors.theme}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -202,7 +244,9 @@ export default function Form() {
|
|||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{errors.message && (
|
{errors.message && (
|
||||||
<p className="font-almarai mt-1 text-xs text-red-500">{errors.message}</p>
|
<p className="font-almarai mt-1 text-xs text-red-500">
|
||||||
|
{errors.message}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -236,7 +280,9 @@ export default function Form() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{errors.agreeToPolicy && (
|
{errors.agreeToPolicy && (
|
||||||
<p className="font-almarai text-xs text-red-500">{errors.agreeToPolicy}</p>
|
<p className="font-almarai text-xs text-red-500">
|
||||||
|
{errors.agreeToPolicy}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Status Messages */}
|
{/* Status Messages */}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Mail, MapPin, Phone, Check } from "lucide-react";
|
import { Mail, MapPin, Phone } from "lucide-react";
|
||||||
import ContactHeader from "./contactHeader";
|
import ContactHeader from "./contactHeader";
|
||||||
import Form from "./form";
|
import Form from "./form";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import Maps from "./maps";
|
||||||
|
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
||||||
|
|
||||||
export function Contact() {
|
export function Contact() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const contactInfo = [
|
const contactInfo = [
|
||||||
{
|
{
|
||||||
icon: Mail,
|
icon: Mail,
|
||||||
title:t("contact.form.email"),
|
title: t("contact.form.email"),
|
||||||
detail: t("contact.form.emailAddress"),
|
detail: t("contact.form.emailAddress"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -25,7 +27,7 @@ export function Contact() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative min-h-175 w-full py-16 md:py-40">
|
<section className="relative min-h-175 w-full py-30 md:py-40">
|
||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
<Image
|
<Image
|
||||||
@@ -40,13 +42,23 @@ export function Contact() {
|
|||||||
className="absolute inset-0"
|
className="absolute inset-0"
|
||||||
style={{
|
style={{
|
||||||
background:
|
background:
|
||||||
"radial-gradient(ellipse at bottom center, #d2610a 0%, #1e1d1ce9 70% , #1e1d1ce9 70%)",
|
"radial-gradient(at center bottom, rgb(144 74 20) 0%, rgba(30, 29, 28, 0.914) 50%, rgba(30, 29, 28, 0.914) 70%)",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="relative z-10 mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
<div className="relative z-10 ">
|
||||||
|
<div className="flex items-center justify-center w-full">
|
||||||
|
<div className="mb-4 flex items-center justify-center gap-2">
|
||||||
|
<DotAnimatsiya />
|
||||||
|
<span className="font-almarai text-sm font-semibold tracking-wider text-white">
|
||||||
|
{t("contact.banner.title")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Maps />
|
||||||
|
<div className="relative z-10 mx-auto max-w-4xl px-4 sm:px-6 lg:px-8 space-y-5">
|
||||||
<ContactHeader />
|
<ContactHeader />
|
||||||
|
|
||||||
<Form />
|
<Form />
|
||||||
@@ -64,11 +76,14 @@ export function Contact() {
|
|||||||
<h3 className="font-almarai text-sm font-bold tracking-wider text-white">
|
<h3 className="font-almarai text-sm font-bold tracking-wider text-white">
|
||||||
{info.title}
|
{info.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="font-almarai mt-1 text-sm text-gray-400">{info.detail}</p>
|
<p className="font-almarai mt-1 text-sm text-gray-400">
|
||||||
|
{info.detail}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
31
components/pages/contact/maps.tsx
Normal file
31
components/pages/contact/maps.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export default function Maps() {
|
||||||
|
return (
|
||||||
|
<section className="w-full px-4 py-10">
|
||||||
|
<div className="mx-auto max-w-7xl grid grid-cols-1 md:grid-cols-2 gap-10">
|
||||||
|
|
||||||
|
{/* Yandex Map */}
|
||||||
|
<div className="h-[300px] sm:h-[400px] md:h-[500px] rounded-2xl overflow-hidden shadow-lg border hover:shadow-xl transition">
|
||||||
|
<iframe
|
||||||
|
src="https://yandex.uz/map-widget/v1/?ll=69.288118%2C41.323186&mode=search&oid=56350803620&ol=biz&z=16.97"
|
||||||
|
className="w-full h-full"
|
||||||
|
frameBorder="0"
|
||||||
|
allowFullScreen
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Google Map */}
|
||||||
|
<div className="h-[300px] sm:h-[400px] md:h-[500px] rounded-2xl overflow-hidden shadow-lg border hover:shadow-xl transition">
|
||||||
|
<iframe
|
||||||
|
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2996.342450769356!2d69.28561627695484!3d41.323166199974985!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x38aef56f76215ce7%3A0xfd64c6a930fb1bbb!2sIGNUM!5e0!3m2!1sru!2s!4v1770203090924"
|
||||||
|
className="w-full h-full"
|
||||||
|
allowFullScreen
|
||||||
|
loading="lazy"
|
||||||
|
referrerPolicy="no-referrer-when-downgrade"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,37 +1,53 @@
|
|||||||
|
"use client";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import FAQAccordion from "./faqAccardion";
|
import FAQAccordion, { FAQItem } from "./faqAccardion";
|
||||||
import { faqItems } from "@/lib/demoData";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export function Togle() {
|
export function Togle() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const faqItems = [
|
const faqItems: FAQItem[] = [
|
||||||
{
|
{
|
||||||
id: "faq-1",
|
id: 1,
|
||||||
question: t("faq.question1.question"),
|
question: t("faq.question1.question"),
|
||||||
answer: t("faq.question1.answer"),
|
answer: t("faq.question1.answer"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "faq-2",
|
id: 2,
|
||||||
question: t("faq.question2.question"),
|
question: t("faq.question2.question"),
|
||||||
answer: t("faq.question2.answer"),
|
answer: t("faq.question2.answer"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "faq-3",
|
id: 3,
|
||||||
question: t("faq.question3.question"),
|
question: t("faq.question3.question"),
|
||||||
answer: t("faq.question3.answer"),
|
answer: t("faq.question3.answer"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "faq-4",
|
id: 4,
|
||||||
question: t("faq.question4.question"),
|
question: t("faq.question4.question"),
|
||||||
answer: t("faq.question4.answer"),
|
answer: t("faq.question4.answer"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "faq-5",
|
id: 5,
|
||||||
question: t("faq.question5.question"),
|
question: t("faq.question5.question"),
|
||||||
answer: t("faq.question5.answer"),
|
answer: t("faq.question5.answer"),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const [faq, setFaq] = useState<any>(faqItems);
|
||||||
|
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ["faq"],
|
||||||
|
queryFn: () => httpClient(endPoints.faq),
|
||||||
|
select: (data) => data?.data?.results,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
data && setFaq(data);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#1e1d1c]">
|
<div className="min-h-screen bg-[#1e1d1c]">
|
||||||
<main className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
|
<main className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
|
||||||
@@ -48,7 +64,7 @@ export function Togle() {
|
|||||||
|
|
||||||
{/* FAQ Section */}
|
{/* FAQ Section */}
|
||||||
<div className="max-w-250 w-full">
|
<div className="max-w-250 w-full">
|
||||||
<FAQAccordion items={faqItems} />
|
<FAQAccordion items={faq} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* ASK QUESTION */}
|
{/* ASK QUESTION */}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import * as Accordion from '@radix-ui/react-accordion';
|
import * as Accordion from "@radix-ui/react-accordion";
|
||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronDown } from "lucide-react";
|
||||||
|
|
||||||
interface FAQItem {
|
export interface FAQItem {
|
||||||
id: string;
|
id: number;
|
||||||
question: string;
|
question: string;
|
||||||
answer: string;
|
answer: string;
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,11 @@ export default function FAQAccordion({ items }: FAQAccordionProps) {
|
|||||||
return (
|
return (
|
||||||
<Accordion.Root type="single" collapsible className="w-full">
|
<Accordion.Root type="single" collapsible className="w-full">
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => (
|
||||||
<Accordion.Item key={item.id} value={item.id} className="border-b border-slate-700 py-6">
|
<Accordion.Item
|
||||||
|
key={item.id}
|
||||||
|
value={String(item.id)}
|
||||||
|
className="border-b border-slate-700 py-6"
|
||||||
|
>
|
||||||
<Accordion.Trigger className="group flex w-full items-center justify-between text-left">
|
<Accordion.Trigger className="group flex w-full items-center justify-between text-left">
|
||||||
<h3 className="font-almarai text-lg font-bold uppercase tracking-wide text-white transition-colors duration-300 group-hover:cursor-pointer md:text-xl">
|
<h3 className="font-almarai text-lg font-bold uppercase tracking-wide text-white transition-colors duration-300 group-hover:cursor-pointer md:text-xl">
|
||||||
{item.question}
|
{item.question}
|
||||||
@@ -31,7 +35,9 @@ export default function FAQAccordion({ items }: FAQAccordionProps) {
|
|||||||
</Accordion.Trigger>
|
</Accordion.Trigger>
|
||||||
|
|
||||||
<Accordion.Content className="overflow-hidden pt-4 text-gray-400 animate-in fade-in slide-in-from-top-2 duration-300 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=closed]:slide-out-to-top-2">
|
<Accordion.Content className="overflow-hidden pt-4 text-gray-400 animate-in fade-in slide-in-from-top-2 duration-300 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=closed]:slide-out-to-top-2">
|
||||||
<p className="font-almarai leading-relaxed text-sm md:text-base">{item.answer}</p>
|
<p className="font-almarai leading-relaxed text-sm md:text-base">
|
||||||
|
{item.answer}
|
||||||
|
</p>
|
||||||
</Accordion.Content>
|
</Accordion.Content>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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/homeBanner.png"
|
|
||||||
alt="Firefighters"
|
|
||||||
loading="lazy"
|
|
||||||
className="lg:w-150 w-100 lg:h-150 max-[300px]:w-[80vw] object-cover object-right rounded-xl drop-shadow-2xl"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right side - Text Content */}
|
|
||||||
<div className="lg:inline-block hidden space-y-6 mb-20">
|
|
||||||
{/* Badge */}
|
|
||||||
<div className="flex items-center gap-2 w-fit">
|
|
||||||
<DotAnimatsiya />
|
|
||||||
<span className="text-sm font-semibold text-white tracking-wide font-almarai">
|
|
||||||
{t("home.banner.title1")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Heading */}
|
|
||||||
<h1 className="font-unbounded uppercase text-4xl bg-linear-to-br from-white via-white to-black
|
|
||||||
text-transparent bg-clip-text sm:text-5xl lg:text-6xl font-bold leading-tight text-pretty">
|
|
||||||
{t("home.banner.title2")}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<p className="font-almarai text-base sm:text-lg text-gray-300 leading-relaxed max-w-md">
|
|
||||||
{t("home.banner.description")}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* CTA Button */}
|
|
||||||
<button className="font-almarai shadow-[0px_0px_2px_8px_#ff01015c] bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-8 rounded-full transition duration-300 transform hover:scale-105 w-fit">
|
|
||||||
{t("home.banner.cta")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
28
components/pages/home/banner/banner.tsx
Normal file
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
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
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import { ChevronRight } from "lucide-react";
|
|
||||||
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
import ProductCard from "../products/productCard";
|
|
||||||
|
|
||||||
export function Blog() {
|
|
||||||
const t = useTranslations();
|
|
||||||
const blogPosts = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
image: "/images/img14.webp",
|
|
||||||
category: "Tips & Trick",
|
|
||||||
title: t("home.blog.articles.article1"),
|
|
||||||
author: "John Doe",
|
|
||||||
date: "July 24, 2025",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
image: "/images/img15.webp",
|
|
||||||
category: "Insight",
|
|
||||||
title: t("home.blog.articles.article2"),
|
|
||||||
author: "John Doe",
|
|
||||||
date: "July 24, 2025",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
image: "/images/img16.webp",
|
|
||||||
category: "News",
|
|
||||||
title: t("home.blog.articles.article3"),
|
|
||||||
author: "John Doe",
|
|
||||||
date: "July 24, 2025",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="bg-[#1f1f1f] py-45">
|
|
||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="mb-12 text-center">
|
|
||||||
<div className="mb-4 flex items-center justify-center gap-2">
|
|
||||||
<DotAnimatsiya />
|
|
||||||
<span className="font-almarai text-sm font-semibold tracking-wider text-white uppercase">
|
|
||||||
{t("products.banner.title")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h2
|
|
||||||
className="font-unbounded bg-linear-to-br from-white py-2 via-white to-black
|
|
||||||
text-transparent bg-clip-text text-4xl font-bold tracking-tight md:text-5xl lg:text-6xl"
|
|
||||||
>
|
|
||||||
{t("products.ourproducts")}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Blog Cards Grid */}
|
|
||||||
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3 max-sm:place-items-center">
|
|
||||||
{/* {blogPosts.map((post) => (
|
|
||||||
<article key={post.id} className="group">
|
|
||||||
|
|
||||||
<div className="relative mb-6 aspect-4/2 md:aspect-4/3 overflow-hidden rounded-lg">
|
|
||||||
<Image
|
|
||||||
src={post.image || "/placeholder.svg"}
|
|
||||||
alt={post.title}
|
|
||||||
fill
|
|
||||||
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="absolute bottom-4 left-4">
|
|
||||||
<span className="font-almarai rounded bg-red-600 px-4 py-2 text-sm font-medium text-white">
|
|
||||||
{post.category}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="font-unbounded uppercase mb-3 text-lg font-bold leading-tight tracking-wide text-white md:text-xl">
|
|
||||||
{post.title}
|
|
||||||
</h3>
|
|
||||||
<p className="font-almarai mb-4 text-sm text-gray-400">
|
|
||||||
<span className="text-gray-500">by </span>
|
|
||||||
<span className="text-white">{post.author}</span>
|
|
||||||
<span className="mx-2 text-gray-500">•</span>
|
|
||||||
<span className="text-gray-400">{post.date}</span>
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="font-almarai inline-flex items-center gap-1 text-sm font-semibold tracking-wider text-red-600 uppercase transition-colors hover:text-red-500"
|
|
||||||
>
|
|
||||||
{t("home.blog.readMore")}
|
|
||||||
<ChevronRight className="h-4 w-4" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
))} */}
|
|
||||||
{Array(3)
|
|
||||||
.fill(null)
|
|
||||||
.map((_, index) => (
|
|
||||||
<ProductCard
|
|
||||||
key={index}
|
|
||||||
title="Elektr yong'in detektori-Ypres ver.2"
|
|
||||||
name="P-0834404"
|
|
||||||
image="/images/products/products.webp"
|
|
||||||
slug="P_0834404"
|
|
||||||
status="full"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
32
components/pages/home/blog/blog.tsx
Normal file
32
components/pages/home/blog/blog.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import Catalog from "./catalog";
|
||||||
|
|
||||||
|
export function Blog() {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-[#1f1f1f] py-30">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-12 text-center">
|
||||||
|
<div className="mb-4 flex items-center justify-center gap-2">
|
||||||
|
<DotAnimatsiya />
|
||||||
|
<span className="font-almarai text-sm font-semibold tracking-wider text-white uppercase">
|
||||||
|
{t("products.banner.title")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2
|
||||||
|
className="font-unbounded bg-linear-to-br from-white py-2 via-white to-black
|
||||||
|
text-transparent bg-clip-text text-4xl font-bold tracking-tight md:text-5xl lg:text-6xl"
|
||||||
|
>
|
||||||
|
{t("products.ourproducts")}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Blog Cards Grid */}
|
||||||
|
<Catalog />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
56
components/pages/home/blog/catalog.tsx
Normal file
56
components/pages/home/blog/catalog.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
"use client";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import CatalogCard from "../../products/catalog";
|
||||||
|
import CatalogCardSkeleton from "@/components/loadingSkleton";
|
||||||
|
import EmptyData from "@/components/EmptyData";
|
||||||
|
import { getRouteLang } from "@/request/getLang";
|
||||||
|
import { CategoryType } from "@/lib/types";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export default function Catalog() {
|
||||||
|
const language = getRouteLang();
|
||||||
|
const { data, isLoading } = useQuery({
|
||||||
|
queryKey: ["category", language],
|
||||||
|
queryFn: () => httpClient(endPoints.category.all),
|
||||||
|
select: (data): CategoryType[] => data?.data?.results,
|
||||||
|
});
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{[...Array(3)].map((_, index) => (
|
||||||
|
<CatalogCardSkeleton key={index} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ma'lumot yo'q holati
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return (
|
||||||
|
<EmptyData
|
||||||
|
title={t("products.noData.title")}
|
||||||
|
description={t("products.noData.description")}
|
||||||
|
icon="shopping"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3 place-items-center">
|
||||||
|
{data.map((item, index) => (
|
||||||
|
<CatalogCard
|
||||||
|
key={index}
|
||||||
|
id={item.id}
|
||||||
|
title={item.name}
|
||||||
|
description={item.description}
|
||||||
|
image={item.image}
|
||||||
|
have_sub_category={item.have_sub_category}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
export { Banner } from "./banner";
|
export { Banner } from "./banner/banner";
|
||||||
export { Statistics } from "./statistics";
|
export { Statistics } from "./statistics";
|
||||||
export { AboutUs } from "./about";
|
export { AboutUs } from "./about";
|
||||||
export { Video } from "./video";
|
export { Video } from "./video";
|
||||||
export { OurService } from "./ourService";
|
export { OurService } from "./ourService";
|
||||||
export { Testimonial } from "./testimonal";
|
export { Testimonial } from "./testimonal";
|
||||||
export { Line } from "./line";
|
export { Line } from "./line";
|
||||||
export { Blog } from "./blog";
|
export { Blog } from "./blog/blog";
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,36 +1,63 @@
|
|||||||
|
"use client";
|
||||||
import { Counter } from "@/components/Counter";
|
import { Counter } from "@/components/Counter";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface Statistics {
|
||||||
|
id: number;
|
||||||
|
number: number;
|
||||||
|
hint: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function Statistics() {
|
export function Statistics() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const stats = [
|
const stats = [
|
||||||
{
|
{
|
||||||
number: "25",
|
id: 1,
|
||||||
symbol: "+",
|
number: 25,
|
||||||
label: t("home.statistics.experience"),
|
hint: "+",
|
||||||
|
description: t("home.statistics.experience"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "450",
|
id: 2,
|
||||||
symbol: "+",
|
number: 450,
|
||||||
label: t("home.statistics.projectsCompleted"),
|
hint: "+",
|
||||||
|
description: t("home.statistics.projectsCompleted"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "99",
|
id: 3,
|
||||||
symbol: "+",
|
number: 99,
|
||||||
label: t("home.statistics.trainedSpecialists"),
|
hint: "+",
|
||||||
|
description: t("home.statistics.trainedSpecialists"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
number: "93",
|
id: 4,
|
||||||
symbol: "%",
|
number: 93,
|
||||||
label: t("home.statistics.trustedClients"),
|
hint: "%",
|
||||||
|
description: t("home.statistics.trustedClients"),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const [stat, setStat] = useState<Statistics[]>(stats);
|
||||||
|
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ["statistics"],
|
||||||
|
queryFn: () => httpClient(endPoints.statistics),
|
||||||
|
select: (data) => data?.data?.results,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
data && setStat(data);
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-full bg-black">
|
<section className="w-full bg-black">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 lg:gap-12">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-8 lg:gap-12">
|
||||||
{stats.map((stat, index) => (
|
{stat.map((stat, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex flex-col items-center justify-center py-10 sm:py-20 lg:py-15 border-b-red-600 border-b"
|
className="flex flex-col items-center justify-center py-10 sm:py-20 lg:py-15 border-b-red-600 border-b"
|
||||||
@@ -41,13 +68,13 @@ export function Statistics() {
|
|||||||
<Counter countNum={Number(stat.number)} />
|
<Counter countNum={Number(stat.number)} />
|
||||||
</span>
|
</span>
|
||||||
<span className="text-4xl sm:text-5xl lg:text-6xl font-bold text-red-600">
|
<span className="text-4xl sm:text-5xl lg:text-6xl font-bold text-red-600">
|
||||||
{stat.symbol}
|
{stat.hint}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Label */}
|
{/* Label */}
|
||||||
<p className="font-almarai text-sm sm:text-base text-gray-300 mt-4 text-center font-medium">
|
<p className="font-almarai text-sm sm:text-base text-gray-300 mt-4 text-center font-medium">
|
||||||
{stat.label}
|
{stat.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
527
components/pages/payment/index.tsx
Normal file
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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { useTranslations } from "next-intl";
|
|||||||
export function ProductBanner() {
|
export function ProductBanner() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
return (
|
return (
|
||||||
<section className="relative w-full h-[60vh] min-h-100 overflow-hidden pt-10">
|
<section className="relative w-full min-[400px]:h-[60vh] h-[75vh] min-h-100 overflow-hidden pt-10">
|
||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 z-0"
|
className="absolute inset-0 z-0"
|
||||||
155
components/pages/products/catalog.tsx
Normal file
155
components/pages/products/catalog.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
// import { useTranslations } from "next-intl";
|
||||||
|
// import Image from "next/image";
|
||||||
|
// import Link from "next/link";
|
||||||
|
|
||||||
|
// interface CatalogProps {
|
||||||
|
// image: string;
|
||||||
|
// title: string;
|
||||||
|
// slug: string;
|
||||||
|
// description: string;
|
||||||
|
// id:string;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export default function CatalogCard({
|
||||||
|
// image,
|
||||||
|
// title,
|
||||||
|
// slug,
|
||||||
|
// description,
|
||||||
|
// id,
|
||||||
|
// }: CatalogProps) {
|
||||||
|
// const t = useTranslations();
|
||||||
|
// return (
|
||||||
|
// <Link
|
||||||
|
// href={`/products?category=${id}`}
|
||||||
|
// className="group h-118 flex flex-col items-center justify-start" // Added 'group' here
|
||||||
|
// >
|
||||||
|
// <div className="h-full flex flex-col justify-between group-hover:scale-105 transition ease-in-out">
|
||||||
|
// <p className="text-white text-2xl font-unbounded font-semibold text-center transition-colors">
|
||||||
|
// {title}
|
||||||
|
// </p>
|
||||||
|
// <p className="text-white/50 font-almarai font-medium text-center">
|
||||||
|
// {t(`${description}`)}
|
||||||
|
// </p>
|
||||||
|
// <Image
|
||||||
|
// src={image}
|
||||||
|
// alt="image"
|
||||||
|
// width={400}
|
||||||
|
// height={90}
|
||||||
|
// className="h-90! rounded-xl object-contain bg-[#444242] transition-colors duration-300" // Added smooth transition
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// </Link>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// bg-[#444242]
|
||||||
|
"use client";
|
||||||
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { ArrowUpRight } from "lucide-react";
|
||||||
|
import { useCategory } from "@/zustand/useCategory";
|
||||||
|
import { useSubCategory } from "@/zustand/useSubCategory";
|
||||||
|
|
||||||
|
interface CatalogProps {
|
||||||
|
id: number;
|
||||||
|
image: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
have_sub_category: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CatalogCard({
|
||||||
|
image,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
id,
|
||||||
|
have_sub_category,
|
||||||
|
}: CatalogProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const locale = useLocale();
|
||||||
|
const setCategory = useCategory((state) => state.setCategory);
|
||||||
|
const clearSubCategory = useSubCategory((state) => state.clearSubCategory);
|
||||||
|
const item = {
|
||||||
|
image: image,
|
||||||
|
name: title,
|
||||||
|
description: description,
|
||||||
|
id: id,
|
||||||
|
have_sub_category: have_sub_category,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateZustands = () => {
|
||||||
|
setCategory(item);
|
||||||
|
clearSubCategory();
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigateLink = have_sub_category
|
||||||
|
? `/${locale}/catalog_page/subCategory?category=${id}`
|
||||||
|
: `/${locale}/catalog_page/products?category=${id}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={navigateLink}
|
||||||
|
onClick={updateZustands}
|
||||||
|
className="group relative h-112.5 w-full overflow-hidden rounded-2xl bg-[#171616] bg-linear-to-br from-[#2a2a2a] to-black border hover:border-red-700 border-white/10 transition-all duration-500 hover:-translate-y-1"
|
||||||
|
>
|
||||||
|
{/* Background glow effect */}
|
||||||
|
<div className="absolute inset-0 bg-linear-to-t from-red-600/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||||
|
|
||||||
|
{/* Decorative corner accent */}
|
||||||
|
<div className="absolute top-0 right-0 w-24 h-24 bg-linear-to-br from-red-500/20 to-transparent rounded-bl-full opacity-0 group-hover:opacity-00 transition-opacity duration-500" />
|
||||||
|
|
||||||
|
{/* Content container */}
|
||||||
|
<div className="relative h-full flex flex-col p-6">
|
||||||
|
{/* Title section */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<h3 className="text-2xl font-unbounded font-bold text-white leading-tight transition-colors duration-300">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<div className="shrink-0 w-8 h-8 rounded-full bg-white/10 flex items-center justify-center group-hover:bg-red-500 transition-all duration-300 group-hover:scale-110">
|
||||||
|
<ArrowUpRight className="w-4 h-4 text-white" strokeWidth={2.5} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p className="text-sm font-almarai text-white/60 line-clamp-2 group-hover:text-white/80 transition-colors duration-300">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Image container with elegant frame */}
|
||||||
|
<div className="relative flex-1 rounded-xl overflow-hidden bg-linear-to-br from-[#444242] to-gray-900/50 border border-white/5 group-hover:border-white/20 transition-all duration-500">
|
||||||
|
{/* Animated gradient overlay */}
|
||||||
|
<div className="absolute inset-0 bg-linear-to-t from-black/60 via-transparent to-transparent z-10" />
|
||||||
|
|
||||||
|
{/* Image */}
|
||||||
|
<div className="relative w-full h-full">
|
||||||
|
<Image
|
||||||
|
src={image}
|
||||||
|
alt={title}
|
||||||
|
fill
|
||||||
|
className="object-contain p-4 transition-transform duration-700 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hover shimmer effect */}
|
||||||
|
{/* <div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-1000" />
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom accent bar */}
|
||||||
|
<div className="mt-4 h-1 w-0 bg-linear-to-r from-red-500 to-red-600 group-hover:w-full transition-all duration-500 rounded-full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subtle noise texture overlay */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 pointer-events-none opacity-[0.03] mix-blend-overlay"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' /%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' /%3E%3C/svg%3E")`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
151
components/pages/products/filter/catalog.tsx
Normal file
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
87
components/pages/products/filter/category.tsx
Normal file
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
components/pages/products/filter/filter.tsx
Normal file
11
components/pages/products/filter/filter.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Category } from "./category";
|
||||||
|
import { CatalogSection } from "./catalog";
|
||||||
|
|
||||||
|
export default function Filter() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-1 lg:px-0 mb-2 px-3 w-full text-white ">
|
||||||
|
<Category />
|
||||||
|
<CatalogSection />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
components/pages/products/filter/useCataloghook.ts
Normal file
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
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,5 +1,5 @@
|
|||||||
export { ProductBanner } from "./productBanner";
|
export { ProductBanner } from "./banner";
|
||||||
export { Products } from "./products";
|
export { Products } from "./product/products";
|
||||||
export { SliderComp } from "./slug/slider";
|
export { SliderComp } from "./slug/slider";
|
||||||
export { RightSide } from "./slug/rightSide";
|
export { RightSide } from "./slug/rightSide";
|
||||||
export { Features } from "./slug/features";
|
export { Features } from "./slug/features";
|
||||||
|
|||||||
135
components/pages/products/product/mianProduct.tsx
Normal file
135
components/pages/products/product/mianProduct.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import ProductCard from "./productCard";
|
||||||
|
import { useCategory } from "@/zustand/useCategory";
|
||||||
|
import { useFilter } from "@/lib/filter-zustand";
|
||||||
|
import { useMemo, useState, useEffect } from "react";
|
||||||
|
import { useProductPageInfo } from "@/zustand/useProduct";
|
||||||
|
import { useSubCategory } from "@/zustand/useSubCategory";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import PaginationLite from "@/components/paginationUI";
|
||||||
|
import { useCatalog } from "@/zustand/useCatalog";
|
||||||
|
|
||||||
|
export default function MainProduct() {
|
||||||
|
const t = useTranslations();
|
||||||
|
const category = useCategory((s) => s.category);
|
||||||
|
const subCategory = useSubCategory((s) => s.subCategory);
|
||||||
|
const filter = useFilter((s) => s.filter);
|
||||||
|
const getFiltersByType = useFilter((s) => s.getFiltersByType);
|
||||||
|
const setProduct = useProductPageInfo((s) => s.setProducts);
|
||||||
|
|
||||||
|
const parentID = useCatalog((state) => state.parentID);
|
||||||
|
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, [parentID]);
|
||||||
|
|
||||||
|
// ── Filter params ────────────────────────────────────────────────────────
|
||||||
|
const queryParams = useMemo(() => {
|
||||||
|
const catalog = getFiltersByType("catalog");
|
||||||
|
const size = getFiltersByType("size");
|
||||||
|
const catalogParams = catalog.map((i) => `catalog=${i.id}`).join("&");
|
||||||
|
const sizeParams = size.map((i) => `size=${i.id}`).join("&");
|
||||||
|
const allParams = [catalogParams, sizeParams].filter(Boolean).join("&");
|
||||||
|
setCurrentPage(1);
|
||||||
|
return allParams ? `&${allParams}` : "";
|
||||||
|
}, [filter]);
|
||||||
|
|
||||||
|
// ── Request URL ──────────────────────────────────────────────────────────
|
||||||
|
const requestLink = useMemo(() => {
|
||||||
|
const baseLink = category.have_sub_category
|
||||||
|
? endPoints.product.bySubCategory({ id: subCategory.id, currentPage })
|
||||||
|
: parentID
|
||||||
|
? endPoints.product.byCatalogSection({ id: parentID, currentPage })
|
||||||
|
: endPoints.product.byCategory({ id: category.id, currentPage });
|
||||||
|
|
||||||
|
return `${baseLink}${queryParams}`;
|
||||||
|
}, [
|
||||||
|
category.id,
|
||||||
|
category.have_sub_category,
|
||||||
|
subCategory.id,
|
||||||
|
currentPage,
|
||||||
|
parentID,
|
||||||
|
queryParams,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ── Query ────────────────────────────────────────────────────────────────
|
||||||
|
const { data, isLoading, error } = useQuery({
|
||||||
|
queryKey: [
|
||||||
|
"products",
|
||||||
|
category.id,
|
||||||
|
category.have_sub_category,
|
||||||
|
subCategory.id,
|
||||||
|
parentID,
|
||||||
|
queryParams,
|
||||||
|
currentPage,
|
||||||
|
],
|
||||||
|
queryFn: () => httpClient(requestLink),
|
||||||
|
placeholderData: (prev) => prev, // no flicker on pagination
|
||||||
|
select: (res) => ({
|
||||||
|
results: res?.data?.data?.results ?? [],
|
||||||
|
totalPages: res?.data?.data?.total_pages ?? 1,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = data?.results ?? [];
|
||||||
|
const totalPages = data?.totalPages ?? 1;
|
||||||
|
|
||||||
|
// ── Render states ────────────────────────────────────────────────────────
|
||||||
|
if (isLoading && !data) {
|
||||||
|
return (
|
||||||
|
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div key={i} className="h-96 bg-gray-800 animate-pulse rounded-2xl" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="text-center text-red-500 py-10">{t("loadingError")}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!results.length) {
|
||||||
|
return (
|
||||||
|
<div className="text-center text-gray-400 py-10">
|
||||||
|
{t("productsNotFound")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div
|
||||||
|
className={`grid lg:grid-cols-4 sm:grid-cols-2 grid-cols-1 gap-5 transition-opacity ${
|
||||||
|
isLoading ? "opacity-50 pointer-events-none" : "opacity-100"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{results.map((item: any) => (
|
||||||
|
<ProductCard
|
||||||
|
key={item.id}
|
||||||
|
getProduct={() => setProduct(item)}
|
||||||
|
title={item.name}
|
||||||
|
image={item?.images?.[0]?.image || ""}
|
||||||
|
slug="special_product"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<PaginationLite
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onChange={(p) => setCurrentPage(p)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
161
components/pages/products/product/productCard.tsx
Normal file
161
components/pages/products/product/productCard.tsx
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
interface ProductCardProps {
|
||||||
|
title: string;
|
||||||
|
image: string;
|
||||||
|
slug: string;
|
||||||
|
getProduct: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProductCard({
|
||||||
|
title,
|
||||||
|
image,
|
||||||
|
slug,
|
||||||
|
getProduct,
|
||||||
|
}: ProductCardProps) {
|
||||||
|
const locale = useLocale();
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/${locale}/catalog_page/products/${slug}`}
|
||||||
|
onClick={getProduct}
|
||||||
|
className="group block"
|
||||||
|
>
|
||||||
|
<article className="
|
||||||
|
relative
|
||||||
|
bg-neutral-900
|
||||||
|
border border-zinc-800
|
||||||
|
rounded-xl
|
||||||
|
overflow-hidden
|
||||||
|
transition-all duration-300
|
||||||
|
hover:border-red-600/60
|
||||||
|
hover:shadow-[0_0_24px_0_rgba(220,38,38,0.15)]
|
||||||
|
max-sm:max-w-100 max-sm:mx-auto max-sm:w-full
|
||||||
|
h-95 flex flex-col
|
||||||
|
">
|
||||||
|
|
||||||
|
{/* Top accent line */}
|
||||||
|
<div className="
|
||||||
|
absolute top-0 left-0 right-0 h-0.5
|
||||||
|
bg-linear-to-r from-transparent via-red-600 to-transparent
|
||||||
|
opacity-0 group-hover:opacity-100
|
||||||
|
transition-opacity duration-300
|
||||||
|
z-10
|
||||||
|
" />
|
||||||
|
|
||||||
|
{/* Image Container */}
|
||||||
|
<div className="
|
||||||
|
relative
|
||||||
|
h-48 sm:h-56 md:h-64
|
||||||
|
bg-zinc-900
|
||||||
|
border-b border-zinc-800
|
||||||
|
overflow-hidden
|
||||||
|
">
|
||||||
|
{/* Background pattern */}
|
||||||
|
<div className="
|
||||||
|
absolute inset-0
|
||||||
|
bg-[radial-gradient(circle_at_center,_rgba(39,39,42,0.8)_0%,_rgba(9,9,11,1)_100%)]
|
||||||
|
" />
|
||||||
|
|
||||||
|
<Image
|
||||||
|
src={image || "/placeholder.svg"}
|
||||||
|
alt={title}
|
||||||
|
fill
|
||||||
|
className="
|
||||||
|
object-contain
|
||||||
|
p-4
|
||||||
|
transition-transform duration-500
|
||||||
|
group-hover:scale-105
|
||||||
|
drop-shadow-[0_4px_12px_rgba(0,0,0,0.5)]
|
||||||
|
"
|
||||||
|
sizes="(max-width: 640px) 90vw, (max-width: 1024px) 45vw, 30vw"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Hover overlay */}
|
||||||
|
<div className="
|
||||||
|
absolute inset-0
|
||||||
|
bg-red-600/5
|
||||||
|
opacity-0 group-hover:opacity-100
|
||||||
|
transition-opacity duration-300
|
||||||
|
" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-5">
|
||||||
|
{/* Decorative line */}
|
||||||
|
<div className="
|
||||||
|
w-8 h-0.5
|
||||||
|
bg-red-600
|
||||||
|
mb-3
|
||||||
|
transition-all duration-300
|
||||||
|
group-hover:w-16
|
||||||
|
" />
|
||||||
|
|
||||||
|
<h3 className="
|
||||||
|
text-sm sm:text-base
|
||||||
|
font-bold
|
||||||
|
text-zinc-100
|
||||||
|
line-clamp-3
|
||||||
|
leading-snug
|
||||||
|
tracking-wide
|
||||||
|
transition-colors duration-300
|
||||||
|
group-hover:text-white
|
||||||
|
">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Bottom row */}
|
||||||
|
<div className="
|
||||||
|
flex items-center justify-between
|
||||||
|
mt-4 pt-4
|
||||||
|
border-t border-zinc-800
|
||||||
|
">
|
||||||
|
<span className="
|
||||||
|
text-xs
|
||||||
|
text-zinc-500
|
||||||
|
tracking-widest
|
||||||
|
uppercase
|
||||||
|
font-medium
|
||||||
|
">
|
||||||
|
{ t("home.services.learnmore")}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Arrow */}
|
||||||
|
<div className="
|
||||||
|
flex items-center justify-center
|
||||||
|
w-7 h-7
|
||||||
|
rounded-full
|
||||||
|
border border-zinc-700
|
||||||
|
text-zinc-500
|
||||||
|
transition-all duration-300
|
||||||
|
group-hover:border-red-600
|
||||||
|
group-hover:text-red-500
|
||||||
|
group-hover:bg-red-600/10
|
||||||
|
">
|
||||||
|
<svg
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 12 12"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2 6H10M10 6L7 3M10 6L7 9"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
components/pages/products/product/products.tsx
Normal file
18
components/pages/products/product/products.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import Filter from "../filter/filter";
|
||||||
|
import MainProduct from "./mianProduct";
|
||||||
|
|
||||||
|
export function Products() {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#1e1d1c] pb-10 pt-5 px-2">
|
||||||
|
<div className="max-w-300 mx-auto w-full z-20 relative">
|
||||||
|
<div className="flex flex-col items-start gap-2">
|
||||||
|
{/* filter part */}
|
||||||
|
<Filter />
|
||||||
|
|
||||||
|
{/* main products */}
|
||||||
|
<MainProduct />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { ArrowRight } from "lucide-react";
|
|
||||||
|
|
||||||
interface ProductCardProps {
|
|
||||||
title: string;
|
|
||||||
name: string;
|
|
||||||
image: string;
|
|
||||||
slug: string;
|
|
||||||
status: "full" | "empty" | "withOrder";
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ProductCard({
|
|
||||||
title,
|
|
||||||
name,
|
|
||||||
image,
|
|
||||||
slug,
|
|
||||||
status,
|
|
||||||
}: ProductCardProps) {
|
|
||||||
const statusColor =
|
|
||||||
status === "full"
|
|
||||||
? "text-green-500"
|
|
||||||
: status === "empty"
|
|
||||||
? "text-red-600"
|
|
||||||
: "text-yellow-800";
|
|
||||||
|
|
||||||
const statusText =
|
|
||||||
status === "full"
|
|
||||||
? "Sotuvda mavjud"
|
|
||||||
: status === "empty"
|
|
||||||
? "Sotuvda qolmagan"
|
|
||||||
: "Buyurtma asosida";
|
|
||||||
return (
|
|
||||||
<Link href={`/products/${slug}`}>
|
|
||||||
<article className="group transition-all duration-300 hover:cursor-pointer max-sm:max-w-100 max-sm:mx-auto max-sm:w-full ">
|
|
||||||
{/* Image Container */}
|
|
||||||
<div className="relative rounded-2xl h-45 sm:h-55 md:h-65 lg:w-[95%] w-[90%] mx-auto overflow-hidden bg-white">
|
|
||||||
<Image
|
|
||||||
src={image || "/placeholder.svg"}
|
|
||||||
alt={title}
|
|
||||||
fill
|
|
||||||
className="object-contain transition-transform duration-300"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content Container */}
|
|
||||||
<div className="p-6 sm:p-4">
|
|
||||||
{/* Title */}
|
|
||||||
<h3 className="text-lg sm:text-xl md:text-2xl font-bold text-white mb-4 line-clamp-3 group-hover:text-red-400 transition-colors duration-300">
|
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{/* Meta Information */}
|
|
||||||
<div className="flex flex-col items-start gap-0 text-gray-400 text-sm sm:text-base mb-6">
|
|
||||||
<span className="font-medium">{name}</span>
|
|
||||||
<span className={`font-medium ${statusColor}`}>{statusText}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Read More Link */}
|
|
||||||
<span className="inline-flex items-center gap-2 text-red-600 font-bold text-base sm:text-lg uppercase tracking-wide hover:gap-4 transition-all duration-300 group/link">
|
|
||||||
Read More
|
|
||||||
<ArrowRight className="w-5 h-5 transition-transform duration-300 group-hover/link:translate-x-1" />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import ProductCard from "./productCard";
|
|
||||||
|
|
||||||
export function Products() {
|
|
||||||
return (
|
|
||||||
<div className="bg-[#1e1d1c] py-20">
|
|
||||||
<div className="max-w-250 mx-auto w-full sm:-mt-50 -mt-30 z-20 relative">
|
|
||||||
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
|
||||||
{Array(9)
|
|
||||||
.fill(null)
|
|
||||||
.map((_, index) => (
|
|
||||||
<ProductCard
|
|
||||||
key={index}
|
|
||||||
title="Elektr yong'in detektori-Ypres ver.2"
|
|
||||||
name="P-0834404"
|
|
||||||
image="/images/products/products.webp"
|
|
||||||
slug="P_0834404"
|
|
||||||
status="full"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
36
components/pages/products/slug/empty.tsx
Normal file
36
components/pages/products/slug/empty.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// Empty State Component
|
||||||
|
export function EmptyState() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#1e1d1c] flex items-center justify-center px-4">
|
||||||
|
<div className="text-center max-w-md">
|
||||||
|
<div className="w-24 h-24 mx-auto mb-6 bg-gray-800 rounded-full flex items-center justify-center">
|
||||||
|
<svg
|
||||||
|
className="w-12 h-12 text-gray-600"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl font-bold text-white mb-2">
|
||||||
|
Mahsulot topilmadi
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-400 mb-6">
|
||||||
|
Siz qidirayotgan mahsulot mavjud emas yoki o'chirilgan
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="/products"
|
||||||
|
className="inline-block bg-red-700 hover:bg-red-800 text-white font-bold py-3 px-6 rounded-lg transition"
|
||||||
|
>
|
||||||
|
Mahsulotlarga qaytish
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,22 @@
|
|||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export function Features({ features }: { features: string[] }) {
|
export function Features({ features }: { features: string[] }) {
|
||||||
|
const t = useTranslations();
|
||||||
|
if (!features || features.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="w-full rounded-xl overflow-hidden">
|
<div className="mt-12">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-white mb-6">
|
||||||
|
{t("products.features")}
|
||||||
|
</h2>
|
||||||
|
<div className="rounded-xl overflow-hidden border border-gray-800 shadow-xl">
|
||||||
|
<table className="w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-gray-700 bg-black">
|
<tr className="bg-linear-to-r from-stone-800 to-black/10 border-b border-gray-800">
|
||||||
<th className="px-4 py-4 md:px-6 text-left text-sm md:text-base font-semibold text-white">
|
<th className="px-4 py-4 md:px-6 text-left text-sm md:text-base font-semibold text-white">
|
||||||
Feature
|
{t("products.feature")}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -12,16 +24,21 @@ export function Features({ features }: { features: string[] }) {
|
|||||||
{features.map((feature, index) => (
|
{features.map((feature, index) => (
|
||||||
<tr
|
<tr
|
||||||
key={index}
|
key={index}
|
||||||
className={` border-gray-700 transition-colors hover:bg-opacity-80 ${
|
className={`border-b border-gray-800 last:border-b-0 transition-colors hover:bg-red-900/10 ${
|
||||||
index % 2 === 0 ? "bg-[#323232]" : "bg-black/20"
|
index % 2 === 0 ? "bg-[#252525]" : "bg-[#1e1e1e]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<td className="px-4 py-4 md:px-6 text-sm md:text-base text-white font-medium">
|
<td className="px-4 py-4 md:px-6 text-sm md:text-base text-gray-300">
|
||||||
{feature}
|
<div className="flex items-start gap-3">
|
||||||
|
<span className="text-red-700 mt-1">•</span>
|
||||||
|
<span>{feature}</span>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
61
components/pages/products/slug/loading.tsx
Normal file
61
components/pages/products/slug/loading.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// Loading Skeleton Component
|
||||||
|
export function LoadingSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[#1e1d1c] py-20 md:py-32 lg:py-40 px-4 md:px-8">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 mb-12">
|
||||||
|
{/* Image Skeleton */}
|
||||||
|
<div className="w-full h-96 md:h-125 bg-gray-800 rounded-lg animate-pulse" />
|
||||||
|
|
||||||
|
{/* Info Skeleton */}
|
||||||
|
<div className="flex flex-col justify-center space-y-4">
|
||||||
|
{/* Title */}
|
||||||
|
<div className="h-8 bg-gray-800 rounded animate-pulse w-3/4" />
|
||||||
|
|
||||||
|
{/* Articular */}
|
||||||
|
<div className="h-6 bg-gray-800 rounded animate-pulse w-1/2" />
|
||||||
|
|
||||||
|
{/* Status */}
|
||||||
|
<div className="h-8 bg-gray-800 rounded animate-pulse w-1/3" />
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="h-4 bg-gray-800 rounded animate-pulse w-full" />
|
||||||
|
<div className="h-4 bg-gray-800 rounded animate-pulse w-5/6" />
|
||||||
|
<div className="h-4 bg-gray-800 rounded animate-pulse w-4/6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Price */}
|
||||||
|
<div className="h-10 bg-gray-800 rounded animate-pulse w-32" />
|
||||||
|
|
||||||
|
{/* Buttons */}
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="h-12 bg-gray-800 rounded animate-pulse flex-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Social */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="w-10 h-10 bg-gray-800 rounded-lg animate-pulse"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Features Skeleton */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="h-12 bg-gray-800 rounded-t-xl animate-pulse" />
|
||||||
|
{[1, 2, 3, 4].map((i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="h-16 bg-gray-800/50 rounded animate-pulse"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,110 +1,152 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { Facebook } from "lucide-react";
|
import { usePriceModalStore } from "@/zustand/useProceModalStore";
|
||||||
|
import { Check, Instagram, Send, Share2 } from "lucide-react";
|
||||||
const socialLinks = [
|
import { useTranslations } from "next-intl";
|
||||||
{ name: "telegram", icon: "✈️", color: "#0088cc" },
|
import { useParams } from "next/navigation";
|
||||||
{ name: "facebook", icon: <Facebook />, color: "#1877F2" },
|
import { useState } from "react";
|
||||||
{ name: "odnoklassniki", icon: "ok", color: "#ED7100" },
|
|
||||||
{ name: "vkontakte", icon: "VK", color: "#0077FF" },
|
|
||||||
{ name: "twitter", icon: "𝕏", color: "#1DA1F2" },
|
|
||||||
{ name: "whatsapp", icon: "W", color: "#25D366" },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface RightSideProps {
|
interface RightSideProps {
|
||||||
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
name: string;
|
articular: string;
|
||||||
|
status: string;
|
||||||
description: string;
|
description: string;
|
||||||
statusText: string;
|
price: string;
|
||||||
statusColor: string;
|
image: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RightSide({
|
export function RightSide({
|
||||||
title,
|
title,
|
||||||
name,
|
articular,
|
||||||
|
status,
|
||||||
description,
|
description,
|
||||||
statusColor,
|
price,
|
||||||
statusText,
|
id,
|
||||||
|
image,
|
||||||
}: RightSideProps) {
|
}: RightSideProps) {
|
||||||
|
const openModal = usePriceModalStore((state) => state.openModal);
|
||||||
|
const t = useTranslations();
|
||||||
|
const params = useParams();
|
||||||
|
const locale = params.locale || "uz";
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
|
const handleShare = async () => {
|
||||||
|
const productUrl = `${window.location.origin}/${locale}/catalog_page/products/special_product?productId=${id}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Modern Web Share API dan foydalanish (mobil qurilmalar uchun)
|
||||||
|
if (navigator.share) {
|
||||||
|
await navigator.share({
|
||||||
|
title: title,
|
||||||
|
text: `${title} - ${description.slice(0, 100)}...`,
|
||||||
|
url: productUrl,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Desktop uchun clipboard ga copy qilish
|
||||||
|
await navigator.clipboard.writeText(productUrl);
|
||||||
|
setCopied(true);
|
||||||
|
|
||||||
|
// 2 soniyadan keyin "Copied" holatini o'chirish
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Share error:", error);
|
||||||
|
// Fallback - clipboard ga copy qilish
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(productUrl);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 2000);
|
||||||
|
} catch (clipboardError) {
|
||||||
|
console.error("Clipboard error:", clipboardError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGetPrice = () => {
|
||||||
|
openModal({
|
||||||
|
id,
|
||||||
|
name: title,
|
||||||
|
image,
|
||||||
|
inStock: status === "Sotuvda mavjud",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Status color logic
|
||||||
|
const isInStock = status === "Sotuvda mavjud";
|
||||||
|
const statusColor = isInStock
|
||||||
|
? "bg-green-600/20 text-green-400 border border-green-600/30"
|
||||||
|
: "bg-red-600/20 text-red-400 border border-red-600/30";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center">
|
<div className="flex flex-col justify-center space-y-6">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold text-white mb-4 leading-tight">
|
<h1 className="text-xl md:text-3xl font-unbounded font-bold text-white leading-tight">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Article ID */}
|
{/* Article ID */}
|
||||||
<div className="mb-3">
|
<div className="flex items-center gap-2 text-sm md:text-base">
|
||||||
<p className="text-gray-400">
|
<span className="text-gray-400">Artikul:</span>
|
||||||
Artikul:
|
<span className="text-white font-semibold">{articular}</span>
|
||||||
<span className="text-white font-semibold">{name}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Badge */}
|
{/* Status Badge */}
|
||||||
<div className="mb-2">
|
<div>
|
||||||
<span
|
<span
|
||||||
className={`inline-block py-2 rounded text-sm font-semibold ${statusColor}`}
|
className={`inline-block px-4 py-2 rounded-lg text-sm font-semibold ${statusColor}`}
|
||||||
>
|
>
|
||||||
{statusText}
|
{status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* description */}
|
{/* Description */}
|
||||||
<div className="mb-2">
|
<div className="border-l-4 border-red-700 pl-4">
|
||||||
<p className="text-sm font-bold text-white mb-4 leading-tight">
|
<p className="text-sm md:text-base text-gray-300 leading-relaxed">
|
||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Price Section */}
|
{/* Price Section */}
|
||||||
<div className="mb-8">
|
<div className="bg-[#1716169f] rounded-xl p-5 space-y-6">
|
||||||
<h2 className="text-2xl md:text-3xl font-bold text-red-700 mb-6">
|
{/* Action Button */}
|
||||||
17.00$
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4 mb-6">
|
|
||||||
{/* <button
|
|
||||||
onClick={onPriceClick}
|
|
||||||
className="flex-1 bg-red-700 hover:bg-red-800 text-white font-bold py-3 px-6 rounded-lg transition duration-300 transform hover:scale-105"
|
|
||||||
>
|
|
||||||
Narxni bilish
|
|
||||||
</button> */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {}}
|
onClick={handleGetPrice}
|
||||||
className="flex-1 border-2 border-red-700 text-red-700 hover:bg-red-50 font-bold py-3 px-6 rounded-lg transition duration-300"
|
className="w-full bg-red-700 hover:bg-red-800 text-white font-bold py-4 px-6 rounded-lg transition-all duration-300 transform hover:scale-105 hover:shadow-lg hover:shadow-red-700/50"
|
||||||
>
|
>
|
||||||
Xabar yuborish
|
{t("products.send")}
|
||||||
</button>
|
</button>
|
||||||
{/* <button
|
|
||||||
onClick={() => setIsFavorite(!isFavorite)}
|
|
||||||
className="p-3 border-2 border-gray-600 rounded-lg hover:border-red-700 transition duration-300"
|
|
||||||
title="Add to favorites"
|
|
||||||
>
|
|
||||||
<Heart
|
|
||||||
size={24}
|
|
||||||
className={
|
|
||||||
isFavorite ? "fill-red-700 text-red-700" : "text-gray-600"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</button> */}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Social Share Icons */}
|
{/* Social Share */}
|
||||||
<div className="flex gap-3 items-center">
|
<div className="pt-4 border-t border-gray-800 flex items-center gap-5">
|
||||||
<div className="flex gap-2">
|
<button
|
||||||
{socialLinks.map((social) => (
|
onClick={handleShare}
|
||||||
<a
|
className="flex items-center gap-3 mb-3 text-gray-400 hover:text-white transition-colors group"
|
||||||
key={social.name}
|
|
||||||
href="#"
|
|
||||||
className="w-10 h-10 rounded-lg flex items-center justify-center text-white text-sm font-bold transition duration-300 hover:scale-110"
|
|
||||||
style={{ backgroundColor: social.color }}
|
|
||||||
title={social.name}
|
|
||||||
>
|
>
|
||||||
{social.icon}
|
{copied ? (
|
||||||
|
<>
|
||||||
|
<Check className="w-5 h-5 text-green-400" />
|
||||||
|
<span className="text-sm text-green-400">
|
||||||
|
{t("products.copied") || "Link nusxalandi!"}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Share2 className="w-5 h-5 group-hover:scale-110 transition-transform" />
|
||||||
|
<span className="text-sm">{t("products.share")}:</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
href="https://t.me/ignum_tech"
|
||||||
|
className="p-2 rounded-md bg-white text-red-500 hover:text-white hover:bg-red-500"
|
||||||
|
>
|
||||||
|
<Send />
|
||||||
</a>
|
</a>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,60 +1,158 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
import { Navigation } from "swiper/modules";
|
import { Navigation, Pagination, Thumbs } from "swiper/modules";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { Swiper as SwiperType } from "swiper";
|
||||||
import "swiper/css";
|
import "swiper/css";
|
||||||
import "swiper/css/navigation";
|
import "swiper/css/navigation";
|
||||||
import { DATA } from "@/lib/demoData";
|
import "swiper/css/pagination";
|
||||||
|
import "swiper/css/thumbs";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
// The custom CSS selectors for navigation
|
|
||||||
const navigationPrevEl = ".custom-swiper-prev";
|
const navigationPrevEl = ".custom-swiper-prev";
|
||||||
const navigationNextEl = ".custom-swiper-next";
|
const navigationNextEl = ".custom-swiper-next";
|
||||||
|
|
||||||
export function SliderComp({ imgs }: { imgs: string[] }) {
|
export function SliderComp({ imgs }: { imgs: string[] }) {
|
||||||
|
const [thumbsSwiper, setThumbsSwiper] = useState<SwiperType | null>(null);
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
// Agar rasm bo'lmasa
|
||||||
|
if (!imgs || imgs.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="w-full h-96 md:h-125 bg-gray-800 rounded-lg flex items-center justify-center">
|
||||||
<div className="flex items-center justify-center relative">
|
<div className="text-center">
|
||||||
|
<svg
|
||||||
|
className="w-20 h-20 text-gray-600 mx-auto mb-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p className="text-gray-500">{t("image_not_found")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Main Slider */}
|
||||||
|
<div className="relative group">
|
||||||
<Swiper
|
<Swiper
|
||||||
modules={[Navigation]}
|
modules={[Navigation, Pagination, Thumbs]}
|
||||||
|
thumbs={{
|
||||||
|
swiper:
|
||||||
|
thumbsSwiper && !thumbsSwiper.destroyed ? thumbsSwiper : null,
|
||||||
|
}}
|
||||||
navigation={{
|
navigation={{
|
||||||
// Pass the class selectors here
|
|
||||||
prevEl: navigationPrevEl,
|
prevEl: navigationPrevEl,
|
||||||
nextEl: navigationNextEl,
|
nextEl: navigationNextEl,
|
||||||
}}
|
}}
|
||||||
pagination={{ clickable: true }}
|
pagination={{ clickable: true }}
|
||||||
className="w-full h-96 md:h-125 bg-white rounded-lg overflow-hidden shadow-lg"
|
loop={imgs.length > 1}
|
||||||
|
className="w-[90%] h-96 md:h-96 rounded-lg overflow-hidden shadow-xl"
|
||||||
>
|
>
|
||||||
{imgs.map((image, index) => (
|
{imgs.map((image, index) => (
|
||||||
<SwiperSlide
|
<SwiperSlide
|
||||||
key={index}
|
key={index}
|
||||||
className="bg-white flex items-center justify-center"
|
className=" flex items-center justify-center"
|
||||||
>
|
>
|
||||||
<img
|
<div className="relative w-full h-full p-4 md:p-8">
|
||||||
src={image || "/placeholder.svg"}
|
<Image
|
||||||
alt={`${DATA[0].title} - ${index + 1}`}
|
src={image}
|
||||||
className="w-full h-full object-contain p-4"
|
alt={`Product image ${index + 1}`}
|
||||||
|
fill
|
||||||
|
className="object-contain"
|
||||||
|
sizes="(max-width: 768px) 100vw, 50vw"
|
||||||
|
priority={index === 0}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
</Swiper>
|
</Swiper>
|
||||||
{/* Custom buttons */}
|
|
||||||
|
{/* Navigation Buttons */}
|
||||||
|
{imgs.length > 1 && (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
className={`${navigationPrevEl.replace(
|
className={`${navigationPrevEl.replace(
|
||||||
".",
|
".",
|
||||||
"",
|
"",
|
||||||
)} absolute z-10 top-1/2 left-5 rounded-sm pb-2 w-8 h-8 bg-primary text-[30px] text-center
|
)} absolute z-10 top-1/2 -translate-y-1/2 left-2 md:left-4 rounded-lg w-10 h-10 md:w-12 md:h-12 bg-red-700/90 hover:bg-red-800 text-white flex items-center justify-center transition opacity-0 group-hover:opacity-100 shadow-lg`}
|
||||||
text-white flex items-center justify-center hover:cursor-pointer transition`}
|
|
||||||
>
|
>
|
||||||
‹
|
<svg
|
||||||
|
className="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M15 19l-7-7 7-7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`${navigationNextEl.replace(
|
className={`${navigationNextEl.replace(
|
||||||
".",
|
".",
|
||||||
"",
|
"",
|
||||||
)} absolute z-10 top-1/2 right-5 rounded-sm pb-2 w-8 h-8 bg-primary text-[30px] text-center text-white flex items-center justify-center hover:cursor-pointer transition `}
|
)} absolute z-10 top-1/2 -translate-y-1/2 right-2 md:right-4 rounded-lg w-10 h-10 md:w-12 md:h-12 bg-red-700/90 hover:bg-red-800 text-white flex items-center justify-center transition opacity-0 group-hover:opacity-100 shadow-lg`}
|
||||||
>
|
>
|
||||||
›
|
<svg
|
||||||
|
className="w-6 h-6"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M9 5l7 7-7 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Thumbnail Slider */}
|
||||||
|
{imgs.length > 1 && (
|
||||||
|
<Swiper
|
||||||
|
onSwiper={setThumbsSwiper}
|
||||||
|
spaceBetween={10}
|
||||||
|
slidesPerView={4}
|
||||||
|
breakpoints={{
|
||||||
|
640: { slidesPerView: 5 },
|
||||||
|
768: { slidesPerView: 6 },
|
||||||
|
}}
|
||||||
|
watchSlidesProgress
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{imgs.map((image, index) => (
|
||||||
|
<SwiperSlide key={index}>
|
||||||
|
<div className="relative h-20 md:h-24 bg-white rounded-lg overflow-hidden cursor-pointer border-2 border-transparent hover:border-red-700 transition">
|
||||||
|
<Image
|
||||||
|
src={image}
|
||||||
|
alt={`Thumbnail ${index + 1}`}
|
||||||
|
fill
|
||||||
|
className="object-contain p-2"
|
||||||
|
sizes="100px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
72
components/pages/services/empty.tsx
Normal file
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
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
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
components/pages/subCategory/body.tsx
Normal file
58
components/pages/subCategory/body.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useCategory } from "@/zustand/useCategory";
|
||||||
|
import Card from "./card";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
export function MainSubCategory() {
|
||||||
|
const category = useCategory((state) => state.category);
|
||||||
|
const t = useTranslations();
|
||||||
|
const { data, isLoading, error } = useQuery({
|
||||||
|
queryKey: ["subCategory"],
|
||||||
|
queryFn: () => httpClient(endPoints.subCategory.byId(category.id)),
|
||||||
|
select: (data) => data?.data?.results,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<div key={i} className="h-96 bg-gray-800 animate-pulse rounded-2xl" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="text-center text-red-500 py-10">
|
||||||
|
{t("loading_error")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-center text-gray-400 py-10">
|
||||||
|
{t("products_not_found")}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||||
|
{data.map((item: any) => (
|
||||||
|
<Card
|
||||||
|
key={item.id}
|
||||||
|
title={item.name}
|
||||||
|
image={item.image}
|
||||||
|
slug={item.slug}
|
||||||
|
id={item.id}
|
||||||
|
category={item.category}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
components/pages/subCategory/card.tsx
Normal file
60
components/pages/subCategory/card.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { useSubCategory } from "@/zustand/useSubCategory";
|
||||||
|
import { useLocale } from "next-intl";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
|
interface ProductCardProps {
|
||||||
|
id: number;
|
||||||
|
category: number;
|
||||||
|
title: string;
|
||||||
|
image: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Card({
|
||||||
|
title,
|
||||||
|
image,
|
||||||
|
slug,
|
||||||
|
id,
|
||||||
|
category,
|
||||||
|
}: ProductCardProps) {
|
||||||
|
const locale = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
const setSubCategory = useSubCategory((state) => state.setSubCategory);
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
setSubCategory({
|
||||||
|
id,
|
||||||
|
name: title,
|
||||||
|
image,
|
||||||
|
category,
|
||||||
|
});
|
||||||
|
router.push(`/${locale}/catalog_page/products`);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Link href="#" onClick={handleClick}>
|
||||||
|
<article className="group transition-all duration-300 hover:cursor-pointer max-sm:max-w-100 max-sm:mx-auto max-sm:w-full">
|
||||||
|
{/* Image Container */}
|
||||||
|
<div className="relative rounded-2xl h-45 sm:h-55 md:h-65 lg:w-[95%] w-[90%] mx-auto overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={image || "/placeholder.svg"}
|
||||||
|
alt={title}
|
||||||
|
fill
|
||||||
|
className="object-contain transition-transform duration-300 group-hover:scale-105"
|
||||||
|
sizes="(max-width: 640px) 90vw, (max-width: 1024px) 45vw, 30vw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content Container */}
|
||||||
|
<div className="p-6 sm:p-4">
|
||||||
|
<h3 className="text-lg text-center font-unbounded font-bold text-white mb-4 line-clamp-3 group-hover:text-red-400 transition-colors duration-300">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
1
components/pages/subCategory/index.ts
Normal file
1
components/pages/subCategory/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { MainSubCategory } from "./body";
|
||||||
99
components/paginationUI.tsx
Normal file
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
265
components/priceContact.tsx
Normal file
265
components/priceContact.tsx
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
"use client";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { usePriceModalStore } from "@/zustand/useProceModalStore";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import httpClient from "@/request/api";
|
||||||
|
import { endPoints } from "@/request/links";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
|
interface FormType {
|
||||||
|
name: string;
|
||||||
|
product: number;
|
||||||
|
number: number; // ✅ String bo'lishi kerak
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PriceModal() {
|
||||||
|
const t = useTranslations("priceModal");
|
||||||
|
const { isOpen, product, closeModal } = usePriceModalStore();
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
number: "+998 ",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [errors, setErrors] = useState({
|
||||||
|
name: "",
|
||||||
|
number: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRequest = useMutation({
|
||||||
|
mutationFn: (data: FormType) =>
|
||||||
|
httpClient.post(endPoints.post.productContact, data),
|
||||||
|
onSuccess: () => {
|
||||||
|
setFormData({
|
||||||
|
name: "",
|
||||||
|
number: "+998 ",
|
||||||
|
});
|
||||||
|
toast.success(t("success") || "Muvaffaqiyatli yuborildi!");
|
||||||
|
closeModal();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error("Error:", error);
|
||||||
|
toast.error(t("error") || "Xatolik yuz berdi");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset form when modal closes for github
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) {
|
||||||
|
setFormData({
|
||||||
|
name: "",
|
||||||
|
number: "+998 ",
|
||||||
|
});
|
||||||
|
setErrors({
|
||||||
|
name: "",
|
||||||
|
number: "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Prevent body scroll when modal is open
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = "unset";
|
||||||
|
};
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const formatPhoneNumber = (value: string) => {
|
||||||
|
const numbers = value.replace(/\D/g, "");
|
||||||
|
if (!numbers.startsWith("998")) {
|
||||||
|
return "+998 ";
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted = "+998 ";
|
||||||
|
const rest = numbers.slice(3);
|
||||||
|
if (rest.length > 0) formatted += rest.slice(0, 2);
|
||||||
|
if (rest.length > 2) formatted += " " + rest.slice(2, 5);
|
||||||
|
if (rest.length > 5) formatted += " " + rest.slice(5, 7);
|
||||||
|
if (rest.length > 7) formatted += " " + rest.slice(7, 9);
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const formatted = formatPhoneNumber(e.target.value);
|
||||||
|
setFormData({ ...formData, number: formatted });
|
||||||
|
if (errors.number) {
|
||||||
|
setErrors({ ...errors, number: "" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData({ ...formData, [name]: value });
|
||||||
|
if (errors[name as keyof typeof errors]) {
|
||||||
|
setErrors({ ...errors, [name]: "" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateForm = () => {
|
||||||
|
const newErrors = {
|
||||||
|
name: "",
|
||||||
|
number: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Name validation
|
||||||
|
if (!formData.name.trim()) {
|
||||||
|
newErrors.name = t("validation.nameRequired") || "Ism kiritilishi shart";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phone validation
|
||||||
|
const phoneNumbers = formData.number.replace(/\D/g, "");
|
||||||
|
if (phoneNumbers.length !== 12) {
|
||||||
|
newErrors.number =
|
||||||
|
t("validation.phoneRequired") || "To'liq telefon raqam kiriting";
|
||||||
|
} else if (!phoneNumbers.startsWith("998")) {
|
||||||
|
newErrors.number =
|
||||||
|
t("validation.phoneInvalid") || "Noto'g'ri telefon raqam";
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrors(newErrors);
|
||||||
|
return !newErrors.name && !newErrors.number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!validateForm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Telefon raqamni tozalash (faqat raqamlar)
|
||||||
|
const cleanPhone = formData.number.replace(/\D/g, "");
|
||||||
|
|
||||||
|
const sendedData: FormType = {
|
||||||
|
name: formData.name,
|
||||||
|
number: Number(cleanPhone.slice(3)), // ✅ String sifatida yuborish
|
||||||
|
product: product?.id || 0,
|
||||||
|
};
|
||||||
|
formRequest.mutate(sendedData);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen || !product) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||||
|
onClick={closeModal}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
<div className="relative bg-[#2a2a2a] rounded-2xl shadow-2xl w-full max-w-md max-h-[90vh] overflow-y-auto">
|
||||||
|
{/* Close button */}
|
||||||
|
<button
|
||||||
|
onClick={closeModal}
|
||||||
|
className="absolute right-4 top-4 text-gray-400 hover:text-white transition-colors z-10"
|
||||||
|
aria-label="Close modal"
|
||||||
|
>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="p-6 md:p-8">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-white mb-6">
|
||||||
|
{t("title") || "Narx so'rash"}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Product Info */}
|
||||||
|
<div className="bg-[#1e1e1e] rounded-lg p-4 mb-6 flex items-center gap-4">
|
||||||
|
<div className="relative w-20 h-20 shrink-0 bg-white rounded-lg overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={product.image || "/placeholder.svg"}
|
||||||
|
alt={product.name}
|
||||||
|
fill
|
||||||
|
className="object-contain p-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-white font-semibold mb-1 line-clamp-2">
|
||||||
|
{product.name}
|
||||||
|
</h3>
|
||||||
|
<span className="text-green-400 text-sm">
|
||||||
|
{product.inStock
|
||||||
|
? t("product.inStock") || "Sotuvda mavjud"
|
||||||
|
: t("product.outOfStock") || "Sotuvda yo'q"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
{/* Name */}
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block text-sm font-medium text-gray-300 mb-2"
|
||||||
|
>
|
||||||
|
{t("form.name") || "Ismingiz"}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className={`w-full px-4 py-3 bg-[#1e1e1e] border ${
|
||||||
|
errors.name ? "border-red-500" : "border-gray-700"
|
||||||
|
} rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-red-700 transition`}
|
||||||
|
placeholder={t("form.namePlaceholder") || "Ismingizni kiriting"}
|
||||||
|
/>
|
||||||
|
{errors.name && (
|
||||||
|
<p className="mt-1 text-sm text-red-500">{errors.name}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Phone */}
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="phone"
|
||||||
|
className="block text-sm font-medium text-gray-300 mb-2"
|
||||||
|
>
|
||||||
|
{t("form.phone") || "Telefon raqam"}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
id="phone"
|
||||||
|
name="phone"
|
||||||
|
value={formData.number}
|
||||||
|
onChange={handlePhoneChange}
|
||||||
|
className={`w-full px-4 py-3 bg-[#1e1e1e] border ${
|
||||||
|
errors.number ? "border-red-500" : "border-gray-700"
|
||||||
|
} rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-red-700 transition`}
|
||||||
|
placeholder="+998 90 123 45 67"
|
||||||
|
maxLength={17}
|
||||||
|
/>
|
||||||
|
{errors.number && (
|
||||||
|
<p className="mt-1 text-sm text-red-500">{errors.number}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={formRequest.isPending}
|
||||||
|
className="w-full bg-red-700 hover:bg-red-800 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-bold py-3 px-6 rounded-lg transition-all duration-300 transform hover:scale-105 disabled:hover:scale-100"
|
||||||
|
>
|
||||||
|
{formRequest.isPending
|
||||||
|
? t("form.submitting") || "Yuborilmoqda..."
|
||||||
|
: t("form.submit") || "Yuborish"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
components/provider/index.tsx
Normal file
33
components/provider/index.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
|
import BackAnimatsiya from "../backAnimatsiya/backAnimatsiya";
|
||||||
|
import { Footer, Navbar } from "../layout";
|
||||||
|
import { PriceModal } from "../priceContact";
|
||||||
|
import { Analytics } from "@vercel/analytics/next";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import { Bounce, Flip, ToastContainer, Zoom } from "react-toastify";
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
export function Providers({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
{/* <BackAnimatsiya /> */}
|
||||||
|
<Navbar />
|
||||||
|
{children}
|
||||||
|
<Footer />
|
||||||
|
<PriceModal />
|
||||||
|
<Analytics />
|
||||||
|
<ToastContainer
|
||||||
|
position="top-center"
|
||||||
|
autoClose={3000}
|
||||||
|
hideProgressBar={true}
|
||||||
|
transition={Zoom}
|
||||||
|
theme="colored"
|
||||||
|
/>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
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
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
298
lib/demoData.ts
298
lib/demoData.ts
@@ -1,3 +1,46 @@
|
|||||||
|
import { title } from "process";
|
||||||
|
|
||||||
|
export const certs = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
src: "/images/about/sertificate.webp",
|
||||||
|
title: "Пожаростойкие армированные трубы SLT BLOCKFIRE PP-R-GF",
|
||||||
|
year: "2024",
|
||||||
|
artikul: "PP-R-GF",
|
||||||
|
features: [
|
||||||
|
"СТО 22.21.29-015-17207509-2022 (версия 2) — согласован МЧС России в качестве нормативного документа по пожарной безопасности.",
|
||||||
|
"СТО 22.21.29-021-17207509-2024 «Автоматическая противопожарная защита многоярусных стеллажных конструкций» — согласован МЧС России №ГУ-исх-66586 от 05.07.2024.",
|
||||||
|
"Протоколы испытаний по ГОСТ Р 58832 ИЛ НИЦ ПТ и СП ФГБУ ВНИИПО МЧС России № 2249/2.1-2022 от 03.03.2022, №2683/2.1-2023 от 06.10.2023.",
|
||||||
|
"Свидетельство о государственной регистрации № RU.77.01.34.013.E.001631.07.20 от 07.07.2020.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
src: "/images/about/sertificate.webp",
|
||||||
|
title: "Пожаростойкие однослойные трубы SLT BLOCKFIRE PP-R",
|
||||||
|
year: "2023",
|
||||||
|
artikul: "PP-R",
|
||||||
|
features: [
|
||||||
|
"СТО 22.21.29-015-17207509-2022 (версия 2) — согласован МЧС России в качестве нормативного документа по пожарной безопасности.",
|
||||||
|
"СТО 22.21.29-021-17207509-2024 «Автоматическая противопожарная защита многоярусных стеллажных конструкций» — согласован МЧС России №ГУ-исх-66586 от 05.07.2024.",
|
||||||
|
"Протоколы испытаний ИЛ НИЦ ПТ и СП ФГБУ ВНИИПО МЧС России № 2249/2.1-2022 от 03.03.2022, №2683/2.1-2023 от 06.10.2023.",
|
||||||
|
"Свидетельство о государственной регистрации № RU.77.01.34.008.E.001638.07.20 от 08.07.2020.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
src: "/images/about/sertificate.webp",
|
||||||
|
title: "Пожаростойкие фитинги SLT BLOCKFIRE PP-R",
|
||||||
|
year: "2023",
|
||||||
|
artikul: "Фитинги",
|
||||||
|
features: [
|
||||||
|
"СТО 22.21.29-015-17207509-2022 (версия 2) — согласован МЧС России в качестве нормативного документа по пожарной безопасности.",
|
||||||
|
"СТО 22.21.29-021-17207509-2024 «Автоматическая противопожарная защита многоярусных стеллажных конструкций» — согласован МЧС России №ГУ-исх-66586 от 05.07.2024.",
|
||||||
|
"Протоколы испытаний по ГОСТ Р 58832 ИЛ НИЦ ПТ и СП ФГБУ ВНИИПО МЧС России № 2249/2.1-2022 от 03.03.2022, №2683/2.1-2023 от 06.10.2023.",
|
||||||
|
"Свидетельство о государственной регистрации № RU.77.01.34.013.E.001630.07.20 от 07.07.2020.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export const DATA = [
|
export const DATA = [
|
||||||
{
|
{
|
||||||
@@ -30,41 +73,234 @@ export const DATA = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const faqItems = [
|
export const ProductCatalog = [
|
||||||
{
|
{
|
||||||
id: "faq-1",
|
id: "slt",
|
||||||
question: "How do I become a firefighter?",
|
slug: "slt_blockfire",
|
||||||
answer:
|
title: "SLT Blockfire",
|
||||||
"Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel sit amet adipiscing sem neque.",
|
description: "products.catalog.blockdescription",
|
||||||
|
image: "/images/products/category/slt.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "faq-2",
|
id: "ca",
|
||||||
question: "What equipment do firefighters use?",
|
slug: "ca_fire_mech",
|
||||||
answer:
|
title: "CA-FIRE | MECH",
|
||||||
"Firefighters use specialized equipment including protective gear, breathing apparatus, fire hoses, ladders, and various rescue tools. Each piece of equipment is designed to keep firefighters safe while they perform their duties. Our team is trained extensively on all equipment and safety protocols.",
|
description: "products.catalog.cadescription",
|
||||||
|
image: "/images/products/category/ca.png",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "faq-3",
|
id: "lede",
|
||||||
question: "Do firefighters only fight fires?",
|
slug: "lede",
|
||||||
answer:
|
title: "LEDE",
|
||||||
"No, modern firefighters respond to a wide variety of emergencies including medical calls, vehicle accidents, hazardous material incidents, and rescue operations. They serve as first responders and are trained in emergency medical services to provide life-saving care to the community.",
|
description: "products.catalog.lededescription",
|
||||||
},
|
image: "/images/products/category/lede.png",
|
||||||
{
|
},
|
||||||
id: "faq-4",
|
];
|
||||||
question: "What are the work hours for firefighters?",
|
|
||||||
answer:
|
export const slt = [
|
||||||
"Firefighters typically work shifts that vary by department. Many operate on a schedule of 24 hours on duty followed by 48-72 hours off duty. This schedule allows for adequate rest and recovery while ensuring continuous emergency response coverage for the community.",
|
{
|
||||||
},
|
id: 1,
|
||||||
{
|
slug: "slt_bir_qavatli_quvr",
|
||||||
id: "faq-5",
|
name: "Bir qavatli PP-R quvuri (SDR 6) SLT BLOCKFIRE",
|
||||||
question: "How long is firefighter training?",
|
description: "",
|
||||||
answer:
|
image: "/images/products/slt/slt_blackfirebittalik-removebg-preview.png",
|
||||||
"Initial firefighter training typically takes 12-18 weeks of full-time instruction at a fire academy. After this, firefighters continue receiving ongoing training throughout their careers. Our department invests heavily in continuous education to maintain the highest standards of service.",
|
},
|
||||||
},
|
{
|
||||||
{
|
id: 2,
|
||||||
id: "faq-6",
|
slug: "slt_yonginga_qarshi_45_burilish",
|
||||||
question: "What is required to apply for the firefighter position?",
|
name: "Yong‘inga qarshi polipropilenli 45° burilish",
|
||||||
answer:
|
description: "",
|
||||||
"Candidates must be at least 18 years old, have a high school diploma or GED, possess a valid drivers license, and pass a background check and medical examination. Physical fitness is essential, and candidates must pass the Candidate Physical Ability Test (CPAT).",
|
image: "/images/products/slt/slt_blackfireburilish-removebg-preview.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
slug: "slt_otish_nuftasi",
|
||||||
|
name: "SLT BLOCKFIRE PP-R o'tish muftasi BxH",
|
||||||
|
description: "",
|
||||||
|
image: "/images/products/slt/slt_blackfireperexodnaya-removebg-preview.png",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ca = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
slug: "takozli_eshik_klapan",
|
||||||
|
name: "Takozli eshik klapanlari",
|
||||||
|
description: "",
|
||||||
|
image: "/images/products/ca/zadviji-removebg-preview.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
slug: "takozli_eshik_klapan",
|
||||||
|
name: "Bosim Regulyatorlari",
|
||||||
|
description: "",
|
||||||
|
image: "/images/products/ca/regulyator-removebg-preview.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
slug: "vites_kapalak_klapan",
|
||||||
|
name: "Vites qutisi bilan kapalak klapanlar",
|
||||||
|
description: "",
|
||||||
|
image: "/images/products/ca/zatvori-removebg-preview.png",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const lede = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
slug: "tishli_tirsak",
|
||||||
|
name: "Tishli tirsak 3j",
|
||||||
|
description: "",
|
||||||
|
image: "/images/products/lede/atvot_rezbavoy-removebg-preview.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
slug: "yivli_flanes",
|
||||||
|
name: "PN16 321 bo'lingan yivli birlashma flanesi",
|
||||||
|
description: "",
|
||||||
|
image: "/images/products/lede/flanes_nakidnoy-removebg-preview.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
slug: "qisqartiruvchi_tee",
|
||||||
|
name: "130R o'yilgan qisqartiruvchi tee",
|
||||||
|
description: "",
|
||||||
|
image: "/images/products/lede/troynik_perexadnoy-removebg-preview.png",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const sectionData = [
|
||||||
|
"SLT-Aqua",
|
||||||
|
"Вварное седло",
|
||||||
|
"Кран шаровый",
|
||||||
|
"Муфты",
|
||||||
|
"Муфты комбинированные",
|
||||||
|
"Муфты переходные",
|
||||||
|
"Тройник комбинированный",
|
||||||
|
"Тройники",
|
||||||
|
"Трубы SDR 6",
|
||||||
|
"Трубы SDR 7,4",
|
||||||
|
"Угол 45",
|
||||||
|
"Угол 90",
|
||||||
|
"Угольник комбинированный",
|
||||||
|
"Фланцы+бурты",
|
||||||
|
];
|
||||||
|
|
||||||
|
const sectionNumber = [
|
||||||
|
"25",
|
||||||
|
'25х1/2"',
|
||||||
|
'25х3/4"',
|
||||||
|
"32",
|
||||||
|
"32x25x25",
|
||||||
|
"32x25x32",
|
||||||
|
'32х1"',
|
||||||
|
'32х1/2"',
|
||||||
|
"32х25",
|
||||||
|
'32х3/4"',
|
||||||
|
"40",
|
||||||
|
"40x25x40",
|
||||||
|
"40x32x40",
|
||||||
|
'40х1 1/4"',
|
||||||
|
'40х1 3/4"',
|
||||||
|
'40х1/2"',
|
||||||
|
"40х25",
|
||||||
|
"40х32",
|
||||||
|
"50",
|
||||||
|
"50x25x50",
|
||||||
|
"50x32x50",
|
||||||
|
"50x40x50",
|
||||||
|
'50х1 1/2"',
|
||||||
|
'50х1/2"',
|
||||||
|
"50х25",
|
||||||
|
"50х32",
|
||||||
|
"50х40",
|
||||||
|
"63",
|
||||||
|
"63x25x63",
|
||||||
|
"63x32x63",
|
||||||
|
"63x40x63",
|
||||||
|
"63x50x63",
|
||||||
|
'63х1/2"',
|
||||||
|
'63х2"',
|
||||||
|
"63х25",
|
||||||
|
"63х32",
|
||||||
|
"63х40",
|
||||||
|
"63х50",
|
||||||
|
"75",
|
||||||
|
"75x25x75",
|
||||||
|
"75x32x75",
|
||||||
|
"75x40x75",
|
||||||
|
"75x50x75",
|
||||||
|
"75x63x75",
|
||||||
|
'75х1/2"',
|
||||||
|
"75х32",
|
||||||
|
"75х40",
|
||||||
|
"75х50",
|
||||||
|
"75х63",
|
||||||
|
"90",
|
||||||
|
"90x40x90",
|
||||||
|
"90x50x90",
|
||||||
|
"90x63x90",
|
||||||
|
"90x75x90",
|
||||||
|
'90х1/2"',
|
||||||
|
"90х32",
|
||||||
|
"90х40",
|
||||||
|
"90х50",
|
||||||
|
"90х63",
|
||||||
|
"90х75",
|
||||||
|
"110",
|
||||||
|
"110x50x110",
|
||||||
|
"110x63x110",
|
||||||
|
"110x75x110",
|
||||||
|
"110x90x110",
|
||||||
|
'110х1/2"',
|
||||||
|
"110х25",
|
||||||
|
"110х32",
|
||||||
|
"110х40",
|
||||||
|
"110х50",
|
||||||
|
"110х63",
|
||||||
|
"110х75",
|
||||||
|
"110х90",
|
||||||
|
"125",
|
||||||
|
"160",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const result = [
|
||||||
|
{
|
||||||
|
type: "section",
|
||||||
|
items: sectionData.map((name, index) => ({
|
||||||
|
id: index + 1,
|
||||||
|
name,
|
||||||
|
type: "catalog",
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "size",
|
||||||
|
items: sectionNumber.map((name, index) => ({
|
||||||
|
id: index + 1,
|
||||||
|
name,
|
||||||
|
type: "size",
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const normativeData = [
|
||||||
|
{
|
||||||
|
title: "certs.slt_blockfire.title",
|
||||||
|
artikul: "SLT BLOCKFIRE",
|
||||||
|
features: [
|
||||||
|
"certs.slt_blockfire.doc1",
|
||||||
|
"certs.slt_blockfire.doc2",
|
||||||
|
"certs.slt_blockfire.doc3",
|
||||||
|
"certs.slt_blockfire.doc4",
|
||||||
|
"certs.slt_blockfire.doc5",
|
||||||
|
"certs.slt_blockfire.doc6",
|
||||||
|
"certs.slt_blockfire.doc7",
|
||||||
|
"certs.slt_blockfire.doc8",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "certs.slt_aqua.title",
|
||||||
|
artikul: "SLT AQUA",
|
||||||
|
features: ["certs.slt_aqua.doc1"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user