diff --git a/web/src/components/MotionPhotoPreview.tsx b/web/src/components/MotionPhotoPreview.tsx index f3627b8ca..69b9510f9 100644 --- a/web/src/components/MotionPhotoPreview.tsx +++ b/web/src/components/MotionPhotoPreview.tsx @@ -41,10 +41,11 @@ const MotionPhotoPreview = ({ containerClassName={cn("max-w-full max-h-full", containerClassName)} mediaClassName={mediaClassName} /> - + ); }; diff --git a/web/src/components/PreviewImageDialog.tsx b/web/src/components/PreviewImageDialog.tsx index 3e6c8b459..303eb4c0d 100644 --- a/web/src/components/PreviewImageDialog.tsx +++ b/web/src/components/PreviewImageDialog.tsx @@ -1,8 +1,11 @@ -import { X } from "lucide-react"; -import React, { useEffect, useState } from "react"; +import { ChevronLeft, ChevronRight, X } from "lucide-react"; +import React, { useEffect, useMemo, useState } from "react"; import MotionPhotoPreview from "@/components/MotionPhotoPreview"; import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { Dialog, DialogContent, 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 { @@ -14,115 +17,216 @@ interface Props { } function PreviewImageDialog({ open, onOpenChange, imgUrls = [], items, initialIndex = 0 }: Props) { + const sm = useMediaQuery("sm"); const [currentIndex, setCurrentIndex] = useState(initialIndex); - const previewItems = - items ?? imgUrls.map((url) => ({ id: url, kind: "image" as const, sourceUrl: url, posterUrl: url, filename: "Image" })); + const previewItems = useMemo( + () => items ?? imgUrls.map((url) => ({ id: url, kind: "image" as const, sourceUrl: url, posterUrl: url, filename: "Image" })), + [imgUrls, items], + ); - // Update current index when initialIndex prop changes useEffect(() => { - setCurrentIndex(initialIndex); - }, [initialIndex]); + 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 canGoPrevious = safeIndex > 0; + const canGoNext = safeIndex < itemCount - 1; - // Handle keyboard navigation useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { - if (!open) return; + if (!open) { + return; + } - switch (event.key) { - case "Escape": - onOpenChange(false); - break; - case "ArrowRight": - setCurrentIndex((prev) => Math.min(prev + 1, previewItems.length - 1)); - break; - case "ArrowLeft": - setCurrentIndex((prev) => Math.max(prev - 1, 0)); - break; - default: - break; + 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); - }, [open, onOpenChange]); + }, [itemCount, onOpenChange, open]); - const handleClose = () => { - onOpenChange(false); - }; + if (!itemCount || !currentItem) { + return null; + } - const handleBackdropClick = (event: React.MouseEvent) => { - if (event.target === event.currentTarget) { - handleClose(); - } - }; - - // Return early if no images provided - if (!previewItems.length) return null; - - // Ensure currentIndex is within bounds - const safeIndex = Math.max(0, Math.min(currentIndex, previewItems.length - 1)); - const currentItem = previewItems[safeIndex]; + const handleClose = () => onOpenChange(false); + const handlePrevious = () => setCurrentIndex((prev) => Math.max(prev - 1, 0)); + const handleNext = () => setCurrentIndex((prev) => Math.min(prev + 1, itemCount - 1)); return ( - {/* Close button */} -
- + + {currentItem.filename || "Attachment preview"} + + +
+
+
+
{currentItem.filename || "Attachment"}
+ {hasMultiple && ( +
+ {safeIndex + 1} / {itemCount} +
+ )} +
+ + +
- {/* Image container */} -
- {currentItem.kind === "video" ? ( -
); } +interface NavButtonProps { + side: "left" | "right"; + disabled: boolean; + label: string; + onClick: () => void; + icon: React.ReactNode; +} + +const NavButton = ({ side, disabled, label, onClick, icon }: NavButtonProps) => ( + +); + export default PreviewImageDialog;