aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components/dashboard/highlights
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2024-12-28 12:30:24 +0000
committerMohamed Bassem <me@mbassem.com>2024-12-28 12:30:24 +0000
commit7956e9fa6772ab57a0794fb7cba7e9d9c0bd7878 (patch)
tree10d6e47afed8be007361cd40ec8b05fc52d6d4d9 /apps/web/components/dashboard/highlights
parent7dd5b2bc8751911f86448e6c4619b2583d9a4b53 (diff)
downloadkarakeep-7956e9fa6772ab57a0794fb7cba7e9d9c0bd7878.tar.zst
feat: Implement the all highlights page. Fixes #620
Diffstat (limited to 'apps/web/components/dashboard/highlights')
-rw-r--r--apps/web/components/dashboard/highlights/AllHighlights.tsx98
-rw-r--r--apps/web/components/dashboard/highlights/HighlightCard.tsx76
2 files changed, 174 insertions, 0 deletions
diff --git a/apps/web/components/dashboard/highlights/AllHighlights.tsx b/apps/web/components/dashboard/highlights/AllHighlights.tsx
new file mode 100644
index 00000000..27dda6ff
--- /dev/null
+++ b/apps/web/components/dashboard/highlights/AllHighlights.tsx
@@ -0,0 +1,98 @@
+"use client";
+
+import { useEffect } from "react";
+import Link from "next/link";
+import { ActionButton } from "@/components/ui/action-button";
+import useRelativeTime from "@/lib/hooks/relative-time";
+import { api } from "@/lib/trpc";
+import { Separator } from "@radix-ui/react-dropdown-menu";
+import dayjs from "dayjs";
+import relativeTime from "dayjs/plugin/relativeTime";
+import { Dot, LinkIcon } from "lucide-react";
+import { useTranslation } from "react-i18next";
+import { useInView } from "react-intersection-observer";
+
+import {
+ ZGetAllHighlightsResponse,
+ ZHighlight,
+} from "@hoarder/shared/types/highlights";
+
+import HighlightCard from "./HighlightCard";
+
+dayjs.extend(relativeTime);
+
+function Highlight({ highlight }: { highlight: ZHighlight }) {
+ const { fromNow, localCreatedAt } = useRelativeTime(highlight.createdAt);
+ const { t } = useTranslation();
+ return (
+ <div className="flex flex-col gap-2">
+ <HighlightCard highlight={highlight} clickable={false} />
+ <span className="flex items-center gap-0.5 text-xs italic text-gray-400">
+ <span title={localCreatedAt}>{fromNow}</span>
+ <Dot />
+ <Link
+ href={`/dashboard/preview/${highlight.bookmarkId}`}
+ className="flex items-center gap-0.5"
+ >
+ <LinkIcon className="size-3 italic" />
+ {t("common.source")}
+ </Link>
+ </span>
+ </div>
+ );
+}
+
+export default function AllHighlights({
+ highlights: initialHighlights,
+}: {
+ highlights: ZGetAllHighlightsResponse;
+}) {
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
+ api.highlights.getAll.useInfiniteQuery(
+ {},
+ {
+ initialData: () => ({
+ pages: [initialHighlights],
+ pageParams: [null],
+ }),
+ initialCursor: null,
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
+ },
+ );
+
+ const { ref: loadMoreRef, inView: loadMoreButtonInView } = useInView();
+ useEffect(() => {
+ if (loadMoreButtonInView && hasNextPage && !isFetchingNextPage) {
+ fetchNextPage();
+ }
+ }, [loadMoreButtonInView]);
+
+ return (
+ <div className="flex flex-col gap-2">
+ {data?.pages
+ .flatMap((p) => p.highlights)
+ .map((h) => (
+ <>
+ <Highlight key={h.id} highlight={h} />
+ <Separator
+ key={`sep-${h.id}`}
+ className="m-2 h-0.5 bg-gray-100 last:hidden"
+ />
+ </>
+ ))}
+ {hasNextPage && (
+ <div className="flex justify-center">
+ <ActionButton
+ ref={loadMoreRef}
+ ignoreDemoMode={true}
+ loading={isFetchingNextPage}
+ onClick={() => fetchNextPage()}
+ variant="ghost"
+ >
+ Load More
+ </ActionButton>
+ </div>
+ )}
+ </div>
+ );
+}
diff --git a/apps/web/components/dashboard/highlights/HighlightCard.tsx b/apps/web/components/dashboard/highlights/HighlightCard.tsx
new file mode 100644
index 00000000..63ff9423
--- /dev/null
+++ b/apps/web/components/dashboard/highlights/HighlightCard.tsx
@@ -0,0 +1,76 @@
+import { ActionButton } from "@/components/ui/action-button";
+import { toast } from "@/components/ui/use-toast";
+import { cn } from "@/lib/utils";
+import { Trash2 } from "lucide-react";
+
+import { useDeleteHighlight } from "@hoarder/shared-react/hooks/highlights";
+import { ZHighlight } from "@hoarder/shared/types/highlights";
+
+import { HIGHLIGHT_COLOR_MAP } from "../preview/highlights";
+
+export default function HighlightCard({
+ highlight,
+ clickable,
+ className,
+}: {
+ highlight: ZHighlight;
+ clickable: boolean;
+ className?: string;
+}) {
+ const { mutate: deleteHighlight, isPending: isDeleting } = useDeleteHighlight(
+ {
+ onSuccess: () => {
+ toast({
+ description: "Highlight has been deleted!",
+ });
+ },
+ onError: () => {
+ toast({
+ description: "Something went wrong",
+ variant: "destructive",
+ });
+ },
+ },
+ );
+
+ const onBookmarkClick = () => {
+ document
+ .querySelector(`[data-highlight-id="${highlight.id}"]`)
+ ?.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ });
+ };
+
+ const Wrapper = ({ children }: { children: React.ReactNode }) =>
+ clickable ? (
+ <button onClick={onBookmarkClick}>{children}</button>
+ ) : (
+ <div>{children}</div>
+ );
+
+ return (
+ <div className={cn("flex items-center justify-between", className)}>
+ <Wrapper>
+ <blockquote
+ cite={highlight.bookmarkId}
+ className={cn(
+ "prose border-l-[6px] p-2 pl-6 italic dark:prose-invert prose-p:text-sm",
+ HIGHLIGHT_COLOR_MAP["border-l"][highlight.color],
+ )}
+ >
+ <p>{highlight.text}</p>
+ </blockquote>
+ </Wrapper>
+ <div className="flex gap-2">
+ <ActionButton
+ loading={isDeleting}
+ variant="ghost"
+ onClick={() => deleteHighlight({ highlightId: highlight.id })}
+ >
+ <Trash2 className="size-4 text-destructive" />
+ </ActionButton>
+ </div>
+ </div>
+ );
+}