Files
f1r3wave-website/src/components/Gallery.tsx
T
janis b10a22442c
Build and push Docker image / build-and-push (push) Successful in 1m45s
Add minor UX improvements
2026-08-31 05:36:14 +02:00

188 lines
4.9 KiB
TypeScript

'use client';
import { ImageMeta } from '@/interfaces/image';
import styles from '@/styles/Gallery.module.scss';
import Image from 'next/image';
import { useCallback, useEffect, useState } from 'react';
interface GalleryProps {
initialImages: ImageMeta[];
}
const HORIZONTAL_ASPECT_RATIO = 1.8;
const VERTICAL_ASPECT_RATIO = 0.8;
export default function Gallery({
initialImages,
}: GalleryProps) {
const [fullScreenImageId, setFullScreenImageIdState] = useState<
string | null
>(null);
const setFullScreenImage = useCallback((imageId: string | null) => {
setFullScreenImageIdState(imageId);
const url = new URL(window.location.href);
if (imageId) {
url.searchParams.set('image', imageId);
} else {
url.searchParams.delete('image');
}
window.history.replaceState(
window.history.state,
'',
`${url.pathname}${url.search}${url.hash}`,
);
}, []);
useEffect(() => {
const synchronizeWithUrl = () => {
const params = new URLSearchParams(window.location.search);
setFullScreenImageIdState(params.get('image'));
};
synchronizeWithUrl();
window.addEventListener('popstate', synchronizeWithUrl);
return () => {
window.removeEventListener('popstate', synchronizeWithUrl);
};
}, []);
useEffect(() => {
if (!fullScreenImageId) {
return;
}
const previousOverflow = document.body.style.overflow;
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setFullScreenImage(null);
}
};
document.body.style.overflow = 'hidden';
window.addEventListener('keydown', handleKeyDown);
return () => {
document.body.style.overflow = previousOverflow;
window.removeEventListener('keydown', handleKeyDown);
};
}, [fullScreenImageId, setFullScreenImage]);
const currentImageIndex = fullScreenImageId
? initialImages.findIndex(
(image) => image.id === fullScreenImageId,
)
: -1;
const previousImage =
currentImageIndex > 0
? initialImages[currentImageIndex - 1]
: null;
const nextImage =
currentImageIndex >= 0 &&
currentImageIndex < initialImages.length - 1
? initialImages[currentImageIndex + 1]
: null;
return (
<div className={styles.gallery}>
<div className={styles.images}>
{initialImages.map((image) => {
const isHorizontal =
image.aspect_ratio > HORIZONTAL_ASPECT_RATIO;
const isVertical =
image.aspect_ratio < VERTICAL_ASPECT_RATIO;
return (
<div
key={image.id}
className={`${styles.imagesContainer} ${
isHorizontal
? styles.horizontal
: isVertical
? styles.vertical
: ''
}`}
>
<Image
src={`/api/images/${image.id}`}
alt={`Gallery image ${image.id}`}
width={isHorizontal ? image.width : 700}
height={isVertical ? image.height : 700}
loading="lazy"
onClick={() => setFullScreenImage(image.id)}
/>
</div>
);
})}
</div>
{fullScreenImageId && (
<div
className={styles.fullscreenModal}
role="dialog"
aria-modal="true"
aria-label="Fullscreen image"
onClick={() => setFullScreenImage(null)}
>
<Image
src={`/api/images/${fullScreenImageId}`}
alt="Fullscreen gallery image"
width={1920}
height={1080}
style={{ objectFit: 'contain' }}
onClick={(event) => event.stopPropagation()}
/>
<button
type="button"
className={styles.closeButton}
aria-label="Close fullscreen image"
onClick={(event) => {
event.stopPropagation();
setFullScreenImage(null);
}}
>
&times;
</button>
{previousImage && (
<button
type="button"
className={`${styles.arrowButton} ${styles.arrowButtonLeft}`}
aria-label="Previous image"
onClick={(event) => {
event.stopPropagation();
setFullScreenImage(previousImage.id);
}}
>
&#8249;
</button>
)}
{nextImage && (
<button
type="button"
className={`${styles.arrowButton} ${styles.arrowButtonRight}`}
aria-label="Next image"
onClick={(event) => {
event.stopPropagation();
setFullScreenImage(nextImage.id);
}}
>
&#8250;
</button>
)}
</div>
)}
</div>
);
}