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/Settings.tsx
// Settings: your name on shared habits, the first day of the week, keeping habits on all devices,
// downloading everything as CSV, and installing Habits on the home screen.
import { useEffect, useState } from "react";
import { useStore, update, replace, fresh, getState } from "./lib/store";
import { csv } from "./lib/habits";
import { keepOnAllDevices, flush, useUser } from "./lib/sync";
import { signOut, explain } from "./lib/bounded";
import { dayKey } from "./lib/dates";
import { download, toast } from "./ui";
import { Download } from "./icons";
import { MAX_PERSON } from "./Share";
function Account() {
const user = useUser();
const [busy, setBusy] = useState(false);
if (!user) return <section className="card pad stack"><h2 className="h3">Your account</h2><p className="muted small">Connecting... Your habits are saved on this device either way.</p></section>;
if (!user.guest) {
return (
<section className="card pad stack">
<h2 className="h3">Your account</h2>
<p className="muted small">Signed in{user.email ? ` as ${user.email}` : ""}. Your habits are saved to your account and show up on every device you sign in on.</p>
<button type="button" className="btn btn-line btn-sm self-start" disabled={busy} onClick={async () => {
setBusy(true);
try {
await flush().catch(() => null);
replace(fresh());
await signOut();
location.href = "/today";
} catch (err) { toast(explain(err)); setBusy(false); }
}}>{busy ? "Signing out..." : "Sign out"}</button>
</section>
);
}
return (
<section className="card pad stack account-card">
<h2 className="h3">Keep my habits on all devices</h2>
<p className="muted small">Right now your habits are saved in this browser and a guest account made for it. Sign in with your email to keep them on your phone, laptop and anywhere else. No password, and it stays free.</p>
<button type="button" className="btn btn-sm self-start" disabled={busy} onClick={async () => {
setBusy(true);
try { await keepOnAllDevices(); } catch (err) { toast(explain(err)); setBusy(false); }
}}>{busy ? "Opening sign-in..." : "Continue with email"}</button>
</section>
);
}
type InstallPrompt = Event & { prompt(): Promise<void>; userChoice: Promise<{ outcome: string }> };
let early: InstallPrompt | null = null;
window.addEventListener("beforeinstallprompt", (e) => { e.preventDefault(); early = e as InstallPrompt; });
function Install() {
const [prompt, setPrompt] = useState<InstallPrompt | null>(early);
const [installed, setInstalled] = useState(() => matchMedia("(display-mode: standalone)").matches);
useEffect(() => {
const offer = (e: Event) => { e.preventDefault(); setPrompt(e as InstallPrompt); };
const done = () => { setInstalled(true); setPrompt(null); };
window.addEventListener("beforeinstallprompt", offer);
window.addEventListener("appinstalled", done);
return () => { window.removeEventListener("beforeinstallprompt", offer); window.removeEventListener("appinstalled", done); };
}, []);
if (installed) return <p className="muted small">Habits is on your home screen.</p>;
if (prompt) {
return <button type="button" className="btn btn-line btn-sm self-start" onClick={async () => { await prompt.prompt(); const { outcome } = await prompt.userChoice; if (outcome === "accepted") setInstalled(true); setPrompt(null); }}>Add Habits to your home screen</button>;
}
return <p className="muted small">On iPhone or iPad, tap Share, then Add to Home Screen. In Chrome or Edge, use Install app in the menu.</p>;
}
export function SettingsPage() {
const s = useStore();
const [name, setName] = useState(s.name);
useEffect(() => setName(s.name), [s.name]);
const all = Object.values(s.habits).filter((h) => !h.deleted);
return (
<div className="page settings">
<header className="page-head"><div><p className="eyebrow">Habits</p><h1 className="h1">Settings</h1></div></header>
<div className="settings-grid">
<section className="card pad stack">
<h2 className="h3">You</h2>
<label className="field">
<span>Your name on shared habits</span>
<input className="input" value={name} maxLength={MAX_PERSON} placeholder="Only friends you share with see it"
onChange={(e) => setName(e.target.value)}
onBlur={() => { if (name.trim() !== s.name) { update((x) => { x.name = name.trim(); x.at = Date.now(); }); toast("Saved"); } }} />
</label>
<div className="field">
<span id="ws-label">Weeks start on</span>
<div className="segmented two-up" role="radiogroup" aria-labelledby="ws-label">
{([1, 0] as const).map((d) => (
<button key={d} type="button" role="radio" aria-checked={s.weekStart === d} onClick={() => update((x) => { x.weekStart = d; x.at = Date.now(); })}>{d === 1 ? "Monday" : "Sunday"}</button>
))}
</div>
</div>
</section>
<Account />
<section className="card pad stack">
<h2 className="h3">Your data</h2>
<p className="muted small">Download every check-in and note as a CSV file that opens in any spreadsheet. Your habits are yours.</p>
<button type="button" className="btn btn-line btn-sm self-start" disabled={!all.length} onClick={() => download(`habits-${dayKey()}.csv`, csv(Object.values(getState().habits).filter((h) => !h.deleted)), "text/csv")}><Download size={16} /> Download CSV</button>
</section>
<section className="card pad stack">
<h2 className="h3">On your phone</h2>
<Install />
<p className="muted small">Reminders: a web page cannot reliably send them on its own, so each habit's page can add a repeating reminder to your calendar instead.</p>
</section>
</div>
<p className="hint fine">Habits is free, with no ads, no tracking and no limit on habits. Your habits are private: only friends you share one habit with see that habit's streak.</p>
</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