Initialize repo

This commit is contained in:
Kazimierz Ciołek
2026-02-02 13:44:55 +01:00
commit 704edb6ae0
149 changed files with 12373 additions and 0 deletions

304
components/gallery.tsx Normal file
View 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>
);
}