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/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
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