changed some color and fix some styles
This commit is contained in:
@@ -202,6 +202,7 @@ export default function CartPage() {
|
|||||||
...item,
|
...item,
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
price: price,
|
price: price,
|
||||||
|
description: item.product.description,
|
||||||
total: total,
|
total: total,
|
||||||
seller: seller,
|
seller: seller,
|
||||||
price_formatted: `${item.product.price_amount} TMT`,
|
price_formatted: `${item.product.price_amount} TMT`,
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ export const revalidate = 600; // ISR: Revalidate every 10 minutes
|
|||||||
|
|
||||||
const CATEGORY_META = {
|
const CATEGORY_META = {
|
||||||
tm: {
|
tm: {
|
||||||
suffix: " | Post shop",
|
suffix: " | SmartElectronics",
|
||||||
description: "Kategoriýa boýunça harytlary gözläň",
|
description: "Kategoriýa boýunça harytlary gözläň",
|
||||||
ogLocale: "tk_TM",
|
ogLocale: "tk_TM",
|
||||||
},
|
},
|
||||||
ru: {
|
ru: {
|
||||||
suffix: " | Post shop",
|
suffix: " | SmartElectronics",
|
||||||
description: "Просмотр товаров в данной категории",
|
description: "Просмотр товаров в данной категории",
|
||||||
ogLocale: "ru_RU",
|
ogLocale: "ru_RU",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ export const revalidate = 600; // ISR: 10 minutes
|
|||||||
|
|
||||||
const META = {
|
const META = {
|
||||||
tm: {
|
tm: {
|
||||||
titleSuffix: " | Post shop",
|
titleSuffix: " | SmartElectronics",
|
||||||
description: (name: string) => `${name} kolleksiýasyndaky harytlary gözläň`,
|
description: (name: string) => `${name} kolleksiýasyndaky harytlary gözläň`,
|
||||||
ogLocale: "tk_TM",
|
ogLocale: "tk_TM",
|
||||||
},
|
},
|
||||||
ru: {
|
ru: {
|
||||||
titleSuffix: " | Post shop",
|
titleSuffix: " | SmartElectronics",
|
||||||
description: (name: string) => `Просмотр товаров из коллекции «${name}»`,
|
description: (name: string) => `Просмотр товаров из коллекции «${name}»`,
|
||||||
ogLocale: "ru_RU",
|
ogLocale: "ru_RU",
|
||||||
},
|
},
|
||||||
|
|||||||
124
app/[locale]/info/page.tsx
Normal file
124
app/[locale]/info/page.tsx
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import {
|
||||||
|
Instagram,
|
||||||
|
Phone,
|
||||||
|
Mail,
|
||||||
|
MessageSquare,
|
||||||
|
ChevronLeft,
|
||||||
|
} from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import Logo from "@/public/logo.png";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
export default function InfoPage() {
|
||||||
|
const t = useTranslations("common");
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const contactItems = [
|
||||||
|
{
|
||||||
|
icon: <Phone className="h-6 w-6" />,
|
||||||
|
label: t("phone"),
|
||||||
|
value: "+993 65 123456",
|
||||||
|
href: "tel:+99365123456",
|
||||||
|
color: "bg-blue-50 text-blue-600",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Instagram className="h-6 w-6" />,
|
||||||
|
label: t("instagram"),
|
||||||
|
value: "@smartelectronics",
|
||||||
|
href: "https://instagram.com/smartelectronics",
|
||||||
|
color: "bg-pink-50 text-pink-600",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Mail className="h-6 w-6" />,
|
||||||
|
label: t("email"),
|
||||||
|
value: "info@smartelectronics.com",
|
||||||
|
href: "mailto:info@smartelectronics.com",
|
||||||
|
color: "bg-gray-50 text-gray-600",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <MessageSquare className="h-6 w-6" />,
|
||||||
|
label: t("imo"),
|
||||||
|
value: "+993 65 123456",
|
||||||
|
href: null,
|
||||||
|
color: "bg-emerald-50 text-emerald-600",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50 pb-20">
|
||||||
|
{/*
|
||||||
|
<div className="bg-white border-b sticky top-0 z-10 px-4 h-16 flex items-center gap-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
className="rounded-full"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
<h1 className="text-xl font-bold">{t("info")}</h1>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
<div className="p-4 max-w-md mx-auto space-y-6">
|
||||||
|
{/* Logo Section */}
|
||||||
|
<div className="bg-white rounded-3xl p-8 flex flex-col items-center justify-center shadow-sm border border-gray-100">
|
||||||
|
<div className="relative h-32 w-[220px] mb-4">
|
||||||
|
<Image src={Logo} alt="Logo" fill className="object-contain" />
|
||||||
|
</div>
|
||||||
|
<p className="text-center text-gray-500 text-sm leading-relaxed">
|
||||||
|
SmartElectronics - yerli we daşary ýurt harytlarynyň onlaýn marketi.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Grid */}
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<h2 className="text-sm font-semibold text-gray-400 uppercase tracking-wider px-2">
|
||||||
|
{t("contact_us")}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{contactItems.map((item, index) => {
|
||||||
|
const Content = (
|
||||||
|
<div className="flex items-center gap-4 p-4 bg-white rounded-2xl border border-gray-100 shadow-sm active:bg-gray-50 transition-colors">
|
||||||
|
<div className={`p-3 rounded-xl ${item.color}`}>
|
||||||
|
{item.icon}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-xs text-gray-400 font-medium">
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
|
<span className="text-base font-semibold text-gray-900">
|
||||||
|
{item.value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (item.href) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={index}
|
||||||
|
href={item.href}
|
||||||
|
target={item.href.startsWith("http") ? "_blank" : undefined}
|
||||||
|
rel={
|
||||||
|
item.href.startsWith("http")
|
||||||
|
? "noopener noreferrer"
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{Content}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div key={index}>{Content}</div>;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,61 +1,65 @@
|
|||||||
import type React from "react"
|
import type React from "react";
|
||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google"
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation";
|
||||||
import { NextIntlClientProvider } from "next-intl"
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
import "./globals.css"
|
import "./globals.css";
|
||||||
import Header from "@/components/layout/Header"
|
import Header from "@/components/layout/Header";
|
||||||
import MobileBottomNav from "@/components/layout/MobileBar"
|
import MobileBottomNav from "@/components/layout/MobileBar";
|
||||||
import { Toaster } from "@/components/ui/sonner"
|
import Footer from "@/components/layout/Footer";
|
||||||
import { Providers } from "@/context/Provider"
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import AuthWrapper from "@/context/AuthWrapper"
|
import { Providers } from "@/context/Provider";
|
||||||
|
import AuthWrapper from "@/context/AuthWrapper";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
})
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const geistMono = Geist_Mono({
|
||||||
variable: "--font-geist-mono",
|
variable: "--font-geist-mono",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
})
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Postshop",
|
title: "SmartElectronics",
|
||||||
description: "E-commerce platform",
|
description: "E-commerce platform",
|
||||||
}
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
params: Promise<{ locale: string }>
|
params: Promise<{ locale: string }>;
|
||||||
}
|
};
|
||||||
|
|
||||||
const locales = ["ru", "tm"]
|
const locales = ["ru", "tm"];
|
||||||
|
|
||||||
export function generateStaticParams() {
|
export function generateStaticParams() {
|
||||||
return locales.map((locale) => ({ locale }))
|
return locales.map((locale) => ({ locale }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function RootLayout({ children, params }: Props) {
|
export default async function RootLayout({ children, params }: Props) {
|
||||||
const { locale } = await params
|
const { locale } = await params;
|
||||||
|
|
||||||
if (!locales.includes(locale)) notFound()
|
if (!locales.includes(locale)) notFound();
|
||||||
|
|
||||||
let messages
|
let messages;
|
||||||
try {
|
try {
|
||||||
messages = (await import(`../../i18n/messages/${locale}.json`)).default
|
messages = (await import(`../../i18n/messages/${locale}.json`)).default;
|
||||||
} catch {
|
} catch {
|
||||||
messages = {}
|
messages = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale} suppressHydrationWarning>
|
<html lang={locale} suppressHydrationWarning>
|
||||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
<body
|
||||||
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
|
>
|
||||||
<Providers>
|
<Providers>
|
||||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||||
<AuthWrapper locale={locale}>
|
<AuthWrapper locale={locale}>
|
||||||
<Header locale={locale} />
|
<Header locale={locale} />
|
||||||
{children}
|
{children}
|
||||||
|
<Footer />
|
||||||
<MobileBottomNav locale={locale} />
|
<MobileBottomNav locale={locale} />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</AuthWrapper>
|
</AuthWrapper>
|
||||||
@@ -63,5 +67,5 @@ export default async function RootLayout({ children, params }: Props) {
|
|||||||
</Providers>
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import OrdersPageClient from "../../../features/orders/components/OrderPage";
|
|||||||
|
|
||||||
const metadataContent = {
|
const metadataContent = {
|
||||||
tm: {
|
tm: {
|
||||||
title: "Meniň Sargytlarym | Post shop",
|
title: "Meniň Sargytlarym | SmartElectronics",
|
||||||
description: "Sargytlaryňyzy görüň",
|
description: "Sargytlaryňyzy görüň",
|
||||||
},
|
},
|
||||||
ru: {
|
ru: {
|
||||||
title: "Мои Заказы | Пост-магазин",
|
title: "Мои Заказы | SmartElectronics",
|
||||||
description: "Просмотр истории заказов",
|
description: "Просмотр истории заказов",
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const META = {
|
|||||||
description: "Качественные товары с быстрой доставкой по всей стране",
|
description: "Качественные товары с быстрой доставкой по всей стране",
|
||||||
},
|
},
|
||||||
tm: {
|
tm: {
|
||||||
title: "Post shop - Iň gowy harytlar, amatly bahada",
|
title: "SmartElectronics - Iň gowy harytlar, amatly bahada",
|
||||||
description:
|
description:
|
||||||
"Ýokary hilli harytlar. Elektronika, eşik, arassaçylyk, sport, kosmetika",
|
"Ýokary hilli harytlar. Elektronika, eşik, arassaçylyk, sport, kosmetika",
|
||||||
},
|
},
|
||||||
|
|||||||
23
app/[locale]/search/page.tsx
Normal file
23
app/[locale]/search/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import SearchPageClient from "@/features/search/components/SearchPageClient";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
params: Promise<{ locale: string }>;
|
||||||
|
searchParams: Promise<{ q?: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
searchParams,
|
||||||
|
}: Props): Promise<Metadata> {
|
||||||
|
const { q } = await searchParams;
|
||||||
|
return {
|
||||||
|
title: q ? `Search: ${q} | SmartElectronics` : "Search | SmartElectronics",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function SearchPage(props: Props) {
|
||||||
|
const params = await props.params;
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
|
|
||||||
|
return <SearchPageClient params={params} searchParams={searchParams} />;
|
||||||
|
}
|
||||||
BIN
app/favicon.ico
BIN
app/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 178 KiB |
138
components/layout/Footer.tsx
Normal file
138
components/layout/Footer.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { Instagram, Phone, Mail, MessageSquare } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import Logo from "@/public/logo.png";
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
const t = useTranslations("common");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="hidden lg:block w-full bg-white border-t border-gray-200 mt-auto">
|
||||||
|
<div className="mx-auto px-4 lg:px-6 max-w-[1520px] py-6">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="inline-block transition-opacity hover:opacity-80"
|
||||||
|
>
|
||||||
|
<div className="relative h-34 w-[200px]">
|
||||||
|
<Image
|
||||||
|
src={Logo}
|
||||||
|
alt="Logo"
|
||||||
|
fill
|
||||||
|
className="object-contain object-left"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 pb-4">
|
||||||
|
{/* Logo and Tagline */}
|
||||||
|
<div className="col-span-1 md:col-span-1 space-y-3">
|
||||||
|
<p className="text-base text-gray-500 max-w-xs">
|
||||||
|
SmartElectronics - yerli we daşary ýurt harytlarynyň onlaýn
|
||||||
|
marketi.
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
className="mt-4 text-base text-gray-500 max-w-xs"
|
||||||
|
href="/info"
|
||||||
|
>
|
||||||
|
{t("about_us")}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
className="mt-4 text-base text-gray-500 max-w-xs"
|
||||||
|
href="/info"
|
||||||
|
>
|
||||||
|
{t("contact_us")}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Links / Contact Info */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="font-bold text-gray-900 text-lg">
|
||||||
|
{t("contact_us")}
|
||||||
|
</h3>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<ul className="space-y-3">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="tel:+99365123456"
|
||||||
|
className="flex items-center gap-3 text-gray-600 hover:text-primary transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="p-2 rounded-full bg-gray-50 group-hover:bg-primary/10 transition-colors">
|
||||||
|
<Phone className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<span>+993 65 123456</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://instagram.com/smartelectronics"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center gap-3 text-gray-600 hover:text-primary transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="p-2 rounded-full bg-gray-50 group-hover:bg-primary/10 transition-colors">
|
||||||
|
<Instagram className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<span>@smartelectronics</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{" "}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="mailto:info@smartelectronics.com"
|
||||||
|
className="flex items-center gap-3 text-gray-600 hover:text-primary transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="p-2 rounded-full bg-gray-50 group-hover:bg-primary/10 transition-colors">
|
||||||
|
<Mail className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<span>info@smartelectronics.com</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div className="flex items-center gap-3 text-gray-600 group">
|
||||||
|
<div className="p-2 rounded-full bg-gray-50">
|
||||||
|
<MessageSquare className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-xs text-gray-400 font-medium">
|
||||||
|
{t("imo")}
|
||||||
|
</span>
|
||||||
|
<span>+993 65 123456</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Placeholder for other sections if needed */}
|
||||||
|
<div className="hidden md:block"></div>
|
||||||
|
<div className="hidden md:block"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 pt-6 border-t border-gray-100 flex flex-col md:flex-row justify-between items-center gap-4 text-sm text-gray-400">
|
||||||
|
<p>
|
||||||
|
© {new Date().getFullYear()} SmartElectronics. All rights reserved.
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-6">
|
||||||
|
<Link href="#" className="hover:text-gray-600 transition-colors">
|
||||||
|
Terms
|
||||||
|
</Link>
|
||||||
|
<Link href="#" className="hover:text-gray-600 transition-colors">
|
||||||
|
Privacy
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import Link from "next/link";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { X, Search } from "lucide-react";
|
import { X, Search } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import Logo from "@/public/logo.webp";
|
import Logo from "@/public/logo.png";
|
||||||
import CategoryMenu from "./ui/CategoryMenu";
|
import CategoryMenu from "./ui/CategoryMenu";
|
||||||
import SearchBar from "./ui/SearchBar";
|
import SearchBar from "./ui/SearchBar";
|
||||||
import AuthDialog from "./ui/AuthDialog";
|
import AuthDialog from "./ui/AuthDialog";
|
||||||
@@ -62,7 +62,7 @@ export default function Header({ locale = "ru" }: HeaderProps) {
|
|||||||
href="/"
|
href="/"
|
||||||
className="shrink-0 transition-opacity hover:opacity-80"
|
className="shrink-0 transition-opacity hover:opacity-80"
|
||||||
>
|
>
|
||||||
<div className="relative h-8 w-[180px]">
|
<div className="relative h-32 w-[188px]">
|
||||||
<Image
|
<Image
|
||||||
src={Logo}
|
src={Logo}
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Menu, Heart, Truck, ShoppingCart, User } from "lucide-react";
|
import { Menu, Heart, Truck, ShoppingCart, Info } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import {
|
import {
|
||||||
@@ -15,9 +15,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||||||
import { useCategories, useFavorites, useOrders } from "@/lib/hooks";
|
import { useCategories, useFavorites, useOrders } from "@/lib/hooks";
|
||||||
import { useCartCount } from "@/features/cart/hooks/useCart";
|
import { useCartCount } from "@/features/cart/hooks/useCart";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useAuthStatus } from "@/lib/hooks/useAuth";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import AuthDialog from "./ui/AuthDialog";
|
|
||||||
|
|
||||||
interface MobileBottomNavProps {
|
interface MobileBottomNavProps {
|
||||||
locale?: string;
|
locale?: string;
|
||||||
@@ -39,10 +37,39 @@ export default function MobileBottomNav({
|
|||||||
}: MobileBottomNavProps) {
|
}: MobileBottomNavProps) {
|
||||||
const [isClient, setIsClient] = useState(false);
|
const [isClient, setIsClient] = useState(false);
|
||||||
const [isCategoryOpen, setIsCategoryOpen] = useState(false);
|
const [isCategoryOpen, setIsCategoryOpen] = useState(false);
|
||||||
const [isLoginOpen, setIsLoginOpen] = useState(false);
|
const [expandedCategories, setExpandedCategories] = useState<Set<number>>(
|
||||||
|
new Set(),
|
||||||
|
);
|
||||||
|
const [touchStart, setTouchStart] = useState<number | null>(null);
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const { isAuthenticated, isLoading: authLoading } = useAuthStatus();
|
const handleTouchStart = (e: React.TouchEvent) => {
|
||||||
|
setTouchStart(e.targetTouches[0].clientX);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = (e: React.TouchEvent) => {
|
||||||
|
if (touchStart === null) return;
|
||||||
|
const touchEnd = e.changedTouches[0].clientX;
|
||||||
|
const distance = touchStart - touchEnd;
|
||||||
|
|
||||||
|
// Side is left, so swiping left (negative delta or positive distance) closes it
|
||||||
|
if (distance > 50) {
|
||||||
|
setIsCategoryOpen(false);
|
||||||
|
}
|
||||||
|
setTouchStart(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCategory = (categoryId: number) => {
|
||||||
|
setExpandedCategories((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
if (newSet.has(categoryId)) {
|
||||||
|
newSet.delete(categoryId);
|
||||||
|
} else {
|
||||||
|
newSet.add(categoryId);
|
||||||
|
}
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const { data: categories = [] } = useCategories();
|
const { data: categories = [] } = useCategories();
|
||||||
|
|
||||||
@@ -56,25 +83,6 @@ export default function MobileBottomNav({
|
|||||||
setIsClient(true);
|
setIsClient(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleProfileClick = (e: React.MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (authLoading) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAuthenticated) {
|
|
||||||
router.push(`/${locale}/me`);
|
|
||||||
} else {
|
|
||||||
if (onLoginClick) {
|
|
||||||
onLoginClick();
|
|
||||||
} else {
|
|
||||||
setIsLoginOpen(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNavigation = (path: string) => (e: React.MouseEvent) => {
|
const handleNavigation = (path: string) => (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
router.push(path);
|
router.push(path);
|
||||||
@@ -85,40 +93,39 @@ export default function MobileBottomNav({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Mobile Bottom Navigation */}
|
{/* Mobile Bottom Navigation */}
|
||||||
<div className="fixed bottom-0 left-0 right-0 z-50 bg-white border-t shadow-lg lg:hidden">
|
<div className="fixed bottom-0 left-0 right-0 z-50 bg-white/95 backdrop-blur-md border-t border-gray-200 shadow-xl lg:hidden">
|
||||||
<div className="flex items-center justify-around h-16 px-2">
|
<div className="flex items-center justify-around h-16 px-2">
|
||||||
{/* Catalog Button */}
|
{/* Catalog Button */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex-col gap-0.5 h-auto px-2 py-2"
|
className="flex-col gap-0.5 h-auto px-2 py-2 hover:bg-gray-100 rounded-xl transition-colors"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsCategoryOpen(true);
|
setIsCategoryOpen(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Menu className="h-5 w-5 text-gray-600" />
|
<Menu className="h-5 w-5 text-gray-700" />
|
||||||
<span className="text-xs text-gray-700">{t("common.catalog")}</span>
|
<span className="text-xs text-gray-700 font-medium">
|
||||||
|
{t("common.catalog")}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Favorites Button */}
|
{/* Favorites Button */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="relative flex-col gap-0.5 h-auto px-2 py-2"
|
className="relative flex-col gap-0.5 h-auto px-2 py-2 hover:bg-gray-100 rounded-xl transition-colors"
|
||||||
onClick={handleNavigation("/favorites")}
|
onClick={handleNavigation("/favorites")}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Heart className="h-5 w-5 text-gray-600" />
|
<Heart className="h-5 w-5 text-gray-700" />
|
||||||
{(favoritesData?.length || 0) > 0 && (
|
{(favoritesData?.length || 0) > 0 && (
|
||||||
<Badge
|
<Badge className="absolute -right-2 -top-2 h-4 w-4 flex items-center justify-center p-0 text-[10px] bg-gray-900 hover:bg-gray-900 text-white border-2 border-white font-bold">
|
||||||
variant="destructive"
|
|
||||||
className="absolute -right-2 -top-2 h-4 w-4 flex items-center justify-center p-0 text-[10px]"
|
|
||||||
>
|
|
||||||
{favoritesData?.length}
|
{favoritesData?.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-700">
|
<span className="text-xs text-gray-700 font-medium">
|
||||||
{t("common.favorites")}
|
{t("common.favorites")}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -127,59 +134,52 @@ export default function MobileBottomNav({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="relative flex-col gap-0.5 h-auto px-2 py-2"
|
className="relative flex-col gap-0.5 h-auto px-2 py-2 hover:bg-gray-100 rounded-xl transition-colors"
|
||||||
onClick={handleNavigation("/orders")}
|
onClick={handleNavigation("/orders")}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Truck className="h-5 w-5 text-gray-600" />
|
<Truck className="h-5 w-5 text-gray-700" />
|
||||||
{(ordersData?.length || 0) > 0 && (
|
{(ordersData?.length || 0) > 0 && (
|
||||||
<Badge
|
<Badge className="absolute -right-2 -top-2 h-4 w-4 flex items-center justify-center p-0 text-[10px] bg-gray-900 hover:bg-gray-900 text-white border-2 border-white font-bold">
|
||||||
variant="destructive"
|
|
||||||
className="absolute -right-2 -top-2 h-4 w-4 flex items-center justify-center p-0 text-[10px]"
|
|
||||||
>
|
|
||||||
{ordersData?.length}
|
{ordersData?.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-700">{t("common.orders")}</span>
|
<span className="text-xs text-gray-700 font-medium">
|
||||||
|
{t("common.orders")}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Cart Button - OPTIMIZED */}
|
{/* Cart Button */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="relative flex-col gap-0.5 h-auto px-2 py-2"
|
className="relative flex-col gap-0.5 h-auto px-2 py-2 hover:bg-gray-100 rounded-xl transition-colors"
|
||||||
onClick={handleNavigation("/cart")}
|
onClick={handleNavigation("/cart")}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<ShoppingCart className="h-5 w-5 text-gray-600" />
|
<ShoppingCart className="h-5 w-5 text-gray-700" />
|
||||||
{cartCount > 0 && (
|
{cartCount > 0 && (
|
||||||
<Badge
|
<Badge className="absolute -right-2 -top-2 h-4 w-4 flex items-center justify-center p-0 text-[10px] bg-gray-900 hover:bg-gray-900 text-white border-2 border-white font-bold">
|
||||||
variant="destructive"
|
|
||||||
className="absolute -right-2 -top-2 h-4 w-4 flex items-center justify-center p-0 text-[10px]"
|
|
||||||
>
|
|
||||||
{cartCount}
|
{cartCount}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-gray-700">{t("common.cart")}</span>
|
<span className="text-xs text-gray-700 font-medium">
|
||||||
|
{t("common.cart")}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* Profile/Login Button */}
|
{/* Info Button */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex-col gap-0.5 h-auto px-2 py-2"
|
className="flex-col gap-0.5 h-auto px-2 py-2 hover:bg-gray-100 rounded-xl transition-colors"
|
||||||
onClick={handleProfileClick}
|
onClick={() => router.push(`/${locale}/info`)}
|
||||||
disabled={authLoading}
|
|
||||||
>
|
>
|
||||||
<User className="h-5 w-5 text-gray-600" />
|
<Info className="h-5 w-5 text-gray-700" />
|
||||||
<span className="text-xs text-gray-700">
|
<span className="text-xs text-gray-700 font-medium">
|
||||||
{authLoading
|
{t("common.info")}
|
||||||
? "..."
|
|
||||||
: isAuthenticated
|
|
||||||
? t("common.profile")
|
|
||||||
: t("common.login")}
|
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,46 +187,80 @@ export default function MobileBottomNav({
|
|||||||
|
|
||||||
{/* Category Sheet/Drawer */}
|
{/* Category Sheet/Drawer */}
|
||||||
<Sheet open={isCategoryOpen} onOpenChange={setIsCategoryOpen}>
|
<Sheet open={isCategoryOpen} onOpenChange={setIsCategoryOpen}>
|
||||||
<SheetContent side="left" className="w-[300px] p-0">
|
<SheetContent
|
||||||
<SheetHeader className="p-4 border-b">
|
side="left"
|
||||||
<SheetTitle>{t("common.catalog")}</SheetTitle>
|
className="w-[300px] p-0 rounded-none border-r-2 border-gray-200"
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
>
|
||||||
|
<SheetHeader className="p-6 border-b border-gray-200 bg-gradient-to-r from-gray-50 to-white">
|
||||||
|
<SheetTitle className="text-xl font-bold text-gray-900">
|
||||||
|
{t("common.catalog")}
|
||||||
|
</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<ScrollArea className="h-[calc(100vh-80px)]">
|
<ScrollArea className="h-[calc(100vh-88px)]">
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<div key={category.id} className="mb-4">
|
<div key={category.id} className="mb-1">
|
||||||
<Link
|
<div className="flex items-center">
|
||||||
href={`/category/${category.slug}?category_id=${category.id}`}
|
<Link
|
||||||
onClick={() => setIsCategoryOpen(false)}
|
href={`/category/${category.slug}?category_id=${category.id}`}
|
||||||
className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors font-semibold"
|
onClick={() => setIsCategoryOpen(false)}
|
||||||
>
|
className="flex-1 flex items-center gap-2 px-3 py-2 rounded-xl hover:bg-gray-100 transition-all font-bold text-gray-900 hover:text-gray-700"
|
||||||
<span>{category.name}</span>
|
>
|
||||||
</Link>
|
<span>{category.name}</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Toggle button if has children */}
|
||||||
|
{category.children && category.children.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => toggleCategory(category.id)}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className={`w-4 h-4 text-gray-600 transition-transform ${
|
||||||
|
expandedCategories.has(category.id)
|
||||||
|
? "rotate-180"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M19 9l-7 7-7-7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Subcategories */}
|
{/* Subcategories */}
|
||||||
{category.children && category.children.length > 0 && (
|
{category.children &&
|
||||||
<div className="ml-8 mt-2 space-y-1">
|
category.children.length > 0 &&
|
||||||
{category.children.map((child: any) => (
|
expandedCategories.has(category.id) && (
|
||||||
<Link
|
<div className="ml-6 mt-1 space-y-0.5">
|
||||||
key={child.id}
|
{category.children.map((child: any) => (
|
||||||
href={`/category/${child.slug}?category_id=${child.id}`}
|
<Link
|
||||||
onClick={() => setIsCategoryOpen(false)}
|
key={child.id}
|
||||||
className="block px-3 py-2 text-sm text-gray-600 hover:text-primary hover:bg-gray-50 rounded-lg transition-colors"
|
href={`/category/${child.slug}?category_id=${child.id}`}
|
||||||
>
|
onClick={() => setIsCategoryOpen(false)}
|
||||||
{child.name}
|
className="block px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-50 rounded-lg transition-all font-medium"
|
||||||
</Link>
|
>
|
||||||
))}
|
{child.name}
|
||||||
</div>
|
</Link>
|
||||||
)}
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
|
||||||
{/* Local Auth Dialog */}
|
|
||||||
<AuthDialog isOpen={isLoginOpen} onClose={() => setIsLoginOpen(false)} />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ function ActionButton({
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="relative flex-col gap-1 h-auto px-3 py-2.5 hover:bg-gray-100 transition-all duration-200 group"
|
className="relative cursor-pointer flex-col gap-1 h-auto px-3 py-2.5 hover:bg-gray-100 transition-all duration-200 group"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef } from "react";
|
import React, { useState } from "react";
|
||||||
import { Search, X, Loader2 } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
@@ -11,8 +11,6 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useSearchProducts } from "@/features/search/hooks/useSearch";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { SearchIcon } from "@/components/icons";
|
import { SearchIcon } from "@/components/icons";
|
||||||
|
|
||||||
interface SearchBarProps {
|
interface SearchBarProps {
|
||||||
@@ -34,95 +32,26 @@ export default function SearchBar({
|
|||||||
}: SearchBarProps) {
|
}: SearchBarProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [searchValue, setSearchValue] = useState("");
|
const [searchValue, setSearchValue] = useState("");
|
||||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
|
||||||
const [showResults, setShowResults] = useState(false);
|
|
||||||
const searchRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const { data, isLoading } = useSearchProducts({ q: debouncedSearch });
|
const performSearch = () => {
|
||||||
|
if (searchValue.trim()) {
|
||||||
useEffect(() => {
|
router.push(
|
||||||
const timer = setTimeout(() => {
|
`/${locale}/search?q=${encodeURIComponent(searchValue.trim())}`,
|
||||||
setDebouncedSearch(searchValue);
|
);
|
||||||
}, 300);
|
if (onClose) onClose();
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [searchValue]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (debouncedSearch && data?.data && data.data.length > 0) {
|
|
||||||
setShowResults(true);
|
|
||||||
} else {
|
|
||||||
setShowResults(false);
|
|
||||||
}
|
}
|
||||||
}, [debouncedSearch, data]);
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
if (e.key === "Enter") {
|
||||||
if (searchRef.current && !searchRef.current.contains(e.target as Node)) {
|
performSearch();
|
||||||
setShowResults(false);
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSearch = (value: string) => {
|
const handleSearch = (value: string) => {
|
||||||
setSearchValue(value);
|
setSearchValue(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleProductClick = (productId: number) => {
|
|
||||||
router.push(`/${locale}/product/${productId}`);
|
|
||||||
setSearchValue("");
|
|
||||||
setShowResults(false);
|
|
||||||
if (onClose) onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClearSearch = () => {
|
|
||||||
setSearchValue("");
|
|
||||||
setShowResults(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SearchResults = () => {
|
|
||||||
if (!showResults || !data?.data) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="absolute top-full left-0 right-0 mt-2 bg-white border border-gray-200 rounded-lg shadow-2xl max-h-[500px] overflow-y-auto z-50 animate-slide-up">
|
|
||||||
<div className="p-2">
|
|
||||||
{data.data.map((product, index) => (
|
|
||||||
<button
|
|
||||||
key={product.id}
|
|
||||||
onClick={() => handleProductClick(product.id)}
|
|
||||||
className={`w-full cursor-pointer flex items-center gap-4 p-3 rounded-lg hover:bg-gray-50 transition-all duration-200 group ${
|
|
||||||
index !== data.data.length - 1 ? "border-b border-gray-100" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="relative w-20 h-20 shrink-0 rounded-lg overflow-hidden bg-gray-50 border border-gray-100">
|
|
||||||
<Image
|
|
||||||
src={product.thumbnail}
|
|
||||||
alt={product.name}
|
|
||||||
fill
|
|
||||||
className="object-cover group-hover:scale-105 transition-transform duration-200"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 text-left min-w-0">
|
|
||||||
<p className="font-semibold text-sm line-clamp-2 text-gray-900 group-hover:text-gray-700 transition-colors mb-1">
|
|
||||||
{product.name}
|
|
||||||
</p>
|
|
||||||
<p className="text-base font-bold text-gray-900 mb-1">
|
|
||||||
{product.price_amount} TMT
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-gray-500 font-medium">
|
|
||||||
{product.brand.name}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
@@ -132,22 +61,22 @@ export default function SearchBar({
|
|||||||
{searchPlaceholder}
|
{searchPlaceholder}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="relative" ref={searchRef}>
|
<div className="relative">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={searchPlaceholder}
|
placeholder={searchPlaceholder}
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={(e) => handleSearch(e.target.value)}
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
className="h-12 rounded-lg pl-12 pr-10 border-gray-200 focus:border-gray-900 focus-visible:border-gray-900 focus-visible:ring-0 transition-colors"
|
className="h-12 rounded-lg pl-12 pr-10 border-gray-200 focus:border-gray-900 focus-visible:border-gray-900 focus-visible:ring-0 transition-colors"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
<Search
|
||||||
{isLoading && (
|
className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 cursor-pointer"
|
||||||
<Loader2 className="absolute right-4 top-1/2 -translate-y-1/2 h-5 w-5 animate-spin text-gray-400" />
|
onClick={performSearch}
|
||||||
)}
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SearchResults />
|
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -157,7 +86,6 @@ export default function SearchBar({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`bg-gray-900 rounded-lg flex items-center relative shadow-sm hover:shadow-md transition-shadow duration-200 ${className}`}
|
className={`bg-gray-900 rounded-lg flex items-center relative shadow-sm hover:shadow-md transition-shadow duration-200 ${className}`}
|
||||||
ref={searchRef}
|
|
||||||
>
|
>
|
||||||
<div className="w-full relative">
|
<div className="w-full relative">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -166,21 +94,19 @@ export default function SearchBar({
|
|||||||
placeholder={searchPlaceholder}
|
placeholder={searchPlaceholder}
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onChange={(e) => handleSearch(e.target.value)}
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
className="border w-full rounded-lg h-11 border-gray-900 bg-white pl-12 pr-4 focus-visible:ring-2 focus-visible:ring-gray-300 transition-all"
|
className="border w-full rounded-lg h-11 border-gray-900 bg-white pl-12 pr-4 focus-visible:ring-2 focus-visible:ring-gray-300 transition-all"
|
||||||
/>
|
/>
|
||||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 pointer-events-none" />
|
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
{isLoading && (
|
|
||||||
<Loader2 className="absolute right-4 top-1/2 -translate-y-1/2 h-5 w-5 animate-spin text-gray-400" />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
|
onClick={performSearch}
|
||||||
className="h-11 w-11 hover:bg-gray-800 cursor-pointer bg-transparent flex items-center mr-1 text-white rounded-lg transition-colors"
|
className="h-11 w-11 hover:bg-gray-800 cursor-pointer bg-transparent flex items-center mr-1 text-white rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
</Button>
|
</Button>
|
||||||
<SearchResults />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,11 +277,11 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className="p-6 shadow-sm border border-gray-200 rounded-lg hover:shadow-md transition-shadow duration-200">
|
<Card className="p-3 shadow-sm border border-gray-200 rounded-lg hover:shadow-md transition-shadow duration-200">
|
||||||
<div className="flex flex-col sm:flex-row gap-6">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
{/* Product Image & Info */}
|
{/* Product Image & Info */}
|
||||||
<div className="flex gap-4 flex-1">
|
<div className="md:flex gap-4 flex-1">
|
||||||
<div className="relative w-[200px] h-[200px] rounded-lg border border-gray-200 overflow-hidden shrink-0 bg-gray-50">
|
<div className="relative w-full h-full min-h-[200px] rounded-lg border border-gray-200 overflow-hidden shrink-0 bg-gray-50">
|
||||||
<Image
|
<Image
|
||||||
src={getImageSrc()}
|
src={getImageSrc()}
|
||||||
alt={item.product.name}
|
alt={item.product.name}
|
||||||
@@ -290,13 +290,27 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
</div>
|
||||||
|
<div className="flex items-start gap-2 pt-2">
|
||||||
<h3 className="font-bold text-base text-gray-900 line-clamp-2">
|
<h3 className="font-bold text-base text-gray-900 line-clamp-2">
|
||||||
{item.product.name}
|
{item.product.name}
|
||||||
</h3>
|
</h3>
|
||||||
{/* <p className="text-sm text-gray-500 font-medium">
|
{/* <div
|
||||||
{item.seller?.name || "Store"}
|
className="text-gray-700 leading-relaxed prose prose-sm max-w-none
|
||||||
</p> */}
|
prose-headings:text-gray-900 prose-headings:font-bold
|
||||||
|
prose-p:text-gray-700 prose-p:leading-relaxed
|
||||||
|
prose-ul:text-gray-700 prose-ol:text-gray-700
|
||||||
|
prose-li:text-gray-700 prose-li:leading-relaxed
|
||||||
|
prose-strong:text-gray-900 prose-strong:font-semibold
|
||||||
|
prose-a:text-gray-900 prose-a:font-medium hover:prose-a:text-gray-700"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html:
|
||||||
|
item.product.description &&
|
||||||
|
item.product.description.length > 175
|
||||||
|
? item.product.description.substring(0, 175) + "..."
|
||||||
|
: item.product.description || "",
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
|
||||||
{/* {availableStock <= 5 && (
|
{/* {availableStock <= 5 && (
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
@@ -312,15 +326,14 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
disabled={isRemoving}
|
disabled={isRemoving}
|
||||||
className="w-fit cursor-pointer p-0 h-auto hover:bg-transparent text-gray-600 hover:text-red-500 transition-colors group"
|
className="w-fit cursor-pointer pt-1 h-auto hover:bg-transparent text-gray-600 hover:text-red-500 transition-colors group"
|
||||||
>
|
>
|
||||||
<Trash2 className="h-5 w-5 group-hover:scale-110 transition-transform" />
|
<Trash2 className="h-5 w-5 group-hover:scale-110 transition-transform" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Price & Quantity */}
|
{/* Price & Quantity */}
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-6 justify-between">
|
<div className="flex flex-col items-start gap-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-gray-600">
|
<p className="text-sm font-medium text-gray-600">
|
||||||
{t("unit_price")}{" "}
|
{t("unit_price")}{" "}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ export default function OrderSummary({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full md:w-[420px] p-8 rounded-lg border border-gray-200 shadow-lg h-fit sticky top-20">
|
<Card className="w-full lg:w-[340px] md:w-[300px] p-6 rounded-lg border border-gray-200 shadow-lg h-fit sticky top-20">
|
||||||
{/* Customer Information */}
|
{/* Customer Information */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h3 className="text-xl font-bold mb-5 text-gray-900">
|
<h3 className="text-xl font-bold mb-5 text-gray-900">
|
||||||
@@ -150,7 +150,7 @@ export default function OrderSummary({
|
|||||||
onChange={(e) => onNameChange(e.target.value)}
|
onChange={(e) => onNameChange(e.target.value)}
|
||||||
placeholder={t("name")}
|
placeholder={t("name")}
|
||||||
className={`rounded-[10px] h-12 border-2 transition-colors ${
|
className={`rounded-[10px] h-12 border-2 transition-colors ${
|
||||||
showValidation && name.trim() === ""
|
showValidation && name.trim() === ""
|
||||||
? "border-red-500"
|
? "border-red-500"
|
||||||
: "border-gray-200 focus:border-gray-900"
|
: "border-gray-200 focus:border-gray-900"
|
||||||
}`}
|
}`}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from "@/components/ui/sheet";
|
} from "@/components/ui/sheet";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
interface CategoryFiltersSheetProps {
|
interface CategoryFiltersSheetProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -24,6 +25,24 @@ export default function CategoryFiltersSheet({
|
|||||||
closeLabel,
|
closeLabel,
|
||||||
children,
|
children,
|
||||||
}: CategoryFiltersSheetProps) {
|
}: CategoryFiltersSheetProps) {
|
||||||
|
const [touchStart, setTouchStart] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const handleTouchStart = (e: React.TouchEvent) => {
|
||||||
|
setTouchStart(e.targetTouches[0].clientX);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = (e: React.TouchEvent) => {
|
||||||
|
if (touchStart === null) return;
|
||||||
|
const touchEnd = e.changedTouches[0].clientX;
|
||||||
|
const distance = touchStart - touchEnd;
|
||||||
|
|
||||||
|
// Side is left, so swiping left (positive distance) closes it
|
||||||
|
if (distance > 50) {
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
setTouchStart(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={isOpen} onOpenChange={onOpenChange}>
|
<Sheet open={isOpen} onOpenChange={onOpenChange}>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
@@ -35,7 +54,12 @@ export default function CategoryFiltersSheet({
|
|||||||
<SlidersHorizontal className="h-5 w-5" />
|
<SlidersHorizontal className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent side="left" className="w-[290px] p-0">
|
<SheetContent
|
||||||
|
side="left"
|
||||||
|
className="w-[290px] p-0"
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
>
|
||||||
<SheetHeader className="p-4 border-b text-gray-900">
|
<SheetHeader className="p-4 border-b text-gray-900">
|
||||||
<SheetTitle className="text-gray-900">{filterLabel}</SheetTitle>
|
<SheetTitle className="text-gray-900">{filterLabel}</SheetTitle>
|
||||||
<button
|
<button
|
||||||
@@ -46,10 +70,8 @@ export default function CategoryFiltersSheet({
|
|||||||
<span className="sr-only">{closeLabel}</span>
|
<span className="sr-only">{closeLabel}</span>
|
||||||
</button>
|
</button>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<ScrollArea className="h-[calc(100vh-80px)] p-4">
|
<ScrollArea className="h-[calc(100vh-80px)] p-4">{children}</ScrollArea>
|
||||||
{children}
|
|
||||||
</ScrollArea>
|
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from "@/components/ui/sheet";
|
} from "@/components/ui/sheet";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
interface CollectionFiltersSheetProps {
|
interface CollectionFiltersSheetProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -24,18 +25,41 @@ export default function CollectionFiltersSheet({
|
|||||||
closeLabel,
|
closeLabel,
|
||||||
children,
|
children,
|
||||||
}: CollectionFiltersSheetProps) {
|
}: CollectionFiltersSheetProps) {
|
||||||
|
const [touchStart, setTouchStart] = useState<number | null>(null);
|
||||||
|
|
||||||
|
const handleTouchStart = (e: React.TouchEvent) => {
|
||||||
|
setTouchStart(e.targetTouches[0].clientX);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = (e: React.TouchEvent) => {
|
||||||
|
if (touchStart === null) return;
|
||||||
|
const touchEnd = e.changedTouches[0].clientX;
|
||||||
|
const distance = touchStart - touchEnd;
|
||||||
|
|
||||||
|
// Side is left, so swiping left (positive distance) closes it
|
||||||
|
if (distance > 50) {
|
||||||
|
onOpenChange(false);
|
||||||
|
}
|
||||||
|
setTouchStart(null);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={isOpen} onOpenChange={onOpenChange}>
|
<Sheet open={isOpen} onOpenChange={onOpenChange}>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
className=" border-gray-200 hover:border-gray-900 hover:bg-gray-50 transition-all duration-200 sm:hidden fixed bottom-20 right-4 rounded-[10px] cursor-pointer font-bold gap-2 z-10 shadow-lg"
|
className=" border-gray-200 hover:border-gray-900 hover:bg-gray-50 transition-all duration-200 sm:hidden fixed bottom-20 right-4 rounded-[10px] cursor-pointer font-bold gap-2 z-10 shadow-lg"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
{filterLabel}
|
{filterLabel}
|
||||||
<SlidersHorizontal className="h-5 w-5" />
|
<SlidersHorizontal className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent side="left" className="w-[290px] p-0">
|
<SheetContent
|
||||||
|
side="left"
|
||||||
|
className="w-[290px] p-0"
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
>
|
||||||
<SheetHeader className="p-4 border-b">
|
<SheetHeader className="p-4 border-b">
|
||||||
<SheetTitle className="text-gray-900">{filterLabel}</SheetTitle>
|
<SheetTitle className="text-gray-900">{filterLabel}</SheetTitle>
|
||||||
<button
|
<button
|
||||||
@@ -46,10 +70,8 @@ export default function CollectionFiltersSheet({
|
|||||||
<span className="sr-only">{closeLabel}</span>
|
<span className="sr-only">{closeLabel}</span>
|
||||||
</button>
|
</button>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<ScrollArea className="h-[calc(100vh-80px)] p-4">
|
<ScrollArea className="h-[calc(100vh-80px)] p-4">{children}</ScrollArea>
|
||||||
{children}
|
|
||||||
</ScrollArea>
|
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,21 +8,21 @@ export default function EmptyFavorites() {
|
|||||||
const router=useRouter();
|
const router=useRouter();
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[60vh] items-center justify-center px-4">
|
<div className="flex min-h-[60vh] items-center justify-center px-4">
|
||||||
<div className="w-full max-w-md rounded-2xl bg-linear-to-br from-blue-50 to-white p-8 text-center shadow-lg">
|
<div className="w-full max-w-md rounded-3xl bg-gradient-to-br from-gray-50 to-white p-12 text-center shadow-xl border border-gray-100">
|
||||||
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-blue-100">
|
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-blue-100">
|
||||||
<Heart className="h-10 w-10 text-blue-600" />
|
<Heart className="h-12 w-12 text-gray-700" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="mb-2 text-2xl font-semibold text-gray-900">
|
<h2 className="mb-2 text-2xl font-semibold text-gray-900">
|
||||||
{t("favorites_empty")}
|
{t("favorites_empty")}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="mb-6 text-sm text-gray-500">
|
<p className="mb-8 text-base text-gray-600 leading-relaxed">
|
||||||
{t("favorites_empty_message")}
|
{t("favorites_empty_message")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button onClick={()=>router.push("/")} className="w-full rounded-lg cursor-pointer bg-blue-600 px-6 py-3 text-sm font-medium text-white transition hover:bg-blue-700 active:scale-95">
|
<Button onClick={()=>router.push("/")} className="w-full cursor-pointer rounded-2xl bg-gradient-to-r from-gray-900 to-gray-800 hover:from-gray-800 hover:to-gray-700 px-8 py-6 text-base font-bold text-white shadow-lg hover:shadow-xl transition-all duration-300 active:scale-95"
|
||||||
{t("start_shopping")}
|
> {t("start_shopping")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ export default function HeroCarousel({ items }: { items: CarouselItem[] }) {
|
|||||||
className="
|
className="
|
||||||
[&_.swiper-button-next]:text-white!
|
[&_.swiper-button-next]:text-white!
|
||||||
[&_.swiper-button-prev]:text-white!
|
[&_.swiper-button-prev]:text-white!
|
||||||
|
[&_.swiper-button-next]:hidden!
|
||||||
|
[&_.swiper-button-prev]:hidden!
|
||||||
|
md:[&_.swiper-button-next]:flex!
|
||||||
|
md:[&_.swiper-button-prev]:flex!
|
||||||
[&_.swiper-pagination-bullet]:bg-white!
|
[&_.swiper-pagination-bullet]:bg-white!
|
||||||
[&_.swiper-pagination-bullet-active]:bg-white!
|
[&_.swiper-pagination-bullet-active]:bg-white!
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -294,12 +294,12 @@ export default function ProductCard({
|
|||||||
<>
|
<>
|
||||||
<CarouselPrevious
|
<CarouselPrevious
|
||||||
data-carousel-control="true"
|
data-carousel-control="true"
|
||||||
className="absolute left-3 opacity-0 group-hover:opacity-100 transition-all duration-300 z-20 h-9 w-9 bg-white/95 hover:bg-white border-0 shadow-lg"
|
className="absolute cursor-pointer left-3 opacity-0 group-hover:opacity-100 transition-all duration-300 z-20 h-9 w-9 bg-white/95 hover:bg-white border-0 shadow-lg"
|
||||||
onClick={(e) => handleNavClick(e, () => api?.scrollPrev())}
|
onClick={(e) => handleNavClick(e, () => api?.scrollPrev())}
|
||||||
/>
|
/>
|
||||||
<CarouselNext
|
<CarouselNext
|
||||||
data-carousel-control="true"
|
data-carousel-control="true"
|
||||||
className="absolute right-3 opacity-0 group-hover:opacity-100 transition-all duration-300 z-20 h-9 w-9 bg-white/95 hover:bg-white border-0 shadow-lg"
|
className="absolute cursor-pointer right-3 opacity-0 group-hover:opacity-100 transition-all duration-300 z-20 h-9 w-9 bg-white/95 hover:bg-white border-0 shadow-lg"
|
||||||
onClick={(e) => handleNavClick(e, () => api?.scrollNext())}
|
onClick={(e) => handleNavClick(e, () => api?.scrollNext())}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@@ -382,7 +382,7 @@ export default function ProductCard({
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleFavorite}
|
onClick={handleFavorite}
|
||||||
disabled={isFavoriteToggling || isFavoriteLoading}
|
disabled={isFavoriteToggling || isFavoriteLoading}
|
||||||
className=" w-9 h-9 rounded-[10px] bg-white/95 backdrop-blur-sm hover:bg-white hover:scale-110 transition-all duration-200 shadow-md disabled:opacity-50"
|
className=" w-7 h-7 md:w-9 cursor-pointer md:h-9 rounded-[10px] bg-white/95 backdrop-blur-sm hover:bg-white hover:scale-110 transition-all duration-200 shadow-md disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{isFavoriteLoading ? (
|
{isFavoriteLoading ? (
|
||||||
<div className="w-5 h-5 border-2 border-gray-200 border-t-gray-700 rounded-full animate-spin" />
|
<div className="w-5 h-5 border-2 border-gray-200 border-t-gray-700 rounded-full animate-spin" />
|
||||||
@@ -403,7 +403,7 @@ export default function ProductCard({
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleAddToCart}
|
onClick={handleAddToCart}
|
||||||
disabled={isSyncing}
|
disabled={isSyncing}
|
||||||
className="w-full h-9 rounded-[10px] bg-gradient-to-r from-gray-900 to-gray-800 hover:from-gray-800 hover:to-gray-700 text-white font-semibold shadow-md hover:shadow-lg transition-all duration-300 gap-2"
|
className="w-full h-7 md:h-9 cursor-pointer rounded-[10px] bg-gradient-to-r from-gray-900 to-gray-800 hover:from-gray-800 hover:to-gray-700 text-white font-semibold shadow-md hover:shadow-lg transition-all duration-300 gap-2"
|
||||||
>
|
>
|
||||||
{isSyncing ? (
|
{isSyncing ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -426,12 +426,12 @@ export default function ProductCard({
|
|||||||
size="icon"
|
size="icon"
|
||||||
onClick={(e) => handleQuantityChange(e, -1)}
|
onClick={(e) => handleQuantityChange(e, -1)}
|
||||||
disabled={isSyncing || localQuantity <= 1}
|
disabled={isSyncing || localQuantity <= 1}
|
||||||
className="rounded-[10px] h-9 w-9 border-2 border-gray-200 hover:border-gray-900 hover:bg-gray-50 transition-all duration-200 disabled:opacity-30"
|
className="rounded-[10px] cursor-pointer h-7 md:h-9 w-7 md:w-9 border-2 border-gray-200 hover:border-gray-900 hover:bg-gray-50 transition-all duration-200 disabled:opacity-30"
|
||||||
>
|
>
|
||||||
<Minus className="h-5 w-5 text-gray-700" />
|
<Minus className="h-5 w-5 text-gray-700" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="flex-1 text-center font-bold text-lg border-2 border-gray-200 rounded-[10px] h-9 flex items-center justify-center bg-white relative">
|
<div className="flex-1 text-center font-bold text-sm md:text-lg border-2 border-gray-200 rounded-[10px] h-7 md:h-9 flex items-center justify-center bg-white relative">
|
||||||
{isSyncing && (
|
{isSyncing && (
|
||||||
<div className="absolute inset-0 bg-white/80 rounded-xl flex items-center justify-center">
|
<div className="absolute inset-0 bg-white/80 rounded-xl flex items-center justify-center">
|
||||||
<div className="w-4 h-4 border-2 border-gray-300 border-t-gray-700 rounded-full animate-spin" />
|
<div className="w-4 h-4 border-2 border-gray-300 border-t-gray-700 rounded-full animate-spin" />
|
||||||
@@ -447,7 +447,7 @@ export default function ProductCard({
|
|||||||
size="icon"
|
size="icon"
|
||||||
onClick={(e) => handleQuantityChange(e, 1)}
|
onClick={(e) => handleQuantityChange(e, 1)}
|
||||||
disabled={isSyncing}
|
disabled={isSyncing}
|
||||||
className="rounded-[10px] h-9 w-9 border-2 border-gray-900 bg-gray-900 hover:bg-gray-800 transition-all duration-200 disabled:opacity-30"
|
className="rounded-[10px] cursor-pointer h-7 md:h-9 w-7 md:w-9 border-2 border-gray-900 bg-gray-900 hover:bg-gray-800 transition-all duration-200 disabled:opacity-30"
|
||||||
>
|
>
|
||||||
<Plus className="h-5 w-5 text-white" />
|
<Plus className="h-5 w-5 text-white" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -8,21 +8,21 @@ export default function EmptyOrders() {
|
|||||||
const router=useRouter();
|
const router=useRouter();
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[60vh] items-center justify-center px-4">
|
<div className="flex min-h-[60vh] items-center justify-center px-4">
|
||||||
<div className="w-full max-w-md rounded-2xl bg-linear-to-br from-blue-50 to-white p-8 text-center shadow-lg">
|
<div className="w-full max-w-md rounded-3xl bg-gradient-to-br from-gray-50 to-white p-12 text-center shadow-xl border border-gray-100">
|
||||||
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-blue-100">
|
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-blue-100">
|
||||||
<ShoppingCart className="h-10 w-10 text-blue-600" />
|
<ShoppingCart className="h-12 w-12 text-gray-700" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 className="mb-2 text-2xl font-semibold text-gray-900">
|
<h2 className="mb-2 text-2xl font-semibold text-gray-900">
|
||||||
{t("orders_empty")}
|
{t("orders_empty")}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="mb-6 text-sm text-gray-500">
|
<p className="mb-8 text-base text-gray-600 leading-relaxed">
|
||||||
{t("orders_empty_message")}
|
{t("orders_empty_message")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Button onClick={()=>router.push("/")} className="w-full rounded-lg cursor-pointer bg-blue-600 px-6 py-3 text-sm font-medium text-white transition hover:bg-blue-700 active:scale-95">
|
<Button onClick={()=>router.push("/")} className="w-full cursor-pointer rounded-2xl bg-gradient-to-r from-gray-900 to-gray-800 hover:from-gray-800 hover:to-gray-700 px-8 py-6 text-base font-bold text-white shadow-lg hover:shadow-xl transition-all duration-300 active:scale-95"
|
||||||
{t("start_shopping")}
|
> {t("start_shopping")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
|||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="bg-yellow-50 text-yellow-700 border-yellow-300"
|
className="bg-amber-50 text-amber-700 border-amber-300 font-semibold"
|
||||||
>
|
>
|
||||||
{status}
|
{status}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -100,7 +100,10 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
|||||||
lowerStatus.includes("işlenýär")
|
lowerStatus.includes("işlenýär")
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<Badge variant="secondary" className="bg-blue-50 text-blue-700">
|
<Badge
|
||||||
|
variant="secondary"
|
||||||
|
className="bg-gray-100 text-gray-900 font-semibold"
|
||||||
|
>
|
||||||
{status}
|
{status}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
@@ -110,24 +113,36 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
|||||||
lowerStatus.includes("shipped") ||
|
lowerStatus.includes("shipped") ||
|
||||||
lowerStatus.includes("iberildi")
|
lowerStatus.includes("iberildi")
|
||||||
) {
|
) {
|
||||||
return <Badge className="bg-purple-500">{status}</Badge>;
|
return (
|
||||||
|
<Badge className="bg-indigo-500 hover:bg-indigo-500 font-semibold">
|
||||||
|
{status}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
lowerStatus.includes("доставлен") ||
|
lowerStatus.includes("доставлен") ||
|
||||||
lowerStatus.includes("delivered") ||
|
lowerStatus.includes("delivered") ||
|
||||||
lowerStatus.includes("eltildi")
|
lowerStatus.includes("eltildi")
|
||||||
) {
|
) {
|
||||||
return <Badge className="bg-green-600">{status}</Badge>;
|
return (
|
||||||
|
<Badge className="bg-emerald-600 hover:bg-emerald-600 font-semibold">
|
||||||
|
{status}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
lowerStatus.includes("отменен") ||
|
lowerStatus.includes("отменен") ||
|
||||||
lowerStatus.includes("cancelled") ||
|
lowerStatus.includes("cancelled") ||
|
||||||
lowerStatus.includes("ýatyryldy")
|
lowerStatus.includes("ýatyryldy")
|
||||||
) {
|
) {
|
||||||
return <Badge variant="destructive">{status}</Badge>;
|
return (
|
||||||
|
<Badge className="bg-red-600 hover:bg-red-600 font-semibold">
|
||||||
|
{status}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Badge>{status}</Badge>;
|
return <Badge className="font-semibold">{status}</Badge>;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const isActiveOrder = useCallback((status: string) => {
|
const isActiveOrder = useCallback((status: string) => {
|
||||||
@@ -147,11 +162,11 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
|||||||
|
|
||||||
const activeOrders = useMemo(
|
const activeOrders = useMemo(
|
||||||
() => orders?.filter((o) => isActiveOrder(o.status)) || [],
|
() => orders?.filter((o) => isActiveOrder(o.status)) || [],
|
||||||
[orders, isActiveOrder]
|
[orders, isActiveOrder],
|
||||||
);
|
);
|
||||||
const completedOrders = useMemo(
|
const completedOrders = useMemo(
|
||||||
() => orders?.filter((o) => !isActiveOrder(o.status)) || [],
|
() => orders?.filter((o) => !isActiveOrder(o.status)) || [],
|
||||||
[orders, isActiveOrder]
|
[orders, isActiveOrder],
|
||||||
);
|
);
|
||||||
|
|
||||||
const calculateTotal = useCallback((order: Order) => {
|
const calculateTotal = useCallback((order: Order) => {
|
||||||
@@ -163,38 +178,39 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto p-2 lg:p-6 md:p-4 mb-16 min-h-screen">
|
<div className="mx-auto p-2 lg:p-6 md:p-4 mb-16 min-h-screen">
|
||||||
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold mb-6">
|
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold mb-6 text-gray-900">
|
||||||
{t("my_orders")}
|
{t("my_orders")}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Tabs Skeleton */}
|
{/* Tabs Skeleton */}
|
||||||
<div className="mb-4 md:mb-6">
|
<div className="mb-4 md:mb-6">
|
||||||
<div className="flex gap-2 mb-4">
|
<div className="flex gap-2 mb-4">
|
||||||
<Skeleton className="h-10 w-32 rounded-md" />
|
<Skeleton className="h-10 w-32 rounded-xl" />
|
||||||
<Skeleton className="h-10 w-32 rounded-md" />
|
<Skeleton className="h-10 w-32 rounded-xl" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Order Cards Skeleton */}
|
{/* Order Cards Skeleton */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{Array.from({ length: 3 }).map((_, i) => (
|
{Array.from({ length: 3 }).map((_, i) => (
|
||||||
<Card key={i} className="overflow-hidden py-2 md:py-4 lg:py-6">
|
<Card
|
||||||
|
key={i}
|
||||||
|
className="overflow-hidden py-2 md:py-4 lg:py-6 rounded-2xl border border-gray-200"
|
||||||
|
>
|
||||||
<div className="p-2 md:p-4 mx-2 md:mx-4 rounded-lg">
|
<div className="p-2 md:p-4 mx-2 md:mx-4 rounded-lg">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{/* Left side - Order info */}
|
|
||||||
<div className="flex items-center gap-4 flex-1">
|
<div className="flex items-center gap-4 flex-1">
|
||||||
<Skeleton className="h-5 w-5 rounded" />
|
<Skeleton className="h-5 w-5 rounded" />
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Skeleton className="h-5 w-32" />
|
<Skeleton className="h-5 w-32 rounded-lg" />
|
||||||
<Skeleton className="h-4 w-24" />
|
<Skeleton className="h-4 w-24 rounded-lg" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side - Status and price */}
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex flex-col md:flex-row gap-2 items-end">
|
<div className="flex flex-col md:flex-row gap-2 items-end">
|
||||||
<Skeleton className="h-6 w-20 rounded-full" />
|
<Skeleton className="h-6 w-20 rounded-full" />
|
||||||
<Skeleton className="h-6 w-24" />
|
<Skeleton className="h-6 w-24 rounded-lg" />
|
||||||
</div>
|
</div>
|
||||||
<Skeleton className="h-5 w-5 rounded" />
|
<Skeleton className="h-5 w-5 rounded" />
|
||||||
</div>
|
</div>
|
||||||
@@ -215,17 +231,23 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className=" mx-auto p-2 lg:p-6 md:p-4 mb-16 min-h-screen">
|
<div className="mx-auto p-2 lg:p-6 md:p-4 mb-16 min-h-screen">
|
||||||
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold mb-6">
|
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold mb-6 text-gray-900">
|
||||||
{t("my_orders")}
|
{t("my_orders")}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<Tabs defaultValue="active" className="w-full">
|
<Tabs defaultValue="active" className="w-full">
|
||||||
<TabsList className="mb-4 md:mb-6 w-full md:w-fit gap-2 p-0">
|
<TabsList className="mb-4 md:mb-6 w-full md:w-fit gap-2 p-1 bg-gray-100 rounded-xl">
|
||||||
<TabsTrigger value="active">
|
<TabsTrigger
|
||||||
|
value="active"
|
||||||
|
className="rounded-lg font-semibold data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm"
|
||||||
|
>
|
||||||
{t("active_orders")} ({activeOrders.length})
|
{t("active_orders")} ({activeOrders.length})
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="completed">
|
<TabsTrigger
|
||||||
|
value="completed"
|
||||||
|
className="rounded-lg font-semibold data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm"
|
||||||
|
>
|
||||||
{t("completed_orders")} ({completedOrders.length})
|
{t("completed_orders")} ({completedOrders.length})
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -233,7 +255,9 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
|||||||
<TabsContent value="active">
|
<TabsContent value="active">
|
||||||
{activeOrders.length === 0 ? (
|
{activeOrders.length === 0 ? (
|
||||||
<div className="flex items-center justify-center min-h-[40vh]">
|
<div className="flex items-center justify-center min-h-[40vh]">
|
||||||
<p className="text-xl text-gray-400">{t("no_active_orders")}</p>
|
<p className="text-xl text-gray-400 font-medium">
|
||||||
|
{t("no_active_orders")}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -258,7 +282,7 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
|||||||
<TabsContent value="completed">
|
<TabsContent value="completed">
|
||||||
{completedOrders.length === 0 ? (
|
{completedOrders.length === 0 ? (
|
||||||
<div className="flex items-center justify-center min-h-[40vh]">
|
<div className="flex items-center justify-center min-h-[40vh]">
|
||||||
<p className="text-xl text-gray-400">
|
<p className="text-xl text-gray-400 font-medium">
|
||||||
{t("no_completed_orders")}
|
{t("no_completed_orders")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -284,27 +308,28 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Dialog open={isCancelDialogOpen} onOpenChange={setIsCancelDialogOpen}>
|
<Dialog open={isCancelDialogOpen} onOpenChange={setIsCancelDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent className="rounded-3xl border border-gray-200">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle className="text-xl font-bold text-gray-900">
|
||||||
{t("cancel_order")} #{orderToCancel?.id}
|
{t("cancel_order")} #{orderToCancel?.id}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>{t("cancel_confirmation")}</DialogDescription>
|
<DialogDescription className="text-gray-600">
|
||||||
|
{t("cancel_confirmation")}
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter className="gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setIsCancelDialogOpen(false)}
|
onClick={() => setIsCancelDialogOpen(false)}
|
||||||
disabled={isCancellingOrder}
|
disabled={isCancellingOrder}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer rounded-xl border-2 border-gray-200 hover:border-gray-900 font-semibold"
|
||||||
>
|
>
|
||||||
{t("keep_order")}
|
{t("keep_order")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
|
||||||
onClick={confirmCancelOrder}
|
onClick={confirmCancelOrder}
|
||||||
disabled={isCancellingOrder}
|
disabled={isCancellingOrder}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer rounded-xl bg-red-600 hover:bg-red-700 font-semibold"
|
||||||
>
|
>
|
||||||
{isCancellingOrder ? t("cancelling") : t("cancel_order")}
|
{isCancellingOrder ? t("cancelling") : t("cancel_order")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -342,21 +367,23 @@ function CompactOrderCard({
|
|||||||
const itemCount = order.orderItems.length;
|
const itemCount = order.orderItems.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="overflow-hidden transition-all py-2 md:py-4 lg:py-6 hover:shadow-md">
|
<Card className="overflow-hidden transition-all py-2 md:py-4 lg:py-6 hover:shadow-lg rounded-2xl border border-gray-200">
|
||||||
{/* Compact Header - Always Visible */}
|
{/* Compact Header - Always Visible */}
|
||||||
<div
|
<div
|
||||||
className="p-2 md:p-4 mx-2 md:mx-4 rounded-lg cursor-pointer bg-linear-to-r from-white to-gray-50 hover:from-gray-50 hover:to-gray-100 transition-colors"
|
className="p-2 md:p-4 mx-2 md:mx-4 rounded-xl cursor-pointer bg-gradient-to-r from-white to-gray-50 hover:from-gray-50 hover:to-gray-100 transition-all duration-200"
|
||||||
onClick={onToggle}
|
onClick={onToggle}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4 flex-1">
|
<div className="flex items-center gap-4 flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Package className="h-5 w-5 text-gray-500" />
|
<div className="h-10 w-10 rounded-xl bg-gray-100 flex items-center justify-center">
|
||||||
|
<Package className="h-5 w-5 text-gray-700" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-base lg:text-lg">
|
<h3 className="font-bold text-base lg:text-lg text-gray-900">
|
||||||
{t("order_number")} {order.id}
|
{t("order_number")} {order.id}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500 font-medium">
|
||||||
{itemCount} {itemCount === 1 ? t("product") : t("products")}
|
{itemCount} {itemCount === 1 ? t("product") : t("products")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -367,83 +394,80 @@ function CompactOrderCard({
|
|||||||
<div className="flex flex-col md:flex-row gap-2 items-end">
|
<div className="flex flex-col md:flex-row gap-2 items-end">
|
||||||
{getStatusBadge(order.status)}
|
{getStatusBadge(order.status)}
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="font-bold text-lg text-green-600">
|
<p className="font-bold text-lg text-emerald-600">
|
||||||
{total.toFixed(2)} TMT
|
{total.toFixed(2)} TMT
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isExpanded ? (
|
<div className="h-8 w-8 rounded-lg bg-gray-100 flex items-center justify-center">
|
||||||
<ChevronUp className="h-5 w-5 text-gray-400" />
|
{isExpanded ? (
|
||||||
) : (
|
<ChevronUp className="h-5 w-5 text-gray-600" />
|
||||||
<ChevronDown className="h-5 w-5 text-gray-400" />
|
) : (
|
||||||
)}
|
<ChevronDown className="h-5 w-5 text-gray-600" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expandable Details */}
|
{/* Expandable Details */}
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="border-t bg-white">
|
<div className="border-t border-gray-200 bg-white">
|
||||||
{/* Order Info Grid */}
|
{/* Order Info Grid */}
|
||||||
<div className="p-4 grid grid-cols-1 md:grid-cols-2 gap-4 bg-gray-50">
|
<div className="p-4 grid grid-cols-1 md:grid-cols-2 gap-4 bg-gray-50">
|
||||||
{/* <div className="flex items-start gap-3">
|
|
||||||
<Calendar className="h-5 w-5 text-blue-500 mt-0.5" />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium text-gray-700">
|
|
||||||
{t("delivery_date")}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-900">
|
|
||||||
{new Date(order.delivery_at).toLocaleDateString()} •{" "}
|
|
||||||
{order.delivery_time}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<MapPin className="h-5 w-5 text-red-500 mt-0.5" />
|
<div className="h-9 w-9 rounded-xl bg-red-100 flex items-center justify-center shrink-0">
|
||||||
|
<MapPin className="h-5 w-5 text-red-600" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-700">
|
<p className="text-sm font-bold text-gray-900">
|
||||||
{t("address")}
|
{t("address")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-900">
|
<p className="text-sm text-gray-600 mt-0.5">
|
||||||
{order.customer_address}
|
{order.customer_address}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<CreditCard className="h-5 w-5 text-green-500 mt-0.5" />
|
<div className="h-9 w-9 rounded-xl bg-emerald-100 flex items-center justify-center shrink-0">
|
||||||
|
<CreditCard className="h-5 w-5 text-emerald-600" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-700">
|
<p className="text-sm font-bold text-gray-900">
|
||||||
{t("payment_method")}
|
{t("payment_method")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-900">{order.payment_type}</p>
|
<p className="text-sm text-gray-600 mt-0.5">
|
||||||
|
{order.payment_type}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<ShoppingBag className="h-5 w-5 text-purple-500 mt-0.5" />
|
<div className="h-9 w-9 rounded-xl bg-indigo-100 flex items-center justify-center shrink-0">
|
||||||
|
<ShoppingBag className="h-5 w-5 text-indigo-600" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-gray-700">
|
<p className="text-sm font-bold text-gray-900">
|
||||||
{t("shipping_method")}
|
{t("shipping_method")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-900">{order.shipping_method}</p>
|
<p className="text-sm text-gray-600 mt-0.5">
|
||||||
|
{order.shipping_method}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Products List */}
|
{/* Products List */}
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<h4 className="font-semibold mb-3 text-gray-700">
|
<h4 className="font-bold mb-3 text-gray-900">{t("products")}:</h4>
|
||||||
{t("products")}:
|
|
||||||
</h4>
|
|
||||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||||
{order.orderItems.map((item, index) => (
|
{order.orderItems.map((item, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="flex items-center gap-4 p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
|
className="flex items-center gap-4 p-3 bg-gray-50 rounded-xl hover:bg-gray-100 transition-colors border border-gray-100"
|
||||||
>
|
>
|
||||||
<div className="relative w-16 h-16 shrink-0 rounded-md overflow-hidden bg-white border">
|
<div className="relative w-16 h-16 shrink-0 rounded-xl overflow-hidden bg-white border border-gray-200">
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
item.product.images_400x400 || item.product.thumbnail
|
item.product.images_400x400 || item.product.thumbnail
|
||||||
@@ -454,15 +478,15 @@ function CompactOrderCard({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="font-medium text-sm line-clamp-2">
|
<p className="font-semibold text-sm line-clamp-2 text-gray-900">
|
||||||
{item.product.name}
|
{item.product.name}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1 font-medium">
|
||||||
{item.quantity} × {item.unit_price_amount} TMT
|
{item.quantity} × {item.unit_price_amount} TMT
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="font-semibold text-sm">
|
<p className="font-bold text-sm text-gray-900">
|
||||||
{(
|
{(
|
||||||
parseFloat(item.unit_price_amount) * item.quantity
|
parseFloat(item.unit_price_amount) * item.quantity
|
||||||
).toFixed(2)}{" "}
|
).toFixed(2)}{" "}
|
||||||
@@ -475,25 +499,24 @@ function CompactOrderCard({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer with Total and Actions */}
|
{/* Footer with Total and Actions */}
|
||||||
<div className="border-t p-4 bg-gray-50">
|
<div className="border-t border-gray-200 p-4 bg-gray-50">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<span className="text-base font-semibold text-gray-700">
|
<span className="text-base font-bold text-gray-900">
|
||||||
{t("total_price")}:
|
{t("total_price")}:
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xl font-bold text-green-600">
|
<span className="text-xl font-bold text-emerald-600">
|
||||||
{total.toFixed(2)} TMT
|
{total.toFixed(2)} TMT
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showCancelButton && (
|
{showCancelButton && (
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onCancel(order);
|
onCancel(order);
|
||||||
}}
|
}}
|
||||||
disabled={isCancelling}
|
disabled={isCancelling}
|
||||||
className="w-full cursor-pointer"
|
className="w-full cursor-pointer rounded-xl bg-red-600 hover:bg-red-700 font-semibold h-11"
|
||||||
>
|
>
|
||||||
{t("cancel_order")}
|
{t("cancel_order")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export function ProductInfoCard({
|
|||||||
return (
|
return (
|
||||||
<div className="flex-1 space-y-6 bg-transparent">
|
<div className="flex-1 space-y-6 bg-transparent">
|
||||||
{/* Main Info Card */}
|
{/* Main Info Card */}
|
||||||
<Card className="p-6 rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow duration-300 gap-0">
|
<Card className="p-3 md:p-6 rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow duration-300 gap-0">
|
||||||
<div className="">
|
<div className="">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 leading-tight mb-3">
|
<h1 className="text-3xl font-bold text-gray-900 leading-tight mb-3">
|
||||||
{name}
|
{name}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export function ProductPurchaseCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="lg:w-[420px] space-y-4">
|
<div className="lg:w-[420px] space-y-4">
|
||||||
<Card className="p-6 rounded-lg border border-gray-200 shadow-lg hover:shadow-xl transition-shadow duration-300">
|
<Card className="p-3 md:p-6 rounded-lg border border-gray-200 shadow-lg hover:shadow-xl transition-shadow duration-300">
|
||||||
{/* Price Section */}
|
{/* Price Section */}
|
||||||
<div className="flex justify-between items-baseline mb-3 pb-4 border-b border-gray-100 ">
|
<div className="flex justify-between items-baseline mb-3 pb-4 border-b border-gray-100 ">
|
||||||
<span className="text-lg font-medium text-gray-600">
|
<span className="text-lg font-medium text-gray-600">
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function ProductReviewsSection({
|
|||||||
const t= useTranslations();
|
const t= useTranslations();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="p-6 rounded-xl">
|
<Card className="p-3 md:p-6 rounded-xl">
|
||||||
<div className="flex justify-between items-center ">
|
<div className="flex justify-between items-center ">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-2xl font-bold">{t("customer_reviews")}</h3>
|
<h3 className="text-2xl font-bold">{t("customer_reviews")}</h3>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function RelatedProductsSection({
|
|||||||
if (!products || products.length === 0) return null;
|
if (!products || products.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg p-6">
|
<div className="bg-white rounded-lg p-2 md:p-6">
|
||||||
<h2 className="text-2xl font-bold mb-6">{t("related_products")}</h2>
|
<h2 className="text-2xl font-bold mb-6">{t("related_products")}</h2>
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||||
{products.slice(0, 4).map((product) => {
|
{products.slice(0, 4).map((product) => {
|
||||||
|
|||||||
237
features/search/components/SearchPageClient.tsx
Normal file
237
features/search/components/SearchPageClient.tsx
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||||
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import type { Product } from "@/lib/types/api";
|
||||||
|
import { useFilteredSearchProducts } from "@/features/search/hooks/useSearch";
|
||||||
|
import { useCategoryFilters } from "@/features/category/hooks/useCategories";
|
||||||
|
import CategoryFilters from "@/features/category/components/CategoryFilters";
|
||||||
|
import CategoryProductsGrid from "@/features/category/components/CategoryProductsGrid";
|
||||||
|
import CategoryFiltersSheet from "@/features/category/components/CategoryFiltersSheet";
|
||||||
|
import ErrorPage from "@/components/ErrorPage";
|
||||||
|
|
||||||
|
interface SearchPageClientProps {
|
||||||
|
params: { locale: string };
|
||||||
|
searchParams: { q?: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SearchPageClient({
|
||||||
|
params,
|
||||||
|
searchParams,
|
||||||
|
}: SearchPageClientProps) {
|
||||||
|
const q = searchParams.q || "";
|
||||||
|
const t = useTranslations();
|
||||||
|
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
||||||
|
|
||||||
|
// State management
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [allProducts, setAllProducts] = useState<Product[]>([]);
|
||||||
|
const [priceSort, setPriceSort] = useState<
|
||||||
|
"none" | "lowToHigh" | "highToLow"
|
||||||
|
>("none");
|
||||||
|
const [priceRange, setPriceRange] = useState<[number, number]>([0, 100000]);
|
||||||
|
const [selectedBrands, setSelectedBrands] = useState<Set<number>>(new Set());
|
||||||
|
const [selectedFilterCategories, setSelectedFilterCategories] = useState<
|
||||||
|
Set<number>
|
||||||
|
>(new Set());
|
||||||
|
|
||||||
|
// Fetch filters (we use generic filters for search page)
|
||||||
|
const {
|
||||||
|
data: filtersData,
|
||||||
|
isLoading: filtersLoading,
|
||||||
|
isError: filtersError,
|
||||||
|
} = useCategoryFilters(undefined);
|
||||||
|
|
||||||
|
// Build filter params
|
||||||
|
const filterParams = useMemo(() => {
|
||||||
|
const params: any = {
|
||||||
|
page: currentPage,
|
||||||
|
limit: 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (selectedBrands.size > 0) {
|
||||||
|
params.brands = Array.from(selectedBrands);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedFilterCategories.size > 0) {
|
||||||
|
params.categories = Array.from(selectedFilterCategories);
|
||||||
|
}
|
||||||
|
|
||||||
|
params.min_price = priceRange[0];
|
||||||
|
params.max_price = priceRange[1];
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}, [currentPage, selectedBrands, selectedFilterCategories, priceRange]);
|
||||||
|
|
||||||
|
// Fetch filtered search products
|
||||||
|
const {
|
||||||
|
data: productsData,
|
||||||
|
isFetching,
|
||||||
|
isError: productsError,
|
||||||
|
} = useFilteredSearchProducts(q, filterParams);
|
||||||
|
|
||||||
|
// Reset on search term change
|
||||||
|
useEffect(() => {
|
||||||
|
setAllProducts([]);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setSelectedBrands(new Set());
|
||||||
|
setSelectedFilterCategories(new Set());
|
||||||
|
setPriceRange([0, 100000]);
|
||||||
|
setPriceSort("none");
|
||||||
|
}, [q]);
|
||||||
|
|
||||||
|
// Update products list
|
||||||
|
useEffect(() => {
|
||||||
|
if (productsData?.data) {
|
||||||
|
setAllProducts((prev) => {
|
||||||
|
if (currentPage === 1) {
|
||||||
|
return productsData.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingIds = new Set(prev.map((p) => p.id));
|
||||||
|
const newProducts = productsData.data.filter(
|
||||||
|
(p: Product) => !existingIds.has(p.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newProducts.length === 0) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...prev, ...newProducts];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [productsData?.data, currentPage]);
|
||||||
|
|
||||||
|
const hasMore = useMemo(() => {
|
||||||
|
if (!productsData?.pagination) return false;
|
||||||
|
if (productsData.pagination.next_page_url) return true;
|
||||||
|
if (
|
||||||
|
productsData.pagination.current_page &&
|
||||||
|
productsData.pagination.last_page
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
productsData.pagination.current_page < productsData.pagination.last_page
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return productsData.pagination.hasMorePages ?? false;
|
||||||
|
}, [productsData?.pagination]);
|
||||||
|
|
||||||
|
const loadMoreData = useCallback(() => {
|
||||||
|
if (!hasMore || isFetching) return;
|
||||||
|
setCurrentPage((prev) => prev + 1);
|
||||||
|
}, [hasMore, isFetching]);
|
||||||
|
|
||||||
|
const sortedProducts = useMemo(() => {
|
||||||
|
const products = [...allProducts];
|
||||||
|
if (priceSort === "lowToHigh") {
|
||||||
|
return products.sort(
|
||||||
|
(a, b) =>
|
||||||
|
parseFloat(a.price_amount || "0") - parseFloat(b.price_amount || "0"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (priceSort === "highToLow") {
|
||||||
|
return products.sort(
|
||||||
|
(a, b) =>
|
||||||
|
parseFloat(b.price_amount || "0") - parseFloat(a.price_amount || "0"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return products;
|
||||||
|
}, [allProducts, priceSort]);
|
||||||
|
|
||||||
|
// Filter handlers
|
||||||
|
const handleBrandToggle = useCallback((brandId: number) => {
|
||||||
|
setSelectedBrands((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.has(brandId) ? newSet.delete(brandId) : newSet.add(brandId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
setCurrentPage(1);
|
||||||
|
setAllProducts([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCategoryToggle = useCallback((categoryId: number) => {
|
||||||
|
setSelectedFilterCategories((prev) => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.has(categoryId)
|
||||||
|
? newSet.delete(categoryId)
|
||||||
|
: newSet.add(categoryId);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
setCurrentPage(1);
|
||||||
|
setAllProducts([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePriceChange = useCallback((values: number[]) => {
|
||||||
|
setPriceRange([values[0], values[1]]);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setAllProducts([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePriceSortChange = useCallback(
|
||||||
|
(sortType: "none" | "lowToHigh" | "highToLow") => {
|
||||||
|
setPriceSort(sortType);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetFilters = useCallback(() => {
|
||||||
|
setSelectedBrands(new Set());
|
||||||
|
setSelectedFilterCategories(new Set());
|
||||||
|
setPriceRange([0, 100000]);
|
||||||
|
setPriceSort("none");
|
||||||
|
setCurrentPage(1);
|
||||||
|
setAllProducts([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filterTranslations = useMemo(
|
||||||
|
() => ({
|
||||||
|
category: t("category"),
|
||||||
|
brands: t("brands"),
|
||||||
|
sort: t("sort"),
|
||||||
|
default: t("default"),
|
||||||
|
price_low_to_high: t("price_low_to_high"),
|
||||||
|
price_high_to_low: t("price_high_to_low"),
|
||||||
|
price: t("price"),
|
||||||
|
price_from: t("price_from"),
|
||||||
|
price_to: t("price_to"),
|
||||||
|
reset: t("reset"),
|
||||||
|
}),
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (productsError) {
|
||||||
|
return <ErrorPage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col mx-auto max-w-[1504px] px-2 md:px-4 lg:px-6 pb-12">
|
||||||
|
<div className="bg-white p-4 rounded-t-lg mb-0">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold">
|
||||||
|
{t("search_results")}: <span className="text-gray-500">"{q}"</span>
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-500 mt-1">
|
||||||
|
{productsData?.pagination?.total || allProducts.length}{" "}
|
||||||
|
{t("products_found")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex p-2 md:p-4 gap-4 bg-white rounded-b-lg">
|
||||||
|
{/* Products Grid */}
|
||||||
|
<div className="flex-1 bg-white rounded-lg mb-6">
|
||||||
|
<CategoryProductsGrid
|
||||||
|
products={sortedProducts}
|
||||||
|
hasMore={hasMore}
|
||||||
|
onLoadMore={loadMoreData}
|
||||||
|
isFetching={isFetching}
|
||||||
|
translations={{
|
||||||
|
loading: t("common.loading"),
|
||||||
|
no_results: t("no_results"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { apiClient } from "@/lib/api";
|
import { apiClient } from "@/lib/api";
|
||||||
import type { SearchResponse, SearchParams } from "../types";
|
import type { SearchResponse, SearchParams } from "../types";
|
||||||
|
import type {
|
||||||
|
Product,
|
||||||
|
PaginatedResponse,
|
||||||
|
ProductFilters,
|
||||||
|
} from "@/lib/types/api";
|
||||||
|
|
||||||
export function useSearchProducts(params: SearchParams) {
|
export function useSearchProducts(params: SearchParams) {
|
||||||
const { q, barcode } = params;
|
const { q, barcode } = params;
|
||||||
@@ -10,14 +15,14 @@ export function useSearchProducts(params: SearchParams) {
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (barcode) {
|
if (barcode) {
|
||||||
const response = await apiClient.get<SearchResponse>(
|
const response = await apiClient.get<SearchResponse>(
|
||||||
`/search-product-barcode?barcode=${barcode}`
|
`/search-product-barcode?barcode=${barcode}`,
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
const response = await apiClient.get<SearchResponse>(
|
const response = await apiClient.get<SearchResponse>(
|
||||||
`/search-product?q=${encodeURIComponent(q)}`
|
`/search-product?q=${encodeURIComponent(q)}`,
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
@@ -27,4 +32,48 @@ export function useSearchProducts(params: SearchParams) {
|
|||||||
enabled: !!(q && q.length > 0) || !!barcode,
|
enabled: !!(q && q.length > 0) || !!barcode,
|
||||||
staleTime: 1000 * 60 * 5,
|
staleTime: 1000 * 60 * 5,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useFilteredSearchProducts(
|
||||||
|
q: string,
|
||||||
|
filters: ProductFilters,
|
||||||
|
options?: { enabled?: boolean },
|
||||||
|
) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["search-filtered", q, filters],
|
||||||
|
queryFn: async () => {
|
||||||
|
const params: Record<string, any> = {
|
||||||
|
q,
|
||||||
|
page: filters.page || 1,
|
||||||
|
per_page: filters.limit || 12,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filters.brands && filters.brands.length > 0) {
|
||||||
|
params.brands = filters.brands.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.categories && filters.categories.length > 0) {
|
||||||
|
params.categories = filters.categories.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.min_price !== undefined) {
|
||||||
|
params.min_price = filters.min_price;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.max_price !== undefined) {
|
||||||
|
params.max_price = filters.max_price;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await apiClient.get<PaginatedResponse<Product>>(
|
||||||
|
"/search-product",
|
||||||
|
{ params },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: response.data.data || [],
|
||||||
|
pagination: response.data.pagination || {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
enabled: options?.enabled !== false && !!q,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,7 +17,13 @@
|
|||||||
"enterPhone": "Введите свой номер телефона",
|
"enterPhone": "Введите свой номер телефона",
|
||||||
"weWillSendCode": "Мы вышлем вам код",
|
"weWillSendCode": "Мы вышлем вам код",
|
||||||
"loading": "Загрузка...",
|
"loading": "Загрузка...",
|
||||||
"all_collections_loaded": "Все коллекции загружены"
|
"all_collections_loaded": "Все коллекции загружены",
|
||||||
|
"info": "Инфо",
|
||||||
|
"instagram": "Instagram",
|
||||||
|
"email": "Email",
|
||||||
|
"imo": "IMO",
|
||||||
|
"contact_us": "Свяжитесь с нами",
|
||||||
|
"about_us": "О нас"
|
||||||
},
|
},
|
||||||
"category": "Категория",
|
"category": "Категория",
|
||||||
"checkout": "Оформить заказ",
|
"checkout": "Оформить заказ",
|
||||||
@@ -189,8 +195,8 @@
|
|||||||
"enter_email": "Введите email",
|
"enter_email": "Введите email",
|
||||||
"uploadPatent": "Загрузить патент",
|
"uploadPatent": "Загрузить патент",
|
||||||
"outOfStock": "Нет в наличии",
|
"outOfStock": "Нет в наличии",
|
||||||
"requiredField": "Обязательное поле",
|
"requiredField": "Обязательное поле",
|
||||||
"fileRequired": "Файл загрузить"
|
"fileRequired": "Файл загрузить",
|
||||||
|
"search_results": "Результаты поиска",
|
||||||
|
"products_found": "товаров найдено"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,13 @@
|
|||||||
"enterPhone": "Telefon belgisini giriziň",
|
"enterPhone": "Telefon belgisini giriziň",
|
||||||
"weWillSendCode": "Biz size kod ugradarys",
|
"weWillSendCode": "Biz size kod ugradarys",
|
||||||
"loading": "Ýüklenýär...",
|
"loading": "Ýüklenýär...",
|
||||||
"all_collections_loaded": "Bütüň koleksiyonlar ýüklendi"
|
"all_collections_loaded": "Bütüň koleksiyonlar ýüklendi",
|
||||||
|
"info": "Maglumat",
|
||||||
|
"instagram": "Instagram",
|
||||||
|
"email": "Email",
|
||||||
|
"imo": "IMO",
|
||||||
|
"contact_us": "Biziň bilen habarlaşyň",
|
||||||
|
"about_us": "Biz barada"
|
||||||
},
|
},
|
||||||
"category": "Bölümler",
|
"category": "Bölümler",
|
||||||
"checkout": "Sargyt et",
|
"checkout": "Sargyt et",
|
||||||
@@ -76,7 +82,6 @@
|
|||||||
"yes": "Hawa",
|
"yes": "Hawa",
|
||||||
"cart_empty": "Siziň söwda sebediňiz boş",
|
"cart_empty": "Siziň söwda sebediňiz boş",
|
||||||
"add_to_cart": "Sebede goş",
|
"add_to_cart": "Sebede goş",
|
||||||
|
|
||||||
"go_to_cart": "Sebede geçmek",
|
"go_to_cart": "Sebede geçmek",
|
||||||
"products": "Azyk harytlary",
|
"products": "Azyk harytlary",
|
||||||
"become_seller": "Satyjy bolmak",
|
"become_seller": "Satyjy bolmak",
|
||||||
@@ -174,7 +179,7 @@
|
|||||||
"submitting": "Ugradylýar...",
|
"submitting": "Ugradylýar...",
|
||||||
"submit_review": "Teswiri ugrat",
|
"submit_review": "Teswiri ugrat",
|
||||||
"characters": "simbol",
|
"characters": "simbol",
|
||||||
"related_products": "Meňzeş harytlar",
|
"related_products": "Meňzeş harytlara",
|
||||||
"cart_empty_message": "Entek sebediňize haryt goşmadyňyz. Söwda etmäge başlaň!!!",
|
"cart_empty_message": "Entek sebediňize haryt goşmadyňyz. Söwda etmäge başlaň!!!",
|
||||||
"start_shopping": "Söwda etmäge başla!",
|
"start_shopping": "Söwda etmäge başla!",
|
||||||
"favorites_empty": "Siziň saýlanan harytlaryňyz ýok",
|
"favorites_empty": "Siziň saýlanan harytlaryňyz ýok",
|
||||||
@@ -191,5 +196,7 @@
|
|||||||
"uploadPatent": "Patent goş",
|
"uploadPatent": "Patent goş",
|
||||||
"outOfStock": "Ammarda ýok",
|
"outOfStock": "Ammarda ýok",
|
||||||
"requiredField": "Zerur maglumat",
|
"requiredField": "Zerur maglumat",
|
||||||
"fileRequired": "Fayl goş"
|
"fileRequired": "Fayl goş",
|
||||||
|
"search_results": "Gözleg netijeleri",
|
||||||
|
"products_found": "haryt tapyldy"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ export interface ProductMedia {
|
|||||||
images_720x720: string;
|
images_720x720: string;
|
||||||
images_800x800: string;
|
images_800x800: string;
|
||||||
images_1200x1200: string;
|
images_1200x1200: string;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Carousel {
|
export interface Carousel {
|
||||||
title: string
|
title: string;
|
||||||
image: string
|
image: string;
|
||||||
url?: string | null
|
url?: string | null;
|
||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
link: string;
|
link: string;
|
||||||
}
|
}
|
||||||
@@ -27,7 +26,6 @@ export interface Review {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type DeliveryType = "SELECTED_DELIVERY" | "PICK_UP";
|
export type DeliveryType = "SELECTED_DELIVERY" | "PICK_UP";
|
||||||
|
|
||||||
export interface PaymentType {
|
export interface PaymentType {
|
||||||
@@ -105,8 +103,7 @@ export interface Category {
|
|||||||
image: string;
|
image: string;
|
||||||
parent_id?: number | null;
|
parent_id?: number | null;
|
||||||
children?: Category[];
|
children?: Category[];
|
||||||
media:ProductMedia[];
|
media: ProductMedia[];
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collection Types
|
// Collection Types
|
||||||
@@ -132,6 +129,7 @@ export interface CartProduct {
|
|||||||
stock: number;
|
stock: number;
|
||||||
image?: string;
|
image?: string;
|
||||||
images?: string[];
|
images?: string[];
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CartItem {
|
export interface CartItem {
|
||||||
@@ -150,6 +148,7 @@ export interface CartItem {
|
|||||||
sub_total_formatted: string;
|
sub_total_formatted: string;
|
||||||
total_formatted: string;
|
total_formatted: string;
|
||||||
discount_formatted: string;
|
discount_formatted: string;
|
||||||
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CartResponse {
|
export interface CartResponse {
|
||||||
@@ -512,7 +511,6 @@ export interface FiltersResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface ProductFilters {
|
export interface ProductFilters {
|
||||||
brands?: number[];
|
brands?: number[];
|
||||||
categories?: number[];
|
categories?: number[];
|
||||||
@@ -520,5 +518,5 @@ export interface ProductFilters {
|
|||||||
max_price?: number;
|
max_price?: number;
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
collection_id?: number;
|
collection_id?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
public/logo.jpeg
Normal file
BIN
public/logo.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
Reference in New Issue
Block a user