feat(ecommerce): banner dinámico desde catálogo real y buscador en dos filas en mobile

- hero-carousel.tsx: slides derivados de productos featured/nuevos reales
  (imagen, nombre, precio, link, badge de % OFF) en vez de un array
  hardcodeado; fallback estático solo si la tienda no tiene productos
  activos.
- navbar.tsx: en mobile/tablet el buscador pasa a una segunda fila a
  todo el ancho debajo de logo+iconos (antes competía por espacio en una
  sola fila y quedaba recortado); desktop sin cambios.
- docs: ADR 0010 documentando el pivote de pago PayU→Rapyd(descartado)→
  ePayco, la integración de EnvíoClick y checklist de pruebas reales
  pendientes desde el storefront; puntero agregado desde ADR 0009.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
This commit is contained in:
devopsandClaude Sonnet 5 committed 2026-09-09 21:45:25 +00:00
1 parent 1e6009daf7
commit 1abc69825c
5 files changed
+402 -35

No files matched your search

@@ -4,15 +4,33 @@ import Image from "next/image";
import Link from "next/link";
import { ArrowRight, BadgePercent, Sparkles } from "lucide-react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { Button } from "@/components/ui/button";
import type { Product } from "@/lib/headless/types";
// ADR 0008 sub-fase 4: hero azul con dots de paginacion (carrusel), en vez
// del bento estatico de 3 tarjetas en fucsia/rojo/violeta del esquema
// arcoiris original. Un slide a la vez, fondo azul de marca en los tres,
// para leerse como una sola pieza de marca en vez de 3 bloques de colores
// distintos.
const slides = [
//
// ADR 0010: los slides ya no son un array hardcodeado -- se derivan de
// productos activos reales del catalogo (pasados por prop desde
// app/page.tsx, que ya trae `products` del backend). Fallback estatico
// solo para el caso borde de una tienda sin ningun producto activo.
type Slide = {
id: string;
badge: string;
icon: typeof Sparkles;
title: string;
subtitle: string;
description: string;
href: string;
image: string;
imageAlt: string;
};
const FALLBACK_SLIDES: Slide[] = [
{
id: "celebracion",
badge: "Celebración ARI",
@@ -48,17 +66,46 @@ const slides = [
},
];
const MAX_SLIDES = 5;
const AUTOPLAY_MS = 6000;
export function HeroCarousel() {
function slidesFromProducts(products: Product[]): Slide[] {
return products.slice(0, MAX_SLIDES).map((product) => {
const hasDiscount = product.compareAtPrice !== null && product.compareAtPrice > (product.price ?? 0);
const pctOff = hasDiscount && product.price !== null
? Math.round(100 - (product.price / (product.compareAtPrice as number)) * 100)
: 0;
return {
id: product.id,
badge: hasDiscount ? `${pctOff}% OFF` : product.collection ? `Colección ${product.collection}` : "Destacado",
icon: hasDiscount ? BadgePercent : Sparkles,
title: product.name,
subtitle: product.priceLabel,
description: product.shortDescription || "Descúbrelo en la tienda ARI Shopping.",
href: `/product/${product.slug}`,
image: product.image,
imageAlt: product.name,
};
});
}
export function HeroCarousel({ products }: { products: Product[] }) {
const reduceMotion = useReducedMotion();
const [index, setIndex] = useState(0);
const slides = useMemo(() => {
const fromProducts = slidesFromProducts(products);
return fromProducts.length > 0 ? fromProducts : FALLBACK_SLIDES;
}, [products]);
useEffect(() => {
if (reduceMotion) return;
const timer = setInterval(() => setIndex((current) => (current + 1) % slides.length), AUTOPLAY_MS);
return () => clearInterval(timer);
}, [reduceMotion]);
}, [reduceMotion, slides.length]);
useEffect(() => {
if (index >= slides.length) setIndex(0);
}, [slides.length, index]);
const slide = slides[index];
if (!slide) return null;