changed some color and fix some styles
This commit is contained in:
@@ -277,11 +277,11 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="p-6 shadow-sm border border-gray-200 rounded-lg hover:shadow-md transition-shadow duration-200">
|
||||
<div className="flex flex-col sm:flex-row gap-6">
|
||||
<Card className="p-3 shadow-sm border border-gray-200 rounded-lg hover:shadow-md transition-shadow duration-200">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{/* Product Image & Info */}
|
||||
<div className="flex gap-4 flex-1">
|
||||
<div className="relative w-[200px] h-[200px] rounded-lg border border-gray-200 overflow-hidden shrink-0 bg-gray-50">
|
||||
<div className="md:flex gap-4 flex-1">
|
||||
<div className="relative w-full h-full min-h-[200px] rounded-lg border border-gray-200 overflow-hidden shrink-0 bg-gray-50">
|
||||
<Image
|
||||
src={getImageSrc()}
|
||||
alt={item.product.name}
|
||||
@@ -290,13 +290,27 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
</div>
|
||||
<div className="flex items-start gap-2 pt-2">
|
||||
<h3 className="font-bold text-base text-gray-900 line-clamp-2">
|
||||
{item.product.name}
|
||||
</h3>
|
||||
{/* <p className="text-sm text-gray-500 font-medium">
|
||||
{item.seller?.name || "Store"}
|
||||
</p> */}
|
||||
{/* <div
|
||||
className="text-gray-700 leading-relaxed prose prose-sm max-w-none
|
||||
prose-headings:text-gray-900 prose-headings:font-bold
|
||||
prose-p:text-gray-700 prose-p:leading-relaxed
|
||||
prose-ul:text-gray-700 prose-ol:text-gray-700
|
||||
prose-li:text-gray-700 prose-li:leading-relaxed
|
||||
prose-strong:text-gray-900 prose-strong:font-semibold
|
||||
prose-a:text-gray-900 prose-a:font-medium hover:prose-a:text-gray-700"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
item.product.description &&
|
||||
item.product.description.length > 175
|
||||
? item.product.description.substring(0, 175) + "..."
|
||||
: item.product.description || "",
|
||||
}}
|
||||
/> */}
|
||||
|
||||
{/* {availableStock <= 5 && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -312,15 +326,14 @@ export default function CartItemCard({ item, onUpdate }: CartItemCardProps) {
|
||||
size="sm"
|
||||
onClick={handleDelete}
|
||||
disabled={isRemoving}
|
||||
className="w-fit cursor-pointer p-0 h-auto hover:bg-transparent text-gray-600 hover:text-red-500 transition-colors group"
|
||||
className="w-fit cursor-pointer pt-1 h-auto hover:bg-transparent text-gray-600 hover:text-red-500 transition-colors group"
|
||||
>
|
||||
<Trash2 className="h-5 w-5 group-hover:scale-110 transition-transform" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Price & Quantity */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-6 justify-between">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-gray-600">
|
||||
{t("unit_price")}{" "}
|
||||
|
||||
@@ -133,7 +133,7 @@ export default function OrderSummary({
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full md:w-[420px] p-8 rounded-lg border border-gray-200 shadow-lg h-fit sticky top-20">
|
||||
<Card className="w-full lg:w-[340px] md:w-[300px] p-6 rounded-lg border border-gray-200 shadow-lg h-fit sticky top-20">
|
||||
{/* Customer Information */}
|
||||
<div className="mb-8">
|
||||
<h3 className="text-xl font-bold mb-5 text-gray-900">
|
||||
@@ -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"
|
||||
}`}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,21 +8,21 @@ export default function EmptyFavorites() {
|
||||
const router=useRouter();
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center px-4">
|
||||
<div className="w-full max-w-md rounded-2xl bg-linear-to-br from-blue-50 to-white p-8 text-center shadow-lg">
|
||||
<div className="w-full max-w-md rounded-3xl bg-gradient-to-br from-gray-50 to-white p-12 text-center shadow-xl border border-gray-100">
|
||||
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-blue-100">
|
||||
<Heart className="h-10 w-10 text-blue-600" />
|
||||
<Heart className="h-12 w-12 text-gray-700" />
|
||||
</div>
|
||||
|
||||
<h2 className="mb-2 text-2xl font-semibold text-gray-900">
|
||||
{t("favorites_empty")}
|
||||
</h2>
|
||||
|
||||
<p className="mb-6 text-sm text-gray-500">
|
||||
<p className="mb-8 text-base text-gray-600 leading-relaxed">
|
||||
{t("favorites_empty_message")}
|
||||
</p>
|
||||
|
||||
<Button onClick={()=>router.push("/")} className="w-full rounded-lg cursor-pointer bg-blue-600 px-6 py-3 text-sm font-medium text-white transition hover:bg-blue-700 active:scale-95">
|
||||
{t("start_shopping")}
|
||||
<Button onClick={()=>router.push("/")} className="w-full cursor-pointer rounded-2xl bg-gradient-to-r from-gray-900 to-gray-800 hover:from-gray-800 hover:to-gray-700 px-8 py-6 text-base font-bold text-white shadow-lg hover:shadow-xl transition-all duration-300 active:scale-95"
|
||||
> {t("start_shopping")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,6 +27,10 @@ export default function HeroCarousel({ items }: { items: CarouselItem[] }) {
|
||||
className="
|
||||
[&_.swiper-button-next]:text-white!
|
||||
[&_.swiper-button-prev]:text-white!
|
||||
[&_.swiper-button-next]:hidden!
|
||||
[&_.swiper-button-prev]:hidden!
|
||||
md:[&_.swiper-button-next]:flex!
|
||||
md:[&_.swiper-button-prev]:flex!
|
||||
[&_.swiper-pagination-bullet]:bg-white!
|
||||
[&_.swiper-pagination-bullet-active]:bg-white!
|
||||
"
|
||||
|
||||
@@ -294,12 +294,12 @@ export default function ProductCard({
|
||||
<>
|
||||
<CarouselPrevious
|
||||
data-carousel-control="true"
|
||||
className="absolute left-3 opacity-0 group-hover:opacity-100 transition-all duration-300 z-20 h-9 w-9 bg-white/95 hover:bg-white border-0 shadow-lg"
|
||||
className="absolute cursor-pointer left-3 opacity-0 group-hover:opacity-100 transition-all duration-300 z-20 h-9 w-9 bg-white/95 hover:bg-white border-0 shadow-lg"
|
||||
onClick={(e) => handleNavClick(e, () => api?.scrollPrev())}
|
||||
/>
|
||||
<CarouselNext
|
||||
data-carousel-control="true"
|
||||
className="absolute right-3 opacity-0 group-hover:opacity-100 transition-all duration-300 z-20 h-9 w-9 bg-white/95 hover:bg-white border-0 shadow-lg"
|
||||
className="absolute cursor-pointer right-3 opacity-0 group-hover:opacity-100 transition-all duration-300 z-20 h-9 w-9 bg-white/95 hover:bg-white border-0 shadow-lg"
|
||||
onClick={(e) => handleNavClick(e, () => api?.scrollNext())}
|
||||
/>
|
||||
</>
|
||||
@@ -382,7 +382,7 @@ export default function ProductCard({
|
||||
<Button
|
||||
onClick={handleFavorite}
|
||||
disabled={isFavoriteToggling || isFavoriteLoading}
|
||||
className=" w-9 h-9 rounded-[10px] bg-white/95 backdrop-blur-sm hover:bg-white hover:scale-110 transition-all duration-200 shadow-md disabled:opacity-50"
|
||||
className=" w-7 h-7 md:w-9 cursor-pointer md:h-9 rounded-[10px] bg-white/95 backdrop-blur-sm hover:bg-white hover:scale-110 transition-all duration-200 shadow-md disabled:opacity-50"
|
||||
>
|
||||
{isFavoriteLoading ? (
|
||||
<div className="w-5 h-5 border-2 border-gray-200 border-t-gray-700 rounded-full animate-spin" />
|
||||
@@ -403,7 +403,7 @@ export default function ProductCard({
|
||||
<Button
|
||||
onClick={handleAddToCart}
|
||||
disabled={isSyncing}
|
||||
className="w-full h-9 rounded-[10px] bg-gradient-to-r from-gray-900 to-gray-800 hover:from-gray-800 hover:to-gray-700 text-white font-semibold shadow-md hover:shadow-lg transition-all duration-300 gap-2"
|
||||
className="w-full h-7 md:h-9 cursor-pointer rounded-[10px] bg-gradient-to-r from-gray-900 to-gray-800 hover:from-gray-800 hover:to-gray-700 text-white font-semibold shadow-md hover:shadow-lg transition-all duration-300 gap-2"
|
||||
>
|
||||
{isSyncing ? (
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -426,12 +426,12 @@ export default function ProductCard({
|
||||
size="icon"
|
||||
onClick={(e) => handleQuantityChange(e, -1)}
|
||||
disabled={isSyncing || localQuantity <= 1}
|
||||
className="rounded-[10px] h-9 w-9 border-2 border-gray-200 hover:border-gray-900 hover:bg-gray-50 transition-all duration-200 disabled:opacity-30"
|
||||
className="rounded-[10px] cursor-pointer h-7 md:h-9 w-7 md:w-9 border-2 border-gray-200 hover:border-gray-900 hover:bg-gray-50 transition-all duration-200 disabled:opacity-30"
|
||||
>
|
||||
<Minus className="h-5 w-5 text-gray-700" />
|
||||
</Button>
|
||||
|
||||
<div className="flex-1 text-center font-bold text-lg border-2 border-gray-200 rounded-[10px] h-9 flex items-center justify-center bg-white relative">
|
||||
<div className="flex-1 text-center font-bold text-sm md:text-lg border-2 border-gray-200 rounded-[10px] h-7 md:h-9 flex items-center justify-center bg-white relative">
|
||||
{isSyncing && (
|
||||
<div className="absolute inset-0 bg-white/80 rounded-xl flex items-center justify-center">
|
||||
<div className="w-4 h-4 border-2 border-gray-300 border-t-gray-700 rounded-full animate-spin" />
|
||||
@@ -447,7 +447,7 @@ export default function ProductCard({
|
||||
size="icon"
|
||||
onClick={(e) => handleQuantityChange(e, 1)}
|
||||
disabled={isSyncing}
|
||||
className="rounded-[10px] h-9 w-9 border-2 border-gray-900 bg-gray-900 hover:bg-gray-800 transition-all duration-200 disabled:opacity-30"
|
||||
className="rounded-[10px] cursor-pointer h-7 md:h-9 w-7 md:w-9 border-2 border-gray-900 bg-gray-900 hover:bg-gray-800 transition-all duration-200 disabled:opacity-30"
|
||||
>
|
||||
<Plus className="h-5 w-5 text-white" />
|
||||
</Button>
|
||||
|
||||
@@ -8,21 +8,21 @@ export default function EmptyOrders() {
|
||||
const router=useRouter();
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center px-4">
|
||||
<div className="w-full max-w-md rounded-2xl bg-linear-to-br from-blue-50 to-white p-8 text-center shadow-lg">
|
||||
<div className="w-full max-w-md rounded-3xl bg-gradient-to-br from-gray-50 to-white p-12 text-center shadow-xl border border-gray-100">
|
||||
<div className="mx-auto mb-6 flex h-20 w-20 items-center justify-center rounded-full bg-blue-100">
|
||||
<ShoppingCart className="h-10 w-10 text-blue-600" />
|
||||
<ShoppingCart className="h-12 w-12 text-gray-700" />
|
||||
</div>
|
||||
|
||||
<h2 className="mb-2 text-2xl font-semibold text-gray-900">
|
||||
{t("orders_empty")}
|
||||
</h2>
|
||||
|
||||
<p className="mb-6 text-sm text-gray-500">
|
||||
<p className="mb-8 text-base text-gray-600 leading-relaxed">
|
||||
{t("orders_empty_message")}
|
||||
</p>
|
||||
|
||||
<Button onClick={()=>router.push("/")} className="w-full rounded-lg cursor-pointer bg-blue-600 px-6 py-3 text-sm font-medium text-white transition hover:bg-blue-700 active:scale-95">
|
||||
{t("start_shopping")}
|
||||
<Button onClick={()=>router.push("/")} className="w-full cursor-pointer rounded-2xl bg-gradient-to-r from-gray-900 to-gray-800 hover:from-gray-800 hover:to-gray-700 px-8 py-6 text-base font-bold text-white shadow-lg hover:shadow-xl transition-all duration-300 active:scale-95"
|
||||
> {t("start_shopping")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
return (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="bg-yellow-50 text-yellow-700 border-yellow-300"
|
||||
className="bg-amber-50 text-amber-700 border-amber-300 font-semibold"
|
||||
>
|
||||
{status}
|
||||
</Badge>
|
||||
@@ -100,7 +100,10 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
lowerStatus.includes("işlenýär")
|
||||
) {
|
||||
return (
|
||||
<Badge variant="secondary" className="bg-blue-50 text-blue-700">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="bg-gray-100 text-gray-900 font-semibold"
|
||||
>
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
@@ -110,24 +113,36 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
lowerStatus.includes("shipped") ||
|
||||
lowerStatus.includes("iberildi")
|
||||
) {
|
||||
return <Badge className="bg-purple-500">{status}</Badge>;
|
||||
return (
|
||||
<Badge className="bg-indigo-500 hover:bg-indigo-500 font-semibold">
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (
|
||||
lowerStatus.includes("доставлен") ||
|
||||
lowerStatus.includes("delivered") ||
|
||||
lowerStatus.includes("eltildi")
|
||||
) {
|
||||
return <Badge className="bg-green-600">{status}</Badge>;
|
||||
return (
|
||||
<Badge className="bg-emerald-600 hover:bg-emerald-600 font-semibold">
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
if (
|
||||
lowerStatus.includes("отменен") ||
|
||||
lowerStatus.includes("cancelled") ||
|
||||
lowerStatus.includes("ýatyryldy")
|
||||
) {
|
||||
return <Badge variant="destructive">{status}</Badge>;
|
||||
return (
|
||||
<Badge className="bg-red-600 hover:bg-red-600 font-semibold">
|
||||
{status}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
return <Badge>{status}</Badge>;
|
||||
return <Badge className="font-semibold">{status}</Badge>;
|
||||
}, []);
|
||||
|
||||
const isActiveOrder = useCallback((status: string) => {
|
||||
@@ -147,11 +162,11 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
|
||||
const activeOrders = useMemo(
|
||||
() => orders?.filter((o) => isActiveOrder(o.status)) || [],
|
||||
[orders, isActiveOrder]
|
||||
[orders, isActiveOrder],
|
||||
);
|
||||
const completedOrders = useMemo(
|
||||
() => orders?.filter((o) => !isActiveOrder(o.status)) || [],
|
||||
[orders, isActiveOrder]
|
||||
[orders, isActiveOrder],
|
||||
);
|
||||
|
||||
const calculateTotal = useCallback((order: Order) => {
|
||||
@@ -163,38 +178,39 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="mx-auto p-2 lg:p-6 md:p-4 mb-16 min-h-screen">
|
||||
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold mb-6">
|
||||
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold mb-6 text-gray-900">
|
||||
{t("my_orders")}
|
||||
</h1>
|
||||
|
||||
{/* Tabs Skeleton */}
|
||||
<div className="mb-4 md:mb-6">
|
||||
<div className="flex gap-2 mb-4">
|
||||
<Skeleton className="h-10 w-32 rounded-md" />
|
||||
<Skeleton className="h-10 w-32 rounded-md" />
|
||||
<Skeleton className="h-10 w-32 rounded-xl" />
|
||||
<Skeleton className="h-10 w-32 rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Order Cards Skeleton */}
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Card key={i} className="overflow-hidden py-2 md:py-4 lg:py-6">
|
||||
<Card
|
||||
key={i}
|
||||
className="overflow-hidden py-2 md:py-4 lg:py-6 rounded-2xl border border-gray-200"
|
||||
>
|
||||
<div className="p-2 md:p-4 mx-2 md:mx-4 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
{/* Left side - Order info */}
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<Skeleton className="h-5 w-5 rounded" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-5 w-32" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-5 w-32 rounded-lg" />
|
||||
<Skeleton className="h-4 w-24 rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side - Status and price */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-col md:flex-row gap-2 items-end">
|
||||
<Skeleton className="h-6 w-20 rounded-full" />
|
||||
<Skeleton className="h-6 w-24" />
|
||||
<Skeleton className="h-6 w-24 rounded-lg" />
|
||||
</div>
|
||||
<Skeleton className="h-5 w-5 rounded" />
|
||||
</div>
|
||||
@@ -215,17 +231,23 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className=" mx-auto p-2 lg:p-6 md:p-4 mb-16 min-h-screen">
|
||||
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold mb-6">
|
||||
<div className="mx-auto p-2 lg:p-6 md:p-4 mb-16 min-h-screen">
|
||||
<h1 className="text-xl md:text-2xl lg:text-3xl font-bold mb-6 text-gray-900">
|
||||
{t("my_orders")}
|
||||
</h1>
|
||||
|
||||
<Tabs defaultValue="active" className="w-full">
|
||||
<TabsList className="mb-4 md:mb-6 w-full md:w-fit gap-2 p-0">
|
||||
<TabsTrigger value="active">
|
||||
<TabsList className="mb-4 md:mb-6 w-full md:w-fit gap-2 p-1 bg-gray-100 rounded-xl">
|
||||
<TabsTrigger
|
||||
value="active"
|
||||
className="rounded-lg font-semibold data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm"
|
||||
>
|
||||
{t("active_orders")} ({activeOrders.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="completed">
|
||||
<TabsTrigger
|
||||
value="completed"
|
||||
className="rounded-lg font-semibold data-[state=active]:bg-white data-[state=active]:text-gray-900 data-[state=active]:shadow-sm"
|
||||
>
|
||||
{t("completed_orders")} ({completedOrders.length})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -233,7 +255,9 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
<TabsContent value="active">
|
||||
{activeOrders.length === 0 ? (
|
||||
<div className="flex items-center justify-center min-h-[40vh]">
|
||||
<p className="text-xl text-gray-400">{t("no_active_orders")}</p>
|
||||
<p className="text-xl text-gray-400 font-medium">
|
||||
{t("no_active_orders")}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
@@ -258,7 +282,7 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
<TabsContent value="completed">
|
||||
{completedOrders.length === 0 ? (
|
||||
<div className="flex items-center justify-center min-h-[40vh]">
|
||||
<p className="text-xl text-gray-400">
|
||||
<p className="text-xl text-gray-400 font-medium">
|
||||
{t("no_completed_orders")}
|
||||
</p>
|
||||
</div>
|
||||
@@ -284,27 +308,28 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) {
|
||||
</Tabs>
|
||||
|
||||
<Dialog open={isCancelDialogOpen} onOpenChange={setIsCancelDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogContent className="rounded-3xl border border-gray-200">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<DialogTitle className="text-xl font-bold text-gray-900">
|
||||
{t("cancel_order")} #{orderToCancel?.id}
|
||||
</DialogTitle>
|
||||
<DialogDescription>{t("cancel_confirmation")}</DialogDescription>
|
||||
<DialogDescription className="text-gray-600">
|
||||
{t("cancel_confirmation")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogFooter className="gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsCancelDialogOpen(false)}
|
||||
disabled={isCancellingOrder}
|
||||
className="cursor-pointer"
|
||||
className="cursor-pointer rounded-xl border-2 border-gray-200 hover:border-gray-900 font-semibold"
|
||||
>
|
||||
{t("keep_order")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={confirmCancelOrder}
|
||||
disabled={isCancellingOrder}
|
||||
className="cursor-pointer"
|
||||
className="cursor-pointer rounded-xl bg-red-600 hover:bg-red-700 font-semibold"
|
||||
>
|
||||
{isCancellingOrder ? t("cancelling") : t("cancel_order")}
|
||||
</Button>
|
||||
@@ -342,21 +367,23 @@ function CompactOrderCard({
|
||||
const itemCount = order.orderItems.length;
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden transition-all py-2 md:py-4 lg:py-6 hover:shadow-md">
|
||||
<Card className="overflow-hidden transition-all py-2 md:py-4 lg:py-6 hover:shadow-lg rounded-2xl border border-gray-200">
|
||||
{/* Compact Header - Always Visible */}
|
||||
<div
|
||||
className="p-2 md:p-4 mx-2 md:mx-4 rounded-lg cursor-pointer bg-linear-to-r from-white to-gray-50 hover:from-gray-50 hover:to-gray-100 transition-colors"
|
||||
className="p-2 md:p-4 mx-2 md:mx-4 rounded-xl cursor-pointer bg-gradient-to-r from-white to-gray-50 hover:from-gray-50 hover:to-gray-100 transition-all duration-200"
|
||||
onClick={onToggle}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="h-5 w-5 text-gray-500" />
|
||||
<div className="h-10 w-10 rounded-xl bg-gray-100 flex items-center justify-center">
|
||||
<Package className="h-5 w-5 text-gray-700" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-base lg:text-lg">
|
||||
<h3 className="font-bold text-base lg:text-lg text-gray-900">
|
||||
{t("order_number")} {order.id}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
<p className="text-sm text-gray-500 font-medium">
|
||||
{itemCount} {itemCount === 1 ? t("product") : t("products")}
|
||||
</p>
|
||||
</div>
|
||||
@@ -367,83 +394,80 @@ function CompactOrderCard({
|
||||
<div className="flex flex-col md:flex-row gap-2 items-end">
|
||||
{getStatusBadge(order.status)}
|
||||
<div className="text-right">
|
||||
<p className="font-bold text-lg text-green-600">
|
||||
<p className="font-bold text-lg text-emerald-600">
|
||||
{total.toFixed(2)} TMT
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{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>
|
||||
|
||||
@@ -35,7 +35,7 @@ export function ProductInfoCard({
|
||||
return (
|
||||
<div className="flex-1 space-y-6 bg-transparent">
|
||||
{/* Main Info Card */}
|
||||
<Card className="p-6 rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow duration-300 gap-0">
|
||||
<Card className="p-3 md:p-6 rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition-shadow duration-300 gap-0">
|
||||
<div className="">
|
||||
<h1 className="text-3xl font-bold text-gray-900 leading-tight mb-3">
|
||||
{name}
|
||||
|
||||
@@ -42,7 +42,7 @@ export function ProductPurchaseCard({
|
||||
|
||||
return (
|
||||
<div className="lg:w-[420px] space-y-4">
|
||||
<Card className="p-6 rounded-lg border border-gray-200 shadow-lg hover:shadow-xl transition-shadow duration-300">
|
||||
<Card className="p-3 md:p-6 rounded-lg border border-gray-200 shadow-lg hover:shadow-xl transition-shadow duration-300">
|
||||
{/* Price Section */}
|
||||
<div className="flex justify-between items-baseline mb-3 pb-4 border-b border-gray-100 ">
|
||||
<span className="text-lg font-medium text-gray-600">
|
||||
|
||||
@@ -45,7 +45,7 @@ export function ProductReviewsSection({
|
||||
const t= useTranslations();
|
||||
|
||||
return (
|
||||
<Card className="p-6 rounded-xl">
|
||||
<Card className="p-3 md:p-6 rounded-xl">
|
||||
<div className="flex justify-between items-center ">
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold">{t("customer_reviews")}</h3>
|
||||
|
||||
@@ -34,7 +34,7 @@ export function RelatedProductsSection({
|
||||
if (!products || products.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg p-6">
|
||||
<div className="bg-white rounded-lg p-2 md:p-6">
|
||||
<h2 className="text-2xl font-bold mb-6">{t("related_products")}</h2>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{products.slice(0, 4).map((product) => {
|
||||
|
||||
237
features/search/components/SearchPageClient.tsx
Normal file
237
features/search/components/SearchPageClient.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useTranslations } from "next-intl";
|
||||
import type { Product } from "@/lib/types/api";
|
||||
import { useFilteredSearchProducts } from "@/features/search/hooks/useSearch";
|
||||
import { useCategoryFilters } from "@/features/category/hooks/useCategories";
|
||||
import CategoryFilters from "@/features/category/components/CategoryFilters";
|
||||
import CategoryProductsGrid from "@/features/category/components/CategoryProductsGrid";
|
||||
import CategoryFiltersSheet from "@/features/category/components/CategoryFiltersSheet";
|
||||
import ErrorPage from "@/components/ErrorPage";
|
||||
|
||||
interface SearchPageClientProps {
|
||||
params: { locale: string };
|
||||
searchParams: { q?: string };
|
||||
}
|
||||
|
||||
export default function SearchPageClient({
|
||||
params,
|
||||
searchParams,
|
||||
}: SearchPageClientProps) {
|
||||
const q = searchParams.q || "";
|
||||
const t = useTranslations();
|
||||
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
||||
|
||||
// State management
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [allProducts, setAllProducts] = useState<Product[]>([]);
|
||||
const [priceSort, setPriceSort] = useState<
|
||||
"none" | "lowToHigh" | "highToLow"
|
||||
>("none");
|
||||
const [priceRange, setPriceRange] = useState<[number, number]>([0, 100000]);
|
||||
const [selectedBrands, setSelectedBrands] = useState<Set<number>>(new Set());
|
||||
const [selectedFilterCategories, setSelectedFilterCategories] = useState<
|
||||
Set<number>
|
||||
>(new Set());
|
||||
|
||||
// Fetch filters (we use generic filters for search page)
|
||||
const {
|
||||
data: filtersData,
|
||||
isLoading: filtersLoading,
|
||||
isError: filtersError,
|
||||
} = useCategoryFilters(undefined);
|
||||
|
||||
// Build filter params
|
||||
const filterParams = useMemo(() => {
|
||||
const params: any = {
|
||||
page: currentPage,
|
||||
limit: 12,
|
||||
};
|
||||
|
||||
if (selectedBrands.size > 0) {
|
||||
params.brands = Array.from(selectedBrands);
|
||||
}
|
||||
|
||||
if (selectedFilterCategories.size > 0) {
|
||||
params.categories = Array.from(selectedFilterCategories);
|
||||
}
|
||||
|
||||
params.min_price = priceRange[0];
|
||||
params.max_price = priceRange[1];
|
||||
|
||||
return params;
|
||||
}, [currentPage, selectedBrands, selectedFilterCategories, priceRange]);
|
||||
|
||||
// Fetch filtered search products
|
||||
const {
|
||||
data: productsData,
|
||||
isFetching,
|
||||
isError: productsError,
|
||||
} = useFilteredSearchProducts(q, filterParams);
|
||||
|
||||
// Reset on search term change
|
||||
useEffect(() => {
|
||||
setAllProducts([]);
|
||||
setCurrentPage(1);
|
||||
setSelectedBrands(new Set());
|
||||
setSelectedFilterCategories(new Set());
|
||||
setPriceRange([0, 100000]);
|
||||
setPriceSort("none");
|
||||
}, [q]);
|
||||
|
||||
// Update products list
|
||||
useEffect(() => {
|
||||
if (productsData?.data) {
|
||||
setAllProducts((prev) => {
|
||||
if (currentPage === 1) {
|
||||
return productsData.data;
|
||||
}
|
||||
|
||||
const existingIds = new Set(prev.map((p) => p.id));
|
||||
const newProducts = productsData.data.filter(
|
||||
(p: Product) => !existingIds.has(p.id),
|
||||
);
|
||||
|
||||
if (newProducts.length === 0) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return [...prev, ...newProducts];
|
||||
});
|
||||
}
|
||||
}, [productsData?.data, currentPage]);
|
||||
|
||||
const hasMore = useMemo(() => {
|
||||
if (!productsData?.pagination) return false;
|
||||
if (productsData.pagination.next_page_url) return true;
|
||||
if (
|
||||
productsData.pagination.current_page &&
|
||||
productsData.pagination.last_page
|
||||
) {
|
||||
return (
|
||||
productsData.pagination.current_page < productsData.pagination.last_page
|
||||
);
|
||||
}
|
||||
return productsData.pagination.hasMorePages ?? false;
|
||||
}, [productsData?.pagination]);
|
||||
|
||||
const loadMoreData = useCallback(() => {
|
||||
if (!hasMore || isFetching) return;
|
||||
setCurrentPage((prev) => prev + 1);
|
||||
}, [hasMore, isFetching]);
|
||||
|
||||
const sortedProducts = useMemo(() => {
|
||||
const products = [...allProducts];
|
||||
if (priceSort === "lowToHigh") {
|
||||
return products.sort(
|
||||
(a, b) =>
|
||||
parseFloat(a.price_amount || "0") - parseFloat(b.price_amount || "0"),
|
||||
);
|
||||
}
|
||||
if (priceSort === "highToLow") {
|
||||
return products.sort(
|
||||
(a, b) =>
|
||||
parseFloat(b.price_amount || "0") - parseFloat(a.price_amount || "0"),
|
||||
);
|
||||
}
|
||||
return products;
|
||||
}, [allProducts, priceSort]);
|
||||
|
||||
// Filter handlers
|
||||
const handleBrandToggle = useCallback((brandId: number) => {
|
||||
setSelectedBrands((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.has(brandId) ? newSet.delete(brandId) : newSet.add(brandId);
|
||||
return newSet;
|
||||
});
|
||||
setCurrentPage(1);
|
||||
setAllProducts([]);
|
||||
}, []);
|
||||
|
||||
const handleCategoryToggle = useCallback((categoryId: number) => {
|
||||
setSelectedFilterCategories((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.has(categoryId)
|
||||
? newSet.delete(categoryId)
|
||||
: newSet.add(categoryId);
|
||||
return newSet;
|
||||
});
|
||||
setCurrentPage(1);
|
||||
setAllProducts([]);
|
||||
}, []);
|
||||
|
||||
const handlePriceChange = useCallback((values: number[]) => {
|
||||
setPriceRange([values[0], values[1]]);
|
||||
setCurrentPage(1);
|
||||
setAllProducts([]);
|
||||
}, []);
|
||||
|
||||
const handlePriceSortChange = useCallback(
|
||||
(sortType: "none" | "lowToHigh" | "highToLow") => {
|
||||
setPriceSort(sortType);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const resetFilters = useCallback(() => {
|
||||
setSelectedBrands(new Set());
|
||||
setSelectedFilterCategories(new Set());
|
||||
setPriceRange([0, 100000]);
|
||||
setPriceSort("none");
|
||||
setCurrentPage(1);
|
||||
setAllProducts([]);
|
||||
}, []);
|
||||
|
||||
const filterTranslations = useMemo(
|
||||
() => ({
|
||||
category: t("category"),
|
||||
brands: t("brands"),
|
||||
sort: t("sort"),
|
||||
default: t("default"),
|
||||
price_low_to_high: t("price_low_to_high"),
|
||||
price_high_to_low: t("price_high_to_low"),
|
||||
price: t("price"),
|
||||
price_from: t("price_from"),
|
||||
price_to: t("price_to"),
|
||||
reset: t("reset"),
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
if (productsError) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col mx-auto max-w-[1504px] px-2 md:px-4 lg:px-6 pb-12">
|
||||
<div className="bg-white p-4 rounded-t-lg mb-0">
|
||||
<h2 className="text-2xl md:text-3xl font-bold">
|
||||
{t("search_results")}: <span className="text-gray-500">"{q}"</span>
|
||||
</h2>
|
||||
<p className="text-gray-500 mt-1">
|
||||
{productsData?.pagination?.total || allProducts.length}{" "}
|
||||
{t("products_found")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex p-2 md:p-4 gap-4 bg-white rounded-b-lg">
|
||||
{/* Products Grid */}
|
||||
<div className="flex-1 bg-white rounded-lg mb-6">
|
||||
<CategoryProductsGrid
|
||||
products={sortedProducts}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={loadMoreData}
|
||||
isFetching={isFetching}
|
||||
translations={{
|
||||
loading: t("common.loading"),
|
||||
no_results: t("no_results"),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { apiClient } from "@/lib/api";
|
||||
import type { SearchResponse, SearchParams } from "../types";
|
||||
import type {
|
||||
Product,
|
||||
PaginatedResponse,
|
||||
ProductFilters,
|
||||
} from "@/lib/types/api";
|
||||
|
||||
export function useSearchProducts(params: SearchParams) {
|
||||
const { q, barcode } = params;
|
||||
@@ -10,14 +15,14 @@ export function useSearchProducts(params: SearchParams) {
|
||||
queryFn: async () => {
|
||||
if (barcode) {
|
||||
const response = await apiClient.get<SearchResponse>(
|
||||
`/search-product-barcode?barcode=${barcode}`
|
||||
`/search-product-barcode?barcode=${barcode}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
if (q) {
|
||||
const response = await apiClient.get<SearchResponse>(
|
||||
`/search-product?q=${encodeURIComponent(q)}`
|
||||
`/search-product?q=${encodeURIComponent(q)}`,
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user