aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components/dashboard
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
parent7dd5b2bc8751911f86448e6c4619b2583d9a4b53 (diff)
downloadkarakeep-7956e9fa6772ab57a0794fb7cba7e9d9c0bd7878.tar.zst
feat: Implement the all highlights page. Fixes #620
Diffstat (limited to 'apps/web/components/dashboard')
-rw-r--r--apps/web/components/dashboard/highlights/AllHighlights.tsx98
-rw-r--r--apps/web/components/dashboard/highlights/HighlightCard.tsx76
-rw-r--r--apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx6
-rw-r--r--apps/web/components/dashboard/preview/BookmarkPreview.tsx15
-rw-r--r--apps/web/components/dashboard/preview/HighlightsBox.tsx64
-rw-r--r--apps/web/components/dashboard/sidebar/ModileSidebar.tsx6
-rw-r--r--apps/web/components/dashboard/sidebar/Sidebar.tsx7
7 files changed, 194 insertions, 78 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>
+ );
+}
diff --git a/apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx b/apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx
index b99a1a56..89472184 100644
--- a/apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx
+++ b/apps/web/components/dashboard/preview/BookmarkHtmlHighlighter.tsx
@@ -1,4 +1,5 @@
import React, { useEffect, useRef, useState } from "react";
+import { ActionButton } from "@/components/ui/action-button";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
@@ -61,14 +62,15 @@ const ColorPickerMenu: React.FC<ColorPickerMenuProps> = ({
</Button>
))}
{selectedHighlight && (
- <Button
+ <ActionButton
+ loading={false}
size="none"
className="size-8 rounded-full"
onClick={onDelete}
variant="ghost"
>
<Trash2 className="size-5 text-destructive" />
- </Button>
+ </ActionButton>
)}
</PopoverContent>
</Popover>
diff --git a/apps/web/components/dashboard/preview/BookmarkPreview.tsx b/apps/web/components/dashboard/preview/BookmarkPreview.tsx
index b854146c..c257d902 100644
--- a/apps/web/components/dashboard/preview/BookmarkPreview.tsx
+++ b/apps/web/components/dashboard/preview/BookmarkPreview.tsx
@@ -1,6 +1,5 @@
"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";
@@ -12,10 +11,9 @@ import {
TooltipPortal,
TooltipTrigger,
} from "@/components/ui/tooltip";
+import useRelativeTime from "@/lib/hooks/relative-time";
import { useTranslation } from "@/lib/i18n/client";
import { api } from "@/lib/trpc";
-import dayjs from "dayjs";
-import relativeTime from "dayjs/plugin/relativeTime";
import { CalendarDays, ExternalLink } from "lucide-react";
import {
@@ -35,8 +33,6 @@ 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">
@@ -48,14 +44,7 @@ function ContentLoading() {
}
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]);
+ const { fromNow, localCreatedAt } = useRelativeTime(createdAt);
return (
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
diff --git a/apps/web/components/dashboard/preview/HighlightsBox.tsx b/apps/web/components/dashboard/preview/HighlightsBox.tsx
index 3c873f3d..af065a9d 100644
--- a/apps/web/components/dashboard/preview/HighlightsBox.tsx
+++ b/apps/web/components/dashboard/preview/HighlightsBox.tsx
@@ -1,72 +1,14 @@
-import { ActionButton } from "@/components/ui/action-button";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
-import { toast } from "@/components/ui/use-toast";
import { useTranslation } from "@/lib/i18n/client";
import { api } from "@/lib/trpc";
-import { cn } from "@/lib/utils";
import { Separator } from "@radix-ui/react-dropdown-menu";
-import { ChevronsDownUp, Trash2 } from "lucide-react";
+import { ChevronsDownUp } from "lucide-react";
-import { useDeleteHighlight } from "@hoarder/shared-react/hooks/highlights";
-import { ZHighlight } from "@hoarder/shared/types/highlights";
-
-import { HIGHLIGHT_COLOR_MAP } from "./highlights";
-
-function HighlightCard({ highlight }: { highlight: ZHighlight }) {
- 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",
- });
- };
- return (
- <div className="flex items-center justify-between">
- <div className="flex items-center gap-3">
- <button onClick={onBookmarkClick}>
- <blockquote
- 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>
- </button>
- </div>
- <div className="flex gap-2">
- <ActionButton
- loading={isDeleting}
- variant="ghost"
- onClick={() => deleteHighlight({ highlightId: highlight.id })}
- >
- <Trash2 className="size-4 text-destructive" />
- </ActionButton>
- </div>
- </div>
- );
-}
+import HighlightCard from "../highlights/HighlightCard";
export default function HighlightsBox({ bookmarkId }: { bookmarkId: string }) {
const { t } = useTranslation();
@@ -87,7 +29,7 @@ export default function HighlightsBox({ bookmarkId }: { bookmarkId: string }) {
<CollapsibleContent className="group flex flex-col py-3 text-sm">
{highlights.highlights.map((highlight) => (
<>
- <HighlightCard key={highlight.id} highlight={highlight} />
+ <HighlightCard key={highlight.id} highlight={highlight} clickable />
<Separator className="m-2 h-0.5 bg-gray-200 last:hidden" />
</>
))}
diff --git a/apps/web/components/dashboard/sidebar/ModileSidebar.tsx b/apps/web/components/dashboard/sidebar/ModileSidebar.tsx
index bfa91afa..777877bf 100644
--- a/apps/web/components/dashboard/sidebar/ModileSidebar.tsx
+++ b/apps/web/components/dashboard/sidebar/ModileSidebar.tsx
@@ -1,6 +1,6 @@
import MobileSidebarItem from "@/components/shared/sidebar/ModileSidebarItem";
import HoarderLogoIcon from "@/public/icons/logo-icon.svg";
-import { ClipboardList, Search, Tag } from "lucide-react";
+import { ClipboardList, Highlighter, Search, Tag } from "lucide-react";
export default async function MobileSidebar() {
return (
@@ -13,6 +13,10 @@ export default async function MobileSidebar() {
<MobileSidebarItem logo={<Search />} path="/dashboard/search" />
<MobileSidebarItem logo={<ClipboardList />} path="/dashboard/lists" />
<MobileSidebarItem logo={<Tag />} path="/dashboard/tags" />
+ <MobileSidebarItem
+ logo={<Highlighter />}
+ path="/dashboard/highlights"
+ />
</ul>
</aside>
);
diff --git a/apps/web/components/dashboard/sidebar/Sidebar.tsx b/apps/web/components/dashboard/sidebar/Sidebar.tsx
index 8891d9bc..0f805a09 100644
--- a/apps/web/components/dashboard/sidebar/Sidebar.tsx
+++ b/apps/web/components/dashboard/sidebar/Sidebar.tsx
@@ -4,7 +4,7 @@ import { Separator } from "@/components/ui/separator";
import { useTranslation } from "@/lib/i18n/server";
import { api } from "@/server/api/client";
import { getServerAuthSession } from "@/server/auth";
-import { Archive, Home, Search, Tag } from "lucide-react";
+import { Archive, Highlighter, Home, Search, Tag } from "lucide-react";
import serverConfig from "@hoarder/shared/config";
@@ -46,6 +46,11 @@ export default async function Sidebar() {
path: "/dashboard/tags",
},
{
+ name: t("common.highlights"),
+ icon: <Highlighter size={18} />,
+ path: "/dashboard/highlights",
+ },
+ {
name: t("common.archive"),
icon: <Archive size={18} />,
path: "/dashboard/archive",