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