Files
apps-registry/workloads/ecommerce/components/product/product-purchase.tsx
T
devopsandClaude Sonnet 5 c3f3857a92
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 25s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 9m36s
feat(ecommerce): aprovechar mejor el espacio en la página de producto
- "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
2026-09-13 22:20:41 +00:00

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>
);
}