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) {
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
<Loader2 className="animate-spin" size={14} />
Saving...
</>
) : lastSaved ? (
) : lastSaved && !isDirty ? (
<>
<Check size={14} className="text-green-500" />
Saved
</>
) : hasContent ? (
<span className="text-muted/50">Auto-saves as you type</span>
) : isDirty ? (
<span className="text-amber-400">Unsaved changes</span>
) : null}
</div>
</div>

View File

@@ -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<Date | null>(null);
const pendingSaveRef = useRef(false);
const savingRef = useRef(false); // Prevent concurrent saves
const entryIdRef = useRef<string | null>(null); // Track entry ID across saves
const [isDirty, setIsDirty] = useState(false);
const entryIdRef = useRef<string | null>(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,