diff options
Diffstat (limited to 'apps/web/components')
5 files changed, 179 insertions, 1 deletions
diff --git a/apps/web/components/dashboard/UploadDropzone.tsx b/apps/web/components/dashboard/UploadDropzone.tsx new file mode 100644 index 00000000..61db8dc5 --- /dev/null +++ b/apps/web/components/dashboard/UploadDropzone.tsx @@ -0,0 +1,79 @@ +"use client"; + +import React, { useState } from "react"; +import { api } from "@/lib/trpc"; +import { useMutation } from "@tanstack/react-query"; +import DropZone from "react-dropzone"; + +import { + zUploadErrorSchema, + zUploadResponseSchema, +} from "@hoarder/trpc/types/uploads"; + +import { toast } from "../ui/use-toast"; + +export default function UploadDropzone({ + children, +}: { + children: React.ReactNode; +}) { + const invalidateAllBookmarks = + api.useUtils().bookmarks.getBookmarks.invalidate; + + const { mutate: createBookmark } = api.bookmarks.createBookmark.useMutation({ + onSuccess: () => { + toast({ description: "Bookmark uploaded" }); + invalidateAllBookmarks(); + }, + onError: () => { + toast({ description: "Something went wrong", variant: "destructive" }); + }, + }); + + const { mutate: uploadAsset } = useMutation({ + mutationFn: async (file: File) => { + const formData = new FormData(); + formData.append("image", file); + const resp = await fetch("/api/assets", { + method: "POST", + body: formData, + }); + if (!resp.ok) { + throw new Error(await resp.text()); + } + return zUploadResponseSchema.parse(await resp.json()); + }, + onSuccess: async (resp) => { + const assetId = resp.assetId; + createBookmark({ type: "asset", assetId, assetType: "image" }); + }, + onError: (error) => { + const err = zUploadErrorSchema.parse(JSON.parse(error.message)); + toast({ description: err.error, variant: "destructive" }); + }, + }); + + const [_isDragging, setDragging] = useState(false); + const onDrop = (acceptedFiles: File[]) => { + const file = acceptedFiles[0]; + setDragging(false); + uploadAsset(file); + }; + + return ( + <DropZone + multiple={false} + noClick + onDrop={onDrop} + onDragEnter={() => setDragging(true)} + onDragLeave={() => setDragging(false)} + > + {({ getRootProps, getInputProps }) => ( + <div {...getRootProps()}> + <input {...getInputProps()} hidden /> + {children} + </div> + )} + </DropZone> + ); +} diff --git a/apps/web/components/dashboard/bookmarks/AssetCard.tsx b/apps/web/components/dashboard/bookmarks/AssetCard.tsx new file mode 100644 index 00000000..460dbe98 --- /dev/null +++ b/apps/web/components/dashboard/bookmarks/AssetCard.tsx @@ -0,0 +1,76 @@ +"use client"; + +import Image from "next/image"; +import { isBookmarkStillTagging } from "@/lib/bookmarkUtils"; +import { api } from "@/lib/trpc"; +import { cn } from "@/lib/utils"; + +import type { ZBookmark } from "@hoarder/trpc/types/bookmarks"; + +import BookmarkActionBar from "./BookmarkActionBar"; +import TagList from "./TagList"; + +export default function AssetCard({ + bookmark: initialData, + className, +}: { + bookmark: ZBookmark; + className?: string; +}) { + const { data: bookmark } = api.bookmarks.getBookmark.useQuery( + { + bookmarkId: initialData.id, + }, + { + initialData, + refetchInterval: (query) => { + const data = query.state.data; + if (!data) { + return false; + } + if (isBookmarkStillTagging(data)) { + return 1000; + } + return false; + }, + }, + ); + const bookmarkedAsset = bookmark.content; + if (bookmarkedAsset.type != "asset") { + throw new Error("Unexpected bookmark type"); + } + + return ( + <div + className={cn( + className, + cn( + "flex h-min max-h-96 flex-col gap-y-1 overflow-hidden rounded-lg shadow-md", + ), + )} + > + {bookmarkedAsset.assetType == "image" && ( + <div className="relative h-56 max-h-56"> + <Image + alt="asset" + src={`/api/assets/${bookmarkedAsset.assetId}`} + fill={true} + className="rounded-t-lg object-cover" + /> + </div> + )} + <div className="flex flex-col gap-y-1 overflow-hidden p-2"> + <div className="flex h-full flex-wrap gap-1 overflow-hidden"> + <TagList + bookmark={bookmark} + loading={isBookmarkStillTagging(bookmark)} + /> + </div> + <div className="flex w-full justify-between"> + <div /> + <BookmarkActionBar bookmark={bookmark} /> + </div> + </div> + </div> + ); +} diff --git a/apps/web/components/dashboard/bookmarks/BookmarkPreview.tsx b/apps/web/components/dashboard/bookmarks/BookmarkPreview.tsx index 69aa60a3..4209192e 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkPreview.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkPreview.tsx @@ -1,5 +1,6 @@ "use client"; +import Image from "next/image"; import Link from "next/link"; import { BackButton } from "@/components/ui/back-button"; import { Skeleton } from "@/components/ui/skeleton"; @@ -70,6 +71,22 @@ export default function BookmarkPreview({ content = <Markdown className="prose">{bookmark.content.text}</Markdown>; break; } + case "asset": { + switch (bookmark.content.assetType) { + case "image": { + content = ( + <div className="relative w-full"> + <Image + alt="asset" + fill={true} + src={`/api/assets/${bookmark.content.assetId}`} + /> + </div> + ); + } + } + break; + } } return ( diff --git a/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx b/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx index b689a192..b40e6e42 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx @@ -14,6 +14,7 @@ import type { ZGetBookmarksResponse, } from "@hoarder/trpc/types/bookmarks"; +import AssetCard from "./AssetCard"; import EditorCard from "./EditorCard"; import LinkCard from "./LinkCard"; import TextCard from "./TextCard"; @@ -47,6 +48,9 @@ function renderBookmark(bookmark: ZBookmark) { case "text": comp = <TextCard bookmark={bookmark} />; break; + case "asset": + comp = <AssetCard bookmark={bookmark} />; + break; } return <BookmarkCard key={bookmark.id}>{comp}</BookmarkCard>; } diff --git a/apps/web/components/dashboard/bookmarks/EditorCard.tsx b/apps/web/components/dashboard/bookmarks/EditorCard.tsx index 28e8f41f..44c5889b 100644 --- a/apps/web/components/dashboard/bookmarks/EditorCard.tsx +++ b/apps/web/components/dashboard/bookmarks/EditorCard.tsx @@ -69,7 +69,9 @@ export default function EditorCard({ className }: { className?: string }) { <Textarea disabled={isPending} className="h-full w-full resize-none border-none text-lg focus-visible:ring-0" - placeholder={"Paste a link or write a note ..."} + placeholder={ + "Paste a link, write a note or drag and drop an image in here ..." + } onKeyDown={(e) => { if (e.key === "Enter" && e.metaKey) { form.handleSubmit(onSubmit, onError)(); |
