All files / src/components/PhotosCarousel index.tsx

0% Statements 0/28
0% Branches 0/22
0% Functions 0/9
0% Lines 0/27

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>
  );
}