Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fc9df3b50 |
No files matched your search
@@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
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 { useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import type { Product } from "@/lib/headless/types";
|
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 { useCartStore } from "@/store/cart-store";
|
||||||
import { useWishlistStore } from "@/store/wishlist-store";
|
import { useWishlistStore } from "@/store/wishlist-store";
|
||||||
|
|
||||||
@@ -23,6 +23,14 @@ export function ProductCard({ product, priority = false }: { product: Product; p
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
const [selectedVariantId, setSelectedVariantId] = useState(product.variants[0]?.id);
|
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 (
|
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">
|
<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>
|
</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="mt-auto flex items-center justify-between gap-3 pt-1">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{product.compareAtPrice && <span className="text-xs font-bold text-slate-400 line-through">{formatMoney(product.compareAtPrice, product.currency)}</span>}
|
{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";
|
"use client";
|
||||||
|
|
||||||
import { CheckCircle2, ShoppingBag, Truck } from "lucide-react";
|
import { CheckCircle2, ShoppingBag, TriangleAlert, Truck } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import type { Product } from "@/lib/headless/types";
|
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 { useCartStore } from "@/store/cart-store";
|
||||||
|
|
||||||
export function ProductPurchase({ product }: { product: Product }) {
|
export function ProductPurchase({ product }: { product: Product }) {
|
||||||
const [variantId, setVariantId] = useState(product.variants[0]?.id);
|
const [variantId, setVariantId] = useState(product.variants[0]?.id);
|
||||||
const addItem = useCartStore((state) => state.addItem);
|
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>;
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,8 @@
|
|||||||
"id": "ari-001-toy-story-5-rex-default",
|
"id": "ari-001-toy-story-5-rex-default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Edición original",
|
"value": "Edición original",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-001-toy-story-5-rex.webp"
|
"thumbnail": "/images/thumbnails/ari-001-toy-story-5-rex.webp"
|
||||||
@@ -66,7 +67,8 @@
|
|||||||
"id": "ari-002-toy-story-5-buzz-lightyear-default",
|
"id": "ari-002-toy-story-5-buzz-lightyear-default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Edición original",
|
"value": "Edición original",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-002-toy-story-5-buzz-lightyear.webp"
|
"thumbnail": "/images/thumbnails/ari-002-toy-story-5-buzz-lightyear.webp"
|
||||||
@@ -102,7 +104,8 @@
|
|||||||
"id": "ari-003-toy-story-5-jessie-default",
|
"id": "ari-003-toy-story-5-jessie-default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Edición original",
|
"value": "Edición original",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-003-toy-story-5-jessie.webp"
|
"thumbnail": "/images/thumbnails/ari-003-toy-story-5-jessie.webp"
|
||||||
@@ -138,7 +141,8 @@
|
|||||||
"id": "ari-004-toy-story-5-woody-default",
|
"id": "ari-004-toy-story-5-woody-default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Edición original",
|
"value": "Edición original",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-004-toy-story-5-woody.webp"
|
"thumbnail": "/images/thumbnails/ari-004-toy-story-5-woody.webp"
|
||||||
@@ -174,7 +178,8 @@
|
|||||||
"id": "ari-005-intensamente-furia-default",
|
"id": "ari-005-intensamente-furia-default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Edición original",
|
"value": "Edición original",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-005-intensamente-furia.webp"
|
"thumbnail": "/images/thumbnails/ari-005-intensamente-furia.webp"
|
||||||
@@ -210,7 +215,8 @@
|
|||||||
"id": "ari-006-intensamente-tristeza-default",
|
"id": "ari-006-intensamente-tristeza-default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Edición original",
|
"value": "Edición original",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-006-intensamente-tristeza.webp"
|
"thumbnail": "/images/thumbnails/ari-006-intensamente-tristeza.webp"
|
||||||
@@ -246,7 +252,8 @@
|
|||||||
"id": "ari-007-minnie-y-mickey-mouse-default",
|
"id": "ari-007-minnie-y-mickey-mouse-default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Edición original",
|
"value": "Edición original",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-007-minnie-y-mickey-mouse.webp"
|
"thumbnail": "/images/thumbnails/ari-007-minnie-y-mickey-mouse.webp"
|
||||||
@@ -282,7 +289,8 @@
|
|||||||
"id": "ari-008-angel-lilo-y-stitch-default",
|
"id": "ari-008-angel-lilo-y-stitch-default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Edición original",
|
"value": "Edición original",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-008-angel-lilo-y-stitch.webp"
|
"thumbnail": "/images/thumbnails/ari-008-angel-lilo-y-stitch.webp"
|
||||||
@@ -318,7 +326,8 @@
|
|||||||
"id": "ari-009-mochilas-disney-coleccion-marvel-default",
|
"id": "ari-009-mochilas-disney-coleccion-marvel-default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Rosa",
|
"value": "Rosa",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-009-mochilas-disney-coleccion-marvel.webp"
|
"thumbnail": "/images/thumbnails/ari-009-mochilas-disney-coleccion-marvel.webp"
|
||||||
@@ -354,7 +363,8 @@
|
|||||||
"id": "ari-010-mochila-ligera-y-resistente-al-agua-de-d-default",
|
"id": "ari-010-mochila-ligera-y-resistente-al-agua-de-d-default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Rosa",
|
"value": "Rosa",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-010-mochila-ligera-y-resistente-al-agua-de-d.webp"
|
"thumbnail": "/images/thumbnails/ari-010-mochila-ligera-y-resistente-al-agua-de-d.webp"
|
||||||
@@ -390,7 +400,8 @@
|
|||||||
"id": "ari-011-mochila-escolar-de-spider-man-de-disney--default",
|
"id": "ari-011-mochila-escolar-de-spider-man-de-disney--default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Rosa",
|
"value": "Rosa",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-011-mochila-escolar-de-spider-man-de-disney-.webp"
|
"thumbnail": "/images/thumbnails/ari-011-mochila-escolar-de-spider-man-de-disney-.webp"
|
||||||
@@ -426,7 +437,8 @@
|
|||||||
"id": "ari-012-disney-mochila-de-poliester-ultraligera--default",
|
"id": "ari-012-disney-mochila-de-poliester-ultraligera--default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Rosa",
|
"value": "Rosa",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-012-disney-mochila-de-poliester-ultraligera-.webp"
|
"thumbnail": "/images/thumbnails/ari-012-disney-mochila-de-poliester-ultraligera-.webp"
|
||||||
@@ -462,7 +474,8 @@
|
|||||||
"id": "ari-013-mochila-pequena-bob-esponja-default",
|
"id": "ari-013-mochila-pequena-bob-esponja-default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Rosa",
|
"value": "Rosa",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-013-mochila-pequena-bob-esponja.webp"
|
"thumbnail": "/images/thumbnails/ari-013-mochila-pequena-bob-esponja.webp"
|
||||||
@@ -498,7 +511,8 @@
|
|||||||
"id": "ari-014-mochila-casual-de-gran-capacidad-con-est-default",
|
"id": "ari-014-mochila-casual-de-gran-capacidad-con-est-default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Rosa",
|
"value": "Rosa",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-014-mochila-casual-de-gran-capacidad-con-est.webp"
|
"thumbnail": "/images/thumbnails/ari-014-mochila-casual-de-gran-capacidad-con-est.webp"
|
||||||
@@ -534,7 +548,8 @@
|
|||||||
"id": "ari-015-mochila-escolar-estilo-britanico-de-disn-default",
|
"id": "ari-015-mochila-escolar-estilo-britanico-de-disn-default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Rosa",
|
"value": "Rosa",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-015-mochila-escolar-estilo-britanico-de-disn.webp"
|
"thumbnail": "/images/thumbnails/ari-015-mochila-escolar-estilo-britanico-de-disn.webp"
|
||||||
@@ -570,7 +585,8 @@
|
|||||||
"id": "ari-016-disney-mochila-iron-man-de-poliester-ult-default",
|
"id": "ari-016-disney-mochila-iron-man-de-poliester-ult-default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Rosa",
|
"value": "Rosa",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-016-disney-mochila-iron-man-de-poliester-ult.webp"
|
"thumbnail": "/images/thumbnails/ari-016-disney-mochila-iron-man-de-poliester-ult.webp"
|
||||||
@@ -606,7 +622,8 @@
|
|||||||
"id": "ari-017-mochila-de-disney-marvel-capitan-america-default",
|
"id": "ari-017-mochila-de-disney-marvel-capitan-america-default",
|
||||||
"name": "Presentación",
|
"name": "Presentación",
|
||||||
"value": "Rosa",
|
"value": "Rosa",
|
||||||
"available": true
|
"available": true,
|
||||||
|
"inventoryQuantity": null
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnail": "/images/thumbnails/ari-017-mochila-de-disney-marvel-capitan-america.webp"
|
"thumbnail": "/images/thumbnails/ari-017-mochila-de-disney-marvel-capitan-america.webp"
|
||||||
|
|||||||
@@ -157,11 +157,19 @@ function mapVariant(variant: MedusaVariant): ProductVariant {
|
|||||||
variant.manage_inventory === false ||
|
variant.manage_inventory === false ||
|
||||||
(variant.inventory_quantity ?? 0) > 0;
|
(variant.inventory_quantity ?? 0) > 0;
|
||||||
|
|
||||||
|
// null = inventario no gestionado (backorder o manage_inventory=false),
|
||||||
|
// no hay un numero real que mostrar como "quedan N".
|
||||||
|
const inventoryQuantity =
|
||||||
|
variant.allow_backorder === true || variant.manage_inventory === false
|
||||||
|
? null
|
||||||
|
: (variant.inventory_quantity ?? 0);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: variant.id,
|
id: variant.id,
|
||||||
name: option?.option?.title || "Presentación",
|
name: option?.option?.title || "Presentación",
|
||||||
value: option?.value || variant.title || "Estándar",
|
value: option?.value || variant.title || "Estándar",
|
||||||
available,
|
available,
|
||||||
|
inventoryQuantity,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export interface ProductVariant {
|
|||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
available: boolean;
|
available: boolean;
|
||||||
|
inventoryQuantity: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Product {
|
export interface Product {
|
||||||
|
|||||||
@@ -13,3 +13,8 @@ export function formatMoney(value: number | null, currency = "COP"): string {
|
|||||||
maximumFractionDigits: 0,
|
maximumFractionDigits: 0,
|
||||||
}).format(value);
|
}).format(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Umbral desde el cual mostramos "Quedan N" en vez de solo "Disponible".
|
||||||
|
// inventoryQuantity=null significa inventario no gestionado (backorder /
|
||||||
|
// manage_inventory=false en Medusa) -- ahi nunca mostramos el mensaje.
|
||||||
|
export const LOW_STOCK_THRESHOLD = 5;
|
||||||
Reference in new issue
Block a user