/// import { action, computed, NAME, pattern, UI, Writable } from "commontools"; import type { Habit, HabitTrackerInput, HabitTrackerOutput, } from "./schemas.tsx"; // Re-export for consumers export type { Habit, HabitLog } from "./schemas.tsx"; // Get today's date as YYYY-MM-DD const getTodayDate = (): string => { const now = new Date(); return now.toISOString().split("T")[0]; }; // Get date N days ago as YYYY-MM-DD const getDateDaysAgo = (daysAgo: number): string => { const date = new Date(); date.setDate(date.getDate() - daysAgo); return date.toISOString().split("T")[0]; }; export default pattern( ({ habits, logs }) => { const todayDate = getTodayDate(); const newHabitName = Writable.of(""); const newHabitIcon = Writable.of("✓"); const habitCount = computed(() => habits.get().length); const hasNoHabits = computed(() => habits.get().length === 0); const summary = computed(() => { return habits.get() .map((h) => `${h.icon} ${h.name}`) .join(", "); }); // Actions close over pattern state directly const toggleHabit = action<{ habitName: string }>(({ habitName }) => { // Only toggle if habit exists const habitExists = habits.get().some((h) => h.name === habitName); if (!habitExists) return; const currentLogs = logs.get(); const existingIdx = currentLogs.findIndex( (log) => log.habitName === habitName && log.date === todayDate, ); if (existingIdx >= 0) { const updated = currentLogs.map((log, i) => i === existingIdx ? { ...log, completed: !log.completed } : log ); logs.set(updated); } else { logs.push({ habitName, date: todayDate, completed: true }); } }); const deleteHabit = action<{ habit: Habit }>(({ habit }) => { const current = habits.get(); const idx = current.findIndex((h) => h.name === habit.name); if (idx >= 0) { habits.set(current.toSpliced(idx, 1)); } }); const addHabit = action<{ name: string; icon: string }>( ({ name, icon }) => { const trimmedName = name.trim(); if (trimmedName) { habits.push({ name: trimmedName, icon: icon || "✓", color: "#3b82f6", }); newHabitName.set(""); } }, ); return { [NAME]: "Habit Tracker", [UI]: ( Habits ({habitCount}) {todayDate} {habits.map((habit) => { // Use computed() to derive values from closed-over cells const isCompletedToday = computed(() => logs.get().some( (log) => log.habitName === habit.name && log.date === todayDate && log.completed, ) ); const streak = computed(() => { const logList = logs.get(); let count = 0; for (let i = 0; i < 365; i++) { const dateToCheck = getDateDaysAgo(i); const completed = logList.some( (log) => log.habitName === habit.name && log.date === dateToCheck && log.completed, ); if (completed) { count++; } else if (i === 0) { continue; // Today not completed is ok } else { break; // Gap found } } return count; }); return ( {habit.icon} {habit.name || "(unnamed)"} Streak: {streak} days toggleHabit.send({ habitName: habit.name })} > {isCompletedToday ? "✓" : "○"} deleteHabit.send({ habit })} > × {/* Last 7 days indicator */} {[6, 5, 4, 3, 2, 1, 0].map((daysAgo) => { const date = getDateDaysAgo(daysAgo); const dayCompleted = computed(() => logs.get().some( (log) => log.habitName === habit.name && log.date === date && log.completed, ) ); return (
{date.slice(-2)}
); })}
); })} {hasNoHabits ? (
No habits yet. Add one below!
) : null}
addHabit.send({ name: newHabitName.get(), icon: newHabitIcon.get(), })} > Add Habit
), habits, logs, todayDate, summary, toggleHabit, addHabit, deleteHabit, }; }, );