From 9711baf54e7bb8e180f9daf2590d4a2368e569d0 Mon Sep 17 00:00:00 2001 From: Gemini Agent Date: Sat, 24 Jan 2026 12:12:41 +0000 Subject: [PATCH] Disable autosave - require explicit save button click Autosave was causing issues with race conditions creating duplicate entries. Simpler to just require manual save. Co-Authored-By: Claude Opus 4.5 --- src/components/EntryForm.tsx | 8 +- src/hooks/useEntry.ts | 147 ++++++++++------------------------- 2 files changed, 44 insertions(+), 111 deletions(-) diff --git a/src/components/EntryForm.tsx b/src/components/EntryForm.tsx index d462de5..cb7159e 100644 --- a/src/components/EntryForm.tsx +++ b/src/components/EntryForm.tsx @@ -14,7 +14,6 @@ interface EntryFormProps { export function EntryForm({ date, entryId, isNewEntry = false, onSaved }: EntryFormProps) { const { - entry, text, mood, roughDay, @@ -22,6 +21,7 @@ export function EntryForm({ date, entryId, isNewEntry = false, onSaved }: EntryF isLoading, isSaving, lastSaved, + isDirty, updateText, updateMood, updateRoughDay, @@ -146,13 +146,13 @@ export function EntryForm({ date, entryId, isNewEntry = false, onSaved }: EntryF Saving... - ) : lastSaved ? ( + ) : lastSaved && !isDirty ? ( <> Saved - ) : hasContent ? ( - Auto-saves as you type + ) : isDirty ? ( + Unsaved changes ) : null} diff --git a/src/hooks/useEntry.ts b/src/hooks/useEntry.ts index 0bee40f..b9d98b6 100644 --- a/src/hooks/useEntry.ts +++ b/src/hooks/useEntry.ts @@ -1,15 +1,13 @@ "use client"; import { useState, useEffect, useCallback, useRef } from "react"; -import { useDebounce } from "./useDebounce"; import type { EntryWithTags, CreateEntryRequest } from "@/lib/types"; import { getLocalDate } from "@/lib/utils/date"; -import { AUTOSAVE_DELAY } from "@/lib/constants"; interface UseEntryOptions { date?: string; entryId?: string; - isNewEntry?: boolean; // If true, start with blank form + isNewEntry?: boolean; } export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions = {}) { @@ -22,11 +20,10 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions const [isSaving, setIsSaving] = useState(false); const [isLoading, setIsLoading] = useState(!isNewEntry); const [lastSaved, setLastSaved] = useState(null); - const pendingSaveRef = useRef(false); - const savingRef = useRef(false); // Prevent concurrent saves - const entryIdRef = useRef(null); // Track entry ID across saves + const [isDirty, setIsDirty] = useState(false); + const entryIdRef = useRef(null); - // Load entry (only if editing existing or viewing by date) + // Load entry (only if editing existing) useEffect(() => { if (isNewEntry) { setIsLoading(false); @@ -63,23 +60,15 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions loadEntry(); }, [targetDate, entryId, isNewEntry]); - // Save function + // Save function - only called explicitly const save = useCallback(async () => { if (!text.trim()) return null; - // Prevent concurrent saves - if (savingRef.current) { - pendingSaveRef.current = true; // Mark that we need to save again - return null; - } - - savingRef.current = true; setIsSaving(true); - pendingSaveRef.current = false; try { const body: CreateEntryRequest = { - id: entryIdRef.current || undefined, // Use ref to always have latest ID + id: entryIdRef.current || undefined, date: entry?.date || targetDate, text: text.trim(), mood, @@ -96,120 +85,63 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions if (res.ok) { const saved: EntryWithTags = await res.json(); setEntry(saved); - entryIdRef.current = saved.id; // Update ref immediately + entryIdRef.current = saved.id; setLastSaved(new Date()); + setIsDirty(false); return saved; } } catch (error) { console.error("Failed to save entry:", error); } finally { - savingRef.current = false; setIsSaving(false); - - // If there were changes while saving, trigger another save - if (pendingSaveRef.current) { - pendingSaveRef.current = false; - // Use setTimeout to avoid immediate recursion - setTimeout(() => { - pendingSaveRef.current = true; - debouncedSave(); - }, 100); - } } return null; }, [text, mood, roughDay, tagNames, entry?.date, targetDate]); - // Debounced save - const debouncedSave = useDebounce(() => { - if (pendingSaveRef.current) { - save(); + // Update handlers - no autosave, just mark dirty + const updateText = useCallback((value: string) => { + setText(value); + setIsDirty(true); + }, []); + + const updateMood = useCallback((value: number | null) => { + setMood(value); + setIsDirty(true); + }, []); + + const updateRoughDay = useCallback((value: boolean) => { + setRoughDay(value); + setIsDirty(true); + }, []); + + const addTag = useCallback((name: string) => { + const normalized = name.toLowerCase().trim(); + if (normalized) { + setTagNames((prev) => { + if (prev.includes(normalized)) return prev; + return [...prev, normalized]; + }); + setIsDirty(true); } - }, AUTOSAVE_DELAY); + }, []); - // Mark pending and trigger debounced save - const markDirty = useCallback(() => { - pendingSaveRef.current = true; - debouncedSave(); - }, [debouncedSave]); - - // Update handlers that trigger autosave - const updateText = useCallback( - (value: string) => { - setText(value); - markDirty(); - }, - [markDirty] - ); - - const updateMood = useCallback( - (value: number | null) => { - setMood(value); - markDirty(); - }, - [markDirty] - ); - - const updateRoughDay = useCallback( - (value: boolean) => { - setRoughDay(value); - markDirty(); - }, - [markDirty] - ); - - const addTag = useCallback( - (name: string) => { - const normalized = name.toLowerCase().trim(); - if (normalized && !tagNames.includes(normalized)) { - setTagNames((prev) => [...prev, normalized]); - markDirty(); - } - }, - [tagNames, markDirty] - ); - - const removeTag = useCallback( - (name: string) => { - setTagNames((prev) => prev.filter((t) => t !== name)); - markDirty(); - }, - [markDirty] - ); + const removeTag = useCallback((name: string) => { + setTagNames((prev) => prev.filter((t) => t !== name)); + setIsDirty(true); + }, []); // Reset form for new entry const reset = useCallback(() => { setEntry(null); - entryIdRef.current = null; // Clear the ID ref + entryIdRef.current = null; setText(""); setMood(null); setRoughDay(false); setTagNames([]); setLastSaved(null); - pendingSaveRef.current = false; - savingRef.current = false; + setIsDirty(false); }, []); - // Save on unmount if pending - useEffect(() => { - return () => { - if (pendingSaveRef.current && text.trim()) { - // Fire and forget - fetch("/api/entries", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - id: entryIdRef.current || undefined, - date: entry?.date || targetDate, - text: text.trim(), - mood, - roughDay, - tagNames, - }), - }).catch(() => {}); - } - }; - }, [text, mood, roughDay, tagNames, entry?.date, targetDate]); - return { entry, text, @@ -219,6 +151,7 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions isLoading, isSaving, lastSaved, + isDirty, updateText, updateMood, updateRoughDay,