feat(ecommerce): migrate Ari Shopping to Next.js 15 headless storefront v4
Build and Push Frontend / build (push) Failing after 9m42s

This commit is contained in:
2026-07-19 21:36:35 -05:00
parent 2e7abc7e72
commit fe7b2019c2
79 changed files with 7686 additions and 187 deletions
@@ -1,9 +1,48 @@
"use client";
import { motion } from "motion/react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import { ProductCard } from "@/components/product/product-card";
import type { Product } from "@/lib/headless/types";
export function ProductCarousel({ products }: { products: Product[] }) {
return <motion.div initial="hidden" whileInView="visible" viewport={{ once: true, margin: "-80px" }} variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.07 } } }} className="grid auto-cols-[82%] grid-flow-col gap-5 overflow-x-auto pb-5 pr-5 [scrollbar-width:none] sm:auto-cols-[44%] lg:auto-cols-[28%] xl:auto-cols-[23%]">{products.map((product, index) => <motion.div key={product.id} variants={{ hidden: { opacity: 0, y: 24 }, visible: { opacity: 1, y: 0 } }}><ProductCard product={product} priority={index < 2} /></motion.div>)}</motion.div>;
const scroller = useRef<HTMLDivElement>(null);
const [canPrevious, setCanPrevious] = useState(false);
const [canNext, setCanNext] = useState(products.length > 1);
const updateControls = useCallback(() => {
const node = scroller.current;
if (!node) return;
setCanPrevious(node.scrollLeft > 8);
setCanNext(node.scrollLeft + node.clientWidth < node.scrollWidth - 8);
}, []);
useEffect(() => {
const node = scroller.current;
if (!node) return;
updateControls();
node.addEventListener("scroll", updateControls, { passive: true });
const observer = new ResizeObserver(updateControls);
observer.observe(node);
return () => {
node.removeEventListener("scroll", updateControls);
observer.disconnect();
};
}, [updateControls]);
const move = (direction: -1 | 1) => {
const node = scroller.current;
if (!node) return;
node.scrollBy({ left: direction * Math.max(280, node.clientWidth * 0.82), behavior: "smooth" });
};
return (
<div className="relative">
<button type="button" aria-label="Productos anteriores" disabled={!canPrevious} onClick={() => move(-1)} className="absolute -left-3 top-[38%] z-10 grid size-12 place-items-center rounded-full border border-slate-200 bg-white text-slate-950 shadow-xl transition hover:bg-violet-50 disabled:pointer-events-none disabled:opacity-0 sm:-left-6"><ChevronLeft className="size-6" /></button>
<div ref={scroller} role="region" aria-label="Carrusel de productos" tabIndex={0} onKeyDown={(event) => { if (event.key === "ArrowLeft") move(-1); if (event.key === "ArrowRight") move(1); }} className="flex snap-x snap-mandatory gap-5 overflow-x-auto scroll-smooth pb-5 pr-5 outline-none [overscroll-behavior-inline:contain] [scrollbar-width:none]">
{products.map((product, index) => <div key={product.id} className="w-[82%] shrink-0 snap-start sm:w-[44%] lg:w-[28%] xl:w-[23%]"><ProductCard product={product} priority={index < 2} /></div>)}
</div>
<button type="button" aria-label="Productos siguientes" disabled={!canNext} onClick={() => move(1)} className="absolute -right-3 top-[38%] z-10 grid size-12 place-items-center rounded-full border border-slate-200 bg-white text-slate-950 shadow-xl transition hover:bg-violet-50 disabled:pointer-events-none disabled:opacity-0 sm:-right-6"><ChevronRight className="size-6" /></button>
</div>
);
}