diff options
| author | schererleander <leander@schererleander.de> | 2025-05-30 01:01:17 +0200 |
|---|---|---|
| committer | schererleander <leander@schererleander.de> | 2025-05-30 01:01:17 +0200 |
| commit | afdc982863b6cca573f1db58e1795aa8c45fabca (patch) | |
| tree | 6b94d2ffdcb0e1b5ccbaf584c825763ab72ab99d /src/components/ImageGalleryGrid.tsx | |
| parent | 8f2c8393510dfefc22871661b0ef9964569e290b (diff) | |
rewrite site
Diffstat (limited to 'src/components/ImageGalleryGrid.tsx')
| -rw-r--r-- | src/components/ImageGalleryGrid.tsx | 96 |
1 files changed, 96 insertions, 0 deletions
diff --git a/src/components/ImageGalleryGrid.tsx b/src/components/ImageGalleryGrid.tsx new file mode 100644 index 0000000..fc651bf --- /dev/null +++ b/src/components/ImageGalleryGrid.tsx @@ -0,0 +1,96 @@ +import React, { useState, useEffect } from 'react'; + +interface Props { + images: Array<{ + src: string; + alt?: string; + id?: string | number; + }>; +} + +export default function ImageGalleryGrid({ images }: Props) { + const [selectedIndex, setSelectedIndex] = useState<number | null>(null); + const closeModal = () => setSelectedIndex(null); + const showPrev = (e: React.MouseEvent) => { + e.stopPropagation(); + setSelectedIndex((prev) => (prev !== null && prev > 0 ? prev - 1 : prev)); + }; + const showNext = (e: React.MouseEvent) => { + e.stopPropagation(); + setSelectedIndex((prev) => + prev !== null && prev < images.length - 1 ? prev + 1 : prev + ); + }; + + useEffect(() => { + function handleKeyDown(event: KeyboardEvent) { + if (selectedIndex === null) return; + if (event.key === 'Escape') { + closeModal; + } else if (event.key === 'ArrowLeft') { + setSelectedIndex((prev) => (prev && prev > 0 ? prev - 1 : prev)); + } else if (event.key === 'ArrowRight') { + setSelectedIndex((prev) => + prev !== null && prev < images.length - 1 ? prev + 1 : prev + ); + } + } + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [selectedIndex, images.length]); + + return ( + <> + <div className="grid gap-2 grid-cols-4 p-4"> + {images.map((image, idx) => ( + <div + key={image.id ?? image.src} + className="relative group overflow-hidden rounded-xl shadow-lg cursor-pointer aspect-square" + onClick={() => setSelectedIndex(idx)} + > + <img + src={image.src} + alt={image.alt || 'Gallery image'} + className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" + /> + </div> + ))} + </div> + + {/* Overlay */} + {selectedIndex !== null && ( + <div + className="fixed inset-0 bg-white/50 dark:bg-black/50 backdrop-blur-sm flex items-center justify-center z-50" + onClick={closeModal} + role="dialog" + aria-modal="true" + > + {/* Prev arrow */} + <button + onClick={showPrev} + className="absolute left-4 text-black dark:text-white text-4xl focus:outline-none cursor-pointer" + disabled={selectedIndex === 0} + > + ← + </button> + + <img + src={images[selectedIndex].src} + alt={images[selectedIndex].alt || 'Enlarged gallery'} + className="w-1/2 h-1/2 object-contain rounded-lg" + /> + + {/* Next arrow */} + <button + onClick={showNext} + className="absolute right-4 text-black dark:text-white text-4xl focus:outline-none cursor-pointer" + disabled={selectedIndex === images.length - 1} + > + → + </button> + </div> + )} + </> + ); +}
\ No newline at end of file |
