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