From 20cdbd69eb290400c2b8ac61ba6a95b3ed04299b Mon Sep 17 00:00:00 2001 From: Nirodan Date: Wed, 6 May 2026 10:19:40 +0200 Subject: [PATCH] Fix Notes feature: correct auto-save logic and missing CSS Functional fixes: - Add isDirtyRef to distinguish user edits from note selection; auto-save now only fires after the user actually modifies content, not on every click - Add selected and isNew to auto-save effect deps so the closure is never stale when switching between notes (previously could save to the wrong note) - Double-check isDirtyRef inside the debounce callback to handle rapid note switches within the 1-second window - Reset isDirtyRef to false after every successful save (manual and auto) CSS fixes: - Add button.ghost and button.ghost.danger styles to buttons.css; delete button was invisible/identical to the save button because .ghost was never defined - Add select element styling to base.css to match input/textarea theming; language picker was unstyled browser-default in both dark and light mode - Add .error class (red text) to base.css; error messages had no colour - Add .notes-layout grid class with responsive media query (stacks below 680px); remove inline gridTemplateColumns that overflowed on small screens Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/NotesTool.jsx | 37 +++++++++++++++------------ frontend/src/css/base.css | 35 +++++++++++++++++++++++-- frontend/src/css/buttons.css | 25 ++++++++++++++++++ 3 files changed, 79 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/NotesTool.jsx b/frontend/src/components/NotesTool.jsx index 2513d44..5223f82 100644 --- a/frontend/src/components/NotesTool.jsx +++ b/frontend/src/components/NotesTool.jsx @@ -20,6 +20,9 @@ function NotesTool() { const [confirmDelete, setConfirmDelete] = useState(false); const [error, setError] = useState(''); const debounceRef = useRef(null); + // Tracks whether the user has actually edited the current note. + // Prevents auto-save from firing just because a note was selected. + const isDirtyRef = useRef(false); const isNew = selected === null; const loadNotes = useCallback(async () => { @@ -34,6 +37,7 @@ function NotesTool() { useEffect(() => { loadNotes(); }, [loadNotes]); const selectNote = (note) => { + isDirtyRef.current = false; setSelected(note.id); setTitle(note.title); setContent(note.content || ''); @@ -44,6 +48,7 @@ function NotesTool() { }; const newNote = () => { + isDirtyRef.current = false; setSelected(null); setTitle('Neue Notiz'); setContent(''); @@ -58,10 +63,12 @@ function NotesTool() { try { if (isNew) { const res = await axios.post('/api/notes', { title, content, language }); + isDirtyRef.current = false; setSelected(res.data.id); await loadNotes(); } else { await axios.put(`/api/notes/${selected}`, { title, content, language }); + isDirtyRef.current = false; await loadNotes(); } setSaved(true); @@ -82,20 +89,23 @@ function NotesTool() { } }; - // Debounced auto-save for existing notes + // Auto-save for existing notes — only fires when the user actually edited content. + // selected is in deps so the closure is never stale when switching between notes. useEffect(() => { - if (isNew) return; + if (isNew || !isDirtyRef.current) return; clearTimeout(debounceRef.current); debounceRef.current = setTimeout(async () => { + if (!isDirtyRef.current) return; try { await axios.put(`/api/notes/${selected}`, { title, content, language }); + isDirtyRef.current = false; await loadNotes(); setSaved(true); setTimeout(() => setSaved(false), 1500); - } catch { /* silent */ } + } catch { /* silent — manual save still available */ } }, 1000); return () => clearTimeout(debounceRef.current); - }, [title, content, language]); // eslint-disable-line react-hooks/exhaustive-deps + }, [title, content, language, selected, isNew, loadNotes]); const formatDate = (iso) => { if (!iso) return ''; @@ -107,9 +117,9 @@ function NotesTool() { return (
-

Notizen & Snippets

+

Notizen & Snippets

-
+
{/* Sidebar */}
{ setTitle(e.target.value); setSaved(false); }} + onChange={(e) => { isDirtyRef.current = true; setTitle(e.target.value); setSaved(false); }} placeholder="Titel" style={{ flex: 1, margin: 0, minWidth: '150px' }} />