From a770e55520245b7afc2b7a30aa6127eebcb6ea0d Mon Sep 17 00:00:00 2001
From: MohamedBassem <me@mbassem.com>
Date: Sun, 22 Sep 2024 14:56:19 +0000
Subject: feature(web): Show attachments and allow users to manipulate them.

---
 .../components/dashboard/preview/AttachmentBox.tsx | 209 +++++++++++++++++++++
 1 file changed, 209 insertions(+)
 create mode 100644 apps/web/components/dashboard/preview/AttachmentBox.tsx

(limited to 'apps/web/components/dashboard/preview/AttachmentBox.tsx')

diff --git a/apps/web/components/dashboard/preview/AttachmentBox.tsx b/apps/web/components/dashboard/preview/AttachmentBox.tsx
new file mode 100644
index 00000000..b2460165
--- /dev/null
+++ b/apps/web/components/dashboard/preview/AttachmentBox.tsx
@@ -0,0 +1,209 @@
+import Link from "next/link";
+import { ActionButton } from "@/components/ui/action-button";
+import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog";
+import { Button } from "@/components/ui/button";
+import {
+  Collapsible,
+  CollapsibleContent,
+  CollapsibleTrigger,
+} from "@/components/ui/collapsible";
+import FilePickerButton from "@/components/ui/file-picker-button";
+import { toast } from "@/components/ui/use-toast";
+import useUpload from "@/lib/hooks/upload-file";
+import {
+  Archive,
+  Camera,
+  ChevronsDownUp,
+  Download,
+  Image,
+  Pencil,
+  Plus,
+  Trash2,
+} from "lucide-react";
+
+import {
+  useAttachBookmarkAsset,
+  useDetachBookmarkAsset,
+  useReplaceBookmarkAsset,
+} from "@hoarder/shared-react/hooks/bookmarks";
+import { getAssetUrl } from "@hoarder/shared-react/utils/assetUtils";
+import { ZAssetType, ZBookmark } from "@hoarder/shared/types/bookmarks";
+import {
+  humanFriendlyNameForAssertType,
+  isAllowedToAttachAsset,
+} from "@hoarder/trpc/lib/attachments";
+
+export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) {
+  const typeToIcon: Record<ZAssetType, React.ReactNode> = {
+    screenshot: <Camera className="size-4" />,
+    fullPageArchive: <Archive className="size-4" />,
+    bannerImage: <Image className="size-4" />,
+  };
+
+  const { mutate: attachAsset, isPending: isAttaching } =
+    useAttachBookmarkAsset({
+      onSuccess: () => {
+        toast({
+          description: "Attachment has been attached!",
+        });
+      },
+      onError: (e) => {
+        toast({
+          description: e.message,
+          variant: "destructive",
+        });
+      },
+    });
+
+  const { mutate: replaceAsset, isPending: isReplacing } =
+    useReplaceBookmarkAsset({
+      onSuccess: () => {
+        toast({
+          description: "Attachment has been replaced!",
+        });
+      },
+      onError: (e) => {
+        toast({
+          description: e.message,
+          variant: "destructive",
+        });
+      },
+    });
+
+  const { mutate: detachAsset, isPending: isDetaching } =
+    useDetachBookmarkAsset({
+      onSuccess: () => {
+        toast({
+          description: "Attachment has been detached!",
+        });
+      },
+      onError: (e) => {
+        toast({
+          description: e.message,
+          variant: "destructive",
+        });
+      },
+    });
+
+  const { mutate: uploadAsset } = useUpload({
+    onError: (e) => {
+      toast({
+        description: e.error,
+        variant: "destructive",
+      });
+    },
+  });
+
+  if (!bookmark.assets.length) {
+    return null;
+  }
+  bookmark.assets.sort((a, b) => a.assetType.localeCompare(b.assetType));
+
+  return (
+    <Collapsible>
+      <CollapsibleTrigger className="flex w-full items-center justify-between gap-2 text-sm text-gray-400">
+        Attachments
+        <ChevronsDownUp className="size-4" />
+      </CollapsibleTrigger>
+      <CollapsibleContent className="flex flex-col gap-1 py-2 text-sm">
+        {bookmark.assets.map((asset) => (
+          <div key={asset.id} className="flex items-center justify-between">
+            <Link
+              target="_blank"
+              href={getAssetUrl(asset.id)}
+              className="flex items-center gap-1"
+            >
+              {typeToIcon[asset.assetType]}
+              <p>{humanFriendlyNameForAssertType(asset.assetType)}</p>
+            </Link>
+            <div className="flex gap-2">
+              <Link
+                title="Download"
+                target="_blank"
+                href={getAssetUrl(asset.id)}
+                className="flex items-center gap-1"
+                download={humanFriendlyNameForAssertType(asset.assetType)}
+              >
+                <Download className="size-4" />
+              </Link>
+              {isAllowedToAttachAsset(asset.assetType) && (
+                <FilePickerButton
+                  title="Replace"
+                  loading={isReplacing}
+                  accept=".jgp,.JPG,.jpeg,.png,.webp"
+                  multiple={false}
+                  variant="none"
+                  size="none"
+                  className="flex items-center gap-2"
+                  onFileSelect={(file) =>
+                    uploadAsset(file, {
+                      onSuccess: (resp) => {
+                        replaceAsset({
+                          bookmarkId: bookmark.id,
+                          oldAssetId: asset.id,
+                          newAssetId: resp.assetId,
+                        });
+                      },
+                    })
+                  }
+                >
+                  <Pencil className="size-4" />
+                </FilePickerButton>
+              )}
+              <ActionConfirmingDialog
+                title="Delete Attachment?"
+                description={`Are you sure you want to delete the attachment of the bookmark?`}
+                actionButton={(setDialogOpen) => (
+                  <ActionButton
+                    loading={isDetaching}
+                    variant="destructive"
+                    onClick={() =>
+                      detachAsset(
+                        { bookmarkId: bookmark.id, assetId: asset.id },
+                        { onSettled: () => setDialogOpen(false) },
+                      )
+                    }
+                  >
+                    <Trash2 className="mr-2 size-4" />
+                    Delete
+                  </ActionButton>
+                )}
+              >
+                <Button variant="none" size="none" title="Delete">
+                  <Trash2 className="size-4" />
+                </Button>
+              </ActionConfirmingDialog>
+            </div>
+          </div>
+        ))}
+        {!bookmark.assets.some((asset) => asset.assetType == "bannerImage") && (
+          <FilePickerButton
+            title="Attach a Banner"
+            loading={isAttaching}
+            accept=".jgp,.JPG,.jpeg,.png,.webp"
+            multiple={false}
+            variant="ghost"
+            size="none"
+            className="flex w-full items-center justify-center gap-2"
+            onFileSelect={(file) =>
+              uploadAsset(file, {
+                onSuccess: (resp) => {
+                  attachAsset({
+                    bookmarkId: bookmark.id,
+                    asset: {
+                      id: resp.assetId,
+                      assetType: "bannerImage",
+                    },
+                  });
+                },
+              })
+            }
+          >
+            <Plus className="size-4" />
+            Attach a Banner
+          </FilePickerButton>
+        )}
+      </CollapsibleContent>
+    </Collapsible>
+  );
+}
-- 
cgit v1.2.3-70-g09d2

