diff options
| author | MohamedBassem <me@mbassem.com> | 2024-09-22 14:56:19 +0000 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-09-22 15:04:20 +0000 |
| commit | a770e55520245b7afc2b7a30aa6127eebcb6ea0d (patch) | |
| tree | 7a2e4041e2d16413ee0e8dd060be41b58253c995 /apps | |
| parent | 55f5c7f40d6569d0769a3b7a9060db5ec1d3b93b (diff) | |
| download | karakeep-a770e55520245b7afc2b7a30aa6127eebcb6ea0d.tar.zst | |
feature(web): Show attachments and allow users to manipulate them.
Diffstat (limited to 'apps')
4 files changed, 216 insertions, 3 deletions
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> + ); +} diff --git a/apps/web/components/dashboard/preview/BookmarkPreview.tsx b/apps/web/components/dashboard/preview/BookmarkPreview.tsx index 13d3c9d8..1f099725 100644 --- a/apps/web/components/dashboard/preview/BookmarkPreview.tsx +++ b/apps/web/components/dashboard/preview/BookmarkPreview.tsx @@ -25,6 +25,7 @@ import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; import ActionBar from "./ActionBar"; import { AssetContentSection } from "./AssetContentSection"; +import AttachmentBox from "./AttachmentBox"; import { EditableTitle } from "./EditableTitle"; import LinkContentSection from "./LinkContentSection"; import { NoteEditor } from "./NoteEditor"; @@ -153,6 +154,7 @@ export default function BookmarkPreview({ <p className="pt-2 text-sm text-gray-400">Note</p> <NoteEditor bookmark={bookmark} /> </div> + <AttachmentBox bookmark={bookmark} /> <ActionBar bookmark={bookmark} /> </div> </div> diff --git a/apps/web/components/dashboard/settings/ImportExport.tsx b/apps/web/components/dashboard/settings/ImportExport.tsx index 4827df93..a19db7fd 100644 --- a/apps/web/components/dashboard/settings/ImportExport.tsx +++ b/apps/web/components/dashboard/settings/ImportExport.tsx @@ -171,6 +171,7 @@ export function Import() { <div className="flex flex-col gap-3"> <div className="flex flex-row gap-2"> <FilePickerButton + loading={false} accept=".html" multiple={false} className="flex items-center gap-2" @@ -183,6 +184,7 @@ export function Import() { </FilePickerButton> <FilePickerButton + loading={false} accept=".html" multiple={false} className="flex items-center gap-2" diff --git a/apps/web/components/ui/file-picker-button.tsx b/apps/web/components/ui/file-picker-button.tsx index ccac1643..95e7bbcd 100644 --- a/apps/web/components/ui/file-picker-button.tsx +++ b/apps/web/components/ui/file-picker-button.tsx @@ -1,8 +1,8 @@ import React, { ChangeEvent, useRef } from "react"; -import { Button, ButtonProps } from "./button"; +import { ActionButton, ActionButtonProps } from "./action-button"; -interface FilePickerButtonProps extends Omit<ButtonProps, "onClick"> { +interface FilePickerButtonProps extends Omit<ActionButtonProps, "onClick"> { onFileSelect?: (file: File) => void; accept?: string; multiple?: boolean; @@ -35,7 +35,7 @@ const FilePickerButton: React.FC<FilePickerButtonProps> = ({ return ( <div> - <Button onClick={handleButtonClick} {...buttonProps} /> + <ActionButton onClick={handleButtonClick} {...buttonProps} /> <input type="file" ref={fileInputRef} |
