'use client'; /* eslint-disable jsx-a11y/alt-text */ import { BLUR_ENABLED } from '@/app/config'; import { useAppState } from '@/state/AppState'; import { clsx} from 'clsx/lite'; import Image, { ImageProps } from 'next/image'; import { useCallback, useEffect, useRef, useState } from 'react'; export default function ImageWithFallback({ className, classNameImage = 'object-cover h-full', debug, priority, blurDataURL, blurCompatibilityLevel = 'low', ...props }: ImageProps & { blurCompatibilityLevel?: 'none' | 'low' | 'high' classNameImage?: string debug?: boolean }) { const { shouldDebugImageFallbacks, areAdminDebugToolsEnabled, } = useAppState(); const [wasCached, setWasCached] = useState(true); const [isLoading, setIsLoading] = useState(true); const [didError, setDidError] = useState(false); const onLoad = useCallback(() => setIsLoading(false), []); const onError = useCallback(() => setDidError(true), []); const [hideFallback, setHideFallback] = useState(false); const refImage = useRef(null); const refFallback = useRef(null); useEffect(() => { setWasCached( Boolean(refImage.current?.complete) && (refImage.current?.naturalWidth ?? 0) > 0, ); }, []); const shouldDebugFallback = areAdminDebugToolsEnabled && debug; const debugFallbackStyles = useCallback(() => { const stylesMap = refFallback.current?.computedStyleMap(); const styles = stylesMap ? Array.from(stylesMap.entries()).reduce((acc, [key, value]) => { acc[key] = value.toString(); return acc; }, {} as Record) : {}; const opacity = (stylesMap?.get('opacity') as CSSUnitValue)?.value; return { styles, opacity, classList: refFallback.current?.classList, }; }, []); const outerTimeout = useRef(undefined); const innerTimeout = useRef(undefined); useEffect(() => { if (!isLoading && !didError) { outerTimeout.current = setTimeout(() => { if (refFallback.current) { const fallbackOpacity = (refFallback .current .computedStyleMap() .get('opacity') as CSSUnitValue )?.value; // Address race condition where cached image is initially loaded // and fallback is still being shown at full opacity if (fallbackOpacity === 0) { // Image has loaded and fallback is already hidden setHideFallback(true); if (shouldDebugFallback) { console.log('Hide fallback: 01', debugFallbackStyles()); } } else { // Image has loaded but fallback is still visible // Delay hiding fallback to avoid abrupt transition innerTimeout.current = setTimeout(() =>{ setHideFallback(true); if (shouldDebugFallback) { console.log('Hide fallback: 02', debugFallbackStyles()); } }, 1000); } } }, 1000); return () => { clearTimeout(outerTimeout.current); clearTimeout(innerTimeout.current); }; } }, [ isLoading, didError, shouldDebugFallback, debugFallbackStyles, ]); const showFallback = !wasCached && !hideFallback; const getBlurClass = () => { switch (blurCompatibilityLevel) { case 'high': // Fix poorly blurred placeholder data generated on client return 'blur-[4px] @xs:blue-md scale-[1.05]'; case 'low': return 'blur-[2px] @xs:blue-md scale-[1.01]'; } }; return (
{(BLUR_ENABLED && blurDataURL) ? :
}
); }