Compare commits
16 Commits
e71d774ccb
...
74f1d7a9fd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74f1d7a9fd | ||
|
|
dbe8399086 | ||
|
|
66bf104cb7 | ||
|
|
873bbb82a9 | ||
|
|
d4a242b169 | ||
|
|
e99df29b81 | ||
|
|
34cb524626 | ||
|
|
3cf5e0efcf | ||
|
|
d7e1990cc9 | ||
|
|
87f304225e | ||
|
|
3c862ea104 | ||
|
|
ca3e28779e | ||
|
|
63b363b142 | ||
|
|
96acd12d9c | ||
|
|
b1095f2c12 | ||
|
|
f439f9bbdf |
14
app/[locale]/catalog_page/page.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
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-20">
|
||||
<MainSubCategory />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,11 +25,13 @@ export default function SlugPage() {
|
||||
<SliderComp imgs={DATA[0].images} />
|
||||
|
||||
<RightSide
|
||||
id={1}
|
||||
title={DATA[0].title}
|
||||
name={DATA[0].name}
|
||||
statusColor={statusColor}
|
||||
statusText={statusText}
|
||||
description={DATA[0].description}
|
||||
image={DATA[0].images[0]}
|
||||
/>
|
||||
</div>
|
||||
<Features features={DATA[0].features} />
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { ProductBanner, Products } from "@/components/pages/products";
|
||||
import FilterCatalog from "@/components/pages/products/filter/catalog/filterCatalog";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="bg-[#1e1d1c] pb-30">
|
||||
<ProductBanner />
|
||||
{/* <FilterCatalog /> */}
|
||||
<Products />
|
||||
</div>
|
||||
);
|
||||
|
||||
13
app/[locale]/subCategory/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
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="py-20">
|
||||
<MainSubCategory />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-roboto: "Roboto", 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-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
@@ -135,4 +135,25 @@ body {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Analytics } from "@vercel/analytics/next";
|
||||
import "./globals.css";
|
||||
import { Footer, Navbar } from "@/components/layout";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages } from "next-intl/server";
|
||||
import BackAnimatsiya from "@/components/backAnimatsiya/backAnimatsiya";
|
||||
import { InitialLoading } from "@/components/initialLoading/initialLoading";
|
||||
import { Providers } from "@/components/provider";
|
||||
|
||||
("info@ignum-tech.com");
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -56,12 +57,9 @@ export default async function RootLayout({
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<InitialLoading />
|
||||
<NextIntlClientProvider messages={messages} locale={locale}>
|
||||
<BackAnimatsiya />
|
||||
<Navbar />
|
||||
{children}
|
||||
<Footer />
|
||||
<Analytics />
|
||||
<Providers>{children}</Providers>
|
||||
</NextIntlClientProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
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 { useEffect, useState } from "react";
|
||||
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() {
|
||||
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 (
|
||||
<div className="bg-[#1e1d1c] py-5">
|
||||
<Marquee>
|
||||
<div className="relative sm:w-125 w-70 sm:h-62.5 h-40 mx-2 overflow-hidden rounded-xl">
|
||||
<Image
|
||||
src="/images/img2.webp"
|
||||
alt="images"
|
||||
fill
|
||||
priority
|
||||
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/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>
|
||||
{marqImg.map((item) => (
|
||||
<div className="relative sm:w-125 w-70 sm:h-62.5 h-40 mx-2 overflow-hidden rounded-xl">
|
||||
<Image
|
||||
src={item}
|
||||
alt="images"
|
||||
fill
|
||||
priority
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Marquee>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import "./back.css";
|
||||
|
||||
export default function BackAnimatsiya() {
|
||||
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
|
||||
id="Layer_2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
117
components/initialLoading/initialLoading.css
Normal file
@@ -0,0 +1,117 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.initial-svg {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
animation: initialFloat 2s ease-in-out infinite, initialScale 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.initial-path {
|
||||
fill: url(#initial-gradient);
|
||||
filter: url(#initial-glow);
|
||||
stroke: #ffffff;
|
||||
stroke-width: 2;
|
||||
animation: pathPulse 1.5s 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: #ff0000;
|
||||
border-radius: 50%;
|
||||
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(-30px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes initialScale {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pathPulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
stroke-width: 2;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
stroke-width: 3;
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,11 @@ import { Mail, Phone, MapPin } from "lucide-react";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "react-toastify";
|
||||
import axios from "axios";
|
||||
import httpClient from "@/request/api";
|
||||
import { endPoints } from "@/request/links";
|
||||
|
||||
export function Footer() {
|
||||
const locale = useLocale();
|
||||
@@ -14,12 +19,25 @@ export function Footer() {
|
||||
const [email, setEmail] = useState("");
|
||||
const [subscribed, setSubscribed] = useState(false);
|
||||
|
||||
const handleSubscribe = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (email) {
|
||||
const formRequest = useMutation({
|
||||
mutationKey: [],
|
||||
mutationFn: (data: any) => httpClient.post(endPoints.post.sendNumber, data),
|
||||
onSuccess: () => {
|
||||
toast.success(t("succes"));
|
||||
setSubscribed(true);
|
||||
setEmail("");
|
||||
setTimeout(() => setSubscribed(false), 3000);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log("error: ", error);
|
||||
toast.error(t("error"));
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubscribe = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (email) {
|
||||
formRequest.mutate({ number: email });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -49,9 +67,11 @@ export function Footer() {
|
||||
className="flex sm:flex-row flex-col w-full gap-2 md:w-auto"
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
type="text"
|
||||
placeholder={t("enterPhone")}
|
||||
value={email}
|
||||
minLength={9}
|
||||
maxLength={13}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="font-almarai flex-1 rounded-full bg-white px-6 py-3 text-gray-800 placeholder-gray-400 focus:outline-none md:w-64"
|
||||
required
|
||||
@@ -162,12 +182,14 @@ export function Footer() {
|
||||
href="mailto:support@fireforce.com"
|
||||
className="hover:text-[#fa1d1d]"
|
||||
>
|
||||
support@fireforce.com
|
||||
info@ignum-tech.com
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex items-start gap-3">
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -179,9 +201,7 @@ export function Footer() {
|
||||
<div className="border-t border-gray-800 px-4 py-8">
|
||||
<div className="mx-auto max-w-6xl">
|
||||
<div className="font-almarai flex flex-col justify-between gap-4 text-sm text-gray-400 md:flex-row md:items-center">
|
||||
<div>
|
||||
Copyright © 2025 Ignum Company.
|
||||
</div>
|
||||
<div>Copyright © 2025 Ignum Company.</div>
|
||||
<div className="flex gap-6">
|
||||
<a href="#terms" className="hover:text-white">
|
||||
Terms & Conditions
|
||||
|
||||
@@ -42,7 +42,7 @@ export function Navbar() {
|
||||
<Link href={`/${locale}/home`} className="hover:cursor-pointer">
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
</Link>
|
||||
@@ -62,53 +62,21 @@ export function Navbar() {
|
||||
{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}/faq`}
|
||||
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
||||
>
|
||||
{t("navbar.faq")}
|
||||
</Link>
|
||||
<Link
|
||||
href={`/${locale}/services`}
|
||||
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
||||
>
|
||||
{t("navbar.services")}
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href={`/${locale}/products`}
|
||||
href={`/${locale}/catalog_page`}
|
||||
className="font-unbounded uppercase text-white text-sm h-full flex items-center font-semibold hover:cursor-pointer hover:text-red-500 transition"
|
||||
>
|
||||
{t("navbar.products")}
|
||||
@@ -210,39 +178,23 @@ export function Navbar() {
|
||||
</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>
|
||||
<Link
|
||||
href={`/${locale}/faq`}
|
||||
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
{t("navbar.faq")}
|
||||
</Link>
|
||||
<Link
|
||||
href={`/${locale}/services`}
|
||||
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
{t("navbar.services")}
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href={`/${locale}/products`}
|
||||
href={`/${locale}/catalog_page`}
|
||||
className="font-unbounded uppercase text-white text-base font-semibold hover:text-red-500 transition py-2"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
>
|
||||
|
||||
@@ -2,14 +2,14 @@ import "./logo.css";
|
||||
|
||||
export default function NavbarLogo() {
|
||||
return (
|
||||
<div className="relative w-24 h-12">
|
||||
<div className="relative w-24 h-20">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 423.22 424.82"
|
||||
className="w-full h-full"
|
||||
>
|
||||
<defs>
|
||||
{/* Qizil neon gradient */}
|
||||
{/* 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" />
|
||||
|
||||
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 "./page-transition.css";
|
||||
|
||||
export default function PageTransition({ children }: { children: React.ReactNode }) {
|
||||
export default function PageTransition({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const [displayPath, setDisplayPath] = useState(pathname);
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsTransitioning(true);
|
||||
if (pathname !== displayPath) {
|
||||
// Start exit animation
|
||||
setIsTransitioning(true);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
setIsTransitioning(false);
|
||||
}, 1500); // Animatsiya davomiyligi
|
||||
const exitTimer = setTimeout(() => {
|
||||
// Update path (triggers content change)
|
||||
setDisplayPath(pathname);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [pathname]);
|
||||
// Start enter animation
|
||||
const enterTimer = setTimeout(() => {
|
||||
setIsTransitioning(false);
|
||||
}, 800); // Enter animation duration
|
||||
|
||||
return () => clearTimeout(enterTimer);
|
||||
}, 800); // Exit animation duration
|
||||
|
||||
return () => clearTimeout(exitTimer);
|
||||
}
|
||||
}, [pathname, displayPath]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -32,7 +48,6 @@ export default function PageTransition({ children }: { children: React.ReactNode
|
||||
<stop offset="50%" stopColor="#ff4444" />
|
||||
<stop offset="100%" stopColor="#ff0000" />
|
||||
</linearGradient>
|
||||
|
||||
<filter id="transition-glow">
|
||||
<feGaussianBlur stdDeviation="5" result="coloredBlur"/>
|
||||
<feMerge>
|
||||
|
||||
@@ -6,12 +6,7 @@ export default function ContactHeader() {
|
||||
const t = useTranslations();
|
||||
return (
|
||||
<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
|
||||
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"
|
||||
|
||||
@@ -3,21 +3,26 @@
|
||||
import { Check } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
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 {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
subject: string;
|
||||
name: string;
|
||||
surname: string;
|
||||
address: string;
|
||||
theme: string;
|
||||
message: string;
|
||||
agreeToPolicy: boolean;
|
||||
}
|
||||
|
||||
interface FormErrors {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
subject?: string;
|
||||
name?: string;
|
||||
surname?: string;
|
||||
address?: string;
|
||||
theme?: string;
|
||||
message?: string;
|
||||
agreeToPolicy?: string;
|
||||
}
|
||||
@@ -25,10 +30,10 @@ interface FormErrors {
|
||||
export default function Form() {
|
||||
const t = useTranslations();
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
email: "",
|
||||
subject: "",
|
||||
name: "",
|
||||
surname: "",
|
||||
address: "",
|
||||
theme: "",
|
||||
message: "",
|
||||
agreeToPolicy: false,
|
||||
});
|
||||
@@ -38,22 +43,48 @@ export default function Form() {
|
||||
"idle" | "success" | "error"
|
||||
>("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 newErrors: FormErrors = {};
|
||||
|
||||
if (!formData.firstName.trim()) {
|
||||
newErrors.firstName = "First name is required";
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = "First name is required";
|
||||
}
|
||||
if (!formData.lastName.trim()) {
|
||||
newErrors.lastName = "Last name is required";
|
||||
if (!formData.surname.trim()) {
|
||||
newErrors.surname = "Last name is required";
|
||||
}
|
||||
if (!formData.email.trim()) {
|
||||
newErrors.email = "Email is required";
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
newErrors.email = "Please enter a valid email";
|
||||
if (!formData.address.trim()) {
|
||||
newErrors.address = "address is required";
|
||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.address)) {
|
||||
newErrors.address = "Please enter a valid address";
|
||||
}
|
||||
if (!formData.subject.trim()) {
|
||||
newErrors.subject = "Subject is required";
|
||||
if (!formData.theme.trim()) {
|
||||
newErrors.theme = "theme is required";
|
||||
}
|
||||
if (!formData.message.trim()) {
|
||||
newErrors.message = "Message is required";
|
||||
@@ -90,32 +121,7 @@ export default function Form() {
|
||||
|
||||
setIsSubmitting(true);
|
||||
setSubmitStatus("idle");
|
||||
|
||||
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);
|
||||
}
|
||||
formRequest.mutate(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -125,66 +131,74 @@ export default function Form() {
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="firstName"
|
||||
name="name"
|
||||
placeholder={t("contact.form.placeholders.firstName")}
|
||||
value={formData.firstName}
|
||||
value={formData.name}
|
||||
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 ${
|
||||
errors.firstName ? "border-red-500" : "border-transparent"
|
||||
}`}
|
||||
errors.name ? "border-red-500" : "border-transparent"
|
||||
}`}
|
||||
/>
|
||||
{errors.firstName && (
|
||||
<p className="font-almarai mt-1 text-xs text-red-500">{errors.firstName}</p>
|
||||
{errors.name && (
|
||||
<p className="font-almarai mt-1 text-xs text-red-500">
|
||||
{errors.name}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="lastName"
|
||||
name="surname"
|
||||
placeholder={t("contact.form.placeholders.lastName")}
|
||||
value={formData.lastName}
|
||||
value={formData.surname}
|
||||
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 ${
|
||||
errors.lastName ? "border-red-500" : "border-transparent"
|
||||
errors.surname ? "border-red-500" : "border-transparent"
|
||||
}`}
|
||||
/>
|
||||
{errors.lastName && (
|
||||
<p className="font-almarai mt-1 text-xs text-red-500">{errors.lastName}</p>
|
||||
{errors.surname && (
|
||||
<p className="font-almarai mt-1 text-xs text-red-500">
|
||||
{errors.surname}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Second Row - Email & Subject */}
|
||||
{/* Second Row - address & theme */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
type="address"
|
||||
name="address"
|
||||
placeholder={t("contact.form.placeholders.email")}
|
||||
value={formData.email}
|
||||
value={formData.address}
|
||||
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 ${
|
||||
errors.email ? "border-red-500" : "border-transparent"
|
||||
errors.address ? "border-red-500" : "border-transparent"
|
||||
}`}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="font-almarai mt-1 text-xs text-red-500">{errors.email}</p>
|
||||
{errors.address && (
|
||||
<p className="font-almarai mt-1 text-xs text-red-500">
|
||||
{errors.address}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
name="subject"
|
||||
name="theme"
|
||||
placeholder={t("contact.form.placeholders.subject")}
|
||||
value={formData.subject}
|
||||
value={formData.theme}
|
||||
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 ${
|
||||
errors.subject ? "border-red-500" : "border-transparent"
|
||||
errors.theme ? "border-red-500" : "border-transparent"
|
||||
}`}
|
||||
/>
|
||||
{errors.subject && (
|
||||
<p className="font-almarai mt-1 text-xs text-red-500">{errors.subject}</p>
|
||||
{errors.theme && (
|
||||
<p className="font-almarai mt-1 text-xs text-red-500">
|
||||
{errors.theme}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -202,7 +216,9 @@ export default function Form() {
|
||||
}`}
|
||||
/>
|
||||
{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>
|
||||
|
||||
@@ -236,7 +252,9 @@ export default function Form() {
|
||||
</button>
|
||||
</div>
|
||||
{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 */}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
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 Form from "./form";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Maps from "./maps";
|
||||
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
||||
|
||||
export function Contact() {
|
||||
const t = useTranslations();
|
||||
const contactInfo = [
|
||||
{
|
||||
icon: Mail,
|
||||
title:t("contact.form.email"),
|
||||
title: t("contact.form.email"),
|
||||
detail: t("contact.form.emailAddress"),
|
||||
},
|
||||
{
|
||||
@@ -25,7 +27,7 @@ export function Contact() {
|
||||
];
|
||||
|
||||
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 */}
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
@@ -46,27 +48,40 @@ export function Contact() {
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
||||
<ContactHeader />
|
||||
<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 />
|
||||
|
||||
<Form />
|
||||
<Form />
|
||||
|
||||
{/* Contact Info */}
|
||||
<div className="mt-12 grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||
{contactInfo.map((info) => (
|
||||
<div
|
||||
key={info.title}
|
||||
className="flex flex-col items-center text-center"
|
||||
>
|
||||
<div className="mb-3 flex h-14 w-14 items-center justify-center rounded-xl bg-[#2c2b2a]">
|
||||
<info.icon className="h-6 w-6 text-red-600" />
|
||||
{/* Contact Info */}
|
||||
<div className="mt-12 grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||
{contactInfo.map((info) => (
|
||||
<div
|
||||
key={info.title}
|
||||
className="flex flex-col items-center text-center"
|
||||
>
|
||||
<div className="mb-3 flex h-14 w-14 items-center justify-center rounded-xl bg-[#2c2b2a]">
|
||||
<info.icon className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<h3 className="font-almarai text-sm font-bold tracking-wider text-white">
|
||||
{info.title}
|
||||
</h3>
|
||||
<p className="font-almarai mt-1 text-sm text-gray-400">
|
||||
{info.detail}
|
||||
</p>
|
||||
</div>
|
||||
<h3 className="font-almarai text-sm font-bold tracking-wider text-white">
|
||||
{info.title}
|
||||
</h3>
|
||||
<p className="font-almarai mt-1 text-sm text-gray-400">{info.detail}</p>
|
||||
</div>
|
||||
))}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
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 FAQAccordion from "./faqAccardion";
|
||||
import { faqItems } from "@/lib/demoData";
|
||||
import FAQAccordion, { FAQItem } from "./faqAccardion";
|
||||
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() {
|
||||
const t = useTranslations();
|
||||
const faqItems = [
|
||||
{
|
||||
id: "faq-1",
|
||||
question: t("faq.question1.question"),
|
||||
answer: t("faq.question1.answer"),
|
||||
},
|
||||
{
|
||||
id: "faq-2",
|
||||
question: t("faq.question2.question"),
|
||||
answer: t("faq.question2.answer"),
|
||||
},
|
||||
{
|
||||
id: "faq-3",
|
||||
question: t("faq.question3.question"),
|
||||
answer: t("faq.question3.answer"),
|
||||
},
|
||||
{
|
||||
id: "faq-4",
|
||||
question: t("faq.question4.question"),
|
||||
answer: t("faq.question4.answer"),
|
||||
},
|
||||
{
|
||||
id: "faq-5",
|
||||
question: t("faq.question5.question"),
|
||||
answer: t("faq.question5.answer"),
|
||||
},
|
||||
];
|
||||
const faqItems: FAQItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
question: t("faq.question1.question"),
|
||||
answer: t("faq.question1.answer"),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
question: t("faq.question2.question"),
|
||||
answer: t("faq.question2.answer"),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
question: t("faq.question3.question"),
|
||||
answer: t("faq.question3.answer"),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
question: t("faq.question4.question"),
|
||||
answer: t("faq.question4.answer"),
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
question: t("faq.question5.question"),
|
||||
answer: t("faq.question5.answer"),
|
||||
},
|
||||
];
|
||||
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 (
|
||||
<div className="min-h-screen bg-[#1e1d1c]">
|
||||
<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 */}
|
||||
<div className="max-w-250 w-full">
|
||||
<FAQAccordion items={faqItems} />
|
||||
<FAQAccordion items={faq} />
|
||||
</div>
|
||||
</div>
|
||||
{/* ASK QUESTION */}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client';
|
||||
"use client";
|
||||
|
||||
import * as Accordion from '@radix-ui/react-accordion';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import * as Accordion from "@radix-ui/react-accordion";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
interface FAQItem {
|
||||
id: string;
|
||||
export interface FAQItem {
|
||||
id: number;
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
@@ -17,7 +17,11 @@ export default function FAQAccordion({ items }: FAQAccordionProps) {
|
||||
return (
|
||||
<Accordion.Root type="single" collapsible className="w-full">
|
||||
{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">
|
||||
<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}
|
||||
@@ -31,7 +35,9 @@ export default function FAQAccordion({ items }: FAQAccordionProps) {
|
||||
</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">
|
||||
<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.Item>
|
||||
))}
|
||||
|
||||
@@ -59,10 +59,10 @@ export function Banner() {
|
||||
{/* Left side - Firefighters Image */}
|
||||
<div className="flex items-end justify-center ">
|
||||
<img
|
||||
src="/images/homeBanner.png"
|
||||
src="/images/homeBanner3.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"
|
||||
className="lg:w-150 w-100 lg:h-150 max-[300px]:w-[80vw] object-contain object-right rounded-xl drop-shadow-2xl"
|
||||
/>
|
||||
</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
@@ -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>
|
||||
);
|
||||
}
|
||||
58
components/pages/home/blog/catalog.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import httpClient from "@/request/api";
|
||||
import { endPoints } from "@/request/links";
|
||||
import { useEffect } from "react";
|
||||
import CatalogCard from "../../products/catalog";
|
||||
import CatalogCardSkeleton from "@/components/loadingSkleton";
|
||||
import EmptyData from "@/components/EmptyData";
|
||||
import { getRouteLang } from "@/request/getLang";
|
||||
import { CategoryType } from "@/lib/types";
|
||||
|
||||
export default function Catalog() {
|
||||
const language = getRouteLang();
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["category", language],
|
||||
queryFn: () => httpClient(endPoints.category.all),
|
||||
select: (data): CategoryType[] => data?.data?.results,
|
||||
});
|
||||
useEffect(() => {
|
||||
console.log("product catalog data: ", data);
|
||||
}, [data]);
|
||||
|
||||
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="Katalog topilmadi"
|
||||
description="Hozircha kategoriyalar mavjud emas. Keyinroq qaytib keling."
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -5,4 +5,4 @@ export { Video } from "./video";
|
||||
export { OurService } from "./ourService";
|
||||
export { Testimonial } from "./testimonal";
|
||||
export { Line } from "./line";
|
||||
export { Blog } from "./blog";
|
||||
export { Blog } from "./blog/blog";
|
||||
|
||||
@@ -1,36 +1,63 @@
|
||||
"use client";
|
||||
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 { useEffect, useState } from "react";
|
||||
|
||||
interface Statistics {
|
||||
id: number;
|
||||
number: number;
|
||||
hint: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export function Statistics() {
|
||||
const t = useTranslations();
|
||||
const stats = [
|
||||
{
|
||||
number: "25",
|
||||
symbol: "+",
|
||||
label: t("home.statistics.experience"),
|
||||
id: 1,
|
||||
number: 25,
|
||||
hint: "+",
|
||||
description: t("home.statistics.experience"),
|
||||
},
|
||||
{
|
||||
number: "450",
|
||||
symbol: "+",
|
||||
label: t("home.statistics.projectsCompleted"),
|
||||
id: 2,
|
||||
number: 450,
|
||||
hint: "+",
|
||||
description: t("home.statistics.projectsCompleted"),
|
||||
},
|
||||
{
|
||||
number: "99",
|
||||
symbol: "+",
|
||||
label: t("home.statistics.trainedSpecialists"),
|
||||
id: 3,
|
||||
number: 99,
|
||||
hint: "+",
|
||||
description: t("home.statistics.trainedSpecialists"),
|
||||
},
|
||||
{
|
||||
number: "93",
|
||||
symbol: "%",
|
||||
label: t("home.statistics.trustedClients"),
|
||||
id: 4,
|
||||
number: 93,
|
||||
hint: "%",
|
||||
description: t("home.statistics.trustedClients"),
|
||||
},
|
||||
];
|
||||
const [stat, setStat] = useState<Statistics[]>(stats);
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["statistics"],
|
||||
queryFn: () => httpClient(endPoints.statistics),
|
||||
select: (data) => data?.data?.results,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
data && setStat(data);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<section className="w-full bg-black">
|
||||
<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">
|
||||
{stats.map((stat, index) => (
|
||||
{stat.map((stat, index) => (
|
||||
<div
|
||||
key={index}
|
||||
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)} />
|
||||
</span>
|
||||
<span className="text-4xl sm:text-5xl lg:text-6xl font-bold text-red-600">
|
||||
{stat.symbol}
|
||||
{stat.hint}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Label */}
|
||||
<p className="font-almarai text-sm sm:text-base text-gray-300 mt-4 text-center font-medium">
|
||||
{stat.label}
|
||||
{stat.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTranslations } from "next-intl";
|
||||
export function ProductBanner() {
|
||||
const t = useTranslations();
|
||||
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 */}
|
||||
<div
|
||||
className="absolute inset-0 z-0"
|
||||
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 "@/store/useCategory";
|
||||
import { useSubCategory } from "@/store/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?category=${id}`
|
||||
: `/${locale}/products?category=${id}`;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={navigateLink}
|
||||
onClick={updateZustands}
|
||||
className="group relative h-112.5 w-full overflow-hidden rounded-2xl bg-[#17161679] from-[#444242] to-black border hover:border-red-700 border-white/10 transition-all duration-500 hover:-translate-y-1"
|
||||
>
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
96
components/pages/products/filter/catalog/filterCatalog.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
import EmptyData from "@/components/EmptyData";
|
||||
import { CategoryType } from "@/lib/types";
|
||||
import httpClient from "@/request/api";
|
||||
import { getRouteLang } from "@/request/getLang";
|
||||
import { endPoints } from "@/request/links";
|
||||
import { useCategory } from "@/store/useCategory";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useEffect } from "react";
|
||||
import CatalogCardSkeletonSmall from "./loading";
|
||||
import Image from "next/image";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
|
||||
export default function FilterCatalog() {
|
||||
const language = getRouteLang();
|
||||
const setCategory = useCategory((state) => state.setCategory);
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ["category", language],
|
||||
queryFn: () => httpClient(endPoints.category.all),
|
||||
select: (data): CategoryType[] => data?.data?.results,
|
||||
});
|
||||
useEffect(() => {
|
||||
console.log("product catalog data: ", data);
|
||||
}, [data]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<CatalogCardSkeletonSmall key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Ma'lumot yo'q holati
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<EmptyData
|
||||
title="Katalog topilmadi"
|
||||
description="Hozircha kategoriyalar mavjud emas. Keyinroq qaytib keling."
|
||||
icon="shopping"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-200 w-full mx-auto space-x-5 px-5 flex items-center justify-around my-10 -mt-30 pb-5 relative z-20 sm:overflow-x-hidden overflow-x-scroll">
|
||||
{data?.map((item) => (
|
||||
<div
|
||||
onClick={() => setCategory(item)}
|
||||
className="shrink-0 group relative w-55 h-60 overflow-hidden rounded-2xl bg-[#17161679] border border-white/10 transition-all duration-500 hover:-translate-y-1 hover:border-red-700 cursor-pointer"
|
||||
>
|
||||
{/* Background glow effect */}
|
||||
<div className="absolute inset-0 bg-linear-to-t from-red-600/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
|
||||
{/* Decorative corner accent */}
|
||||
<div className="absolute top-0 right-0 w-16 h-16 bg-linear-to-br from-red-500/20 to-transparent rounded-bl-full opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
|
||||
{/* Content container */}
|
||||
<div className="relative h-full flex flex-col p-4">
|
||||
{/* Title section */}
|
||||
<div className="mb-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h3 className="text-lg font-unbounded font-bold text-white leading-tight transition-colors duration-300">
|
||||
{item.name}
|
||||
</h3>
|
||||
|
||||
<div className="shrink-0 w-6 h-6 rounded-full bg-white/10 flex items-center justify-center group-hover:bg-red-500 transition-all duration-300 group-hover:scale-110">
|
||||
<ArrowUpRight
|
||||
className="w-3.5 h-3.5 text-white"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image container */}
|
||||
<div className="relative flex-1 rounded-xl overflow-hidden bg-linear-to-br from-[#444242] to-gray-900/50 border border-white/5 group-hover:border-white/20 transition-all duration-500">
|
||||
<div className="absolute inset-0 bg-linear-to-t from-black/60 via-transparent to-transparent z-10" />
|
||||
|
||||
<div className="relative w-full h-full">
|
||||
<Image
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
fill
|
||||
className="object-contain p-3 transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
components/pages/products/filter/catalog/loading.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
// components/CatalogCardSkeletonSmall.tsx
|
||||
export default function CatalogCardSkeletonSmall() {
|
||||
return (
|
||||
<div className="relative w-50 h-87.5 overflow-hidden rounded-2xl bg-[#17161679] border border-white/10 animate-pulse">
|
||||
<div className="flex flex-col h-full p-4 gap-3">
|
||||
|
||||
{/* Title skeleton */}
|
||||
<div className="space-y-2">
|
||||
<div className="h-5 bg-white/10 rounded-md w-3/4" />
|
||||
<div className="h-5 bg-white/10 rounded-md w-1/2" />
|
||||
</div>
|
||||
|
||||
{/* Image skeleton */}
|
||||
<div className="flex-1 rounded-xl bg-linear-to-br from-[#444242] to-gray-900/50 border border-white/5 flex items-center justify-center">
|
||||
<div className="w-20 h-20 bg-white/5 rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shimmer */}
|
||||
<div className="absolute inset-0 -translate-x-full animate-shimmer bg-linear-to-r from-transparent via-white/5 to-transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
151
components/pages/products/filter/filter.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
import { result } from "@/lib/demoData";
|
||||
import { useFilter } from "@/lib/filter-zustand";
|
||||
import httpClient from "@/request/api";
|
||||
import { endPoints } from "@/request/links";
|
||||
import { useCategory } from "@/store/useCategory";
|
||||
import { useSubCategory } from "@/store/useSubCategory";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Check } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export default function Filter() {
|
||||
const filter = useFilter((state) => state.filter);
|
||||
const toggleFilter = useFilter((state) => state.toggleFilter);
|
||||
const hasData = useFilter((state) => state.hasFilter);
|
||||
const category = useCategory((state) => state.category);
|
||||
const subCategory = useSubCategory((state) => state.subCategory);
|
||||
|
||||
const [dataExpanded, setDataExpanded] = useState<boolean>(false);
|
||||
const [numberExpanded, setNumberExpanded] = useState<boolean>(false);
|
||||
|
||||
const [catalogData, setCatalogData] = useState<
|
||||
{ id: number; name: string; type: string }[]
|
||||
>(result[0].items);
|
||||
const [sizeData, setSizeData] = useState<
|
||||
{ id: number; name: string; type: string }[]
|
||||
>(result[1].items);
|
||||
|
||||
const { data: catalog } = useQuery({
|
||||
queryKey: ["catalog"],
|
||||
queryFn: () => httpClient(endPoints.filter.catalogCategoryId(category.id)),
|
||||
select: (data) => {
|
||||
const catalogData = data?.data?.results;
|
||||
return catalogData.map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
type: "catalog",
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
const { data: size } = useQuery({
|
||||
queryKey: ["size"],
|
||||
queryFn: () => httpClient(endPoints.filter.sizeCategoryId(category.id)),
|
||||
select: (data) => {
|
||||
const sizedata = data?.data?.results;
|
||||
return sizedata.map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
type: "size",
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
catalog && setCatalogData(catalog);
|
||||
size && setSizeData(size);
|
||||
console.log("catalog: ", catalog, "size: ", size);
|
||||
}, [size, catalog]);
|
||||
|
||||
// Bo'lim uchun ko'rsatiladigan itemlar
|
||||
const visibleSectionData = dataExpanded
|
||||
? catalogData
|
||||
: catalogData.slice(0, 5);
|
||||
|
||||
// O'lcham uchun ko'rsatiladigan itemlar
|
||||
const visibleSectionNumber = numberExpanded
|
||||
? sizeData
|
||||
: sizeData.slice(0, 10);
|
||||
console.log("filter: ", filter);
|
||||
|
||||
return (
|
||||
<div className="space-y-3 lg:max-w-70 lg:px-0 px-3 w-full text-white">
|
||||
{/* Bo'lim filtri */}
|
||||
{visibleSectionData && (
|
||||
<div className="bg-gray-500 rounded-lg w-full">
|
||||
<p className="bg-red-500 text-white p-2 font-semibold font-almarai text-lg rounded-t-lg">
|
||||
Bo'lim
|
||||
</p>
|
||||
<div className="lg:space-y-3 space-x-6 lg:p-2 p-5 flex lg:flex-col overflow-x-auto lg:overflow-x-hidden items-start justify-start w-full">
|
||||
{visibleSectionData.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => toggleFilter(item)}
|
||||
className="hover:cursor-pointer flex items-center gap-2 w-auto shrink-0"
|
||||
>
|
||||
<span
|
||||
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition ${
|
||||
hasData(item.name)
|
||||
? "border-red-600 bg-red-600"
|
||||
: "border-gray-400 bg-transparent"
|
||||
}`}
|
||||
aria-label="Filter checkbox"
|
||||
>
|
||||
{hasData(item.name) && (
|
||||
<Check className="h-3 w-3 text-white" strokeWidth={3} />
|
||||
)}
|
||||
</span>
|
||||
<p className="whitespace-nowrap">{item.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
className="lg:flex hidden p-2 text-lg underline hover:text-red-300 transition"
|
||||
onClick={() => setDataExpanded(!dataExpanded)}
|
||||
>
|
||||
{dataExpanded ? "Yashirish" : "Ko'proq ko'rish"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* O'lcham filtri */}
|
||||
{visibleSectionNumber && (
|
||||
<div className="bg-gray-500 rounded-lg">
|
||||
<p className="bg-red-500 text-white p-2 font-semibold font-almarai text-lg rounded-t-lg">
|
||||
O'lcham
|
||||
</p>
|
||||
<div className="lg:space-y-3 space-x-6 lg:p-2 p-5 flex lg:flex-col overflow-x-auto lg:overflow-x-hidden items-start justify-start w-full">
|
||||
{visibleSectionNumber.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => toggleFilter(item)}
|
||||
className="hover:cursor-pointer flex items-center gap-2 w-auto shrink-0"
|
||||
>
|
||||
<span
|
||||
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition ${
|
||||
hasData(item.name)
|
||||
? "border-red-600 bg-red-600"
|
||||
: "border-gray-400 bg-transparent"
|
||||
}`}
|
||||
aria-label="Filter checkbox"
|
||||
>
|
||||
{hasData(item.name) && (
|
||||
<Check className="h-3 w-3 text-white" strokeWidth={3} />
|
||||
)}
|
||||
</span>
|
||||
<p>{item.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setNumberExpanded(!numberExpanded)}
|
||||
className="lg:flex hidden p-2 text-lg underline hover:text-red-300 transition"
|
||||
>
|
||||
{numberExpanded ? "Yashirish" : "Ko'proq ko'rish"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
components/pages/products/filter/filterInfo.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
"use client";
|
||||
|
||||
import { useFilter } from "@/lib/filter-zustand";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
export default function FilterInfo() {
|
||||
const filtered = useFilter((state) => state.filter);
|
||||
const resetFilter = useFilter((state) => state.resetFilter);
|
||||
const togleFilter = useFilter((state) => state.toggleFilter);
|
||||
if (filtered.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="fixed bottom-13 left-5 z-10 bg-gray-500 p-3 rounded-lg space-y-3 max-w-70 w-full">
|
||||
<p className="text-white ">Found: 20</p>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{filtered &&
|
||||
filtered.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center gap-1 p-1 rounded-lg bg-gray-700 text-white text-sm "
|
||||
>
|
||||
<button onClick={() => togleFilter(item)}>
|
||||
<X size={16} />
|
||||
</button>
|
||||
{item.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button onClick={resetFilter} className="text-white underline ">
|
||||
Clear all
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export { ProductBanner } from "./productBanner";
|
||||
export { Products } from "./products";
|
||||
export { ProductBanner } from "./banner";
|
||||
export { Products } from "./product/products";
|
||||
export { SliderComp } from "./slug/slider";
|
||||
export { RightSide } from "./slug/rightSide";
|
||||
export { Features } from "./slug/features";
|
||||
|
||||
84
components/pages/products/product/mianProduct.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
import httpClient from "@/request/api";
|
||||
import { endPoints } from "@/request/links";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import ProductCard from "./productCard";
|
||||
import { useCategory } from "@/store/useCategory";
|
||||
import { useFilter } from "@/lib/filter-zustand";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export default function MainProduct() {
|
||||
const category = useCategory((state) => state.category);
|
||||
const filter = useFilter((state) => state.filter);
|
||||
const getFiltersByType = useFilter((state)=>state.getFiltersByType)
|
||||
|
||||
// Query params yaratish
|
||||
const queryParams = useMemo(() => {
|
||||
const catalog = getFiltersByType("catalog");
|
||||
const size = getFiltersByType("size");
|
||||
|
||||
// Har bir filter uchun query string yaratish
|
||||
const catalogParams = catalog.map((item) => `catalog=${item.id}`).join("&");
|
||||
const sizeParams = size.map((item) => `size=${item.id}`).join("&");
|
||||
|
||||
// Barcha paramslarni birlashtirish
|
||||
const allParams = [catalogParams, sizeParams].filter(Boolean).join("&");
|
||||
|
||||
return allParams ? `&${allParams}` : "";
|
||||
}, [filter, getFiltersByType]);
|
||||
|
||||
// Request link yaratish
|
||||
const requestLink = useMemo(() => {
|
||||
const baseLink = category.have_sub_category
|
||||
? endPoints.subCategory.byId(category.id)
|
||||
: endPoints.product.byCategory(category.id || 0);
|
||||
|
||||
// Query params qo'shish
|
||||
return `${baseLink}${queryParams}`;
|
||||
}, [category.id, category.have_sub_category, queryParams]);
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["products", category.id , queryParams],
|
||||
queryFn: () => httpClient(requestLink),
|
||||
select: (data) => 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">
|
||||
Ma'lumotlarni yuklashda xatolik yuz berdi
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<div className="text-center text-gray-400 py-10">
|
||||
Mahsulotlar topilmadi
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||
{data.map((item: any) => (
|
||||
<ProductCard
|
||||
key={item.id} // ✅ index o'rniga id ishlatish
|
||||
title={item.name}
|
||||
image={item.image}
|
||||
slug={item.slug}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
42
components/pages/products/product/productCard.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
import { useLocale } from "next-intl";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
interface ProductCardProps {
|
||||
title: string;
|
||||
image: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export default function ProductCard({
|
||||
title,
|
||||
image,
|
||||
slug,
|
||||
}: ProductCardProps) {
|
||||
const locale = useLocale();
|
||||
|
||||
return (
|
||||
<Link href={`/${locale}/products/${slug}`}>
|
||||
<article className="group transition-all duration-300 hover:cursor-pointer max-sm:max-w-100 max-sm:mx-auto max-sm:w-full">
|
||||
{/* 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 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>
|
||||
</div>
|
||||
</article>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
21
components/pages/products/product/products.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import Filter from "../filter/filter";
|
||||
import FilterInfo from "../filter/filterInfo";
|
||||
import MainProduct from "./mianProduct";
|
||||
|
||||
export function Products() {
|
||||
return (
|
||||
<div className="bg-[#1e1d1c] py-10">
|
||||
<div className="max-w-300 mx-auto w-full z-20 relative">
|
||||
<div className="flex lg:flex-row flex-col lg:items-start items-center gap-5">
|
||||
{/* filter part */}
|
||||
<Filter />
|
||||
|
||||
{/* main products */}
|
||||
<MainProduct />
|
||||
|
||||
<FilterInfo />
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
import { usePriceModalStore } from "@/store/useProceModalStore";
|
||||
import { Facebook } from "lucide-react";
|
||||
|
||||
const socialLinks = [
|
||||
@@ -11,11 +11,13 @@ const socialLinks = [
|
||||
];
|
||||
|
||||
interface RightSideProps {
|
||||
id: number;
|
||||
title: string;
|
||||
name: string;
|
||||
description: string;
|
||||
statusText: string;
|
||||
statusColor: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export function RightSide({
|
||||
@@ -24,7 +26,18 @@ export function RightSide({
|
||||
description,
|
||||
statusColor,
|
||||
statusText,
|
||||
id,
|
||||
image,
|
||||
}: RightSideProps) {
|
||||
const openModal = usePriceModalStore((state) => state.openModal);
|
||||
const handleGetPrice = () => {
|
||||
openModal({
|
||||
id: id,
|
||||
name: title,
|
||||
image: image,
|
||||
inStock: true,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col justify-center">
|
||||
{/* Title */}
|
||||
@@ -71,7 +84,7 @@ export function RightSide({
|
||||
Narxni bilish
|
||||
</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"
|
||||
>
|
||||
Xabar yuborish
|
||||
|
||||
56
components/pages/subCategory/body.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import httpClient from "@/request/api";
|
||||
import { endPoints } from "@/request/links";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useCategory } from "@/store/useCategory";
|
||||
import Card from "./card";
|
||||
|
||||
export function MainSubCategory() {
|
||||
const category = useCategory((state) => state.category);
|
||||
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">
|
||||
Ma'lumotlarni yuklashda xatolik yuz berdi
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<div className="text-center text-gray-400 py-10">
|
||||
Mahsulotlar topilmadi
|
||||
</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} // ✅ index o'rniga id ishlatish
|
||||
title={item.name}
|
||||
image={item.image}
|
||||
slug={item.slug}
|
||||
id={item.id}
|
||||
category={item.category}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
components/pages/subCategory/card.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { useSubCategory } from "@/store/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}/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 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>
|
||||
</div>
|
||||
</article>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
1
components/pages/subCategory/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { MainSubCategory } from "./body";
|
||||
276
components/priceContact.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import Image from "next/image";
|
||||
import { useState, useEffect } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import { usePriceModalStore } from "@/store/useProceModalStore";
|
||||
|
||||
export function PriceModal() {
|
||||
const t = useTranslations("priceModal");
|
||||
const { isOpen, product, closeModal } = usePriceModalStore();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
phone: "+998 ",
|
||||
captcha: "",
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState({
|
||||
name: "",
|
||||
phone: "",
|
||||
captcha: "",
|
||||
});
|
||||
|
||||
const [captchaCode, setCaptchaCode] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// Generate random captcha
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
const code = Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||
setCaptchaCode(code);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Reset form when modal closes
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setFormData({ name: "", phone: "+998 ", captcha: "" });
|
||||
setErrors({ name: "", phone: "", captcha: "" });
|
||||
}
|
||||
}, [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, phone: formatted });
|
||||
if (errors.phone) {
|
||||
setErrors({ ...errors, phone: "" });
|
||||
}
|
||||
};
|
||||
|
||||
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: "",
|
||||
phone: "",
|
||||
captcha: "",
|
||||
};
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = t("validation.nameRequired");
|
||||
}
|
||||
|
||||
const phoneNumbers = formData.phone.replace(/\D/g, "");
|
||||
if (phoneNumbers.length !== 12) {
|
||||
newErrors.phone = t("validation.phoneRequired");
|
||||
} else if (!phoneNumbers.startsWith("998")) {
|
||||
newErrors.phone = t("validation.phoneInvalid");
|
||||
}
|
||||
|
||||
if (!formData.captcha.trim()) {
|
||||
newErrors.captcha = t("validation.captchaRequired");
|
||||
} else if (formData.captcha.toUpperCase() !== captchaCode) {
|
||||
newErrors.captcha = t("validation.captchaRequired");
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return !newErrors.name && !newErrors.phone && !newErrors.captcha;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
// API call logikangiz
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
// Success
|
||||
alert(t("success"));
|
||||
closeModal();
|
||||
} catch (error) {
|
||||
alert(t("error"));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
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/50"
|
||||
onClick={closeModal}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative bg-white rounded-lg w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 transition-colors z-10"
|
||||
aria-label="Close modal"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-8">
|
||||
<h2 className="text-3xl font-bold mb-8">{t("title")}</h2>
|
||||
|
||||
{/* Product Info */}
|
||||
<div className="bg-[#f5f0e8] rounded-lg p-6 mb-8 flex items-center gap-6">
|
||||
<div className="relative w-24 h-24 shrink-0">
|
||||
<Image
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-semibold mb-2">{product.name}</h3>
|
||||
<span className="text-green-600 font-medium">
|
||||
{t("product.inStock")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium mb-2">
|
||||
{t("form.name")}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
placeholder={t("form.namePlaceholder")}
|
||||
className={`w-full px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 ${
|
||||
errors.name ? "border-red-500" : "border-gray-300"
|
||||
}`}
|
||||
/>
|
||||
{errors.name && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Phone */}
|
||||
<div>
|
||||
<label htmlFor="phone" className="block text-sm font-medium mb-2">
|
||||
{t("form.phone")}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="tel"
|
||||
id="phone"
|
||||
name="phone"
|
||||
value={formData.phone}
|
||||
onChange={handlePhoneChange}
|
||||
placeholder={t("form.phonePlaceholder")}
|
||||
className={`w-full pl-2 pr-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 ${
|
||||
errors.phone ? "border-red-500" : "border-gray-300"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
{errors.phone && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.phone}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Captcha */}
|
||||
{/* <div>
|
||||
<label htmlFor="captcha" className="block text-sm font-medium mb-2">
|
||||
{t("form.captcha")}
|
||||
</label>
|
||||
<div className="flex gap-4">
|
||||
<div className="relative w-40 h-12 bg-linear-to-r from-purple-200 via-pink-200 to-blue-200 rounded-lg flex items-center justify-center">
|
||||
<span className="text-2xl font-bold tracking-widest select-none">
|
||||
{captchaCode}
|
||||
</span>
|
||||
<div className="absolute inset-0 opacity-30">
|
||||
<svg className="w-full h-full">
|
||||
<line x1="0" y1="0" x2="100%" y2="100%" stroke="currentColor" strokeWidth="1" />
|
||||
<line x1="100%" y1="0" x2="0" y2="100%" stroke="currentColor" strokeWidth="1" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="captcha"
|
||||
name="captcha"
|
||||
value={formData.captcha}
|
||||
onChange={handleInputChange}
|
||||
placeholder={t("form.captchaPlaceholder")}
|
||||
className={`flex-1 px-4 py-3 border rounded-lg focus:outline-none focus:ring-2 focus:ring-red-500 ${
|
||||
errors.captcha ? "border-red-500" : "border-gray-300"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
{errors.captcha && (
|
||||
<p className="text-red-500 text-sm mt-1">{errors.captcha}</p>
|
||||
)}
|
||||
</div> */}
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full bg-[#8B1538] hover:bg-[#6d1028] text-white font-semibold py-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSubmitting ? t("form.submitting") : t("form.submit")}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
234
lib/demoData.ts
@@ -1,4 +1,3 @@
|
||||
|
||||
export const DATA = [
|
||||
{
|
||||
name: "P-0834405",
|
||||
@@ -30,41 +29,212 @@ export const DATA = [
|
||||
},
|
||||
];
|
||||
|
||||
export const faqItems = [
|
||||
export const ProductCatalog = [
|
||||
{
|
||||
id: "faq-1",
|
||||
question: "How do I become a firefighter?",
|
||||
answer:
|
||||
"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.",
|
||||
id: "slt",
|
||||
slug: "slt_blockfire",
|
||||
title: "SLT Blockfire",
|
||||
description: "products.catalog.blockdescription",
|
||||
image: "/images/products/category/slt.png",
|
||||
},
|
||||
{
|
||||
id: "faq-2",
|
||||
question: "What equipment do firefighters use?",
|
||||
answer:
|
||||
"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.",
|
||||
id: "ca",
|
||||
slug: "ca_fire_mech",
|
||||
title: "CA-FIRE | MECH",
|
||||
description: "products.catalog.cadescription",
|
||||
image: "/images/products/category/ca.png",
|
||||
},
|
||||
{
|
||||
id: "faq-3",
|
||||
question: "Do firefighters only fight fires?",
|
||||
answer:
|
||||
"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.",
|
||||
},
|
||||
{
|
||||
id: "faq-4",
|
||||
question: "What are the work hours for firefighters?",
|
||||
answer:
|
||||
"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: "faq-5",
|
||||
question: "How long is firefighter training?",
|
||||
answer:
|
||||
"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: "faq-6",
|
||||
question: "What is required to apply for the firefighter position?",
|
||||
answer:
|
||||
"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).",
|
||||
id: "lede",
|
||||
slug: "lede",
|
||||
title: "LEDE",
|
||||
description: "products.catalog.lededescription",
|
||||
image: "/images/products/category/lede.png",
|
||||
},
|
||||
];
|
||||
|
||||
export const slt = [
|
||||
{
|
||||
id: 1,
|
||||
slug: "slt_bir_qavatli_quvr",
|
||||
name: "Bir qavatli PP-R quvuri (SDR 6) SLT BLOCKFIRE",
|
||||
description: "",
|
||||
image: "/images/products/slt/slt_blackfirebittalik-removebg-preview.png",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
slug: "slt_yonginga_qarshi_45_burilish",
|
||||
name: "Yong‘inga qarshi polipropilenli 45° burilish",
|
||||
description: "",
|
||||
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",
|
||||
})),
|
||||
},
|
||||
];
|
||||
68
lib/filter-zustand.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { immer } from "zustand/middleware/immer";
|
||||
|
||||
export interface FilterItem {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface FilterZustandTypes {
|
||||
filter: FilterItem[];
|
||||
removeFilter: (id: number) => void;
|
||||
toggleFilter: (item: FilterItem) => void;
|
||||
resetFilter: () => void;
|
||||
hasFilter: (name: string) => boolean;
|
||||
getFiltersByType: (type: string) => FilterItem[]; // ✅ Qo'shimcha
|
||||
removeFiltersByType: (type: string) => void; // ✅ Qo'shimcha
|
||||
}
|
||||
|
||||
export const useFilter = create<FilterZustandTypes>()(
|
||||
persist(
|
||||
immer((set, get) => ({
|
||||
filter: [],
|
||||
|
||||
removeFilter: (id) =>
|
||||
set((state) => {
|
||||
state.filter = state.filter.filter((item:FilterItem) => item.id !== id);
|
||||
}),
|
||||
|
||||
toggleFilter: (item) =>
|
||||
set((state) => {
|
||||
const index = state.filter.findIndex((f:FilterItem) => f.name === item.name);
|
||||
if (index !== -1) {
|
||||
// Agar bor bo'lsa o'chirish
|
||||
state.filter.splice(index, 1);
|
||||
} else {
|
||||
// Agar yo'q bo'lsa qo'shish
|
||||
state.filter.push(item);
|
||||
}
|
||||
}),
|
||||
|
||||
resetFilter: () =>
|
||||
set((state) => {
|
||||
state.filter = [];
|
||||
}),
|
||||
|
||||
hasFilter: (name) => {
|
||||
return get().filter.some((item) => item.name === name);
|
||||
},
|
||||
|
||||
// ✅ Type bo'yicha filterlarni olish
|
||||
getFiltersByType: (type) => {
|
||||
return get().filter.filter((item) => item.type === type);
|
||||
},
|
||||
|
||||
// ✅ Type bo'yicha filterlarni o'chirish
|
||||
removeFiltersByType: (type) =>
|
||||
set((state) => {
|
||||
state.filter = state.filter.filter((item:FilterItem) => item.type !== type);
|
||||
}),
|
||||
})),
|
||||
{
|
||||
name: "filter-storage", // localStorage key nomi
|
||||
partialize: (state) => ({ filter: state.filter }), // Faqat filter'ni saqlash
|
||||
}
|
||||
)
|
||||
);
|
||||
40
lib/types.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export interface CategoryType {
|
||||
id: number;
|
||||
name: string;
|
||||
image: string;
|
||||
description: string;
|
||||
have_sub_category: boolean;
|
||||
}
|
||||
|
||||
export interface SubCategoryType {
|
||||
id: number;
|
||||
name: string;
|
||||
category: number;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export interface ProductsPageTypes {
|
||||
id: number;
|
||||
name: string;
|
||||
image: string;
|
||||
}
|
||||
|
||||
export interface ProductImage {
|
||||
id: number;
|
||||
product: number;
|
||||
image: string;
|
||||
is_main: boolean;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export interface ProductDetail {
|
||||
id: number;
|
||||
name: string;
|
||||
articular: string;
|
||||
status: string;
|
||||
description: string;
|
||||
size: number;
|
||||
price: string;
|
||||
features: string[];
|
||||
images: ProductImage[];
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"banner": {
|
||||
"title1": "Welcome to Ignum",
|
||||
"title2": "FIRE PROTECTION GUARDIAN",
|
||||
"description": "We are seen as a beacon of hope, a figure that brings calm amidst chaos and light in the darkest of moments.",
|
||||
"description": "We provide professional services for the installation of fire safety systems and the sale of certified protective equipment.",
|
||||
"cta": "Get Started"
|
||||
},
|
||||
"statistics": {
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"about": {
|
||||
"title": "About us",
|
||||
"subtitle": "Fire Protection Systems Ready",
|
||||
"subtitle": "Fire Protection Systems",
|
||||
"prevention": {
|
||||
"title": "Fire Prevention Systems",
|
||||
"description": "We offer the most advanced fire prevention technologies. Every detail is designed to ensure safety and reliability."
|
||||
@@ -139,13 +139,18 @@
|
||||
"privacy": "You agree to our friendly privacy policy",
|
||||
"send": "SEND MESSAGE",
|
||||
"email": "EMAIL",
|
||||
"emailAddress": "support@ignum.com",
|
||||
"emailAddress": "info@ignum-tech.com",
|
||||
"location": "Our Location",
|
||||
"address": "Jl. Dr. Ir. Soekarno No. 99x Tabanan – Bali",
|
||||
"address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy",
|
||||
"phone": "Phone"
|
||||
}
|
||||
},
|
||||
"products": {
|
||||
"catalog": {
|
||||
"blockdescription": "Polypropylene pipes and fittings for automatic fire suppression systems and internal fire water supply",
|
||||
"cadescription": "Equipment for automatic fire suppression",
|
||||
"lededescription": "Weld-free coupling connections for installation of water pipelines of any purpose"
|
||||
},
|
||||
"banner": {
|
||||
"title": "Products",
|
||||
"subtitle": "Ignum Technology Ready",
|
||||
@@ -203,7 +208,7 @@
|
||||
"emergency": "Emergency Call!"
|
||||
},
|
||||
"footer": {
|
||||
"description": "We are seen as a beacon of hope, a figure that brings calm amidst chaos and light in the darkest of moments.",
|
||||
"description": "We provide professional services for the installation of fire safety systems and the sale of certified protective equipment.",
|
||||
"quickLinks": {
|
||||
"title": "QUICK LINKS",
|
||||
"home": "Home",
|
||||
@@ -216,12 +221,39 @@
|
||||
"title": "SUPPORT",
|
||||
"contact": "Contact",
|
||||
"help": "Help"
|
||||
}
|
||||
},
|
||||
"address":"Tashkent city, Yunusabad district, 3rd dead-end of Niyozbek Yoli street, house 39"
|
||||
},
|
||||
"rasmlar": "Images",
|
||||
"fotogalereya": "Photo Gallery",
|
||||
"contactTitle": "Send us your phone number",
|
||||
"contactSubTitle": "Our staff will contact you",
|
||||
"enterPhone": "Enter your phone number",
|
||||
"send": "Send"
|
||||
"send": "Sent",
|
||||
"error":"Error!",
|
||||
"succes":"sent!",
|
||||
"priceModal": {
|
||||
"title": "Get Price",
|
||||
"product": {
|
||||
"inStock": "In Stock"
|
||||
},
|
||||
"form": {
|
||||
"name": "Name",
|
||||
"namePlaceholder": "Enter your name",
|
||||
"phone": "Phone",
|
||||
"phonePlaceholder": "+998 91 234 56 78",
|
||||
"captcha": "Spam Protection",
|
||||
"captchaPlaceholder": "Enter code",
|
||||
"submit": "Submit",
|
||||
"submitting": "Submitting..."
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Please enter your name",
|
||||
"phoneRequired": "Please enter phone number",
|
||||
"phoneInvalid": "Invalid phone format",
|
||||
"captchaRequired": "Please enter code"
|
||||
},
|
||||
"success": "Your request has been sent successfully!",
|
||||
"error": "An error occurred. Please try again."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"banner": {
|
||||
"title1": "Добро пожаловать в Ignum",
|
||||
"title2": "Защита от Пожара",
|
||||
"description": "Мы воспринимаемся как луч надежды, фигура, которая приносит спокойствие среди хаоса и свет в самые темные моменты.",
|
||||
"description": "Мы предоставляем профессиональные услуги по установке систем пожарной безопасности и продаже сертифицированных средств защиты.",
|
||||
"cta": "Начать"
|
||||
},
|
||||
"statistics": {
|
||||
@@ -139,13 +139,18 @@
|
||||
"privacy": "Вы соглашаетесь с нашей дружественной политикой конфиденциальности",
|
||||
"send": "ОТПРАВИТЬ СООБЩЕНИЕ",
|
||||
"email": "ЭЛЕКТРОННАЯ ПОЧТА",
|
||||
"emailAddress": "support@ignum.com",
|
||||
"emailAddress": "info@ignum-tech.com",
|
||||
"location": "Наше Местоположение",
|
||||
"address": "Jl. Dr. Ir. Soekarno No. 99x Tabanan – Bali",
|
||||
"address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy",
|
||||
"phone": "Телефон"
|
||||
}
|
||||
},
|
||||
"products": {
|
||||
"catalog": {
|
||||
"blockdescription": "Полипропиленовые трубы и фитинги для систем автоматического пожаротушения и внутреннего противопожарного водопровода",
|
||||
"cadescription": "Оборудование для автоматического пожаротушения",
|
||||
"lededescription": "Безсварные муфтовые соединения для монтажа водяных трубопроводов любого назначения"
|
||||
},
|
||||
"banner": {
|
||||
"title": "Продукты",
|
||||
"subtitle": "Технология Ignum Готова",
|
||||
@@ -203,7 +208,7 @@
|
||||
"emergency": "Экстренный Вызов!"
|
||||
},
|
||||
"footer": {
|
||||
"description": "Мы воспринимаемся как луч надежды, фигура, которая приносит спокойствие среди хаоса и свет в самые темные моменты.",
|
||||
"description": "Мы предоставляем профессиональные услуги по установке систем пожарной безопасности и продаже сертифицированных средств защиты.",
|
||||
"quickLinks": {
|
||||
"title": "БЫСТРЫЕ ССЫЛКИ",
|
||||
"home": "Главная",
|
||||
@@ -216,12 +221,39 @@
|
||||
"title": "ПОДДЕРЖКА",
|
||||
"contact": "Контакты",
|
||||
"help": "Помощь"
|
||||
}
|
||||
},
|
||||
"address":"г. Ташкент, Юнусабадский район, 3-й тупик улицы Ниязбек йўли, дом 39"
|
||||
},
|
||||
"rasmlar": "Изображения",
|
||||
"fotogalereya": "Фотогалерея",
|
||||
"contactTitle": "Отправьте нам свой номер",
|
||||
"contactSubTitle": "Наши сотрудники свяжутся с вами",
|
||||
"enterPhone": "Введите ваш номер телефона",
|
||||
"send": "Отправить"
|
||||
"send": "Отправить",
|
||||
"error": "Ошибка!",
|
||||
"succes": "Отправлено!",
|
||||
"priceModal": {
|
||||
"title": "Узнать цену",
|
||||
"product": {
|
||||
"inStock": "В наличии"
|
||||
},
|
||||
"form": {
|
||||
"name": "Имя",
|
||||
"namePlaceholder": "Введите ваше имя",
|
||||
"phone": "Телефон",
|
||||
"phonePlaceholder": "+998 91 234 56 78",
|
||||
"captcha": "Защита от спама",
|
||||
"captchaPlaceholder": "Введите код",
|
||||
"submit": "Отправить",
|
||||
"submitting": "Отправка..."
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Введите ваше имя",
|
||||
"phoneRequired": "Введите номер телефона",
|
||||
"phoneInvalid": "Неверный формат телефона",
|
||||
"captchaRequired": "Введите код"
|
||||
},
|
||||
"success": "Ваш запрос успешно отправлен!",
|
||||
"error": "Произошла ошибка. Попробуйте снова."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"banner": {
|
||||
"title1": "Ignum-ga xush kelibsiz",
|
||||
"title2": "YONG'INGA QARSHI HIMOYA",
|
||||
"description": "Biz umid nuri, tartibsizlik davrida tinchlik va eng qiyin vaziyatlarda ishonchli himoya manbai sifatida ko'rilamiz.",
|
||||
"description": "Biz yong‘in xavfsizligi tizimlarini o‘rnatish va sertifikatlangan himoya vositalari savdosi bo‘yicha professional xizmatlar ko‘rsatamiz.",
|
||||
"cta": "Boshlash"
|
||||
},
|
||||
"statistics": {
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"about": {
|
||||
"title": "Biz haqimizda",
|
||||
"subtitle": "YONG'INGA QARSHI TIZIMLAR TAYYOR",
|
||||
"subtitle": "YONG'INGA QARSHI TIZIMLAR",
|
||||
"prevention": {
|
||||
"title": "YONG'INDI OLDINI OLISH TIZIMLARI",
|
||||
"description": "Biz eng ilg'or yong'in oldini olish texnologiyalarini taklif etamiz. Har bir detal xavfsizlik va ishonchlilikni ta'minlash uchun ishlab chiqilgan."
|
||||
@@ -139,13 +139,18 @@
|
||||
"privacy": "Bizning maxfiylik siyosatimizga rozilik bildirasiz",
|
||||
"send": "XABAR YUBORISH",
|
||||
"email": "ELEKTRON POCHTA",
|
||||
"emailAddress": "support@ignum.com",
|
||||
"emailAddress": "info@ignum-tech.com",
|
||||
"location": "Bizning Manzilimiz",
|
||||
"address": "Jl. Dr. Ir. Soekarno No. 99x Tabanan – Bali",
|
||||
"address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy",
|
||||
"phone": "Telefon"
|
||||
}
|
||||
},
|
||||
"products": {
|
||||
"catalog": {
|
||||
"blockdescription": "Avtomatik yong‘in o‘chirish tizimlari va ichki yong‘inga qarshi suv ta’minoti uchun polipropilen quvurlar va fitinglar",
|
||||
"cadescription": "Avtomatik yong‘in o‘chirish uchun uskunalar",
|
||||
"lededescription": "Har qanday maqsaddagi suv quvurlarini o‘rnatish uchun payvandsiz muftali ulanishlar"
|
||||
},
|
||||
"banner": {
|
||||
"title": "Mahsulotlar",
|
||||
"subtitle": "Ignum Texnologiyasi Tayyor",
|
||||
@@ -172,7 +177,7 @@
|
||||
"answer": "Biz muntazam texnik xizmat ko'rsatish va 24/7 favqulodda xizmatni taklif etamiz. Har olti oyda bir marta profilaktik tekshiruvlar o'tkaziladi."
|
||||
},
|
||||
"question4": {
|
||||
"question": "Kafolatiy muddati qancha?",
|
||||
"question": "Kafolat muddati qancha?",
|
||||
"answer": "Barcha uskunalarimiz uchun 2 yillik kafolat va 10 yillik texnik xizmat ko'rsatish kafolati beriladi."
|
||||
},
|
||||
"question5": {
|
||||
@@ -203,7 +208,7 @@
|
||||
"emergency": "Favqulodda Qo'ng'iroq!"
|
||||
},
|
||||
"footer": {
|
||||
"description": "Biz umid nuri, tartibsizlik davrida tinchlik va eng qiyin vaziyatlarda ishonchli himoya manbai sifatida ko'rilamiz.",
|
||||
"description": "Biz yong‘in xavfsizligi tizimlarini o‘rnatish va sertifikatlangan himoya vositalari savdosi bo‘yicha professional xizmatlar ko‘rsatamiz.",
|
||||
"quickLinks": {
|
||||
"title": "TEZKOR HAVOLALAR",
|
||||
"home": "Asosiy",
|
||||
@@ -216,12 +221,39 @@
|
||||
"title": "YORDAM",
|
||||
"contact": "Aloqa",
|
||||
"help": "Yordam"
|
||||
}
|
||||
},
|
||||
"address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy"
|
||||
},
|
||||
"rasmlar": "Rasmlar",
|
||||
"fotogalereya": "Fotogalereya",
|
||||
"contactTitle": "Bizga raqamingizni yuboring",
|
||||
"contactSubTitle": "Xodimlarimiz siz bilan bog'lanishadi",
|
||||
"enterPhone":"Telefon raqamingiz kiriting",
|
||||
"send":"Yuborish"
|
||||
"enterPhone": "Telefon raqamingiz kiriting",
|
||||
"send": "Yuborish",
|
||||
"error": "Xatolik!",
|
||||
"succes": "Yuborildi!",
|
||||
"priceModal": {
|
||||
"title": "Narxni bilish",
|
||||
"product": {
|
||||
"inStock": "Sotuvda mavjud"
|
||||
},
|
||||
"form": {
|
||||
"name": "Ism",
|
||||
"namePlaceholder": "Ismingizni kiriting",
|
||||
"phone": "Telefon raqam",
|
||||
"phonePlaceholder": "+998 91 234 56 78",
|
||||
"captcha": "Spamdan himoya",
|
||||
"captchaPlaceholder": "Kodni kiriting",
|
||||
"submit": "Yuborish",
|
||||
"submitting": "Yuborilmoqda..."
|
||||
},
|
||||
"validation": {
|
||||
"nameRequired": "Iltimos, ismingizni kiriting",
|
||||
"phoneRequired": "Iltimos, telefon raqamingizni kiriting",
|
||||
"phoneInvalid": "Telefon raqami noto‘g‘ri formatda kiritilgan",
|
||||
"captchaRequired": "Iltimos, kodni kiriting"
|
||||
},
|
||||
"success": "So‘rovingiz muvaffaqiyatli yuborildi!",
|
||||
"error": "Xatolik yuz berdi. Iltimos, qayta urinib ko‘ring."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,14 +38,17 @@
|
||||
"@radix-ui/react-toggle": "1.1.1",
|
||||
"@radix-ui/react-toggle-group": "1.1.1",
|
||||
"@radix-ui/react-tooltip": "1.1.6",
|
||||
"@tanstack/react-query": "^5.90.20",
|
||||
"@vercel/analytics": "1.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^1.13.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
"date-fns": "4.1.0",
|
||||
"embla-carousel-react": "8.5.1",
|
||||
"framer-motion": "^12.29.2",
|
||||
"immer": "^11.1.3",
|
||||
"input-otp": "1.4.1",
|
||||
"lucide-react": "^0.454.0",
|
||||
"negotiator": "^1.0.0",
|
||||
@@ -58,13 +61,15 @@
|
||||
"react-fast-marquee": "^1.6.5",
|
||||
"react-hook-form": "^7.60.0",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-toastify": "^11.0.5",
|
||||
"recharts": "2.15.4",
|
||||
"sonner": "^1.7.4",
|
||||
"swiper": "^12.0.3",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "3.25.76"
|
||||
"zod": "3.25.76",
|
||||
"zustand": "^5.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.9",
|
||||
|
||||
259
pnpm-lock.yaml
generated
@@ -95,12 +95,18 @@ importers:
|
||||
'@radix-ui/react-tooltip':
|
||||
specifier: 1.1.6
|
||||
version: 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
'@tanstack/react-query':
|
||||
specifier: ^5.90.20
|
||||
version: 5.90.20(react@19.2.0)
|
||||
'@vercel/analytics':
|
||||
specifier: 1.3.1
|
||||
version: 1.3.1(next@16.0.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)
|
||||
autoprefixer:
|
||||
specifier: ^10.4.20
|
||||
version: 10.4.23(postcss@8.5.6)
|
||||
axios:
|
||||
specifier: ^1.13.4
|
||||
version: 1.13.4
|
||||
class-variance-authority:
|
||||
specifier: ^0.7.1
|
||||
version: 0.7.1
|
||||
@@ -119,6 +125,9 @@ importers:
|
||||
framer-motion:
|
||||
specifier: ^12.29.2
|
||||
version: 12.29.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
immer:
|
||||
specifier: ^11.1.3
|
||||
version: 11.1.3
|
||||
input-otp:
|
||||
specifier: 1.4.1
|
||||
version: 1.4.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
@@ -155,6 +164,9 @@ importers:
|
||||
react-resizable-panels:
|
||||
specifier: ^2.1.7
|
||||
version: 2.1.9(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react-toastify:
|
||||
specifier: ^11.0.5
|
||||
version: 11.0.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
recharts:
|
||||
specifier: 2.15.4
|
||||
version: 2.15.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
@@ -176,6 +188,9 @@ importers:
|
||||
zod:
|
||||
specifier: 3.25.76
|
||||
version: 3.25.76
|
||||
zustand:
|
||||
specifier: ^5.0.10
|
||||
version: 5.0.10(@types/react@19.2.9)(immer@11.1.3)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0))
|
||||
devDependencies:
|
||||
'@tailwindcss/postcss':
|
||||
specifier: ^4.1.9
|
||||
@@ -1376,6 +1391,14 @@ packages:
|
||||
'@tailwindcss/postcss@4.1.18':
|
||||
resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==}
|
||||
|
||||
'@tanstack/query-core@5.90.20':
|
||||
resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==}
|
||||
|
||||
'@tanstack/react-query@5.90.20':
|
||||
resolution: {integrity: sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19
|
||||
|
||||
'@types/d3-array@3.2.2':
|
||||
resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
|
||||
|
||||
@@ -1432,6 +1455,9 @@ packages:
|
||||
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
autoprefixer@10.4.23:
|
||||
resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
@@ -1439,6 +1465,9 @@ packages:
|
||||
peerDependencies:
|
||||
postcss: ^8.1.0
|
||||
|
||||
axios@1.13.4:
|
||||
resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==}
|
||||
|
||||
baseline-browser-mapping@2.9.17:
|
||||
resolution: {integrity: sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ==}
|
||||
hasBin: true
|
||||
@@ -1448,6 +1477,10 @@ packages:
|
||||
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
|
||||
hasBin: true
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
caniuse-lite@1.0.30001765:
|
||||
resolution: {integrity: sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==}
|
||||
|
||||
@@ -1467,6 +1500,10 @@ packages:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
csstype@3.2.3:
|
||||
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
|
||||
|
||||
@@ -1526,6 +1563,10 @@ packages:
|
||||
decimal.js@10.6.0:
|
||||
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1536,6 +1577,10 @@ packages:
|
||||
dom-helpers@5.2.1:
|
||||
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
electron-to-chromium@1.5.267:
|
||||
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
|
||||
|
||||
@@ -1556,6 +1601,22 @@ packages:
|
||||
resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
es-define-property@1.0.1:
|
||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-errors@1.3.0:
|
||||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-set-tostringtag@2.1.0:
|
||||
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
escalade@3.2.0:
|
||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1567,6 +1628,19 @@ packages:
|
||||
resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
follow-redirects@1.15.11:
|
||||
resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
peerDependencies:
|
||||
debug: '*'
|
||||
peerDependenciesMeta:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
form-data@4.0.5:
|
||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
fraction.js@5.3.4:
|
||||
resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==}
|
||||
|
||||
@@ -1584,13 +1658,43 @@ packages:
|
||||
react-dom:
|
||||
optional: true
|
||||
|
||||
function-bind@1.1.2:
|
||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-nonce@1.0.1:
|
||||
resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
get-proto@1.0.1:
|
||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
has-symbols@1.1.0:
|
||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
immer@11.1.3:
|
||||
resolution: {integrity: sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==}
|
||||
|
||||
input-otp@1.4.1:
|
||||
resolution: {integrity: sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==}
|
||||
peerDependencies:
|
||||
@@ -1704,6 +1808,18 @@ packages:
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
motion-dom@12.29.2:
|
||||
resolution: {integrity: sha512-/k+NuycVV8pykxyiTCoFzIVLA95Nb1BFIVvfSu9L50/6K6qNeAYtkxXILy/LRutt7AzaYDc2myj0wkCVVYAPPA==}
|
||||
|
||||
@@ -1793,6 +1909,9 @@ packages:
|
||||
prop-types@15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
|
||||
react-day-picker@9.8.0:
|
||||
resolution: {integrity: sha512-E0yhhg7R+pdgbl/2toTb0xBhsEAtmAx1l7qjIWYfcxOy8w4rTSVfbtBoSzVVhPwKP/5E9iL38LivzoE3AQDhCQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1864,6 +1983,12 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
react-toastify@11.0.5:
|
||||
resolution: {integrity: sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19
|
||||
react-dom: ^18 || ^19
|
||||
|
||||
react-transition-group@4.4.5:
|
||||
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
||||
peerDependencies:
|
||||
@@ -2006,6 +2131,24 @@ packages:
|
||||
zod@3.25.76:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
zustand@5.0.10:
|
||||
resolution: {integrity: sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
peerDependencies:
|
||||
'@types/react': '>=18.0.0'
|
||||
immer: '>=9.0.6'
|
||||
react: '>=18.0.0'
|
||||
use-sync-external-store: '>=1.2.0'
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
use-sync-external-store:
|
||||
optional: true
|
||||
|
||||
snapshots:
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
@@ -3100,6 +3243,13 @@ snapshots:
|
||||
postcss: 8.5.6
|
||||
tailwindcss: 4.1.18
|
||||
|
||||
'@tanstack/query-core@5.90.20': {}
|
||||
|
||||
'@tanstack/react-query@5.90.20(react@19.2.0)':
|
||||
dependencies:
|
||||
'@tanstack/query-core': 5.90.20
|
||||
react: 19.2.0
|
||||
|
||||
'@types/d3-array@3.2.2': {}
|
||||
|
||||
'@types/d3-color@3.1.3': {}
|
||||
@@ -3149,6 +3299,8 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
autoprefixer@10.4.23(postcss@8.5.6):
|
||||
dependencies:
|
||||
browserslist: 4.28.1
|
||||
@@ -3158,6 +3310,14 @@ snapshots:
|
||||
postcss: 8.5.6
|
||||
postcss-value-parser: 4.2.0
|
||||
|
||||
axios@1.13.4:
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11
|
||||
form-data: 4.0.5
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
baseline-browser-mapping@2.9.17: {}
|
||||
|
||||
browserslist@4.28.1:
|
||||
@@ -3168,6 +3328,11 @@ snapshots:
|
||||
node-releases: 2.0.27
|
||||
update-browserslist-db: 1.2.3(browserslist@4.28.1)
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
function-bind: 1.1.2
|
||||
|
||||
caniuse-lite@1.0.30001765: {}
|
||||
|
||||
class-variance-authority@0.7.1:
|
||||
@@ -3190,6 +3355,10 @@ snapshots:
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
d3-array@3.2.4:
|
||||
@@ -3238,6 +3407,8 @@ snapshots:
|
||||
|
||||
decimal.js@10.6.0: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
detect-node-es@1.1.0: {}
|
||||
@@ -3247,6 +3418,12 @@ snapshots:
|
||||
'@babel/runtime': 7.28.6
|
||||
csstype: 3.2.3
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
electron-to-chromium@1.5.267: {}
|
||||
|
||||
embla-carousel-react@8.5.1(react@19.2.0):
|
||||
@@ -3266,12 +3443,37 @@ snapshots:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.3.0
|
||||
|
||||
es-define-property@1.0.1: {}
|
||||
|
||||
es-errors@1.3.0: {}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
es-set-tostringtag@2.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
has-tostringtag: 1.0.2
|
||||
hasown: 2.0.2
|
||||
|
||||
escalade@3.2.0: {}
|
||||
|
||||
eventemitter3@4.0.7: {}
|
||||
|
||||
fast-equals@5.4.0: {}
|
||||
|
||||
follow-redirects@1.15.11: {}
|
||||
|
||||
form-data@4.0.5:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
fraction.js@5.3.4: {}
|
||||
|
||||
framer-motion@12.29.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
@@ -3283,10 +3485,44 @@ snapshots:
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-define-property: 1.0.1
|
||||
es-errors: 1.3.0
|
||||
es-object-atoms: 1.1.1
|
||||
function-bind: 1.1.2
|
||||
get-proto: 1.0.1
|
||||
gopd: 1.2.0
|
||||
has-symbols: 1.1.0
|
||||
hasown: 2.0.2
|
||||
math-intrinsics: 1.1.0
|
||||
|
||||
get-nonce@1.0.1: {}
|
||||
|
||||
get-proto@1.0.1:
|
||||
dependencies:
|
||||
dunder-proto: 1.0.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
has-symbols@1.1.0: {}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
dependencies:
|
||||
has-symbols: 1.1.0
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
immer@11.1.3: {}
|
||||
|
||||
input-otp@1.4.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
@@ -3374,6 +3610,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-types@2.1.35:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
|
||||
motion-dom@12.29.2:
|
||||
dependencies:
|
||||
motion-utils: 12.29.2
|
||||
@@ -3462,6 +3706,8 @@ snapshots:
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
|
||||
react-day-picker@9.8.0(react@19.2.0):
|
||||
dependencies:
|
||||
'@date-fns/tz': 1.2.0
|
||||
@@ -3527,6 +3773,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.9
|
||||
|
||||
react-toastify@11.0.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
clsx: 2.1.1
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
react-transition-group@4.4.5(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.6
|
||||
@@ -3687,3 +3939,10 @@ snapshots:
|
||||
d3-timer: 3.0.1
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
zustand@5.0.10(@types/react@19.2.9)(immer@11.1.3)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)):
|
||||
optionalDependencies:
|
||||
'@types/react': 19.2.9
|
||||
immer: 11.1.3
|
||||
react: 19.2.0
|
||||
use-sync-external-store: 1.6.0(react@19.2.0)
|
||||
|
||||
BIN
public/images/homeBanner2.png
Normal file
|
After Width: | Height: | Size: 763 KiB |
BIN
public/images/homeBanner3.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
public/images/products/ca/regulyator-removebg-preview.png
Normal file
|
After Width: | Height: | Size: 165 KiB |
BIN
public/images/products/ca/zadviji-removebg-preview.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
public/images/products/ca/zatvori-removebg-preview.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
public/images/products/category/ca.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
BIN
public/images/products/category/lede.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
public/images/products/category/slt.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
public/images/products/lede/atvot_rezbavoy-removebg-preview.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
public/images/products/lede/flanes_nakidnoy-removebg-preview.png
Normal file
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 27 KiB |
BIN
public/images/products/slt/slt_flanes_burt-removebg-preview.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 21 KiB |
BIN
public/images/products/slt/slt_temir_tirsak-removebg-preview.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 31 KiB |
BIN
public/images/products/slt/slt_varnoe_Sedlo-removebg-preview.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
35
request/api.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import axios, { InternalAxiosRequestConfig } from "axios";
|
||||
import { getRouteLang } from "./getLang";
|
||||
|
||||
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL;
|
||||
|
||||
const httpClient = axios.create({
|
||||
baseURL: baseUrl,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
// Request interceptor
|
||||
httpClient.interceptors.request.use(
|
||||
(config: InternalAxiosRequestConfig) => {
|
||||
const language = getRouteLang();
|
||||
config.headers["Accept-Language"] = language;
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Response interceptor (xatoliklarni boshqarish uchun)
|
||||
httpClient.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
// Xatolikni formatlash
|
||||
const message = error.response?.data?.message || error.message;
|
||||
return Promise.reject(message);
|
||||
}
|
||||
);
|
||||
|
||||
export default httpClient;
|
||||
18
request/getLang.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// utils/getRouteLang.ts
|
||||
import { locales, defaultLocale } from "@/i18n/config";
|
||||
|
||||
export function getRouteLang(): string {
|
||||
if (typeof window === "undefined") return defaultLocale;
|
||||
|
||||
// 1)Fall back to the first non-empty pathname segment
|
||||
const rawSegments = window.location.pathname.split("/"); // e.g. ['', 'uz', 'path', ...]
|
||||
const segments = rawSegments.filter(Boolean); // removes empty strings
|
||||
if (segments.length > 0) {
|
||||
const candidate = segments[0]; // first segment after root
|
||||
if (locales.includes(candidate as (typeof locales)[number]))
|
||||
return candidate;
|
||||
}
|
||||
|
||||
// 2) final fallback
|
||||
return defaultLocale;
|
||||
}
|
||||
31
request/links.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export const endPoints = {
|
||||
category: {
|
||||
all: "category/",
|
||||
},
|
||||
subCategory: {
|
||||
byId: (id: number) => `subCategory/?category=${id}`,
|
||||
},
|
||||
product: {
|
||||
byCategory: (categoryId: number) => `product/?category=${categoryId}`,
|
||||
bySubCategory: (subCategoryId: number) =>
|
||||
`product/?subCategory=${subCategoryId}`,
|
||||
detail: (id: number) => `product/${id}/`,
|
||||
},
|
||||
faq: "faq/",
|
||||
gallery: "gallery/?page_size=500",
|
||||
contact: "contact/",
|
||||
statistics: "statistics/",
|
||||
filter: {
|
||||
size: "size/",
|
||||
sizePageItems: "size/?page_size=500",
|
||||
sizeCategoryId:(id:number)=>`size/?category=${id}&page_size=500`,
|
||||
catalog: "catalog/",
|
||||
catalogPageItems: "catalog/?page_size=500",
|
||||
catalogCategoryId:(id:number)=>`catalog/?category=${id}&page_size=500`,
|
||||
},
|
||||
post: {
|
||||
sendNumber: "callBack/",
|
||||
productContact: "customer/",
|
||||
contact: "question/",
|
||||
},
|
||||
};
|
||||
22
store/useCategory.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { CategoryType } from "@/lib/types";
|
||||
import { create } from "zustand";
|
||||
|
||||
interface CategoryZustandType {
|
||||
category: CategoryType;
|
||||
setCategory: (category: CategoryType) => void;
|
||||
clearCatalog: () => void;
|
||||
}
|
||||
|
||||
const demoCategory: CategoryType = {
|
||||
id: 0,
|
||||
name: "",
|
||||
description: "",
|
||||
image: "",
|
||||
have_sub_category: false,
|
||||
};
|
||||
|
||||
export const useCategory = create<CategoryZustandType>((set) => ({
|
||||
category: demoCategory,
|
||||
setCategory: (data) => set({ category: data }),
|
||||
clearCatalog: () => set({ category: demoCategory }),
|
||||
}));
|
||||
22
store/useProceModalStore.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface Product {
|
||||
id: number;
|
||||
name: string;
|
||||
image: string;
|
||||
inStock: boolean;
|
||||
}
|
||||
|
||||
interface PriceModalStore {
|
||||
isOpen: boolean;
|
||||
product: Product | null;
|
||||
openModal: (product: Product) => void;
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
export const usePriceModalStore = create<PriceModalStore>((set) => ({
|
||||
isOpen: false,
|
||||
product: null,
|
||||
openModal: (product) => set({ isOpen: true, product }),
|
||||
closeModal: () => set({ isOpen: false, product: null }),
|
||||
}));
|
||||
18
store/useProduct.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { ProductsPageTypes } from "@/lib/types";
|
||||
import { create } from "zustand";
|
||||
|
||||
const demoProductPageData = {
|
||||
id: 0,
|
||||
name: "",
|
||||
image: "",
|
||||
};
|
||||
|
||||
interface ProductPageZustanType {
|
||||
product: ProductsPageTypes;
|
||||
setProducts: (data: ProductsPageTypes) => void;
|
||||
}
|
||||
|
||||
export const useProductPageInfo = create<ProductPageZustanType>((set) => ({
|
||||
product: demoProductPageData,
|
||||
setProducts: (data) => set({ product: data }),
|
||||
}));
|
||||
20
store/useSubCategory.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { SubCategoryType } from "@/lib/types";
|
||||
import { create } from "zustand";
|
||||
|
||||
interface SubCategoryZustandType {
|
||||
subCategory: SubCategoryType;
|
||||
setSubCategory: (subCategory: SubCategoryType) => void;
|
||||
clearSubCategory: () => void;
|
||||
}
|
||||
|
||||
const demoSubCategory = {
|
||||
id: 0,
|
||||
name: "",
|
||||
category: 0,
|
||||
image: "",
|
||||
};
|
||||
export const useSubCategory = create<SubCategoryZustandType>((set) => ({
|
||||
subCategory: demoSubCategory,
|
||||
setSubCategory: (data) => set({ subCategory: data }),
|
||||
clearSubCategory: () => set({ subCategory: demoSubCategory }),
|
||||
}));
|
||||