Spaces:
Running
Running
import { useState, useRef, useEffect } from "react"; | |
import { cn } from "@/lib/utils"; | |
interface LazyImageProps { | |
src: string; | |
alt: string; | |
className?: string; | |
placeholder?: string; | |
onLoad?: () => void; | |
onError?: () => void; | |
} | |
export default function LazyImage({ | |
src, | |
alt, | |
className, | |
placeholder = "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0iI2YzZjRmNiIvPgo8dGV4dCB4PSI1MCUiIHk9IjUwJSIgZm9udC1zaXplPSIxOCIgZmlsbD0iIzlDQTNCRiIgZHk9Ii4zZW0iIHRleHQtYW5jaG9yPSJtaWRkbGUiPkxvYWRpbmcuLi48L3RleHQ+Cjwvc3ZnPg==", | |
onLoad, | |
onError | |
}: LazyImageProps) { | |
const [isLoaded, setIsLoaded] = useState(false); | |
const [isInView, setIsInView] = useState(false); | |
const [hasError, setHasError] = useState(false); | |
const imgRef = useRef<HTMLImageElement>(null); | |
useEffect(() => { | |
const observer = new IntersectionObserver( | |
([entry]) => { | |
if (entry.isIntersecting) { | |
setIsInView(true); | |
observer.disconnect(); | |
} | |
}, | |
{ threshold: 0.1, rootMargin: '50px' } | |
); | |
if (imgRef.current) { | |
observer.observe(imgRef.current); | |
} | |
return () => observer.disconnect(); | |
}, []); | |
const handleLoad = () => { | |
setIsLoaded(true); | |
onLoad?.(); | |
}; | |
const handleError = () => { | |
setHasError(true); | |
onError?.(); | |
}; | |
return ( | |
<div ref={imgRef} className={cn("overflow-hidden bg-gray-100", className)}> | |
<img | |
src={isInView ? src : placeholder} | |
alt={alt} | |
onLoad={handleLoad} | |
onError={handleError} | |
loading="lazy" | |
className={cn( | |
"transition-opacity duration-300 w-full h-full object-cover", | |
isLoaded ? "opacity-100" : "opacity-70", | |
hasError ? "opacity-50" : "" | |
)} | |
aria-label={alt} | |
/> | |
</div> | |
); | |
} |