chore: add client/src/components/layout/CartDrawer.tsx

This commit is contained in:
notshop 2026-04-26 16:34:59 +00:00
parent ab05edf3ea
commit eee8e16ca0

View file

@ -0,0 +1,90 @@
import { X, Trash2, ShoppingBag } from "lucide-react";
import { Link } from "wouter";
import { useCart } from "../../hooks/use-cart";
export default function CartDrawer() {
const { items, isOpen, closeCart, updateQuantity, removeItem, subtotal } = useCart();
if (!isOpen) return null;
return (
<>
<div
className="fixed inset-0 bg-black/40 z-40"
onClick={closeCart}
aria-hidden
/>
<div className="fixed right-0 top-0 h-full w-full max-w-sm bg-background shadow-xl z-50 flex flex-col" data-testid="cart-drawer">
<div className="flex items-center justify-between p-4 border-b border-border">
<h2 className="font-semibold text-lg">Your Cart</h2>
<button onClick={closeCart} className="p-1 text-muted-foreground hover:text-foreground" data-testid="button-close-cart">
<X size={20} />
</button>
</div>
{items.length === 0 ? (
<div className="flex-1 flex flex-col items-center justify-center gap-4 p-8 text-center">
<ShoppingBag size={40} className="text-muted-foreground/40" />
<p className="text-muted-foreground">Your cart is empty</p>
<button onClick={closeCart} className="text-sm text-primary hover:underline">Continue shopping</button>
</div>
) : (
<>
<div className="flex-1 overflow-y-auto p-4 flex flex-col gap-4">
{items.map((item) => (
<div key={item.id} className="flex gap-3" data-testid={`cart-item-${item.id}`}>
{item.imageUrl && (
<img src={item.imageUrl} alt={item.title} className="w-16 h-16 object-cover rounded-md border border-border flex-shrink-0" />
)}
<div className="flex-1 min-w-0">
<p className="font-medium text-sm truncate">{item.title}</p>
{item.variantTitle && item.variantTitle !== "Default Title" && (
<p className="text-xs text-muted-foreground">{item.variantTitle}</p>
)}
<p className="text-sm font-medium mt-1">${parseFloat(item.price).toFixed(2)}</p>
<div className="flex items-center gap-2 mt-2">
<button
className="w-6 h-6 rounded border border-border text-sm hover:bg-muted flex items-center justify-center"
onClick={() => updateQuantity(item.id, item.quantity - 1)}
data-testid={`cart-item-decrease-${item.id}`}
></button>
<span className="text-sm w-4 text-center" data-testid={`cart-item-qty-${item.id}`}>{item.quantity}</span>
<button
className="w-6 h-6 rounded border border-border text-sm hover:bg-muted flex items-center justify-center"
onClick={() => updateQuantity(item.id, item.quantity + 1)}
data-testid={`cart-item-increase-${item.id}`}
>+</button>
<button
className="ml-auto text-muted-foreground hover:text-destructive"
onClick={() => removeItem(item.id)}
data-testid={`cart-item-remove-${item.id}`}
>
<Trash2 size={14} />
</button>
</div>
</div>
</div>
))}
</div>
<div className="border-t border-border p-4 space-y-3">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Subtotal</span>
<span className="font-semibold" data-testid="cart-subtotal">${subtotal.toFixed(2)}</span>
</div>
<p className="text-xs text-muted-foreground">Taxes and shipping calculated at checkout</p>
<Link
href="/checkout"
onClick={closeCart}
className="w-full bg-primary text-primary-foreground py-3 rounded-md text-sm font-semibold text-center block hover:bg-primary/90 transition-colors"
data-testid="link-checkout"
>
Checkout · ${subtotal.toFixed(2)}
</Link>
</div>
</>
)}
</div>
</>
);
}