mirror of
https://github.com/Tony0410/quietthanks.git
synced 2026-05-24 13:21:38 +08:00
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:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user