diff options
| author | MohamedBassem <me@mbassem.com> | 2024-04-07 18:30:00 +0100 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-04-07 19:00:00 +0100 |
| commit | 79d61be7e15dc5d23fb687a5f71e0097088a99ac (patch) | |
| tree | da72f19cdb74ef4ed2a75bcfddd13bdfb874f205 /apps/web/components/dashboard/preview | |
| parent | 44918316007ed3153dc802a4b11db3ea09024a8b (diff) | |
| download | karakeep-79d61be7e15dc5d23fb687a5f71e0097088a99ac.tar.zst | |
feature: Extract hook logic into separate package and add a new action bar in bookmark preview
Diffstat (limited to 'apps/web/components/dashboard/preview')
5 files changed, 381 insertions, 0 deletions
diff --git a/apps/web/components/dashboard/preview/ActionBar.tsx b/apps/web/components/dashboard/preview/ActionBar.tsx new file mode 100644 index 00000000..f2e3023e --- /dev/null +++ b/apps/web/components/dashboard/preview/ActionBar.tsx @@ -0,0 +1,115 @@ +import { ActionButton } from "@/components/ui/action-button"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { toast } from "@/components/ui/use-toast"; +import { Trash2 } from "lucide-react"; + +import type { ZBookmark } from "@hoarder/trpc/types/bookmarks"; +import { + useDeleteBookmark, + useUpdateBookmark, +} from "@hoarder/shared-react/hooks/bookmarks"; + +import { ArchivedActionIcon, FavouritedActionIcon } from "../bookmarks/icons"; + +export default function ActionBar({ bookmark }: { bookmark: ZBookmark }) { + const onError = () => { + toast({ + variant: "destructive", + title: "Something went wrong", + description: "There was a problem with your request.", + }); + }; + const { mutate: favBookmark, isPending: pendingFav } = useUpdateBookmark({ + onSuccess: () => { + toast({ + description: "The bookmark has been updated!", + }); + }, + onError, + }); + const { mutate: archiveBookmark, isPending: pendingArchive } = + useUpdateBookmark({ + onSuccess: (resp) => { + toast({ + description: `The bookmark has been ${resp.archived ? "Archived" : "Un-archived"}!`, + }); + }, + onError, + }); + const { mutate: deleteBookmark, isPending: pendingDeletion } = + useDeleteBookmark({ + onSuccess: () => { + toast({ + description: "The bookmark has been deleted!", + }); + }, + onError, + }); + + return ( + <TooltipProvider> + <div className="flex items-center justify-center gap-3"> + <Tooltip delayDuration={0}> + <TooltipTrigger> + <ActionButton + variant="none" + className="size-14 rounded-full bg-background" + loading={pendingFav} + onClick={() => { + favBookmark({ + bookmarkId: bookmark.id, + favourited: !bookmark.favourited, + }); + }} + > + <FavouritedActionIcon favourited={bookmark.favourited} /> + </ActionButton> + </TooltipTrigger> + <TooltipContent side="bottom"> + {bookmark.favourited ? "Un-favourite" : "Favourite"} + </TooltipContent> + </Tooltip> + <Tooltip delayDuration={0}> + <TooltipTrigger> + <ActionButton + variant="none" + loading={pendingArchive} + className="size-14 rounded-full bg-background" + onClick={() => { + archiveBookmark({ + bookmarkId: bookmark.id, + archived: !bookmark.archived, + }); + }} + > + <ArchivedActionIcon archived={bookmark.archived} /> + </ActionButton> + </TooltipTrigger> + <TooltipContent side="bottom"> + {bookmark.archived ? "Un-archive" : "Archive"} + </TooltipContent> + </Tooltip> + <Tooltip delayDuration={0}> + <TooltipTrigger> + <ActionButton + loading={pendingDeletion} + className="size-14 rounded-full bg-background" + variant="none" + onClick={() => { + deleteBookmark({ bookmarkId: bookmark.id }); + }} + > + <Trash2 /> + </ActionButton> + </TooltipTrigger> + <TooltipContent side="bottom">Delete</TooltipContent> + </Tooltip> + </div> + </TooltipProvider> + ); +} diff --git a/apps/web/components/dashboard/preview/AssetContentSection.tsx b/apps/web/components/dashboard/preview/AssetContentSection.tsx new file mode 100644 index 00000000..3fbbc519 --- /dev/null +++ b/apps/web/components/dashboard/preview/AssetContentSection.tsx @@ -0,0 +1,31 @@ +import Image from "next/image"; + +import type { ZBookmark } from "@hoarder/trpc/types/bookmarks"; + +export function AssetContentSection({ bookmark }: { bookmark: ZBookmark }) { + if (bookmark.content.type != "asset") { + throw new Error("Invalid content type"); + } + + let content; + switch (bookmark.content.assetType) { + case "image": { + switch (bookmark.content.assetType) { + case "image": { + content = ( + <div className="relative h-full min-w-full"> + <Image + alt="asset" + fill={true} + className="object-contain" + src={`/api/assets/${bookmark.content.assetId}`} + /> + </div> + ); + } + } + break; + } + } + return content; +} diff --git a/apps/web/components/dashboard/preview/BookmarkPreview.tsx b/apps/web/components/dashboard/preview/BookmarkPreview.tsx new file mode 100644 index 00000000..bd7881a3 --- /dev/null +++ b/apps/web/components/dashboard/preview/BookmarkPreview.tsx @@ -0,0 +1,154 @@ +"use client"; + +import Link from "next/link"; +import { TagsEditor } from "@/components/dashboard/bookmarks/TagsEditor"; +import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Tooltip, + TooltipContent, + TooltipPortal, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { + isBookmarkStillCrawling, + isBookmarkStillLoading, +} from "@/lib/bookmarkUtils"; +import { api } from "@/lib/trpc"; +import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { CalendarDays, ExternalLink } from "lucide-react"; + +import type { ZBookmark } from "@hoarder/trpc/types/bookmarks"; + +import ActionBar from "./ActionBar"; +import { AssetContentSection } from "./AssetContentSection"; +import { NoteEditor } from "./NoteEditor"; +import { TextContentSection } from "./TextContentSection"; + +dayjs.extend(relativeTime); + +function ContentLoading() { + return ( + <div className="flex w-full flex-col gap-2"> + <Skeleton className="h-4" /> + <Skeleton className="h-4" /> + <Skeleton className="h-4" /> + </div> + ); +} + +function CreationTime({ createdAt }: { createdAt: Date }) { + return ( + <TooltipProvider> + <Tooltip delayDuration={0}> + <TooltipTrigger asChild> + <span className="flex w-fit gap-2"> + <CalendarDays /> {dayjs(createdAt).fromNow()} + </span> + </TooltipTrigger> + <TooltipPortal> + <TooltipContent>{createdAt.toLocaleString()}</TooltipContent> + </TooltipPortal> + </Tooltip> + </TooltipProvider> + ); +} + +function LinkHeader({ bookmark }: { bookmark: ZBookmark }) { + if (bookmark.content.type !== "link") { + throw new Error("Unexpected content type"); + } + + const title = bookmark.content.title ?? bookmark.content.url; + + return ( + <div className="flex w-full flex-col items-center justify-center space-y-3"> + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <p className="line-clamp-2 text-center text-lg">{title}</p> + </TooltipTrigger> + <TooltipPortal> + <TooltipContent side="bottom" className="w-96"> + {title} + </TooltipContent> + </TooltipPortal> + </Tooltip> + </TooltipProvider> + <Link + href={bookmark.content.url} + className="mx-auto flex gap-2 text-gray-400" + > + <span className="my-auto">View Original</span> + <ExternalLink /> + </Link> + <Separator /> + </div> + ); +} + +export default function BookmarkPreview({ + initialData, +}: { + initialData: ZBookmark; +}) { + const { data: bookmark } = api.bookmarks.getBookmark.useQuery( + { + bookmarkId: initialData.id, + }, + { + initialData, + refetchInterval: (query) => { + const data = query.state.data; + if (!data) { + return false; + } + // If the link is not crawled or not tagged + if (isBookmarkStillLoading(data)) { + return 1000; + } + return false; + }, + }, + ); + + let content; + switch (bookmark.content.type) { + case "link": + case "text": { + content = <TextContentSection bookmark={bookmark} />; + break; + } + case "asset": { + content = <AssetContentSection bookmark={bookmark} />; + break; + } + } + + const linkHeader = bookmark.content.type == "link" && ( + <LinkHeader bookmark={bookmark} /> + ); + + return ( + <div className="grid grid-rows-3 gap-2 overflow-hidden bg-background lg:grid-cols-3 lg:grid-rows-none"> + <div className="row-span-2 h-full w-full overflow-hidden p-2 md:col-span-2 lg:row-auto"> + {isBookmarkStillCrawling(bookmark) ? <ContentLoading /> : content} + </div> + <div className="lg:col-span1 row-span-1 flex flex-col gap-4 overflow-auto bg-accent p-4 lg:row-auto"> + {linkHeader} + <CreationTime createdAt={bookmark.createdAt} /> + <div className="flex gap-4"> + <p className="text-sm text-gray-400">Tags</p> + <TagsEditor bookmark={bookmark} /> + </div> + <div className="flex gap-4"> + <p className="text-sm text-gray-400">Note</p> + <NoteEditor bookmark={bookmark} /> + </div> + <ActionBar bookmark={bookmark} /> + </div> + </div> + ); +} diff --git a/apps/web/components/dashboard/preview/NoteEditor.tsx b/apps/web/components/dashboard/preview/NoteEditor.tsx new file mode 100644 index 00000000..6011e89d --- /dev/null +++ b/apps/web/components/dashboard/preview/NoteEditor.tsx @@ -0,0 +1,42 @@ +import { Textarea } from "@/components/ui/textarea"; +import { toast } from "@/components/ui/use-toast"; +import { useClientConfig } from "@/lib/clientConfig"; + +import type { ZBookmark } from "@hoarder/trpc/types/bookmarks"; +import { useUpdateBookmark } from "@hoarder/shared-react/hooks/bookmarks"; + +export function NoteEditor({ bookmark }: { bookmark: ZBookmark }) { + const demoMode = !!useClientConfig().demoMode; + + const updateBookmarkMutator = useUpdateBookmark({ + onSuccess: () => { + toast({ + description: "The bookmark has been updated!", + }); + }, + onError: () => { + toast({ + description: "Something went wrong while saving the note", + variant: "destructive", + }); + }, + }); + + return ( + <Textarea + className="h-44 w-full overflow-auto rounded bg-background p-2 text-sm text-gray-400 dark:text-gray-300" + defaultValue={bookmark.note ?? ""} + disabled={demoMode} + placeholder="Write some notes ..." + onBlur={(e) => { + if (e.currentTarget.value == bookmark.note) { + return; + } + updateBookmarkMutator.mutate({ + bookmarkId: bookmark.id, + note: e.currentTarget.value, + }); + }} + /> + ); +} diff --git a/apps/web/components/dashboard/preview/TextContentSection.tsx b/apps/web/components/dashboard/preview/TextContentSection.tsx new file mode 100644 index 00000000..35ee1b33 --- /dev/null +++ b/apps/web/components/dashboard/preview/TextContentSection.tsx @@ -0,0 +1,39 @@ +import { ScrollArea } from "@radix-ui/react-scroll-area"; +import Markdown from "react-markdown"; + +import type { ZBookmark } from "@hoarder/trpc/types/bookmarks"; + +export function TextContentSection({ bookmark }: { bookmark: ZBookmark }) { + let content; + switch (bookmark.content.type) { + case "link": { + if (!bookmark.content.htmlContent) { + content = ( + <div className="text-destructive"> + Failed to fetch link content ... + </div> + ); + } else { + content = ( + <div + dangerouslySetInnerHTML={{ + __html: bookmark.content.htmlContent || "", + }} + className="prose mx-auto dark:prose-invert" + /> + ); + } + break; + } + case "text": { + content = ( + <Markdown className="prose mx-auto dark:prose-invert"> + {bookmark.content.text} + </Markdown> + ); + break; + } + } + + return <ScrollArea className="h-full">{content}</ScrollArea>; +} |
