fix(ecommerce): saca la barra flotante de "Añadir al carrito" en mobile
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 23s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 15m20s

Pedido del usuario con capturas reales: la barra fija seguía
superponiéndose sobre contenido de abajo al scrollear (ej. sobre la
tarjeta de "También te puede interesar"), incluso después de varias
vueltas ajustando cuándo debía aparecer/ocultarse. En vez de seguir
parchando esa lógica, se saca la barra flotante por completo -- queda
un solo botón "Añadir al carrito", visible en su lugar normal dentro
de la página también en mobile (sin duplicar), igual que la mayoría de
tiendas reales.

Se borra components/product/mobile-add-to-cart.tsx (ya sin uso) y el
id="product-buy-button"/id="recommendations-section" que solo existían
para el IntersectionObserver de ese componente.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012Y4df3Z7odr9Cg8F2nw6uV
This commit is contained in:
devopsandClaude Sonnet 5 committed 2026-09-14 03:30:55 +00:00
1 parent 972b11772a
commit 5c79cc3345
3 files changed
+6 -105

No files matched your search

@@ -1,7 +1,6 @@
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";
@@ -49,8 +48,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
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 DOM.
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
@@ -107,13 +105,12 @@ export default async function ProductPage({ params }: ProductPageProps) {
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 id="recommendations-section" className="min-w-0 pb-16 lg:col-span-2">
<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} />
</>
);
}
@@ -1,93 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import { ShoppingBag } from "lucide-react";
import { Button } from "@/components/ui/button";
import type { Product } from "@/lib/headless/types";
import { useCartStore } from "@/store/cart-store";
const SCROLL_THRESHOLD_PX = 80;
// Antes esta barra quedaba fija desde el primer render en mobile,
// tapando permanentemente parte de la imagen del producto solo para
// duplicar un boton que ya está visible más abajo. Ahora combina tres
// condiciones, igual que el "Buy Now" sticky de Amazon:
// - scrolled: recien aparece despues de que el usuario empieza a
// scrollear (nunca sobre la vista inicial de la imagen/hero).
// - !buttonVisible: el boton real (#product-buy-button en
// ProductPurchase, visible tambien en mobile) no esta en pantalla --
// ya sea porque todavia no se llego a el, o porque se scrolleo mas
// alla.
// - !pastRecommendations: una vez que "También te puede gustar"
// (#recommendations-section en app/product/[slug]/page.tsx) empieza a
// entrar en pantalla, la barra se oculta y sigue oculta el resto del
// scroll (footer incluido) -- pedido explicito del usuario. OJO:
// condicionar esto a que el borde superior de la seccion cruce el
// TOPE del viewport (top <= 0) rompe en paginas cortas -- si no hay
// suficiente contenido/footer debajo, nunca se puede scrollear tan
// lejos y el bug original (barra pegada hasta el fondo) segunia
// reproduciendose. En cambio, se compara contra la ALTURA del
// viewport (top < innerHeight): se oculta apenas la seccion empieza a
// asomar desde abajo, se mantiene oculta al seguir scrolleando mas
// alla (top se vuelve cada vez mas negativo, sigue siendo <
// innerHeight), y solo reaparece si se vuelve a scrollear hacia
// arriba lo suficiente como para que la seccion salga de pantalla per
// completo otra vez.
export function MobileAddToCart({ product }: { product: Product }) {
const addItem = useCartStore((state) => state.addItem);
const [buttonVisible, setButtonVisible] = useState(true);
const [pastRecommendations, setPastRecommendations] = useState(false);
const [scrolled, setScrolled] = useState(false);
const outOfStock = product.variants[0]?.available === false;
useEffect(() => {
const target = document.getElementById("product-buy-button");
if (!target) return;
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
if (entry) setButtonVisible(entry.isIntersecting);
}, { threshold: 0 });
observer.observe(target);
return () => observer.disconnect();
}, []);
useEffect(() => {
const target = document.getElementById("recommendations-section");
if (!target) return;
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
if (entry) setPastRecommendations(entry.boundingClientRect.top < window.innerHeight);
}, { threshold: 0 });
observer.observe(target);
return () => observer.disconnect();
}, []);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > SCROLL_THRESHOLD_PX);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
const visible = scrolled && !buttonVisible && !pastRecommendations;
return (
<div
aria-hidden={!visible}
className={`fixed inset-x-0 bottom-0 z-40 border-t border-slate-200 bg-white/95 p-3 shadow-2xl backdrop-blur transition-transform duration-200 lg:hidden ${
visible ? "translate-y-0" : "pointer-events-none translate-y-full"
}`}
>
<Button
variant="primary"
size="lg"
className="w-full"
disabled={outOfStock}
onClick={() => addItem(product, product.variants[0]?.id)}
>
<ShoppingBag />
{outOfStock ? "Agotado" : "Añadir al carrito"}
</Button>
</div>
);
}
@@ -41,14 +41,11 @@ export function ProductPurchase({ product }: { product: Product }) {
<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>
{/* Visible tambien en mobile (antes quedaba oculto <lg porque
MobileAddToCart quedaba fijo abajo desde el primer render,
tapando parte de la imagen sin necesidad). id usado por
MobileAddToCart para saber cuando este boton real sale del
viewport y recien ahi mostrar su barra sticky -- mismo patron
que el "Buy Now" sticky de Amazon en mobile. */}
{/* Un solo botón, visible también en mobile -- sin barra flotante
aparte (se probó, pero terminaba tapando contenido de abajo al
scrollear, ej. "También te puede interesar"; el usuario pidió
sacarla y dejar el botón normal como en la mayoría de tiendas). */}
<Button
id="product-buy-button"
variant="primary"
size="lg"
className="w-full"