Files
reui-blocks/blocks/shopping-cart19.tsx
T

349 lines
13 KiB
TypeScript

"use client";
import { Minus, Plus, ShoppingBag, Tag, Truck, X } from "lucide-react";
import { useState } from "react";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Progress } from "@/components/ui/progress";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import { cn } from "@/lib/utils";
interface CartItem {
id: string;
name: string;
image: string;
price: number;
quantity: number;
variant?: string;
}
interface UpsellProduct {
id: string;
name: string;
image: string;
price: number;
}
const DEFAULT_ITEMS: CartItem[] = [
{
id: "1",
name: "هدفون بی‌سیم نویزکنسل",
image:
"/block-media/shadcnblocks/block/ecommerce/electronics/Modern-White-Headphones-1.png",
price: 249.99,
quantity: 1,
},
{
id: "2",
name: "کیف چرمی پریمیوم",
image:
"/block-media/shadcnblocks/block/ecommerce/accessories/Elegant-Watch-on-Womans-Wrist-1.png",
price: 79.0,
quantity: 1,
},
];
const DEFAULT_UPSELLS: UpsellProduct[] = [
{
id: "u1",
name: "پایه هدفون",
image: "/block-media/shadcnblocks/block/placeholder-1.svg",
price: 29.99,
},
{
id: "u2",
name: "کیف حمل",
image: "/block-media/shadcnblocks/block/placeholder-2.svg",
price: 39.99,
},
];
const FREE_SHIPPING_THRESHOLD = 150;
interface ShoppingCart19Props {
items?: CartItem[];
upsells?: UpsellProduct[];
className?: string;
}
const ShoppingCart19 = ({
items: initialItems = DEFAULT_ITEMS,
upsells = DEFAULT_UPSELLS,
className,
}: ShoppingCart19Props) => {
const [items, setItems] = useState(initialItems);
const [promoCode, setPromoCode] = useState("");
const [isOpen, setIsOpen] = useState(false);
const updateQuantity = (id: string, delta: number) => {
setItems((prev) =>
prev
.map((item) =>
item.id === id
? { ...item, quantity: Math.max(0, item.quantity + delta) }
: item,
)
.filter((item) => item.quantity > 0),
);
};
const removeItem = (id: string) => {
setItems((prev) => prev.filter((item) => item.id !== id));
};
const addUpsell = (product: UpsellProduct) => {
const existing = items.find((item) => item.id === product.id);
if (existing) {
updateQuantity(product.id, 1);
} else {
setItems((prev) => [...prev, { ...product, quantity: 1 }]);
}
};
const formatPrice = (price: number) => {
return new Intl.NumberFormat("fa-IR", {
style: "currency",
currency: "IRR",
}).format(price);
};
const subtotal = items.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
);
const itemCount = items.reduce((sum, item) => sum + item.quantity, 0);
const freeShippingProgress = Math.min(
(subtotal / FREE_SHIPPING_THRESHOLD) * 100,
100,
);
const amountToFreeShipping = FREE_SHIPPING_THRESHOLD - subtotal;
return (
<div className={cn("inline-flex", className)}>
<Sheet open={isOpen} onOpenChange={setIsOpen}>
<SheetTrigger asChild>
<Button
variant="outline"
size="icon"
className="relative"
aria-label="سبد خرید"
>
<ShoppingBag className="size-5" />
{itemCount > 0 && (
<span className="absolute -top-1 -end-1 flex size-5 items-center justify-center rounded-full bg-primary text-xs font-medium text-primary-foreground">
{itemCount.toLocaleString("fa-IR")}
</span>
)}
</Button>
</SheetTrigger>
<SheetContent className="flex w-full flex-col p-0 sm:max-w-md">
<SheetHeader className="border-b p-4">
<SheetTitle>
سبد خرید ({itemCount.toLocaleString("fa-IR")})
</SheetTitle>
</SheetHeader>
{items.length > 0 ? (
<>
<div className="border-b bg-muted/30 p-4">
{subtotal >= FREE_SHIPPING_THRESHOLD ? (
<div className="flex items-center gap-2 text-sm text-emerald-600">
<Truck className="size-4" />
<span className="font-medium">ارسال رایگان فعال شد!</span>
</div>
) : (
<>
<div className="mb-2 flex items-center gap-2 text-sm">
<Truck className="size-4 text-muted-foreground" />
<span>
فقط{" "}
<span className="font-semibold">
{formatPrice(amountToFreeShipping)}
</span>{" "}
دیگر تا ارسال رایگان
</span>
</div>
<Progress value={freeShippingProgress} className="h-2" />
</>
)}
</div>
<ScrollArea className="flex-1">
<div className="p-4">
<div className="space-y-4">
{items.map((item) => (
<div key={item.id} className="flex gap-3">
<div className="size-20 shrink-0 overflow-hidden rounded-lg bg-muted">
<AspectRatio ratio={1}>
<img
src={item.image}
alt={item.name}
className="size-full object-cover"
/>
</AspectRatio>
</div>
<div className="flex min-w-0 flex-1 flex-col">
<div className="flex items-start justify-between gap-2">
<h4 className="text-sm leading-tight font-medium">
{item.name}
</h4>
<Button
variant="ghost"
size="icon"
className="size-6 shrink-0"
onClick={() => removeItem(item.id)}
aria-label={`حذف ${item.name} از سبد`}
>
<X className="size-3" />
</Button>
</div>
{item.variant && (
<p className="text-xs text-muted-foreground">
{item.variant}
</p>
)}
<div className="mt-auto flex items-center justify-between pt-2">
<div className="flex items-center rounded-md border">
<Button
variant="ghost"
size="icon"
className="size-8 rounded-none"
onClick={() => updateQuantity(item.id, -1)}
aria-label={`کاهش تعداد ${item.name}`}
>
<Minus className="size-3" />
</Button>
<span className="w-8 text-center text-sm">
{item.quantity.toLocaleString("fa-IR")}
</span>
<Button
variant="ghost"
size="icon"
className="size-8 rounded-none"
onClick={() => updateQuantity(item.id, 1)}
aria-label={`افزایش تعداد ${item.name}`}
>
<Plus className="size-3" />
</Button>
</div>
<span className="font-semibold">
{formatPrice(item.price * item.quantity)}
</span>
</div>
</div>
</div>
))}
</div>
{upsells.length > 0 && (
<div className="mt-6">
<p className="mb-3 text-sm font-medium">
شاید این‌ها را هم بپسندید
</p>
<div className="flex gap-3 overflow-x-auto pb-2">
{upsells.map((product) => (
<div
key={product.id}
className="w-28 shrink-0 rounded-lg border p-2"
>
<div className="mb-2 aspect-square overflow-hidden rounded bg-muted">
<img
src={product.image}
alt={product.name}
className="size-full object-cover"
/>
</div>
<p className="truncate text-xs font-medium">
{product.name}
</p>
<p className="text-xs text-muted-foreground">
{formatPrice(product.price)}
</p>
<Button
size="sm"
variant="outline"
className="mt-2 h-7 w-full text-xs"
onClick={() => addUpsell(product)}
aria-label={`افزودن ${product.name}`}
>
افزودن
</Button>
</div>
))}
</div>
</div>
)}
</div>
</ScrollArea>
<div className="border-t p-4">
<div className="mb-4 flex gap-2">
<div className="relative flex-1">
<Tag className="absolute top-1/2 start-3 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="کد تخفیف"
value={promoCode}
onChange={(e) => setPromoCode(e.target.value)}
className="ps-9"
/>
</div>
<Button variant="outline">اعمال کد</Button>
</div>
<Separator className="mb-4" />
<div className="mb-4 space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">جمع جزء</span>
<span>{formatPrice(subtotal)}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">هزینه ارسال</span>
<span>
{subtotal >= FREE_SHIPPING_THRESHOLD
? "رایگان"
: "محاسبه در صفحه پرداخت"}
</span>
</div>
<Separator />
<div className="flex justify-between font-semibold">
<span>مبلغ قابل پرداخت</span>
<span>{formatPrice(subtotal)}</span>
</div>
</div>
<Button className="w-full" size="lg">
ادامه خرید · {formatPrice(subtotal)}
</Button>
</div>
</>
) : (
<div className="flex flex-1 flex-col items-center justify-center p-8 text-center">
<ShoppingBag className="mb-4 size-12 text-muted-foreground" />
<h3 className="font-semibold">سبد خرید شما خالی است</h3>
<p className="mt-1 text-sm text-muted-foreground">
برای شروع، محصولی اضافه کنید
</p>
<Button className="mt-6" onClick={() => setIsOpen(false)}>
مشاهده محصولات
</Button>
</div>
)}
</SheetContent>
</Sheet>
</div>
);
};
export { ShoppingCart19 };