changed some color and fix some styles
This commit is contained in:
@@ -202,6 +202,7 @@ export default function CartPage() {
|
||||
...item,
|
||||
quantity: quantity,
|
||||
price: price,
|
||||
description: item.product.description,
|
||||
total: total,
|
||||
seller: seller,
|
||||
price_formatted: `${item.product.price_amount} TMT`,
|
||||
|
||||
@@ -8,12 +8,12 @@ export const revalidate = 600; // ISR: Revalidate every 10 minutes
|
||||
|
||||
const CATEGORY_META = {
|
||||
tm: {
|
||||
suffix: " | Post shop",
|
||||
suffix: " | SmartElectronics",
|
||||
description: "Kategoriýa boýunça harytlary gözläň",
|
||||
ogLocale: "tk_TM",
|
||||
},
|
||||
ru: {
|
||||
suffix: " | Post shop",
|
||||
suffix: " | SmartElectronics",
|
||||
description: "Просмотр товаров в данной категории",
|
||||
ogLocale: "ru_RU",
|
||||
},
|
||||
|
||||
@@ -8,12 +8,12 @@ export const revalidate = 600; // ISR: 10 minutes
|
||||
|
||||
const META = {
|
||||
tm: {
|
||||
titleSuffix: " | Post shop",
|
||||
titleSuffix: " | SmartElectronics",
|
||||
description: (name: string) => `${name} kolleksiýasyndaky harytlary gözläň`,
|
||||
ogLocale: "tk_TM",
|
||||
},
|
||||
ru: {
|
||||
titleSuffix: " | Post shop",
|
||||
titleSuffix: " | SmartElectronics",
|
||||
description: (name: string) => `Просмотр товаров из коллекции «${name}»`,
|
||||
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 { Metadata } from "next"
|
||||
import { Geist, Geist_Mono } from "next/font/google"
|
||||
import { notFound } from "next/navigation"
|
||||
import { NextIntlClientProvider } from "next-intl"
|
||||
import "./globals.css"
|
||||
import Header from "@/components/layout/Header"
|
||||
import MobileBottomNav from "@/components/layout/MobileBar"
|
||||
import { Toaster } from "@/components/ui/sonner"
|
||||
import { Providers } from "@/context/Provider"
|
||||
import AuthWrapper from "@/context/AuthWrapper"
|
||||
import type React from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { notFound } from "next/navigation";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import "./globals.css";
|
||||
import Header from "@/components/layout/Header";
|
||||
import MobileBottomNav from "@/components/layout/MobileBar";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { Providers } from "@/context/Provider";
|
||||
import AuthWrapper from "@/context/AuthWrapper";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
})
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
})
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Postshop",
|
||||
title: "SmartElectronics",
|
||||
description: "E-commerce platform",
|
||||
}
|
||||
};
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode
|
||||
params: Promise<{ locale: string }>
|
||||
}
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ locale: string }>;
|
||||
};
|
||||
|
||||
const locales = ["ru", "tm"]
|
||||
const locales = ["ru", "tm"];
|
||||
|
||||
export function generateStaticParams() {
|
||||
return locales.map((locale) => ({ locale }))
|
||||
return locales.map((locale) => ({ locale }));
|
||||
}
|
||||
|
||||
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 {
|
||||
messages = (await import(`../../i18n/messages/${locale}.json`)).default
|
||||
messages = (await import(`../../i18n/messages/${locale}.json`)).default;
|
||||
} catch {
|
||||
messages = {}
|
||||
messages = {};
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<Providers>
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<AuthWrapper locale={locale}>
|
||||
<Header locale={locale} />
|
||||
{children}
|
||||
<Footer />
|
||||
<MobileBottomNav locale={locale} />
|
||||
<Toaster />
|
||||
</AuthWrapper>
|
||||
@@ -63,5 +67,5 @@ export default async function RootLayout({ children, params }: Props) {
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -3,11 +3,11 @@ import OrdersPageClient from "../../../features/orders/components/OrderPage";
|
||||
|
||||
const metadataContent = {
|
||||
tm: {
|
||||
title: "Meniň Sargytlarym | Post shop",
|
||||
title: "Meniň Sargytlarym | SmartElectronics",
|
||||
description: "Sargytlaryňyzy görüň",
|
||||
},
|
||||
ru: {
|
||||
title: "Мои Заказы | Пост-магазин",
|
||||
title: "Мои Заказы | SmartElectronics",
|
||||
description: "Просмотр истории заказов",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -7,7 +7,7 @@ const META = {
|
||||
description: "Качественные товары с быстрой доставкой по всей стране",
|
||||
},
|
||||
tm: {
|
||||
title: "Post shop - Iň gowy harytlar, amatly bahada",
|
||||
title: "SmartElectronics - Iň gowy harytlar, amatly bahada",
|
||||
description:
|
||||
"Ý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 { X, Search } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Logo from "@/public/logo.webp";
|
||||
import Logo from "@/public/logo.png";
|
||||
import CategoryMenu from "./ui/CategoryMenu";
|
||||
import SearchBar from "./ui/SearchBar";
|
||||
import AuthDialog from "./ui/AuthDialog";
|
||||
@@ -62,7 +62,7 @@ export default function Header({ locale = "ru" }: HeaderProps) {
|
||||
href="/"
|
||||
className="shrink-0 transition-opacity hover:opacity-80"
|
||||
>
|
||||
<div className="relative h-8 w-[180px]">
|
||||
<div className="relative h-32 w-[188px]">
|
||||
<Image
|
||||
src={Logo}
|
||||
alt="Logo"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
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 { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
@@ -15,9 +15,7 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useCategories, useFavorites, useOrders } from "@/lib/hooks";
|
||||
import { useCartCount } from "@/features/cart/hooks/useCart";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuthStatus } from "@/lib/hooks/useAuth";
|
||||
import { useTranslations } from "next-intl";
|
||||
import AuthDialog from "./ui/AuthDialog";
|
||||
|
||||
interface MobileBottomNavProps {
|
||||
locale?: string;
|
||||
@@ -39,10 +37,39 @@ export default function MobileBottomNav({
|
||||
}: MobileBottomNavProps) {
|
||||
const [isClient, setIsClient] = 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 { 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();
|
||||
|
||||
@@ -56,25 +83,6 @@ export default function MobileBottomNav({
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
router.push(path);
|
||||
@@ -85,40 +93,39 @@ export default function MobileBottomNav({
|
||||
return (
|
||||
<>
|
||||
{/* 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">
|
||||
{/* Catalog Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
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={() => {
|
||||
setIsCategoryOpen(true);
|
||||
}}
|
||||
>
|
||||
<Menu className="h-5 w-5 text-gray-600" />
|
||||
<span className="text-xs text-gray-700">{t("common.catalog")}</span>
|
||||
<Menu className="h-5 w-5 text-gray-700" />
|
||||
<span className="text-xs text-gray-700 font-medium">
|
||||
{t("common.catalog")}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
{/* Favorites Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
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")}
|
||||
>
|
||||
<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 && (
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="absolute -right-2 -top-2 h-4 w-4 flex items-center justify-center p-0 text-[10px]"
|
||||
>
|
||||
<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">
|
||||
{favoritesData?.length}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-gray-700">
|
||||
<span className="text-xs text-gray-700 font-medium">
|
||||
{t("common.favorites")}
|
||||
</span>
|
||||
</Button>
|
||||
@@ -127,59 +134,52 @@ export default function MobileBottomNav({
|
||||
<Button
|
||||
variant="ghost"
|
||||
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")}
|
||||
>
|
||||
<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 && (
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="absolute -right-2 -top-2 h-4 w-4 flex items-center justify-center p-0 text-[10px]"
|
||||
>
|
||||
<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">
|
||||
{ordersData?.length}
|
||||
</Badge>
|
||||
)}
|
||||
</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>
|
||||
|
||||
{/* Cart Button - OPTIMIZED */}
|
||||
{/* Cart Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
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")}
|
||||
>
|
||||
<div className="relative">
|
||||
<ShoppingCart className="h-5 w-5 text-gray-600" />
|
||||
<ShoppingCart className="h-5 w-5 text-gray-700" />
|
||||
{cartCount > 0 && (
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="absolute -right-2 -top-2 h-4 w-4 flex items-center justify-center p-0 text-[10px]"
|
||||
>
|
||||
<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">
|
||||
{cartCount}
|
||||
</Badge>
|
||||
)}
|
||||
</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>
|
||||
|
||||
{/* Profile/Login Button */}
|
||||
{/* Info Button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="flex-col gap-0.5 h-auto px-2 py-2"
|
||||
onClick={handleProfileClick}
|
||||
disabled={authLoading}
|
||||
className="flex-col gap-0.5 h-auto px-2 py-2 hover:bg-gray-100 rounded-xl transition-colors"
|
||||
onClick={() => router.push(`/${locale}/info`)}
|
||||
>
|
||||
<User className="h-5 w-5 text-gray-600" />
|
||||
<span className="text-xs text-gray-700">
|
||||
{authLoading
|
||||
? "..."
|
||||
: isAuthenticated
|
||||
? t("common.profile")
|
||||
: t("common.login")}
|
||||
<Info className="h-5 w-5 text-gray-700" />
|
||||
<span className="text-xs text-gray-700 font-medium">
|
||||
{t("common.info")}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -187,31 +187,68 @@ export default function MobileBottomNav({
|
||||
|
||||
{/* Category Sheet/Drawer */}
|
||||
<Sheet open={isCategoryOpen} onOpenChange={setIsCategoryOpen}>
|
||||
<SheetContent side="left" className="w-[300px] p-0">
|
||||
<SheetHeader className="p-4 border-b">
|
||||
<SheetTitle>{t("common.catalog")}</SheetTitle>
|
||||
<SheetContent
|
||||
side="left"
|
||||
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>
|
||||
<ScrollArea className="h-[calc(100vh-80px)]">
|
||||
<ScrollArea className="h-[calc(100vh-88px)]">
|
||||
<div className="p-4">
|
||||
{categories.map((category) => (
|
||||
<div key={category.id} className="mb-4">
|
||||
<div key={category.id} className="mb-1">
|
||||
<div className="flex items-center">
|
||||
<Link
|
||||
href={`/category/${category.slug}?category_id=${category.id}`}
|
||||
onClick={() => setIsCategoryOpen(false)}
|
||||
className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors font-semibold"
|
||||
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>
|
||||
|
||||
{/* Subcategories */}
|
||||
{/* Toggle button if has children */}
|
||||
{category.children && category.children.length > 0 && (
|
||||
<div className="ml-8 mt-2 space-y-1">
|
||||
<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 */}
|
||||
{category.children &&
|
||||
category.children.length > 0 &&
|
||||
expandedCategories.has(category.id) && (
|
||||
<div className="ml-6 mt-1 space-y-0.5">
|
||||
{category.children.map((child: any) => (
|
||||
<Link
|
||||
key={child.id}
|
||||
href={`/category/${child.slug}?category_id=${child.id}`}
|
||||
onClick={() => setIsCategoryOpen(false)}
|
||||
className="block px-3 py-2 text-sm text-gray-600 hover:text-primary hover:bg-gray-50 rounded-lg transition-colors"
|
||||
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"
|
||||
>
|
||||
{child.name}
|
||||
</Link>
|
||||
@@ -224,9 +261,6 @@ export default function MobileBottomNav({
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
{/* Local Auth Dialog */}
|
||||
<AuthDialog isOpen={isLoginOpen} onClose={() => setIsLoginOpen(false)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ function ActionButton({
|
||||
<Button
|
||||
variant="ghost"
|
||||
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}
|
||||
>
|
||||
<div className="relative">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Search, X, Loader2 } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { Search } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
@@ -11,8 +11,6 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSearchProducts } from "@/features/search/hooks/useSearch";
|
||||
import Image from "next/image";
|
||||
import { SearchIcon } from "@/components/icons";
|
||||
|
||||
interface SearchBarProps {
|
||||
@@ -34,95 +32,26 @@ export default function SearchBar({
|
||||
}: SearchBarProps) {
|
||||
const router = useRouter();
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [debouncedSearch, setDebouncedSearch] = useState("");
|
||||
const [showResults, setShowResults] = useState(false);
|
||||
const searchRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { data, isLoading } = useSearchProducts({ q: debouncedSearch });
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setDebouncedSearch(searchValue);
|
||||
}, 300);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchValue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedSearch && data?.data && data.data.length > 0) {
|
||||
setShowResults(true);
|
||||
} else {
|
||||
setShowResults(false);
|
||||
}
|
||||
}, [debouncedSearch, data]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (searchRef.current && !searchRef.current.contains(e.target as Node)) {
|
||||
setShowResults(false);
|
||||
const performSearch = () => {
|
||||
if (searchValue.trim()) {
|
||||
router.push(
|
||||
`/${locale}/search?q=${encodeURIComponent(searchValue.trim())}`,
|
||||
);
|
||||
if (onClose) onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
performSearch();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (value: string) => {
|
||||
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) {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
@@ -132,22 +61,22 @@ export default function SearchBar({
|
||||
{searchPlaceholder}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="relative" ref={searchRef}>
|
||||
<div className="relative">
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchValue}
|
||||
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"
|
||||
autoFocus
|
||||
/>
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
|
||||
{isLoading && (
|
||||
<Loader2 className="absolute right-4 top-1/2 -translate-y-1/2 h-5 w-5 animate-spin text-gray-400" />
|
||||
)}
|
||||
<Search
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 cursor-pointer"
|
||||
onClick={performSearch}
|
||||
/>
|
||||
</div>
|
||||
<SearchResults />
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -157,7 +86,6 @@ export default function SearchBar({
|
||||
return (
|
||||
<div
|
||||
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="relative">
|
||||
@@ -166,21 +94,19 @@ export default function SearchBar({
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchValue}
|
||||
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"
|
||||
/>
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400 pointer-events-none" />
|
||||
</div>
|
||||
{isLoading && (
|
||||
<Loader2 className="absolute right-4 top-1/2 -translate-y-1/2 h-5 w-5 animate-spin text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
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"
|
||||
>
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
<SearchResults />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -277,11 +277,11 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="p-6 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">
|
||||
<Card className="p-3 shadow-sm border border-gray-200 rounded-lg hover:shadow-md transition-shadow duration-200">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{/* Product Image & Info */}
|
||||
<div className="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="md:flex gap-4 flex-1">
|
||||
<div className="relative w-full h-full min-h-[200px] rounded-lg border border-gray-200 overflow-hidden shrink-0 bg-gray-50">
|
||||
<Image
|
||||
src={getImageSrc()}
|
||||
alt={item.product.name}
|
||||
@@ -290,13 +290,27 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
||||
/>
|
||||
</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">
|
||||
{item.product.name}
|
||||
</h3>
|
||||
{/* <p className="text-sm text-gray-500 font-medium">
|
||||
{item.seller?.name || "Store"}
|
||||
</p> */}
|
||||
{/* <div
|
||||
className="text-gray-700 leading-relaxed prose prose-sm max-w-none
|
||||
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 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -312,15 +326,14 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
||||
size="sm"
|
||||
onClick={handleDelete}
|
||||
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" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<p className="text-sm font-medium text-gray-600">
|
||||
{t("unit_price")}{" "}
|
||||
|
||||
@@ -133,7 +133,7 @@ export default function OrderSummary({
|
||||
};
|
||||
|
||||
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 */}
|
||||
<div className="mb-8">
|
||||
<h3 className="text-xl font-bold mb-5 text-gray-900">
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useState } from "react";
|
||||
|
||||
interface CategoryFiltersSheetProps {
|
||||
isOpen: boolean;
|
||||
@@ -24,6 +25,24 @@ export default function CategoryFiltersSheet({
|
||||
closeLabel,
|
||||
children,
|
||||
}: 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 (
|
||||
<Sheet open={isOpen} onOpenChange={onOpenChange}>
|
||||
<SheetTrigger asChild>
|
||||
@@ -35,7 +54,12 @@ export default function CategoryFiltersSheet({
|
||||
<SlidersHorizontal className="h-5 w-5" />
|
||||
</Button>
|
||||
</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">
|
||||
<SheetTitle className="text-gray-900">{filterLabel}</SheetTitle>
|
||||
<button
|
||||
@@ -46,9 +70,7 @@ export default function CategoryFiltersSheet({
|
||||
<span className="sr-only">{closeLabel}</span>
|
||||
</button>
|
||||
</SheetHeader>
|
||||
<ScrollArea className="h-[calc(100vh-80px)] p-4">
|
||||
{children}
|
||||
</ScrollArea>
|
||||
<ScrollArea className="h-[calc(100vh-80px)] p-4">{children}</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { useState } from "react";
|
||||
|
||||
interface CollectionFiltersSheetProps {
|
||||
isOpen: boolean;
|
||||
@@ -24,6 +25,24 @@ export default function CollectionFiltersSheet({
|
||||
closeLabel,
|
||||
children,
|
||||
}: 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 (
|
||||
<Sheet open={isOpen} onOpenChange={onOpenChange}>
|
||||
<SheetTrigger asChild>
|
||||
@@ -35,7 +54,12 @@ export default function CollectionFiltersSheet({
|
||||
<SlidersHorizontal className="h-5 w-5" />
|
||||
</Button>
|
||||
</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">
|
||||
<SheetTitle className="text-gray-900">{filterLabel}</SheetTitle>
|
||||
<button
|
||||
@@ -46,9 +70,7 @@ export default function CollectionFiltersSheet({
|
||||
<span className="sr-only">{closeLabel}</span>
|
||||
</button>
|
||||
</SheetHeader>
|
||||
<ScrollArea className="h-[calc(100vh-80px)] p-4">
|
||||
{children}
|
||||
</ScrollArea>
|
||||
<ScrollArea className="h-[calc(100vh-80px)] p-4">{children}</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
|
||||
@@ -8,21 +8,21 @@ export default function EmptyFavorites() {
|
||||
const router=useRouter();
|
||||
return (
|
||||
<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">
|
||||
<Heart className="h-10 w-10 text-blue-600" />
|
||||
<Heart className="h-12 w-12 text-gray-700" />
|
||||
</div>
|
||||
|
||||
<h2 className="mb-2 text-2xl font-semibold text-gray-900">
|
||||
{t("favorites_empty")}
|
||||
</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")}
|
||||
</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">
|
||||
{t("start_shopping")}
|
||||
<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")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,10 @@ export default function HeroCarousel({ items }: { items: CarouselItem[] }) {
|
||||
className="
|
||||
[&_.swiper-button-next]: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-active]:bg-white!
|
||||
"
|
||||
|
||||
@@ -294,12 +294,12 @@ export default function ProductCard({
|
||||
<>
|
||||
<CarouselPrevious
|
||||
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())}
|
||||
/>
|
||||
<CarouselNext
|
||||
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())}
|
||||
/>
|
||||
</>
|
||||
@@ -382,7 +382,7 @@ export default function ProductCard({
|
||||
<Button
|
||||
onClick={handleFavorite}
|
||||
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 ? (
|
||||
<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
|
||||
onClick={handleAddToCart}
|
||||
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 ? (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -426,12 +426,12 @@ export default function ProductCard({
|
||||
size="icon"
|
||||
onClick={(e) => handleQuantityChange(e, -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" />
|
||||
</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 && (
|
||||
<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" />
|
||||
@@ -447,7 +447,7 @@ export default function ProductCard({
|
||||
size="icon"
|
||||
onClick={(e) => handleQuantityChange(e, 1)}
|
||||
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" />
|
||||
</Button>
|
||||
|
||||
@@ -8,21 +8,21 @@ export default function EmptyOrders() {
|
||||
const router=useRouter();
|
||||
return (
|
||||
<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">
|
||||
<ShoppingCart className="h-10 w-10 text-blue-600" />
|
||||
<ShoppingCart className="h-12 w-12 text-gray-700" />
|
||||
</div>
|
||||
|
||||
<h2 className="mb-2 text-2xl font-semibold text-gray-900">
|
||||
{t("orders_empty")}
|
||||
</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")}
|
||||
</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">
|
||||
{t("start_shopping")}
|
||||
<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")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
return (
|
||||
<Badge
|
||||
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}
|
||||
</Badge>
|
||||
@@ -100,7 +100,10 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
lowerStatus.includes("işlenýär")
|
||||
) {
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-blue-50 text-blue-700">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-gray-100 text-gray-900 font-semibold"
|
||||
>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
@@ -110,24 +113,36 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
lowerStatus.includes("shipped") ||
|
||||
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 (
|
||||
lowerStatus.includes("доставлен") ||
|
||||
lowerStatus.includes("delivered") ||
|
||||
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 (
|
||||
lowerStatus.includes("отменен") ||
|
||||
lowerStatus.includes("cancelled") ||
|
||||
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) => {
|
||||
@@ -147,11 +162,11 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
|
||||
const activeOrders = useMemo(
|
||||
() => orders?.filter((o) => isActiveOrder(o.status)) || [],
|
||||
[orders, isActiveOrder]
|
||||
[orders, isActiveOrder],
|
||||
);
|
||||
const completedOrders = useMemo(
|
||||
() => orders?.filter((o) => !isActiveOrder(o.status)) || [],
|
||||
[orders, isActiveOrder]
|
||||
[orders, isActiveOrder],
|
||||
);
|
||||
|
||||
const calculateTotal = useCallback((order: Order) => {
|
||||
@@ -163,38 +178,39 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<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")}
|
||||
</h1>
|
||||
|
||||
{/* Tabs Skeleton */}
|
||||
<div className="mb-4 md:mb-6">
|
||||
<div className="flex gap-2 mb-4">
|
||||
<Skeleton className="h-10 w-32 rounded-md" />
|
||||
<Skeleton className="h-10 w-32 rounded-md" />
|
||||
<Skeleton className="h-10 w-32 rounded-xl" />
|
||||
<Skeleton className="h-10 w-32 rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Order Cards Skeleton */}
|
||||
<div className="space-y-4">
|
||||
{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="flex items-center justify-between">
|
||||
{/* Left side - Order info */}
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<Skeleton className="h-5 w-5 rounded" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-5 w-32" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-5 w-32 rounded-lg" />
|
||||
<Skeleton className="h-4 w-24 rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - Status and price */}
|
||||
<div className="flex items-center gap-4">
|
||||
<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-24" />
|
||||
<Skeleton className="h-6 w-24 rounded-lg" />
|
||||
</div>
|
||||
<Skeleton className="h-5 w-5 rounded" />
|
||||
</div>
|
||||
@@ -215,17 +231,23 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<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">
|
||||
<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 text-gray-900">
|
||||
{t("my_orders")}
|
||||
</h1>
|
||||
|
||||
<Tabs defaultValue="active" className="w-full">
|
||||
<TabsList className="mb-4 md:mb-6 w-full md:w-fit gap-2 p-0">
|
||||
<TabsTrigger value="active">
|
||||
<TabsList className="mb-4 md:mb-6 w-full md:w-fit gap-2 p-1 bg-gray-100 rounded-xl">
|
||||
<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})
|
||||
</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})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -233,7 +255,9 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
<TabsContent value="active">
|
||||
{activeOrders.length === 0 ? (
|
||||
<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 className="space-y-4">
|
||||
@@ -258,7 +282,7 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
<TabsContent value="completed">
|
||||
{completedOrders.length === 0 ? (
|
||||
<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")}
|
||||
</p>
|
||||
</div>
|
||||
@@ -284,27 +308,28 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
</Tabs>
|
||||
|
||||
<Dialog open={isCancelDialogOpen} onOpenChange={setIsCancelDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogContent className="rounded-3xl border border-gray-200">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<DialogTitle className="text-xl font-bold text-gray-900">
|
||||
{t("cancel_order")} #{orderToCancel?.id}
|
||||
</DialogTitle>
|
||||
<DialogDescription>{t("cancel_confirmation")}</DialogDescription>
|
||||
<DialogDescription className="text-gray-600">
|
||||
{t("cancel_confirmation")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogFooter className="gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsCancelDialogOpen(false)}
|
||||
disabled={isCancellingOrder}
|
||||
className="cursor-pointer"
|
||||
className="cursor-pointer rounded-xl border-2 border-gray-200 hover:border-gray-900 font-semibold"
|
||||
>
|
||||
{t("keep_order")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={confirmCancelOrder}
|
||||
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")}
|
||||
</Button>
|
||||
@@ -342,21 +367,23 @@ function CompactOrderCard({
|
||||
const itemCount = order.orderItems.length;
|
||||
|
||||
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 */}
|
||||
<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}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<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>
|
||||
<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}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
<p className="text-sm text-gray-500 font-medium">
|
||||
{itemCount} {itemCount === 1 ? t("product") : t("products")}
|
||||
</p>
|
||||
</div>
|
||||
@@ -367,83 +394,80 @@ function CompactOrderCard({
|
||||
<div className="flex flex-col md:flex-row gap-2 items-end">
|
||||
{getStatusBadge(order.status)}
|
||||
<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
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-8 w-8 rounded-lg bg-gray-100 flex items-center justify-center">
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-5 w-5 text-gray-400" />
|
||||
<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>
|
||||
|
||||
{/* Expandable Details */}
|
||||
{isExpanded && (
|
||||
<div className="border-t bg-white">
|
||||
<div className="border-t border-gray-200 bg-white">
|
||||
{/* Order Info Grid */}
|
||||
<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">
|
||||
<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>
|
||||
<p className="text-sm font-medium text-gray-700">
|
||||
<p className="text-sm font-bold text-gray-900">
|
||||
{t("address")}
|
||||
</p>
|
||||
<p className="text-sm text-gray-900">
|
||||
<p className="text-sm text-gray-600 mt-0.5">
|
||||
{order.customer_address}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<p className="text-sm font-medium text-gray-700">
|
||||
<p className="text-sm font-bold text-gray-900">
|
||||
{t("payment_method")}
|
||||
</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 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>
|
||||
<p className="text-sm font-medium text-gray-700">
|
||||
<p className="text-sm font-bold text-gray-900">
|
||||
{t("shipping_method")}
|
||||
</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>
|
||||
|
||||
{/* Products List */}
|
||||
<div className="p-4">
|
||||
<h4 className="font-semibold mb-3 text-gray-700">
|
||||
{t("products")}:
|
||||
</h4>
|
||||
<h4 className="font-bold mb-3 text-gray-900">{t("products")}:</h4>
|
||||
<div className="space-y-3 max-h-96 overflow-y-auto">
|
||||
{order.orderItems.map((item, index) => (
|
||||
<div
|
||||
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
|
||||
src={
|
||||
item.product.images_400x400 || item.product.thumbnail
|
||||
@@ -454,15 +478,15 @@ function CompactOrderCard({
|
||||
/>
|
||||
</div>
|
||||
<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}
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
<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
|
||||
).toFixed(2)}{" "}
|
||||
@@ -475,25 +499,24 @@ function CompactOrderCard({
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<span className="text-base font-semibold text-gray-700">
|
||||
<span className="text-base font-bold text-gray-900">
|
||||
{t("total_price")}:
|
||||
</span>
|
||||
<span className="text-xl font-bold text-green-600">
|
||||
<span className="text-xl font-bold text-emerald-600">
|
||||
{total.toFixed(2)} TMT
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{showCancelButton && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCancel(order);
|
||||
}}
|
||||
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")}
|
||||
</Button>
|
||||
|
||||
@@ -35,7 +35,7 @@ export function ProductInfoCard({
|
||||
return (
|
||||
<div className="flex-1 space-y-6 bg-transparent">
|
||||
{/* 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="">
|
||||
<h1 className="text-3xl font-bold text-gray-900 leading-tight mb-3">
|
||||
{name}
|
||||
|
||||
@@ -42,7 +42,7 @@ export function ProductPurchaseCard({
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<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">
|
||||
|
||||
@@ -45,7 +45,7 @@ export function ProductReviewsSection({
|
||||
const t= useTranslations();
|
||||
|
||||
return (
|
||||
<Card className="p-6 rounded-xl">
|
||||
<Card className="p-3 md:p-6 rounded-xl">
|
||||
<div className="flex justify-between items-center ">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold">{t("customer_reviews")}</h3>
|
||||
|
||||
@@ -34,7 +34,7 @@ export function RelatedProductsSection({
|
||||
if (!products || products.length === 0) return null;
|
||||
|
||||
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>
|
||||
<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) => {
|
||||
|
||||
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 { apiClient } from "@/lib/api";
|
||||
import type { SearchResponse, SearchParams } from "../types";
|
||||
import type {
|
||||
Product,
|
||||
PaginatedResponse,
|
||||
ProductFilters,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
export function useSearchProducts(params: SearchParams) {
|
||||
const { q, barcode } = params;
|
||||
@@ -10,14 +15,14 @@ export function useSearchProducts(params: SearchParams) {
|
||||
queryFn: async () => {
|
||||
if (barcode) {
|
||||
const response = await apiClient.get<SearchResponse>(
|
||||
`/search-product-barcode?barcode=${barcode}`
|
||||
`/search-product-barcode?barcode=${barcode}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
if (q) {
|
||||
const response = await apiClient.get<SearchResponse>(
|
||||
`/search-product?q=${encodeURIComponent(q)}`
|
||||
`/search-product?q=${encodeURIComponent(q)}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -28,3 +33,47 @@ export function useSearchProducts(params: SearchParams) {
|
||||
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": "Введите свой номер телефона",
|
||||
"weWillSendCode": "Мы вышлем вам код",
|
||||
"loading": "Загрузка...",
|
||||
"all_collections_loaded": "Все коллекции загружены"
|
||||
"all_collections_loaded": "Все коллекции загружены",
|
||||
"info": "Инфо",
|
||||
"instagram": "Instagram",
|
||||
"email": "Email",
|
||||
"imo": "IMO",
|
||||
"contact_us": "Свяжитесь с нами",
|
||||
"about_us": "О нас"
|
||||
},
|
||||
"category": "Категория",
|
||||
"checkout": "Оформить заказ",
|
||||
@@ -190,7 +196,7 @@
|
||||
"uploadPatent": "Загрузить патент",
|
||||
"outOfStock": "Нет в наличии",
|
||||
"requiredField": "Обязательное поле",
|
||||
"fileRequired": "Файл загрузить"
|
||||
"fileRequired": "Файл загрузить",
|
||||
"search_results": "Результаты поиска",
|
||||
"products_found": "товаров найдено"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,13 @@
|
||||
"enterPhone": "Telefon belgisini giriziň",
|
||||
"weWillSendCode": "Biz size kod ugradarys",
|
||||
"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",
|
||||
"checkout": "Sargyt et",
|
||||
@@ -76,7 +82,6 @@
|
||||
"yes": "Hawa",
|
||||
"cart_empty": "Siziň söwda sebediňiz boş",
|
||||
"add_to_cart": "Sebede goş",
|
||||
|
||||
"go_to_cart": "Sebede geçmek",
|
||||
"products": "Azyk harytlary",
|
||||
"become_seller": "Satyjy bolmak",
|
||||
@@ -174,7 +179,7 @@
|
||||
"submitting": "Ugradylýar...",
|
||||
"submit_review": "Teswiri ugrat",
|
||||
"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ň!!!",
|
||||
"start_shopping": "Söwda etmäge başla!",
|
||||
"favorites_empty": "Siziň saýlanan harytlaryňyz ýok",
|
||||
@@ -191,5 +196,7 @@
|
||||
"uploadPatent": "Patent goş",
|
||||
"outOfStock": "Ammarda ýok",
|
||||
"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_800x800: string;
|
||||
images_1200x1200: string;
|
||||
|
||||
}
|
||||
|
||||
export interface Carousel {
|
||||
title: string
|
||||
image: string
|
||||
url?: string | null
|
||||
title: string;
|
||||
image: string;
|
||||
url?: string | null;
|
||||
thumbnail: string;
|
||||
link: string;
|
||||
}
|
||||
@@ -27,7 +26,6 @@ export interface Review {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
|
||||
export type DeliveryType = "SELECTED_DELIVERY" | "PICK_UP";
|
||||
|
||||
export interface PaymentType {
|
||||
@@ -105,8 +103,7 @@ export interface Category {
|
||||
image: string;
|
||||
parent_id?: number | null;
|
||||
children?: Category[];
|
||||
media:ProductMedia[];
|
||||
|
||||
media: ProductMedia[];
|
||||
}
|
||||
|
||||
// Collection Types
|
||||
@@ -132,6 +129,7 @@ export interface CartProduct {
|
||||
stock: number;
|
||||
image?: string;
|
||||
images?: string[];
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface CartItem {
|
||||
@@ -150,6 +148,7 @@ export interface CartItem {
|
||||
sub_total_formatted: string;
|
||||
total_formatted: string;
|
||||
discount_formatted: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface CartResponse {
|
||||
@@ -512,7 +511,6 @@ export interface FiltersResponse {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export interface ProductFilters {
|
||||
brands?: number[];
|
||||
categories?: 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