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,