- "Detalles del producto" (marca, edad, categoría, colección) pasa de
acordeón colapsado a una mini tabla siempre visible -- mismos datos
reales de siempre, solo que ya no hace falta un click para verlos.
- Buy-box: nueva franja de confianza real con enlaces a /envios y
/cambios (5 días hábiles, envío de vuelta gratis) -- mismas
políticas ya publicadas en el footer, no texto inventado.
- "También te puede interesar" ahora muestra hasta 2 sugerencias en
vez de 1, mismo cálculo real por categoría/colección que ya
alimentaba el carrusel de abajo.
Investigado de paso (sin fix): la discrepancia de stock reportada
("Quedan 1" vs available:false) no es un bug de ProductPurchase -- la
página de producto usa /store/products de Medusa (en vivo, correcto),
mientras que /store/search (Meilisearch) devuelve inventario
desactualizado para el mismo producto. Eso vive en commerce-backend,
fuera de alcance de este cambio de frontend.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
101 lines
4.3 KiB
TypeScript
101 lines
4.3 KiB
TypeScript
"use client";
|
|
|
|
import { CheckCircle2, RotateCcw, ShoppingBag, TriangleAlert, Truck, XCircle } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { useState } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import type { Product } from "@/lib/headless/types";
|
|
import { formatMoney, LOW_STOCK_THRESHOLD } from "@/lib/utils";
|
|
import { useCartStore } from "@/store/cart-store";
|
|
|
|
export function ProductPurchase({ product }: { product: Product }) {
|
|
const [variantId, setVariantId] = useState(product.variants[0]?.id);
|
|
const addItem = useCartStore((state) => state.addItem);
|
|
const selectedVariant = product.variants.find((variant) => variant.id === variantId) ?? product.variants[0];
|
|
const outOfStock = selectedVariant?.available === false;
|
|
const lowStock =
|
|
!outOfStock &&
|
|
selectedVariant?.inventoryQuantity !== null &&
|
|
selectedVariant?.inventoryQuantity !== undefined &&
|
|
selectedVariant.inventoryQuantity > 0 &&
|
|
selectedVariant.inventoryQuantity <= LOW_STOCK_THRESHOLD
|
|
? selectedVariant.inventoryQuantity
|
|
: null;
|
|
|
|
return (
|
|
<div>
|
|
<div className="mb-5 flex flex-wrap gap-2">
|
|
{product.variants.map((variant) => (
|
|
<button
|
|
key={variant.id}
|
|
onClick={() => setVariantId(variant.id)}
|
|
className={`rounded-full border px-4 py-2 text-sm font-bold ${
|
|
variantId === variant.id ? "border-ari-marino bg-ari-marino text-white" : "border-slate-200"
|
|
} ${variant.available === false ? "opacity-40" : ""}`}
|
|
>
|
|
{variant.value}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="mb-6 flex items-end gap-3">
|
|
<strong className="text-3xl font-black">{formatMoney(product.price, product.currency)}</strong>
|
|
{product.compareAtPrice && <del className="text-slate-400">{formatMoney(product.compareAtPrice, product.currency)}</del>}
|
|
</div>
|
|
{/* Visible tambien en mobile (antes quedaba oculto <lg porque
|
|
MobileAddToCart quedaba fijo abajo desde el primer render,
|
|
tapando parte de la imagen sin necesidad). id usado por
|
|
MobileAddToCart para saber cuando este boton real sale del
|
|
viewport y recien ahi mostrar su barra sticky -- mismo patron
|
|
que el "Buy Now" sticky de Amazon en mobile. */}
|
|
<Button
|
|
id="product-buy-button"
|
|
variant="primary"
|
|
size="lg"
|
|
className="w-full"
|
|
disabled={outOfStock}
|
|
onClick={() => addItem(product, variantId)}
|
|
>
|
|
<ShoppingBag />
|
|
{outOfStock ? "Agotado" : "Añadir al carrito"}
|
|
</Button>
|
|
<div className="mt-5 grid gap-3 rounded-2xl bg-slate-50 p-4 text-sm">
|
|
{outOfStock ? (
|
|
<span className="flex items-center gap-2 font-bold text-rose-600">
|
|
<XCircle className="size-4" />
|
|
Agotado
|
|
</span>
|
|
) : lowStock !== null ? (
|
|
<span className="flex items-center gap-2 font-bold text-amber-600">
|
|
<TriangleAlert className="size-4" />
|
|
Quedan {lowStock} unidades
|
|
</span>
|
|
) : (
|
|
<span className="flex items-center gap-2 font-bold">
|
|
<CheckCircle2 className="size-4 text-emerald-500" />
|
|
Disponibilidad por confirmar
|
|
</span>
|
|
)}
|
|
<span className="flex items-center gap-2 font-bold">
|
|
<Truck className="size-4 text-cyan-600" />
|
|
Envíos nacionales
|
|
</span>
|
|
</div>
|
|
{/* Franja de confianza real (estilo Walmart) -- enlaza a las
|
|
páginas reales de /envios y /cambios en vez de repetir texto
|
|
suelto, con los datos reales que confirmó el usuario (5 días
|
|
hábiles, envío de vuelta a cargo de ARI Shopping). Nada
|
|
inventado: mismas políticas ya publicadas en el footer. */}
|
|
<div className="mt-3 grid gap-2 text-sm">
|
|
<Link href="/envios" className="flex items-center gap-2 font-bold text-slate-600 hover:text-ari-marino hover:underline">
|
|
<Truck className="size-4 text-cyan-600" />
|
|
Envíos calculados en el checkout · ver detalles
|
|
</Link>
|
|
<Link href="/cambios" className="flex items-center gap-2 font-bold text-slate-600 hover:text-ari-marino hover:underline">
|
|
<RotateCcw className="size-4 text-ari-marino" />
|
|
Cambios en 5 días hábiles, envío de vuelta gratis
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|