aboutsummaryrefslogtreecommitdiffstats
path: root/apps/browser-extension
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-11-08 18:52:30 +0000
committerGitHub <noreply@github.com>2025-11-08 18:52:30 +0000
commit098e56a8950efbef79e551e12622ae7c8cd90c03 (patch)
tree6d6f98b4e69ede48ea6efc5101065a3f05fccbdc /apps/browser-extension
parenta2203196ff3353a6f7efaac5df25844880807baf (diff)
downloadkarakeep-098e56a8950efbef79e551e12622ae7c8cd90c03.tar.zst
feat(extension): Allow writing notes directly in the extension (#2104)
* feat(extension): add notes editor to bookmark hoarded screen Adds the ability to directly add and edit notes for bookmarks in the browser extension's hoarded screen (the page shown after saving a bookmark). Changes: - Created Textarea UI component for the browser extension - Created NoteEditor component that uses useUpdateBookmark hook - Added Notes section to BookmarkSavedPage, displayed between the header and tags - Notes auto-save when the user clicks away from the textarea (onBlur) - Shows saving state and error messages to the user This brings feature parity with the web app's notes functionality. * add explicit button * more fixes --------- Co-authored-by: Claude <noreply@anthropic.com>
Diffstat (limited to 'apps/browser-extension')
-rw-r--r--apps/browser-extension/src/BookmarkSavedPage.tsx4
-rw-r--r--apps/browser-extension/src/components/NoteEditor.tsx105
-rw-r--r--apps/browser-extension/src/components/ui/textarea.tsx23
3 files changed, 132 insertions, 0 deletions
diff --git a/apps/browser-extension/src/BookmarkSavedPage.tsx b/apps/browser-extension/src/BookmarkSavedPage.tsx
index 67e6f753..3be5f9d0 100644
--- a/apps/browser-extension/src/BookmarkSavedPage.tsx
+++ b/apps/browser-extension/src/BookmarkSavedPage.tsx
@@ -6,6 +6,7 @@ import { useDeleteBookmark } from "@karakeep/shared-react/hooks/bookmarks";
import BookmarkLists from "./components/BookmarkLists";
import { ListsSelector } from "./components/ListsSelector";
+import { NoteEditor } from "./components/NoteEditor";
import TagList from "./components/TagList";
import { TagsSelector } from "./components/TagsSelector";
import { Button, buttonVariants } from "./components/ui/button";
@@ -79,6 +80,9 @@ export default function BookmarkSavedPage() {
</div>
</div>
<hr />
+ <p className="text-lg">Notes</p>
+ <NoteEditor bookmarkId={bookmarkId} />
+ <hr />
<p className="text-lg">Tags</p>
<TagList bookmarkId={bookmarkId} />
<TagsSelector bookmarkId={bookmarkId} />
diff --git a/apps/browser-extension/src/components/NoteEditor.tsx b/apps/browser-extension/src/components/NoteEditor.tsx
new file mode 100644
index 00000000..15f1515b
--- /dev/null
+++ b/apps/browser-extension/src/components/NoteEditor.tsx
@@ -0,0 +1,105 @@
+import { useEffect, useState } from "react";
+import { Check, Save } from "lucide-react";
+
+import {
+ useAutoRefreshingBookmarkQuery,
+ useUpdateBookmark,
+} from "@karakeep/shared-react/hooks/bookmarks";
+
+import { Button } from "./ui/button";
+import { Textarea } from "./ui/textarea";
+
+export function NoteEditor({ bookmarkId }: { bookmarkId: string }) {
+ const { data: bookmark } = useAutoRefreshingBookmarkQuery({ bookmarkId });
+ const [error, setError] = useState<string | null>(null);
+ const [isSaving, setIsSaving] = useState(false);
+ const [noteValue, setNoteValue] = useState("");
+ const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
+
+ // Update local state when bookmark changes, but only if there are no unsaved changes
+ // This prevents overwriting user's edits while they're typing
+ useEffect(() => {
+ if (bookmark && !hasUnsavedChanges) {
+ setNoteValue(bookmark.note ?? "");
+ }
+ }, [bookmark?.note, bookmark, hasUnsavedChanges]);
+
+ const updateBookmarkMutator = useUpdateBookmark({
+ onSuccess: () => {
+ setError(null);
+ setIsSaving(false);
+ setHasUnsavedChanges(false);
+ },
+ onError: (e) => {
+ setError(e.message || "Failed to save note");
+ setIsSaving(false);
+ },
+ });
+
+ const handleSave = () => {
+ if (!bookmark || noteValue === bookmark.note || isSaving) {
+ return;
+ }
+ setIsSaving(true);
+ setError(null);
+ updateBookmarkMutator.mutate({
+ bookmarkId: bookmark.id,
+ note: noteValue,
+ });
+ };
+
+ if (!bookmark) {
+ return null;
+ }
+
+ return (
+ <div className="flex flex-col gap-2">
+ <Textarea
+ className="h-32 w-full overflow-auto rounded bg-background p-2 text-sm text-gray-400 dark:text-gray-300"
+ value={noteValue}
+ placeholder="Write some notes ..."
+ onChange={(e) => {
+ setNoteValue(e.currentTarget.value);
+ setHasUnsavedChanges(e.currentTarget.value !== bookmark.note);
+ }}
+ />
+ <div className="flex items-center justify-between gap-2">
+ <div className="flex-1">
+ {isSaving && <p className="text-xs text-gray-500">Saving note...</p>}
+ {error && <p className="text-xs text-red-500">{error}</p>}
+ {!isSaving && !error && hasUnsavedChanges && (
+ <p className="text-xs text-amber-600 dark:text-amber-500">
+ Unsaved changes
+ </p>
+ )}
+ </div>
+ {hasUnsavedChanges && (
+ <Button
+ onClick={handleSave}
+ disabled={isSaving}
+ size="sm"
+ className="gap-1.5"
+ >
+ {isSaving ? (
+ <>
+ <Save className="h-3.5 w-3.5 animate-pulse" />
+ Saving...
+ </>
+ ) : (
+ <>
+ <Save className="h-3.5 w-3.5" />
+ Save Note
+ </>
+ )}
+ </Button>
+ )}
+ {!hasUnsavedChanges && !isSaving && noteValue && (
+ <div className="flex items-center gap-1 text-xs text-green-600 dark:text-green-500">
+ <Check className="h-3.5 w-3.5" />
+ Saved
+ </div>
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/apps/browser-extension/src/components/ui/textarea.tsx b/apps/browser-extension/src/components/ui/textarea.tsx
new file mode 100644
index 00000000..6651e331
--- /dev/null
+++ b/apps/browser-extension/src/components/ui/textarea.tsx
@@ -0,0 +1,23 @@
+import * as React from "react";
+
+import { cn } from "../../utils/css";
+
+export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
+
+const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
+ ({ className, ...props }, ref) => {
+ return (
+ <textarea
+ className={cn(
+ "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
+ className,
+ )}
+ ref={ref}
+ {...props}
+ />
+ );
+ },
+);
+Textarea.displayName = "Textarea";
+
+export { Textarea };