import React, { useCallback, useEffect, useState } from 'react'; import { X, ChevronLeft, ChevronRight, Download } from 'lucide-react'; import { cn } from '@/lib/utils'; /** * ImageViewerModalProps - Propriétés du composant ImageViewerModal * * @interface ImageViewerModalProps */ interface ImageViewerModalProps { /** * URL de l'image à afficher */ src: string; /** * Texte alternatif de l'image */ alt?: string; /** * Fonction appelée pour fermer le visualiseur */ onClose: () => void; /** * Fonction appelée pour passer à l'image suivante */ onNext?: () => void; /** * Fonction appelée pour passer à l'image précédente */ onPrev?: () => void; /** * Si `true`, affiche le bouton suivant */ hasNext?: boolean; /** * Si `true`, affiche le bouton précédent */ hasPrev?: boolean; /** * Index courant dans la galerie (1-based pour l'affichage) */ currentIndex?: number; /** * Nombre total d'images dans la galerie */ totalImages?: number; } /** * ImageViewerModal - Composant de visualisation d'image en plein écran * * Composant modal pour afficher une image en plein écran avec : * - Navigation entre images (précédent/suivant) via boutons et clavier (←/→) * - Téléchargement de l'image * - Fermeture avec bouton ou Escape * - Fond sombre avec backdrop blur * - Zoom toggle (clic sur l'image : fit ↔ natural size) * - Compteur d'images en mode galerie * - Skeleton de chargement pendant le téléchargement de l'image * * @example * ```tsx * // Visualiseur simple * setShowViewer(false)} * /> * ``` * * @example * ```tsx * // Avec navigation * setShowViewer(false)} * onNext={() => setCurrentIndex(i => i + 1)} * onPrev={() => setCurrentIndex(i => i - 1)} * hasNext={currentIndex < images.length - 1} * hasPrev={currentIndex > 0} * currentIndex={currentIndex + 1} * totalImages={images.length} * /> * ``` * * @component * @param {ImageViewerModalProps} props - Propriétés du composant * @returns {JSX.Element} Modal plein écran avec image et contrôles */ export const ImageViewerModal: React.FC = ({ src, alt, onClose, onNext, onPrev, hasNext, hasPrev, currentIndex, totalImages, }) => { const [zoomed, setZoomed] = useState(false); const [imageLoaded, setImageLoaded] = useState(false); // Reset zoom and loading state when image source changes useEffect(() => { setZoomed(false); setImageLoaded(false); }, [src]); // Keyboard navigation: Escape, ArrowLeft, ArrowRight const handleKeyDown = useCallback( (e: KeyboardEvent) => { switch (e.key) { case 'Escape': onClose(); break; case 'ArrowLeft': if (hasPrev && onPrev) onPrev(); break; case 'ArrowRight': if (hasNext && onNext) onNext(); break; } }, [onClose, onNext, onPrev, hasNext, hasPrev], ); useEffect(() => { document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [handleKeyDown]); const showCounter = currentIndex !== undefined && totalImages !== undefined && totalImages > 1; return (
{/* Toolbar */}
{alt || 'Image Preview'}
{/* Navigation */} {hasPrev && ( )} {hasNext && ( )} {/* Image with loading skeleton and zoom */}
{/* Loading skeleton */} {!imageLoaded && (
)} {alt} setImageLoaded(true)} onClick={() => setZoomed((z) => !z)} className={cn( 'shadow-2xl rounded-lg transition-all duration-200', imageLoaded ? 'opacity-100' : 'opacity-0', zoomed ? 'object-none cursor-zoom-out scale-150' : 'object-contain cursor-zoom-in max-w-full max-h-full', )} />
{/* Image counter */} {showCounter && ( {currentIndex} / {totalImages} )}
); };