Compare commits
13 Commits
6a89bc1acc
...
4ee9ae3acb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ee9ae3acb | ||
|
|
a7b665b50c | ||
|
|
6fbe23109c | ||
|
|
071685b52c | ||
|
|
f688a01afd | ||
|
|
1ab5c6b741 | ||
|
|
cd86d6397e | ||
|
|
dcdfce4d79 | ||
|
|
625e21394f | ||
|
|
4c2dc6a0f5 | ||
|
|
a9161b16b9 | ||
|
|
6a598ebfd3 | ||
|
|
2706dc387f |
@@ -1,3 +1,4 @@
|
|||||||
|
import { Breadcrumb } from "@/components/breadCrumb";
|
||||||
import Catalog from "@/components/pages/home/blog/catalog";
|
import Catalog from "@/components/pages/home/blog/catalog";
|
||||||
import { ProductBanner } from "@/components/pages/products";
|
import { ProductBanner } from "@/components/pages/products";
|
||||||
import { MainSubCategory } from "@/components/pages/subCategory";
|
import { MainSubCategory } from "@/components/pages/subCategory";
|
||||||
@@ -6,8 +7,11 @@ export default function Page() {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-[#1e1d1c] pb-30">
|
<div className="bg-[#1e1d1c] pb-30">
|
||||||
<ProductBanner />
|
<ProductBanner />
|
||||||
<div className="max-w-300 mx-auto w-full pt-20">
|
<div className="max-w-300 mx-auto w-full pt-5">
|
||||||
<MainSubCategory />
|
<div className="pb-8">
|
||||||
|
<Breadcrumb />
|
||||||
|
</div>
|
||||||
|
<Catalog />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { AlertCircle } from "lucide-react";
|
|||||||
import { LoadingSkeleton } from "@/components/pages/products/slug/loading";
|
import { LoadingSkeleton } from "@/components/pages/products/slug/loading";
|
||||||
import { EmptyState } from "@/components/pages/products/slug/empty";
|
import { EmptyState } from "@/components/pages/products/slug/empty";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
import { Breadcrumb } from "@/components/breadCrumb";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
interface ProductImage {
|
interface ProductImage {
|
||||||
@@ -41,7 +42,7 @@ export default function SlugPage() {
|
|||||||
enabled: !!productZustand.id,
|
enabled: !!productZustand.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(()=>console.log("product detail: ",product))
|
useEffect(() => console.log("product detail: ", product));
|
||||||
|
|
||||||
// Loading State
|
// Loading State
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -55,12 +56,16 @@ export default function SlugPage() {
|
|||||||
|
|
||||||
// Extract images
|
// Extract images
|
||||||
const productImages = product.images?.map((img) => img.image) || [];
|
const productImages = product.images?.map((img) => img.image) || [];
|
||||||
const mainImage = product.images?.find((img) => img.is_main)?.image || productImages[0] || "";
|
const mainImage =
|
||||||
const features = product.features.map((item:any)=>item.name)
|
product.images?.find((img) => img.is_main)?.image || productImages[0] || "";
|
||||||
|
const features = product.features.map((item: any) => item.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#1e1d1c] py-20 md:py-32 lg:py-40 px-4 md:px-8">
|
<div className="min-h-screen bg-[#1e1d1c] px-4 md:px-8">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<div className="pt-30 pb-10">
|
||||||
|
<Breadcrumb />
|
||||||
|
</div>
|
||||||
{/* Main Product Section */}
|
{/* Main Product Section */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 mb-12">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12 mb-12">
|
||||||
{/* Left - Image Slider */}
|
{/* Left - Image Slider */}
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
|
"use client";
|
||||||
|
import { Breadcrumb } from "@/components/breadCrumb";
|
||||||
import { ProductBanner, Products } from "@/components/pages/products";
|
import { ProductBanner, Products } from "@/components/pages/products";
|
||||||
import FilterCatalog from "@/components/pages/products/filter/catalog/filterCatalog";
|
import FilterCatalog from "@/components/pages/products/filter/catalog/filterCatalog";
|
||||||
|
import { useSubCategory } from "@/store/useSubCategory";
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
const subCategory = useSubCategory((state) => state.subCategory);
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#1e1d1c] pb-30">
|
<div className="bg-[#1e1d1c] pb-30">
|
||||||
<ProductBanner />
|
<ProductBanner />
|
||||||
{/* <FilterCatalog /> */}
|
{/* <FilterCatalog /> */}
|
||||||
|
<div className="max-w-300 w-full mx-auto pt-8">
|
||||||
|
<Breadcrumb customLabels={{ subCategory: subCategory.name}} />
|
||||||
|
</div>
|
||||||
<Products />
|
<Products />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Breadcrumb } from "@/components/breadCrumb";
|
||||||
import { ProductBanner } from "@/components/pages/products";
|
import { ProductBanner } from "@/components/pages/products";
|
||||||
import { MainSubCategory } from "@/components/pages/subCategory";
|
import { MainSubCategory } from "@/components/pages/subCategory";
|
||||||
|
|
||||||
@@ -5,7 +6,10 @@ export default function Page() {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-[#1e1d1c] pb-30">
|
<div className="bg-[#1e1d1c] pb-30">
|
||||||
<ProductBanner />
|
<ProductBanner />
|
||||||
<div className="py-20">
|
<div className="pb-20">
|
||||||
|
<div className="max-w-350 mx-auto w-full py-10">
|
||||||
|
<Breadcrumb />
|
||||||
|
</div>
|
||||||
<MainSubCategory />
|
<MainSubCategory />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@@ -157,3 +157,7 @@ body {
|
|||||||
.delay-300 {
|
.delay-300 {
|
||||||
animation-delay: 300ms;
|
animation-delay: 300ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leo{
|
||||||
|
color: #979797;
|
||||||
|
}
|
||||||
@@ -7,8 +7,6 @@ import { getMessages } from "next-intl/server";
|
|||||||
import { InitialLoading } from "@/components/initialLoading/initialLoading";
|
import { InitialLoading } from "@/components/initialLoading/initialLoading";
|
||||||
import { Providers } from "@/components/provider";
|
import { Providers } from "@/components/provider";
|
||||||
|
|
||||||
("info@ignum-tech.com");
|
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@@ -20,10 +18,77 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "FireForce - Emergency Services",
|
title: {
|
||||||
|
default: "Ignum Technologies - Fire Safety Systems Installation & Sales",
|
||||||
|
template: "%s | Ignum Technologies",
|
||||||
|
},
|
||||||
description:
|
description:
|
||||||
"FireForce - Your trusted emergency response team bringing calm amidst chaos",
|
"Ignum Technologies specializes in professional fire safety systems installation and sales. Protect your property with cutting-edge fire detection, suppression, and alarm systems from certified experts.",
|
||||||
generator: "v0.app",
|
keywords: [
|
||||||
|
"fire safety systems",
|
||||||
|
"fire alarm installation",
|
||||||
|
"fire suppression systems",
|
||||||
|
"fire detection",
|
||||||
|
"Ignum Technologies",
|
||||||
|
"fire safety equipment",
|
||||||
|
"fire protection services",
|
||||||
|
"commercial fire systems",
|
||||||
|
"residential fire safety",
|
||||||
|
],
|
||||||
|
authors: [{ name: "Ignum Technologies" }],
|
||||||
|
creator: "Ignum Technologies",
|
||||||
|
publisher: "Ignum Technologies",
|
||||||
|
formatDetection: {
|
||||||
|
email: false,
|
||||||
|
address: false,
|
||||||
|
telephone: false,
|
||||||
|
},
|
||||||
|
metadataBase: new URL("https://ignum-tech.com"), // O'zingizning domen manzilingizni kiriting
|
||||||
|
alternates: {
|
||||||
|
canonical: "/",
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
type: "website",
|
||||||
|
locale: "uz_UZ",
|
||||||
|
url: "https://ignum-tech.com",
|
||||||
|
siteName: "Ignum Technologies",
|
||||||
|
title: "Ignum Technologies - Professional Fire Safety Systems",
|
||||||
|
description:
|
||||||
|
"Leading provider of fire safety systems installation and sales. Comprehensive fire protection solutions including detection, suppression, and alarm systems for commercial and residential properties.",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: "/og-image.jpg", // 1200x630 o'lchamda rasm qo'shing
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: "Ignum Technologies - Fire Safety Systems",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: "/og-image-square.jpg", // 1200x1200 o'lchamda rasm qo'shing
|
||||||
|
width: 1200,
|
||||||
|
height: 1200,
|
||||||
|
alt: "Ignum Technologies Logo",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: "Ignum Technologies - Fire Safety Systems Installation & Sales",
|
||||||
|
description:
|
||||||
|
"Professional fire safety systems installation and sales. Protect your property with certified fire detection, suppression, and alarm solutions.",
|
||||||
|
images: ["/twitter-image.jpg"], // 1200x600 o'lchamda rasm qo'shing
|
||||||
|
creator: "@ignumtech", // Twitter username-ingizni kiriting
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
googleBot: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
"max-video-preview": -1,
|
||||||
|
"max-image-preview": "large",
|
||||||
|
"max-snippet": -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
icons: {
|
icons: {
|
||||||
icon: [
|
icon: [
|
||||||
{
|
{
|
||||||
@@ -41,6 +106,10 @@ export const metadata: Metadata = {
|
|||||||
],
|
],
|
||||||
apple: "/apple-icon.png",
|
apple: "/apple-icon.png",
|
||||||
},
|
},
|
||||||
|
verification: {
|
||||||
|
google: "your-google-verification-code", // Google Search Console verification kodi
|
||||||
|
// yandex: "your-yandex-verification-code", // Agar kerak bo'lsa
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootLayout({
|
export default async function RootLayout({
|
||||||
@@ -52,8 +121,17 @@ export default async function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const messages: any = await getMessages();
|
const messages: any = await getMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale} suppressHydrationWarning>
|
<html lang={locale} suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
{/* Qo'shimcha SEO elementlar */}
|
||||||
|
<link rel="manifest" href="/manifest.json" />
|
||||||
|
<meta name="theme-color" content="#FF4500" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Ignum Tech" />
|
||||||
|
</head>
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
@@ -64,4 +142,4 @@ export default async function RootLayout({
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
195
components/breadCrumb.tsx
Normal file
195
components/breadCrumb.tsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
"use client";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { ChevronRight, Home } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useCategory } from "@/store/useCategory";
|
||||||
|
import { useSubCategory } from "@/store/useSubCategory";
|
||||||
|
import { useProductPageInfo } from "@/store/useProduct";
|
||||||
|
|
||||||
|
interface BreadcrumbProps {
|
||||||
|
customLabels?: Record<string, string>;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Breadcrumb({
|
||||||
|
customLabels = {},
|
||||||
|
className = "",
|
||||||
|
}: BreadcrumbProps) {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const t = useTranslations();
|
||||||
|
const category = useCategory((state) => state.category);
|
||||||
|
const subCategory = useSubCategory((state) => state.subCategory);
|
||||||
|
const product = useProductPageInfo((state) => state.product);
|
||||||
|
console.log("sub category: ", subCategory);
|
||||||
|
|
||||||
|
// Pathdan segments olish
|
||||||
|
const segments = pathname.split("/").filter((segment) => segment !== "");
|
||||||
|
|
||||||
|
// Agar locale bo'lsa, uni olib tashlash (uz, en, ru)
|
||||||
|
const locales = ["uz", "en", "ru"];
|
||||||
|
const filteredSegments = segments.filter(
|
||||||
|
(segment) => !locales.includes(segment),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Agar faqat home page bo'lsa, breadcrumb ko'rsatmaslik
|
||||||
|
if (filteredSegments.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Breadcrumb items yaratish
|
||||||
|
const breadcrumbItems: Array<{
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
isLast: boolean;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
|
// Home qo'shish (har doim birinchi)
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: t("breadcrumb.home") || "Home",
|
||||||
|
href: "/",
|
||||||
|
isLast: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Locale olish
|
||||||
|
const locale = segments.find((seg) => locales.includes(seg)) || "";
|
||||||
|
const localePrefix = locale ? `/${locale}` : "";
|
||||||
|
|
||||||
|
// Segmentlarni tahlil qilish
|
||||||
|
filteredSegments.forEach((segment, index) => {
|
||||||
|
const isLast = index === filteredSegments.length - 1;
|
||||||
|
|
||||||
|
if (segment === "catalog_page") {
|
||||||
|
// Catalog_page - asosiy kategoriyalar sahifasi
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: t("breadcrumb.catalog_page") || "Katalog",
|
||||||
|
href: `${localePrefix}/catalog_page`,
|
||||||
|
isLast: isLast,
|
||||||
|
});
|
||||||
|
} else if (segment === "subCategory") {
|
||||||
|
// SubCategory - kategoriya nomi ko'rsatiladi
|
||||||
|
if (category?.name) {
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: category.name,
|
||||||
|
href: `${localePrefix}/catalog_page/subCategory`,
|
||||||
|
isLast: isLast,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (segment === "products") {
|
||||||
|
if (subCategory?.name) {
|
||||||
|
// Agar subCategory orqali kelgan bo'lsa
|
||||||
|
// 1. Kategoriya
|
||||||
|
if (category?.name) {
|
||||||
|
const categoryInBreadcrumb = breadcrumbItems.find(
|
||||||
|
(item) => item.label === category.name,
|
||||||
|
);
|
||||||
|
if (!categoryInBreadcrumb) {
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: category.name,
|
||||||
|
href: `${localePrefix}/catalog_page/subCategory`,
|
||||||
|
isLast: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. SubKategoriya
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: subCategory.name,
|
||||||
|
href: `${localePrefix}/catalog_page/subCategory`,
|
||||||
|
isLast: false,
|
||||||
|
});
|
||||||
|
} else if (category?.name) {
|
||||||
|
// To'g'ridan-to'g'ri kategoriyadan products ga kelgan
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: category.name,
|
||||||
|
href: `${localePrefix}/catalog_page`,
|
||||||
|
isLast: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (segment.startsWith("[") && segment.endsWith("]")) {
|
||||||
|
// Dynamic route (masalan, [slug])
|
||||||
|
// Custom label yoki default
|
||||||
|
const slugValue = segment.replace(/\[|\]/g, "");
|
||||||
|
const label = customLabels[slugValue] || slugValue;
|
||||||
|
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: label,
|
||||||
|
href: `${localePrefix}/${filteredSegments.slice(0, index + 1).join("/")}`,
|
||||||
|
isLast: isLast,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Boshqa segmentlar
|
||||||
|
const label = getLabel(segment);
|
||||||
|
breadcrumbItems.push({
|
||||||
|
label: label,
|
||||||
|
href: `${localePrefix}/${filteredSegments.slice(0, index + 1).join("/")}`,
|
||||||
|
isLast: isLast,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default label translator
|
||||||
|
function getLabel(segment: string): string {
|
||||||
|
// Agar custom label berilgan bo'lsa
|
||||||
|
if (customLabels[segment]) {
|
||||||
|
return customLabels[segment];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agar translation mavjud bo'lsa
|
||||||
|
try {
|
||||||
|
if (segment === "special_product") {
|
||||||
|
return product.name;
|
||||||
|
}
|
||||||
|
return t(`breadcrumb.${segment}`);
|
||||||
|
} catch {
|
||||||
|
// Aks holda, segment nomini formatlash
|
||||||
|
return segment
|
||||||
|
.replace(/-/g, " ")
|
||||||
|
.replace(/_/g, " ")
|
||||||
|
.split(" ")
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav aria-label="Breadcrumb" className={`py-4 ${className} sm:px-5 px-2`}>
|
||||||
|
<ol className="flex items-center flex-wrap gap-2 sm:text-xl text-lg">
|
||||||
|
{breadcrumbItems.map((item, index) => (
|
||||||
|
<li
|
||||||
|
key={`${item.label}-${index}`}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{index > 0 && (
|
||||||
|
<ChevronRight className="w-4 h-4 text-gray-400 dark:text-gray-600" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{index === 0 ? (
|
||||||
|
// Home link with icon
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
className="flex items-center gap-1.5 text-gray-600 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-500 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
<Home className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
) : item.isLast ? (
|
||||||
|
// Last item (current page)
|
||||||
|
<span className="text-gray-900 dark:text-white font-medium line-clamp-1">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
// Regular link
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
className="text-gray-600 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-500 transition-colors duration-200 line-clamp-1"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,20 +23,23 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
}
|
|
||||||
|
|
||||||
.initial-svg {
|
|
||||||
width: 250px;
|
width: 250px;
|
||||||
height: 250px;
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.initial-loading-content svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
animation: initialFloat 2s ease-in-out infinite, initialScale 1.5s ease-in-out infinite;
|
animation: initialFloat 2s ease-in-out infinite, initialScale 1.5s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.initial-path {
|
/* SVG path'larga stil - tiniq va aniq */
|
||||||
fill: url(#initial-gradient);
|
.logo-path {
|
||||||
filter: url(#initial-glow);
|
fill: url(#neon-gradient);
|
||||||
stroke: #ffffff;
|
stroke: #ffffff;
|
||||||
stroke-width: 2;
|
stroke-width: 0.3;
|
||||||
animation: pathPulse 1.5s ease-in-out infinite;
|
filter: drop-shadow(0 0 3px rgba(255, 255, 255, 0.3));
|
||||||
|
animation: pathPulse 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loading dots animation */
|
/* Loading dots animation */
|
||||||
@@ -53,8 +56,9 @@
|
|||||||
.loading-dots span {
|
.loading-dots span {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
background: #ff0000;
|
background: linear-gradient(135deg, #e0e0e0 0%, #ffffff 50%, #c0c0c0 100%);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 5px rgba(255, 255, 255, 0.3);
|
||||||
animation: dotBounce 1.4s ease-in-out infinite;
|
animation: dotBounce 1.4s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +80,7 @@
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
transform: translateY(-30px);
|
transform: translateY(-15px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,18 +89,18 @@
|
|||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
transform: scale(1.1);
|
transform: scale(1.03);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pathPulse {
|
@keyframes pathPulse {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
stroke-width: 2;
|
filter: drop-shadow(0 0 3px rgba(255, 255, 255, 0.3));
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
opacity: 0.7;
|
opacity: 0.95;
|
||||||
stroke-width: 3;
|
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.4));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,4 +118,24 @@
|
|||||||
/* Smooth scroll prevention */
|
/* Smooth scroll prevention */
|
||||||
body:has(.initial-loading:not(.fade-out)) {
|
body:has(.initial-loading:not(.fade-out)) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.initial-loading-content {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.initial-loading-content {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-dots span {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,18 +3,18 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Mail, Phone, MapPin } from "lucide-react";
|
import { Mail, Phone, MapPin, Instagram, Send } from "lucide-react";
|
||||||
import { useLocale, useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import axios from "axios";
|
|
||||||
import httpClient from "@/request/api";
|
import httpClient from "@/request/api";
|
||||||
import { endPoints } from "@/request/links";
|
import { endPoints } from "@/request/links";
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer() {
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
const [errors, setErrors] = useState<any>({});
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [subscribed, setSubscribed] = useState(false);
|
const [subscribed, setSubscribed] = useState(false);
|
||||||
@@ -34,6 +34,30 @@ export function Footer() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formatPhoneNumber = (value: string) => {
|
||||||
|
const numbers = value.replace(/\D/g, "");
|
||||||
|
if (!numbers.startsWith("998")) {
|
||||||
|
return "+998 ";
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted = "+998 ";
|
||||||
|
const rest = numbers.slice(3);
|
||||||
|
if (rest.length > 0) formatted += rest.slice(0, 2);
|
||||||
|
if (rest.length > 2) formatted += " " + rest.slice(2, 5);
|
||||||
|
if (rest.length > 5) formatted += " " + rest.slice(5, 7);
|
||||||
|
if (rest.length > 7) formatted += " " + rest.slice(7, 9);
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const formatted = formatPhoneNumber(e.target.value);
|
||||||
|
setEmail(formatted);
|
||||||
|
if (errors.address) {
|
||||||
|
setErrors({ ...errors, address: "" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubscribe = (e: React.FormEvent) => {
|
const handleSubscribe = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (email) {
|
if (email) {
|
||||||
@@ -52,7 +76,7 @@ export function Footer() {
|
|||||||
{/* Newsletter Section for gitea */}
|
{/* Newsletter Section for gitea */}
|
||||||
<div className=" absolute w-full -top-40 px-4 py-12 md:py-16">
|
<div className=" absolute w-full -top-40 px-4 py-12 md:py-16">
|
||||||
<div className="mx-auto max-w-6xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
<div className="rounded-2xl bg-[#fa1d1d] px-6 py-8 md:flex lg:flex-row flex-col max-lg:gap-5 md:items-center lg:justify-between justify-center md:px-10 md:py-12">
|
<div className="rounded-2xl bg-red-600 px-6 py-8 md:flex lg:flex-row flex-col max-lg:gap-5 md:items-center lg:justify-between justify-center md:px-10 md:py-12">
|
||||||
<div className="mb-8 md:mb-0">
|
<div className="mb-8 md:mb-0">
|
||||||
<h2 className="font-unbounded text-2xl font-bold text-white md:text-3xl">
|
<h2 className="font-unbounded text-2xl font-bold text-white md:text-3xl">
|
||||||
{t("contactTitle")}
|
{t("contactTitle")}
|
||||||
@@ -67,14 +91,16 @@ export function Footer() {
|
|||||||
className="flex sm:flex-row flex-col w-full gap-2 md:w-auto"
|
className="flex sm:flex-row flex-col w-full gap-2 md:w-auto"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="tel"
|
||||||
placeholder={t("enterPhone")}
|
id="phone"
|
||||||
|
name="phone"
|
||||||
value={email}
|
value={email}
|
||||||
minLength={9}
|
onChange={handlePhoneChange}
|
||||||
maxLength={13}
|
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
errors.email ? "border-red-500" : "border-transparent"
|
||||||
className="font-almarai flex-1 rounded-full bg-white px-6 py-3 text-gray-800 placeholder-gray-400 focus:outline-none md:w-64"
|
}`}
|
||||||
required
|
placeholder="+998 90 123 45 67"
|
||||||
|
maxLength={17}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -107,6 +133,20 @@ export function Footer() {
|
|||||||
<p className="font-almarai text-sm leading-relaxed text-gray-300">
|
<p className="font-almarai text-sm leading-relaxed text-gray-300">
|
||||||
{t("footer.description")}
|
{t("footer.description")}
|
||||||
</p>
|
</p>
|
||||||
|
<div className="flex items-center gap-5 mt-5">
|
||||||
|
{/* <a
|
||||||
|
href=""
|
||||||
|
className="p-2 rounded-md bg-gray-700 hover:bg-red-500"
|
||||||
|
>
|
||||||
|
<Instagram />
|
||||||
|
</a> */}
|
||||||
|
<a
|
||||||
|
href="https://t.me/ignum_tech"
|
||||||
|
className="p-2 rounded-md bg-gray-700 hover:bg-red-500"
|
||||||
|
>
|
||||||
|
<Send />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick Links */}
|
{/* Quick Links */}
|
||||||
@@ -187,9 +227,7 @@ export function Footer() {
|
|||||||
</li>
|
</li>
|
||||||
<li className="flex items-start gap-3">
|
<li className="flex items-start gap-3">
|
||||||
<MapPin className="mt-1 h-5 w-5 shrink-0 text-white" />
|
<MapPin className="mt-1 h-5 w-5 shrink-0 text-white" />
|
||||||
<span>
|
<span>{t("footer.address")}</span>
|
||||||
{t("footer.address")}
|
|
||||||
</span>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -200,16 +238,16 @@ export function Footer() {
|
|||||||
{/* Copyright Section */}
|
{/* Copyright Section */}
|
||||||
<div className="border-t border-gray-800 px-4 py-8">
|
<div className="border-t border-gray-800 px-4 py-8">
|
||||||
<div className="mx-auto max-w-6xl">
|
<div className="mx-auto max-w-6xl">
|
||||||
<div className="font-almarai flex flex-col justify-between gap-4 text-sm text-gray-400 md:flex-row md:items-center">
|
<div className="font-almarai flex flex-col justify-center gap-4 text-lg text-gray-400 md:flex-row md:items-center">
|
||||||
<div>Copyright © 2025 Ignum Company.</div>
|
<a href="http://felix-its.uz/" className="hover:text-red-600">{t("footer.create", { name: "Felix-its.uz" })}</a>
|
||||||
<div className="flex gap-6">
|
{/* <div className="flex gap-6">
|
||||||
<a href="#terms" className="hover:text-white">
|
<a href="#terms" className="hover:text-white">
|
||||||
Terms & Conditions
|
Terms & Conditions
|
||||||
</a>
|
</a>
|
||||||
<a href="#privacy" className="hover:text-white">
|
<a href="#privacy" className="hover:text-white">
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -103,7 +103,8 @@ export function Navbar() {
|
|||||||
</span>
|
</span>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-white text-sm font-bold">
|
<div className="text-white text-sm font-bold">
|
||||||
<div>+998-98-099-21-21</div> <div>+998-77-372-21-21</div>
|
<div>+998-77-372-21-21</div>
|
||||||
|
<div>+998-98-099-21-21</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ export default function Form() {
|
|||||||
|
|
||||||
const formRequest = useMutation({
|
const formRequest = useMutation({
|
||||||
mutationKey: [],
|
mutationKey: [],
|
||||||
mutationFn: (data: FormData) => httpClient.post(endPoints.post.contact, data),
|
mutationFn: (data: FormData) =>
|
||||||
|
httpClient.post(endPoints.post.contact, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setSubmitStatus("success");
|
setSubmitStatus("success");
|
||||||
setFormData({
|
setFormData({
|
||||||
@@ -78,10 +79,13 @@ export default function Form() {
|
|||||||
if (!formData.surname.trim()) {
|
if (!formData.surname.trim()) {
|
||||||
newErrors.surname = "Last name is required";
|
newErrors.surname = "Last name is required";
|
||||||
}
|
}
|
||||||
if (!formData.address.trim()) {
|
const phoneNumbers = formData.address.replace(/\D/g, "");
|
||||||
newErrors.address = "address is required";
|
if (phoneNumbers.length !== 12) {
|
||||||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.address)) {
|
newErrors.address =
|
||||||
newErrors.address = "Please enter a valid address";
|
t("validation.phoneRequired") || "To'liq telefon raqam kiriting";
|
||||||
|
} else if (!phoneNumbers.startsWith("998")) {
|
||||||
|
newErrors.address =
|
||||||
|
t("validation.phoneInvalid") || "Noto'g'ri telefon raqam";
|
||||||
}
|
}
|
||||||
if (!formData.theme.trim()) {
|
if (!formData.theme.trim()) {
|
||||||
newErrors.theme = "theme is required";
|
newErrors.theme = "theme is required";
|
||||||
@@ -97,6 +101,30 @@ export default function Form() {
|
|||||||
return Object.keys(newErrors).length === 0;
|
return Object.keys(newErrors).length === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatPhoneNumber = (value: string) => {
|
||||||
|
const numbers = value.replace(/\D/g, "");
|
||||||
|
if (!numbers.startsWith("998")) {
|
||||||
|
return "+998 ";
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted = "+998 ";
|
||||||
|
const rest = numbers.slice(3);
|
||||||
|
if (rest.length > 0) formatted += rest.slice(0, 2);
|
||||||
|
if (rest.length > 2) formatted += " " + rest.slice(2, 5);
|
||||||
|
if (rest.length > 5) formatted += " " + rest.slice(5, 7);
|
||||||
|
if (rest.length > 7) formatted += " " + rest.slice(7, 9);
|
||||||
|
|
||||||
|
return formatted;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhoneChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const formatted = formatPhoneNumber(e.target.value);
|
||||||
|
setFormData({ ...formData, address: formatted });
|
||||||
|
if (errors.address) {
|
||||||
|
setErrors({ ...errors, address: "" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
||||||
) => {
|
) => {
|
||||||
@@ -169,19 +197,19 @@ export default function Form() {
|
|||||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
type="address"
|
type="tel"
|
||||||
name="address"
|
id="phone"
|
||||||
placeholder={t("contact.form.placeholders.email")}
|
name="phone"
|
||||||
value={formData.address}
|
value={formData.address}
|
||||||
onChange={handleChange}
|
onChange={handlePhoneChange}
|
||||||
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
className={`font-almarai w-full rounded-3xl border bg-white px-4 py-3 text-sm text-gray-700 placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-red-500 ${
|
||||||
errors.address ? "border-red-500" : "border-transparent"
|
errors.address ? "border-red-500" : "border-transparent"
|
||||||
}`}
|
}`}
|
||||||
|
placeholder="+998 90 123 45 67"
|
||||||
|
maxLength={17}
|
||||||
/>
|
/>
|
||||||
{errors.address && (
|
{errors.address && (
|
||||||
<p className="font-almarai mt-1 text-xs text-red-500">
|
<p className="mt-1 text-sm text-red-500">{errors.address}</p>
|
||||||
{errors.address}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function Contact() {
|
|||||||
className="absolute inset-0"
|
className="absolute inset-0"
|
||||||
style={{
|
style={{
|
||||||
background:
|
background:
|
||||||
"radial-gradient(ellipse at bottom center, #d2610a 0%, #1e1d1ce9 70% , #1e1d1ce9 70%)",
|
"radial-gradient(at center bottom, rgb(144 74 20) 0%, rgba(30, 29, 28, 0.914) 70%, rgba(30, 29, 28, 0.914) 70%)",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Flame, Building2, Ambulance } from "lucide-react";
|
import { Flame, Building2, Ambulance } from "lucide-react";
|
||||||
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
import DotAnimatsiya from "@/components/dot/DotAnimatsiya";
|
||||||
import { useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
interface ServiceItem {
|
interface ServiceItem {
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
@@ -16,6 +15,7 @@ interface ServiceItem {
|
|||||||
|
|
||||||
export function AboutUs() {
|
export function AboutUs() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const locale = useLocale();
|
||||||
const services: ServiceItem[] = [
|
const services: ServiceItem[] = [
|
||||||
{
|
{
|
||||||
icon: <Flame width={40} height={40} className="text-red-500" />,
|
icon: <Flame width={40} height={40} className="text-red-500" />,
|
||||||
@@ -76,9 +76,9 @@ export function AboutUs() {
|
|||||||
|
|
||||||
{/* Button */}
|
{/* Button */}
|
||||||
<div>
|
<div>
|
||||||
<Button className="font-almarai bg-red-600 hover:bg-red-700 text-white font-bold px-8 py-3 rounded-full transition-colors duration-300 shadow-[0px_0px_2px_8px_#ff01015c]">
|
<Link href={`/${locale}/about`} className="font-almarai bg-red-600 hover:bg-red-700 text-white font-bold px-8 py-3 rounded-full transition-colors duration-300 shadow-[0px_0px_2px_8px_#ff01015c]">
|
||||||
{t("home.about.title")}
|
{t("home.about.title")}
|
||||||
</Button>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useTranslations } from "next-intl";
|
import { useLocale, useTranslations } from "next-intl";
|
||||||
import DotAnimatsiya from "../../dot/DotAnimatsiya";
|
import DotAnimatsiya from "../../dot/DotAnimatsiya";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export function Banner() {
|
export function Banner() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const locale = useLocale();
|
||||||
return (
|
return (
|
||||||
<section className="relative w-full lg:h-[86vh] h-screen min-h-150 overflow-hidden pt-20">
|
<section className="relative w-full lg:h-[86vh] h-screen min-h-150 overflow-hidden pt-20">
|
||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
@@ -77,8 +79,10 @@ export function Banner() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Heading */}
|
{/* Main Heading */}
|
||||||
<h1 className="font-unbounded uppercase text-4xl bg-linear-to-br from-white via-white to-black
|
<h1
|
||||||
text-transparent bg-clip-text sm:text-5xl lg:text-6xl font-bold leading-tight text-pretty">
|
className="font-unbounded uppercase text-4xl bg-linear-to-br from-white via-white to-black
|
||||||
|
text-transparent bg-clip-text sm:text-5xl lg:text-6xl font-bold leading-tight text-pretty"
|
||||||
|
>
|
||||||
{t("home.banner.title2")}
|
{t("home.banner.title2")}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -88,9 +92,12 @@ export function Banner() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* CTA Button */}
|
{/* CTA Button */}
|
||||||
<button className="font-almarai shadow-[0px_0px_2px_8px_#ff01015c] bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-8 rounded-full transition duration-300 transform hover:scale-105 w-fit">
|
<Link
|
||||||
|
href={`/${locale}/contact`}
|
||||||
|
className="font-almarai shadow-[0px_0px_2px_8px_#ff01015c] bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-8 rounded-full transition duration-300 transform hover:scale-105 w-fit"
|
||||||
|
>
|
||||||
{t("home.banner.cta")}
|
{t("home.banner.cta")}
|
||||||
</button>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,14 +2,13 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import httpClient from "@/request/api";
|
import httpClient from "@/request/api";
|
||||||
import { endPoints } from "@/request/links";
|
import { endPoints } from "@/request/links";
|
||||||
import { useEffect } from "react";
|
|
||||||
import CatalogCard from "../../products/catalog";
|
import CatalogCard from "../../products/catalog";
|
||||||
import CatalogCardSkeleton from "@/components/loadingSkleton";
|
import CatalogCardSkeleton from "@/components/loadingSkleton";
|
||||||
import EmptyData from "@/components/EmptyData";
|
import EmptyData from "@/components/EmptyData";
|
||||||
import { getRouteLang } from "@/request/getLang";
|
import { getRouteLang } from "@/request/getLang";
|
||||||
import { CategoryType } from "@/lib/types";
|
import { CategoryType } from "@/lib/types";
|
||||||
|
|
||||||
export default function Catalog() {
|
export default function Catalog() {
|
||||||
const language = getRouteLang();
|
const language = getRouteLang();
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ["category", language],
|
queryKey: ["category", language],
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ export function Line() {
|
|||||||
>
|
>
|
||||||
<Phone className="text-white w-5 h-5" />
|
<Phone className="text-white w-5 h-5" />
|
||||||
</span>
|
</span>
|
||||||
+123-456-7890
|
+998-77-372-21-21
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Image
|
<Image
|
||||||
src="/images/home/fireHydrant.png"
|
src="/images/home/balon.png"
|
||||||
alt="image"
|
alt="image"
|
||||||
width={60}
|
width={60}
|
||||||
height={60}
|
height={60}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function OurService() {
|
|||||||
|
|
||||||
{/* cards */}
|
{/* cards */}
|
||||||
<div className="max-w-250 w-full mx-auto flex sm:flex-row flex-col items-center gap-5 my-10">
|
<div className="max-w-250 w-full mx-auto flex sm:flex-row flex-col items-center gap-5 my-10">
|
||||||
<div className="relative space-y-4 py-6 px-8 rounded-xl sm:w-[55%] w-full bg-[linear-gradient(to_bottom_right,#000000,#000000,#000000,#d2610a)]">
|
<div className="relative space-y-4 py-6 px-8 rounded-xl sm:w-[55%] w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)]">
|
||||||
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
||||||
{t("home.services.services.operation.title")}
|
{t("home.services.services.operation.title")}
|
||||||
</p>
|
</p>
|
||||||
@@ -43,7 +43,7 @@ export function OurService() {
|
|||||||
className="object-contain sm:absolute bottom-0 right-2 z-10"
|
className="object-contain sm:absolute bottom-0 right-2 z-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative overflow-hidden space-y-4 py-6 px-8 rounded-xl sm:w-[45%] w-full bg-[linear-gradient(to_bottom_right,#000000,#000000,#000000,#d2610a)]">
|
<div className="relative overflow-hidden space-y-4 py-6 px-8 rounded-xl sm:w-[45%] w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)]">
|
||||||
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
||||||
{t("home.services.services.suppression.title")}
|
{t("home.services.services.suppression.title")}
|
||||||
</p>
|
</p>
|
||||||
@@ -64,7 +64,7 @@ export function OurService() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-250 flex sm:flex-row flex-col items-start justify-between gap-5 mt-5 w-full mx-auto">
|
<div className="max-w-250 flex sm:flex-row flex-col items-start justify-between gap-5 mt-5 w-full mx-auto">
|
||||||
<div className="relative rounded-xl sm:w-[40%] w-full bg-[linear-gradient(to_bottom_right,#d2610a,#000000,#000000)]">
|
<div className="relative rounded-xl sm:w-[40%] w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)]">
|
||||||
<Image
|
<Image
|
||||||
src="/images/home/ambulance.png"
|
src="/images/home/ambulance.png"
|
||||||
alt="images"
|
alt="images"
|
||||||
@@ -85,7 +85,7 @@ export function OurService() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:w-[60%] w-full">
|
<div className="sm:w-[60%] w-full">
|
||||||
<div className="relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#000000,#000000,#d2610a)]">
|
<div className="relative overflow-hidden space-y-4 py-6 px-8 rounded-xl w-full bg-[linear-gradient(to_bottom_right,#000000,#190b00,#542604,#8f4308)]">
|
||||||
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
<p className="uppercase font-unbounded font-bold bg-linear-to-br from-white via-white to-black text-transparent bg-clip-text">
|
||||||
{t("home.services.services.monitoring.title")}
|
{t("home.services.services.monitoring.title")}
|
||||||
</p>
|
</p>
|
||||||
@@ -103,7 +103,7 @@ export function OurService() {
|
|||||||
className="object-contain sm:absolute -bottom-20 -right-4 max-sm:-mb-20 z-10"
|
className="object-contain sm:absolute -bottom-20 -right-4 max-sm:-mb-20 z-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="py-8 px-8 rounded-xl mt-5 w-full p-5 bg-[linear-gradient(to_top_right,#000000,#000000,#d2610a)] flex sm:flex-row flex-col gap-5 items-center justify-between">
|
<div className="py-8 px-8 rounded-xl mt-5 w-full p-5 bg-[linear-gradient(to_top_right,#000000,#190b00,#542604,#8f4308)] flex sm:flex-row flex-col gap-5 items-center justify-between">
|
||||||
<h2 className="font-unbounded sm:text-3xl text-xl font-semibold font-armanai text-white">
|
<h2 className="font-unbounded sm:text-3xl text-xl font-semibold font-armanai text-white">
|
||||||
{t("home.services.viewMoreServices")}
|
{t("home.services.viewMoreServices")}
|
||||||
</h2>
|
</h2>
|
||||||
|
|||||||
@@ -84,14 +84,14 @@ export default function CatalogCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const navigateLink = have_sub_category
|
const navigateLink = have_sub_category
|
||||||
? `/${locale}/catalog_page?category=${id}`
|
? `/${locale}/catalog_page/subCategory?category=${id}`
|
||||||
: `/${locale}/products?category=${id}`;
|
: `/${locale}/catalog_page/products?category=${id}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={navigateLink}
|
href={navigateLink}
|
||||||
onClick={updateZustands}
|
onClick={updateZustands}
|
||||||
className="group relative h-112.5 w-full overflow-hidden rounded-2xl bg-[#17161679] from-[#444242] to-black border hover:border-red-700 border-white/10 transition-all duration-500 hover:-translate-y-1"
|
className="group relative h-112.5 w-full overflow-hidden rounded-2xl bg-[#171616] from-[#2a2a2a] to-black border hover:border-red-700 border-white/10 transition-all duration-500 hover:-translate-y-1"
|
||||||
>
|
>
|
||||||
{/* Background glow effect */}
|
{/* Background glow effect */}
|
||||||
<div className="absolute inset-0 bg-linear-to-t from-red-600/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
<div className="absolute inset-0 bg-linear-to-t from-red-600/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { endPoints } from "@/request/links";
|
|||||||
import { useCategory } from "@/store/useCategory";
|
import { useCategory } from "@/store/useCategory";
|
||||||
import { useSubCategory } from "@/store/useSubCategory";
|
import { useSubCategory } from "@/store/useSubCategory";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Check } from "lucide-react";
|
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function Filter() {
|
export default function Filter() {
|
||||||
@@ -14,11 +14,19 @@ export default function Filter() {
|
|||||||
const toggleFilter = useFilter((state) => state.toggleFilter);
|
const toggleFilter = useFilter((state) => state.toggleFilter);
|
||||||
const hasData = useFilter((state) => state.hasFilter);
|
const hasData = useFilter((state) => state.hasFilter);
|
||||||
const category = useCategory((state) => state.category);
|
const category = useCategory((state) => state.category);
|
||||||
|
const setCategory = useCategory((state) => state.setCategory);
|
||||||
const subCategory = useSubCategory((state) => state.subCategory);
|
const subCategory = useSubCategory((state) => state.subCategory);
|
||||||
|
const setSubCategory = useSubCategory((state) => state.setSubCategory);
|
||||||
|
const clearSubCategory = useSubCategory((state) => state.clearSubCategory);
|
||||||
|
|
||||||
const [dataExpanded, setDataExpanded] = useState<boolean>(false);
|
const [dataExpanded, setDataExpanded] = useState<boolean>(false);
|
||||||
const [numberExpanded, setNumberExpanded] = useState<boolean>(false);
|
const [numberExpanded, setNumberExpanded] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// ⭐ YANGI: Dropdown state'lar - har bir kategoriya uchun
|
||||||
|
const [openDropdowns, setOpenDropdowns] = useState<Record<number, boolean>>(
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
const [catalogData, setCatalogData] = useState<
|
const [catalogData, setCatalogData] = useState<
|
||||||
{ id: number; name: string; type: string }[]
|
{ id: number; name: string; type: string }[]
|
||||||
>(result[0].items);
|
>(result[0].items);
|
||||||
@@ -26,11 +34,40 @@ export default function Filter() {
|
|||||||
{ id: number; name: string; type: string }[]
|
{ id: number; name: string; type: string }[]
|
||||||
>(result[1].items);
|
>(result[1].items);
|
||||||
|
|
||||||
const { data: catalog } = useQuery({
|
// Category data
|
||||||
queryKey: ["catalog"],
|
const { data: categoryBack } = useQuery({
|
||||||
queryFn: () => httpClient(endPoints.filter.catalogCategoryId(category.id)),
|
queryKey: ["category"],
|
||||||
|
queryFn: () => httpClient(endPoints.category.all),
|
||||||
select: (data) => {
|
select: (data) => {
|
||||||
const catalogData = data?.data?.results;
|
console.log("category data on filter: ", data?.data?.results);
|
||||||
|
return data?.data?.results;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ⭐ O'ZGARTIRILDI: subCategory so'rovi faqat dropdown ochilganda va have_sub_category true bo'lganda
|
||||||
|
const { data: subCategoryBack, isLoading: subCategoryLoading } = useQuery({
|
||||||
|
queryKey: ["subCategory", category.id],
|
||||||
|
queryFn: () => httpClient(endPoints.subCategory.byId(category.id)),
|
||||||
|
// ⭐ YANGI: Faqat category tanlangan va have_sub_category true bo'lsa ishlaydi
|
||||||
|
enabled:
|
||||||
|
!!category.id &&
|
||||||
|
category.have_sub_category === true &&
|
||||||
|
openDropdowns[category.id] === true,
|
||||||
|
select: (data) => {
|
||||||
|
console.log("subCategory filter data: ", data?.data?.results);
|
||||||
|
return data?.data?.results;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ⭐ O'ZGARTIRILDI: Catalog va Size query'lari category yoki subCategory ID'ga qarab ishlaydi
|
||||||
|
const activeId = subCategory.id || category.id;
|
||||||
|
|
||||||
|
const { data: catalog } = useQuery({
|
||||||
|
queryKey: ["catalog", activeId],
|
||||||
|
queryFn: () => httpClient(endPoints.filter.catalogCategoryId(activeId)),
|
||||||
|
enabled: !!activeId,
|
||||||
|
select: (data) => {
|
||||||
|
const catalogData = data?.data?.results || [];
|
||||||
return catalogData.map((item: any) => ({
|
return catalogData.map((item: any) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
@@ -40,10 +77,11 @@ export default function Filter() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { data: size } = useQuery({
|
const { data: size } = useQuery({
|
||||||
queryKey: ["size"],
|
queryKey: ["size", activeId],
|
||||||
queryFn: () => httpClient(endPoints.filter.sizeCategoryId(category.id)),
|
queryFn: () => httpClient(endPoints.filter.sizeCategoryId(activeId)),
|
||||||
|
enabled: !!activeId,
|
||||||
select: (data) => {
|
select: (data) => {
|
||||||
const sizedata = data?.data?.results;
|
const sizedata = data?.data?.results || [];
|
||||||
return sizedata.map((item: any) => ({
|
return sizedata.map((item: any) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
@@ -67,20 +105,140 @@ export default function Filter() {
|
|||||||
? sizeData
|
? sizeData
|
||||||
: sizeData.slice(0, 10);
|
: sizeData.slice(0, 10);
|
||||||
|
|
||||||
|
// ⭐ O'ZGARTIRILDI: Category bosilganda dropdown toggle qilish
|
||||||
|
const handleCategoryClick = (item: any) => {
|
||||||
|
if (item.have_sub_category) {
|
||||||
|
// Agar subCategory bo'lsa, dropdown ochish/yopish
|
||||||
|
setOpenDropdowns((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[item.id]: !prev[item.id],
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Category'ni set qilish (filterlar yangilanishi uchun)
|
||||||
|
setCategory(item);
|
||||||
|
|
||||||
|
// SubCategory'ni tozalash (yangisini tanlash uchun)
|
||||||
|
if (!openDropdowns[item.id]) {
|
||||||
|
clearSubCategory();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Agar subCategory bo'lmasa, to'g'ridan-to'g'ri category ni set qilish
|
||||||
|
setCategory(item);
|
||||||
|
clearSubCategory();
|
||||||
|
|
||||||
|
// Barcha dropdown'larni yopish
|
||||||
|
setOpenDropdowns({});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ⭐ YANGI: SubCategory bosilganda
|
||||||
|
const handleSubCategoryClick = (item: any) => {
|
||||||
|
setSubCategory(item);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3 lg:max-w-70 lg:px-0 px-3 w-full text-white">
|
<div className="space-y-3 lg:max-w-70 lg:px-0 px-3 w-full text-white ">
|
||||||
|
{/* ⭐ O'ZGARTIRILDI: Category filter with dropdown */}
|
||||||
|
<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">
|
||||||
|
Kategoriyalar
|
||||||
|
</p>
|
||||||
|
<div className="lg:space-y-2 space-x-6 lg:p-2 p-5 flex lg:flex-col overflow-x-auto lg:overflow-x-hidden items-start justify-start w-full">
|
||||||
|
{categoryBack?.map((item: any) => (
|
||||||
|
<div key={item.id} className="w-full">
|
||||||
|
{/* Main Category */}
|
||||||
|
<div
|
||||||
|
onClick={() => handleCategoryClick(item)}
|
||||||
|
className="hover:cursor-pointer flex items-center gap-2 w-auto shrink-0 hover:bg-gray-600 lg:p-2 rounded transition-colors"
|
||||||
|
>
|
||||||
|
{/* Checkbox yoki Dropdown icon */}
|
||||||
|
{!item.have_sub_category ? (
|
||||||
|
<span
|
||||||
|
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition ${
|
||||||
|
category.id === item.id
|
||||||
|
? "border-red-600 bg-red-600"
|
||||||
|
: "border-gray-400 bg-transparent"
|
||||||
|
}`}
|
||||||
|
aria-label="Filter checkbox"
|
||||||
|
>
|
||||||
|
{category.id === item.id && (
|
||||||
|
<Check className="h-3 w-3 text-white" strokeWidth={3} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className={`flex h-5 w-5 items-center justify-center rounded transition ${
|
||||||
|
openDropdowns[item.id] ? "text-red-500" : "text-gray-400"
|
||||||
|
}`}
|
||||||
|
aria-label="Dropdown icon"
|
||||||
|
>
|
||||||
|
{openDropdowns[item.id] ? (
|
||||||
|
<ChevronUp className="h-4 w-4" strokeWidth={2.5} />
|
||||||
|
) : (
|
||||||
|
<ChevronDown className="h-4 w-4" strokeWidth={2.5} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="whitespace-nowrap font-medium">{item.name}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ⭐ YANGI: SubCategory Dropdown */}
|
||||||
|
{item.have_sub_category && openDropdowns[item.id] && (
|
||||||
|
<div className="ml-7 mt-2 space-y-2 border-l-2 border-gray-400 pl-3">
|
||||||
|
{subCategoryLoading ? (
|
||||||
|
<p className="text-sm text-gray-300">Yuklanmoqda...</p>
|
||||||
|
) : subCategoryBack && subCategoryBack.length > 0 ? (
|
||||||
|
subCategoryBack.map((subItem: any) => (
|
||||||
|
<div
|
||||||
|
key={subItem.id}
|
||||||
|
onClick={() => handleSubCategoryClick(subItem)}
|
||||||
|
className="hover:cursor-pointer flex items-center gap-2 hover:bg-gray-600 p-1.5 rounded transition-colors"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`flex h-4 w-4 items-center justify-center rounded border-2 transition ${
|
||||||
|
subCategory.id === subItem.id
|
||||||
|
? "border-red-600 bg-red-600"
|
||||||
|
: "border-gray-400 bg-transparent"
|
||||||
|
}`}
|
||||||
|
aria-label="SubCategory checkbox"
|
||||||
|
>
|
||||||
|
{subCategory.id === subItem.id && (
|
||||||
|
<Check
|
||||||
|
className="h-2.5 w-2.5 text-white"
|
||||||
|
strokeWidth={3}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<p className="text-sm whitespace-nowrap">
|
||||||
|
{subItem.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-300">
|
||||||
|
SubCategory topilmadi
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Bo'lim filtri */}
|
{/* Bo'lim filtri */}
|
||||||
{visibleSectionData && (
|
{visibleSectionData && visibleSectionData.length > 0 && (
|
||||||
<div className="bg-gray-500 rounded-lg w-full">
|
<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">
|
<p className="bg-red-500 text-white p-2 font-semibold font-almarai text-lg rounded-t-lg">
|
||||||
Bo'lim
|
Bo'lim
|
||||||
</p>
|
</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">
|
<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) => (
|
{visibleSectionData.map((item: any) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => toggleFilter(item)}
|
onClick={() => toggleFilter(item)}
|
||||||
className="hover:cursor-pointer flex items-center gap-2 w-auto shrink-0"
|
className="hover:cursor-pointer flex items-center gap-2 w-auto shrink-0 hover:bg-gray-600 lg:p-2 rounded transition-colors"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition ${
|
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition ${
|
||||||
@@ -108,17 +266,17 @@ export default function Filter() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* O'lcham filtri */}
|
{/* O'lcham filtri */}
|
||||||
{visibleSectionNumber && (
|
{visibleSectionNumber && visibleSectionNumber.length > 0 && (
|
||||||
<div className="bg-gray-500 rounded-lg">
|
<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">
|
<p className="bg-red-500 text-white p-2 font-semibold font-almarai text-lg rounded-t-lg">
|
||||||
O'lcham
|
O'lcham
|
||||||
</p>
|
</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">
|
<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) => (
|
{visibleSectionNumber.map((item: any) => (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={() => toggleFilter(item)}
|
onClick={() => toggleFilter(item)}
|
||||||
className="hover:cursor-pointer flex items-center gap-2 w-auto shrink-0"
|
className="hover:cursor-pointer flex items-center gap-2 w-auto shrink-0 hover:bg-gray-600 lg:p-2 rounded transition-colors"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition ${
|
className={`flex h-5 w-5 items-center justify-center rounded border-2 transition ${
|
||||||
|
|||||||
@@ -7,13 +7,16 @@ import { useCategory } from "@/store/useCategory";
|
|||||||
import { useFilter } from "@/lib/filter-zustand";
|
import { useFilter } from "@/lib/filter-zustand";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { useProductPageInfo } from "@/store/useProduct";
|
import { useProductPageInfo } from "@/store/useProduct";
|
||||||
|
import { useSubCategory } from "@/store/useSubCategory";
|
||||||
|
|
||||||
export default function MainProduct() {
|
export default function MainProduct() {
|
||||||
const category = useCategory((state) => state.category);
|
const category = useCategory((state) => state.category);
|
||||||
|
const subCategory = useSubCategory((state) => state.subCategory);
|
||||||
const filter = useFilter((state) => state.filter);
|
const filter = useFilter((state) => state.filter);
|
||||||
const getFiltersByType = useFilter((state) => state.getFiltersByType);
|
const getFiltersByType = useFilter((state) => state.getFiltersByType);
|
||||||
const setProduct = useProductPageInfo((state) => state.setProducts);
|
const setProduct = useProductPageInfo((state) => state.setProducts);
|
||||||
|
|
||||||
|
console.log("subCategory data: ", subCategory);
|
||||||
// Query params yaratish
|
// Query params yaratish
|
||||||
const queryParams = useMemo(() => {
|
const queryParams = useMemo(() => {
|
||||||
const catalog = getFiltersByType("catalog");
|
const catalog = getFiltersByType("catalog");
|
||||||
@@ -23,7 +26,7 @@ export default function MainProduct() {
|
|||||||
const catalogParams = catalog.map((item) => `catalog=${item.id}`).join("&");
|
const catalogParams = catalog.map((item) => `catalog=${item.id}`).join("&");
|
||||||
const sizeParams = size.map((item) => `size=${item.id}`).join("&");
|
const sizeParams = size.map((item) => `size=${item.id}`).join("&");
|
||||||
|
|
||||||
// Barcha paramslarni birlashtirish
|
// Barcha paramslarni birlashtirish for gitea
|
||||||
const allParams = [catalogParams, sizeParams].filter(Boolean).join("&");
|
const allParams = [catalogParams, sizeParams].filter(Boolean).join("&");
|
||||||
|
|
||||||
return allParams ? `&${allParams}` : "";
|
return allParams ? `&${allParams}` : "";
|
||||||
@@ -32,27 +35,22 @@ export default function MainProduct() {
|
|||||||
// Request link yaratish
|
// Request link yaratish
|
||||||
const requestLink = useMemo(() => {
|
const requestLink = useMemo(() => {
|
||||||
const baseLink = category.have_sub_category
|
const baseLink = category.have_sub_category
|
||||||
? endPoints.subCategory.byId(category.id)
|
? endPoints.product.bySubCategory(subCategory.id)
|
||||||
: endPoints.product.byCategory(category.id || 0);
|
: endPoints.product.byCategory(category.id || 0);
|
||||||
|
|
||||||
// Query params qo'shish
|
// Query params qo'shish
|
||||||
return `${baseLink}${queryParams}`;
|
return `${baseLink}${queryParams}`;
|
||||||
}, [category.id, category.have_sub_category, queryParams]);
|
}, [category.id, category.have_sub_category, queryParams , subCategory.id]);
|
||||||
|
|
||||||
const { data, isLoading, error } = useQuery({
|
const { data, isLoading, error } = useQuery({
|
||||||
queryKey: ["products", category.id, queryParams],
|
queryKey: ["products", subCategory.id, queryParams],
|
||||||
queryFn: () => httpClient(requestLink),
|
queryFn: () => httpClient(requestLink),
|
||||||
select: (data) => {
|
select: (data) => {
|
||||||
const product = data?.data?.data?.results;
|
console.log("product: ", data?.data?.data?.results);
|
||||||
return product.map((item: any) => ({
|
return data?.data?.data?.results;
|
||||||
id: item.id,
|
|
||||||
name: item.name,
|
|
||||||
image: item.images[0].image,
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
<div className="grid lg:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-5">
|
||||||
@@ -86,8 +84,8 @@ export default function MainProduct() {
|
|||||||
key={item.id} // ✅ index o'rniga id ishlatish
|
key={item.id} // ✅ index o'rniga id ishlatish
|
||||||
getProduct={() => setProduct(item)}
|
getProduct={() => setProduct(item)}
|
||||||
title={item.name}
|
title={item.name}
|
||||||
image={item.image}
|
image={item?.images[0]?.image || ""}
|
||||||
slug={item.slug}
|
slug="special_product"
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function ProductCard({
|
|||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/${locale}/products/${slug}`} onClick={getProduct}>
|
<Link href={`/${locale}/catalog_page/products/${slug}`} onClick={getProduct}>
|
||||||
<article className="group transition-all duration-300 hover:cursor-pointer max-sm:max-w-100 max-sm:mx-auto max-sm:w-full">
|
<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 */}
|
{/* Image Container */}
|
||||||
<div className="relative rounded-2xl h-45 sm:h-55 md:h-65 lg:w-[95%] w-[90%] mx-auto overflow-hidden">
|
<div className="relative rounded-2xl h-45 sm:h-55 md:h-65 lg:w-[95%] w-[90%] mx-auto overflow-hidden">
|
||||||
@@ -33,7 +33,7 @@ export default function ProductCard({
|
|||||||
|
|
||||||
{/* Content Container */}
|
{/* Content Container */}
|
||||||
<div className="p-6 sm:p-4">
|
<div className="p-6 sm:p-4">
|
||||||
<h3 className="text-lg text-center sm:text-xl md:text-2xl font-bold text-white mb-4 line-clamp-3 group-hover:text-red-400 transition-colors duration-300">
|
<h3 className="text-lg text-center sm:text-xl md:text-2xl font-unbounded font-bold text-white mb-4 line-clamp-3 group-hover:text-red-400 transition-colors duration-300">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import MainProduct from "./mianProduct";
|
|||||||
|
|
||||||
export function Products() {
|
export function Products() {
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#1e1d1c] py-10">
|
<div className="bg-[#1e1d1c] pb-10 pt-5 px-2">
|
||||||
<div className="max-w-300 mx-auto w-full z-20 relative">
|
<div className="max-w-300 mx-auto w-full z-20 relative">
|
||||||
<div className="flex lg:flex-row flex-col lg:items-start items-center gap-5">
|
<div className="flex lg:flex-row flex-col lg:items-start items-center gap-5">
|
||||||
{/* filter part */}
|
{/* filter part */}
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
import { usePriceModalStore } from "@/store/useProceModalStore";
|
import { usePriceModalStore } from "@/store/useProceModalStore";
|
||||||
import { Facebook, Share2 } from "lucide-react";
|
import { Instagram, Send, Share2 } from "lucide-react";
|
||||||
|
|
||||||
const socialLinks = [
|
|
||||||
{ name: "telegram", icon: "✈️", color: "#0088cc" },
|
|
||||||
{ name: "facebook", icon: <Facebook size={18} />, color: "#1877F2" },
|
|
||||||
{ name: "whatsapp", icon: "💬", color: "#25D366" },
|
|
||||||
{ name: "twitter", icon: "𝕏", color: "#1DA1F2" },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface RightSideProps {
|
interface RightSideProps {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -47,7 +40,7 @@ export function RightSide({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center space-y-6">
|
<div className="flex flex-col justify-center space-y-6">
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<h1 className="text-2xl md:text-3xl lg:text-4xl font-bold text-white leading-tight">
|
<h1 className="text-2xl md:text-3xl lg:text-4xl font-unbounded font-bold text-white leading-tight">
|
||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -95,19 +88,14 @@ export function RightSide({
|
|||||||
<Share2 className="w-5 h-5 text-gray-400" />
|
<Share2 className="w-5 h-5 text-gray-400" />
|
||||||
<span className="text-sm text-gray-400">Ulashish:</span>
|
<span className="text-sm text-gray-400">Ulashish:</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex items-center gap-5 mt-5">
|
||||||
{socialLinks.map((social) => (
|
{/* <a href="" className="p-2 rounded-md bg-white text-red-500 hover:text-white hover:bg-red-500">
|
||||||
<a
|
<Instagram />
|
||||||
key={social.name}
|
</a> */}
|
||||||
href="#"
|
<a href="https://t.me/ignum_tech" className="p-2 rounded-md bg-white text-red-500 hover:text-white hover:bg-red-500">
|
||||||
className="w-10 h-10 rounded-lg flex items-center justify-center text-white text-sm font-bold transition-all duration-300 hover:scale-110 hover:shadow-lg"
|
<Send />
|
||||||
style={{ backgroundColor: social.color }}
|
</a>
|
||||||
title={social.name}
|
</div>
|
||||||
>
|
|
||||||
{social.icon}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function ServiceBanner() {
|
|||||||
<div
|
<div
|
||||||
className="absolute inset-0 z-10"
|
className="absolute inset-0 z-10"
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(to top right, #d2610a 0%, #1e1d1ce3 30%, #1e1d1ce3 100%)`,
|
background: `linear-gradient(to right top, rgb(157 73 9) 0%, rgb(33 32 31 / 89%) 40%, rgba(30, 29, 28, 0.89) 100%)`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export default function Card({
|
|||||||
image,
|
image,
|
||||||
category,
|
category,
|
||||||
});
|
});
|
||||||
router.push(`/${locale}/products`);
|
router.push(`/${locale}/catalog_page/products`);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Link href="#" onClick={handleClick}>
|
<Link href="#" onClick={handleClick}>
|
||||||
@@ -50,7 +50,7 @@ export default function Card({
|
|||||||
|
|
||||||
{/* Content Container */}
|
{/* Content Container */}
|
||||||
<div className="p-6 sm:p-4">
|
<div className="p-6 sm:p-4">
|
||||||
<h3 className="text-lg text-center sm:text-xl md:text-2xl font-bold text-white mb-4 line-clamp-3 group-hover:text-red-400 transition-colors duration-300">
|
<h3 className="text-lg text-center sm:text-xl md:text-2xl font-unbounded font-bold text-white mb-4 line-clamp-3 group-hover:text-red-400 transition-colors duration-300">
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function PriceModal() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset form when modal closes
|
// Reset form when modal closes for github
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
setFormData({
|
setFormData({
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function Providers({ children }: { children: ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<BackAnimatsiya />
|
{/* <BackAnimatsiya /> */}
|
||||||
<Navbar />
|
<Navbar />
|
||||||
{children}
|
{children}
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -222,7 +222,8 @@
|
|||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"help": "Help"
|
"help": "Help"
|
||||||
},
|
},
|
||||||
"address":"Tashkent city, Yunusabad district, 3rd dead-end of Niyozbek Yoli street, house 39"
|
"address": "Tashkent city, Yunusabad district, 3rd dead-end of Niyozbek Yoli street, house 39",
|
||||||
|
"create": "Created by {name}"
|
||||||
},
|
},
|
||||||
"rasmlar": "Images",
|
"rasmlar": "Images",
|
||||||
"fotogalereya": "Photo Gallery",
|
"fotogalereya": "Photo Gallery",
|
||||||
@@ -230,8 +231,8 @@
|
|||||||
"contactSubTitle": "Our staff will contact you",
|
"contactSubTitle": "Our staff will contact you",
|
||||||
"enterPhone": "Enter your phone number",
|
"enterPhone": "Enter your phone number",
|
||||||
"send": "Sent",
|
"send": "Sent",
|
||||||
"error":"Error!",
|
"error": "Error!",
|
||||||
"succes":"sent!",
|
"succes": "sent!",
|
||||||
"priceModal": {
|
"priceModal": {
|
||||||
"title": "Get Price",
|
"title": "Get Price",
|
||||||
"product": {
|
"product": {
|
||||||
@@ -255,5 +256,23 @@
|
|||||||
},
|
},
|
||||||
"success": "Your request has been sent successfully!",
|
"success": "Your request has been sent successfully!",
|
||||||
"error": "An error occurred. Please try again."
|
"error": "An error occurred. Please try again."
|
||||||
|
},
|
||||||
|
"breadcrumb": {
|
||||||
|
"home": "Home",
|
||||||
|
"about": "About Us",
|
||||||
|
"services": "Services",
|
||||||
|
"products": "Products",
|
||||||
|
"contact": "Contact",
|
||||||
|
"blog": "Blog",
|
||||||
|
"fire-safety": "Fire Safety",
|
||||||
|
"fire-alarm": "Fire Alarm",
|
||||||
|
"fire-suppression": "Fire Suppression",
|
||||||
|
"installation": "Installation",
|
||||||
|
"maintenance": "Maintenance"
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"category": "Categories",
|
||||||
|
"catalog": "Section",
|
||||||
|
"size": "Sizes"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,7 +222,8 @@
|
|||||||
"contact": "Контакты",
|
"contact": "Контакты",
|
||||||
"help": "Помощь"
|
"help": "Помощь"
|
||||||
},
|
},
|
||||||
"address":"г. Ташкент, Юнусабадский район, 3-й тупик улицы Ниязбек йўли, дом 39"
|
"address": "г. Ташкент, Юнусабадский район, 3-й тупик улицы Ниязбек йўли, дом 39",
|
||||||
|
"create": "Разработано {name}"
|
||||||
},
|
},
|
||||||
"rasmlar": "Изображения",
|
"rasmlar": "Изображения",
|
||||||
"fotogalereya": "Фотогалерея",
|
"fotogalereya": "Фотогалерея",
|
||||||
@@ -255,5 +256,23 @@
|
|||||||
},
|
},
|
||||||
"success": "Ваш запрос успешно отправлен!",
|
"success": "Ваш запрос успешно отправлен!",
|
||||||
"error": "Произошла ошибка. Попробуйте снова."
|
"error": "Произошла ошибка. Попробуйте снова."
|
||||||
|
},
|
||||||
|
"breadcrumb": {
|
||||||
|
"home": "Главная",
|
||||||
|
"about": "О нас",
|
||||||
|
"services": "Услуги",
|
||||||
|
"products": "Продукция",
|
||||||
|
"contact": "Контакты",
|
||||||
|
"blog": "Блог",
|
||||||
|
"fire-safety": "Пожарная безопасность",
|
||||||
|
"fire-alarm": "Пожарная сигнализация",
|
||||||
|
"fire-suppression": "Пожаротушение",
|
||||||
|
"installation": "Монтаж",
|
||||||
|
"maintenance": "Техническое обслуживание"
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"category": "Категории",
|
||||||
|
"catalog": "Раздел",
|
||||||
|
"size": "Размеры"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,7 +222,8 @@
|
|||||||
"contact": "Aloqa",
|
"contact": "Aloqa",
|
||||||
"help": "Yordam"
|
"help": "Yordam"
|
||||||
},
|
},
|
||||||
"address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy"
|
"address": "Toshkent shahri , Yunusabod tumani , Niyozbek yo'li 3 tor ko'chasi , 39 uy",
|
||||||
|
"create": "{name} - Jamoasi tomonidan ishlab chiqilgan"
|
||||||
},
|
},
|
||||||
"rasmlar": "Rasmlar",
|
"rasmlar": "Rasmlar",
|
||||||
"fotogalereya": "Fotogalereya",
|
"fotogalereya": "Fotogalereya",
|
||||||
@@ -255,5 +256,24 @@
|
|||||||
},
|
},
|
||||||
"success": "So‘rovingiz muvaffaqiyatli yuborildi!",
|
"success": "So‘rovingiz muvaffaqiyatli yuborildi!",
|
||||||
"error": "Xatolik yuz berdi. Iltimos, qayta urinib ko‘ring."
|
"error": "Xatolik yuz berdi. Iltimos, qayta urinib ko‘ring."
|
||||||
|
},
|
||||||
|
"breadcrumb": {
|
||||||
|
"home": "Bosh sahifa",
|
||||||
|
"about": "Biz haqimizda",
|
||||||
|
"services": "Xizmatlar",
|
||||||
|
"catalog_page": "Mahsulotlar",
|
||||||
|
"subCategory":"{subCategory}",
|
||||||
|
"contact": "Bog'lanish",
|
||||||
|
"blog": "Blog",
|
||||||
|
"fire-safety": "Yong'in xavfsizligi",
|
||||||
|
"fire-alarm": "Yong'in signalizatsiyasi",
|
||||||
|
"fire-suppression": "Yong'in o'chirish",
|
||||||
|
"installation": "O'rnatish",
|
||||||
|
"maintenance": "Texnik xizmat"
|
||||||
|
},
|
||||||
|
"filter":{
|
||||||
|
"category":"Kategoriyalar",
|
||||||
|
"catalog":"Bo'lim",
|
||||||
|
"size":"O'lchamlar"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
public/manifest.json
Normal file
21
public/manifest.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "Ignum Technologies - Fire Safety Systems",
|
||||||
|
"short_name": "Ignum Tech",
|
||||||
|
"description": "Professional fire safety systems installation and sales",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"theme_color": "#FF4500",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/og-image.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/og-image.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 353 KiB |
4
public/robots.txt
Normal file
4
public/robots.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://ignum-tech.com/sitemap.xml
|
||||||
Reference in New Issue
Block a user