From 303971424d2635e7c73f4c26a40714647aa34337 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Sun, 10 May 2026 15:11:33 +0800 Subject: [PATCH] feat: create ScreenshotLightbox component with zoom/pan and navigation (Task 1.1) --- components/ui/screenshot-lightbox.tsx | 127 ++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 components/ui/screenshot-lightbox.tsx diff --git a/components/ui/screenshot-lightbox.tsx b/components/ui/screenshot-lightbox.tsx new file mode 100644 index 0000000..47a0701 --- /dev/null +++ b/components/ui/screenshot-lightbox.tsx @@ -0,0 +1,127 @@ +// components/ui/screenshot-lightbox.tsx +"use client" + +import { useState, useCallback, useEffect } from "react" +import { AnimatePresence, motion } from "motion/react" +import { XIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react" + +interface ScreenshotLightboxProps { + screenshots: Array<{ id: string; url: string; width: number; height: number }> + initialIndex: number + onClose: () => void +} + +export function ScreenshotLightbox({ + screenshots, + initialIndex, + onClose, +}: ScreenshotLightboxProps) { + const [currentIndex, setCurrentIndex] = useState(initialIndex) + const [zoomed, setZoomed] = useState(false) + const hasMultiple = screenshots.length > 1 + + const goNext = useCallback(() => { + setZoomed(false) + setCurrentIndex((prev) => (prev < screenshots.length - 1 ? prev + 1 : prev)) + }, [screenshots.length]) + + const goPrev = useCallback(() => { + setZoomed(false) + setCurrentIndex((prev) => (prev > 0 ? prev - 1 : prev)) + }, []) + + const toggleZoom = useCallback(() => { + setZoomed((z) => !z) + }, []) + + // Keyboard handlers + useEffect(() => { + function handleKeyDown(e: KeyboardEvent) { + if (e.key === "Escape") { + onClose() + } else if (e.key === "ArrowRight" && hasMultiple) { + goNext() + } else if (e.key === "ArrowLeft" && hasMultiple) { + goPrev() + } + } + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + }, [onClose, goNext, goPrev, hasMultiple]) + + const current = screenshots[currentIndex] + if (!current) return null + + return ( + + { + if (e.target === e.currentTarget) onClose() + }} + > + {/* Close button */} + + + {/* Previous arrow */} + {hasMultiple && currentIndex > 0 && ( + + )} + + {/* Next arrow */} + {hasMultiple && currentIndex < screenshots.length - 1 && ( + + )} + + {/* Image container — scrollable for panning when zoomed */} +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {`Screenshot +
+ + {/* Counter */} + {hasMultiple && ( +
+ {currentIndex + 1} / {screenshots.length} +
+ )} +
+
+ ) +}