diff --git a/app/data/gallery-events.json b/app/data/gallery-events.json new file mode 100644 index 0000000..58ea4d2 --- /dev/null +++ b/app/data/gallery-events.json @@ -0,0 +1,77 @@ +[ + { + "id": "2025_juin_pic_nique_midi_jeux", + "title": "Juin Pic Nique Midi Jeux", + "year": "2025", + "cover": "/gallery/2025_juin_pic_nique_midi_jeux/WhatsApp Image 2026-02-12 at 12.42.06.jpeg", + "images": [ + "/gallery/2025_juin_pic_nique_midi_jeux/WhatsApp Image 2026-02-12 at 12.42.06.jpeg", + "/gallery/2025_juin_pic_nique_midi_jeux/WhatsApp Image 2026-02-12 at 12.42.53 (1).jpeg", + "/gallery/2025_juin_pic_nique_midi_jeux/WhatsApp Image 2026-02-12 at 12.42.53.jpeg" + ], + "isFolder": true + }, + { + "id": "2025_pique_nic_acrobranche", + "title": "Pique Nic Acrobranche", + "year": "2025", + "cover": "/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.14 (1).jpeg", + "images": [ + "/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.14 (1).jpeg", + "/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.14.jpeg", + "/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.15 (1).jpeg", + "/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.15.jpeg" + ], + "isFolder": true + }, + { + "id": "2025_laser_bowling", + "title": "Laser Bowling", + "year": "2025", + "cover": "/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.02.jpeg", + "images": [ + "/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.02.jpeg", + "/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.03 (1).jpeg", + "/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.03 (2).jpeg", + "/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.03.jpeg" + ], + "isFolder": true + }, + { + "id": "2024_juin_pique_nique", + "title": "Juin Pique Nique", + "year": "2024", + "cover": "/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.30 (1).jpeg", + "images": [ + "/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.30 (1).jpeg", + "/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.30.jpeg", + "/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.31 (1).jpeg", + "/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.31 (2).jpeg", + "/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.31.jpeg" + ], + "isFolder": true + }, + { + "id": "2024_septembre_rando_pique_nique", + "title": "Septembre Rando Pique Nique", + "year": "2024", + "cover": "/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.12 (1).jpeg", + "images": [ + "/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.12 (1).jpeg", + "/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.12 (2).jpeg", + "/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.12.jpeg", + "/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.13 (1).jpeg", + "/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.13 (2).jpeg", + "/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.13.jpeg" + ], + "isFolder": true + }, + { + "id": "2024_laser_bowling", + "title": "Laser Bowling", + "year": "2024", + "cover": "/gallery/2024_laser_bowling.jpeg", + "images": ["/gallery/2024_laser_bowling.jpeg"], + "isFolder": false + } +] diff --git a/app/routes/galerie.tsx b/app/routes/galerie.tsx index a1c317b..5f248bc 100644 --- a/app/routes/galerie.tsx +++ b/app/routes/galerie.tsx @@ -1,6 +1,19 @@ -import { X, ZoomIn } from "lucide-react"; -import { useState } from "react"; -import gallery from "../data/gallery.json"; +import { + ChevronLeft, + ChevronRight, + Image as ImageIcon, + X, + ZoomIn, +} from "lucide-react"; +import { useCallback, useEffect, useState } from "react"; +import galleryData from "../data/gallery-events.json"; +import { resolveAsset } from "../utils/assets"; + +const events = galleryData.map((event) => ({ + ...event, + cover: resolveAsset(event.cover), + images: event.images.map(resolveAsset), +})); export function meta() { return [ @@ -14,12 +27,81 @@ export function meta() { } export default function Galerie() { - const [selectedImage, setSelectedImage] = useState< - (typeof gallery)[0] | null - >(null); + const [selectedEvent, setSelectedEvent] = useState<(typeof events)[0] | null>( + null, + ); + const [focusedImageIndex, setFocusedImageIndex] = useState( + null, + ); + + const handleEventClick = (event: (typeof events)[0]) => { + setSelectedEvent(event); + setFocusedImageIndex(null); + }; + + const closeEvent = useCallback(() => { + setSelectedEvent(null); + setFocusedImageIndex(null); + }, []); + + const openImage = (index: number) => { + setFocusedImageIndex(index); + }; + + // Keyboard navigation + useEffect(() => { + if (!selectedEvent) return; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape") { + if (focusedImageIndex !== null) { + setFocusedImageIndex(null); + } else { + closeEvent(); + } + } + + if (focusedImageIndex !== null) { + if (e.key === "ArrowRight") { + setFocusedImageIndex((prev) => { + if (prev === null) return 0; + return prev < selectedEvent.images.length - 1 ? prev + 1 : 0; + }); + } + if (e.key === "ArrowLeft") { + setFocusedImageIndex((prev) => { + if (prev === null) return 0; + return prev > 0 ? prev - 1 : selectedEvent.images.length - 1; + }); + } + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [selectedEvent, focusedImageIndex, closeEvent]); + + const navigateImage = (direction: "next" | "prev", e: React.MouseEvent) => { + e.stopPropagation(); + if (!selectedEvent || focusedImageIndex === null) return; + + if (direction === "next") { + setFocusedImageIndex( + focusedImageIndex < selectedEvent.images.length - 1 + ? focusedImageIndex + 1 + : 0, + ); + } else { + setFocusedImageIndex( + focusedImageIndex > 0 + ? focusedImageIndex - 1 + : selectedEvent.images.length - 1, + ); + } + }; return ( -
+

Galerie Souvenirs @@ -30,77 +112,207 @@ export default function Galerie() {

-
- {gallery.map((image) => ( - +
+ {events.map((event) => ( + handleEventClick(event)} + /> ))}
- {/* Lightbox */} - {selectedImage && ( - +
+ + {/* Grid Content */} +
+
+ {selectedEvent.images.map((img, idx) => ( + + ))} +
+
+
+ )} + + {/* Fullscreen Lightbox */} + {selectedEvent && focusedImageIndex !== null && ( + // biome-ignore lint/a11y/useKeyWithClickEvents: Handled globally via useEffect +
setFocusedImageIndex(null)} + role="dialog" + aria-modal="true" > -
e.stopPropagation()} - onKeyDown={(e) => e.stopPropagation()} - role="none" + + + + + {/* biome-ignore lint/a11y/useKeyWithClickEvents: Image view within lightbox */} + {`${selectedEvent.title} e.stopPropagation()} + /> + +
+ {focusedImageIndex + 1} / {selectedEvent.images.length} +
+
+ )} +
+ ); +} + +function EventCard({ + event, + onClick, +}: { + event: { title: string; year: string; cover: string; images: string[] }; + onClick: () => void; +}) { + const count = event.images.length; + // Use different images for sides if available, otherwise reuse cover or second image + const leftImg = count > 1 ? event.images[1] : null; + const rightImg = + count > 2 ? event.images[2] : count > 1 ? event.images[1] : null; + + return ( + - )} -
+ + + ); } diff --git a/app/utils/assets.ts b/app/utils/assets.ts new file mode 100644 index 0000000..ce49fcc --- /dev/null +++ b/app/utils/assets.ts @@ -0,0 +1,14 @@ +/** + * Resolves a path for a static asset in the public directory, + * taking into account the Vite base URL (e.g. for previews). + * + * @param path The absolute path to the asset (e.g. "/gallery/image.jpg") + * @returns The resolved path including the base URL + */ +export function resolveAsset(path: string): string { + const baseUrl = import.meta.env.BASE_URL; + if (baseUrl === "/") return path; + const cleanPath = path.startsWith("/") ? path.slice(1) : path; + + return `${baseUrl}${cleanPath}`; +} diff --git a/public/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.30 (1).jpeg b/public/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.30 (1).jpeg new file mode 100644 index 0000000..fd2b791 Binary files /dev/null and b/public/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.30 (1).jpeg differ diff --git a/public/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.30.jpeg b/public/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.30.jpeg new file mode 100644 index 0000000..c6c0dfb Binary files /dev/null and b/public/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.30.jpeg differ diff --git a/public/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.31 (1).jpeg b/public/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.31 (1).jpeg new file mode 100644 index 0000000..aa9f6de Binary files /dev/null and b/public/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.31 (1).jpeg differ diff --git a/public/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.31 (2).jpeg b/public/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.31 (2).jpeg new file mode 100644 index 0000000..f84349e Binary files /dev/null and b/public/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.31 (2).jpeg differ diff --git a/public/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.31.jpeg b/public/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.31.jpeg new file mode 100644 index 0000000..679324a Binary files /dev/null and b/public/gallery/2024_juin_pique_nique/WhatsApp Image 2026-02-12 at 12.55.31.jpeg differ diff --git a/public/gallery/2024_laser_bowling.jpeg b/public/gallery/2024_laser_bowling.jpeg new file mode 100644 index 0000000..fdf2946 Binary files /dev/null and b/public/gallery/2024_laser_bowling.jpeg differ diff --git a/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.12 (1).jpeg b/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.12 (1).jpeg new file mode 100644 index 0000000..3210f59 Binary files /dev/null and b/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.12 (1).jpeg differ diff --git a/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.12 (2).jpeg b/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.12 (2).jpeg new file mode 100644 index 0000000..d1881dd Binary files /dev/null and b/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.12 (2).jpeg differ diff --git a/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.12.jpeg b/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.12.jpeg new file mode 100644 index 0000000..ba17db6 Binary files /dev/null and b/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.12.jpeg differ diff --git a/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.13 (1).jpeg b/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.13 (1).jpeg new file mode 100644 index 0000000..ee85e91 Binary files /dev/null and b/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.13 (1).jpeg differ diff --git a/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.13 (2).jpeg b/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.13 (2).jpeg new file mode 100644 index 0000000..08fe29b Binary files /dev/null and b/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.13 (2).jpeg differ diff --git a/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.13.jpeg b/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.13.jpeg new file mode 100644 index 0000000..e0b143c Binary files /dev/null and b/public/gallery/2024_septembre_rando_pique_nique/WhatsApp Image 2026-02-12 at 12.53.13.jpeg differ diff --git a/public/gallery/2025_juin_pic_nique_midi_jeux/WhatsApp Image 2026-02-12 at 12.42.06.jpeg b/public/gallery/2025_juin_pic_nique_midi_jeux/WhatsApp Image 2026-02-12 at 12.42.06.jpeg new file mode 100644 index 0000000..fbbcb2c Binary files /dev/null and b/public/gallery/2025_juin_pic_nique_midi_jeux/WhatsApp Image 2026-02-12 at 12.42.06.jpeg differ diff --git a/public/gallery/2025_juin_pic_nique_midi_jeux/WhatsApp Image 2026-02-12 at 12.42.53 (1).jpeg b/public/gallery/2025_juin_pic_nique_midi_jeux/WhatsApp Image 2026-02-12 at 12.42.53 (1).jpeg new file mode 100644 index 0000000..73fc588 Binary files /dev/null and b/public/gallery/2025_juin_pic_nique_midi_jeux/WhatsApp Image 2026-02-12 at 12.42.53 (1).jpeg differ diff --git a/public/gallery/2025_juin_pic_nique_midi_jeux/WhatsApp Image 2026-02-12 at 12.42.53.jpeg b/public/gallery/2025_juin_pic_nique_midi_jeux/WhatsApp Image 2026-02-12 at 12.42.53.jpeg new file mode 100644 index 0000000..7aaa0e3 Binary files /dev/null and b/public/gallery/2025_juin_pic_nique_midi_jeux/WhatsApp Image 2026-02-12 at 12.42.53.jpeg differ diff --git a/public/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.02.jpeg b/public/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.02.jpeg new file mode 100644 index 0000000..901a5ed Binary files /dev/null and b/public/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.02.jpeg differ diff --git a/public/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.03 (1).jpeg b/public/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.03 (1).jpeg new file mode 100644 index 0000000..d3e58ab Binary files /dev/null and b/public/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.03 (1).jpeg differ diff --git a/public/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.03 (2).jpeg b/public/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.03 (2).jpeg new file mode 100644 index 0000000..2505ced Binary files /dev/null and b/public/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.03 (2).jpeg differ diff --git a/public/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.03.jpeg b/public/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.03.jpeg new file mode 100644 index 0000000..8386add Binary files /dev/null and b/public/gallery/2025_laser_bowling/WhatsApp Image 2026-02-12 at 12.33.03.jpeg differ diff --git a/public/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.14 (1).jpeg b/public/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.14 (1).jpeg new file mode 100644 index 0000000..f11dfa0 Binary files /dev/null and b/public/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.14 (1).jpeg differ diff --git a/public/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.14.jpeg b/public/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.14.jpeg new file mode 100644 index 0000000..563cb00 Binary files /dev/null and b/public/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.14.jpeg differ diff --git a/public/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.15 (1).jpeg b/public/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.15 (1).jpeg new file mode 100644 index 0000000..2a156b3 Binary files /dev/null and b/public/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.15 (1).jpeg differ diff --git a/public/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.15.jpeg b/public/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.15.jpeg new file mode 100644 index 0000000..9cb2306 Binary files /dev/null and b/public/gallery/2025_pique_nic_acrobranche/WhatsApp Image 2026-02-12 at 12.40.15.jpeg differ diff --git a/scripts/generate-gallery.ts b/scripts/generate-gallery.ts new file mode 100644 index 0000000..3d8b851 --- /dev/null +++ b/scripts/generate-gallery.ts @@ -0,0 +1,97 @@ +import fs from "node:fs"; +import path from "node:path"; + +const PUBLIC_DIR = path.join(process.cwd(), "public"); +const GALLERY_DIR = path.join(PUBLIC_DIR, "gallery"); +const OUTPUT_FILE = path.join(process.cwd(), "app/data/gallery-events.json"); + +export interface GalleryEvent { + id: string; + title: string; + year: string; + cover: string; + images: string[]; + isFolder: boolean; +} + +function getGalleryEvents(): GalleryEvent[] { + if (!fs.existsSync(GALLERY_DIR)) { + console.log("Gallery directory not found:", GALLERY_DIR); + return []; + } + + const entries = fs.readdirSync(GALLERY_DIR, { withFileTypes: true }); + const events: GalleryEvent[] = []; + + for (const entry of entries) { + if (entry.name.startsWith(".")) continue; + + if (entry.isDirectory()) { + const dirPath = path.join(GALLERY_DIR, entry.name); + const files = fs.readdirSync(dirPath); + const images = files + .filter((file) => /\.(jpg|jpeg|png|webp|gif)$/i.test(file)) + .map((file) => `/gallery/${entry.name}/${file}`); + + if (images.length > 0) { + const titleParts = entry.name.split("_"); + // Check if first part is a year (4 digits) + const possibleYear = titleParts[0]; + const isYear = /^\d{4}$/.test(possibleYear); + const year = isYear + ? possibleYear + : new Date().getFullYear().toString(); + + // Remove year from title parts if present + const titleWords = isYear ? titleParts.slice(1) : titleParts; + const title = titleWords + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + + events.push({ + id: entry.name, + title, + year, + cover: images[0], + images, + isFolder: true, + }); + } + } else if ( + entry.isFile() && + /\.(jpg|jpeg|png|webp|gif)$/i.test(entry.name) + ) { + // It's a single image file + const nameWithoutExt = path.parse(entry.name).name; + const titleParts = nameWithoutExt.split("_"); + + const possibleYear = titleParts[0]; + const isYear = /^\d{4}$/.test(possibleYear); + const year = isYear ? possibleYear : new Date().getFullYear().toString(); + + const titleWords = isYear ? titleParts.slice(1) : titleParts; + const title = titleWords + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + + const url = `/gallery/${entry.name}`; + events.push({ + id: nameWithoutExt, + title, + year, + cover: url, + images: [url], + isFolder: false, + }); + } + } + + // Sort by year descending + return events.sort((a, b) => Number(b.year) - Number(a.year)); +} + +const events = getGalleryEvents(); +fs.writeFileSync(OUTPUT_FILE, JSON.stringify(events, null, 2)); +console.log( + `Generated gallery manifest with ${events.length} events at ${OUTPUT_FILE}`, +);