mirror of
https://github.com/karakeep-app/karakeep.git
synced 2025-12-12 20:35:52 +01:00
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>
This commit is contained in:
@@ -19,3 +19,6 @@ yarn.lock
|
|||||||
|
|
||||||
# Expo build
|
# Expo build
|
||||||
**/.expo/**
|
**/.expo/**
|
||||||
|
|
||||||
|
apps/mobile/android
|
||||||
|
apps/mobile/ios
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { ActivityIndicator, Pressable, View } from "react-native";
|
import { ActivityIndicator, Pressable, Switch, View } from "react-native";
|
||||||
import { Slider } from "react-native-awesome-slider";
|
import { Slider } from "react-native-awesome-slider";
|
||||||
import { useSharedValue } from "react-native-reanimated";
|
import { useSharedValue } from "react-native-reanimated";
|
||||||
import { Link } from "expo-router";
|
import { Link } from "expo-router";
|
||||||
@@ -87,6 +87,18 @@ export default function Dashboard() {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</Link>
|
</Link>
|
||||||
</View>
|
</View>
|
||||||
|
<View className="flex w-full flex-row items-center justify-between gap-8 rounded-lg bg-card px-4 py-2">
|
||||||
|
<Text>Show note preview in bookmark</Text>
|
||||||
|
<Switch
|
||||||
|
value={settings.showNotes}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
showNotes: value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
<Text className="w-full p-1 text-2xl font-bold text-foreground">
|
<Text className="w-full p-1 text-2xl font-bold text-foreground">
|
||||||
Upload Settings
|
Upload Settings
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import React from "react";
|
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Alert,
|
Alert,
|
||||||
@@ -37,6 +36,7 @@ import { Skeleton } from "../ui/Skeleton";
|
|||||||
import { useToast } from "../ui/Toast";
|
import { useToast } from "../ui/Toast";
|
||||||
import BookmarkAssetImage from "./BookmarkAssetImage";
|
import BookmarkAssetImage from "./BookmarkAssetImage";
|
||||||
import BookmarkTextMarkdown from "./BookmarkTextMarkdown";
|
import BookmarkTextMarkdown from "./BookmarkTextMarkdown";
|
||||||
|
import { NotePreview } from "./NotePreview";
|
||||||
import TagPill from "./TagPill";
|
import TagPill from "./TagPill";
|
||||||
|
|
||||||
function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
|
function ActionBar({ bookmark }: { bookmark: ZBookmark }) {
|
||||||
@@ -285,6 +285,7 @@ function LinkCard({
|
|||||||
throw new Error("Wrong content type rendered");
|
throw new Error("Wrong content type rendered");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const note = settings.showNotes ? bookmark.note?.trim() : undefined;
|
||||||
const url = bookmark.content.url;
|
const url = bookmark.content.url;
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
|
|
||||||
@@ -329,6 +330,7 @@ function LinkCard({
|
|||||||
>
|
>
|
||||||
{bookmark.title ?? bookmark.content.title ?? parsedUrl.host}
|
{bookmark.title ?? bookmark.content.title ?? parsedUrl.host}
|
||||||
</Text>
|
</Text>
|
||||||
|
{note && <NotePreview note={note} bookmarkId={bookmark.id} />}
|
||||||
<TagList bookmark={bookmark} />
|
<TagList bookmark={bookmark} />
|
||||||
<Divider orientation="vertical" className="mt-2 h-0.5 w-full" />
|
<Divider orientation="vertical" className="mt-2 h-0.5 w-full" />
|
||||||
<View className="mt-2 flex flex-row justify-between px-2 pb-2">
|
<View className="mt-2 flex flex-row justify-between px-2 pb-2">
|
||||||
@@ -347,9 +349,11 @@ function TextCard({
|
|||||||
bookmark: ZBookmark;
|
bookmark: ZBookmark;
|
||||||
onOpenBookmark: () => void;
|
onOpenBookmark: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { settings } = useAppSettings();
|
||||||
if (bookmark.content.type !== BookmarkTypes.TEXT) {
|
if (bookmark.content.type !== BookmarkTypes.TEXT) {
|
||||||
throw new Error("Wrong content type rendered");
|
throw new Error("Wrong content type rendered");
|
||||||
}
|
}
|
||||||
|
const note = settings.showNotes ? bookmark.note?.trim() : undefined;
|
||||||
const content = bookmark.content.text;
|
const content = bookmark.content.text;
|
||||||
return (
|
return (
|
||||||
<View className="flex max-h-96 gap-2 p-2">
|
<View className="flex max-h-96 gap-2 p-2">
|
||||||
@@ -365,6 +369,7 @@ function TextCard({
|
|||||||
<BookmarkTextMarkdown text={content} />
|
<BookmarkTextMarkdown text={content} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
{note && <NotePreview note={note} bookmarkId={bookmark.id} />}
|
||||||
<TagList bookmark={bookmark} />
|
<TagList bookmark={bookmark} />
|
||||||
<Divider orientation="vertical" className="mt-2 h-0.5 w-full" />
|
<Divider orientation="vertical" className="mt-2 h-0.5 w-full" />
|
||||||
<View className="flex flex-row justify-between p-2">
|
<View className="flex flex-row justify-between p-2">
|
||||||
@@ -382,9 +387,11 @@ function AssetCard({
|
|||||||
bookmark: ZBookmark;
|
bookmark: ZBookmark;
|
||||||
onOpenBookmark: () => void;
|
onOpenBookmark: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const { settings } = useAppSettings();
|
||||||
if (bookmark.content.type !== BookmarkTypes.ASSET) {
|
if (bookmark.content.type !== BookmarkTypes.ASSET) {
|
||||||
throw new Error("Wrong content type rendered");
|
throw new Error("Wrong content type rendered");
|
||||||
}
|
}
|
||||||
|
const note = settings.showNotes ? bookmark.note?.trim() : undefined;
|
||||||
const title = bookmark.title ?? bookmark.content.fileName;
|
const title = bookmark.title ?? bookmark.content.fileName;
|
||||||
|
|
||||||
const assetImage =
|
const assetImage =
|
||||||
@@ -405,6 +412,7 @@ function AssetCard({
|
|||||||
<Text className="line-clamp-2 text-xl font-bold">{title}</Text>
|
<Text className="line-clamp-2 text-xl font-bold">{title}</Text>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
{note && <NotePreview note={note} bookmarkId={bookmark.id} />}
|
||||||
<TagList bookmark={bookmark} />
|
<TagList bookmark={bookmark} />
|
||||||
<Divider orientation="vertical" className="mt-2 h-0.5 w-full" />
|
<Divider orientation="vertical" className="mt-2 h-0.5 w-full" />
|
||||||
<View className="mt-2 flex flex-row justify-between px-2 pb-2">
|
<View className="mt-2 flex flex-row justify-between px-2 pb-2">
|
||||||
|
|||||||
83
apps/mobile/components/bookmarks/NotePreview.tsx
Normal file
83
apps/mobile/components/bookmarks/NotePreview.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Modal, Pressable, ScrollView, View } from "react-native";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { ExternalLink, NotepadText, X } from "lucide-react-native";
|
||||||
|
import { useColorScheme } from "nativewind";
|
||||||
|
|
||||||
|
import { Button } from "../ui/Button";
|
||||||
|
import { Text } from "../ui/Text";
|
||||||
|
|
||||||
|
interface NotePreviewProps {
|
||||||
|
note: string;
|
||||||
|
bookmarkId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NotePreview({ note, bookmarkId }: NotePreviewProps) {
|
||||||
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
|
const { colorScheme } = useColorScheme();
|
||||||
|
const iconColor = colorScheme === "dark" ? "#9ca3af" : "#6b7280";
|
||||||
|
const modalIconColor = colorScheme === "dark" ? "#d1d5db" : "#374151";
|
||||||
|
|
||||||
|
if (!note?.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Pressable onPress={() => setIsModalVisible(true)}>
|
||||||
|
<View className="flex flex-row items-center gap-2">
|
||||||
|
<NotepadText size={24} color={iconColor} />
|
||||||
|
<Text
|
||||||
|
className="flex-1 text-sm italic text-gray-500 dark:text-gray-400"
|
||||||
|
numberOfLines={2}
|
||||||
|
>
|
||||||
|
{note}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
visible={isModalVisible}
|
||||||
|
transparent
|
||||||
|
animationType="slide"
|
||||||
|
onRequestClose={() => setIsModalVisible(false)}
|
||||||
|
>
|
||||||
|
<View className="flex-1 justify-end bg-black/50">
|
||||||
|
<View className="max-h-[80%] rounded-t-3xl bg-card p-6">
|
||||||
|
{/* Header */}
|
||||||
|
<View className="mb-4 flex flex-row items-center justify-between">
|
||||||
|
<Text className="text-lg font-semibold">Note</Text>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setIsModalVisible(false)}
|
||||||
|
className="p-2"
|
||||||
|
>
|
||||||
|
<X size={24} color={modalIconColor} />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Note Content */}
|
||||||
|
<ScrollView className="mb-4 max-h-96">
|
||||||
|
<Text className="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{note}
|
||||||
|
</Text>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Action Button */}
|
||||||
|
<View className="flex flex-row justify-end border-t border-border pt-4">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onPress={() => {
|
||||||
|
setIsModalVisible(false);
|
||||||
|
router.push(`/dashboard/bookmarks/${bookmarkId}/info`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text className="text-sm">Edit Notes</Text>
|
||||||
|
<ExternalLink size={14} color={modalIconColor} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ const zSettingsSchema = z.object({
|
|||||||
.enum(["reader", "browser"])
|
.enum(["reader", "browser"])
|
||||||
.optional()
|
.optional()
|
||||||
.default("reader"),
|
.default("reader"),
|
||||||
|
showNotes: z.boolean().optional().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Settings = z.infer<typeof zSettingsSchema>;
|
export type Settings = z.infer<typeof zSettingsSchema>;
|
||||||
@@ -32,6 +33,7 @@ const useSettings = create<AppSettingsState>((set, get) => ({
|
|||||||
imageQuality: 0.2,
|
imageQuality: 0.2,
|
||||||
theme: "system",
|
theme: "system",
|
||||||
defaultBookmarkView: "reader",
|
defaultBookmarkView: "reader",
|
||||||
|
showNotes: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
setSettings: async (settings) => {
|
setSettings: async (settings) => {
|
||||||
|
|||||||
@@ -27,6 +27,8 @@
|
|||||||
".next",
|
".next",
|
||||||
"dist",
|
"dist",
|
||||||
"build",
|
"build",
|
||||||
|
"public/sw.js",
|
||||||
|
"public/workbox-*.js",
|
||||||
"pnpm-lock.yaml"
|
"pnpm-lock.yaml"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import { createElement, useEffect, useState } from "react";
|
||||||
import { ButtonWithTooltip } from "@/components/ui/button";
|
import { ButtonWithTooltip } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@@ -9,14 +9,19 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { useTranslation } from "@/lib/i18n/client";
|
||||||
import {
|
import {
|
||||||
|
useBookmarkDisplaySettings,
|
||||||
useBookmarkLayout,
|
useBookmarkLayout,
|
||||||
useGridColumns,
|
useGridColumns,
|
||||||
} from "@/lib/userLocalSettings/bookmarksLayout";
|
} from "@/lib/userLocalSettings/bookmarksLayout";
|
||||||
import {
|
import {
|
||||||
updateBookmarksLayout,
|
updateBookmarksLayout,
|
||||||
updateGridColumns,
|
updateGridColumns,
|
||||||
|
updateShowNotes,
|
||||||
} from "@/lib/userLocalSettings/userLocalSettings";
|
} from "@/lib/userLocalSettings/userLocalSettings";
|
||||||
import {
|
import {
|
||||||
Check,
|
Check,
|
||||||
@@ -25,6 +30,7 @@ import {
|
|||||||
LayoutList,
|
LayoutList,
|
||||||
List,
|
List,
|
||||||
LucideIcon,
|
LucideIcon,
|
||||||
|
NotepadText,
|
||||||
Settings,
|
Settings,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
@@ -37,22 +43,17 @@ const iconMap: Record<LayoutType, LucideIcon> = {
|
|||||||
compact: List,
|
compact: List,
|
||||||
};
|
};
|
||||||
|
|
||||||
const layoutNames: Record<LayoutType, string> = {
|
|
||||||
masonry: "Masonry",
|
|
||||||
grid: "Grid",
|
|
||||||
list: "List",
|
|
||||||
compact: "Compact",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ViewOptions() {
|
export default function ViewOptions() {
|
||||||
|
const { t } = useTranslation();
|
||||||
const layout = useBookmarkLayout();
|
const layout = useBookmarkLayout();
|
||||||
const gridColumns = useGridColumns();
|
const gridColumns = useGridColumns();
|
||||||
const [tempColumns, setTempColumns] = React.useState(gridColumns);
|
const { showNotes } = useBookmarkDisplaySettings();
|
||||||
|
const [tempColumns, setTempColumns] = useState(gridColumns);
|
||||||
|
|
||||||
const showColumnSlider = layout === "grid" || layout === "masonry";
|
const showColumnSlider = layout === "grid" || layout === "masonry";
|
||||||
|
|
||||||
// Update temp value when actual value changes
|
// Update temp value when actual value changes
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
setTempColumns(gridColumns);
|
setTempColumns(gridColumns);
|
||||||
}, [gridColumns]);
|
}, [gridColumns]);
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ export default function ViewOptions() {
|
|||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<ButtonWithTooltip
|
<ButtonWithTooltip
|
||||||
tooltip="View Options"
|
tooltip={t("view_options.title")}
|
||||||
delayDuration={100}
|
delayDuration={100}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
@@ -68,7 +69,9 @@ export default function ViewOptions() {
|
|||||||
</ButtonWithTooltip>
|
</ButtonWithTooltip>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56">
|
<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) => (
|
{(Object.keys(iconMap) as LayoutType[]).map((key) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={key}
|
key={key}
|
||||||
@@ -76,8 +79,8 @@ export default function ViewOptions() {
|
|||||||
onClick={async () => await updateBookmarksLayout(key as LayoutType)}
|
onClick={async () => await updateBookmarksLayout(key as LayoutType)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{React.createElement(iconMap[key as LayoutType], { size: 18 })}
|
{createElement(iconMap[key as LayoutType], { size: 18 })}
|
||||||
<span>{layoutNames[key]}</span>
|
<span>{t(`layouts.${key}`)}</span>
|
||||||
</div>
|
</div>
|
||||||
{layout === key && <Check className="ml-2 size-4" />}
|
{layout === key && <Check className="ml-2 size-4" />}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -88,7 +91,9 @@ export default function ViewOptions() {
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<div className="px-2 py-3">
|
<div className="px-2 py-3">
|
||||||
<div className="mb-2 flex items-center justify-between">
|
<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">
|
<span className="text-sm text-muted-foreground">
|
||||||
{tempColumns}
|
{tempColumns}
|
||||||
</span>
|
</span>
|
||||||
@@ -109,6 +114,28 @@ export default function ViewOptions() {
|
|||||||
</div>
|
</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>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import type { BookmarksLayoutTypes } from "@/lib/userLocalSettings/types";
|
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 Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import useBulkActionsStore from "@/lib/bulkActions";
|
import useBulkActionsStore from "@/lib/bulkActions";
|
||||||
import {
|
import {
|
||||||
bookmarkLayoutSwitch,
|
bookmarkLayoutSwitch,
|
||||||
|
useBookmarkDisplaySettings,
|
||||||
useBookmarkLayout,
|
useBookmarkLayout,
|
||||||
} from "@/lib/userLocalSettings/bookmarksLayout";
|
} from "@/lib/userLocalSettings/bookmarksLayout";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -17,14 +21,15 @@ import { isBookmarkStillTagging } from "@karakeep/shared/utils/bookmarkUtils";
|
|||||||
|
|
||||||
import BookmarkActionBar from "./BookmarkActionBar";
|
import BookmarkActionBar from "./BookmarkActionBar";
|
||||||
import BookmarkFormattedCreatedAt from "./BookmarkFormattedCreatedAt";
|
import BookmarkFormattedCreatedAt from "./BookmarkFormattedCreatedAt";
|
||||||
|
import { NotePreview } from "./NotePreview";
|
||||||
import TagList from "./TagList";
|
import TagList from "./TagList";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
bookmark: ZBookmark;
|
bookmark: ZBookmark;
|
||||||
image: (layout: BookmarksLayoutTypes, className: string) => React.ReactNode;
|
image: (layout: BookmarksLayoutTypes, className: string) => ReactNode;
|
||||||
title?: React.ReactNode;
|
title?: ReactNode;
|
||||||
content?: React.ReactNode;
|
content?: ReactNode;
|
||||||
footer?: React.ReactNode;
|
footer?: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
fitHeight?: boolean;
|
fitHeight?: boolean;
|
||||||
wrapTags: boolean;
|
wrapTags: boolean;
|
||||||
@@ -34,7 +39,7 @@ function BottomRow({
|
|||||||
footer,
|
footer,
|
||||||
bookmark,
|
bookmark,
|
||||||
}: {
|
}: {
|
||||||
footer?: React.ReactNode;
|
footer?: ReactNode;
|
||||||
bookmark: ZBookmark;
|
bookmark: ZBookmark;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -112,6 +117,9 @@ function ListView({
|
|||||||
footer,
|
footer,
|
||||||
className,
|
className,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const { showNotes } = useBookmarkDisplaySettings();
|
||||||
|
const note = showNotes ? bookmark.note?.trim() : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -131,6 +139,7 @@ function ListView({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{content && <div className="shrink-1 overflow-hidden">{content}</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">
|
<div className="flex shrink-0 flex-wrap gap-1 overflow-hidden">
|
||||||
<TagList
|
<TagList
|
||||||
bookmark={bookmark}
|
bookmark={bookmark}
|
||||||
@@ -155,7 +164,9 @@ function GridView({
|
|||||||
layout,
|
layout,
|
||||||
fitHeight = false,
|
fitHeight = false,
|
||||||
}: Props & { layout: BookmarksLayoutTypes }) {
|
}: 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -166,7 +177,7 @@ function GridView({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<MultiBookmarkSelector bookmark={bookmark} />
|
<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="flex h-full flex-col justify-between gap-2 overflow-hidden p-2">
|
||||||
<div className="grow-1 flex flex-col gap-2 overflow-hidden">
|
<div className="grow-1 flex flex-col gap-2 overflow-hidden">
|
||||||
{title && (
|
{title && (
|
||||||
@@ -175,6 +186,7 @@ function GridView({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{content && <div className="shrink-1 overflow-hidden">{content}</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">
|
<div className="flex shrink-0 flex-wrap gap-1 overflow-hidden">
|
||||||
<TagList
|
<TagList
|
||||||
className={wrapTags ? undefined : "h-full"}
|
className={wrapTags ? undefined : "h-full"}
|
||||||
|
|||||||
66
apps/web/components/dashboard/bookmarks/NotePreview.tsx
Normal file
66
apps/web/components/dashboard/bookmarks/NotePreview.tsx
Normal file
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -47,6 +47,13 @@
|
|||||||
"list": "List",
|
"list": "List",
|
||||||
"compact": "Compact"
|
"compact": "Compact"
|
||||||
},
|
},
|
||||||
|
"view_options": {
|
||||||
|
"title": "View Options",
|
||||||
|
"layout": "Layout",
|
||||||
|
"columns": "Columns",
|
||||||
|
"display_options": "Display Options",
|
||||||
|
"show_note_previews": "Show Notes"
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"change_layout": "Change Layout",
|
"change_layout": "Change Layout",
|
||||||
"archive": "Archive",
|
"archive": "Archive",
|
||||||
@@ -59,6 +66,7 @@
|
|||||||
"recrawl": "Recrawl",
|
"recrawl": "Recrawl",
|
||||||
"download_full_page_archive": "Download Full Page Archive",
|
"download_full_page_archive": "Download Full Page Archive",
|
||||||
"edit_tags": "Edit Tags",
|
"edit_tags": "Edit Tags",
|
||||||
|
"edit_notes": "Edit Notes",
|
||||||
"add_to_list": "Add to List",
|
"add_to_list": "Add to List",
|
||||||
"select_all": "Select All",
|
"select_all": "Select All",
|
||||||
"unselect_all": "Unselect All",
|
"unselect_all": "Unselect All",
|
||||||
|
|||||||
@@ -14,12 +14,20 @@ export const UserLocalSettingsCtx = createContext<
|
|||||||
bookmarkGridLayout: defaultLayout,
|
bookmarkGridLayout: defaultLayout,
|
||||||
lang: fallbackLng,
|
lang: fallbackLng,
|
||||||
gridColumns: 3,
|
gridColumns: 3,
|
||||||
|
showNotes: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
function useUserLocalSettings() {
|
function useUserLocalSettings() {
|
||||||
return useContext(UserLocalSettingsCtx);
|
return useContext(UserLocalSettingsCtx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useBookmarkDisplaySettings() {
|
||||||
|
const settings = useUserLocalSettings();
|
||||||
|
return {
|
||||||
|
showNotes: settings.showNotes,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function useBookmarkLayout() {
|
export function useBookmarkLayout() {
|
||||||
const settings = useUserLocalSettings();
|
const settings = useUserLocalSettings();
|
||||||
return settings.bookmarkGridLayout;
|
return settings.bookmarkGridLayout;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const zUserLocalSettings = z.object({
|
|||||||
bookmarkGridLayout: zBookmarkGridLayout.optional().default("masonry"),
|
bookmarkGridLayout: zBookmarkGridLayout.optional().default("masonry"),
|
||||||
lang: z.string().optional().default("en"),
|
lang: z.string().optional().default("en"),
|
||||||
gridColumns: z.number().min(1).max(6).optional().default(3),
|
gridColumns: z.number().min(1).max(6).optional().default(3),
|
||||||
|
showNotes: z.boolean().optional().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UserLocalSettings = z.infer<typeof zUserLocalSettings>;
|
export type UserLocalSettings = z.infer<typeof zUserLocalSettings>;
|
||||||
|
|||||||
@@ -48,3 +48,14 @@ export async function updateGridColumns(gridColumns: number) {
|
|||||||
sameSite: "lax",
|
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",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user