Initialize repo
This commit is contained in:
304
components/gallery.tsx
Normal file
304
components/gallery.tsx
Normal file
@@ -0,0 +1,304 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user