<div id="app"></div>
* {
  box-sizing: border-box;
  margin: 0;
}

body {
  font-family: sans-serif;
}

.wrapper {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100vh;
  width: 100vw;
}

.image-carousel {
  background-color: #000;
  height: 400px;
  overflow: hidden;
  width: min(600px, 100vh);
  position: relative;
}

.image-carousel__image {
  object-fit: contain;
  position: absolute;
  inset: 0;
  height: 100%;
  width: 100%;
  transition: transform 0.5s linear;
}

.image-carousel__image--displaced-left {
  transform: translateX(-100%);
}

.image-carousel__image--displaced-right {
  transform: translateX(100%);
}

.image-carousel__button {
  --size: 40px;
  height: var(--size);
  width: var(--size);

  background-color: grey;
  border-radius: 100%;
  border: none;
  color: #fff;
  position: absolute;
  cursor: pointer;
  transform: translateY(-50%);
  top: 50%;
}

.image-carousel__button:hover {
  background-color: lightgrey;
}

.image-carousel__button--prev {
  left: 16px;
}

.image-carousel__button--next {
  right: 16px;
}
/*
 * https://frontendeval.com/questions/image-carousel
 *
 * Build an auto-playing image carousel
 */

const useFetch = (url) => {
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);

  const acceptedExtension = "jpg";

  React.useEffect(() => {
    async function fetchData() {
      try {
        const response = await fetch(url);
        const result = await response.json();
        const data = result?.data?.children.flatMap((item) =>
          item?.data?.url.includes(acceptedExtension)
            ? [{ alt: item.data.title, src: item.data.url }]
            : []
        );
        setData(data);
      } catch (error) {
        setError(error);
      } finally {
        setLoading(false);
      }
    }

    fetchData();
  }, [url]);

  return { data, loading, error };
};

const clsx = (...classnames) => {
  return classnames.filter(Boolean).join(" ");
};

const shouldTransitionToLeftDirection = (currIndex, nextIndex, totalImages) => {
  if (currIndex === totalImages - 1 && nextIndex === 0) {
    return true;
  }

  if (currIndex === 0 && nextIndex === totalImages - 1) {
    return false;
  }

  return currIndex < nextIndex;
};

const ImageCarousel = ({ images }) => {
  const [currIndex, setCurrIndex] = React.useState(0);
  const [nextIndex, setNextIndex] = React.useState(null);
  const [isTransitioning, setIsTransitioning] = React.useState(false);
  const [autoplay, setAutoplay] = React.useState(false);

  const currImage = images[currIndex];
  const nextImage = nextIndex != null ? images[nextIndex] : null;

  const { exitClassname, enterClassname } =
    nextIndex != null &&
    shouldTransitionToLeftDirection(currIndex, nextIndex, images.length)
      ? {
          exitClassname: "image-carousel__image--displaced-left",
          enterClassname: "image-carousel__image--displaced-right"
        }
      : {
          exitClassname: "image-carousel__image--displaced-right",
          enterClassname: "image-carousel__image--displaced-left"
        };

  const changeImageIndex = (index) => {
    setNextIndex(index);

    requestAnimationFrame(() => {
      setIsTransitioning(true);
      setTimeout(() => setIsTransitioning(false), 3000);
    });
  };

  React.useEffect(() => {
    if (!autoplay || isTransitioning) return;

    const interval = setInterval(() => {
      const next = (currIndex + 1) % images.length;
      changeImageIndex(next);
    }, 3000);

    return () => clearInterval(interval);
  }, [autoplay, currIndex, isTransitioning]);

  return (
    <div
      className="image-carousel"
      onMouseEnter={() => setAutoplay(false)}
      onMouseLeave={() => setAutoplay(true)}
    >
      <img
        alt={currImage.alt}
        src={currImage.src}
        key={currImage.src}
        className={clsx(
          "image-carousel__image",
          isTransitioning && exitClassname
        )}
        onTransitionEnd={() => {
          if (nextIndex != null) {
            setCurrIndex(nextIndex);
            setNextIndex(null);
            setIsTransitioning(false);
          }
        }}
      />
      {nextImage != null && (
        <img
          alt={nextImage.alt}
          src={nextImage.src}
          key={nextImage.src}
          onTransitionEnd={() => {
            setCurrIndex(nextIndex != null ? nextIndex : null);
            setNextIndex(null);
            setIsTransitioning(false);
          }}
          className={clsx(
            "image-carousel__image",
            !isTransitioning && enterClassname
          )}
        />
      )}
      <button
        aria-label="Previous image"
        disabled={isTransitioning}
        className="image-carousel__button image-carousel__button--prev"
        onClick={() => {
          const nextIndex = (currIndex - 1 + images.length) % images.length;
          console.log('nextIndex', nextIndex);
          changeImageIndex(nextIndex);
        }}
      >
        &#10094;
      </button>
      <button
        aria-label="Next image"
        disabled={isTransitioning}
        className={clsx(
          "image-carousel__button",
          "image-carousel__button--next"
        )}
        onClick={() => {
          const nextIndex = (currIndex + 1) % images.length;
          changeImageIndex(nextIndex);
        }}
      >
        &#10095;
      </button>
    </div>
  );
};

const App = () => {
  const { data, loading, error } = useFetch(
    "https://www.reddit.com/r/aww/top/.json?t=all"
  );

  if (loading) {
    return <div>Loading...</div>;
  }

  if (error) {
    return <div>Error {error.message}</div>;
  }

  return (
    <div className="wrapper">
      <ImageCarousel images={data} />
    </div>
  );
};

ReactDOM.render(<App />, document.getElementById("app"));
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js