73 lines
5.4 KiB
TypeScript
73 lines
5.4 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import { Maximize2, Minus, Plus, RotateCcw, ZoomIn } from "lucide-react";
|
|
import { PointerEvent, WheelEvent, useRef, useState } from "react";
|
|
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
|
|
|
interface Offset { x: number; y: number }
|
|
interface DragState { pointerId: number; startX: number; startY: number; originX: number; originY: number }
|
|
|
|
export function ProductGallery({ images, name }: { images: string[]; name: string }) {
|
|
const [selected, setSelected] = useState(images[0] ?? "");
|
|
const [open, setOpen] = useState(false);
|
|
const [scale, setScale] = useState(1);
|
|
const [offset, setOffset] = useState<Offset>({ x: 0, y: 0 });
|
|
const drag = useRef<DragState | null>(null);
|
|
|
|
const applyScale = (next: number) => {
|
|
const value = Math.min(3, Math.max(1, Number(next.toFixed(2))));
|
|
setScale(value);
|
|
if (value === 1) setOffset({ x: 0, y: 0 });
|
|
};
|
|
|
|
const reset = () => { setScale(1); setOffset({ x: 0, y: 0 }); };
|
|
const changeOpen = (value: boolean) => { setOpen(value); if (!value) reset(); };
|
|
const wheel = (event: WheelEvent<HTMLDivElement>) => { event.preventDefault(); applyScale(scale + (event.deltaY < 0 ? .2 : -.2)); };
|
|
const pointerDown = (event: PointerEvent<HTMLDivElement>) => {
|
|
if (scale <= 1) return;
|
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
drag.current = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, originX: offset.x, originY: offset.y };
|
|
};
|
|
const pointerMove = (event: PointerEvent<HTMLDivElement>) => {
|
|
if (!drag.current || drag.current.pointerId !== event.pointerId) return;
|
|
setOffset({ x: drag.current.originX + event.clientX - drag.current.startX, y: drag.current.originY + event.clientY - drag.current.startY });
|
|
};
|
|
const pointerEnd = (event: PointerEvent<HTMLDivElement>) => {
|
|
if (drag.current?.pointerId === event.pointerId) drag.current = null;
|
|
};
|
|
|
|
return (
|
|
<div className="grid gap-4 lg:grid-cols-[86px_1fr]">
|
|
<div className="order-2 flex gap-3 overflow-x-auto lg:order-1 lg:flex-col">
|
|
{images.map((image) => <button type="button" key={image} onClick={() => { setSelected(image); reset(); }} className={`relative aspect-square w-20 shrink-0 overflow-hidden rounded-2xl border-2 ${selected === image ? "border-fuchsia-500" : "border-transparent"}`}><Image src={image} alt="" fill sizes="80px" className="object-cover" /></button>)}
|
|
</div>
|
|
<Dialog open={open} onOpenChange={changeOpen}>
|
|
<DialogTrigger asChild>
|
|
<button type="button" className="group relative order-1 aspect-square overflow-hidden rounded-[2rem] bg-slate-50 lg:order-2">
|
|
<Image src={selected} alt={name} fill priority sizes="(max-width: 1024px) 100vw, 50vw" className="object-cover transition duration-500 group-hover:scale-[1.03]" />
|
|
<span className="absolute bottom-5 right-5 inline-flex items-center gap-2 rounded-full bg-white px-4 py-2 text-xs font-black shadow-xl"><ZoomIn className="size-4" />Zoom interactivo</span>
|
|
</button>
|
|
</DialogTrigger>
|
|
<DialogContent className="max-w-6xl overflow-hidden bg-slate-950 p-0">
|
|
<div className="grid min-h-[70vh] grid-rows-[1fr_auto]">
|
|
<div className="relative overflow-hidden bg-slate-950" onWheel={wheel} onPointerDown={pointerDown} onPointerMove={pointerMove} onPointerUp={pointerEnd} onPointerCancel={pointerEnd} onDoubleClick={() => applyScale(scale > 1 ? 1 : 2)} style={{ touchAction: "none", cursor: scale > 1 ? "grab" : "zoom-in" }}>
|
|
<div className="absolute inset-6 transition-transform duration-100 will-change-transform" style={{ transform: `translate3d(${offset.x}px, ${offset.y}px, 0) scale(${scale})` }}>
|
|
<Image src={selected} alt={name} fill sizes="95vw" className="select-none object-contain" draggable={false} />
|
|
</div>
|
|
<div className="pointer-events-none absolute left-5 top-5 rounded-full bg-black/65 px-3 py-1.5 text-xs font-black text-white backdrop-blur">Rueda o botones para acercar · Arrastra para mover</div>
|
|
</div>
|
|
<div className="flex flex-wrap items-center justify-center gap-3 border-t border-white/10 bg-slate-900 p-4 text-white">
|
|
<button type="button" aria-label="Alejar" onClick={() => applyScale(scale - .25)} disabled={scale <= 1} className="grid size-11 place-items-center rounded-full bg-white/10 hover:bg-white/20 disabled:opacity-35"><Minus /></button>
|
|
<input aria-label="Nivel de zoom" type="range" min="1" max="3" step="0.1" value={scale} onChange={(event) => applyScale(Number(event.target.value))} className="w-44 accent-fuchsia-500 sm:w-64" />
|
|
<button type="button" aria-label="Acercar" onClick={() => applyScale(scale + .25)} disabled={scale >= 3} className="grid size-11 place-items-center rounded-full bg-white/10 hover:bg-white/20 disabled:opacity-35"><Plus /></button>
|
|
<button type="button" onClick={reset} className="inline-flex h-11 items-center gap-2 rounded-full bg-white/10 px-4 text-sm font-black hover:bg-white/20"><RotateCcw className="size-4" />Restablecer</button>
|
|
<span className="inline-flex h-11 items-center gap-2 rounded-full bg-fuchsia-500 px-4 text-sm font-black"><Maximize2 className="size-4" />{Math.round(scale * 100)}%</span>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|