jalil.arfaoui.net/src/components/photo/Lightbox.astro

352 lines
9.7 KiB
Text
Raw Normal View History

---
import { getCollection } from 'astro:content';
import { t, type Locale } from '../../utils/i18n';
interface Props {
images: { src: string; alt: string; title?: string }[];
albumTitle?: string;
showCategory?: boolean;
category?: string;
lang?: Locale;
}
const { images, albumTitle = '', showCategory = false, category = '', lang = 'fr' } = Astro.props;
const imagesForJS = JSON.stringify(images);
// Construire les labels depuis la collection filtrée par langue
const allCategories = await getCollection('photoCategories');
const langCategories = allCategories.filter(c => (c.data.lang ?? 'fr') === lang);
const effectiveCategories = langCategories.length > 0 ? langCategories : allCategories.filter(c => (c.data.lang ?? 'fr') === 'fr');
const categoryLabels: Record<string, string> = {
'blog': t('photo', 'photoFeed', lang),
...Object.fromEntries(effectiveCategories.map(cat => [cat.id.replace(/\.(en|ar)$/, ''), cat.data.title]))
};
---
<div id="lightbox" class="lightbox hidden">
<div class="lightbox-controls">
<button class="lightbox-fullscreen" aria-label={t('photo', 'fullscreen', lang)}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button class="lightbox-close" aria-label={t('photo', 'close', lang)}>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
<path d="M18 6L6 18M6 6l12 12" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<button class="lightbox-prev" aria-label={t('photo', 'previousImage', lang)}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M15 18L9 12L15 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button class="lightbox-next" aria-label={t('photo', 'nextImage', lang)}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M9 18L15 12L9 6" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="lightbox-image-container">
<img id="lightbox-image" class="lightbox-image" src="" alt="" />
</div>
<div class="lightbox-bottom-bar">
<div class="lightbox-info">
<span id="lightbox-title" class="info-title"></span>
{showCategory && (
<>
<span class="info-separator">•</span>
<span id="lightbox-category" class="info-category"></span>
</>
)}
<span class="info-separator">•</span>
<span id="lightbox-counter" class="info-counter"></span>
</div>
</div>
</div>
<script is:inline define:vars={{ imagesForJS, albumTitle, showCategory, category, categoryLabels }}>
window.lightboxData = {
images: JSON.parse(imagesForJS),
albumTitle,
showCategory,
category,
categoryLabels
};
</script>
<script>
2026-01-07 03:03:42 +01:00
declare global {
interface Window {
lightboxData?: {
images: { src: string; alt: string; title?: string; category?: string }[];
albumTitle: string;
showCategory: boolean;
category: string;
categoryLabels: Record<string, string>;
};
}
}
class Lightbox {
2026-01-07 03:03:42 +01:00
images: { src: string; alt: string; title?: string; category?: string }[] = [];
currentIndex = 0;
lightbox: HTMLElement | null;
lightboxImage: HTMLImageElement | null;
lightboxTitle: HTMLElement | null;
lightboxCategory: HTMLElement | null;
lightboxCounter: HTMLElement | null;
albumTitle = '';
showCategory = false;
category = '';
categoryLabels: Record<string, string> = {};
constructor() {
this.lightbox = document.getElementById('lightbox');
2026-01-07 03:03:42 +01:00
this.lightboxImage = document.getElementById('lightbox-image') as HTMLImageElement | null;
this.lightboxTitle = document.getElementById('lightbox-title');
this.lightboxCategory = document.getElementById('lightbox-category');
this.lightboxCounter = document.getElementById('lightbox-counter');
this.init();
}
init() {
if (window.lightboxData) {
this.images = window.lightboxData.images;
this.albumTitle = window.lightboxData.albumTitle;
this.showCategory = window.lightboxData.showCategory;
this.category = window.lightboxData.category;
this.categoryLabels = window.lightboxData.categoryLabels;
}
this.bindEvents();
}
bindEvents() {
// Clic sur les éléments de galerie (thumbnails ou gallery-items)
document.querySelectorAll('.thumbnail-link, .gallery-item').forEach((item, index) => {
item.addEventListener('click', (e) => {
e.preventDefault();
this.openLightbox(index);
});
});
document.querySelector('.lightbox-close')?.addEventListener('click', () => this.closeLightbox());
document.querySelector('.lightbox-fullscreen')?.addEventListener('click', () => this.toggleFullscreen());
document.querySelector('.lightbox-prev')?.addEventListener('click', () => this.prevImage());
document.querySelector('.lightbox-next')?.addEventListener('click', () => this.nextImage());
// Fermer en cliquant sur le fond
this.lightbox?.addEventListener('click', (e) => {
if (e.target === this.lightbox || (e.target as HTMLElement).classList.contains('lightbox-image-container')) {
this.closeLightbox();
}
});
document.addEventListener('keydown', (e) => {
if (!this.lightbox?.classList.contains('hidden')) {
switch(e.key) {
case 'Escape': this.closeLightbox(); break;
case 'ArrowLeft': this.prevImage(); break;
case 'ArrowRight': this.nextImage(); break;
}
}
});
}
openLightbox(index: number) {
this.currentIndex = index;
this.updateLightboxContent();
this.lightbox?.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
closeLightbox() {
this.lightbox?.classList.add('hidden');
document.body.style.overflow = '';
}
updateLightboxContent() {
const image = this.images[this.currentIndex];
if (image && this.lightboxImage) {
this.lightboxImage.src = image.src;
this.lightboxImage.alt = image.alt;
// Titre: soit le titre de l'image, soit le titre de l'album
if (this.lightboxTitle) {
this.lightboxTitle.textContent = image.title || this.albumTitle;
}
// Catégorie (optionnel)
if (this.showCategory && this.lightboxCategory) {
const cat = image.category || this.category;
this.lightboxCategory.textContent = this.categoryLabels[cat] || cat;
}
// Compteur
if (this.lightboxCounter) {
this.lightboxCounter.textContent = `${this.currentIndex + 1} / ${this.images.length}`;
}
}
}
nextImage() {
this.currentIndex = (this.currentIndex + 1) % this.images.length;
this.updateLightboxContent();
}
prevImage() {
this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
this.updateLightboxContent();
}
toggleFullscreen() {
const fullscreenBtn = document.querySelector('.lightbox-fullscreen svg');
if (!document.fullscreenElement) {
this.lightbox?.requestFullscreen?.();
if (fullscreenBtn) {
fullscreenBtn.innerHTML = '<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
}
} else {
document.exitFullscreen?.();
if (fullscreenBtn) {
fullscreenBtn.innerHTML = '<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>';
}
}
}
}
document.addEventListener('DOMContentLoaded', () => new Lightbox());
</script>
<style>
.lightbox {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
z-index: 9999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.lightbox.hidden {
display: none;
}
.lightbox-image-container {
position: relative;
width: 100%;
height: calc(100% - 50px);
display: flex;
align-items: center;
justify-content: center;
padding: 0;
cursor: pointer;
}
.lightbox-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
display: block;
cursor: default;
}
.lightbox-controls {
position: absolute;
top: 20px;
right: 20px;
display: flex;
align-items: center;
gap: 15px;
z-index: 10001;
}
.lightbox-fullscreen,
.lightbox-close {
background: none;
border: none;
color: white;
cursor: pointer;
opacity: 0.8;
transition: opacity 0.3s;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
}
.lightbox-fullscreen:hover,
.lightbox-close:hover {
opacity: 1;
}
.lightbox-prev,
.lightbox-next {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.3);
border: none;
color: white;
cursor: pointer;
padding: 30px 20px;
z-index: 10000;
transition: background 0.3s;
}
.lightbox-prev:hover,
.lightbox-next:hover {
background: rgba(0, 0, 0, 0.5);
}
.lightbox-prev {
left: 0;
}
.lightbox-next {
right: 0;
}
.lightbox-prev svg,
.lightbox-next svg {
width: 30px;
height: 30px;
}
.lightbox-bottom-bar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 50px;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
padding: 0 20px;
color: white;
}
.lightbox-info {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
}
.info-separator {
opacity: 0.5;
}
</style>