habits-free.openapps.xyz

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.
Owned app · rev 5f57a54c4912
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.
← All files
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
5f57a54c49 · 19 hours ago
Point OpenApps links to openapps.free
bc888334fc · 1 day ago
Bind agent address in policy
1f744a2b83 · 1 day ago
Use OpenApp service URLs and add contribution strip
a9bb601ee0 · 1 day ago
Bounded source sync
5646afc3a0 · 2 days ago
Bounded source sync
f1b1bac8d6 · 2 days ago
Bounded source sync
b1e14b41b0 · 2 days ago