177 lines
6.7 KiB
TypeScript
177 lines
6.7 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import Image from 'next/image';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { X, ChevronLeft, ChevronRight, Maximize2, ImageIcon } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface GalleryDialogProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
images: string[];
|
|
vehicleName: string;
|
|
categoryName: string;
|
|
}
|
|
|
|
export function GalleryDialog({ isOpen, onClose, images, vehicleName, categoryName }: GalleryDialogProps) {
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
const [direction, setDirection] = useState(0);
|
|
|
|
// Lock scroll when open
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
document.body.style.overflow = 'hidden';
|
|
} else {
|
|
document.body.style.overflow = 'unset';
|
|
}
|
|
return () => {
|
|
document.body.style.overflow = 'unset';
|
|
};
|
|
}, [isOpen]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const nextImage = () => {
|
|
setDirection(1);
|
|
setCurrentIndex((prev) => (prev + 1) % images.length);
|
|
};
|
|
|
|
const prevImage = () => {
|
|
setDirection(-1);
|
|
setCurrentIndex((prev) => (prev - 1 + images.length) % images.length);
|
|
};
|
|
|
|
const variants = {
|
|
enter: (direction: number) => ({
|
|
x: direction > 0 ? 1000 : -1000,
|
|
opacity: 0,
|
|
scale: 0.9,
|
|
}),
|
|
center: {
|
|
zIndex: 1,
|
|
x: 0,
|
|
opacity: 1,
|
|
scale: 1,
|
|
},
|
|
exit: (direction: number) => ({
|
|
zIndex: 0,
|
|
x: direction < 0 ? 1000 : -1000,
|
|
opacity: 0,
|
|
scale: 0.9,
|
|
}),
|
|
};
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{isOpen && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/95 backdrop-blur-xl"
|
|
>
|
|
{/* Close Button */}
|
|
<button
|
|
onClick={onClose}
|
|
className="absolute top-4 right-4 md:top-8 md:right-8 z-[110] p-3 md:p-4 text-white hover:text-primary transition-all bg-black/40 hover:bg-black/60 rounded-full"
|
|
>
|
|
<X className="w-6 h-6 md:w-8 md:h-8" />
|
|
</button>
|
|
|
|
{/* Header Info */}
|
|
<div className="absolute top-4 left-4 md:top-8 md:left-8 z-[110] flex flex-col gap-1 md:gap-2 pr-20">
|
|
<h3 className="text-sm md:text-2xl font-black uppercase tracking-[0.1em] md:tracking-[0.3em] text-white leading-tight">
|
|
<span className="md:hidden block">{categoryName}</span>
|
|
<span className="hidden md:block">{vehicleName}</span>
|
|
</h3>
|
|
<div className="flex items-center gap-2 md:gap-4 text-primary text-[8px] md:text-[10px] font-black tracking-widest uppercase">
|
|
<ImageIcon className="w-3 h-3 md:w-4 md:h-4" />
|
|
<span>{currentIndex + 1} / {images.length} Kép</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Stage */}
|
|
<div className="relative w-full h-[50vh] md:h-[65vh] max-w-7xl px-4 md:px-12 flex items-center justify-center">
|
|
<AnimatePresence initial={false} custom={direction}>
|
|
{images.length > 0 && (
|
|
<motion.div
|
|
key={currentIndex}
|
|
custom={direction}
|
|
variants={variants}
|
|
initial="enter"
|
|
animate="center"
|
|
exit="exit"
|
|
transition={{
|
|
x: { type: "spring", stiffness: 300, damping: 30 },
|
|
opacity: { duration: 0.3 },
|
|
scale: { duration: 0.4 }
|
|
}}
|
|
className="absolute inset-0 flex items-center justify-center p-2 md:p-8"
|
|
>
|
|
<div className="relative w-full h-full shadow-2xl shadow-primary/10 rounded-[1.5rem] md:rounded-[2rem] overflow-hidden border border-white/10 bg-black/20">
|
|
<Image
|
|
src={images[currentIndex]}
|
|
alt={`${vehicleName} view ${currentIndex + 1}`}
|
|
fill
|
|
className="object-contain"
|
|
priority
|
|
quality={100}
|
|
/>
|
|
|
|
{/* Vignette Overlay - Lighter for object-contain */}
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/20 via-transparent to-transparent pointer-events-none" />
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* Navigation Handles */}
|
|
<button
|
|
onClick={prevImage}
|
|
className="absolute left-2 md:left-8 z-[110] w-12 h-12 md:w-16 md:h-16 rounded-full border border-white/10 bg-black/40 backdrop-blur-md flex items-center justify-center text-white hover:bg-primary hover:border-primary transition-all group active:scale-95"
|
|
>
|
|
<ChevronLeft className="w-6 h-6 md:w-8 md:h-8 group-hover:-translate-x-1 transition-transform" />
|
|
</button>
|
|
<button
|
|
onClick={nextImage}
|
|
className="absolute right-2 md:right-8 z-[110] w-12 h-12 md:w-16 md:h-16 rounded-full border border-white/10 bg-black/40 backdrop-blur-md flex items-center justify-center text-white hover:bg-primary hover:border-primary transition-all group active:scale-95"
|
|
>
|
|
<ChevronRight className="w-6 h-6 md:w-8 md:h-8 group-hover:translate-x-1 transition-transform" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Thumbnails Sidebar/Bottom Bar */}
|
|
<div className="absolute bottom-4 md:bottom-8 left-0 right-0 z-[110] flex justify-center gap-2 md:gap-4 px-4 md:px-8 overflow-x-auto pb-4 scrollbar-hide">
|
|
{images.map((img, idx) => (
|
|
<button
|
|
key={idx}
|
|
onClick={() => {
|
|
setDirection(idx > currentIndex ? 1 : -1);
|
|
setCurrentIndex(idx);
|
|
}}
|
|
className={cn(
|
|
"relative w-20 h-14 md:w-32 md:h-20 rounded-lg md:rounded-xl overflow-hidden border-2 transition-all flex-shrink-0 active:scale-95",
|
|
currentIndex === idx
|
|
? "border-primary scale-105 md:scale-110 shadow-lg shadow-primary/20"
|
|
: "border-white/10 opacity-40 hover:opacity-100"
|
|
)}
|
|
>
|
|
<Image
|
|
src={img}
|
|
alt="Thumbnail"
|
|
fill
|
|
className="object-cover"
|
|
/>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Subtle noise/texture overlay for premium feel */}
|
|
<div className="fixed inset-0 pointer-events-none opacity-[0.03] bg-[url('/images/noise.png')] mix-blend-overlay" />
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|