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/Share.tsx
// Sharing one habit with a friend: a link that lets both of you see each other's streak for this
// habit, live. Nothing else about anyone's habits is shared.
import { useState } from "react";
import { useStore } from "./lib/store";
import { shareHabit, leaveTeam, useUser, type Member } from "./lib/sync";
import { explain } from "./lib/bounded";
import { dayNum, fmt, keyOf } from "./lib/dates";
import type { Habit } from "./lib/habits";
import { SITE_URL } from "../functions/lib/config";
import { useToday } from "./lib/useToday";
import { useMembers, memberStreak } from "./Checkin";
import { copy, share, toast } from "./ui";
import { People } from "./icons";
export const MAX_PERSON = 40;
export const shareUrl = (team: string) => `${SITE_URL}/s/${team}`;
function status(m: Member, todayKey: string): string {
if (m.unit === "without") return m.last ? (m.last === todayKey ? "Slipped today" : `Last slip ${fmt(dayNum(m.last))}`) : "No slips yet";
if (!m.last) return "Not started yet";
return m.last === todayKey ? "Done today" : `Last done ${fmt(dayNum(m.last))}`;
}
export function MemberList({ team }: { team: string }) {
const user = useUser();
const now = useToday();
const { rows, error } = useMembers(team);
if (!user) return <p className="muted small">Connecting...</p>;
if (error && !rows) return <p className="muted small">Could not load the streaks right now. They come back when you are online.</p>;
if (!rows) return <p className="loading small"><span className="spinner" /> Loading streaks...</p>;
const sorted = [...rows].sort((a, b) => (a.id === user.id ? -1 : b.id === user.id ? 1 : b.streak - a.streak));
return (
<ul className="members">
{sorted.map((m) => (
<li key={m.id} className={m.id === user.id ? "me" : ""}>
<span className="member-name">{m.name}{m.id === user.id && <span className="you-tag">You</span>}</span>
<span className="member-streak">{memberStreak(m)}</span>
<span className="member-status">{status(m, keyOf(now))}</span>
</li>
))}
{sorted.length < 2 && <li className="members-wait">Your friend shows up here once they open the link.</li>}
</ul>
);
}
export function ShareCard({ h }: { h: Habit }) {
const s = useStore();
const [name, setName] = useState(s.name);
const [busy, setBusy] = useState(false);
const [open, setOpen] = useState(false);
if (h.team) {
const url = shareUrl(h.team);
return (
<section className="card pad stack share-card">
<h2 className="h3 icon-title"><People /> Shared with friends</h2>
<MemberList team={h.team} />
<div className="link-row">
<span className="link-code">{url.replace(/^https:\/\//, "")}</span>
<button type="button" className="btn btn-sm" onClick={() => share(url, `Track "${h.name}" with me on Habits`)}>Share</button>
<button type="button" className="btn btn-line btn-sm" onClick={() => copy(url, "Link copied")}>Copy</button>
</div>
<p className="hint">Anyone with this link can see the streaks for this habit, and nothing else. Send it only to the people you want.</p>
<button type="button" className="btn btn-quiet btn-sm btn-danger self-start" disabled={busy} onClick={async () => {
if (!window.confirm("Stop sharing this habit? Your friends will no longer see your streak, and you will not see theirs.")) return;
setBusy(true);
try { await leaveTeam(h.id); toast("You stopped sharing this habit"); } catch (err) { toast(explain(err)); }
setBusy(false);
}}>Stop sharing</button>
</section>
);
}
const start = async () => {
const n = name.trim();
if (!n) { toast("Add your name so your friend knows it is you."); return; }
setBusy(true);
try {
const team = await shareHabit(h.id, n.slice(0, MAX_PERSON));
void share(shareUrl(team), `Track "${h.name}" with me on Habits`);
} catch (err) {
toast(explain(err));
}
setBusy(false);
};
return (
<section className="card pad stack share-card">
<h2 className="h3 icon-title"><People /> Do it with a friend</h2>
<p className="muted small">Share this one habit by link. You see each other's streak for it, live. Nothing else is shared: not your other habits, notes or history.</p>
{open ? (
<form className="stack" onSubmit={(e) => { e.preventDefault(); void start(); }}>
<label className="field">
<span>Your name, as your friend sees it</span>
<input className="input" value={name} maxLength={MAX_PERSON} autoFocus placeholder="Sam" onChange={(e) => setName(e.target.value)} />
</label>
<button type="submit" className="btn" disabled={busy}>{busy ? "Making the link..." : "Make a share link"}</button>
</form>
) : (
<button type="button" className="btn btn-line self-start" onClick={() => setOpen(true)}>Share with a friend</button>
)}
</section>
);
}
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