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/Habits.tsx
// All habits: put them in order, open one, and see the archived ones.
import { useState } from "react";
import { useStore, listHabits, update, editHabit } from "./lib/store";
import { scheduleLabel, stats, streakLabel, type Habit } from "./lib/habits";
import { useToday } from "./lib/useToday";
import { Editor } from "./Editor";
import { A, toast } from "./ui";
import { Up, Down, Plus, People } from "./icons";

/** Swaps a habit with its neighbour in the list. */
function move(list: Habit[], i: number, by: -1 | 1) {
  const j = i + by;
  if (j < 0 || j >= list.length) return;
  update((s) => {
    list.forEach((h, k) => { const x = s.habits[h.id]; if (x.order !== k) { x.order = k; x.at = Date.now(); } });
    const a = s.habits[list[i].id];
    const b = s.habits[list[j].id];
    [a.order, b.order] = [b.order, a.order];
    a.at = b.at = Date.now();
  });
}

export function HabitsPage() {
  const s = useStore();
  const now = useToday();
  const [adding, setAdding] = useState(false);
  const live = listHabits(s);
  const archived = listHabits(s, true);
  return (
    <div className="page habits-page">
      <header className="page-head">
        <div>
          <p className="eyebrow">{live.length} {live.length === 1 ? "habit" : "habits"}</p>
          <h1 className="h1">Your habits</h1>
        </div>
        <button type="button" className="btn head-add" onClick={() => setAdding(true)}><Plus size={18} /> Add a habit</button>
      </header>
      {live.length === 0 ? (
        <div className="empty"><p>No habits yet.</p><button type="button" className="btn" onClick={() => setAdding(true)}>Add your first habit</button></div>
      ) : (
        <ul className="manage-list">
          {live.map((h, i) => {
            const st = stats(h, s.weekStart, now);
            return (
              <li key={h.id}>
                <A href={`/h/${h.id}`} className="manage-main">
                  <strong>{h.name}</strong>
                  <span>{scheduleLabel(h)} · {streakLabel(st.current, st.unit, h.kind === "avoid")}{h.team && <> · <People size={14} className="inline-icon" /> shared</>}</span>
                </A>
                <div className="move">
                  <button type="button" className="icon-btn" aria-label={`Move ${h.name} up`} disabled={i === 0} onClick={() => move(live, i, -1)}><Up /></button>
                  <button type="button" className="icon-btn" aria-label={`Move ${h.name} down`} disabled={i === live.length - 1} onClick={() => move(live, i, 1)}><Down /></button>
                </div>
              </li>
            );
          })}
        </ul>
      )}
      <button type="button" className="btn btn-line btn-block add-bottom" onClick={() => setAdding(true)}><Plus size={18} /> Add a habit</button>

      {archived.length > 0 && (
        <section className="today-section">
          <h2 className="section-label">Archived</h2>
          <ul className="manage-list">
            {archived.map((h) => (
              <li key={h.id}>
                <A href={`/h/${h.id}`} className="manage-main"><strong>{h.name}</strong><span>{scheduleLabel(h)}</span></A>
                <button type="button" className="btn btn-line btn-sm" onClick={() => { editHabit(h.id, (x) => { x.archived = false; }); toast(`Restored ${h.name}`); }}>Restore</button>
              </li>
            ))}
          </ul>
        </section>
      )}
      {adding && <Editor onClose={() => setAdding(false)} />}
    </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