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

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

View File

@@ -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">
@@ -150,7 +150,7 @@ export default function OrderSummary({
onChange={(e) => onNameChange(e.target.value)}
placeholder={t("name")}
className={`rounded-[10px] h-12 border-2 transition-colors ${
showValidation && name.trim() === ""
showValidation && name.trim() === ""
? "border-red-500"
: "border-gray-200 focus:border-gray-900"
}`}

View File

@@ -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,10 +70,8 @@ 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>
);
}
}

View File

@@ -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,18 +25,41 @@ 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>
<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"
>
{filterLabel}
<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,10 +70,8 @@ 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>
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>
{isExpanded ? (
<ChevronUp className="h-5 w-5 text-gray-400" />
) : (
<ChevronDown className="h-5 w-5 text-gray-400" />
)}
<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-600" />
) : (
<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>

View File

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

View File

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

View File

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

View File

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

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 { 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;
}
@@ -27,4 +32,48 @@ export function useSearchProducts(params: SearchParams) {
enabled: !!(q && q.length > 0) || !!barcode,
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,
});
}