diff --git a/apps/web/src/views/card/components/AttachmentThumbnails.tsx b/apps/web/src/views/card/components/AttachmentThumbnails.tsx index 34ae1dbf..f7608a5f 100644 --- a/apps/web/src/views/card/components/AttachmentThumbnails.tsx +++ b/apps/web/src/views/card/components/AttachmentThumbnails.tsx @@ -1,7 +1,17 @@ import Image from "next/image"; import { Dialog, Transition } from "@headlessui/react"; +import { t } from "@lingui/core/macro"; import { Fragment, useEffect, useState } from "react"; -import { HiChevronLeft, HiChevronRight, HiXMark } from "react-icons/hi2"; +import { + HiArrowDownTray, + HiChevronLeft, + HiChevronRight, + HiOutlineTrash, + HiXMark, +} from "react-icons/hi2"; + +import { usePopup } from "~/providers/popup"; +import { api } from "~/utils/api"; interface Attachment { publicId: string; @@ -13,9 +23,13 @@ interface Attachment { export function AttachmentThumbnails({ attachments, + cardPublicId, }: { attachments?: Attachment[]; + cardPublicId: string; }) { + const { showPopup } = usePopup(); + const utils = api.useUtils(); const imageAttachments = attachments?.filter( (attachment) => @@ -24,6 +38,38 @@ export function AttachmentThumbnails({ const [selectedIndex, setSelectedIndex] = useState(null); + const deleteAttachment = api.attachment.delete.useMutation({ + onMutate: async (args) => { + await utils.card.byId.cancel({ cardPublicId }); + const currentState = utils.card.byId.getData({ cardPublicId }); + + utils.card.byId.setData({ cardPublicId }, (oldCard) => { + if (!oldCard) return oldCard; + const updatedAttachments = oldCard.attachments.filter( + (attachment) => attachment.publicId !== args.attachmentPublicId, + ); + return { ...oldCard, attachments: updatedAttachments }; + }); + + return { previousState: currentState }; + }, + onError: (_error, _args, context) => { + utils.card.byId.setData({ cardPublicId }, context?.previousState); + showPopup({ + header: t`Unable to delete attachment`, + message: t`Please try again later, or contact customer support.`, + icon: "error", + }); + }, + onSuccess: () => { + // Close viewer if the deleted image was being viewed + setSelectedIndex(null); + }, + onSettled: async () => { + await utils.card.byId.invalidate({ cardPublicId }); + }, + }); + // Keyboard navigation useEffect(() => { if (selectedIndex === null) return; @@ -74,6 +120,21 @@ export function AttachmentThumbnails({ setSelectedIndex(newIndex); }; + const handleDownload = (attachment: Attachment) => { + if (!attachment.url) { + showPopup({ + header: t`Download failed`, + message: t`No download URL available for this attachment.`, + icon: "error", + }); + return; + } + + // Open the URL directly - the browser will handle the download + // if the S3 object has Content-Disposition: attachment header + window.open(attachment.url, "_blank", "noopener,noreferrer"); + }; + const selectedAttachment = selectedIndex !== null ? imageAttachments[selectedIndex] : null; @@ -97,7 +158,14 @@ export function AttachmentThumbnails({ - {}} static> + { + // Dialog closing is handled by the background overlay click + }} + static + >
{ + // Only close if clicking directly on the background, not on buttons + if (e.target === e.currentTarget) { + closeViewer(); + } + }} />
-
+
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + {selectedIndex !== null && selectedAttachment && ( + <> + + + + )} +
+ +
{imageAttachments.length > 1 && selectedIndex !== null && (