From 1abc69825c04fff068c5e7b42d81e8d6fa69e8b8 Mon Sep 17 00:00:00 2001 From: Cristian Felipe Cruz Buitron Date: Wed, 9 Sep 2026 21:45:25 +0000 Subject: [PATCH] =?UTF-8?q?feat(ecommerce):=20banner=20din=C3=A1mico=20des?= =?UTF-8?q?de=20cat=C3=A1logo=20real=20y=20buscador=20en=20dos=20filas=20e?= =?UTF-8?q?n=20mobile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR --- .../docs/decisiones/0009-checkout-real.md | 7 +- .../0010-pago-envio-reales-y-storefront.md | 298 ++++++++++++++++++ workloads/ecommerce/app/page.tsx | 8 +- .../components/home/hero-carousel.tsx | 55 +++- .../ecommerce/components/layout/navbar.tsx | 69 ++-- 5 files changed, 402 insertions(+), 35 deletions(-) create mode 100644 workloads/docs-portal/docs/decisiones/0010-pago-envio-reales-y-storefront.md diff --git a/workloads/docs-portal/docs/decisiones/0009-checkout-real.md b/workloads/docs-portal/docs/decisiones/0009-checkout-real.md index f8449c9..62978ac 100644 --- a/workloads/docs-portal/docs/decisiones/0009-checkout-real.md +++ b/workloads/docs-portal/docs/decisiones/0009-checkout-real.md @@ -1,7 +1,10 @@ # ADR 0009 — Checkout real (pago en línea + envíos) para ARI Shopping -**Estado:** aceptada — proveedor de pago decidido (PayU, §3); -implementación real (credenciales, shipping options, código) pendiente +**Estado:** aceptada — proveedor de pago decidido (PayU, §3); la +implementación real terminó usando ePayco en vez de PayU (PayU LatAm +quedó inviable, Rapyd se evaluó y se descartó sin código) y sumó +EnvíoClick para envío calculado — ver [ADR 0010](./0010-pago-envio-reales-y-storefront.md) +para el pivote completo y el estado final desplegado **Fecha:** 2026-08-28 ## Contexto diff --git a/workloads/docs-portal/docs/decisiones/0010-pago-envio-reales-y-storefront.md b/workloads/docs-portal/docs/decisiones/0010-pago-envio-reales-y-storefront.md new file mode 100644 index 0000000..e8c5c8b --- /dev/null +++ b/workloads/docs-portal/docs/decisiones/0010-pago-envio-reales-y-storefront.md @@ -0,0 +1,298 @@ +# ADR 0010 — Pago (ePayco PSE) y envío (EnvíoClick) reales; banner y buscador del storefront + +**Estado:** aceptada — ambas integraciones desplegadas y verificadas en vivo (sandbox de cada proveedor); banner dinámico y buscador rediseñado desplegados +**Fecha:** 2026-09-09 + +## Contexto + +El ADR 0009 dejó **PayU** decidido como proveedor de pago de negocio y +documentó la necesidad de un módulo custom (`AbstractPaymentProvider`) +porque ningún plugin comunitario de Medusa sirve para la API real de +PayU LatAm. Entre esa decisión y la implementación real pasaron dos +correcciones más, ninguna prevista en el ADR original — este documento +cierra esa cadena y agrega la segunda mitad de "checkout real" que el +0009 dejó pendiente: el envío (EnvíoClick), más dos mejoras de UI del +storefront relacionadas (banner dinámico, buscador) pedidas una vez que +ambas integraciones ya estaban en producción. + +## 1. Pago — de PayU a Rapyd a ePayco + +### 1.1 PayU LatAm quedó inviable: Rapyd compró ese negocio + +Al intentar registrar la cuenta de comercio en `developers.payulatam.com` +(2026-08-29), el registro real redirige a **Rapyd** — confirmado en vivo +contra `board.rapyd.net` con capturas de pantalla del propio usuario, no +solo documentación. Investigación confirmó la causa: **Rapyd adquirió el +negocio de PayU LatAm/África en 2023.** PayU LatAm como entidad separada +con la API documentada en el ADR 0009 (`apiLogin`/`apiKey`/`accountId`) +ya no es la vía de registro real para un comercio nuevo. + +Se investigó y planificó por completo la alternativa Rapyd (API REST +moderna, firma HMAC-SHA256, endpoint `/v1/payments` — arquitectura +totalmente distinta a la de PayU LatAm clásico, no una migración menor +de credenciales) pero **nunca se escribió código**: antes de implementar, +el usuario planteó ePayco como alternativa y, al evaluarla, resultó +preferible por costo/encaje — Rapyd quedó descartado en la etapa de plan, +no de código. + +### 1.2 Decisión final: ePayco + +**Motivo de negocio:** ePayco es 100% propiedad de Davivienda desde 2024 +— mejor tarifa PSE dado que Davivienda ya es uno de los bancos +disponibles del comercio (mismo dato de negocio del ADR 0009 §3: +Davivienda, Lulo Bank, Banco de Bogotá). + +**Hallazgo técnico no obvio, verificado en vivo, no en documentación:** +ePayco tiene dos productos de integración distintos bajo la misma marca: + +- Un producto nuevo tipo "Apify"/Web-Checkout (widget alojado). +- La integración real de PSE que efectivamente se usó: la API + "restpagos" / `secure.payco.co`, **más antigua**, que combina login con + bearer token **más cifrado AES-CBC campo por campo** usando la llave + privada del comercio como clave AES con IV en cero — un protocolo poco + común que no vale la pena reimplementar a mano. Se optó por depender + del paquete oficial `epayco-sdk-node` en vez de escribir el cliente + HTTP propio (a diferencia de EnvíoClick, ver §2). + +Nombres de método/campo reales del SDK, corregidos contra su código +fuente instalado (no contra su propia documentación pública, que estaba +desactualizada respecto al paquete): + +- Lista de bancos PSE: `getBanks()` (la doc pública sugiere `pseBank()`, + que no existe en el SDK real). +- Confirmación de método: campo `method_confirmation` (la doc pública + sugiere `metodoconfirmacion`). + +**Implementación** (`commerce-backend/src/modules/epayco-pse/`, +`src/lib/epayco.ts`): módulo de pago custom +(`AbstractPaymentProvider`), estado `pending_authorization` tras iniciar +el pago (mecanismo documentado de Medusa para métodos asíncronos — +permite que el carrito complete en una orden "esperando confirmación" sin +bloquear sobre la respuesta del banco), webhook nativo de Medusa en +`/hooks/payment/epayco_pse` (sin subscriber custom — el +`payment-webhook` + `process-payment-workflow` incluidos en el core ya +completan la orden automáticamente al recibir la notificación). + +**Verificado en vivo (2026-09):** +- Credenciales reales del comercio (`P_CUST_ID_CLIENTE`, `P_KEY`, + `PUBLIC_KEY`, `PRIVATE_KEY`) cargadas en el secret real del cluster. +- Llamada real a `getBanks()` contra la API de ePayco devolvió el listado + real de bancos PSE en modo sandbox. +- El webhook `/hooks/payment/epayco_pse` es alcanzable desde afuera: + `POST` externo contra `https://commerce.cruzcloud.net/hooks/payment/epayco_pse` + responde `200 OK` (la firma inválida se maneja puertas adentro sin + romper la respuesta HTTP) — cierra el riesgo de red que el ADR 0009 §2 + había señalado como pendiente de validar (mismo patrón de incidente ya + documentado para otro servicio, + `incidente-cloudflared-504-quic-2026-08.md`). + +**Limitaciones conocidas y aceptadas, no bloqueantes:** +- `refundPayment` tira un error explícito de "no implementado" — el + endpoint real de reembolso nunca se verificó en vivo, se prefirió + fallar ruidosamente a fingir éxito. +- El nombre exacto del campo de la URL de redirección del banco en la + respuesta de `bank.create()` se maneja de forma defensiva, probando + varios nombres candidatos con un error explícito y el payload completo + si ninguno matchea — **todavía no se probó con un pago PSE real de + principio a fin** (solo `getBanks()` está confirmado en vivo). Ver §3 + para cómo cerrar esto desde el storefront real. +- El comercio está en modo **sandbox** de ePayco (`EPAYCO_ENV: sandbox` + en el ConfigMap) — no se ha movido dinero real todavía. + +## 2. Envío — EnvíoClick reemplaza la tarifa fija + +El ADR 0009 (nota de implementación) dejó una shipping option con tarifa +fija de $15.000 COP como **placeholder de negocio**, anotando +explícitamente que un cálculo real por peso/volumen era trabajo futuro +— "eso es justamente lo que EnvíoClick resolvería más adelante". Este +ADR cierra ese punto. + +### Hallazgo de negocio, no técnico, que condiciona la precisión real + +**Ningún producto del catálogo tiene peso ni dimensiones cargados** +(confirmado contra la base real: 18 productos activos, 0 con `weight`, +0 con `length`). Sin este dato, cualquier cotización real de EnvíoClick +no puede ser precisa. Se aceptó como **tarea de datos pendiente, no de +código** — la integración usa un paquete de fallback conservador (1 kg, +20×20×20 cm) mientras no haya datos reales por producto, documentado en +`src/modules/envioclick-fulfillment/types.ts` (`DEFAULT_PACKAGE`). + +### API real, corregida contra la fuente primaria, no contra un plugin de referencia desactualizado + +Un plugin de WordPress usado como primera referencia resultó ser para la +**API v1** de EnvíoClick, ya obsoleta. La doc real +(`apidoc.envioclickpro.com.co`, generada con apiDoc — se bajó y leyó +directo `api_data.json`/`api_project.json`, ya que la SPA no renderizaba +sin sesión) reveló diferencias reales, confirmadas con una llamada real +exitosa: + +| | Plugin de referencia (v1, desactualizado) | API real (v2, confirmada en vivo) | +|---|---|---| +| Base URL | `api.envioclickpro.com` | `https://api.envioclickpro.com.co/api/v2` | +| Header de auth | `AuthorizationKey: ` | `Authorization: ` (sin prefijo) | + +`POST /quotation` devolvió en vivo tarifas reales de PIBOX, COORDINADORA, +ENVIA, RAPPI y TCC para una cotización de prueba real — se toma +`flete + minimumInsurance` (seguro mínimo obligatorio, hay que sumarlo al +flete para el precio real a cobrar) más barato del array `data.rates[]`, +mismo criterio de "más económico" que ya usaba el plugin de referencia. + +### Hallazgo crítico de Medusa, evitado antes de romper nada (no un incidente real) + +Leyendo directo `@medusajs/utils/dist/common/define-config.js` (no +documentación): `defineConfig()` ya registra por defecto +`Modules.FULFILLMENT` con el provider `manual_manual`, aunque +`medusa-config.ts` no lo mencione explícitamente. `transformModules()` +indexa por `serviceName` y el último bloque que se declara **pisa**, no +suma, al anterior. Agregar un bloque de fulfillment propio sin incluir +también `@medusajs/medusa/fulfillment-manual` adentro habría roto el +fulfillment manual existente en silencio — mismo tipo de riesgo que casi +pasó con `payment` en el ADR 0009, evitado leyendo el código fuente real +antes de escribir el bloque nuevo (ambos providers conviven hoy en el +mismo bloque de `medusa-config.ts`). + +**Implementación** (`commerce-backend/src/modules/envioclick-fulfillment/`, +`src/lib/envioclick.ts`): `AbstractFulfillmentProviderService`, +`calculatePrice()` invocado en vivo cada vez que el carrito refresca +shipping options (Medusa advierte que si este método falla, **bloquea el +checkout** — el fallback conservador de arriba existe también por esto, +no solo por falta de datos). `createFulfillment()` genera la guía real vía +`/shipment` (o `/shipment_sandbox` en modo prueba — +**el único endpoint con sandbox real**, la cotización siempre usa +tarifas reales). Sin cambios en el storefront: `GET /store/shipping-options` +nativo de Medusa ya invocaba `calculatePrice()` automáticamente para +cualquier shipping option `price_type: "calculated"`. + +**Verificado en vivo (2026-09):** +- Shipping option "Envío nacional estándar" migrada de `manual_manual`/ + tarifa fija a `envioclick_standard`/`calculated` vía Admin API (con + usuario admin temporal, eliminado después de usarlo). +- Un `400` inicial (`"Providers (envioclick_standard) are not enabled + for the service location"`) se resolvió vinculando el provider a la + stock location (`POST /admin/stock-locations/:id/fulfillment-providers`) + — Medusa exige esto además del bloque de módulo. +- Carrito de prueba real con dirección → `shipping_total: 8941` COP, + tarifa real de EnvíoClick, no el fijo de $15.000. +- Datos reales de la bodega de origen cargados (dirección `Cra 100 + #42-65`, contacto `cristiancruz29@outlook.com` / `3014750823`) tanto en + el ConfigMap (`ENVIOCLICK_ORIGIN_*`) como en el registro real de la + stock location en Medusa (antes: "Sin dirección física registrada"). +- El comercio está en modo **sandbox** de EnvíoClick + (`ENVIOCLICK_SANDBOX: "true"`) — las guías que se generan son de + prueba, sin costo; ninguna cotización es de prueba (no existe sandbox + de cotización). + +**Limitaciones conocidas y aceptadas, no bloqueantes:** +- `createReturnFulfillment` tira "no implementado" — devoluciones fuera + de alcance. +- Cancelación de guía real (`POST cancellation/batch/order`, hasta 50 por + lote, usando los `idOrder` que devuelve `/shipment`) confirmada como + existente en la API pero **no implementada en el módulo** — se + descubrió durante la investigación, no se integró por no ser parte del + flujo de checkout mínimo. +- Solo EnvíoClick como provider — no hay selección de múltiples + couriers por el comprador (v1 siempre toma el más barato). +- Ningún producto tiene peso/dimensiones reales — ver arriba. + +## 3. Storefront — banner dinámico y buscador + +Con ambas integraciones ya en producción, se atendieron dos pedidos de +UI del storefront relacionados con la experiencia de compra real: + +### 3.1 Banner dinámico (`components/home/hero-carousel.tsx`) + +Antes: tres slides con copy e imágenes fijas, hardcodeadas en el +componente, sin relación con el catálogo real. Ahora: `HomePage` +(`app/page.tsx`, que ya hace `getProducts()` del backend real) le pasa al +carrusel los productos marcados como `featured` en el admin (o, si no hay +ninguno, los mismos "tesoros" —nuevos o destacados— que ya usa la sección +de abajo, para no duplicar el criterio de selección); el carrusel arma +cada slide con la imagen, nombre, precio (`priceLabel`) y link reales del +producto, calculando el badge de descuento (`% OFF`) cuando +`compareAtPrice` existe. Si la tienda no tuviera ningún producto activo +(caso borde), cae a un slide estático de respaldo para no romper la +página. Autoplay, dots de paginación y animación quedan igual — solo +cambia el origen de los datos. + +### 3.2 Buscador (`components/layout/navbar.tsx`) + +El header en mobile competía el buscador con el logo y los tres iconos +de acción en una sola fila de grid (`grid-cols-[auto_1fr_auto]`), +quedando visualmente recortado — reportado por el usuario junto a una +captura de referencia del header mobile de Best Buy (fila de iconos +arriba, buscador a todo el ancho debajo, con micrófono y escaneo QR +inline). + +**Decisión:** adoptar el mismo patrón de dos filas en mobile/tablet +(logo + iconos arriba, buscador a todo el ancho debajo) pero **sin** los +íconos de micrófono/QR — no hay búsqueda por voz ni por código de barras +implementada, y un ícono sin función real confunde más de lo que ayuda. +En desktop (`lg:` y arriba) se mantuvo la fila única original del grid +`[auto_1fr_auto]`: ahí ya sobraba espacio horizontal y partirla en dos +solo alargaría el header sin necesidad. Sin cambios en la lógica de +`PredictiveSearch` (autocompletado, debounce, navegación) — solo se +reubicó su contenedor según el breakpoint. + +## Opciones consideradas (pago) + +| Opción | A favor | En contra | +|---|---|---| +| PayU LatAm (ADR 0009) | Ya decidido, credenciales bien documentadas | Registro real redirige a Rapyd — la entidad "PayU LatAm" independiente ya no existe para un comercio nuevo | +| Rapyd | Dueño real del negocio ex-PayU LatAm, API moderna y bien documentada | API completamente distinta (HMAC, REST moderno) — no es una migración de credenciales, es una integración nueva desde cero; descartada en fase de plan por costo/encaje frente a ePayco | +| **ePayco** — elegido | 100% Davivienda desde 2024, mejor tarifa PSE con el banco ya disponible del comercio; SDK oficial cubre el protocolo inusual (login + cifrado AES por campo) | Dos productos bajo la misma marca (Apify vs. la API real usada) fáciles de confundir; doc pública desactualizada respecto al SDK real; reembolsos no implementados | + +## Consecuencias + +- Checkout real de punta a punta (pago + envío calculado) queda + desplegado y verificado a nivel API/backend, pero **ambos proveedores + siguen en modo sandbox** — no se ha completado ni un pago real ni un + envío real todavía. Ver checklist de pruebas abajo. +- La precisión de la tarifa de envío depende de cargar peso/dimensiones + reales por producto — deuda de datos explícita, no de código; candidato + natural a extender el importador CSV existente con columnas nuevas. +- Reembolsos (ePayco) y devoluciones/cancelación de guía (EnvíoClick) + quedan fuera de alcance — fallan explícitamente en vez de fingir éxito. +- Revertir el pago implica deshabilitar el provider `epayco_pse` en la + región y volver al placeholder anterior; revertir el envío implica + volver la shipping option a `manual_manual`/tarifa fija — ninguna de + las dos requiere cambios de esquema de datos para revertirse. +- El banner y el buscador no introducen dependencias nuevas ni cambian + ninguna ruta de API — son cambios de storefront puros, reversibles + cambiando los dos componentes de vuelta a su versión anterior. + +## Checklist de pruebas reales desde el storefront (pendiente, no ejecutado en este ADR) + +Todo lo de arriba se verificó a nivel API/backend con llamadas directas +o carritos de prueba armados por API — **no** todavía manejando el +storefront real como lo haría un comprador. Pendiente, con la próxima +sesión que tenga acceso a un navegador real: + +1. **Pago PSE completo:** agregar un producto real al carrito → checkout + → elegir PSE → seleccionar un banco del listado real (sandbox) → + confirmar que la redirección al banco ocurre (esto es justo lo que + nunca se probó — el nombre del campo de URL de `bank.create()` está + manejado a la defensiva y podría no matchear ningún candidato) → + volver a `/checkout/pse/return` → confirmar que la orden queda en + estado correcto. +2. **Envío calculado real:** en el paso de dirección del checkout, + confirmar que el precio de envío mostrado ya no es el fijo de + $15.000 sino el que devuelve EnvíoClick en vivo para esa dirección + (visible también sin completar el pago, solo llegando al paso de + shipping). +3. **Resiliencia del fallback de envío:** si es posible, forzar una + dirección o producto que haga fallar la cotización real (por ejemplo, + un DANE code inválido) y confirmar que el checkout sigue avanzando + con el precio de respaldo en vez de romperse — el comportamiento + documentado en `calculatePrice()`, nunca ejercitado con un fallo real. +4. **Banner dinámico:** confirmar en `/` que el carrusel muestra + productos reales del catálogo (no las tres imágenes viejas + hardcodeadas) y que cada slide lleva al producto correcto al hacer + clic. +5. **Buscador:** en un viewport mobile real (no solo devtools), confirmar + que el buscador ya no se ve recortado y que el autocompletado + despliega bien debajo de la fila nueva a todo el ancho. +6. **Webhook de ePayco con un pago real de sandbox:** confirmar que, + tras completar un pago PSE de prueba desde el banco sandbox, el + webhook mueve la orden a completada sin intervención manual (hoy solo + se confirmó que el endpoint responde `200` a un `POST` sintético, no + con un webhook real disparado por ePayco). diff --git a/workloads/ecommerce/app/page.tsx b/workloads/ecommerce/app/page.tsx index 1fb74e0..ff98cbf 100644 --- a/workloads/ecommerce/app/page.tsx +++ b/workloads/ecommerce/app/page.tsx @@ -18,7 +18,13 @@ export const dynamic = "force-dynamic"; export default async function HomePage() { const [products, categories] = await Promise.all([getProducts(), getCategories()]); const treasures = products.filter((product) => product.isNew || product.featured).slice(0, 8); - return <>
Novedades

