Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da7f316eb1 | ||
|
|
123cbbd87a |
No files matched your search
@@ -0,0 +1,112 @@
|
||||
"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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Heart, ShoppingBag, Sparkles, TriangleAlert } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { ProductCardGallery } from "@/components/product/product-card-gallery";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { Product } from "@/lib/headless/types";
|
||||
@@ -22,6 +22,9 @@ export function ProductCard({ product, priority = false }: { product: Product; p
|
||||
? Math.round((1 - product.price / product.compareAtPrice) * 100)
|
||||
: null;
|
||||
|
||||
const galleryImages = Array.from(new Set(product.images.filter(Boolean)));
|
||||
if (galleryImages.length === 0) galleryImages.push(product.thumbnail);
|
||||
|
||||
const [selectedVariantId, setSelectedVariantId] = useState(product.variants[0]?.id);
|
||||
const selectedVariant = product.variants.find((variant) => variant.id === selectedVariantId) ?? product.variants[0];
|
||||
const outOfStock = selectedVariant?.available === false;
|
||||
@@ -37,7 +40,7 @@ export function ProductCard({ product, priority = false }: { product: Product; p
|
||||
return (
|
||||
<article className="group flex h-full flex-col overflow-hidden rounded-lg border border-slate-100 bg-white shadow-lg shadow-ari-marino/5 transition duration-300 hover:-translate-y-1.5 hover:shadow-2xl hover:shadow-ari-coral/10">
|
||||
<div className="relative aspect-square overflow-hidden bg-white">
|
||||
<Link href={`/product/${product.slug}`} prefetch={false}><Image src={product.thumbnail} alt={product.name} fill priority={priority} sizes="(max-width: 640px) 82vw, (max-width: 1024px) 44vw, 280px" className="object-cover transition duration-500 group-hover:scale-[1.035]" /></Link>
|
||||
<ProductCardGallery images={galleryImages} name={product.name} href={`/product/${product.slug}`} priority={priority} />
|
||||
<div className="absolute left-3 top-3 flex gap-2">{outOfStock && <Badge className="bg-rose-600 text-white">Agotado</Badge>}{discountPercent && discountPercent > 0 && <Badge className="bg-ari-coral-claro text-ari-marino">-{discountPercent}%</Badge>}{product.isNew && <Badge className="bg-ari-marino text-white">Nuevo</Badge>}{product.featured && <Badge className="bg-ari-coral-claro text-ari-marino"><Sparkles className="mr-1 size-3" />Top</Badge>}</div>
|
||||
{outOfStock && <div className="pointer-events-none absolute inset-0 bg-white/40" />}
|
||||
<button type="button" aria-label={isFavorite ? "Quitar de favoritos" : "Agregar a favoritos"} aria-pressed={isFavorite} className={`absolute right-3 top-3 rounded-full p-2.5 shadow-lg backdrop-blur transition ${isFavorite ? "bg-ari-coral text-white" : "bg-white/95 text-slate-900 hover:text-ari-coral"}`} onClick={() => toggleProduct(product)}><Heart className={`size-4 ${isFavorite ? "fill-current" : ""}`} /></button>
|
||||
|
||||
Reference in new issue
Block a user