Files
plant-disease-id/src/components/ImageLightbox.tsx
2026-06-08 16:42:04 -04:00

144 lines
4.1 KiB
TypeScript

"use client";
import { useState, useEffect, useCallback } from "react";
interface ImageLightboxProps {
images: { src: string; alt: string }[];
initialIndex: number;
onClose: () => void;
}
export default function ImageLightbox({ images, initialIndex, onClose }: ImageLightboxProps) {
const [currentIndex, setCurrentIndex] = useState(
Math.max(0, Math.min(initialIndex, images.length - 1)),
);
const goTo = useCallback(
(i: number) => {
setCurrentIndex(Math.max(0, Math.min(i, images.length - 1)));
},
[images.length],
);
// Close on Escape key, navigate with arrows
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
if (e.key === "ArrowLeft") goTo(currentIndex - 1);
if (e.key === "ArrowRight") goTo(currentIndex + 1);
};
window.addEventListener("keydown", handleKey);
return () => window.removeEventListener("keydown", handleKey);
}, [onClose, currentIndex, goTo]);
// Prevent body scroll while open
useEffect(() => {
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = "";
};
}, []);
if (!images.length) return null;
const current = images[currentIndex];
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
role="dialog"
aria-modal="true"
aria-label="Image viewer"
>
{/* Faded backdrop */}
<div
className="absolute inset-0 bg-black/80 backdrop-blur-sm"
onClick={onClose}
aria-hidden="true"
/>
{/* Close button — top right */}
<button
type="button"
onClick={onClose}
className="absolute top-4 right-4 z-10 rounded-full p-2 text-white/70 hover:text-white transition-colors"
aria-label="Close image"
>
<svg
className="h-8 w-8"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
{/* Navigation — previous */}
{images.length > 1 && currentIndex > 0 && (
<button
type="button"
onClick={() => goTo(currentIndex - 1)}
className="absolute left-4 z-10 rounded-full p-2 text-white/70 hover:text-white transition-colors"
aria-label="Previous image"
>
<svg
className="h-8 w-8"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="15 18 9 12 15 6" />
</svg>
</button>
)}
{/* Navigation — next */}
{images.length > 1 && currentIndex < images.length - 1 && (
<button
type="button"
onClick={() => goTo(currentIndex + 1)}
className="absolute right-4 z-10 rounded-full p-2 text-white/70 hover:text-white transition-colors"
aria-label="Next image"
>
<svg
className="h-8 w-8"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="9 18 15 12 9 6" />
</svg>
</button>
)}
{/* Full image */}
<div className="relative z-0 max-w-[90vw] max-h-[85vh] flex flex-col items-center">
<img
src={current.src}
alt={current.alt}
className="max-w-full max-h-[80vh] object-contain rounded-lg shadow-2xl"
/>
<p className="mt-3 text-sm text-white/70 text-center max-w-lg">{current.alt}</p>
{/* Image counter */}
{images.length > 1 && (
<p className="mt-1 text-xs text-white/50">
{currentIndex + 1} / {images.length}
</p>
)}
</div>
</div>
);
}