aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components/dashboard
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2024-04-14 00:51:56 +0300
committerGitHub <noreply@github.com>2024-04-14 00:51:56 +0300
commit4f17ea61cbb11a72712a1ea8c98904a1cc513e41 (patch)
tree4f1dd775e25feb3495ddb208c5fe4aa03c66fe3a /apps/web/components/dashboard
parentcf0df0e6d84a76649d8cbf8adcbf83efb6e883ab (diff)
downloadkarakeep-4f17ea61cbb11a72712a1ea8c98904a1cc513e41.tar.zst
feature(web): Allow changing the bookmark grid layout (#98)
Diffstat (limited to 'apps/web/components/dashboard')
-rw-r--r--apps/web/components/dashboard/bookmarks/AssetCard.tsx96
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkCardSkeleton.tsx30
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx131
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx44
-rw-r--r--apps/web/components/dashboard/bookmarks/EditorCard.tsx9
-rw-r--r--apps/web/components/dashboard/bookmarks/LinkCard.tsx125
-rw-r--r--apps/web/components/dashboard/bookmarks/TagList.tsx5
-rw-r--r--apps/web/components/dashboard/bookmarks/TextCard.tsx54
-rw-r--r--apps/web/components/dashboard/preview/BookmarkPreview.tsx2
-rw-r--r--apps/web/components/dashboard/sidebar/SidebarProfileOptions.tsx90
10 files changed, 404 insertions, 182 deletions
diff --git a/apps/web/components/dashboard/bookmarks/AssetCard.tsx b/apps/web/components/dashboard/bookmarks/AssetCard.tsx
index 8997a7e2..3bda1ee8 100644
--- a/apps/web/components/dashboard/bookmarks/AssetCard.tsx
+++ b/apps/web/components/dashboard/bookmarks/AssetCard.tsx
@@ -3,12 +3,48 @@
import Image from "next/image";
import { isBookmarkStillTagging } from "@/lib/bookmarkUtils";
import { api } from "@/lib/trpc";
-import { cn } from "@/lib/utils";
-import type { ZBookmark } from "@hoarder/trpc/types/bookmarks";
+import type {
+ ZBookmark,
+ ZBookmarkTypeAsset,
+} from "@hoarder/trpc/types/bookmarks";
-import BookmarkActionBar from "./BookmarkActionBar";
-import TagList from "./TagList";
+import { BookmarkLayoutAdaptingCard } from "./BookmarkLayoutAdaptingCard";
+
+function AssetImage({
+ bookmark,
+ className,
+}: {
+ bookmark: ZBookmarkTypeAsset;
+ className?: string;
+}) {
+ const bookmarkedAsset = bookmark.content;
+ switch (bookmarkedAsset.assetType) {
+ case "image": {
+ return (
+ <Image
+ alt="asset"
+ src={`/api/assets/${bookmarkedAsset.assetId}`}
+ fill={true}
+ className={className}
+ />
+ );
+ }
+ case "pdf": {
+ return (
+ <iframe
+ title={bookmarkedAsset.assetId}
+ className={className}
+ src={`/api/assets/${bookmarkedAsset.assetId}`}
+ />
+ );
+ }
+ default: {
+ const _exhaustiveCheck: never = bookmarkedAsset.assetType;
+ return <span />;
+ }
+ }
+}
export default function AssetCard({
bookmark: initialData,
@@ -35,49 +71,25 @@ export default function AssetCard({
},
},
);
- const bookmarkedAsset = bookmark.content;
- if (bookmarkedAsset.type != "asset") {
+
+ if (bookmark.content.type != "asset") {
throw new Error("Unexpected bookmark type");
}
+ const bookmarkedAsset = { ...bookmark, content: bookmark.content };
+
return (
- <div
- className={cn(
- className,
- cn(
- "flex h-min max-h-96 flex-col gap-y-1 overflow-hidden rounded-lg shadow-md",
- ),
- )}
- >
- {bookmarkedAsset.assetType == "image" && (
- <div className="relative h-56 max-h-56">
- <Image
- alt="asset"
- src={`/api/assets/${bookmarkedAsset.assetId}`}
- fill={true}
- className="rounded-t-lg object-cover"
- />
+ <BookmarkLayoutAdaptingCard
+ title={bookmarkedAsset.content.fileName}
+ footer={null}
+ bookmark={bookmarkedAsset}
+ className={className}
+ wrapTags={true}
+ image={(_layout, className) => (
+ <div className="relative size-full flex-1">
+ <AssetImage bookmark={bookmarkedAsset} className={className} />
</div>
)}
- {bookmarkedAsset.assetType == "pdf" && (
- <iframe
- title={bookmarkedAsset.assetId}
- className="h-56 max-h-56 w-full"
- src={`/api/assets/${bookmarkedAsset.assetId}`}
- />
- )}
- <div className="flex flex-col gap-y-1 overflow-hidden p-2">
- <div className="flex h-full flex-wrap gap-1 overflow-hidden">
- <TagList
- bookmark={bookmark}
- loading={isBookmarkStillTagging(bookmark)}
- />
- </div>
- <div className="flex w-full justify-between">
- <div />
- <BookmarkActionBar bookmark={bookmark} />
- </div>
- </div>
- </div>
+ />
);
}
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkCardSkeleton.tsx b/apps/web/components/dashboard/bookmarks/BookmarkCardSkeleton.tsx
deleted file mode 100644
index 026b8d37..00000000
--- a/apps/web/components/dashboard/bookmarks/BookmarkCardSkeleton.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import {
- ImageCard,
- ImageCardBanner,
- ImageCardBody,
- ImageCardContent,
- ImageCardFooter,
- ImageCardTitle,
-} from "@/components/ui/imageCard";
-import { Skeleton } from "@/components/ui/skeleton";
-
-export default function BookmarkCardSkeleton() {
- return (
- <ImageCard
- className={
- "border-grey-100 border bg-gray-50 duration-300 ease-in hover:border-blue-300 hover:transition-all"
- }
- >
- <ImageCardBanner src="/blur.avif" />
- <ImageCardContent>
- <ImageCardTitle></ImageCardTitle>
- <ImageCardBody className="space-y-2">
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-full" />
- <Skeleton className="h-4 w-full" />
- </ImageCardBody>
- <ImageCardFooter></ImageCardFooter>
- </ImageCardContent>
- </ImageCard>
- );
-}
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx
new file mode 100644
index 00000000..3d7b93f3
--- /dev/null
+++ b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx
@@ -0,0 +1,131 @@
+import type { BookmarksLayoutTypes } from "@/lib/userLocalSettings/types";
+import React from "react";
+import Link from "next/link";
+import { isBookmarkStillTagging } from "@/lib/bookmarkUtils";
+import {
+ bookmarkLayoutSwitch,
+ useBookmarkLayout,
+} from "@/lib/userLocalSettings/bookmarksLayout";
+import { cn } from "@/lib/utils";
+import dayjs from "dayjs";
+
+import type { ZBookmark } from "@hoarder/trpc/types/bookmarks";
+
+import BookmarkActionBar from "./BookmarkActionBar";
+import TagList from "./TagList";
+
+interface Props {
+ bookmark: ZBookmark;
+ image: (layout: BookmarksLayoutTypes, className: string) => React.ReactNode;
+ title?: React.ReactNode;
+ content?: React.ReactNode;
+ footer?: React.ReactNode;
+ className?: string;
+ fitHeight?: boolean;
+ wrapTags: boolean;
+}
+
+function BottomRow({
+ footer,
+ bookmark,
+}: {
+ footer?: React.ReactNode;
+ bookmark: ZBookmark;
+}) {
+ return (
+ <div className="justify flex w-full shrink-0 justify-between text-gray-500">
+ <div className="flex items-center gap-2 overflow-hidden text-nowrap">
+ {footer && <>{footer}•</>}
+ <Link href={`/dashboard/preview/${bookmark.id}`}>
+ {dayjs(bookmark.createdAt).format("MMM DD")}
+ </Link>
+ </div>
+ <BookmarkActionBar bookmark={bookmark} />
+ </div>
+ );
+}
+
+function ListView({
+ bookmark,
+ image,
+ title,
+ content,
+ footer,
+ className,
+}: Props) {
+ return (
+ <div
+ className={cn(
+ "flex max-h-96 gap-4 overflow-hidden rounded-lg p-2 shadow-md",
+ className,
+ )}
+ >
+ <div className="flex size-32 items-center justify-center overflow-hidden">
+ {image("list", "object-cover rounded-lg size-32")}
+ </div>
+ <div className="flex h-full flex-1 flex-col justify-between gap-2 overflow-hidden">
+ <div className="flex flex-col gap-2 overflow-hidden">
+ {title && <div className="flex-none shrink-0 text-lg">{title}</div>}
+ {content && <div className="shrink-1 overflow-hidden">{content}</div>}
+ <div className="flex shrink-0 flex-wrap gap-1 overflow-hidden">
+ <TagList
+ bookmark={bookmark}
+ loading={isBookmarkStillTagging(bookmark)}
+ />
+ </div>
+ </div>
+ <BottomRow footer={footer} bookmark={bookmark} />
+ </div>
+ </div>
+ );
+}
+
+function GridView({
+ bookmark,
+ image,
+ title,
+ content,
+ footer,
+ className,
+ wrapTags,
+ layout,
+ fitHeight = false,
+}: Props & { layout: BookmarksLayoutTypes }) {
+ const img = image("grid", "h-56 min-h-56 w-full object-cover rounded-t-lg");
+
+ return (
+ <div
+ className={cn(
+ "flex flex-col overflow-hidden rounded-lg shadow-md",
+ className,
+ fitHeight && layout != "grid" ? "max-h-96" : "h-96",
+ )}
+ >
+ {img && <div className="h-56 w-full shrink-0 overflow-hidden">{img}</div>}
+ <div className="flex h-full flex-col justify-between gap-2 overflow-hidden p-2">
+ <div className="grow-1 flex flex-col gap-2 overflow-hidden">
+ {title && <div className="flex-none shrink-0 text-lg">{title}</div>}
+ {content && <div className="shrink-1 overflow-hidden">{content}</div>}
+ <div className="flex shrink-0 flex-wrap gap-1 overflow-hidden">
+ <TagList
+ className={wrapTags ? undefined : "h-full"}
+ bookmark={bookmark}
+ loading={isBookmarkStillTagging(bookmark)}
+ />
+ </div>
+ </div>
+ <BottomRow footer={footer} bookmark={bookmark} />
+ </div>
+ </div>
+ );
+}
+
+export function BookmarkLayoutAdaptingCard(props: Props) {
+ const layout = useBookmarkLayout();
+
+ return bookmarkLayoutSwitch(layout, {
+ masonry: <GridView layout={layout} {...props} />,
+ grid: <GridView layout={layout} {...props} />,
+ list: <ListView {...props} />,
+ });
+}
diff --git a/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx b/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx
index bace3435..01f18815 100644
--- a/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx
+++ b/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx
@@ -1,5 +1,9 @@
import { useMemo } from "react";
import { ActionButton } from "@/components/ui/action-button";
+import {
+ bookmarkLayoutSwitch,
+ useBookmarkLayout,
+} from "@/lib/userLocalSettings/bookmarksLayout";
import tailwindConfig from "@/tailwind.config";
import { Slot } from "@radix-ui/react-slot";
import Masonry from "react-masonry-css";
@@ -36,13 +40,15 @@ function renderBookmark(bookmark: ZBookmark) {
let comp;
switch (bookmark.content.type) {
case "link":
- comp = <LinkCard bookmark={bookmark} />;
+ comp = <LinkCard bookmark={{ ...bookmark, content: bookmark.content }} />;
break;
case "text":
- comp = <TextCard bookmark={bookmark} />;
+ comp = <TextCard bookmark={{ ...bookmark, content: bookmark.content }} />;
break;
case "asset":
- comp = <AssetCard bookmark={bookmark} />;
+ comp = (
+ <AssetCard bookmark={{ ...bookmark, content: bookmark.content }} />
+ );
break;
}
return <BookmarkCard key={bookmark.id}>{comp}</BookmarkCard>;
@@ -61,20 +67,36 @@ export default function BookmarksGrid({
isFetchingNextPage?: boolean;
fetchNextPage?: () => void;
}) {
+ const layout = useBookmarkLayout();
const breakpointConfig = useMemo(() => getBreakpointConfig(), []);
+
if (bookmarks.length == 0 && !showEditorCard) {
return <p>No bookmarks</p>;
}
+
+ const children = [
+ showEditorCard && (
+ <BookmarkCard key={"editor"}>
+ <EditorCard />
+ </BookmarkCard>
+ ),
+ ...bookmarks.map((b) => renderBookmark(b)),
+ ];
return (
<>
- <Masonry className="flex gap-4" breakpointCols={breakpointConfig}>
- {showEditorCard && (
- <BookmarkCard>
- <EditorCard />
- </BookmarkCard>
- )}
- {bookmarks.map((b) => renderBookmark(b))}
- </Masonry>
+ {bookmarkLayoutSwitch(layout, {
+ masonry: (
+ <Masonry className="flex gap-4" breakpointCols={breakpointConfig}>
+ {children}
+ </Masonry>
+ ),
+ grid: (
+ <Masonry className="flex gap-4" breakpointCols={breakpointConfig}>
+ {children}
+ </Masonry>
+ ),
+ list: <div className="grid grid-cols-1">{children}</div>,
+ })}
{hasNextPage && (
<div className="flex justify-center">
<ActionButton
diff --git a/apps/web/components/dashboard/bookmarks/EditorCard.tsx b/apps/web/components/dashboard/bookmarks/EditorCard.tsx
index 10ad1f13..f6ea0c9a 100644
--- a/apps/web/components/dashboard/bookmarks/EditorCard.tsx
+++ b/apps/web/components/dashboard/bookmarks/EditorCard.tsx
@@ -7,6 +7,7 @@ import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "@/components/ui/use-toast";
import { useClientConfig } from "@/lib/clientConfig";
+import { useBookmarkLayoutSwitch } from "@/lib/userLocalSettings/bookmarksLayout";
import { cn } from "@/lib/utils";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
@@ -78,13 +79,19 @@ export default function EditorCard({ className }: { className?: string }) {
variant: "destructive",
});
};
+ const cardHeight = useBookmarkLayoutSwitch({
+ grid: "h-96",
+ masonry: "h-96",
+ list: undefined,
+ });
return (
<Form {...form}>
<form
className={cn(
className,
- "flex h-96 flex-col gap-2 rounded-xl bg-card p-4",
+ "flex flex-col gap-2 rounded-xl bg-card p-4",
+ cardHeight,
)}
onSubmit={form.handleSubmit(onSubmit, onError)}
>
diff --git a/apps/web/components/dashboard/bookmarks/LinkCard.tsx b/apps/web/components/dashboard/bookmarks/LinkCard.tsx
index 9796ed4f..5c329424 100644
--- a/apps/web/components/dashboard/bookmarks/LinkCard.tsx
+++ b/apps/web/components/dashboard/bookmarks/LinkCard.tsx
@@ -2,30 +2,70 @@
import Link from "next/link";
import {
- ImageCard,
- ImageCardBanner,
- ImageCardBody,
- ImageCardContent,
- ImageCardFooter,
- ImageCardTitle,
-} from "@/components/ui/imageCard";
-import {
isBookmarkStillCrawling,
isBookmarkStillLoading,
- isBookmarkStillTagging,
} from "@/lib/bookmarkUtils";
import { api } from "@/lib/trpc";
-import type { ZBookmark } from "@hoarder/trpc/types/bookmarks";
+import type { ZBookmarkTypeLink } from "@hoarder/trpc/types/bookmarks";
+
+import { BookmarkLayoutAdaptingCard } from "./BookmarkLayoutAdaptingCard";
+
+function LinkTitle({ bookmark }: { bookmark: ZBookmarkTypeLink }) {
+ const link = bookmark.content;
+ const parsedUrl = new URL(link.url);
+ return (
+ <Link className="line-clamp-2" href={link.url} target="_blank">
+ {link?.title ?? parsedUrl.host}
+ </Link>
+ );
+}
+
+function LinkImage({
+ bookmark,
+ className,
+}: {
+ bookmark: ZBookmarkTypeLink;
+ className?: string;
+}) {
+ const link = bookmark.content;
+
+ // A dummy white pixel for when there's no image.
+ // TODO: Better handling for cards with no images
+ const image =
+ link.imageUrl ??
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdj+P///38ACfsD/QVDRcoAAAAASUVORK5CYII=";
+ return (
+ <Link href={link.url} target="_blank">
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+ <img
+ className={className}
+ alt="card banner"
+ src={isBookmarkStillCrawling(bookmark) ? "/blur.avif" : image}
+ />
+ </Link>
+ );
+}
-import BookmarkActionBar from "./BookmarkActionBar";
-import TagList from "./TagList";
+function LinkUrl({ bookmark }: { bookmark: ZBookmarkTypeLink }) {
+ const link = bookmark.content;
+ const parsedUrl = new URL(link.url);
+ return (
+ <Link
+ className="line-clamp-1 hover:text-foreground"
+ href={link.url}
+ target="_blank"
+ >
+ {parsedUrl.host}
+ </Link>
+ );
+}
export default function LinkCard({
bookmark: initialData,
className,
}: {
- bookmark: ZBookmark;
+ bookmark: ZBookmarkTypeLink;
className?: string;
}) {
const { data: bookmark } = api.bookmarks.getBookmark.useQuery(
@@ -47,54 +87,23 @@ export default function LinkCard({
},
},
);
- const link = bookmark.content;
- if (link.type != "link") {
- throw new Error("Unexpected bookmark type");
+
+ if (bookmark.content.type !== "link") {
+ throw new Error("Invalid bookmark type");
}
- const parsedUrl = new URL(link.url);
- // A dummy white pixel for when there's no image.
- // TODO: Better handling for cards with no images
- const image =
- link.imageUrl ??
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdj+P///38ACfsD/QVDRcoAAAAASUVORK5CYII=";
+ const bookmarkLink = { ...bookmark, content: bookmark.content };
return (
- <ImageCard className={className}>
- <Link href={link.url} target="_blank">
- <ImageCardBanner
- src={isBookmarkStillCrawling(bookmark) ? "/blur.avif" : image}
- />
- </Link>
- <ImageCardContent>
- <ImageCardTitle>
- <Link className="line-clamp-2" href={link.url} target="_blank">
- {link?.title ?? parsedUrl.host}
- </Link>
- </ImageCardTitle>
- {/* There's a hack here. Every tag has the full hight of the container itself. That why, when we enable flex-wrap,
- the overflowed don't show up. */}
- <ImageCardBody className="flex h-full flex-wrap space-x-1 overflow-hidden">
- <TagList
- bookmark={bookmark}
- loading={isBookmarkStillTagging(bookmark)}
- />
- </ImageCardBody>
- <ImageCardFooter>
- <div className="mt-1 flex justify-between text-gray-500">
- <div className="my-auto">
- <Link
- className="line-clamp-1 hover:text-foreground"
- href={link.url}
- target="_blank"
- >
- {parsedUrl.host}
- </Link>
- </div>
- <BookmarkActionBar bookmark={bookmark} />
- </div>
- </ImageCardFooter>
- </ImageCardContent>
- </ImageCard>
+ <BookmarkLayoutAdaptingCard
+ title={<LinkTitle bookmark={bookmarkLink} />}
+ footer={<LinkUrl bookmark={bookmarkLink} />}
+ bookmark={bookmarkLink}
+ wrapTags={false}
+ image={(_layout, className) => (
+ <LinkImage className={className} bookmark={bookmarkLink} />
+ )}
+ className={className}
+ />
);
}
diff --git a/apps/web/components/dashboard/bookmarks/TagList.tsx b/apps/web/components/dashboard/bookmarks/TagList.tsx
index e9161961..e0387bd2 100644
--- a/apps/web/components/dashboard/bookmarks/TagList.tsx
+++ b/apps/web/components/dashboard/bookmarks/TagList.tsx
@@ -8,9 +8,11 @@ import type { ZBookmark } from "@hoarder/trpc/types/bookmarks";
export default function TagList({
bookmark,
loading,
+ className,
}: {
bookmark: ZBookmark;
loading?: boolean;
+ className?: string;
}) {
if (loading) {
return (
@@ -23,8 +25,9 @@ export default function TagList({
return (
<>
{bookmark.tags.map((t) => (
- <div key={t.id} className="flex h-full flex-col justify-end">
+ <div key={t.id} className={className}>
<Link
+ key={t.id}
className={cn(
badgeVariants({ variant: "outline" }),
"text-nowrap font-normal hover:bg-foreground hover:text-secondary",
diff --git a/apps/web/components/dashboard/bookmarks/TextCard.tsx b/apps/web/components/dashboard/bookmarks/TextCard.tsx
index 42d09c23..c715c8ab 100644
--- a/apps/web/components/dashboard/bookmarks/TextCard.tsx
+++ b/apps/web/components/dashboard/bookmarks/TextCard.tsx
@@ -3,14 +3,14 @@
import { useState } from "react";
import { isBookmarkStillTagging } from "@/lib/bookmarkUtils";
import { api } from "@/lib/trpc";
+import { bookmarkLayoutSwitch } from "@/lib/userLocalSettings/bookmarksLayout";
import { cn } from "@/lib/utils";
import Markdown from "react-markdown";
import type { ZBookmark } from "@hoarder/trpc/types/bookmarks";
-import BookmarkActionBar from "./BookmarkActionBar";
import { BookmarkedTextViewer } from "./BookmarkedTextViewer";
-import TagList from "./TagList";
+import { BookmarkLayoutAdaptingCard } from "./BookmarkLayoutAdaptingCard";
export default function TextCard({
bookmark: initialData,
@@ -50,28 +50,34 @@ export default function TextCard({
open={previewModalOpen}
setOpen={setPreviewModalOpen}
/>
- <div
- className={cn(
- className,
- cn(
- "flex h-min max-h-96 flex-col gap-y-1 overflow-hidden rounded-lg p-2 shadow-md",
- ),
- )}
- >
- <Markdown className="prose grow overflow-hidden dark:prose-invert">
- {bookmarkedText.text}
- </Markdown>
- <div className="mt-4 flex flex-none flex-wrap gap-1 overflow-hidden">
- <TagList
- bookmark={bookmark}
- loading={isBookmarkStillTagging(bookmark)}
- />
- </div>
- <div className="flex w-full justify-between">
- <div />
- <BookmarkActionBar bookmark={bookmark} />
- </div>
- </div>
+ <BookmarkLayoutAdaptingCard
+ content={
+ <Markdown className="prose dark:prose-invert">
+ {bookmarkedText.text}
+ </Markdown>
+ }
+ footer={null}
+ wrapTags={true}
+ bookmark={bookmark}
+ className={className}
+ fitHeight={true}
+ image={(layout, className) =>
+ bookmarkLayoutSwitch(layout, {
+ grid: null,
+ masonry: null,
+ list: (
+ <div
+ className={cn(
+ "flex size-full items-center justify-center bg-accent text-center",
+ className,
+ )}
+ >
+ Note
+ </div>
+ ),
+ })
+ }
+ />
</>
);
}
diff --git a/apps/web/components/dashboard/preview/BookmarkPreview.tsx b/apps/web/components/dashboard/preview/BookmarkPreview.tsx
index cf8bc2d8..4cd9199d 100644
--- a/apps/web/components/dashboard/preview/BookmarkPreview.tsx
+++ b/apps/web/components/dashboard/preview/BookmarkPreview.tsx
@@ -128,7 +128,7 @@ export default function BookmarkPreview({
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">
+ <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">
diff --git a/apps/web/components/dashboard/sidebar/SidebarProfileOptions.tsx b/apps/web/components/dashboard/sidebar/SidebarProfileOptions.tsx
index 3fe4d52f..c75e292a 100644
--- a/apps/web/components/dashboard/sidebar/SidebarProfileOptions.tsx
+++ b/apps/web/components/dashboard/sidebar/SidebarProfileOptions.tsx
@@ -6,33 +6,86 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
-import { Slot } from "@radix-ui/react-slot";
-import { LogOut, Moon, MoreHorizontal, Sun } from "lucide-react";
+import { useBookmarkLayout } from "@/lib/userLocalSettings/bookmarksLayout";
+import { updateBookmarksLayout } from "@/lib/userLocalSettings/userLocalSettings";
+import {
+ Check,
+ LayoutDashboard,
+ LayoutGrid,
+ LayoutList,
+ LayoutPanelLeft,
+ LogOut,
+ Moon,
+ MoreHorizontal,
+ Sun,
+} from "lucide-react";
import { signOut } from "next-auth/react";
import { useTheme } from "next-themes";
+function BookmarkLayoutSelector() {
+ const layout = useBookmarkLayout();
+
+ const checkedComp = <Check className="ml-2 size-4" />;
+
+ return (
+ <>
+ <DropdownMenuItem
+ className="justify-between"
+ onClick={async () => await updateBookmarksLayout("masonry")}
+ >
+ <div className="flex items-center gap-2">
+ <LayoutDashboard className="size-4" />
+ <span>Masonry</span>
+ </div>
+ {layout == "masonry" && checkedComp}
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ className="justify-between"
+ onClick={async () => await updateBookmarksLayout("grid")}
+ >
+ <div className="flex items-center gap-2">
+ <LayoutGrid className="size-4" />
+ <span>Grid</span>
+ </div>
+ {layout == "grid" && checkedComp}
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ className="justify-between"
+ onClick={async () => await updateBookmarksLayout("list")}
+ >
+ <div className="flex items-center gap-2">
+ <LayoutList className="size-4" />
+ <span>List</span>
+ </div>
+ {layout == "list" && checkedComp}
+ </DropdownMenuItem>
+ </>
+ );
+}
+
function DarkModeToggle() {
const { theme } = useTheme();
- let comp;
if (theme == "dark") {
- comp = (
- <span>
- <Sun className="size-4" />
- <p>Light Mode</p>
- </span>
+ return (
+ <>
+ <Sun className="mr-2 size-4" />
+ <span>Light Mode</span>
+ </>
);
} else {
- comp = (
- <span>
- <Moon className="size-4" />
- <p>Dark Mode</p>
- </span>
+ return (
+ <>
+ <Moon className="mr-2 size-4" />
+ <span>Dark Mode</span>
+ </>
);
}
- return <Slot className="flex flex-row gap-2">{comp}</Slot>;
}
export default function SidebarProfileOptions() {
@@ -48,6 +101,15 @@ export default function SidebarProfileOptions() {
<DropdownMenuItem onClick={toggleTheme}>
<DarkModeToggle />
</DropdownMenuItem>
+ <DropdownMenuSub>
+ <DropdownMenuSubTrigger>
+ <LayoutPanelLeft className="mr-2 size-4" />
+ <span>Layout</span>
+ </DropdownMenuSubTrigger>
+ <DropdownMenuSubContent>
+ <BookmarkLayoutSelector />
+ </DropdownMenuSubContent>
+ </DropdownMenuSub>
<DropdownMenuItem
onClick={() =>
signOut({