Permite ver todas las imágenes de un producto desde el catálogo y los carruseles sin entrar a la ficha: swipe con scroll-snap nativo en móvil, flechas al pasar el mouse en escritorio (pointer-fine) y puntos de posición. Cada imagen sigue enlazando a la ficha; productos con una sola imagen no muestran controles. overscroll-behavior-x: contain evita que el swipe de imágenes mueva el carrusel de productos del inicio. Validado antes del merge con build de producción local (datos mock) y Playwright con eventos táctiles reales a 390/320 px y 1280 px. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018r19s4H8MBGegqToqw5guP
113 lines
4.1 KiB
TypeScript
113 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import Link from "next/link";
|
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
import { useRef, useState } from "react";
|
|
|
|
// Galería deslizable de la tarjeta de producto: permite ver todas las
|
|
// imágenes desde el listado sin entrar al producto. En móvil se desliza
|
|
// con el dedo (scroll-snap nativo); en escritorio aparecen flechas al
|
|
// pasar el mouse. Cada imagen sigue siendo un link a la ficha.
|
|
export function ProductCardGallery({
|
|
images,
|
|
name,
|
|
href,
|
|
priority = false,
|
|
}: {
|
|
images: string[];
|
|
name: string;
|
|
href: string;
|
|
priority?: boolean;
|
|
}) {
|
|
const scroller = useRef<HTMLDivElement>(null);
|
|
const [index, setIndex] = useState(0);
|
|
const total = images.length;
|
|
|
|
const goTo = (target: number) => {
|
|
const node = scroller.current;
|
|
if (!node) return;
|
|
const next = Math.max(0, Math.min(total - 1, target));
|
|
node.scrollTo({ left: next * node.clientWidth, behavior: "smooth" });
|
|
};
|
|
|
|
const onScroll = () => {
|
|
const node = scroller.current;
|
|
if (!node || node.clientWidth === 0) return;
|
|
setIndex(Math.round(node.scrollLeft / node.clientWidth));
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
ref={scroller}
|
|
onScroll={total > 1 ? onScroll : undefined}
|
|
className="flex h-full snap-x snap-mandatory overflow-x-auto [overscroll-behavior-x:contain] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
|
>
|
|
{images.map((src, slide) => (
|
|
<Link
|
|
key={src}
|
|
href={href}
|
|
prefetch={false}
|
|
className="relative block h-full w-full shrink-0 snap-center snap-always"
|
|
aria-label={total > 1 ? `${name}, imagen ${slide + 1} de ${total}` : name}
|
|
tabIndex={slide === 0 ? 0 : -1}
|
|
>
|
|
<Image
|
|
src={src}
|
|
alt={slide === 0 ? name : `${name} (imagen ${slide + 1})`}
|
|
fill
|
|
priority={priority && slide === 0}
|
|
sizes="(max-width: 640px) 82vw, (max-width: 1024px) 44vw, 280px"
|
|
className="object-cover transition duration-500 group-hover:scale-[1.035]"
|
|
/>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
|
|
{total > 1 && (
|
|
<>
|
|
<button
|
|
type="button"
|
|
aria-label="Imagen anterior"
|
|
disabled={index === 0}
|
|
onClick={() => goTo(index - 1)}
|
|
className="absolute left-2 top-1/2 hidden size-9 -translate-y-1/2 place-items-center rounded-full bg-white/95 text-ari-marino opacity-0 shadow-lg transition group-hover:opacity-100 focus-visible:opacity-100 disabled:pointer-events-none disabled:!opacity-0 pointer-fine:grid"
|
|
>
|
|
<ChevronLeft className="size-5" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
aria-label="Imagen siguiente"
|
|
disabled={index === total - 1}
|
|
onClick={() => goTo(index + 1)}
|
|
className="absolute right-2 top-1/2 hidden size-9 -translate-y-1/2 place-items-center rounded-full bg-white/95 text-ari-marino opacity-0 shadow-lg transition group-hover:opacity-100 focus-visible:opacity-100 disabled:pointer-events-none disabled:!opacity-0 pointer-fine:grid"
|
|
>
|
|
<ChevronRight className="size-5" />
|
|
</button>
|
|
<div className="absolute inset-x-0 bottom-2 flex justify-center">
|
|
<div className="flex items-center rounded-full bg-white/85 px-1 shadow-sm backdrop-blur">
|
|
{images.map((src, dot) => (
|
|
<button
|
|
key={src}
|
|
type="button"
|
|
aria-label={`Ver imagen ${dot + 1} de ${total}`}
|
|
aria-current={dot === index}
|
|
onClick={() => goTo(dot)}
|
|
className="grid h-5 place-items-center px-[3px]"
|
|
>
|
|
<span
|
|
className={`block h-1.5 rounded-full transition-all ${
|
|
dot === index ? "w-4 bg-ari-coral" : "w-1.5 bg-ari-marino/35"
|
|
}`}
|
|
/>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|