305 lines
12 KiB
TypeScript
305 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import Image from "next/image";
|
|
import { X, ChevronLeft, ChevronRight, Camera } from "lucide-react";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
export function Gallery() {
|
|
/* eslint-disable react/no-array-index-key */
|
|
const [lightboxOpen, setLightboxOpen] = useState(false);
|
|
const [activeItemIndex, setActiveItemIndex] = useState<number | null>(null);
|
|
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
|
|
const [direction, setDirection] = useState(0);
|
|
|
|
const galleryItems = [
|
|
{
|
|
title: "Pokój 1-osobowy",
|
|
badge: "Nr 31",
|
|
cover: "/31_1.webp",
|
|
images: [
|
|
"/31_1.webp", "/31_2.webp", "/31_3.webp", "/31_4.webp", "/31_5.webp", "/31_6.webp"
|
|
],
|
|
},
|
|
{
|
|
title: "Pokój 2-osobowy",
|
|
badge: "Nr 30, 32",
|
|
cover: "/placeholder.svg",
|
|
images: ["/placeholder.svg"],
|
|
},
|
|
{
|
|
title: "Domek 2-osobowy",
|
|
badge: "Nr 5, 5a",
|
|
cover: "/5_1.webp",
|
|
images: [
|
|
"/5_1.webp", "/5_2.webp", "/5_3.webp", "/5_4.webp", "/5_5.webp", "/5_6.webp"
|
|
],
|
|
},
|
|
{
|
|
title: "Domek 3-osobowy",
|
|
badge: "Nr 7-9, 20-23",
|
|
cover: "/placeholder.svg",
|
|
images: ["/placeholder.svg"],
|
|
},
|
|
{
|
|
title: "Domek 4-osobowy (1 pomieszczenie)",
|
|
badge: "Nr 10-14",
|
|
cover: "/11_1.webp",
|
|
images: [
|
|
"/11_1.webp", "/11_2.webp", "/11_3.webp", "/11_4.webp", "/11_5.webp", "/11_6.webp", "/11_7.webp"
|
|
],
|
|
},
|
|
{
|
|
title: "Domek 4-osobowy (2 pomieszczenia)",
|
|
badge: "Nr 6, 33, 4, 2, 3",
|
|
cover: "/4_1.webp",
|
|
images: [
|
|
"/4_1.webp", "/4_2.webp", "/4_3.webp", "/4_4.webp", "/4_5.webp", "/4_6.webp"
|
|
],
|
|
},
|
|
{
|
|
title: "Domek 4-osobowy",
|
|
badge: "Nr 15-19",
|
|
cover: "/19_1.webp",
|
|
images: [
|
|
"/19_1.webp", "/19_2.webp", "/19_3.webp", "/19_4.webp", "/19_5.webp", "/19_6.webp", "/19_7.webp"
|
|
],
|
|
|
|
},
|
|
{
|
|
title: "Domek 4-osobowy",
|
|
badge: "Nr 24-26",
|
|
cover: "/26_1.webp",
|
|
images: [
|
|
"/26_1.webp", "/26_2.webp", "/26_3.webp", "/26_4.webp", "/26_5.webp", "/26_6.webp"
|
|
],
|
|
},
|
|
{
|
|
title: "Pokój studio 5 osobowy",
|
|
badge: "Nr 27-29",
|
|
cover: "/29_1.webp",
|
|
images: [
|
|
"/29_1.webp", "/29_2.webp", "/29_3.webp", "/29_4.webp", "/29_5.webp", "/29_6.webp", "/29_7.webp", "/29_8.webp", "/29_9.webp"
|
|
],
|
|
},
|
|
];
|
|
|
|
const openLightbox = (index: number) => {
|
|
setActiveItemIndex(index);
|
|
setCurrentPhotoIndex(0);
|
|
setLightboxOpen(true);
|
|
document.body.style.overflow = "hidden";
|
|
};
|
|
|
|
const closeLightbox = () => {
|
|
setLightboxOpen(false);
|
|
setActiveItemIndex(null);
|
|
document.body.style.overflow = "";
|
|
};
|
|
|
|
const goToPrevious = () => {
|
|
setDirection(-1);
|
|
if (activeItemIndex === null) return;
|
|
const images = galleryItems[activeItemIndex].images;
|
|
setCurrentPhotoIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1));
|
|
};
|
|
|
|
const goToNext = () => {
|
|
setDirection(1);
|
|
if (activeItemIndex === null) return;
|
|
const images = galleryItems[activeItemIndex].images;
|
|
setCurrentPhotoIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1));
|
|
};
|
|
|
|
const currentImages = activeItemIndex !== null ? galleryItems[activeItemIndex].images : [];
|
|
|
|
const swipeConfidenceThreshold = 10000;
|
|
const swipePower = (offset: number, velocity: number) => {
|
|
return Math.abs(offset) * velocity;
|
|
};
|
|
|
|
return (
|
|
<section id="gallery" className="py-24 bg-gray-50/50">
|
|
<div className="container mx-auto px-4">
|
|
{/* ... (header and grid remain same as original, just need to be careful with replace) */}
|
|
|
|
<div className="max-w-7xl mx-auto">
|
|
<div className="text-center mb-16">
|
|
<h2 className="text-4xl font-bold text-gray-900 mb-4 tracking-tight">
|
|
Nasze Pokoje i Domki
|
|
</h2>
|
|
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
|
|
Zapraszamy do zapoznania się z naszą ofertą noclegową.
|
|
Kliknij na wybrany obiekt, aby zobaczyć galerię zdjęć.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
|
{galleryItems.map((item, index) => (
|
|
<div
|
|
key={index}
|
|
className="group cursor-pointer bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-xl transition-all duration-300 border border-gray-100 transform hover:-translate-y-1"
|
|
onClick={() => openLightbox(index)}
|
|
>
|
|
<div className="relative aspect-4/3 overflow-hidden">
|
|
<Image
|
|
src={item.cover}
|
|
alt={item.title}
|
|
fill
|
|
className="object-cover transition-transform duration-500 group-hover:scale-110"
|
|
/>
|
|
<div className="absolute inset-0 bg-linear-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
|
|
|
<div className="absolute top-4 right-4 bg-white/90 backdrop-blur-sm text-primary px-3 py-1 rounded-full text-xs font-semibold shadow-sm z-20">
|
|
{item.badge}
|
|
</div>
|
|
|
|
<div className="absolute bottom-4 left-4 right-4 translate-y-4 opacity-0 group-hover:translate-y-0 group-hover:opacity-100 transition-all duration-300 delay-100 flex items-center justify-between text-white">
|
|
<span className="text-sm font-medium">Zobacz zdjęcia</span>
|
|
<Camera className="w-5 h-5" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-6 relative bg-white z-10 transition-colors duration-300 group-hover:bg-blue-50/50">
|
|
<h3 className="text-xl font-bold text-gray-900 mb-2 group-hover:text-blue-600 transition-colors">
|
|
{item.title}
|
|
</h3>
|
|
<div className="h-1 w-12 bg-blue-500 rounded-full transition-all duration-300 group-hover:w-full opacity-50" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<AnimatePresence>
|
|
{lightboxOpen && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 z-50 flex items-center justify-center p-0 sm:p-8 bg-black/95 backdrop-blur-sm"
|
|
>
|
|
{/* Backdrop with click to close */}
|
|
<div className="absolute inset-0" onClick={closeLightbox} />
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="absolute top-4 right-4 text-white hover:bg-white/10 z-50 rounded-full w-12 h-12"
|
|
onClick={closeLightbox}
|
|
aria-label="Zamknij"
|
|
>
|
|
<X className="w-8 h-8" />
|
|
</Button>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="absolute left-4 top-1/2 -translate-y-1/2 text-white hover:bg-white/10 z-50 rounded-full w-12 h-12 flex" // Removed hidden sm:flex
|
|
onClick={(e) => { e.stopPropagation(); goToPrevious(); }}
|
|
aria-label="Poprzednie zdjęcie"
|
|
>
|
|
<ChevronLeft className="w-8 h-8" />
|
|
</Button>
|
|
|
|
<div className="relative w-full h-full sm:h-[80vh] flex items-center justify-center z-40 pointer-events-none">
|
|
<div className="relative w-full h-full pointer-events-auto flex items-center justify-center">
|
|
<AnimatePresence initial={false} custom={direction} mode="popLayout">
|
|
<motion.div
|
|
key={currentPhotoIndex}
|
|
custom={direction}
|
|
variants={{
|
|
enter: (direction: number) => ({
|
|
x: direction > 0 ? 1000 : -1000,
|
|
opacity: 0
|
|
}),
|
|
center: {
|
|
zIndex: 1,
|
|
x: 0,
|
|
opacity: 1
|
|
},
|
|
exit: (direction: number) => ({
|
|
zIndex: 0,
|
|
x: direction < 0 ? 1000 : -1000,
|
|
opacity: 0
|
|
})
|
|
}}
|
|
initial="enter"
|
|
animate="center"
|
|
exit="exit"
|
|
transition={{
|
|
x: { type: "spring", stiffness: 300, damping: 30 },
|
|
opacity: { duration: 0.2 }
|
|
}}
|
|
drag="x"
|
|
dragConstraints={{ left: 0, right: 0 }}
|
|
dragElastic={1}
|
|
onDragEnd={(e, { offset, velocity }) => {
|
|
const swipe = swipePower(offset.x, velocity.x);
|
|
if (swipe < -swipeConfidenceThreshold) {
|
|
goToNext();
|
|
} else if (swipe > swipeConfidenceThreshold) {
|
|
goToPrevious();
|
|
}
|
|
}}
|
|
className="absolute w-full h-full flex items-center justify-center"
|
|
>
|
|
<div className="relative w-full h-full max-w-5xl max-h-[80vh]">
|
|
<Image
|
|
src={currentImages[currentPhotoIndex] || "/placeholder.svg"}
|
|
alt={activeItemIndex !== null ? galleryItems[activeItemIndex].title : "Gallery Image"}
|
|
fill
|
|
className="object-contain"
|
|
quality={100}
|
|
priority
|
|
draggable={false}
|
|
/>
|
|
</div>
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
</div>
|
|
|
|
{activeItemIndex !== null && (
|
|
<div className="absolute bottom-20 sm:bottom-0 left-0 right-0 text-center text-white pointer-events-none">
|
|
<h3 className="text-xl font-semibold drop-shadow-md">{galleryItems[activeItemIndex].title}</h3>
|
|
<p className="text-white/80 text-sm mt-1 drop-shadow-md">
|
|
Zdjęcie {currentPhotoIndex + 1} z {currentImages.length}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="absolute right-4 top-1/2 -translate-y-1/2 text-white hover:bg-white/10 z-50 rounded-full w-12 h-12 flex" // Removed hidden sm:flex
|
|
onClick={(e) => { e.stopPropagation(); goToNext(); }}
|
|
aria-label="Następne zdjęcie"
|
|
>
|
|
<ChevronRight className="w-8 h-8" />
|
|
</Button>
|
|
|
|
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-2 z-50 overflow-x-auto max-w-[90vw] px-4 scrollbar-hide">
|
|
{currentImages.map((_, index) => (
|
|
<button
|
|
key={index}
|
|
onClick={(e) => { e.stopPropagation(); setCurrentPhotoIndex(index); }}
|
|
className={`w-2 h-2 rounded-full transition-all duration-300 shrink-0 ${
|
|
index === currentPhotoIndex
|
|
? "bg-white scale-150"
|
|
: "bg-white/40 hover:bg-white/60"
|
|
}`}
|
|
aria-label={`Przejdź do zdjęcia ${index + 1}`}
|
|
/>
|
|
))}
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</section>
|
|
);
|
|
}
|