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/Join.tsx
// Opening a friend's share link: see the shared habit and its streaks, then track it together,
// as a new habit or with one you already have.
import { useEffect, useState } from "react";
import { useStore, listHabits, update, getState, nextOrder } from "./lib/store";
import { openTeam, linkTeam, teamSchedule, type Team } from "./lib/sync";
import { explain } from "./lib/bounded";
import { newHabit, scheduleLabel, type Kind } from "./lib/habits";
import { navigate } from "./lib/router";
import { MemberList, MAX_PERSON } from "./Share";
import { A, toast } from "./ui";

export function JoinPage({ id }: { id: string }) {
  const s = useStore();
  const habits = listHabits(s).filter((h) => !h.team);
  const linked = Object.values(s.habits).find((h) => h.team === id && !h.deleted);
  const [team, setTeam] = useState<Team | null | undefined>(undefined);
  const [error, setError] = useState("");
  const [name, setName] = useState(s.name);
  const [mode, setMode] = useState<"new" | "mine">("new");
  const [pick, setPick] = useState(habits[0]?.id ?? "");

  useEffect(() => {
    if (linked) { navigate(`/h/${linked.id}`, true); return; }
    openTeam(id).then(setTeam).catch((err) => setError(explain(err)));
  }, [id]);

  if (linked) return null;
  if (error) return <div className="page"><div className="empty"><p>{error}</p><button type="button" className="btn" onClick={() => location.reload()}>Try again</button></div></div>;
  if (team === undefined) return <div className="page"><p className="loading"><span className="spinner" /> Opening the shared habit...</p></div>;
  if (team === null) {
    return (
      <div className="page">
        <div className="empty">
          <h1 className="h2">This link does not work.</h1>
          <p className="muted">The shared habit was not found. Ask your friend to send the link again.</p>
          <A className="btn" href="/today">Go to my habits</A>
        </div>
      </div>
    );
  }

  const kind = (["check", "count", "avoid"].includes(team.kind) ? team.kind : "check") as Kind;
  const schedule = teamSchedule(team);

  const join = () => {
    const n = name.trim();
    if (!n) { toast("Add your name so your friend knows it is you."); return; }
    let habitId = pick;
    if (mode === "new" || !habits.length) {
      const h = newHabit({ name: team.name, kind, target: Math.max(1, team.target), unit: team.unit, schedule, team: id }, nextOrder(getState()));
      update((x) => { x.habits[h.id] = h; x.name = n.slice(0, MAX_PERSON); x.at = Date.now(); });
      habitId = h.id;
    } else {
      linkTeam(habitId, id, n.slice(0, MAX_PERSON));
    }
    toast("You are in. Your friend can see your streak for this habit.");
    navigate(`/h/${habitId}`);
  };

  return (
    <div className="page join">
      <header className="page-head">
        <div>
          <p className="eyebrow">A friend shared a habit with you</p>
          <h1 className="h1">{team.name}</h1>
          <p className="muted join-sub">{scheduleLabel({ kind, schedule })}{kind === "count" ? ` · ${team.target}${team.unit ? ` ${team.unit}` : ""} a day` : ""}</p>
        </div>
      </header>
      <div className="join-grid">
        <section className="card pad stack">
          <h2 className="h3">Their streak</h2>
          <MemberList team={id} />
        </section>
        <section className="card pad stack">
          <h2 className="h3">Track it together</h2>
          <p className="muted small">You will see each other's streak for this habit, live. Nothing else is shared: not your other habits, notes or history. Free, and no sign-up.</p>
          <label className="field">
            <span>Your name, as your friend sees it</span>
            <input className="input" value={name} maxLength={MAX_PERSON} placeholder="Alex" onChange={(e) => setName(e.target.value)} />
          </label>
          {habits.length > 0 && (
            <div className="field">
              <span id="join-mode">Which habit</span>
              <div className="choice-list" role="radiogroup" aria-labelledby="join-mode">
                <button type="button" role="radio" aria-checked={mode === "new"} className="choice" onClick={() => setMode("new")}><span className="choice-dot" aria-hidden="true" />Add "{team.name}" as a new habit</button>
                <button type="button" role="radio" aria-checked={mode === "mine"} className="choice" onClick={() => setMode("mine")}><span className="choice-dot" aria-hidden="true" />Use one I already track</button>
              </div>
              {mode === "mine" && (
                <select className="select" aria-label="Your habit" value={pick} onChange={(e) => setPick(e.target.value)}>
                  {habits.map((h) => <option key={h.id} value={h.id}>{h.name}</option>)}
                </select>
              )}
            </div>
          )}
          <button type="button" className="btn btn-lg" onClick={join}>Track it together</button>
        </section>
      </div>
    </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