"use client"; import { Loader2 } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events"; import { getCachedHabitsTabData, prefetchHabitsTabData, } from "@/lib/app-tab-data-cache"; import type { DailyTask } from "@/lib/daily-tasks"; import { addHabit, getTodayDateKey, isHabitCompletedToday, removeHabit, toggleHabitCompletion, type Habit, } from "@/lib/habits-storage"; import { formatCoinsDelta } from "@/lib/coins"; import { describeHabitWellnessBoost } from "@/lib/habit-wellness-effects"; type HabitTrackerProps = { mode?: "daily" | "manage" | "all"; }; const reasonLabels = { custom: "Custom", suggested: "Suggested for you", } as const; 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) { return (

No daily tasks yet. Complete your daily check-in or update your profile so AI can suggest habits for you.

); } return ( <> {tasks.map((habit) => { const isCompleted = getIsCompleted(habit); const pending = isPending(habit.id); const toggleError = getToggleError(habit.id); return (
onToggle(habit.id)} /> {pending ? ( <>
{reasonLabels[habit.reason]} {habit.reason === "suggested" ? ( by AI ) : null}
{toggleError ? (

{toggleError}

) : null}
{habit.streak} day streak
); })} ); } function CustomHabitList({ habits, onRemove, }: { habits: Habit[]; onRemove: (habitId: string) => void; }) { if (habits.length === 0) { return (

No custom habits yet. Add one below to include it in your daily tasks.

); } return ( <> {habits.map((habit) => (
{habit.label}
{habit.streak} day streak
))} ); } export function HabitTracker({ mode = "daily" }: HabitTrackerProps) { const cachedHabits = getCachedHabitsTabData(); const [dailyTasks, setDailyTasks] = useState( cachedHabits?.dailyTasks ?? [], ); const [customHabits, setCustomHabits] = useState( cachedHabits?.customHabits ?? [], ); const [newHabitLabel, setNewHabitLabel] = useState(""); const [focusTopic, setFocusTopic] = useState( cachedHabits?.focusTopic ?? null, ); const [quizCompletedToday, setQuizCompletedToday] = useState( cachedHabits?.quizCompletedToday ?? false, ); const [error, setError] = useState(null); const [isReady, setIsReady] = useState(cachedHabits !== null); const [isSaving, setIsSaving] = useState(false); const [optimisticCompletion, setOptimisticCompletion] = useState< Record >({}); const [pendingHabitIds, setPendingHabitIds] = useState>( () => new Set(), ); const [toggleErrors, setToggleErrors] = useState>({}); const [coinMessage, setCoinMessage] = useState(null); const [todayKey, setTodayKey] = useState(() => getTodayDateKey()); 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 applyHabitsData = useCallback( (data: NonNullable>) => { setDailyTasks(data.dailyTasks); setCustomHabits(data.customHabits); setFocusTopic(data.focusTopic); setQuizCompletedToday(data.quizCompletedToday); }, [], ); const refresh = useCallback(async () => { try { setError(null); const data = await prefetchHabitsTabData({ force: true }); applyHabitsData(data); setIsReady(true); } catch (refreshError) { setError( refreshError instanceof Error ? refreshError.message : "Could not load habits.", ); setIsReady(true); } }, [applyHabitsData]); useEffect(() => { void prefetchHabitsTabData() .then(applyHabitsData) .catch((refreshError) => { setError( refreshError instanceof Error ? refreshError.message : "Could not load habits.", ); }) .finally(() => { setIsReady(true); }); window.addEventListener(HABIT_PET_DATA_UPDATED_EVENT, refresh); return () => { window.removeEventListener(HABIT_PET_DATA_UPDATED_EVENT, refresh); }; }, [applyHabitsData, refresh]); useEffect(() => { if (!coinMessage) { return; } const timeoutId = window.setTimeout(() => { setCoinMessage(null); }, 3000); return () => { window.clearTimeout(timeoutId); }; }, [coinMessage]); useEffect(() => { const syncForNewDay = () => { const nextTodayKey = getTodayDateKey(); if (nextTodayKey === todayKey) { return; } setTodayKey(nextTodayKey); setOptimisticCompletion({}); setCoinMessage(null); void refresh(); }; const intervalId = window.setInterval(syncForNewDay, 60_000); const handleVisibilityChange = () => { if (document.visibilityState === "visible") { syncForNewDay(); } }; window.addEventListener("focus", syncForNewDay); document.addEventListener("visibilitychange", handleVisibilityChange); return () => { window.clearInterval(intervalId); window.removeEventListener("focus", syncForNewDay); document.removeEventListener("visibilitychange", handleVisibilityChange); }; }, [refresh, todayKey]); 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 { habits: updatedHabits, coinsDelta } = 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; }); setCoinMessage( [ formatCoinsDelta(coinsDelta), nextCompleted ? describeHabitWellnessBoost(habit.label) : null, ] .filter(Boolean) .join(" ยท "), ); } 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 () => { try { setIsSaving(true); setError(null); await addHabit(newHabitLabel); setNewHabitLabel(""); } catch (addError) { setError( addError instanceof Error ? addError.message : "Could not add habit.", ); } finally { setIsSaving(false); } }; const handleRemoveHabit = async (habitId: string) => { try { setIsSaving(true); setError(null); await removeHabit(habitId); } catch (removeError) { setError( removeError instanceof Error ? removeError.message : "Could not remove habit.", ); } finally { setIsSaving(false); } }; if (!isReady) { return (

Loading and personalizing your tasks...

); } if (mode === "all") { return (
Today's tasks Personalized from your focus topics {quizCompletedToday ? ", today's check-in," : ""} and custom habits. {error ?

{error}

: null} {coinMessage ? (

{coinMessage}

) : null}
Custom habits Add your own habits. Custom habits always appear in your daily task list.
setNewHabitLabel(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); void handleAddHabit(); } }} />
How daily tasks are picked Your tasks combine focus preferences, custom habits, and today's check-in.

Focus topics: {focusTopic ?? "None selected"}

Daily check-in today:{" "} {quizCompletedToday ? "Completed" : "Not completed yet"}

Suggested habits can appear when your check-in shows low energy, high stress, or poor sleep.

); } if (mode === "manage") { return (
{error ?

{error}

: null} Custom habits Add your own habits. Custom habits always appear in your daily task list.
setNewHabitLabel(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); void handleAddHabit(); } }} />
How daily tasks are picked Your tasks combine custom habits and AI suggestions based on your profile, onboarding, daily check-in, and journal.

Focus topics:{" "} {focusTopic ?? "None selected"}

Daily check-in today:{" "} {quizCompletedToday ? "Completed" : "Not completed yet"}

Suggested tasks are generated daily from your preferences, onboarding answers, wellness check-in, and journal.

); } return ( Today's tasks Personalized from your profile, daily check-in, journal, and custom habits. {error ?

{error}

: null} {coinMessage ? (

{coinMessage}

) : null}
); }