Source

components/image.js

import React, { useState } from 'react'
import PropTypes from 'prop-types'
import { makeStyles } from '@material-ui/core/styles'
import Skeleton from '@material-ui/lab/Skeleton'
import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'
import { Typography } from '@material-ui/core'

const useStyles = makeStyles(
  theme => ({
    root: {
      width: '100%',
      backgroundColor: theme.palette.common.grey,
      position: 'relative',

      '& > *': {
        maxWidth: '100%',
        maxHeight: '100%',
      },
    },
    centered: {
      position: 'absolute !important',
      top: '50%',
      left: '50%',
      transform: 'translateX(-50%) translateY(-50%)',
    },
    static: {
      width: '100%',
      height: '100%',
    },
    hidden: {
      visibility: 'hidden',
    },
    error: {
      '& > *': {
        display: 'inline-block',
        verticalAlign: 'middle',
        marginLeft: '.5em',
      },
    },
  }),
  { name: 'image' }
)

/**
 * Generic image view component.
 *
 * @component
 */
const Image = ({
  src,
  ratioX,
  ratioY,
  alt,
  className,
  loadingPlaceholder,
  missingText,
  errorText,
  ...props
}) => {
  const [loaded, setLoaded] = useState(false)
  const [error, setError] = useState(false)
  const classes = useStyles()

  const ratio = (ratioY / ratioX) * 100

  const handleImageLoaded = () => {
    setLoaded(true)
    setError(false)
  }

  const handleImageError = () => {
    setLoaded(true)
    setError(true)
  }

  if (!src) {
    return (
      <div
        className={[classes.root, className].join(' ')}
        style={{ paddingBottom: `${ratio}%` }}
        {...props}
      >
        <div className={[classes.centered, classes.error].join(' ')}>
          <ErrorOutlineIcon />
          <Typography>{missingText}</Typography>
        </div>
      </div>
    )
  }

  return (
    <div
      className={[classes.root, className].join(' ')}
      style={{ paddingBottom: `${ratio}%` }}
      {...props}
    >
      <img
        src={src}
        alt={alt}
        className={[
          classes.centered,
          !loaded || error ? classes.hidden : undefined,
        ].join(' ')}
        onLoad={handleImageLoaded}
        onError={handleImageError}
      />
      {!loaded &&
        (loadingPlaceholder || (
          <Skeleton
            className={classes.centered}
            width="100%"
            height="100%"
            variant="rect"
            animation="wave"
          />
        ))}
      {loaded && error && (
        <div className={[classes.centered, classes.error].join(' ')}>
          <ErrorOutlineIcon />
          <Typography>{errorText}</Typography>
        </div>
      )}
    </div>
  )
}

Image.defaultProps = {
  missingText: 'missing.image',
  errorText: 'error.image',
}

Image.propTypes = {
  /** Target URL */
  src: PropTypes.string,
  /** Alternative text shown when image could not be loaded */
  alt: PropTypes.string,
  /** Ratio number for x direction */
  ratioX: PropTypes.number.isRequired,
  /** Ratio number for y direction */
  ratioY: PropTypes.number.isRequired,
  /** Placeholder shown when image is loading */
  loadingPlaceholder: PropTypes.node,
  /** Text displayed when no image is available. */
  missingText: PropTypes.string,
  /** Text displayed when the image could not be loaded. */
  errorText: PropTypes.string,
  /** @ignore */
  className: PropTypes.string,
}

export default Image