From b5ac2b4d45af0d14b83da7de5efd29877f57e46f Mon Sep 17 00:00:00 2001 From: Fadeedev Date: Tue, 20 Oct 2026 22:17:55 +0100 Subject: [PATCH] feat(gallery): analytics, keyboard nav, memoization, integration tests (#1256 #1257 #1258 #1259) --- docs/GALLERY.md | 24 ++ src/components/property/ImageGallery.tsx | 382 +++++++++++++----- src/components/property/ImageLightbox.tsx | 44 +- .../ImageGallery.integration.test.tsx | 99 +++++ src/lib/__tests__/galleryAnalytics.test.ts | 39 ++ src/lib/galleryAnalytics.ts | 54 +++ 6 files changed, 516 insertions(+), 126 deletions(-) create mode 100644 docs/GALLERY.md create mode 100644 src/components/property/__tests__/ImageGallery.integration.test.tsx create mode 100644 src/lib/__tests__/galleryAnalytics.test.ts create mode 100644 src/lib/galleryAnalytics.ts diff --git a/docs/GALLERY.md b/docs/GALLERY.md new file mode 100644 index 00000000..a4ffba40 --- /dev/null +++ b/docs/GALLERY.md @@ -0,0 +1,24 @@ +# Property Image Gallery + +Covers issues **#1256–#1259**. + +## Features + +| Issue | Behavior | +| --- | --- | +| #1259 Analytics | `trackGalleryEvent` → gtag + `dataLayer` (`gallery_view`, open/close/navigate, keyboard) | +| #1258 Keyboard | Arrow/Home/End focus movement; Enter/Space opens lightbox; lightbox already supports ←/→/Esc | +| #1257 Re-renders | `React.memo` on gallery shells; `useCallback` / `useMemo` for handlers and thumbnail list | +| #1256 Tests | `ImageGallery.integration.test.tsx`, `galleryAnalytics.test.ts` | + +## Usage + +```tsx +import { ImageGallery } from '@/components/property/ImageGallery'; + + +``` + +## Analytics payload + +Events include optional `propertyName`, `imageIndex`, `imageCount`, `direction`, `method`. diff --git a/src/components/property/ImageGallery.tsx b/src/components/property/ImageGallery.tsx index 54cbcf47..29e75dbd 100644 --- a/src/components/property/ImageGallery.tsx +++ b/src/components/property/ImageGallery.tsx @@ -1,11 +1,19 @@ 'use client'; -import React, { useState } from 'react'; +import React, { + useState, + useCallback, + useMemo, + memo, + useRef, + useEffect, +} from 'react'; import Image from 'next/image'; import { ImageLightbox } from './ImageLightbox'; import { ErrorBoundary } from '@/components/ui/SimpleErrorBoundary'; +import { trackGalleryEvent } from '@/lib/galleryAnalytics'; -interface ImageGalleryProps { +export interface ImageGalleryProps { images: string[]; propertyName: string; } @@ -15,130 +23,280 @@ interface ImageGalleryProps { * failure in either used to take the whole property page down with it. The * boundary below keeps the failure inside the gallery, where it degrades to a * retryable fallback. + * + * #1257 — memoized root + stable callbacks to limit re-renders. + * #1258 — keyboard navigation on the thumbnail strip and main image. + * #1259 — analytics events for open / navigate / close. */ -export const ImageGallery: React.FC = (props) => ( +export const ImageGallery: React.FC = memo((props) => ( -); +)); +ImageGallery.displayName = 'ImageGallery'; -const ImageGalleryContent: React.FC = ({ - images, - propertyName -}) => { - const [lightboxIndex, setLightboxIndex] = useState(null); - // Issue #1253 — the gallery had no loading state, so an image that was still - // being fetched left an empty grey box. The skeleton is drawn over the - // image until the browser reports it has loaded. - const [mainImageLoaded, setMainImageLoaded] = useState(false); +const ImageGalleryContent: React.FC = memo( + ({ images, propertyName }) => { + const [lightboxIndex, setLightboxIndex] = useState(null); + const [mainImageLoaded, setMainImageLoaded] = useState(false); + const [focusIndex, setFocusIndex] = useState(0); + const thumbRefs = useRef<(HTMLButtonElement | null)[]>([]); - const openLightbox = (index: number) => { - setLightboxIndex(index); - }; + const imageCount = images?.length ?? 0; + const thumbSources = useMemo( + () => (images && images.length > 1 ? images.slice(1, 5) : []), + [images], + ); - const closeLightbox = () => { - setLightboxIndex(null); - }; + useEffect(() => { + if (imageCount > 0) { + trackGalleryEvent('gallery_view', { + propertyName, + imageCount, + }); + } + }, [propertyName, imageCount]); - if (!images || images.length === 0) { - return ( -
-
- - - -

No images available

-
-
+ const openLightbox = useCallback( + (index: number, method: 'click' | 'keyboard' = 'click') => { + setLightboxIndex(index); + trackGalleryEvent('gallery_lightbox_open', { + propertyName, + imageIndex: index, + imageCount, + method, + }); + if (method === 'click' && index > 0) { + trackGalleryEvent('gallery_thumbnail_click', { + propertyName, + imageIndex: index, + imageCount, + }); + } + }, + [propertyName, imageCount], ); - } - return ( - <> -
- {/* Main Image */} -
- {propertyName} openLightbox(0)} - onLoad={() => setMainImageLoaded(true)} - /> + const closeLightbox = useCallback(() => { + if (lightboxIndex !== null) { + trackGalleryEvent('gallery_lightbox_close', { + propertyName, + imageIndex: lightboxIndex, + imageCount, + }); + } + setLightboxIndex(null); + }, [lightboxIndex, propertyName, imageCount]); - {/* Issue #1253 — loading skeleton, drawn over the image until it has - loaded. It is decorative, so it is hidden from assistive tech. */} - {!mainImageLoaded && ( -