Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edd14ca790 | ||
|
|
b6e3bddbc6 | ||
|
|
3c0ce03e09 | ||
|
|
e7699befc5 | ||
|
|
44d6e10ea2 | ||
|
|
f5de074ea1 | ||
|
|
8e74d0b516 | ||
|
|
e95ac086f0 | ||
|
|
6a78257d71 | ||
|
|
7a67ba0743 | ||
|
|
8fc9df3b50 |
No files matched your search
@@ -3,11 +3,19 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ChevronRight, SlidersHorizontal, X } from "lucide-react";
|
import { ChevronRight, SlidersHorizontal, X } from "lucide-react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { PromoTile } from "@/components/home/category-tiles";
|
||||||
import { ProductCard } from "@/components/product/product-card";
|
import { ProductCard } from "@/components/product/product-card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import type { Category, Product } from "@/lib/headless/types";
|
import type { Category, Product } from "@/lib/headless/types";
|
||||||
|
|
||||||
|
// Cada cuantas tarjetas de producto se intercala un tile promocional en
|
||||||
|
// la grilla (patron Walmart, ADR 0005 seccion 3): grid con tiles
|
||||||
|
// promocionales intercalados, no solo tarjetas homogeneas.
|
||||||
|
const PROMO_TILE_INTERVAL = 8;
|
||||||
|
|
||||||
|
type GridItem = { type: "product"; product: Product } | { type: "promo"; category: Category };
|
||||||
|
|
||||||
type CatalogClientProps = {
|
type CatalogClientProps = {
|
||||||
products: Product[];
|
products: Product[];
|
||||||
categories?: Category[];
|
categories?: Category[];
|
||||||
@@ -96,6 +104,11 @@ export function CatalogClient({
|
|||||||
// trata como el nombre de categoria exacto de siempre.
|
// trata como el nombre de categoria exacto de siempre.
|
||||||
const allowedCategories = categoryGroups[category] ?? [category];
|
const allowedCategories = categoryGroups[category] ?? [category];
|
||||||
const breadcrumb = useMemo(() => (category === "Todos" ? [] : buildBreadcrumb(categoryNodes, category)), [categoryNodes, category]);
|
const breadcrumb = useMemo(() => (category === "Todos" ? [] : buildBreadcrumb(categoryNodes, category)), [categoryNodes, category]);
|
||||||
|
// No promociona la misma categoria raiz que el usuario ya esta viendo.
|
||||||
|
const promoRoots = useMemo(() => {
|
||||||
|
const excludeRootId = breadcrumb[0]?.id;
|
||||||
|
return categoryNodes.filter((node) => !node.parentId && node.id !== excludeRootId);
|
||||||
|
}, [categoryNodes, breadcrumb]);
|
||||||
|
|
||||||
const filtered = useMemo(
|
const filtered = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -119,6 +132,27 @@ export function CatalogClient({
|
|||||||
[age, allowedCategories, brand, category, collection, maxPrice, products, query, sort],
|
[age, allowedCategories, brand, category, collection, maxPrice, products, query, sort],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Intercala un tile promocional cada PROMO_TILE_INTERVAL productos,
|
||||||
|
// ciclando por las categorias raiz -- si no hay jerarquia de
|
||||||
|
// categorias disponible, la grilla queda igual que antes (solo
|
||||||
|
// productos).
|
||||||
|
const gridItems = useMemo<GridItem[]>(() => {
|
||||||
|
if (promoRoots.length === 0) return filtered.map((product) => ({ type: "product", product }));
|
||||||
|
|
||||||
|
const items: GridItem[] = [];
|
||||||
|
let promoIndex = 0;
|
||||||
|
filtered.forEach((product, index) => {
|
||||||
|
items.push({ type: "product", product });
|
||||||
|
const isLast = index === filtered.length - 1;
|
||||||
|
const promoCategory = promoRoots[promoIndex % promoRoots.length];
|
||||||
|
if (!isLast && (index + 1) % PROMO_TILE_INTERVAL === 0 && promoCategory) {
|
||||||
|
items.push({ type: "promo", category: promoCategory });
|
||||||
|
promoIndex += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}, [filtered, promoRoots]);
|
||||||
|
|
||||||
// Conteos por valor de filtro (patron Amazon: "Marca (12)"), calculados
|
// Conteos por valor de filtro (patron Amazon: "Marca (12)"), calculados
|
||||||
// sobre los productos que ya pasan el resto de filtros activos -- cada
|
// sobre los productos que ya pasan el resto de filtros activos -- cada
|
||||||
// dimension se cuenta excluyendose a si misma para reflejar cuantos
|
// dimension se cuenta excluyendose a si misma para reflejar cuantos
|
||||||
@@ -284,9 +318,13 @@ export function CatalogClient({
|
|||||||
|
|
||||||
{filtered.length ? (
|
{filtered.length ? (
|
||||||
<div className="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
<div className="grid gap-5 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
{filtered.map((product) => (
|
{gridItems.map((item, index) =>
|
||||||
<ProductCard key={product.id} product={product} />
|
item.type === "product" ? (
|
||||||
))}
|
<ProductCard key={item.product.id} product={item.product} />
|
||||||
|
) : (
|
||||||
|
<PromoTile key={`promo-${index}`} category={item.category} className="min-h-[22rem]" />
|
||||||
|
),
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid min-h-80 place-items-center rounded-3xl border border-dashed border-slate-300 text-center">
|
<div className="grid min-h-80 place-items-center rounded-3xl border border-dashed border-slate-300 text-center">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowUpRight } from "lucide-react";
|
import { ArrowUpRight } from "lucide-react";
|
||||||
import type { Category } from "@/lib/headless/types";
|
import type { Category } from "@/lib/headless/types";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type TileContent = { subtitle: string; image?: string; color: string };
|
type TileContent = { subtitle: string; image?: string; color: string };
|
||||||
|
|
||||||
@@ -20,29 +21,17 @@ const tileContent: Record<string, TileContent> = {
|
|||||||
|
|
||||||
const defaultTile: TileContent = { subtitle: "Explora esta categoría", color: "from-slate-400 to-slate-600" };
|
const defaultTile: TileContent = { subtitle: "Explora esta categoría", color: "from-slate-400 to-slate-600" };
|
||||||
|
|
||||||
export function CategoryTiles({ categories }: { categories: Category[] }) {
|
// Tile individual reutilizado tanto por la seccion de mundos del home
|
||||||
const roots = categories.filter((category) => !category.parentId);
|
// como por el grid del catalogo (tiles promocionales intercalados,
|
||||||
|
// ADR 0005 seccion 3, patron Walmart) -- mismo primitivo visual, no un
|
||||||
if (roots.length === 0) return null;
|
// componente nuevo.
|
||||||
|
export function PromoTile({ category, className = "" }: { category: Category; className?: string }) {
|
||||||
return (
|
|
||||||
<section className="mx-auto max-w-7xl px-4 py-14">
|
|
||||||
<div className="mb-8 flex items-end justify-between gap-4">
|
|
||||||
<div>
|
|
||||||
<span className="text-xs font-black uppercase tracking-[.18em] text-fuchsia-600">Explora por mundos</span>
|
|
||||||
<h2 className="mt-2 text-4xl font-black tracking-tight sm:text-5xl">Encuentra tu próximo tesoro</h2>
|
|
||||||
</div>
|
|
||||||
<Link href="/catalog" className="hidden font-black text-cyan-700 sm:block">Ver todo →</Link>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
|
||||||
{roots.map((category) => {
|
|
||||||
const content = tileContent[category.handle] ?? defaultTile;
|
const content = tileContent[category.handle] ?? defaultTile;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={category.id}
|
|
||||||
href={`/catalog?category=${encodeURIComponent(category.name)}`}
|
href={`/catalog?category=${encodeURIComponent(category.name)}`}
|
||||||
className={`group relative min-h-64 overflow-hidden rounded-[2rem] bg-gradient-to-br ${content.color} shadow-xl`}
|
className={cn(`group relative min-h-64 overflow-hidden rounded-[2rem] bg-gradient-to-br ${content.color} shadow-xl`, className)}
|
||||||
>
|
>
|
||||||
{content.image && (
|
{content.image && (
|
||||||
<>
|
<>
|
||||||
@@ -61,7 +50,26 @@ export function CategoryTiles({ categories }: { categories: Category[] }) {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
}
|
||||||
|
|
||||||
|
export function CategoryTiles({ categories }: { categories: Category[] }) {
|
||||||
|
const roots = categories.filter((category) => !category.parentId);
|
||||||
|
|
||||||
|
if (roots.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="mx-auto max-w-7xl px-4 py-14">
|
||||||
|
<div className="mb-8 flex items-end justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs font-black uppercase tracking-[.18em] text-fuchsia-600">Explora por mundos</span>
|
||||||
|
<h2 className="mt-2 text-4xl font-black tracking-tight sm:text-5xl">Encuentra tu próximo tesoro</h2>
|
||||||
|
</div>
|
||||||
|
<Link href="/catalog" className="hidden font-black text-cyan-700 sm:block">Ver todo →</Link>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||||
|
{roots.map((category) => (
|
||||||
|
<PromoTile key={category.id} category={category} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ spec:
|
|||||||
runAsUser: 1001
|
runAsUser: 1001
|
||||||
containers:
|
containers:
|
||||||
- name: app
|
- name: app
|
||||||
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.147
|
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.157
|
||||||
env:
|
env:
|
||||||
- name: HEADLESS_PROVIDER
|
- name: HEADLESS_PROVIDER
|
||||||
value: medusa
|
value: medusa
|
||||||
|
|||||||
@@ -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