Files
apps-registry/workloads/ecommerce/app/product/[slug]/page.tsx
T
devopsandClaude Sonnet 5 401f43a136
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 28s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 7m38s
feat(ecommerce): página de producto estilo Amazon (buy-box + más ancho)
- max-w de la página de producto pasa de 1280px a 1600px -- aprovecha
  pantallas grandes en vez de dejar margenes vacíos enormes.
- Nueva 3ra columna desde xl: buy-box propio (tarjeta con borde,
  sticky) para precio/variantes/carrito, separado del texto
  descriptivo. Por debajo de xl cae al flujo normal de la 2da columna
  sin duplicar el componente (mismo <ProductPurchase>, un solo
  id="product-buy-button" en el DOM).
- Dentro del buy-box: un producto sugerido real de la misma
  categoría/colección (mismo cálculo que ya alimenta el carrusel de
  abajo, no un dato inventado).
- MobileAddToCart: la barra fija ahora también requiere haber
  scrolleado más de 80px además de que el botón real no esté en
  pantalla -- antes aparecía inmediatamente sobre la vista inicial del
  producto (imagen + variantes) apenas cargaba la página.

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

92 lines
5.1 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 suggested = recommended[0];
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). */}
<section className="mx-auto grid max-w-[1600px] gap-10 px-4 py-10 lg:grid-cols-[1.08fr_.92fr] lg:py-16 xl:grid-cols-[1.1fr_.75fr_360px]">
<ProductGallery images={product.images} name={product.name} />
<div>
<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>
<Accordion type="single" collapsible className="mt-8">
<AccordionItem value="details">
<AccordionTrigger>Detalles del producto</AccordionTrigger>
<AccordionContent>Marca: {product.brand}. Edad recomendada: {product.ageRange}. La disponibilidad y presentación final se confirman con Ari Shopping.</AccordionContent>
</AccordionItem>
<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. El sugerido usa 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="xl:sticky xl:top-24 xl:self-start">
<div className="rounded-2xl border border-slate-200 p-6 shadow-sm">
<ProductPurchase product={product} />
</div>
{suggested && (
<div className="mt-6">
<h2 className="mb-4 text-xs font-black uppercase tracking-[.14em] text-slate-500">También te puede interesar</h2>
<ProductCard product={suggested} />
</div>
)}
</div>
</section>
{recommended.length > 0 && (
<section className="mx-auto max-w-[1600px] px-4 pb-24">
<h2 className="mb-7 text-3xl font-black">También te puede gustar</h2>
<ProductCarousel products={recommended} />
</section>
)}
<MobileAddToCart product={product} />
</>
);
}