aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web
diff options
context:
space:
mode:
authorxuatz <xzlow10@gmail.com>2025-11-03 04:32:18 +0900
committerGitHub <noreply@github.com>2025-11-02 19:32:18 +0000
commit33f407797213c56dd2f13e98228a5305efdf90fd (patch)
tree149f477c1ab61b46bb0d9cc49c656a03b1c56f64 /apps/web
parentb63a49fc3980296c6a6ea6ac0624142e8af94d52 (diff)
downloadkarakeep-33f407797213c56dd2f13e98228a5305efdf90fd.tar.zst
feat: display notes on bookmark card (#2083)
* feat: display notes on bookmark card * apply styling * include mobile impl * apply pr comments * add display options menu into PR * put it under app setting * cleanup * address pr comments * change the default for show notes to false * make the in-card note font lighter --------- Co-authored-by: Mohamed Bassem <me@mbassem.com>
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/.oxlintrc.json2
-rw-r--r--apps/web/components/dashboard/ViewOptions.tsx57
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx28
-rw-r--r--apps/web/components/dashboard/bookmarks/NotePreview.tsx66
-rw-r--r--apps/web/lib/i18n/locales/en/translation.json8
-rw-r--r--apps/web/lib/userLocalSettings/bookmarksLayout.tsx8
-rw-r--r--apps/web/lib/userLocalSettings/types.ts1
-rw-r--r--apps/web/lib/userLocalSettings/userLocalSettings.ts11
8 files changed, 158 insertions, 23 deletions
diff --git a/apps/web/.oxlintrc.json b/apps/web/.oxlintrc.json
index d0a51846..4c437efb 100644
--- a/apps/web/.oxlintrc.json
+++ b/apps/web/.oxlintrc.json
@@ -27,6 +27,8 @@
".next",
"dist",
"build",
+ "public/sw.js",
+ "public/workbox-*.js",
"pnpm-lock.yaml"
]
}
diff --git a/apps/web/components/dashboard/ViewOptions.tsx b/apps/web/components/dashboard/ViewOptions.tsx
index 6367421f..c86f43fb 100644
--- a/apps/web/components/dashboard/ViewOptions.tsx
+++ b/apps/web/components/dashboard/ViewOptions.tsx
@@ -1,6 +1,6 @@
"use client";
-import React from "react";
+import { createElement, useEffect, useState } from "react";
import { ButtonWithTooltip } from "@/components/ui/button";
import {
DropdownMenu,
@@ -9,14 +9,19 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
+import { Label } from "@/components/ui/label";
import { Slider } from "@/components/ui/slider";
+import { Switch } from "@/components/ui/switch";
+import { useTranslation } from "@/lib/i18n/client";
import {
+ useBookmarkDisplaySettings,
useBookmarkLayout,
useGridColumns,
} from "@/lib/userLocalSettings/bookmarksLayout";
import {
updateBookmarksLayout,
updateGridColumns,
+ updateShowNotes,
} from "@/lib/userLocalSettings/userLocalSettings";
import {
Check,
@@ -25,6 +30,7 @@ import {
LayoutList,
List,
LucideIcon,
+ NotepadText,
Settings,
} from "lucide-react";
@@ -37,22 +43,17 @@ const iconMap: Record<LayoutType, LucideIcon> = {
compact: List,
};
-const layoutNames: Record<LayoutType, string> = {
- masonry: "Masonry",
- grid: "Grid",
- list: "List",
- compact: "Compact",
-};
-
export default function ViewOptions() {
+ const { t } = useTranslation();
const layout = useBookmarkLayout();
const gridColumns = useGridColumns();
- const [tempColumns, setTempColumns] = React.useState(gridColumns);
+ const { showNotes } = useBookmarkDisplaySettings();
+ const [tempColumns, setTempColumns] = useState(gridColumns);
const showColumnSlider = layout === "grid" || layout === "masonry";
// Update temp value when actual value changes
- React.useEffect(() => {
+ useEffect(() => {
setTempColumns(gridColumns);
}, [gridColumns]);
@@ -60,7 +61,7 @@ export default function ViewOptions() {
<DropdownMenu>
<DropdownMenuTrigger asChild>
<ButtonWithTooltip
- tooltip="View Options"
+ tooltip={t("view_options.title")}
delayDuration={100}
variant="ghost"
>
@@ -68,7 +69,9 @@ export default function ViewOptions() {
</ButtonWithTooltip>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
- <div className="px-2 py-1.5 text-sm font-semibold">Layout</div>
+ <div className="px-2 py-1.5 text-sm font-semibold">
+ {t("view_options.layout")}
+ </div>
{(Object.keys(iconMap) as LayoutType[]).map((key) => (
<DropdownMenuItem
key={key}
@@ -76,8 +79,8 @@ export default function ViewOptions() {
onClick={async () => await updateBookmarksLayout(key as LayoutType)}
>
<div className="flex items-center gap-2">
- {React.createElement(iconMap[key as LayoutType], { size: 18 })}
- <span>{layoutNames[key]}</span>
+ {createElement(iconMap[key as LayoutType], { size: 18 })}
+ <span>{t(`layouts.${key}`)}</span>
</div>
{layout === key && <Check className="ml-2 size-4" />}
</DropdownMenuItem>
@@ -88,7 +91,9 @@ export default function ViewOptions() {
<DropdownMenuSeparator />
<div className="px-2 py-3">
<div className="mb-2 flex items-center justify-between">
- <span className="text-sm font-semibold">Columns</span>
+ <span className="text-sm font-semibold">
+ {t("view_options.columns")}
+ </span>
<span className="text-sm text-muted-foreground">
{tempColumns}
</span>
@@ -109,6 +114,28 @@ export default function ViewOptions() {
</div>
</>
)}
+
+ <DropdownMenuSeparator />
+ <div className="px-2 py-1.5 text-sm font-semibold">
+ {t("view_options.display_options")}
+ </div>
+
+ <div className="space-y-3 px-2 py-2">
+ <div className="flex items-center justify-between">
+ <Label
+ htmlFor="show-notes"
+ className="flex cursor-pointer items-center gap-2 text-sm"
+ >
+ <NotepadText size={16} />
+ <span>{t("view_options.show_note_previews")}</span>
+ </Label>
+ <Switch
+ id="show-notes"
+ checked={showNotes}
+ onCheckedChange={updateShowNotes}
+ />
+ </div>
+ </div>
</DropdownMenuContent>
</DropdownMenu>
);
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx
index 4b511a3c..26c1c72b 100644
--- a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx
+++ b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx
@@ -1,10 +1,14 @@
+"use client";
+
import type { BookmarksLayoutTypes } from "@/lib/userLocalSettings/types";
-import React, { useEffect, useState } from "react";
+import type { ReactNode } from "react";
+import { useEffect, useState } from "react";
import Image from "next/image";
import Link from "next/link";
import useBulkActionsStore from "@/lib/bulkActions";
import {
bookmarkLayoutSwitch,
+ useBookmarkDisplaySettings,
useBookmarkLayout,
} from "@/lib/userLocalSettings/bookmarksLayout";
import { cn } from "@/lib/utils";
@@ -17,14 +21,15 @@ import { isBookmarkStillTagging } from "@karakeep/shared/utils/bookmarkUtils";
import BookmarkActionBar from "./BookmarkActionBar";
import BookmarkFormattedCreatedAt from "./BookmarkFormattedCreatedAt";
+import { NotePreview } from "./NotePreview";
import TagList from "./TagList";
interface Props {
bookmark: ZBookmark;
- image: (layout: BookmarksLayoutTypes, className: string) => React.ReactNode;
- title?: React.ReactNode;
- content?: React.ReactNode;
- footer?: React.ReactNode;
+ image: (layout: BookmarksLayoutTypes, className: string) => ReactNode;
+ title?: ReactNode;
+ content?: ReactNode;
+ footer?: ReactNode;
className?: string;
fitHeight?: boolean;
wrapTags: boolean;
@@ -34,7 +39,7 @@ function BottomRow({
footer,
bookmark,
}: {
- footer?: React.ReactNode;
+ footer?: ReactNode;
bookmark: ZBookmark;
}) {
return (
@@ -112,6 +117,9 @@ function ListView({
footer,
className,
}: Props) {
+ const { showNotes } = useBookmarkDisplaySettings();
+ const note = showNotes ? bookmark.note?.trim() : undefined;
+
return (
<div
className={cn(
@@ -131,6 +139,7 @@ function ListView({
</div>
)}
{content && <div className="shrink-1 overflow-hidden">{content}</div>}
+ {note && <NotePreview note={note} bookmarkId={bookmark.id} />}
<div className="flex shrink-0 flex-wrap gap-1 overflow-hidden">
<TagList
bookmark={bookmark}
@@ -155,7 +164,9 @@ function GridView({
layout,
fitHeight = false,
}: Props & { layout: BookmarksLayoutTypes }) {
- const img = image("grid", "h-56 min-h-56 w-full object-cover rounded-t-lg");
+ const { showNotes } = useBookmarkDisplaySettings();
+ const note = showNotes ? bookmark.note?.trim() : undefined;
+ const img = image("grid", "h-52 min-h-52 w-full object-cover rounded-t-lg");
return (
<div
@@ -166,7 +177,7 @@ function GridView({
)}
>
<MultiBookmarkSelector bookmark={bookmark} />
- {img && <div className="h-56 w-full shrink-0 overflow-hidden">{img}</div>}
+ {img && <div className="h-52 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 && (
@@ -175,6 +186,7 @@ function GridView({
</div>
)}
{content && <div className="shrink-1 overflow-hidden">{content}</div>}
+ {note && <NotePreview note={note} bookmarkId={bookmark.id} />}
<div className="flex shrink-0 flex-wrap gap-1 overflow-hidden">
<TagList
className={wrapTags ? undefined : "h-full"}
diff --git a/apps/web/components/dashboard/bookmarks/NotePreview.tsx b/apps/web/components/dashboard/bookmarks/NotePreview.tsx
new file mode 100644
index 00000000..c32c85a3
--- /dev/null
+++ b/apps/web/components/dashboard/bookmarks/NotePreview.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import { useState } from "react";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { useTranslation } from "@/lib/i18n/client";
+import { cn } from "@/lib/utils";
+import { ExternalLink, NotepadText } from "lucide-react";
+
+interface NotePreviewProps {
+ note: string;
+ bookmarkId: string;
+ className?: string;
+}
+
+export function NotePreview({ note, bookmarkId, className }: NotePreviewProps) {
+ const { t } = useTranslation();
+ const [isOpen, setIsOpen] = useState(false);
+
+ if (!note?.trim()) {
+ return null;
+ }
+
+ return (
+ <Popover open={isOpen} onOpenChange={setIsOpen}>
+ <PopoverTrigger asChild>
+ <div
+ className={cn(
+ "flex cursor-pointer items-center gap-1.5 text-sm font-light italic text-gray-500 dark:text-gray-400",
+ className,
+ )}
+ >
+ <NotepadText className="size-5 shrink-0" />
+ <div className="line-clamp-2 min-w-0 flex-1 overflow-hidden text-wrap break-words">
+ {note}
+ </div>
+ </div>
+ </PopoverTrigger>
+ <PopoverContent className="w-96 max-w-[calc(100vw-2rem)]" align="start">
+ <div className="space-y-3">
+ <div className="max-h-60 overflow-y-auto whitespace-pre-wrap break-words text-sm text-gray-700 dark:text-gray-300">
+ {note}
+ </div>
+ <div className="flex justify-end">
+ <Link href={`/dashboard/preview/${bookmarkId}`}>
+ <Button
+ variant="outline"
+ size="sm"
+ className="gap-2"
+ onClick={() => setIsOpen(false)}
+ >
+ {t("actions.edit_notes")}
+ <ExternalLink className="size-4" />
+ </Button>
+ </Link>
+ </div>
+ </div>
+ </PopoverContent>
+ </Popover>
+ );
+}
diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json
index 63e4ae2d..9b40416d 100644
--- a/apps/web/lib/i18n/locales/en/translation.json
+++ b/apps/web/lib/i18n/locales/en/translation.json
@@ -47,6 +47,13 @@
"list": "List",
"compact": "Compact"
},
+ "view_options": {
+ "title": "View Options",
+ "layout": "Layout",
+ "columns": "Columns",
+ "display_options": "Display Options",
+ "show_note_previews": "Show Notes"
+ },
"actions": {
"change_layout": "Change Layout",
"archive": "Archive",
@@ -59,6 +66,7 @@
"recrawl": "Recrawl",
"download_full_page_archive": "Download Full Page Archive",
"edit_tags": "Edit Tags",
+ "edit_notes": "Edit Notes",
"add_to_list": "Add to List",
"select_all": "Select All",
"unselect_all": "Unselect All",
diff --git a/apps/web/lib/userLocalSettings/bookmarksLayout.tsx b/apps/web/lib/userLocalSettings/bookmarksLayout.tsx
index 346c85e0..504d8d8c 100644
--- a/apps/web/lib/userLocalSettings/bookmarksLayout.tsx
+++ b/apps/web/lib/userLocalSettings/bookmarksLayout.tsx
@@ -14,12 +14,20 @@ export const UserLocalSettingsCtx = createContext<
bookmarkGridLayout: defaultLayout,
lang: fallbackLng,
gridColumns: 3,
+ showNotes: false,
});
function useUserLocalSettings() {
return useContext(UserLocalSettingsCtx);
}
+export function useBookmarkDisplaySettings() {
+ const settings = useUserLocalSettings();
+ return {
+ showNotes: settings.showNotes,
+ };
+}
+
export function useBookmarkLayout() {
const settings = useUserLocalSettings();
return settings.bookmarkGridLayout;
diff --git a/apps/web/lib/userLocalSettings/types.ts b/apps/web/lib/userLocalSettings/types.ts
index c87c8c33..54b75b80 100644
--- a/apps/web/lib/userLocalSettings/types.ts
+++ b/apps/web/lib/userLocalSettings/types.ts
@@ -9,6 +9,7 @@ export const zUserLocalSettings = z.object({
bookmarkGridLayout: zBookmarkGridLayout.optional().default("masonry"),
lang: z.string().optional().default("en"),
gridColumns: z.number().min(1).max(6).optional().default(3),
+ showNotes: z.boolean().optional().default(false),
});
export type UserLocalSettings = z.infer<typeof zUserLocalSettings>;
diff --git a/apps/web/lib/userLocalSettings/userLocalSettings.ts b/apps/web/lib/userLocalSettings/userLocalSettings.ts
index 11bd0a84..25c10e1b 100644
--- a/apps/web/lib/userLocalSettings/userLocalSettings.ts
+++ b/apps/web/lib/userLocalSettings/userLocalSettings.ts
@@ -48,3 +48,14 @@ export async function updateGridColumns(gridColumns: number) {
sameSite: "lax",
});
}
+
+export async function updateShowNotes(showNotes: boolean) {
+ const userSettings = (await cookies()).get(USER_LOCAL_SETTINGS_COOKIE_NAME);
+ const parsed = parseUserLocalSettings(userSettings?.value);
+ (await cookies()).set({
+ name: USER_LOCAL_SETTINGS_COOKIE_NAME,
+ value: JSON.stringify({ ...parsed, showNotes }),
+ maxAge: 34560000, // Chrome caps max age to 400 days
+ sameSite: "lax",
+ });
+}