import React, { useState, useRef, useCallback, useEffect } from 'react'; import { useIntersectionObserver } from '@/hooks/useIntersectionObserver'; interface OptimizedImageProps { src: string; alt: string; width?: number; height?: number; className?: string; placeholder?: string; blurDataURL?: string; priority?: boolean; quality?: number; sizes?: string; onLoad?: () => void; onError?: () => void; fallback?: React.ReactNode; } // Configuration des formats supportés const SUPPORTED_FORMATS = ['webp', 'avif', 'jpeg', 'png', 'gif']; const FALLBACK_FORMAT = 'jpeg'; // Générer les sources pour différents formats function generateImageSources(src: string, sizes?: string) { const baseUrl = src.replace(/\.[^/.]+$/, ''); return SUPPORTED_FORMATS.map(format => { const formatSrc = `${baseUrl}.${format}`; return { src: formatSrc, type: `image/${format}`, sizes: sizes || '100vw', }; }); } // Composant de placeholder avec blur function BlurPlaceholder({ blurDataURL, width, height, className }: { blurDataURL?: string; width?: number; height?: number; className?: string; }) { if (!blurDataURL) { return (
); } return ( ); } // Hook pour détecter le support des formats d'image function useImageFormatSupport() { const [supportedFormats, setSupportedFormats] = useState([]); useEffect(() => { const checkFormatSupport = async () => { const formats: string[] = []; // Test WebP const webpSupported = await new Promise((resolve) => { const webp = new Image(); webp.onload = webp.onerror = () => resolve(webp.height === 2); webp.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA'; }); // Test AVIF const avifSupported = await new Promise((resolve) => { const avif = new Image(); avif.onload = avif.onerror = () => resolve(avif.height === 2); avif.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAABcAAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAEAAAABAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQAMAAAAABNjb2xybmNseAACAAIABoAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAAB9tZGF0EgAKCBgABogQEAwgMgkAAAAAAAAG8AAAAA=='; }); if (webpSupported) formats.push('webp'); if (avifSupported) formats.push('avif'); formats.push('jpeg', 'png', 'gif'); // Formats de base toujours supportés setSupportedFormats(formats); }; checkFormatSupport(); }, []); return supportedFormats; } export function OptimizedImage({ src, alt, width, height, className = '', placeholder, blurDataURL, priority = false, quality: _quality = 75, sizes = '100vw', onLoad, onError, fallback, }: OptimizedImageProps) { const [isLoaded, setIsLoaded] = useState(false); const [hasError, setHasError] = useState(false); const [currentSrc, setCurrentSrc] = useState(null); const imgRef = useRef(null); const supportedFormats = useImageFormatSupport(); // Intersection Observer pour le lazy loading const { isIntersecting, ref: intersectionRef } = useIntersectionObserver({ threshold: 0.1, rootMargin: '50px', }); // Générer les sources optimisées const imageSources = React.useMemo(() => { return generateImageSources(src, sizes); }, [src, sizes]); // Sélectionner la meilleure source supportée const selectBestSource = useCallback(() => { const bestFormat = supportedFormats.find(format => SUPPORTED_FORMATS.includes(format) ) || FALLBACK_FORMAT; const bestSource = imageSources.find(source => source.type === `image/${bestFormat}` ); return bestSource?.src || src; }, [supportedFormats, imageSources, src]); // Charger l'image const loadImage = useCallback(() => { if (isLoaded || hasError) return; const imageSrc = selectBestSource(); setCurrentSrc(imageSrc); const img = new Image(); img.onload = () => { setIsLoaded(true); onLoad?.(); }; img.onerror = () => { setHasError(true); onError?.(); }; img.src = imageSrc; }, [isLoaded, hasError, selectBestSource, onLoad, onError]); // Charger l'image quand elle devient visible ou si priorité useEffect(() => { if (priority || isIntersecting) { loadImage(); } }, [priority, isIntersecting, loadImage]); // Gestion des erreurs de chargement const handleImageError = useCallback(() => { setHasError(true); onError?.(); }, [onError]); // Gestion du chargement réussi const handleImageLoad = useCallback(() => { setIsLoaded(true); onLoad?.(); }, [onLoad]); // Rendu du fallback en cas d'erreur if (hasError) { return fallback || (
Image non disponible
); } // Rendu du placeholder pendant le chargement if (!isLoaded && !priority) { return (
{placeholder && (
{placeholder}
)}
); } // Rendu de l'image optimisée return ( {/* Sources pour différents formats */} {imageSources.map((source, index) => ( ))} {/* Image principale */} {alt} ); } // Hook pour preloader des images export function useImagePreloader() { const preloadImage = useCallback((src: string) => { const img = new Image(); img.src = src; return new Promise((resolve, reject) => { img.onload = () => resolve(img); img.onerror = reject; }); }, []); const preloadImages = useCallback(async (srcs: string[]) => { const promises = srcs.map(src => preloadImage(src)); return Promise.allSettled(promises); }, [preloadImage]); return { preloadImage, preloadImages }; } // Composant pour les images responsives avec srcset export function ResponsiveImage({ src, alt, className = '', sizes = '(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw', ...props }: OptimizedImageProps & { sizes?: string }) { const [isLoaded, setIsLoaded] = useState(false); // Générer srcset pour différentes tailles const generateSrcSet = useCallback((baseSrc: string) => { const widths = [320, 640, 768, 1024, 1280, 1920]; return widths .map(width => `${baseSrc}?w=${width} ${width}w`) .join(', '); }, []); const srcSet = generateSrcSet(src); return ( setIsLoaded(true)} > {alt} ); }