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 415x 415x 415x 415x 415x 415x 415x 415x 415x 415x   415x 194x 415x 415x 415x   415x 197x 3x 3x 3x 197x   415x 194x 3x 3x 3x 194x 191x 191x   194x 415x   415x 3x 3x 3x   415x 4x   4x 4x 4x 3x 3x 2x 2x   4x 2x 2x 4x   415x 415x 415x 415x 415x 415x 415x 415x 415x 415x 415x   415x  
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}
    />
  );
};