Files
apps-registry/workloads/ecommerce/app/product/[slug]/page.tsx
T
devopsandClaude Sonnet 5 1c9693e573
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 30s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 19m41s
fix(ecommerce): overflow horizontal en mobile hacía ver todo "a tamaño real"
Root cause real del bug reportado por el usuario (imagen de producto
viéndose gigante/cortada en Chrome y Edge Android) -- no era la imagen:
al mover "También te puede gustar" DENTRO del grid de la página (para
el layout de 2 filas del commit anterior), su carrusel horizontal
(overflow-x-auto interno, sin flex-wrap) empezó a contar como item de
ese grid. Sin min-w-0, el ancho mínimo intrínseco de TODAS las cards
del carrusel sin wrappear se usa para dimensionar la columna
compartida del grid -- en mobile (grid de una sola columna), eso
empujaba TODO (imagen, título, specs) a ~800px de ancho en un viewport
real de ~380px. El navegador termina "zoomeando" el layout para que
entre, dando la sensación de que todo se ve a tamaño real / cortado.

Confirmado en vivo con bisección real: sin el fix, el grid medía 783px
en un viewport de 380px; agregando min-w-0 al contenedor del carrusel
vuelve a 380px. Se agrega min-w-0 también a los otros 3 contenedores
del grid (galería, info, buy-box) y a la tira de miniaturas dentro de
ProductGallery (mismo patrón de overflow-x-auto) como defensa
preventiva -- práctica estándar para items de grid/flex con scroll
horizontal interno.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
2026-09-14 00:29:29 +00:00

114 lines
7.0 KiB
TypeScript

