Compare commits

...

16 Commits

Author SHA1 Message Date
nabijonovdavronbek619@gmail.com
74f1d7a9fd unuse tools removed 2026-02-07 11:28:49 +05:00
nabijonovdavronbek619@gmail.com
dbe8399086 add catalog filter card 2026-02-07 11:27:13 +05:00
nabijonovdavronbek619@gmail.com
66bf104cb7 add maps to contact page catalog_page added , email addres updated , remove page drop down 2026-02-06 23:03:46 +05:00
nabijonovdavronbek619@gmail.com
873bbb82a9 get products by filter connected 2026-02-06 21:47:56 +05:00
nabijonovdavronbek619@gmail.com
d4a242b169 catalog connected to backend 2026-02-06 18:57:47 +05:00
nabijonovdavronbek619@gmail.com
e99df29b81 subcategory zustand fixed and created product zustand for get product detail information 2026-02-05 20:08:13 +05:00
nabijonovdavronbek619@gmail.com
34cb524626 catalog part connected to backend , added empty data and loading component 2026-02-05 19:56:23 +05:00
nabijonovdavronbek619@gmail.com
3cf5e0efcf filter connected to backend 2026-02-05 15:11:36 +05:00
nabijonovdavronbek619@gmail.com
d7e1990cc9 faq , statistics , gallary parts connected to backend 2026-02-05 12:21:33 +05:00
nabijonovdavronbek619@gmail.com
87f304225e toast part design update 2026-02-05 11:28:19 +05:00
nabijonovdavronbek619@gmail.com
3c862ea104 connetcted to backend: form request 2026-02-05 11:02:57 +05:00
nabijonovdavronbek619@gmail.com
ca3e28779e loading and catalog card 2026-02-02 18:22:53 +05:00
nabijonovdavronbek619@gmail.com
63b363b142 priceContact added 2026-02-01 19:19:46 +05:00
nabijonovdavronbek619@gmail.com
96acd12d9c catalog part is done 2026-01-30 20:01:56 +05:00
nabijonovdavronbek619@gmail.com
b1095f2c12 filter 2026-01-30 11:24:07 +05:00
nabijonovdavronbek619@gmail.com
f439f9bbdf background animation and navbar logo animation 2026-01-29 17:08:51 +05:00
90 changed files with 2752 additions and 565 deletions

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

View File

@@ -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} />

View File

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

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

View File

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

View File

@@ -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
View File

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

View File

@@ -1,51 +1,46 @@
"use client";
import httpClient from "@/request/api";
import { endPoints } from "@/request/links";
import { useQuery } from "@tanstack/react-query";
import Image from "next/image";
import { 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>
);

View File

@@ -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"

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

View File

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

View File

@@ -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

View File

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

View File

@@ -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" />

View File

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

View File

@@ -3,19 +3,35 @@ import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import "./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>

View File

@@ -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"

View File

@@ -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 */}

View File

@@ -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>

View File

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

View File

@@ -1,37 +1,53 @@
"use client";
import Link from "next/link";
import 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 */}

View File

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

View File

@@ -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>

View File

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

View File

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

View File

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

View File

@@ -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";

View File

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

View File

@@ -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"

View File

@@ -0,0 +1,155 @@
// import { useTranslations } from "next-intl";
// import Image from "next/image";
// import Link from "next/link";
// interface CatalogProps {
// image: string;
// title: string;
// slug: string;
// description: string;
// id:string;
// }
// export default function CatalogCard({
// image,
// title,
// slug,
// description,
// id,
// }: CatalogProps) {
// const t = useTranslations();
// return (
// <Link
// href={`/products?category=${id}`}
// className="group h-118 flex flex-col items-center justify-start" // Added 'group' here
// >
// <div className="h-full flex flex-col justify-between group-hover:scale-105 transition ease-in-out">
// <p className="text-white text-2xl font-unbounded font-semibold text-center transition-colors">
// {title}
// </p>
// <p className="text-white/50 font-almarai font-medium text-center">
// {t(`${description}`)}
// </p>
// <Image
// src={image}
// alt="image"
// width={400}
// height={90}
// className="h-90! rounded-xl object-contain bg-[#444242] transition-colors duration-300" // Added smooth transition
// />
// </div>
// </Link>
// );
// }
// bg-[#444242]
"use client";
import { useLocale, useTranslations } from "next-intl";
import Image from "next/image";
import Link from "next/link";
import { ArrowUpRight } from "lucide-react";
import { useCategory } from "@/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>
);
}

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

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

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

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

View File

@@ -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";

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

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

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

View File

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

View File

@@ -1,24 +0,0 @@
import ProductCard from "./productCard";
export function Products() {
return (
<div className="bg-[#1e1d1c] py-20">
<div className="max-w-250 mx-auto w-full sm:-mt-50 -mt-30 z-20 relative">
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
{Array(9)
.fill(null)
.map((_, index) => (
<ProductCard
key={index}
title="Elektr yong'in detektori-Ypres ver.2"
name="P-0834404"
image="/images/products/products.webp"
slug="P_0834404"
status="full"
/>
))}
</div>
</div>
</div>
);
}

View File

@@ -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

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

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

View File

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

276
components/priceContact.tsx Normal file
View 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>
);
}

View File

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

View File

@@ -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: "Yonginga 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
View 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
View 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[];
}

View File

@@ -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."
}
}

View File

@@ -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": "Произошла ошибка. Попробуйте снова."
}
}

View File

@@ -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 yongin xavfsizligi tizimlarini ornatish va sertifikatlangan himoya vositalari savdosi boyicha professional xizmatlar korsatamiz.",
"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 yongin ochirish tizimlari va ichki yonginga qarshi suv taminoti uchun polipropilen quvurlar va fitinglar",
"cadescription": "Avtomatik yongin ochirish uchun uskunalar",
"lededescription": "Har qanday maqsaddagi suv quvurlarini ornatish 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 yongin xavfsizligi tizimlarini ornatish va sertifikatlangan himoya vositalari savdosi boyicha professional xizmatlar korsatamiz.",
"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 notogri formatda kiritilgan",
"captchaRequired": "Iltimos, kodni kiriting"
},
"success": "Sorovingiz muvaffaqiyatli yuborildi!",
"error": "Xatolik yuz berdi. Iltimos, qayta urinib koring."
}
}

View File

@@ -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
View File

@@ -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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

35
request/api.ts Normal file
View 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
View 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
View 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
View 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 }),
}));

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