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/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
Bind agent address in policy
Use OpenApp service URLs and add contribution strip
Bounded source sync
Bounded source sync
Bounded source sync