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 101 102 103 | import React, { type JSX, useCallback, useEffect, useRef, useState } from 'react';
import Carousel from 'react-multi-carousel';
import 'react-multi-carousel/lib/styles.css';
import { Box, Typography } from '@mui/material';
import BusySpinner from '../BusySpinner';
import './styles.scss';
export type PhotoItem = {
url: string;
alt?: string;
decoration?: React.ReactNode;
};
export interface PhotosCarouselProps {
photos: PhotoItem[];
selectedSlide?: number;
onSlideChange?: (index: number) => void;
showArrows?: boolean;
numbered?: boolean;
dots?: boolean;
}
const responsive = {
mobile: {
breakpoint: { max: 4000, min: 0 },
items: 1,
},
};
export default function PhotosCarousel(props: PhotosCarouselProps): JSX.Element {
const { photos, selectedSlide, onSlideChange, showArrows, numbered, dots } = props;
const isControlled = selectedSlide !== undefined;
const [internalSlide, setInternalSlide] = useState(0);
const [isLoading, setIsLoading] = useState<boolean[]>([]);
const myCarousel = useRef<Carousel>(null);
const currentSlide = isControlled ? selectedSlide : internalSlide;
// Keyed on the URLs (not just photos.length) so swapping in a same-length array of
// different photos still resets the loading state instead of leaving stale flags.
const photoKey = photos.map((p) => p.url).join('|');
useEffect(() => {
setIsLoading(new Array(photos.length).fill(true));
}, [photoKey]);
useEffect(() => {
if (myCarousel.current && myCarousel.current.state.currentSlide !== currentSlide) {
myCarousel.current.goToSlide(currentSlide);
}
}, [currentSlide]);
const handleAfterChange = useCallback(() => {
const slide = myCarousel.current?.state.currentSlide ?? 0;
if (!isControlled) {
setInternalSlide(slide);
}
if (slide !== currentSlide) {
onSlideChange?.(slide);
}
}, [isControlled, currentSlide, onSlideChange]);
const finishLoading = (index: number) => {
setIsLoading((prev) => {
const next = [...prev];
next[index] = false;
return next;
});
};
return (
<Box
sx={{
'& .react-multi-carousel-list': {
paddingBottom: '20px',
},
}}
>
<Carousel
responsive={responsive}
ref={myCarousel}
showDots={dots ?? true}
arrows={showArrows ?? false}
ssr={true}
afterChange={handleAfterChange}
>
{photos.map((p, i) => (
<div key={`photo-${i}-container`} className='photos-carousel-container'>
{isLoading[i] ? <BusySpinner noBackground={true} /> : undefined}
<a href={p.url} target='_blank' rel='noopener noreferrer'>
<img className='photos-carousel-image' src={p.url} alt={p.alt} onLoad={() => finishLoading(i)} />
</a>
</div>
))}
</Carousel>
{numbered ? (
<Typography className='photo-numbering'>{`${currentSlide + 1}/${photos.length}`}</Typography>
) : undefined}
{photos[currentSlide] && photos[currentSlide].decoration}
</Box>
);
}
|