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";
|
"use client";
|
||||||
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Heart, ShoppingBag, Sparkles, TriangleAlert } from "lucide-react";
|
import { Heart, ShoppingBag, Sparkles, TriangleAlert } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { ProductCardGallery } from "@/components/product/product-card-gallery";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import type { Product } from "@/lib/headless/types";
|
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)
|
? Math.round((1 - product.price / product.compareAtPrice) * 100)
|
||||||
: null;
|
: 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 [selectedVariantId, setSelectedVariantId] = useState(product.variants[0]?.id);
|
||||||
const selectedVariant = product.variants.find((variant) => variant.id === selectedVariantId) ?? product.variants[0];
|
const selectedVariant = product.variants.find((variant) => variant.id === selectedVariantId) ?? product.variants[0];
|
||||||
const outOfStock = selectedVariant?.available === false;
|
const outOfStock = selectedVariant?.available === false;
|
||||||
@@ -37,7 +40,7 @@ export function ProductCard({ product, priority = false }: { product: Product; p
|
|||||||
return (
|
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">
|
<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">
|
<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>
|
<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" />}
|
{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>
|
<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