"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 { getDailyTasks, type DailyTask, } from "@/lib/daily-tasks"; import { addHabit, getCustomHabits, isHabitCompletedToday, removeHabit, toggleHabitCompletion, type Habit, } from "@/lib/habits-storage"; import { formatCoinsDelta } from "@/lib/coins"; import { getProfilePreferences } from "@/lib/profile-preferences-storage"; import { hasCompletedDailyQuizToday } from "@/lib/daily-quiz-storage"; type HabitTrackerProps = { mode?: "daily" | "manage"; }; 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 quiz or update your profile so AI can suggest habits for you.

); } const sortedTasks = [...tasks].sort((a, b) => { const aCompleted = getIsCompleted(a); const bCompleted = getIsCompleted(b); if (aCompleted === bCompleted) { return 0; } return aCompleted ? 1 : -1; }); return ( <> {sortedTasks.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 [dailyTasks, setDailyTasks] = useState([]); const [customHabits, setCustomHabits] = useState([]); const [newHabitLabel, setNewHabitLabel] = useState(""); const [focusTopic, setFocusTopic] = useState(null); const [quizCompletedToday, setQuizCompletedToday] = useState(false); 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 [coinMessage, setCoinMessage] = useState(null); 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 { setError(null); const [tasks, customs] = await Promise.all([ getDailyTasks(), getCustomHabits(), ]); setDailyTasks(tasks); setCustomHabits(customs); setFocusTopic((await getProfilePreferences()).focusTopic); setQuizCompletedToday(await hasCompletedDailyQuizToday()); setIsReady(true); } catch (refreshError) { setError( refreshError instanceof Error ? refreshError.message : "Could not load habits.", ); setIsReady(true); } }, []); useEffect(() => { if (!coinMessage) { return; } const timeoutId = window.setTimeout(() => { setCoinMessage(null); }, 3000); return () => { window.clearTimeout(timeoutId); }; }, [coinMessage]); useEffect(() => { void refresh(); window.addEventListener(HABIT_PET_DATA_UPDATED_EVENT, refresh); return () => { window.removeEventListener(HABIT_PET_DATA_UPDATED_EVENT, refresh); }; }, [refresh]); 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)); } 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 === "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 quiz, and journal.

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

Daily quiz 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 quiz, journal, and custom habits. {error ?

{error}

: null} {coinMessage ? (

{coinMessage}

) : null}
); }