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 bc888334fc51
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/Week.tsx
// The week at a glance: every habit against the seven days, tap a day to check it off or change it.
import { useState } from "react";
import { useStore, listHabits, setDay } from "./lib/store";
import { met, type Habit } from "./lib/habits";
import { fmt, keyOf, weekday, weekOf, WEEKDAY_SHORT } from "./lib/dates";
import { useToday } from "./lib/useToday";
import { DaySheet } from "./Checkin";
import { A, toast } from "./ui";
import { Back, Next, Check, Close } from "./icons";

function scheduled(h: Habit, d: number): boolean {
  return h.kind === "avoid" || h.schedule.type !== "days" || h.schedule.days.includes(weekday(d));
}

function summary(h: Habit, days: number[], now: number): string {
  const past = days.filter((d) => d <= now);
  const hits = past.filter((d) => met(h, h.days[keyOf(d)])).length;
  if (h.kind === "avoid") return hits === 0 ? "No slips" : `${hits} ${hits === 1 ? "slip" : "slips"}`;
  if (h.schedule.type === "weekly") return `${hits} of ${h.schedule.times}`;
  if (h.schedule.type === "every") return `${hits} done`;
  const due = days.filter((d) => scheduled(h, d)).length;
  return `${hits} of ${due}`;
}

export function WeekPage() {
  const s = useStore();
  const now = useToday();
  const [offset, setOffset] = useState(0);
  const [editing, setEditing] = useState<{ id: string; day: string } | null>(null);
  const start = weekOf(now, s.weekStart) + offset * 7;
  const days = Array.from({ length: 7 }, (_, i) => start + i);
  const habits = listHabits(s);

  const tap = (h: Habit, d: number) => {
    const key = keyOf(d);
    if (h.kind === "count") return setEditing({ id: h.id, day: key });
    const was = h.days[key]?.n ?? 0;
    const next = was > 0 ? 0 : 1;
    setDay(h.id, key, next);
    const what = h.kind === "avoid" ? (next ? "Slip noted" : "Slip removed") : next ? "Checked off" : "Unchecked";
    toast(`${what}: ${h.name}, ${fmt(d, { weekday: "short" })}`, { label: "Undo", run: () => setDay(h.id, key, was) });
  };

  return (
    <div className="page week">
      <header className="page-head">
        <div>
          <p className="eyebrow">{fmt(days[0])} to {fmt(days[6])}</p>
          <h1 className="h1">{offset === 0 ? "This week" : offset === -1 ? "Last week" : "Week"}</h1>
        </div>
        <div className="row week-nav">
          <button type="button" className="round-btn" aria-label="Previous week" onClick={() => setOffset(offset - 1)}><Back /></button>
          <button type="button" className="round-btn" aria-label="Next week" disabled={offset >= 0} onClick={() => setOffset(offset + 1)}><Next /></button>
        </div>
      </header>

      {habits.length === 0 ? (
        <div className="empty">
          <p>No habits yet.</p>
          <A className="btn" href="/today">Add your first habit</A>
        </div>
      ) : (
        <div className="week-grid" role="table" aria-label="Habits this week">
          <div className="week-row week-headrow" role="row">
            <span className="week-name" role="columnheader"><span className="sr-only">Habit</span></span>
            {days.map((d) => (
              <span key={d} role="columnheader" className={`week-day ${d === now ? "today" : ""}`}>
                <small>{WEEKDAY_SHORT[weekday(d)].slice(0, 1)}</small>{fmt(d, { day: "numeric" })}
              </span>
            ))}
            <span className="week-sum" role="columnheader"><span className="sr-only">Total</span></span>
          </div>
          {habits.map((h) => (
            <div className="week-row" role="row" key={h.id}>
              <A href={`/h/${h.id}`} className="week-name">{h.name}</A>
              {days.map((d) => {
                const key = keyOf(d);
                const entry = h.days[key];
                const ok = met(h, entry);
                const part = !ok && (entry?.n ?? 0) > 0;
                const future = d > now;
                const cls = h.kind === "avoid"
                  ? (ok ? "slip" : future || key < h.start ? "" : "clean")
                  : ok ? "done" : part ? "part" : scheduled(h, d) ? "" : "off";
                const label = `${h.name}, ${fmt(d, { weekday: "long", month: "short", day: "numeric" })}: ${h.kind === "avoid" ? (ok ? "slipped" : "no slip") : ok ? "done" : part ? `${entry!.n} of ${h.target}` : "not done"}`;
                return (
                  <span role="cell" key={d} className="week-cell-wrap">
                    <button type="button" className={`week-cell ${cls}`} disabled={future} aria-label={label} onClick={() => tap(h, d)}>
                      {h.kind === "avoid" ? (ok ? <Close size={16} /> : null) : ok ? <Check size={18} /> : part ? entry!.n : null}
                    </button>
                  </span>
                );
              })}
              <span className="week-sum" role="cell">{summary(h, days, now)}</span>
            </div>
          ))}
        </div>
      )}
      {habits.length > 0 && <p className="hint week-hint">Tap a day to check it off. Counted habits open the day so you can set the number.</p>}
      {editing && <DaySheet habitId={editing.id} day={editing.day} onClose={() => setEditing(null)} />}
    </div>
  );
}

Change history

Point OpenApps links to openapps.free
bc888334fc · 18 hours ago
Bind agent address in policy
1f744a2b83 · 19 hours ago
Use OpenApp service URLs and add contribution strip
a9bb601ee0 · 19 hours ago
Bounded source sync
5646afc3a0 · 1 day ago
Bounded source sync
f1b1bac8d6 · 1 day ago
Bounded source sync
b1e14b41b0 · 1 day ago