/// import { Cell, computed, Default, ifElse, lift, NAME, pattern, UI, } from "commontools"; interface Habit { name: string; icon: Default; color: Default; } interface HabitLog { habitName: string; date: string; // YYYY-MM-DD completed: boolean; } interface Input { habits: Cell>; logs: Cell>; } interface Output { habits: Habit[]; logs: HabitLog[]; todayDate: string; } // 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]; }; // Pure functions wrapped with lift() - use object arg for multiple params const checkCompleted = lift( (args: { logs: HabitLog[]; name: string; date: string }): boolean => { const { logs, name, date } = args; if (!Array.isArray(logs)) return false; return logs.some( (log) => log.habitName === name && log.date === date && log.completed, ); }, ); const calcStreak = lift((args: { logs: HabitLog[]; name: string }): number => { const { logs, name } = args; if (!Array.isArray(logs)) return 0; let streak = 0; for (let i = 0; i < 365; i++) { const dateToCheck = getDateDaysAgo(i); const completed = logs.some( (log) => log.habitName === name && log.date === dateToCheck && log.completed, ); if (completed) { streak++; } else if (i === 0) { // Today not completed is ok, continue checking continue; } else { // Gap found, stop break; } } return streak; }); export default pattern(({ habits, logs }) => { const todayDate = getTodayDate(); const newHabitName = Cell.of(""); const newHabitIcon = Cell.of("✓"); const habitCount = computed(() => habits.get().length); return { [NAME]: "Habit Tracker", [UI]: ( Habits ({habitCount}) {todayDate} {habits.map((habit) => { // Use lift() - just call with reactive args in an object const isCompletedToday = checkCompleted({ logs, name: habit.name, date: todayDate, }); const streak = calcStreak({ logs, name: habit.name }); return ( {habit.icon} {habit.name || "(unnamed)"} Streak: {streak} days { const habitName = habit.name; const currentLogs = logs.get(); const existingIdx = currentLogs.findIndex( (log) => log.habitName === habitName && log.date === todayDate, ); if (existingIdx >= 0) { // Toggle existing const updated = currentLogs.map((log, i) => i === existingIdx ? { ...log, completed: !log.completed } : log ); logs.set(updated); } else { // Create new logs.push({ habitName, date: todayDate, completed: true, }); } }} > {ifElse(isCompletedToday, "✓", "○")} { const current = habits.get(); const idx = current.findIndex((h) => Cell.equals(habit, h) ); if (idx >= 0) { habits.set(current.toSpliced(idx, 1)); } }} > × {/* Last 7 days indicator */} {[6, 5, 4, 3, 2, 1, 0].map((daysAgo) => { const dateStr = getDateDaysAgo(daysAgo); const wasCompleted = checkCompleted({ logs, name: habit.name, date: dateStr, }); return (
{dateStr.slice(-2)}
); })}
); })} {ifElse( computed(() => habits.get().length === 0),
No habits yet. Add one below!
, null, )}
{ const name = newHabitName.get().trim(); if (name) { habits.push({ name, icon: newHabitIcon.get() || "✓", color: "#3b82f6", }); newHabitName.set(""); } }} > Add Habit
), habits, logs, todayDate, }; });