changed some color and fix some styles

This commit is contained in:
@jcarymuhammedow
2026-02-07 16:06:33 +05:00
parent 022c7290b4
commit b27b8436d1
34 changed files with 999 additions and 368 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 178 KiB

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,9 +70,7 @@ 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>
); );

View File

@@ -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,9 +70,7 @@ 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>
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -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;
} }
@@ -28,3 +33,47 @@ export function useSearchProducts(params: SearchParams) {
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,
});
}

View File

@@ -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": "товаров найдено"
} }

View File

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

View File

@@ -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[];

BIN
public/logo.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB