All files / src/components/common/ImageFallback index.tsx

100% Statements 65/65
100% Branches 18/18
100% Functions 4/4
100% Lines 65/65

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 1011x                 1x     1x     1x                           1x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x   14x 11x 14x 14x 14x   14x 14x 3x 3x 3x 14x   14x 11x 3x 3x 3x 11x 8x 8x   11x 14x   14x 3x 3x 3x   14x 4x   4x 4x 4x 3x 3x 2x 2x   4x 2x 2x 4x   14x 14x 14x 14x 14x 14x 14x 14x 14x 14x 14x   14x  
import {
  ImgHTMLAttributes,
  useState,
  useEffect,
  type SyntheticEvent,
  useRef,
} from 'react';
 
// Constants
import { BLUR_SRC, FALLBACK_SRC } from '@shared/constants';
 
// Types
import { Placeholder } from '@shared/types';
 
// Utils
import { combineClasses } from '@shared/utils';
 
type BaseImgProps = Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'>;
 
interface ImageProps extends BaseImgProps {
  src: string | Blob;
  alt: string;
  fallbackSrc?: string;
  blurDataURL?: string;
  placeholder?: Placeholder;
  width?: number;
  height?: number;
}
 
export const ImageFallback = (props: ImageProps) => {
  const {
    src,
    alt,
    fallbackSrc = FALLBACK_SRC.DEFAULT,
    blurDataURL = BLUR_SRC.DEFAULT,
    placeholder,
    width,
    height,
    ...rest
  } = props;
 
  const [imgSrc, setImgSrc] = useState<string>(() =>
    src instanceof Blob ? URL.createObjectURL(src) : src,
  );
  const objectUrlRef = useRef<string | null>(null);
  const shouldUseBlur = placeholder === Placeholder.Blur && blurDataURL;
 
  const revokeBlobUrl = () => {
    if (objectUrlRef.current) {
      URL.revokeObjectURL(objectUrlRef.current);
      objectUrlRef.current = null;
    }
  };
 
  useEffect(() => {
    if (src instanceof Blob) {
      const blobUrl = URL.createObjectURL(src);
      setImgSrc(blobUrl);
      objectUrlRef.current = blobUrl;
    } else {
      setImgSrc(src);
    }
 
    return revokeBlobUrl;
  }, [src]);
 
  const handleError = () => {
    revokeBlobUrl();
    setImgSrc(fallbackSrc);
  };
 
  const handleLoad = (e: SyntheticEvent<HTMLImageElement>) => {
    const img = e.currentTarget;
 
    const isBroken =
      !img.complete ||
      !img.naturalWidth ||
      !img.naturalHeight ||
      (objectUrlRef.current &&
        imgSrc === objectUrlRef.current &&
        (!img.width || !img.height));
 
    if (isBroken) {
      handleError();
    }
  };
 
  return (
    <img
      src={imgSrc}
      alt={alt}
      width={width}
      height={height}
      onLoad={handleLoad}
      onError={handleError}
      className={combineClasses(shouldUseBlur && 'blur-md', rest.className)}
      {...rest}
    />
  );
};