lifetracker/apps/web/components/dashboard/preview/BookmarkPreview.tsx

156 lines
4.6 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { BookmarkTagsEditor } from "@/components/dashboard/bookmarks/BookmarkTagsEditor";
import { FullPageSpinner } from "@/components/ui/full-page-spinner";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import {
Tooltip,
TooltipContent,
TooltipPortal,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/lib/trpc";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { CalendarDays, ExternalLink } from "lucide-react";
import {
getSourceUrl,
isBookmarkStillCrawling,
isBookmarkStillLoading,
} from "@hoarder/shared-react/utils/bookmarkUtils";
import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks";
import SummarizeBookmarkArea from "../bookmarks/SummarizeBookmarkArea";
import ActionBar from "./ActionBar";
import { AssetContentSection } from "./AssetContentSection";
import AttachmentBox from "./AttachmentBox";
import { EditableTitle } from "./EditableTitle";
import LinkContentSection from "./LinkContentSection";
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 }) {
const [fromNow, setFromNow] = useState("");
const [localCreatedAt, setLocalCreatedAt] = useState("");
// This is to avoid hydration errors when server and clients are in different timezones
useEffect(() => {
setFromNow(dayjs(createdAt).fromNow());
setLocalCreatedAt(createdAt.toLocaleString());
}, [createdAt]);
return (
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<span className="flex w-fit gap-2">
<CalendarDays /> {fromNow}
</span>
</TooltipTrigger>
<TooltipPortal>
<TooltipContent>{localCreatedAt}</TooltipContent>
</TooltipPortal>
</Tooltip>
);
}
export default function BookmarkPreview({
bookmarkId,
initialData,
}: {
bookmarkId: string;
initialData?: ZBookmark;
}) {
const { data: bookmark } = api.bookmarks.getBookmark.useQuery(
{
bookmarkId,
},
{
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;
},
},
);
if (!bookmark) {
return <FullPageSpinner />;
}
let content;
switch (bookmark.content.type) {
case BookmarkTypes.LINK: {
content = <LinkContentSection bookmark={bookmark} />;
break;
}
case BookmarkTypes.TEXT: {
content = <TextContentSection bookmark={bookmark} />;
break;
}
case BookmarkTypes.ASSET: {
content = <AssetContentSection bookmark={bookmark} />;
break;
}
}
const sourceUrl = getSourceUrl(bookmark);
return (
<div className="grid h-full 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-auto 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">
<div className="flex w-full flex-col items-center justify-center gap-y-2">
<EditableTitle bookmark={bookmark} />
{sourceUrl && (
<Link
href={sourceUrl}
className="flex items-center gap-2 text-gray-400"
>
<span>View Original</span>
<ExternalLink />
</Link>
)}
<Separator />
</div>
<CreationTime createdAt={bookmark.createdAt} />
<SummarizeBookmarkArea bookmark={bookmark} />
<div className="flex items-center gap-4">
<p className="text-sm text-gray-400">Tags</p>
<BookmarkTagsEditor bookmark={bookmark} />
</div>
<div className="flex gap-4">
<p className="pt-2 text-sm text-gray-400">Note</p>
<NoteEditor bookmark={bookmark} />
</div>
<AttachmentBox bookmark={bookmark} />
<ActionBar bookmark={bookmark} />
</div>
</div>
);
}