56 lines
1.5 KiB
TypeScript
56 lines
1.5 KiB
TypeScript
|
|
import { useState, useCallback, type CSSProperties, type ImgHTMLAttributes } from "react";
|
||
|
|
|
||
|
|
const FALLBACK_SRC = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='80' viewBox='0 0 80 80'%3E%3Crect fill='%23222' width='80' height='80' rx='8'/%3E%3Cpath d='M28 52l8-12 6 8 12-16 10 20H28z' fill='%23444'/%3E%3Ccircle cx='32' cy='32' r='5' fill='%23444'/%3E%3C/svg%3E";
|
||
|
|
|
||
|
|
const baseStyle: CSSProperties = {
|
||
|
|
transition: "opacity 0.3s ease",
|
||
|
|
};
|
||
|
|
|
||
|
|
interface OptimizedImageProps extends ImgHTMLAttributes<HTMLImageElement> {
|
||
|
|
fallbackSrc?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export default function OptimizedImage({
|
||
|
|
src,
|
||
|
|
alt = "",
|
||
|
|
fallbackSrc = FALLBACK_SRC,
|
||
|
|
style,
|
||
|
|
onLoad,
|
||
|
|
onError,
|
||
|
|
...rest
|
||
|
|
}: OptimizedImageProps) {
|
||
|
|
const [loaded, setLoaded] = useState(false);
|
||
|
|
const [errored, setErrored] = useState(false);
|
||
|
|
|
||
|
|
const handleLoad = useCallback(
|
||
|
|
(e: React.SyntheticEvent<HTMLImageElement>) => {
|
||
|
|
setLoaded(true);
|
||
|
|
onLoad?.(e);
|
||
|
|
},
|
||
|
|
[onLoad],
|
||
|
|
);
|
||
|
|
|
||
|
|
const handleError = useCallback(
|
||
|
|
(e: React.SyntheticEvent<HTMLImageElement>) => {
|
||
|
|
if (!errored) {
|
||
|
|
setErrored(true);
|
||
|
|
(e.target as HTMLImageElement).src = fallbackSrc;
|
||
|
|
}
|
||
|
|
onError?.(e);
|
||
|
|
},
|
||
|
|
[errored, fallbackSrc, onError],
|
||
|
|
);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<img
|
||
|
|
src={src}
|
||
|
|
alt={alt}
|
||
|
|
loading="lazy"
|
||
|
|
style={{ ...baseStyle, opacity: loaded || errored ? 1 : 0, ...style }}
|
||
|
|
onLoad={handleLoad}
|
||
|
|
onError={handleError}
|
||
|
|
{...rest}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
}
|