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 <noreply@anthropic.com>
This commit is contained in:
Gemini Agent
2026-01-24 12:12:41 +00:00
parent e99401858f
commit 9711baf54e
2 changed files with 44 additions and 111 deletions

View File

@@ -14,7 +14,6 @@ interface EntryFormProps {
export function EntryForm({ date, entryId, isNewEntry = false, onSaved }: EntryFormProps) { export function EntryForm({ date, entryId, isNewEntry = false, onSaved }: EntryFormProps) {
const { const {
entry,
text, text,
mood, mood,
roughDay, roughDay,
@@ -22,6 +21,7 @@ export function EntryForm({ date, entryId, isNewEntry = false, onSaved }: EntryF
isLoading, isLoading,
isSaving, isSaving,
lastSaved, lastSaved,
isDirty,
updateText, updateText,
updateMood, updateMood,
updateRoughDay, updateRoughDay,
@@ -146,13 +146,13 @@ export function EntryForm({ date, entryId, isNewEntry = false, onSaved }: EntryF
<Loader2 className="animate-spin" size={14} /> <Loader2 className="animate-spin" size={14} />
Saving... Saving...
</> </>
) : lastSaved ? ( ) : lastSaved && !isDirty ? (
<> <>
<Check size={14} className="text-green-500" /> <Check size={14} className="text-green-500" />
Saved Saved
</> </>
) : hasContent ? ( ) : isDirty ? (
<span className="text-muted/50">Auto-saves as you type</span> <span className="text-amber-400">Unsaved changes</span>
) : null} ) : null}
</div> </div>
</div> </div>

View File

@@ -1,15 +1,13 @@
"use client"; "use client";
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { useDebounce } from "./useDebounce";
import type { EntryWithTags, CreateEntryRequest } from "@/lib/types"; import type { EntryWithTags, CreateEntryRequest } from "@/lib/types";
import { getLocalDate } from "@/lib/utils/date"; import { getLocalDate } from "@/lib/utils/date";
import { AUTOSAVE_DELAY } from "@/lib/constants";
interface UseEntryOptions { interface UseEntryOptions {
date?: string; date?: string;
entryId?: string; entryId?: string;
isNewEntry?: boolean; // If true, start with blank form isNewEntry?: boolean;
} }
export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions = {}) { 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 [isSaving, setIsSaving] = useState(false);
const [isLoading, setIsLoading] = useState(!isNewEntry); const [isLoading, setIsLoading] = useState(!isNewEntry);
const [lastSaved, setLastSaved] = useState<Date | null>(null); const [lastSaved, setLastSaved] = useState<Date | null>(null);
const pendingSaveRef = useRef(false); const [isDirty, setIsDirty] = useState(false);
const savingRef = useRef(false); // Prevent concurrent saves const entryIdRef = useRef<string | null>(null);
const entryIdRef = useRef<string | null>(null); // Track entry ID across saves
// Load entry (only if editing existing or viewing by date) // Load entry (only if editing existing)
useEffect(() => { useEffect(() => {
if (isNewEntry) { if (isNewEntry) {
setIsLoading(false); setIsLoading(false);
@@ -63,23 +60,15 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions
loadEntry(); loadEntry();
}, [targetDate, entryId, isNewEntry]); }, [targetDate, entryId, isNewEntry]);
// Save function // Save function - only called explicitly
const save = useCallback(async () => { const save = useCallback(async () => {
if (!text.trim()) return null; 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); setIsSaving(true);
pendingSaveRef.current = false;
try { try {
const body: CreateEntryRequest = { const body: CreateEntryRequest = {
id: entryIdRef.current || undefined, // Use ref to always have latest ID id: entryIdRef.current || undefined,
date: entry?.date || targetDate, date: entry?.date || targetDate,
text: text.trim(), text: text.trim(),
mood, mood,
@@ -96,120 +85,63 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions
if (res.ok) { if (res.ok) {
const saved: EntryWithTags = await res.json(); const saved: EntryWithTags = await res.json();
setEntry(saved); setEntry(saved);
entryIdRef.current = saved.id; // Update ref immediately entryIdRef.current = saved.id;
setLastSaved(new Date()); setLastSaved(new Date());
setIsDirty(false);
return saved; return saved;
} }
} catch (error) { } catch (error) {
console.error("Failed to save entry:", error); console.error("Failed to save entry:", error);
} finally { } finally {
savingRef.current = false;
setIsSaving(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; return null;
}, [text, mood, roughDay, tagNames, entry?.date, targetDate]); }, [text, mood, roughDay, tagNames, entry?.date, targetDate]);
// Debounced save // Update handlers - no autosave, just mark dirty
const debouncedSave = useDebounce(() => { const updateText = useCallback((value: string) => {
if (pendingSaveRef.current) {
save();
}
}, 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); setText(value);
markDirty(); setIsDirty(true);
}, }, []);
[markDirty]
);
const updateMood = useCallback( const updateMood = useCallback((value: number | null) => {
(value: number | null) => {
setMood(value); setMood(value);
markDirty(); setIsDirty(true);
}, }, []);
[markDirty]
);
const updateRoughDay = useCallback( const updateRoughDay = useCallback((value: boolean) => {
(value: boolean) => {
setRoughDay(value); setRoughDay(value);
markDirty(); setIsDirty(true);
}, }, []);
[markDirty]
);
const addTag = useCallback( const addTag = useCallback((name: string) => {
(name: string) => {
const normalized = name.toLowerCase().trim(); const normalized = name.toLowerCase().trim();
if (normalized && !tagNames.includes(normalized)) { if (normalized) {
setTagNames((prev) => [...prev, normalized]); setTagNames((prev) => {
markDirty(); if (prev.includes(normalized)) return prev;
return [...prev, normalized];
});
setIsDirty(true);
} }
}, }, []);
[tagNames, markDirty]
);
const removeTag = useCallback( const removeTag = useCallback((name: string) => {
(name: string) => {
setTagNames((prev) => prev.filter((t) => t !== name)); setTagNames((prev) => prev.filter((t) => t !== name));
markDirty(); setIsDirty(true);
}, }, []);
[markDirty]
);
// Reset form for new entry // Reset form for new entry
const reset = useCallback(() => { const reset = useCallback(() => {
setEntry(null); setEntry(null);
entryIdRef.current = null; // Clear the ID ref entryIdRef.current = null;
setText(""); setText("");
setMood(null); setMood(null);
setRoughDay(false); setRoughDay(false);
setTagNames([]); setTagNames([]);
setLastSaved(null); setLastSaved(null);
pendingSaveRef.current = false; setIsDirty(false);
savingRef.current = 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 { return {
entry, entry,
text, text,
@@ -219,6 +151,7 @@ export function useEntry({ date, entryId, isNewEntry = false }: UseEntryOptions
isLoading, isLoading,
isSaving, isSaving,
lastSaved, lastSaved,
isDirty,
updateText, updateText,
updateMood, updateMood,
updateRoughDay, updateRoughDay,