All files / src/components/public/EventPhotos ClickablePhotoMarquee.tsx

86.66% Statements 13/15
72.72% Branches 8/11
83.33% Functions 5/6
85.71% Lines 12/14

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                              2x               3x 3x 3x   3x       3x 1x 1x         3x 9x     1x                                 3x                            
/**
 * ClickablePhotoMarquee — an auto-scrolling photo strip whose photos open a zoomable, swipeable
 * lightbox on click. Shared by the event-page photo strip ({@link EventPhotosMarquee}) and the
 * homepage's "recent event photos" row (TestimonialSection).
 *
 * Tailwind-only (public bundle boundary). The lightbox is lazy-loaded — its chunk (and the YARL
 * CSS) only load the first time a photo is opened. Renders only the inner marquee + lightbox (no
 * outer <section>), so callers control the surrounding layout / full-bleed wrapper.
 */
import { lazy, Suspense, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { InfiniteMarquee } from '@/components/public/Testimonials/InfiniteMarquee';
import { buildCdnImageUrl } from '@/utils/cdnImage';
import type { PhotoLike } from './PhotoLightbox';
 
const PhotoLightbox = lazy(() => import('./PhotoLightbox'));
 
interface ClickablePhotoMarqueeProps {
  photos: PhotoLike[];
  direction?: 'left' | 'right';
}
 
export function ClickablePhotoMarquee({ photos, direction = 'left' }: ClickablePhotoMarqueeProps) {
  const { t } = useTranslation();
  const [open, setOpen] = useState(false);
  const [index, setIndex] = useState(0);
 
  Iif (!photos || photos.length === 0) {
    return null;
  }
 
  const openAt = (i: number) => {
    setIndex(i);
    setOpen(true);
  };
 
  // The marquee renders its children twice (seamless loop), so both copies of a given photo
  // share the same index → clicking either opens the correct slide.
  const thumbnails = photos.map((photo, i) => (
    <button
      key={photo.id}
      type="button"
      onClick={() => openAt(i)}
      aria-label={t('archive.photos.viewPhoto')}
      data-testid="marquee-photo"
      className="shrink-0 rounded-lg overflow-hidden focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 focus:ring-offset-zinc-900 cursor-zoom-in"
    >
      <img
        src={
          buildCdnImageUrl(photo.displayUrl, { w: 512, h: 384, fit: 'cover' }) ?? photo.displayUrl
        }
        alt={photo.filename || 'BATbern event photo'}
        loading="lazy"
        decoding="async"
        className="object-cover h-48 w-64 transition-transform duration-200 hover:scale-105"
      />
    </button>
  ));
 
  return (
    <>
      <InfiniteMarquee direction={direction} speed="slow">
        {thumbnails}
      </InfiniteMarquee>
 
      {open && (
        <Suspense fallback={null}>
          <PhotoLightbox photos={photos} index={index} open={open} onClose={() => setOpen(false)} />
        </Suspense>
      )}
    </>
  );
}