188 lines
4.9 KiB
TypeScript
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);
|
|
}}
|
|
>
|
|
×
|
|
</button>
|
|
|
|
{previousImage && (
|
|
<button
|
|
type="button"
|
|
className={`${styles.arrowButton} ${styles.arrowButtonLeft}`}
|
|
aria-label="Previous image"
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
setFullScreenImage(previousImage.id);
|
|
}}
|
|
>
|
|
‹
|
|
</button>
|
|
)}
|
|
|
|
{nextImage && (
|
|
<button
|
|
type="button"
|
|
className={`${styles.arrowButton} ${styles.arrowButtonRight}`}
|
|
aria-label="Next image"
|
|
onClick={(event) => {
|
|
event.stopPropagation();
|
|
setFullScreenImage(nextImage.id);
|
|
}}
|
|
>
|
|
›
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
} |