mirror of
https://github.com/Ichitux/lambada-fiesta-live.git
synced 2026-06-11 02:44:58 +02:00
New pictures & gallery lightbox
All checks were successful
Deploy NPM app / Deploy NPM (push) Successful in 1m22s
All checks were successful
Deploy NPM app / Deploy NPM (push) Successful in 1m22s
This commit is contained in:
BIN
src/assets/staff/braz_romi.jpg
Normal file
BIN
src/assets/staff/braz_romi.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 214 KiB |
BIN
src/assets/staff/milu.jpg
Normal file
BIN
src/assets/staff/milu.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 193 KiB |
BIN
src/assets/staff/safira.jpg
Normal file
BIN
src/assets/staff/safira.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 193 KiB |
@@ -2,9 +2,18 @@ import { motion } from "framer-motion";
|
|||||||
import { GALLERY_IMAGES } from "@/data/event-data";
|
import { GALLERY_IMAGES } from "@/data/event-data";
|
||||||
import { ImageIcon } from "lucide-react";
|
import { ImageIcon } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useState } from "react";
|
||||||
|
import Lightbox from "./Lightbox";
|
||||||
|
|
||||||
const GallerySection = () => {
|
const GallerySection = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
|
||||||
|
const openLightbox = (i: number) => {
|
||||||
|
setCurrentIndex(i);
|
||||||
|
setIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
@@ -32,7 +41,8 @@ const GallerySection = () => {
|
|||||||
whileInView={{ opacity: 1, scale: 1 }}
|
whileInView={{ opacity: 1, scale: 1 }}
|
||||||
viewport={{ once: false, amount: 0.15 }}
|
viewport={{ once: false, amount: 0.15 }}
|
||||||
transition={{ delay: i * 0.08 }}
|
transition={{ delay: i * 0.08 }}
|
||||||
className="aspect-square rounded-xl overflow-hidden bg-muted flex items-center justify-center"
|
className="aspect-square rounded-xl overflow-hidden bg-muted flex items-center justify-center cursor-pointer"
|
||||||
|
onClick={() => openLightbox(i)}
|
||||||
>
|
>
|
||||||
{img.src ? (
|
{img.src ? (
|
||||||
<img
|
<img
|
||||||
@@ -50,6 +60,9 @@ const GallerySection = () => {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{isOpen && (
|
||||||
|
<Lightbox images={GALLERY_IMAGES} initialIndex={currentIndex} onClose={() => setIsOpen(false)} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
148
src/components/Lightbox.tsx
Normal file
148
src/components/Lightbox.tsx
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
|
import { ImageIcon, X, ChevronLeft, ChevronRight, Play, Pause, ZoomIn, ZoomOut } from "lucide-react";
|
||||||
|
|
||||||
|
type ImageItem = { src?: string; alt?: string };
|
||||||
|
|
||||||
|
type LightboxProps = {
|
||||||
|
images: ImageItem[];
|
||||||
|
initialIndex?: number;
|
||||||
|
onClose?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Lightbox: React.FC<LightboxProps> = ({ images, initialIndex = 0, onClose }) => {
|
||||||
|
const [index, setIndex] = useState(initialIndex);
|
||||||
|
const [scale, setScale] = useState(1);
|
||||||
|
const [playing, setPlaying] = useState(false);
|
||||||
|
const timerRef = useRef<number | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIndex(initialIndex);
|
||||||
|
}, [initialIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") handleClose();
|
||||||
|
if (e.key === "ArrowRight") next();
|
||||||
|
if (e.key === "ArrowLeft") prev();
|
||||||
|
if (e.key === "+") zoomIn();
|
||||||
|
if (e.key === "-") zoomOut();
|
||||||
|
if (e.key === " ") setPlaying((p) => !p);
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", onKey);
|
||||||
|
return () => window.removeEventListener("keydown", onKey);
|
||||||
|
}, [index]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (playing) {
|
||||||
|
timerRef.current = window.setInterval(() => setIndex((i) => (i + 1) % images.length), 3500);
|
||||||
|
} else if (timerRef.current) {
|
||||||
|
window.clearInterval(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (timerRef.current) window.clearInterval(timerRef.current);
|
||||||
|
};
|
||||||
|
}, [playing, images.length]);
|
||||||
|
|
||||||
|
const prev = () => setIndex((i) => (i - 1 + images.length) % images.length);
|
||||||
|
const next = () => setIndex((i) => (i + 1) % images.length);
|
||||||
|
const handleClose = () => {
|
||||||
|
setPlaying(false);
|
||||||
|
setScale(1);
|
||||||
|
onClose?.();
|
||||||
|
};
|
||||||
|
const zoomIn = () => setScale((s) => Math.min(4, +(s + 0.25).toFixed(2)));
|
||||||
|
const zoomOut = () => setScale((s) => Math.max(0.5, +(s - 0.25).toFixed(2)));
|
||||||
|
|
||||||
|
if (!images || images.length === 0) return null;
|
||||||
|
|
||||||
|
const img = images[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80">
|
||||||
|
<div className="absolute top-4 right-4">
|
||||||
|
<button
|
||||||
|
aria-label="Close"
|
||||||
|
onClick={handleClose}
|
||||||
|
className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute left-4 inset-y-0 flex items-center">
|
||||||
|
<button
|
||||||
|
onClick={prev}
|
||||||
|
className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white"
|
||||||
|
aria-label="Previous"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-8 h-8" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute right-4 inset-y-0 flex items-center">
|
||||||
|
<button
|
||||||
|
onClick={next}
|
||||||
|
className="p-2 rounded-full bg-white/10 hover:bg-white/20 text-white"
|
||||||
|
aria-label="Next"
|
||||||
|
>
|
||||||
|
<ChevronRight className="w-8 h-8" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-[95%] max-h-[85%] flex items-center justify-center">
|
||||||
|
{img?.src ? (
|
||||||
|
<img
|
||||||
|
src={img.src}
|
||||||
|
alt={img.alt || "image"}
|
||||||
|
style={{ transform: `scale(${scale})` }}
|
||||||
|
className="max-w-full max-h-full object-contain transition-transform duration-200"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-white/70 flex flex-col items-center">
|
||||||
|
<ImageIcon className="w-20 h-20 mb-2" />
|
||||||
|
<div>{img?.alt}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
onClick={zoomOut}
|
||||||
|
className="p-2 rounded-md bg-white/10 hover:bg-white/20 text-white flex items-center gap-2"
|
||||||
|
aria-label="Zoom out"
|
||||||
|
>
|
||||||
|
<ZoomOut className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setScale(1)}
|
||||||
|
className="p-2 rounded-md bg-white/10 hover:bg-white/20 text-white"
|
||||||
|
aria-label="Reset zoom"
|
||||||
|
>
|
||||||
|
100%
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={zoomIn}
|
||||||
|
className="p-2 rounded-md bg-white/10 hover:bg-white/20 text-white flex items-center gap-2"
|
||||||
|
aria-label="Zoom in"
|
||||||
|
>
|
||||||
|
<ZoomIn className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setPlaying((p) => !p)}
|
||||||
|
className="p-2 rounded-md bg-white/10 hover:bg-white/20 text-white flex items-center gap-2"
|
||||||
|
aria-label="Play slideshow"
|
||||||
|
>
|
||||||
|
{playing ? <Pause className="w-5 h-5" /> : <Play className="w-5 h-5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-4 right-4 text-white/80 text-sm">
|
||||||
|
{index + 1} / {images.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Lightbox;
|
||||||
@@ -21,6 +21,9 @@ import djwinx from "@/assets/staff/djwinx.jpg";
|
|||||||
import djklebynho from "@/assets/staff/djklebynho.jpg";
|
import djklebynho from "@/assets/staff/djklebynho.jpg";
|
||||||
import letialex from "@/assets/staff/letialex.jpg";
|
import letialex from "@/assets/staff/letialex.jpg";
|
||||||
import djcathie from "@/assets/staff/djcathie.jpg";
|
import djcathie from "@/assets/staff/djcathie.jpg";
|
||||||
|
import milu from "@/assets/staff/milu.jpg";
|
||||||
|
import safira from "@/assets/staff/safira.jpg";
|
||||||
|
import brazromi from "@/assets/staff/braz_romi.jpg";
|
||||||
|
|
||||||
import gal1 from "@/assets/gallery/gal1.jpg";
|
import gal1 from "@/assets/gallery/gal1.jpg";
|
||||||
import gal2 from "@/assets/gallery/gal2.jpg";
|
import gal2 from "@/assets/gallery/gal2.jpg";
|
||||||
@@ -189,6 +192,36 @@ export const STAFF = [
|
|||||||
soundcloud: "",
|
soundcloud: "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "11",
|
||||||
|
name: "[Milu]",
|
||||||
|
role: "Instructor" as const,
|
||||||
|
description: "[Breve biografía del instructor]",
|
||||||
|
image: milu,
|
||||||
|
socials: {
|
||||||
|
instagram: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "12",
|
||||||
|
name: "[Safira]",
|
||||||
|
role: "Instructor" as const,
|
||||||
|
description: "[Breve biografía del instructor]",
|
||||||
|
image: safira,
|
||||||
|
socials: {
|
||||||
|
instagram: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "13",
|
||||||
|
name: "[Braz & Romina]",
|
||||||
|
role: "Instructor" as const,
|
||||||
|
description: "[Breve biografía del instructor]",
|
||||||
|
image: brazromi,
|
||||||
|
socials: {
|
||||||
|
instagram: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// ---- PROGRAMA DEL EVENTO ----
|
// ---- PROGRAMA DEL EVENTO ----
|
||||||
|
|||||||
Reference in New Issue
Block a user