import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { MobileAddToCart } from "@/components/product/mobile-add-to-cart";
import { ProductCard } from "@/components/product/product-card";
import { ProductCarousel } from "@/components/product/product-carousel";
import { ProductGallery } from "@/components/product/product-gallery";
import { ProductPurchase } from "@/components/product/product-purchase";
import { getProductBySlug, getProducts } from "@/lib/headless/client";
interface ProductPageProps { params: Promise<{ slug: string }> }
// generateStaticParams + revalidate quedaban aquí para pre-renderizar en
// build time, pero HEADLESS_PROVIDER/MEDUSA_* no existen en el contexto del
// Docker build: el snapshot estático quedaba congelado con el catálogo mock
// (todos los productos con price: null) hasta que ISR lo revalidara. Con
// frontend-deploy corriendo 2 réplicas y sin cacheHandler compartido, cada
// pod revalida su propia caché de forma independiente, así que un producto
// podía mostrar "Consultar precio" en un pod y el precio real en el otro
// según cuál hubiera sido "calentado". force-dynamic elimina esa caché por
// pod y deja esta ruta en vivo contra Medusa, igual que el listado
// (dinámico por searchParams).
export const dynamic = "force-dynamic";
export async function generateMetadata({ params }: ProductPageProps): Promise<Metadata> {
const { slug } = await params;
const product = await getProductBySlug(slug);
return product ? { title: product.name, description: product.shortDescription, openGraph: { images: [product.image] } } : { title: "Producto" };
}
export default async function ProductPage({ params }: ProductPageProps) {
const { slug } = await params;
const product = await getProductBySlug(slug);
if (!product) notFound();
const products = await getProducts();
const recommended = products.filter((candidate) => candidate.id !== product.id && (candidate.category === product.category || candidate.collection === product.collection)).slice(0, 8);
const suggestions = recommended.slice(0, 2);
return (
<>
{/* max-w-[1600px] (antes max-w-7xl/1280px) y una 3ra columna desde
xl -- aprovecha pantallas grandes en vez de dejar el buy box
apretado contra el texto o cortado por el panel de zoom de al
lado (ver ProductGallery). Por debajo de xl, el buy box cae al
flujo normal en la 2da columna, sin duplicar el componente en
el DOM (evita el id="product-buy-button" repetido que rompería
el IntersectionObserver de MobileAddToCart).
El buy box ocupa las 2 filas (xl:row-span-2) para no quedar
atado a la altura de galería+descripción -- "También te puede
gustar" pasa a ser la 2da fila debajo de esas dos columnas
(xl:col-span-2), en paralelo al buy box sticky, en vez de
esperar a que termine la columna más alta antes de aparecer. */}
<section className="mx-auto grid max-w-[1600px] gap-x-10 gap-y-12 px-4 py-10 lg:grid-cols-[1.08fr_.92fr] lg:py-16 xl:grid-cols-[1.1fr_.75fr_360px] xl:grid-rows-[auto_auto]">
<ProductGallery images={product.images} name={product.name} />
<div className="min-w-0">
<span className="text-xs font-black uppercase tracking-[.18em] text-ari-coral">{product.category} · {product.collection}</span>
<h1 className="mt-3 text-4xl font-black tracking-tight sm:text-6xl">{product.name}</h1>
<p className="mt-5 text-lg leading-8 text-slate-600">{product.description}</p>
{/* Antes escondido detrás de un click en el acordeón --
siempre visible, estilo Amazon, con los mismos datos
reales que ya existían (marca/edad/categoría/colección),
sin inventar specs que el catálogo no tiene. */}
<h2 className="mb-4 mt-8 text-sm font-black uppercase tracking-wide text-slate-500">Detalles del producto</h2>
<dl className="grid grid-cols-2 gap-x-6 gap-y-4 rounded-2xl border border-slate-200 p-5 text-sm sm:grid-cols-4">
<div><dt className="text-slate-400">Marca</dt><dd className="mt-0.5 font-bold text-slate-900">{product.brand}</dd></div>
<div><dt className="text-slate-400">Edad recomendada</dt><dd className="mt-0.5 font-bold text-slate-900">{product.ageRange}</dd></div>
<div><dt className="text-slate-400">Categoría</dt><dd className="mt-0.5 font-bold text-slate-900">{product.category}</dd></div>
<div><dt className="text-slate-400">Colección</dt><dd className="mt-0.5 font-bold text-slate-900">{product.collection}</dd></div>
</dl>
<Accordion type="single" collapsible className="mt-6">
<AccordionItem value="shipping">
<AccordionTrigger>Envíos y entregas</AccordionTrigger>
<AccordionContent>Coordinamos envíos nacionales y entrega según ubicación. Los tiempos se confirman durante la cotización.</AccordionContent>
</AccordionItem>
</Accordion>
</div>
{/* Buy box estilo Amazon: precio/variantes/carrito en su propia
tarjeta, sticky desde xl mientras el resto de la pagina
scrollea. Los sugeridos (hasta 2) usan el mismo cálculo de
"recommended" que ya alimenta el carrusel de abajo -- mismo
criterio real (categoría/colección), no un dato inventado. */}
<div className="min-w-0 xl:sticky xl:top-24 xl:row-span-2 xl:self-start">
<div className="rounded-2xl border border-slate-200 p-6 shadow-sm">
<ProductPurchase product={product} />
</div>
{suggestions.length > 0 && (
<div className="mt-6">
<h2 className="mb-4 text-xs font-black uppercase tracking-[.14em] text-slate-500">También te puede interesar</h2>
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-1">
{suggestions.map((item) => <ProductCard key={item.id} product={item} />)}
</div>
</div>
)}
</div>
{/* min-w-0 es necesario: sin esto, el carrusel horizontal
(overflow-x-auto interno) contaba como item de este grid, y su
ancho minimo intrinseco (la suma de TODAS las cards sin
wrappear, ya que el scroller no tiene flex-wrap) empujaba todo
el grid completo a ~800px en mobile -- afectaba a TODA la
columna compartida (imagen, titulo, specs), no solo al
carrusel. Confirmado en vivo: sin min-w-0 el grid medía 783px
en un viewport real de 380px; con min-w-0 vuelve a 380px. */}
{recommended.length > 0 && (
<div className="min-w-0 pb-16 lg:col-span-2">
<h2 className="mb-7 text-3xl font-black">También te puede gustar</h2>
<ProductCarousel products={recommended} />
</div>
)}
</section>
<MobileAddToCart product={product} />
</>
);
}