Files
devops bb2c0bbe85 feat(ecommerce): ADR 0009 - checkout con direccion y seleccion de envio
Siguiente corte tras el carrito real: pagina /checkout de 3 pasos
(direccion -> envio -> confirmar) contra el Store API real, usando la
shipping option que ya se verifico en produccion. Endpoints nuevos
(setShippingAddress, getShippingOptions, addShippingMethod) probados
en vivo con un carrito de prueba real antes de escribir el codigo --
direccion, opciones de envio y total ya confirmados correctos
(80.000 + 5.000 = 95.000).

El pago (PayU custom) sigue sin existir, asi que el ultimo paso sigue
siendo WhatsApp -- pero ahora el mensaje incluye direccion, metodo de
envio y total real en vez de solo la lista de productos. El boton del
CartDrawer pasa de abrir WhatsApp directo a llevar a /checkout.
2026-08-29 04:12:11 +00:00

61 lines
4.3 KiB
TypeScript

"use client";
import Image from "next/image";
import Link from "next/link";
import { Minus, Plus, ShoppingBag, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet";
import { formatMoney } from "@/lib/utils";
import { useCartStore } from "@/store/cart-store";
const FREE_SHIPPING_THRESHOLD = 250_000;
export function CartDrawer() {
const { items, isOpen, closeCart, updateQuantity, removeItem, clearCart } = useCartStore();
const subtotal = items.reduce((sum, item) => sum + (item.product.price ?? 0) * item.quantity, 0);
const pricedItems = items.some((item) => item.product.price !== null);
const progress = pricedItems ? Math.min(100, (subtotal / FREE_SHIPPING_THRESHOLD) * 100) : 0;
const count = items.reduce((sum, item) => sum + item.quantity, 0);
return (
<Sheet open={isOpen} onOpenChange={(open) => { if (!open) closeCart(); }}>
<SheetContent>
<SheetHeader>
<SheetTitle>Tu carrito</SheetTitle>
<SheetDescription>{count} producto{count === 1 ? "" : "s"} seleccionado{count === 1 ? "" : "s"}.</SheetDescription>
</SheetHeader>
<div className="flex-1 space-y-5 overflow-y-auto p-6">
{items.length === 0 ? (
<div className="grid min-h-72 place-items-center rounded-3xl border border-dashed border-slate-300 bg-slate-50 text-center">
<div><ShoppingBag className="mx-auto mb-3 size-10 text-cyan-500" /><p className="font-black">Tu carrito está vacío</p><p className="mt-1 text-sm text-slate-500">Agrega un tesoro para comenzar.</p></div>
</div>
) : items.map((item) => (
<article key={`${item.product.id}-${item.variantId ?? "default"}`} className="grid grid-cols-[82px_1fr_auto] gap-4 rounded-2xl border border-slate-100 p-3 shadow-sm">
<div className="relative aspect-square overflow-hidden rounded-xl bg-slate-100"><Image src={item.product.thumbnail} alt={item.product.name} fill sizes="82px" className="object-cover" /></div>
<div><h3 className="text-sm font-black leading-5">{item.product.name}</h3><p className="mt-1 text-xs text-slate-500">{formatMoney(item.product.price, item.product.currency)}</p><div className="mt-3 inline-flex items-center rounded-full border border-slate-200"><button aria-label="Restar" className="p-2" onClick={() => updateQuantity(item.product.id, item.quantity - 1)}><Minus className="size-3" /></button><span className="min-w-7 text-center text-xs font-black">{item.quantity}</span><button aria-label="Sumar" className="p-2" onClick={() => updateQuantity(item.product.id, item.quantity + 1)}><Plus className="size-3" /></button></div></div>
<button className="self-start rounded-full p-2 text-slate-400 hover:bg-rose-50 hover:text-rose-600" onClick={() => removeItem(item.product.id)} aria-label="Eliminar"><Trash2 className="size-4" /></button>
</article>
))}
</div>
<div className="space-y-4 border-t border-slate-100 p-6">
<div><div className="mb-2 flex justify-between text-xs font-bold text-slate-600"><span>{pricedItems ? "Progreso para envío gratis" : "Cotización personalizada"}</span><span>{pricedItems ? `${Math.round(progress)}%` : "Por confirmar"}</span></div><Progress value={progress} /></div>
<div className="flex items-center justify-between"><span className="font-bold text-slate-600">Subtotal</span><strong className="text-xl font-black">{pricedItems ? formatMoney(subtotal) : "Por confirmar"}</strong></div>
{/* Button asChild + disabled no funciona sobre un <a> (:disabled
es pseudo-clase de elementos de formulario, un Link nunca la
cumple) -- se ramifica en vez de confiar en el prop disabled
sobre un Link. */}
{items.length > 0 ? (
<Button asChild variant="primary" size="lg" className="w-full"><Link href="/checkout" onClick={closeCart}>Confirmar pedido</Link></Button>
) : (
<Button variant="primary" size="lg" className="w-full" disabled>Confirmar pedido</Button>
)}
<Button variant="ghost" className="w-full" disabled={!items.length} onClick={clearCart}>Vaciar carrito</Button>
</div>
</SheetContent>
</Sheet>
);
}