Source
This app’s code and revision history are public. Download a copy or explore the files below.
Public
The app’s code and revision history are public.
Run it yourself
Take the whole tree and run it on your own machine. The download needs no account. Cloning needs a sign-in.
Download .zip
bounded clone e8a688fd21351d04b05468df
A copy you download is not bounded. The guarantees this app runs under - declared egress, proven invariants, writes that fail closed - live in the runtime serving this page, not in the files. What you get is auditable source you can read and run, with no runtime guarantees attached.
src/Today.tsx
// Today: the habits due today, each one tap to check off, with streaks; then what is not due today
// and the habits being avoided.
import { useState } from "react";
import { useStore, listHabits, getState, update, nextOrder } from "./lib/store";
import { stats, streakLabel, newHabit, type Habit, type Stats } from "./lib/habits";
import { fmt, keyOf } from "./lib/dates";
import { navigate } from "./lib/router";
import { useToday } from "./lib/useToday";
import { Editor, type Draft } from "./Editor";
import { CheckButton, FriendsLine } from "./Checkin";
import { A, toast } from "./ui";
import { Flame, Plus } from "./icons";
export const SUGGESTIONS: (Draft & { about: string })[] = [
{ name: "Drink water", kind: "count", target: 8, unit: "glasses", schedule: { type: "daily" }, about: "8 glasses a day" },
{ name: "Read", kind: "count", target: 10, unit: "pages", schedule: { type: "daily" }, about: "10 pages a day" },
{ name: "Walk", kind: "check", target: 1, unit: "", schedule: { type: "daily" }, about: "Every day" },
{ name: "Exercise", kind: "check", target: 1, unit: "", schedule: { type: "weekly", times: 3 }, about: "3 times a week" },
{ name: "Meditate", kind: "check", target: 1, unit: "", schedule: { type: "daily" }, about: "Every day" },
{ name: "No sugar", kind: "avoid", target: 1, unit: "", schedule: { type: "daily" }, about: "Days without it" },
];
/** Adds a suggested habit as it is. */
export function addSuggestion(d: Draft): Habit {
const h = newHabit({ name: d.name, kind: d.kind, target: d.target, unit: d.unit, schedule: d.schedule }, nextOrder(getState()));
update((s) => { s.habits[h.id] = h; });
return h;
}
function Meta({ h, st }: { h: Habit; st: Stats }) {
const parts: React.ReactNode[] = [];
if (st.current > 0) parts.push(<span key="s" className={`meta-streak ${st.doneToday || h.kind === "avoid" ? "on" : ""}`}><Flame size={14} /> {streakLabel(st.current, st.unit, h.kind === "avoid")}</span>);
if (h.kind === "count") parts.push(<span key="c">{st.countToday} of {h.target}{h.unit ? ` ${h.unit}` : ""}</span>);
if (h.kind !== "avoid" && h.schedule.type === "weekly") parts.push(<span key="w">{st.week} of {h.schedule.times} this week</span>);
else if (st.rest) parts.push(<span key="r">{st.rest}</span>);
if (h.kind === "avoid" && st.doneToday) parts.push(<span key="a">Slipped today</span>);
if (!parts.length) parts.push(<span key="n">Not done yet</span>);
return (
<span className="hrow-meta">
{parts}
{h.team && <FriendsLine team={h.team} />}
</span>
);
}
function Row({ h, st, day }: { h: Habit; st: Stats; day: string }) {
return (
<li className={`hrow ${st.doneToday && h.kind !== "avoid" ? "is-done" : ""}`}>
<A href={`/h/${h.id}`} className="hrow-main">
<span className="hrow-name">{h.name}</span>
<Meta h={h} st={st} />
</A>
<CheckButton h={h} day={day} />
</li>
);
}
export function TodayPage({ draft }: { draft?: Draft }) {
const s = useStore();
const now = useToday();
const day = keyOf(now);
const [adding, setAdding] = useState<Draft | null>(draft ?? null);
const [showRest, setShowRest] = useState(false);
const rows = listHabits(s).map((h) => ({ h, st: stats(h, s.weekStart, now) }));
const due = rows.filter((r) => r.h.kind !== "avoid" && (r.st.due || r.st.doneToday));
const rest = rows.filter((r) => r.h.kind !== "avoid" && !r.st.due && !r.st.doneToday);
const avoid = rows.filter((r) => r.h.kind === "avoid");
const done = due.filter((r) => r.st.doneToday).length;
const closeEditor = () => { setAdding(null); if (draft) navigate("/today", true); };
return (
<div className="page today">
<header className="page-head">
<div>
<p className="eyebrow">{fmt(now, { weekday: "long", month: "long", day: "numeric" })}</p>
<h1 className="h1">Today</h1>
</div>
{rows.length > 0 && <button type="button" className="btn head-add" onClick={() => setAdding({ name: "", kind: "check", target: 8, unit: "", schedule: { type: "daily" } })}><Plus size={18} /> Add a habit</button>}
</header>
{rows.length === 0 ? (
<section className="empty-start">
<img className="art empty-art" src="/art/card-habits.webp" alt="" width="220" height="194" />
<h2 className="h2">Start with one small habit.</h2>
<p className="muted">Pick one below or make your own. You can change it any time.</p>
<ul className="suggest-list">
{SUGGESTIONS.map((sg) => (
<li key={sg.name}>
<button type="button" className="suggest" onClick={() => { const h = addSuggestion(sg); toast(`Added ${h.name}. Tap the circle when it is done.`); }}>
<strong>{sg.name}</strong><span>{sg.about}</span><Plus size={18} />
</button>
</li>
))}
</ul>
<button type="button" className="btn btn-lg" onClick={() => setAdding({ name: "", kind: "check", target: 8, unit: "", schedule: { type: "daily" } })}>Make my own habit</button>
</section>
) : (
<>
{due.length > 0 && (
<div className="progress-line" aria-label={`${done} of ${due.length} done today`}>
<span className="progress-text">{done === due.length ? "All done for today." : `${done} of ${due.length} done`}</span>
<span className="bar"><span style={{ width: `${(done / due.length) * 100}%` }} /></span>
</div>
)}
{due.length > 0 ? (
<ul className="hlist">{due.map((r) => <Row key={r.h.id} {...r} day={day} />)}</ul>
) : (
<p className="notice">Nothing is due today. Enjoy the day off, or check something off anyway below.</p>
)}
{avoid.length > 0 && (
<section className="today-section">
<h2 className="section-label">Going without</h2>
<ul className="hlist">{avoid.map((r) => <Row key={r.h.id} {...r} day={day} />)}</ul>
</section>
)}
{rest.length > 0 && (
<section className="today-section">
<button type="button" className="section-toggle" aria-expanded={showRest} onClick={() => setShowRest(!showRest)}>
Not due today ({rest.length}) <span aria-hidden="true">{showRest ? "-" : "+"}</span>
</button>
{showRest && <ul className="hlist">{rest.map((r) => <Row key={r.h.id} {...r} day={day} />)}</ul>}
</section>
)}
<button type="button" className="btn btn-line btn-block add-bottom" onClick={() => setAdding({ name: "", kind: "check", target: 8, unit: "", schedule: { type: "daily" } })}><Plus size={18} /> Add a habit</button>
</>
)}
{adding && <Editor initial={adding} onClose={closeEditor} />}
</div>
);
}
Change history
Add openapi.json for the new-habit link and link it from the guides
Point OpenApps links to openapps.free
Bind agent address in policy
Use OpenApp service URLs and add contribution strip
Bounded source sync
Bounded source sync
Bounded source sync