import { ChevronLeft, ChevronRight, RotateCcw, X, ZoomIn, ZoomOut } from "lucide-react"; import React, { useEffect, useMemo, useState } from "react"; import MotionPhotoPreview from "@/components/MotionPhotoPreview"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"; import { VisuallyHidden } from "@/components/ui/visually-hidden"; import useMediaQuery from "@/hooks/useMediaQuery"; import { cn } from "@/lib/utils"; import type { PreviewMediaItem } from "@/utils/media-item"; interface Props { open: boolean; onOpenChange: (open: boolean) => void; imgUrls?: string[]; items?: PreviewMediaItem[]; initialIndex?: number; } const MIN_ZOOM = 1; const MAX_ZOOM = 4; const ZOOM_STEP = 0.2; const DOUBLE_TAP_ZOOM = 2; const clampZoom = (scale: number) => Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, scale)); function PreviewImageDialog({ open, onOpenChange, imgUrls = [], items, initialIndex = 0 }: Props) { const sm = useMediaQuery("sm"); const [currentIndex, setCurrentIndex] = useState(initialIndex); const [zoomScale, setZoomScale] = useState(MIN_ZOOM); const previewItems = useMemo( () => items ?? imgUrls.map((url) => ({ id: url, kind: "image" as const, sourceUrl: url, posterUrl: url, filename: "Image" })), [imgUrls, items], ); useEffect(() => { if (open) { setCurrentIndex(initialIndex); } }, [initialIndex, open]); const itemCount = previewItems.length; const safeIndex = Math.max(0, Math.min(currentIndex, itemCount - 1)); const currentItem = previewItems[safeIndex]; const hasMultiple = itemCount > 1; const isImagePreview = currentItem?.kind === "image"; const canGoPrevious = safeIndex > 0; const canGoNext = safeIndex < itemCount - 1; const zoomPercent = Math.round(zoomScale * 100); const isZoomed = zoomScale > MIN_ZOOM; useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (!open) { return; } if (event.key === "Escape") { onOpenChange(false); return; } if (event.key === "ArrowLeft") { setCurrentIndex((prev) => Math.max(prev - 1, 0)); return; } if (event.key === "ArrowRight") { setCurrentIndex((prev) => Math.min(prev + 1, itemCount - 1)); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [itemCount, onOpenChange, open]); useEffect(() => { setZoomScale(MIN_ZOOM); }, [currentItem?.id, open]); const handleClose = () => onOpenChange(false); const handlePrevious = () => { setCurrentIndex((prev) => Math.max(prev - 1, 0)); }; const handleNext = () => { setCurrentIndex((prev) => Math.min(prev + 1, itemCount - 1)); }; const updateZoom = (nextScale: number) => { setZoomScale(clampZoom(nextScale)); }; const resetZoom = () => setZoomScale(MIN_ZOOM); const handleZoomIn = () => updateZoom(zoomScale + ZOOM_STEP); const handleZoomOut = () => updateZoom(zoomScale - ZOOM_STEP); const handleWheel = (event: React.WheelEvent) => { if (isImagePreview) { event.preventDefault(); updateZoom(zoomScale + (event.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP)); } }; const handleDoubleClick = () => setZoomScale((scale) => (scale === MIN_ZOOM ? DOUBLE_TAP_ZOOM : MIN_ZOOM)); if (!itemCount || !currentItem) { return null; } return ( {currentItem.filename || "Attachment preview"} Attachment preview dialog. Press Escape to close, use left or right arrow keys to switch items, and zoom images with the controls, mouse wheel, or double tap.
{currentItem.filename || "Attachment"}
{hasMultiple && (
{safeIndex + 1} / {itemCount}
)}
{ if (event.target === event.currentTarget && !isZoomed) { handleClose(); } }} >
event.stopPropagation()}> {currentItem.kind === "video" ? (
{isImagePreview && (
{hasMultiple && !sm && ( <>
{safeIndex + 1}/{itemCount}
)}
{zoomPercent}%
)} {hasMultiple && sm && ( <> } /> } /> )} {hasMultiple && !sm && !isImagePreview && (
{safeIndex + 1} / {itemCount}
)}
); } interface NavButtonProps { side: "left" | "right"; disabled: boolean; label: string; onClick: () => void; icon: React.ReactNode; } const NavButton = ({ side, disabled, label, onClick, icon }: NavButtonProps) => ( ); const ZoomButton = ({ disabled, label, onClick, children, }: { disabled?: boolean; label: string; onClick: () => void; children: React.ReactNode; }) => ( ); export default PreviewImageDialog;