import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; import { LucideAngularModule, X, Image as ImageIcon, ChevronLeft, ChevronRight } from 'lucide-angular'; import { TranslatePipe } from '@ngx-translate/core'; import { ImageService } from '../../services/image.service'; import { Image } from '../../services/image.model'; import { ImageLayout } from '../../services/template.model'; import { ImageUploaderComponent } from '../image-uploader/image-uploader.component'; /** * Composant reutilisable de galerie d'images. * * Deux modes : * - editable=false (defaut) : affichage en grille, clic sur une image ouvre * un lightbox plein ecran pour zoomer. * - editable=true : bouton "+ ajouter" en fin de grille (via app-image-uploader), * chaque vignette a un bouton "X" pour la retirer. * * La galerie raisonne sur une liste d'IDs d'images (string[]). Elle ne stocke * pas les objets Image eux-memes : les thumbs utilisent `imageService.contentUrl(id)` * directement comme src. Le navigateur cache les binaires via Cache-Control immutable * pose par le backend, donc aucune requete redondante. * * Usage : * * */ @Component({ selector: 'app-image-gallery', imports: [LucideAngularModule, ImageUploaderComponent, TranslatePipe], templateUrl: './image-gallery.component.html', styleUrls: ['./image-gallery.component.scss'] }) export class ImageGalleryComponent { readonly X = X; readonly ImageIcon = ImageIcon; readonly ChevronLeft = ChevronLeft; readonly ChevronRight = ChevronRight; /** IDs d'images a afficher. */ @Input() imageIds: string[] = []; /** Mode edition : afficher le bouton d'ajout + les boutons de suppression. */ @Input() editable = false; /** * Variante de mise en page. Null/undefined = GALLERY (rendu historique). * HERO : premiere image en banniere pleine largeur, suivantes en petit dessous. * MASONRY : mosaique a hauteurs variables. * CAROUSEL : defilement horizontal avec fleches. */ @Input() layout: ImageLayout | null | undefined = 'GALLERY'; /** Emet la nouvelle liste quand l'utilisateur ajoute/retire une image. */ @Output() imageIdsChange = new EventEmitter(); /** ID de l'image actuellement ouverte en lightbox (null = ferme). */ lightboxId: string | null = null; @ViewChild('carouselTrack') carouselTrack?: ElementRef; constructor(private imageService: ImageService) {} /** Layout effectif (null → GALLERY). */ get effectiveLayout(): ImageLayout { return this.layout ?? 'GALLERY'; } /** Premiere image (pour le layout HERO). */ get heroId(): string | null { return this.imageIds[0] ?? null; } /** Images restantes apres la hero (pour le layout HERO). */ get restIds(): string[] { return this.imageIds.slice(1); } scrollCarousel(direction: -1 | 1): void { const el = this.carouselTrack?.nativeElement; if (!el) return; el.scrollBy({ left: direction * Math.max(240, el.clientWidth * 0.8), behavior: 'smooth' }); } /** URL absolue du binaire d'une image. */ urlFor(id: string): string { return this.imageService.contentUrl(id); } onUploaded(image: Image): void { this.imageIdsChange.emit([...this.imageIds, image.id]); } remove(id: string, event: MouseEvent): void { event.stopPropagation(); // Evite d'ouvrir le lightbox en cliquant sur X. // On supprime aussi cote serveur pour ne pas laisser d'image orpheline. // Best-effort : on n'attend pas le retour pour emettre la nouvelle liste. this.imageService.delete(id).subscribe({ error: () => { /* best-effort : erreur ignorée volontairement */ } }); this.imageIdsChange.emit(this.imageIds.filter(i => i !== id)); } openLightbox(id: string): void { this.lightboxId = id; } closeLightbox(): void { this.lightboxId = null; } }