import { Archive, Settings2 } from "lucide-react"; import { ArchivedHabits } from "../components/ArchivedHabits"; import { AccountSettings } from "../components/AccountSettings"; import { useCallback, useEffect, useRef, useState } from "react"; import { useAuth } from "../components/AuthProvider"; import { DiscordSignInButton } from "../components/DiscordSignInButton"; import { PageLayout } from "../components/design-system/PageLayout"; import { Button, ButtonLink, Checkbox, Counter, SectionHeading, } from "../components/design-system/primitives"; import { Card, CardGrid } from "../components/design-system/Card"; import { HabitChart } from "../components/design-system/HabitChart"; import { HABIT_COLORS } from "../components/design-system/calendar-model"; import { HabitForm } from "../components/HabitForm"; import { habitRequest, scheduleLabel, type TodayHabit, type TodayResponse } from "../lib/dashboard"; import { HabitHistory } from "../components/HabitHistory"; import { WelcomePanel } from "../components/design-system/WelcomePanel"; import type { PublicUser } from "../shared/user"; import { ItemActions } from "../components/design-system/ItemActions"; import { type ItemMode } from "../components/InlineItemForm"; import { InlineHabitEditor } from "../components/InlineHabitEditor"; import { InlineTaskEditor } from "../components/InlineTaskEditor"; import type { Schedule } from "../habits/contracts"; import { ShareProgress } from "../components/ShareProgress"; export function Home() { const { user, loading, busy, error, accountError, signIn, signOut, retry } = useAuth(); return ( Loading account… ) : user ? ( ) : ( !accountError && ) } > {error && (
{error}
)} {loading ? (

Getting things ready…

) : accountError ? (

Let’s try that again.

Your account couldn’t be loaded.

) : user ? ( ) : ( )}
); } function Welcome({ onSignIn }: { onSignIn: () => void }) { const [water, setWater] = useState(3); const [read, setRead] = useState(false); return ( <>

A LITTLE, EVERY DAY

Small steps.
Lasting rhythm.

Check in, count a little more, or work through a few tasks. See your progress grow, one day at a time.

Try it below ↓

Sign in with your Discord account to save your habits.

A little progress. Made visible. } /> Example · September 4, 2026 · not saved
setRead(event.target.checked)} />

Pick a check-in, a count target, or a task list. Repeat daily, on selected weekdays, or at your own interval.

