import { Image, ImageProps, Skeleton } from "antd"; import { CSSProperties, useCallback, useEffect, useRef, useState } from "react"; import styles from "./ImageWithFallback.module.css"; interface ImageWithFallbackProps extends ImageProps { fallbackSrc: string; borderRadius?: number | string; priority?: boolean; lazy?: boolean; placeholder?: "blur" | "empty"; blurDataURL?: string; width?: number | string; height?: number | string; loadingContainerStyle?: CSSProperties; } const ImageWithFallback = ({ fallbackSrc, src, borderRadius, style, alt, priority = false, lazy = true, width, height, loadingContainerStyle, ...rest }: ImageWithFallbackProps) => { const [imgSrc, setImgSrc] = useState(src || fallbackSrc); const [isLoading, setIsLoading] = useState(true); const [isInView, setIsInView] = useState(false); const [hasError, setHasError] = useState(false); const imageRef = useRef(null); // Intersection Observer for lazy loading useEffect(() => { if (lazy && !priority) { const observer = new IntersectionObserver( ([entry]) => { if (entry.isIntersecting) { setIsInView(true); observer.disconnect(); } }, { rootMargin: "50px", // Start loading 50px before the image comes into view threshold: 0.1, } ); if (imageRef.current) { observer.observe(imageRef.current); } return () => observer.disconnect(); } else { setIsInView(true); } }, [lazy, priority]); const handleError = useCallback(() => { setImgSrc(fallbackSrc); setHasError(true); setIsLoading(false); }, [fallbackSrc]); const handleLoad = useCallback(() => { setIsLoading(false); setHasError(false); }, []); // Combine custom style with borderRadius if provided const combinedStyle = { ...style, ...(borderRadius && { borderRadius }), }; // Don't render the image until it's in view (for lazy loading) if (lazy && !priority && !isInView) { return (
); } return (
{isLoading && !hasError && (
)} {alt
); }; export default ImageWithFallback;