diff --git a/apps/web/src/views/card/components/AttachmentUpload.tsx b/apps/web/src/views/card/components/AttachmentUpload.tsx new file mode 100644 index 00000000..ac76a81e --- /dev/null +++ b/apps/web/src/views/card/components/AttachmentUpload.tsx @@ -0,0 +1,113 @@ +import { t } from "@lingui/core/macro"; +import { useRef, useState } from "react"; +import { HiOutlinePaperClip } from "react-icons/hi"; + +import Button from "~/components/Button"; +import { usePopup } from "~/providers/popup"; +import { api } from "~/utils/api"; + +export function AttachmentUpload({ cardPublicId }: { cardPublicId: string }) { + const { showPopup } = usePopup(); + const utils = api.useUtils(); + const [uploading, setUploading] = useState(false); + const inputRef = useRef(null); + + const generateUploadUrl = api.attachment.generateUploadUrl.useMutation(); + const confirmAttachment = api.attachment.confirm.useMutation({ + onSuccess: async () => { + await utils.card.byId.invalidate({ cardPublicId }); + showPopup({ + header: t`Attachment uploaded`, + message: t`Your file has been uploaded successfully.`, + icon: "success", + }); + }, + onError: () => { + showPopup({ + header: t`Upload failed`, + message: t`Failed to upload attachment. Please try again.`, + icon: "error", + }); + }, + onSettled: () => { + setUploading(false); + }, + }); + + const handleFileSelect = async ( + event: React.ChangeEvent, + ) => { + const file = event.target.files?.[0]; + if (!file) return; + + // Reset input + event.target.value = ""; + + setUploading(true); + + try { + // Generate presigned URL + const { url, key } = await generateUploadUrl.mutateAsync({ + cardPublicId, + filename: file.name, + contentType: file.type, + size: file.size, + }); + + // Upload file to S3 + const uploadResponse = await fetch(url, { + method: "PUT", + body: file, + headers: { + "Content-Type": file.type, + }, + }); + + if (!uploadResponse.ok) { + throw new Error("Upload failed"); + } + + // Confirm attachment in database + await confirmAttachment.mutateAsync({ + cardPublicId, + s3Key: key, + filename: file.name, + originalFilename: file.name, + contentType: file.type, + size: file.size, + }); + } catch { + showPopup({ + header: t`Upload failed`, + message: t`Failed to upload attachment. Please try again.`, + icon: "error", + }); + setUploading(false); + } + }; + + return ( +
+ +
+
+
+ ); +} diff --git a/apps/web/src/views/card/index.tsx b/apps/web/src/views/card/index.tsx index a5809ce1..eecb88ff 100644 --- a/apps/web/src/views/card/index.tsx +++ b/apps/web/src/views/card/index.tsx @@ -20,6 +20,7 @@ import { api } from "~/utils/api"; import { formatMemberDisplayName, getAvatarUrl } from "~/utils/helpers"; import { DeleteLabelConfirmation } from "../../components/DeleteLabelConfirmation"; import ActivityList from "./components/ActivityList"; +import { AttachmentUpload } from "./components/AttachmentUpload"; import Checklists from "./components/Checklists"; import { DeleteCardConfirmation } from "./components/DeleteCardConfirmation"; import { DeleteChecklistConfirmation } from "./components/DeleteChecklistConfirmation"; @@ -286,6 +287,11 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) { activeChecklistForm={activeChecklistForm} setActiveChecklistForm={setActiveChecklistForm} /> + {!isTemplate && ( +
+ +
+ )}

{t`Activity`}