aboutsummaryrefslogtreecommitdiffstats
path: root/apps/browser-extension/src/components/NoteEditor.tsx
blob: 15f1515b2e4d2da60f8029fa1729448d2dfd0556 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
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>
  );
}