Tesoros recién llegados

Desliza y encuentra nuevos favoritos.

Club ARI

Promociones, lanzamientos y mucha diversión.

Únete a nuestra comunidad y recibe primero las novedades del catálogo.

{/* No hay herramienta de generacion de imagenes disponible para crear un + // ADR 0010: el banner toma primero productos destacados (curaduria + // explicita del admin via el flag "featured"); si no hay ninguno, + // cae a los mismos "tesoros" (nuevos/destacados) de la seccion de + // abajo para no repetir la logica de seleccion. + const featuredProducts = products.filter((product) => product.featured); + const bannerProducts = featuredProducts.length > 0 ? featuredProducts : treasures; + return <>
Novedades

Tesoros recién llegados

Desliza y encuentra nuevos favoritos.

Club ARI

Promociones, lanzamientos y mucha diversión.

Únete a nuestra comunidad y recibe primero las novedades del catálogo.

{/* No hay herramienta de generacion de imagenes disponible para crear un asset nuevo -- se reconstruye el mismo efecto (fondo punteado + fotos estilo polaroid) con CSS y fotos reales de producto ya existentes, en azul/amarillo de marca en vez del violeta original diff --git a/workloads/ecommerce/components/home/hero-carousel.tsx b/workloads/ecommerce/components/home/hero-carousel.tsx index 1c5879d..bc45f53 100644 --- a/workloads/ecommerce/components/home/hero-carousel.tsx +++ b/workloads/ecommerce/components/home/hero-carousel.tsx @@ -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; diff --git a/workloads/ecommerce/components/layout/navbar.tsx b/workloads/ecommerce/components/layout/navbar.tsx index 57f122b..98079fb 100644 --- a/workloads/ecommerce/components/layout/navbar.tsx +++ b/workloads/ecommerce/components/layout/navbar.tsx @@ -55,34 +55,47 @@ export function Navbar({ products, categories }: { products: Product[]; categori
-
- {/* Isotipo circular con icono + wordmark integrados (como en la - version previa a ADR 0008), pedido explicito del usuario en - vez del lockup rectangular. logo-ari.svg (rectangular) se - mantiene para el footer. */} - - {/* eslint-disable-next-line @next/next/no-img-element -- vector propio, next/image no optimiza SVG sin habilitar dangerouslyAllowSVG */} - ARI shopping - - -
- {/* Antes escondido en mobile (solo alcanzable desde el menu - hamburguesa) -- pedido explicito del usuario: visible - siempre en el header, con un punto verde cuando hay sesion - iniciada (mismo patron que el badge de cantidad del - carrito/favoritos, pero de presencia, no de conteo). */} - - - + {/* ADR 0010: en mobile/tablet el buscador se movio a una segunda + fila a todo el ancho, debajo de logo+iconos (antes competia por + espacio en una sola fila con el grid_[auto_1fr_auto] y quedaba + angosto/recortado). En desktop (lg+) se mantiene la fila unica + original -- ahi sobra espacio y partirla en dos solo alargaria + el header sin necesidad. */} +
+
+ {/* Isotipo circular con icono + wordmark integrados (como en la + version previa a ADR 0008), pedido explicito del usuario en + vez del lockup rectangular. logo-ari.svg (rectangular) se + mantiene para el footer. */} + + {/* eslint-disable-next-line @next/next/no-img-element -- vector propio, next/image no optimiza SVG sin habilitar dangerouslyAllowSVG */} + ARI shopping + +
+ +
+
+ {/* Antes escondido en mobile (solo alcanzable desde el menu + hamburguesa) -- pedido explicito del usuario: visible + siempre en el header, con un punto verde cuando hay sesion + iniciada (mismo patron que el badge de cantidad del + carrito/favoritos, pero de presencia, no de conteo). */} + + + +
+
+
+