feat: enhance habit tracking with pending state management and history refresh
This commit is contained in:
@@ -162,8 +162,11 @@ function Welcome({ onSignIn }: { onSignIn: () => void }) {
|
||||
function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => void }) {
|
||||
const [today, setToday] = useState<TodayResponse | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const [notice, setNotice] = useState("");
|
||||
const [revision, setRevision] = useState(0);
|
||||
const [calendarRevision, setCalendarRevision] = useState(0);
|
||||
const [habitRevisions, setHabitRevisions] = useState<Record<string, number>>({});
|
||||
const [pendingHabits, setPendingHabits] = useState<Set<string>>(new Set());
|
||||
const progressSaves = useRef(new Set<string>());
|
||||
const [attempt, setAttempt] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [habitView, setHabitView] = useState<"today" | "all">("today");
|
||||
@@ -203,6 +206,7 @@ function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => voi
|
||||
setToday(data);
|
||||
setNeedsRefresh(false);
|
||||
setRevision((value) => value + 1);
|
||||
setCalendarRevision((value) => value + 1);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -223,7 +227,7 @@ function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => voi
|
||||
// Refresh after returning to the page and across the account's local midnight.
|
||||
useEffect(() => {
|
||||
const refresh = () => {
|
||||
if (!saving.current && document.visibilityState === "visible")
|
||||
if (!saving.current && progressSaves.current.size === 0 && document.visibilityState === "visible")
|
||||
setAttempt((value) => value + 1);
|
||||
};
|
||||
const timer = window.setInterval(refresh, 60_000);
|
||||
@@ -239,11 +243,10 @@ function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => voi
|
||||
body: { count: number } | { done: boolean },
|
||||
taskId?: string
|
||||
) {
|
||||
if (saving.current || loading || needsRefresh || !today) return;
|
||||
saving.current = true;
|
||||
setBusy(true);
|
||||
if (saving.current || progressSaves.current.has(habit.habitId) || loading || needsRefresh || !today) return;
|
||||
progressSaves.current.add(habit.habitId);
|
||||
setPendingHabits(new Set(progressSaves.current));
|
||||
setError("");
|
||||
setNotice("");
|
||||
try {
|
||||
const path = `/habits/${habit.habitId}/days/${today.date}/${taskId ? `tasks/${taskId}` : "progress"}`;
|
||||
const updated = await habitRequest<TodayHabit>(path, {
|
||||
@@ -265,12 +268,12 @@ function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => voi
|
||||
};
|
||||
});
|
||||
setRevision((value) => value + 1);
|
||||
setNotice(`${habit.name} saved.`);
|
||||
setHabitRevisions(current => ({ ...current, [habit.habitId]: (current[habit.habitId] ?? 0) + 1 }));
|
||||
} catch (error) {
|
||||
if (mounted.current) reportError(error);
|
||||
} finally {
|
||||
saving.current = false;
|
||||
if (mounted.current) setBusy(false);
|
||||
progressSaves.current.delete(habit.habitId);
|
||||
if (mounted.current) setPendingHabits(new Set(progressSaves.current));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,12 +283,11 @@ function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => voi
|
||||
taskId?: string,
|
||||
createTask = false
|
||||
) {
|
||||
if (saving.current || loading || needsRefresh)
|
||||
if (saving.current || progressSaves.current.size > 0 || loading || needsRefresh)
|
||||
throw new Error("Please wait for the dashboard to refresh.");
|
||||
saving.current = true;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
setNotice("");
|
||||
try {
|
||||
await habitRequest(
|
||||
`/habits/${habit.habitId}${createTask ? "/tasks" : taskId ? `/tasks/${taskId}` : ""}`,
|
||||
@@ -305,6 +307,7 @@ function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => voi
|
||||
if (refreshed.habits.some(item => item.habitId === habit.habitId && !item.due)) setHabitView("all");
|
||||
setNeedsRefresh(false);
|
||||
setRevision((value) => value + 1);
|
||||
setHabitRevisions(current => ({ ...current, [habit.habitId]: (current[habit.habitId] ?? 0) + 1 }));
|
||||
}
|
||||
} catch (refreshError) {
|
||||
if (refreshError instanceof Error && refreshError.message.includes("session has expired"))
|
||||
@@ -317,9 +320,6 @@ function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => voi
|
||||
}
|
||||
}
|
||||
if (mounted.current) {
|
||||
setNotice(
|
||||
`${taskId || createTask ? "Task" : "Habit"} ${createTask ? "added" : patch ? "updated" : taskId ? "deleted" : "archived"}.`
|
||||
);
|
||||
if (!patch)
|
||||
window.requestAnimationFrame(() => {
|
||||
(
|
||||
@@ -422,18 +422,14 @@ function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => voi
|
||||
date={today.date}
|
||||
onCancel={() => setAdding(false)}
|
||||
onExpired={() => expired.current()}
|
||||
onCreated={(name) => {
|
||||
onCreated={() => {
|
||||
focusAddAfterRefresh.current = true;
|
||||
setAdding(false);
|
||||
setNotice(`${name} created.`);
|
||||
setHabitView("all");
|
||||
setAttempt((value) => value + 1);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<p className="ds-form-feedback ds-save-notice" role="status">
|
||||
{notice}
|
||||
</p>
|
||||
{today.habits.length > 0 && (
|
||||
<section
|
||||
className="ds-section ds-habits-section"
|
||||
@@ -461,8 +457,8 @@ function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => voi
|
||||
key={habit.habitId}
|
||||
habit={habit}
|
||||
date={today.date}
|
||||
revision={revision}
|
||||
disabled={busy || loading || needsRefresh}
|
||||
revision={calendarRevision + (habitRevisions[habit.habitId] ?? 0)}
|
||||
disabled={busy || pendingHabits.has(habit.habitId) || loading || needsRefresh}
|
||||
onHistorySaved={() => setAttempt(value => value + 1)}
|
||||
onUpdate={update}
|
||||
onManage={manage}
|
||||
|
||||
Reference in New Issue
Block a user