diff --git a/components/habits/habit-tracker.tsx b/components/habits/habit-tracker.tsx
index ad97611..3ffcd5c 100644
--- a/components/habits/habit-tracker.tsx
+++ b/components/habits/habit-tracker.tsx
@@ -1,5 +1,6 @@
"use client";
+import { Loader2 } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge";
@@ -42,9 +43,15 @@ const reasonLabels = {
function DailyTaskList({
tasks,
+ getIsCompleted,
+ isPending,
+ getToggleError,
onToggle,
}: {
tasks: DailyTask[];
+ getIsCompleted: (habit: DailyTask) => boolean;
+ isPending: (habitId: string) => boolean;
+ getToggleError: (habitId: string) => string | null;
onToggle: (habitId: string) => void;
}) {
if (tasks.length === 0) {
@@ -57,8 +64,8 @@ function DailyTaskList({
}
const sortedTasks = [...tasks].sort((a, b) => {
- const aCompleted = isHabitCompletedToday(a);
- const bCompleted = isHabitCompletedToday(b);
+ const aCompleted = getIsCompleted(a);
+ const bCompleted = getIsCompleted(b);
if (aCompleted === bCompleted) {
return 0;
@@ -70,7 +77,9 @@ function DailyTaskList({
return (
<>
{sortedTasks.map((habit) => {
- const isCompleted = isHabitCompletedToday(habit);
+ const isCompleted = getIsCompleted(habit);
+ const pending = isPending(habit.id);
+ const toggleError = getToggleError(habit.id);
return (
-
onToggle(habit.id)}
- className="mt-0.5"
- />
+
+ onToggle(habit.id)}
+ />
+ {pending ? (
+ <>
+
+ Saving habit completion
+ >
+ ) : null}
+
@@ -157,6 +180,34 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
const [error, setError] = useState(null);
const [isReady, setIsReady] = useState(false);
const [isSaving, setIsSaving] = useState(false);
+ const [optimisticCompletion, setOptimisticCompletion] = useState<
+ Record
+ >({});
+ const [pendingHabitIds, setPendingHabitIds] = useState>(
+ () => new Set(),
+ );
+ const [toggleErrors, setToggleErrors] = useState>({});
+
+ const getIsCompleted = useCallback(
+ (habit: DailyTask) => {
+ if (habit.id in optimisticCompletion) {
+ return optimisticCompletion[habit.id];
+ }
+
+ return isHabitCompletedToday(habit);
+ },
+ [optimisticCompletion],
+ );
+
+ const isPending = useCallback(
+ (habitId: string) => pendingHabitIds.has(habitId),
+ [pendingHabitIds],
+ );
+
+ const getToggleError = useCallback(
+ (habitId: string) => toggleErrors[habitId] ?? null,
+ [toggleErrors],
+ );
const refresh = useCallback(async () => {
try {
@@ -189,20 +240,62 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
};
}, [refresh]);
- const handleToggle = async (habitId: string) => {
- try {
- setIsSaving(true);
- setError(null);
- await toggleHabitCompletion(habitId);
- } catch (toggleError) {
- setError(
- toggleError instanceof Error
- ? toggleError.message
- : "Could not update habit.",
- );
- } finally {
- setIsSaving(false);
+ const handleToggle = (habitId: string) => {
+ const habit = dailyTasks.find((task) => task.id === habitId);
+
+ if (!habit || pendingHabitIds.has(habitId)) {
+ return;
}
+
+ const nextCompleted = !getIsCompleted(habit);
+
+ setOptimisticCompletion((current) => ({
+ ...current,
+ [habitId]: nextCompleted,
+ }));
+ setPendingHabitIds((current) => new Set(current).add(habitId));
+ setToggleErrors((current) => {
+ const next = { ...current };
+ delete next[habitId];
+ return next;
+ });
+
+ void (async () => {
+ try {
+ const updatedHabits = await toggleHabitCompletion(habitId);
+
+ setDailyTasks((current) =>
+ current.map((task) => {
+ const updated = updatedHabits.find((item) => item.id === task.id);
+ return updated ? { ...task, ...updated } : task;
+ }),
+ );
+ setOptimisticCompletion((current) => {
+ const next = { ...current };
+ delete next[habitId];
+ return next;
+ });
+ } catch (toggleError) {
+ setOptimisticCompletion((current) => {
+ const next = { ...current };
+ delete next[habitId];
+ return next;
+ });
+ setToggleErrors((current) => ({
+ ...current,
+ [habitId]:
+ toggleError instanceof Error
+ ? toggleError.message
+ : "Could not update habit.",
+ }));
+ } finally {
+ setPendingHabitIds((current) => {
+ const next = new Set(current);
+ next.delete(habitId);
+ return next;
+ });
+ }
+ })();
};
const handleAddHabit = async () => {
@@ -323,7 +416,13 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
{error ? {error}
: null}
-
+
);