diff options
Diffstat (limited to 'apps/browser-extension/src')
| -rw-r--r-- | apps/browser-extension/src/BookmarkSavedPage.tsx | 4 | ||||
| -rw-r--r-- | apps/browser-extension/src/components/NoteEditor.tsx | 105 | ||||
| -rw-r--r-- | apps/browser-extension/src/components/ui/textarea.tsx | 23 |
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 }; |