Explore your calendar to see the progress behind each square. Days off stay distinct from missed days.

); } function Dashboard({ user, onExpired }: { user: PublicUser; onExpired: () => void }) { const [today, setToday] = useState(null); const [error, setError] = useState(""); const [notice, setNotice] = useState(""); const [revision, setRevision] = useState(0); const [attempt, setAttempt] = useState(0); const [loading, setLoading] = useState(true); const [habitView, setHabitView] = useState<"today" | "all">("today"); const [adding, setAdding] = useState(false); const [sharing, setSharing] = useState(false); const [settings, setSettings] = useState(false); const [archive, setArchive] = useState(false); const [busy, setBusy] = useState(false); const [needsRefresh, setNeedsRefresh] = useState(false); const focusAddAfterRefresh = useRef(false); const saving = useRef(false); const mounted = useRef(true); const expired = useRef(onExpired); expired.current = onExpired; useEffect(() => { mounted.current = true; return () => { mounted.current = false; }; }, []); const reportError = useCallback((error: unknown) => { if (error instanceof Error && error.message.includes("session has expired")) expired.current(); else setError( error instanceof Error ? error.message : "Could not load your habits. Please try again." ); }, []); useEffect(() => { const controller = new AbortController(); setLoading(true); setError(""); habitRequest("/today", { signal: controller.signal }) .then((data) => { if (!controller.signal.aborted) { setToday(data); setNeedsRefresh(false); setRevision((value) => value + 1); } }) .catch((error) => { if (!controller.signal.aborted) reportError(error); }) .finally(() => { if (!controller.signal.aborted) { setLoading(false); if (focusAddAfterRefresh.current) { focusAddAfterRefresh.current = false; window.requestAnimationFrame(() => document.getElementById("add-habit")?.focus({ preventScroll: true })); } } }); return () => controller.abort(); }, [attempt, reportError]); // Refresh after returning to the page and across the account's local midnight. useEffect(() => { const refresh = () => { if (!saving.current && document.visibilityState === "visible") setAttempt((value) => value + 1); }; const timer = window.setInterval(refresh, 60_000); window.addEventListener("focus", refresh); return () => { window.clearInterval(timer); window.removeEventListener("focus", refresh); }; }, []); async function update( habit: TodayHabit, body: { count: number } | { done: boolean }, taskId?: string ) { if (saving.current || loading || needsRefresh || !today) return; saving.current = true; setBusy(true); setError(""); setNotice(""); try { const path = `/habits/${habit.habitId}/days/${today.date}/${taskId ? `tasks/${taskId}` : "progress"}`; const updated = await habitRequest(path, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!mounted.current) return; setToday((current) => { if (!current) return current; const habits = current.habits.map((item) => item.habitId === updated.habitId ? updated : item ); return { ...current, habits, due: habits.filter((item) => item.due).length, completed: habits.filter((item) => item.complete).length, }; }); setRevision((value) => value + 1); setNotice(`${habit.name} saved.`); } catch (error) { if (mounted.current) reportError(error); } finally { saving.current = false; if (mounted.current) setBusy(false); } } async function manage( habit: TodayHabit, patch: Record | null, taskId?: string, createTask = false ) { if (saving.current || 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}` : ""}`, { method: createTask ? "POST" : patch ? "PATCH" : "DELETE", ...(patch ? { headers: { "Content-Type": "application/json" }, body: JSON.stringify(patch) } : {}), } ); // A refresh failure must never turn a successful delete into a retryable delete. try { const refreshed = await habitRequest("/today"); if (mounted.current) { setToday(refreshed); // Keep a habit reachable when an edit makes it unscheduled (including an empty routine). if (refreshed.habits.some(item => item.habitId === habit.habitId && !item.due)) setHabitView("all"); setNeedsRefresh(false); setRevision((value) => value + 1); } } catch (refreshError) { if (refreshError instanceof Error && refreshError.message.includes("session has expired")) expired.current(); if (mounted.current) { setNeedsRefresh(true); setError( "Your change was saved, but the dashboard could not refresh. Try again to load the latest data." ); } } if (mounted.current) { setNotice( `${taskId || createTask ? "Task" : "Habit"} ${createTask ? "added" : patch ? "updated" : taskId ? "deleted" : "archived"}.` ); if (!patch) window.requestAnimationFrame(() => { ( document.getElementById("habits-title") ?? document.getElementById("add-habit") )?.focus(); }); } } catch (error) { if (error instanceof Error && error.message.includes("session has expired")) expired.current(); throw error; } finally { saving.current = false; if (mounted.current) setBusy(false); } } return ( <> {today && ( <>
{today.due > 0 ? ( <> {today.completed} / {today.due} habits complete today {today.completed === today.due && ( A little, all done. )} ) : (

{today.habits.length ? "Nothing scheduled today. Enjoy a little breathing room." : "Start with one habit. Your first small step starts here."}

)}
{today.habits.length > 0 && ( <> )}
)}
{archive && today && setArchive(false)} date={today.date} revision={revision} onExpired={onExpired} onChanged={() => setAttempt(value => value + 1)} />} {settings && setSettings(false)} />} {sharing && today && { setSharing(false); requestAnimationFrame(() => document.getElementById("open-sharing")?.focus()); }} />} {error && (

{error}

)} {!today && loading && (

Loading your habits…

)} {today && ( <> {adding && ( setAdding(false)} onExpired={() => expired.current()} onCreated={(name) => { focusAddAfterRefresh.current = true; setAdding(false); setNotice(`${name} created.`); setHabitView("all"); setAttempt((value) => value + 1); }} /> )}

{notice}

{today.habits.length > 0 && (
Today, at your pace. } />
{habitView === "today" && today.due === 0 &&

You’re all set for today. Choose All habits to view or edit your routines.

}
{today.habits.filter(habit => habitView === "all" || habit.due).map((habit) => ( setAttempt(value => value + 1)} onUpdate={update} onManage={manage} onExpired={() => expired.current()} /> ))}
)} )} ); } function SavedHabit({ habit, date, revision, disabled, onUpdate, onManage, onHistorySaved, onExpired, }: { habit: TodayHabit; date: string; revision: number; disabled: boolean; onUpdate: ( habit: TodayHabit, body: { count: number } | { done: boolean }, taskId?: string ) => Promise; onExpired: () => void; onHistorySaved: () => void; onManage: ( habit: TodayHabit, patch: Record | null, taskId?: string, createTask?: boolean ) => Promise; }) { const [editing, setEditing] = useState<{ mode: ItemMode; color: string } | null>(null); const [addingTask, setAddingTask] = useState(false); const blocked = disabled || !habit.due; const method = habit.requirements?.method ?? habit.method; return ( setEditing({ mode: "edit", color })} onDelete={() => setEditing({ mode: "delete", color: "#196127" })} editor={ editing && habit.requirements ? ( setEditing(null)} onSave={(patch) => onManage(habit, patch)} onDelete={() => onManage(habit, null)} /> ) : undefined } tasks={ habit.requirements?.method === "tasks" ? ( <>
    {habit.requirements.tasks.map((config) => { const occurrence = habit.tasks.find((task) => task.taskId === config.id); return ( void onUpdate(habit, { done }, config.id)} onSave={(patch) => onManage(habit, patch, config.id)} onDelete={() => onManage(habit, null, config.id)} /> ); })}
{addingTask && ( setAddingTask(false)} onSave={(patch) => onManage(habit, patch, undefined, true)} onDelete={async () => {}} /> )}
) : undefined } > {method === "count" ? ( void onUpdate(habit, { count })} /> ) : method === "manual" ? ( void onUpdate(habit, { done: event.target.checked })} /> ) : undefined}
); } function SavedTask({ task, schedule, habitSchedule, date, disabled, blocked, scheduled, onCheck, onSave, onDelete, }: { task: Pick; disabled: boolean; blocked: boolean; scheduled: boolean; schedule: Schedule; habitSchedule: Schedule; date: string; onCheck: (done: boolean) => void; onSave: (patch: { name?: string; schedule?: Schedule }) => Promise; onDelete: () => Promise; }) { const [mode, setMode] = useState(null); return (
  • onCheck(event.target.checked)} /> setMode("edit")} onDelete={() => setMode("delete")} />
    {mode && ( setMode(null)} /> )}
  • ); }