<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);
}}
>
❮
</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);
}}
>
❯
</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
This Pen doesn't use any external CSS resources.