feat(ecommerce): mensaje de urgencia por inventario real
Expone ProductVariant.inventoryQuantity (ya calculado desde variant.inventory_quantity en Medusa, hoy colapsado al booleano available) y lo usa para mostrar "Quedan N" cuando el stock real es bajo (<=5), tanto en la tarjeta de producto como en la ficha. null indica inventario no gestionado (backorder / manage_inventory=false), donde nunca se muestra un numero. ADR 0005 seccion 3 (patron eBay). Catalogo mock actualizado (data/products.json) con inventoryQuantity=null en todas las variantes para seguir tipando contra Product[] via `satisfies`.
This commit is contained in:
1 parent
707a05cc6e
commit
8fc9df3b50
6 files changed
+72
-22
No files matched your search
@@ -2,12 +2,12 @@
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Heart, ShoppingBag, Sparkles } from "lucide-react";
|
||||
import { Heart, ShoppingBag, Sparkles, TriangleAlert } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Product } from "@/lib/headless/types";
|
||||
import { formatMoney } from "@/lib/utils";
|
||||
import { formatMoney, LOW_STOCK_THRESHOLD } from "@/lib/utils";
|
||||
import { useCartStore } from "@/store/cart-store";
|
||||
import { useWishlistStore } from "@/store/wishlist-store";
|
||||
|
||||
@@ -23,6 +23,14 @@ export function ProductCard({ product, priority = false }: { product: Product; p
|
||||
: null;
|
||||
|
||||
const [selectedVariantId, setSelectedVariantId] = useState(product.variants[0]?.id);
|
||||
const selectedVariant = product.variants.find((variant) => variant.id === selectedVariantId) ?? product.variants[0];
|
||||
const lowStock =
|
||||
selectedVariant?.inventoryQuantity !== null &&
|
||||
selectedVariant?.inventoryQuantity !== undefined &&
|
||||
selectedVariant.inventoryQuantity > 0 &&
|
||||
selectedVariant.inventoryQuantity <= LOW_STOCK_THRESHOLD
|
||||
? selectedVariant.inventoryQuantity
|
||||
: null;
|
||||
|
||||
return (
|
||||
<article className="group flex h-full flex-col overflow-hidden rounded-3xl border border-slate-100 bg-white shadow-lg shadow-slate-900/5 transition duration-300 hover:-translate-y-1.5 hover:shadow-2xl hover:shadow-violet-500/10">
|
||||
@@ -56,6 +64,9 @@ export function ProductCard({ product, priority = false }: { product: Product; p
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{lowStock !== null && (
|
||||
<span className="flex items-center gap-1.5 text-xs font-bold text-amber-600"><TriangleAlert className="size-3.5" />Quedan {lowStock}</span>
|
||||
)}
|
||||
<div className="mt-auto flex items-center justify-between gap-3 pt-1">
|
||||
<div className="flex flex-col">
|
||||
{product.compareAtPrice && <span className="text-xs font-bold text-slate-400 line-through">{formatMoney(product.compareAtPrice, product.currency)}</span>}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { CheckCircle2, ShoppingBag, Truck } from "lucide-react";
|
||||
import { CheckCircle2, ShoppingBag, TriangleAlert, Truck } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Product } from "@/lib/headless/types";
|
||||
import { formatMoney } from "@/lib/utils";
|
||||
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);
|
||||
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-fuchsia-500 bg-fuchsia-50 text-fuchsia-700" : "border-slate-200"}`}>{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><Button variant="magenta" size="lg" className="w-full" onClick={() => addItem(product, variantId)}><ShoppingBag />Añadir al carrito</Button><div className="mt-5 grid gap-3 rounded-2xl bg-slate-50 p-4 text-sm"><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></div>;
|
||||
const selectedVariant = product.variants.find((variant) => variant.id === variantId) ?? product.variants[0];
|
||||
const lowStock =
|
||||
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-fuchsia-500 bg-fuchsia-50 text-fuchsia-700" : "border-slate-200"}`}>{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><Button variant="magenta" size="lg" className="w-full" onClick={() => addItem(product, variantId)}><ShoppingBag />Añadir al carrito</Button><div className="mt-5 grid gap-3 rounded-2xl bg-slate-50 p-4 text-sm">{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></div>;
|
||||
}
|
||||
Reference in new issue
Block a user