From 7d7d3754d33b41478fea2d2d7ed902d665a9e03d Mon Sep 17 00:00:00 2001
From: MohamedBassem <me@mbassem.com>
Date: Thu, 21 Mar 2024 02:15:56 +0000
Subject: feature: A better looking bookmark preview page

---
 .../dashboard/bookmarks/BookmarkPreview.tsx        | 204 +++++++++++++++------
 1 file changed, 145 insertions(+), 59 deletions(-)

(limited to 'apps/web/components/dashboard/bookmarks/BookmarkPreview.tsx')

diff --git a/apps/web/components/dashboard/bookmarks/BookmarkPreview.tsx b/apps/web/components/dashboard/bookmarks/BookmarkPreview.tsx
index 4209192e..632422c4 100644
--- a/apps/web/components/dashboard/bookmarks/BookmarkPreview.tsx
+++ b/apps/web/components/dashboard/bookmarks/BookmarkPreview.tsx
@@ -2,58 +2,98 @@
 
 import Image from "next/image";
 import Link from "next/link";
-import { BackButton } from "@/components/ui/back-button";
+import { ScrollArea } from "@/components/ui/scroll-area";
 import { Skeleton } from "@/components/ui/skeleton";
-import { isBookmarkStillCrawling } from "@/lib/bookmarkUtils";
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipPortal,
+  TooltipProvider,
+  TooltipTrigger,
+} from "@/components/ui/tooltip";
+import {
+  isBookmarkStillCrawling,
+  isBookmarkStillLoading,
+} from "@/lib/bookmarkUtils";
 import { api } from "@/lib/trpc";
-import { ArrowLeftCircle, CalendarDays, ExternalLink } from "lucide-react";
+import dayjs from "dayjs";
+import relativeTime from "dayjs/plugin/relativeTime";
+import { CalendarDays, ExternalLink } from "lucide-react";
 import Markdown from "react-markdown";
 
 import type { ZBookmark } from "@hoarder/trpc/types/bookmarks";
 
-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 (isBookmarkStillCrawling(data)) {
-          return 1000;
-        }
-        return false;
-      },
-    },
+import { TagsEditor } from "./TagsEditor";
+
+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>
   );
+}
 
-  const linkHeader = bookmark.content.type == "link" && (
-    <div className="flex flex-col space-y-2">
-      <p className="text-center text-3xl">
-        {bookmark.content.title ?? bookmark.content.url}
-      </p>
-      <Link href={bookmark.content.url} className="mx-auto flex gap-2">
+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>
+        <TooltipContent>{createdAt.toLocaleString()}</TooltipContent>
+      </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>
+      <hr />
     </div>
   );
+}
 
+function TextContentSection({ bookmark }: { bookmark: ZBookmark }) {
   let content;
   switch (bookmark.content.type) {
     case "link": {
       if (!bookmark.content.htmlContent) {
         content = (
-          <div className="text-red-500">Failed to fetch link content ...</div>
+          <div className="text-destructive">
+            Failed to fetch link content ...
+          </div>
         );
       } else {
         content = (
@@ -61,24 +101,39 @@ export default function BookmarkPreview({
             dangerouslySetInnerHTML={{
               __html: bookmark.content.htmlContent || "",
             }}
-            className="prose"
+            className="prose mx-auto"
           />
         );
       }
       break;
     }
     case "text": {
-      content = <Markdown className="prose">{bookmark.content.text}</Markdown>;
+      content = (
+        <Markdown className="prose mx-auto">{bookmark.content.text}</Markdown>
+      );
       break;
     }
-    case "asset": {
+  }
+
+  return <ScrollArea className="h-full">{content}</ScrollArea>;
+}
+
+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 w-full">
+            <div className="relative h-full min-w-full">
               <Image
                 alt="asset"
                 fill={true}
+                className="object-contain"
                 src={`/api/assets/${bookmark.content.assetId}`}
               />
             </div>
@@ -88,31 +143,62 @@ export default function BookmarkPreview({
       break;
     }
   }
+  return content;
+}
+
+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="m-4 min-h-screen space-y-4 rounded-md border bg-background p-4">
-      <div className="flex justify-between">
-        <BackButton className="ghost" variant="ghost">
-          <ArrowLeftCircle />
-        </BackButton>
-        <div className="my-auto">
-          <span className="my-auto flex gap-2">
-            <CalendarDays /> {bookmark.createdAt.toLocaleString()}
-          </span>
-        </div>
+    <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>
-      <hr />
-      {linkHeader}
-      <div className="mx-auto flex h-full border-x p-2 px-4 lg:w-2/3">
-        {isBookmarkStillCrawling(bookmark) ? (
-          <div className="flex w-full flex-col gap-2">
-            <Skeleton className="h-4" />
-            <Skeleton className="h-4" />
-            <Skeleton className="h-4" />
-          </div>
-        ) : (
-          content
-        )}
+      <div className="lg:col-span1 row-span-1 flex flex-col gap-4 bg-gray-100 p-4 lg:row-auto">
+        {linkHeader}
+        <CreationTime createdAt={bookmark.createdAt} />
+        <div className="flex gap-2">
+          <TagsEditor bookmark={bookmark} />
+        </div>
       </div>
     </div>
   );
-- 
cgit v1.2.3-70-g09d2

