baby-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 c84ed8821371
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 381bd6356d4b794ac2a7c1b8
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/Start.tsx
// Starting a family log (the landing hero, and the app when there is no family yet) and joining one
// from a family link. The data layer loads only when someone starts or joins.
import { useEffect, useState, type FormEvent } from "react";
import { navigate, onLink } from "./lib/router";
import { read, useMyName } from "./lib/prefs";
import { explain } from "./lib/explain";

export function StartForm({ compact = false }: { compact?: boolean }) {
  const [myName, setMyName] = useMyName();
  const [baby, setBaby] = useState("");
  const [birth, setBirth] = useState("");
  const [name, setName] = useState(myName);
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const today = new Date().toLocaleDateString("en-CA");

  const submit = async (e: FormEvent) => {
    e.preventDefault();
    if (!baby.trim()) return setError("Add your baby's name, or a nickname.");
    setBusy(true);
    setError(null);
    try {
      const who = name.trim() || "Parent";
      setMyName(who);
      const { createFamily } = await import("./lib/family");
      await createFamily({ babyName: baby.trim().slice(0, 40), birth: /^\d{4}-\d{2}-\d{2}$/.test(birth) ? birth : "", myName: who.slice(0, 40) });
      navigate("/app");
    } catch (err) {
      setError(explain(err));
      setBusy(false);
    }
  };

  return (
    <form className={`start-form ${compact ? "" : "card"}`} onSubmit={submit} noValidate>
      <label className="field"><span>Baby's name</span>
        <input className="input input-lg" value={baby} onChange={(e) => { setBaby(e.target.value); setError(null); }} maxLength={40} placeholder="Ada, or a nickname" autoComplete="off" aria-invalid={error && !baby.trim() ? true : undefined} />
      </label>
      <div className="two start-two">
        <label className="field"><span>Your name</span>
          <input className="input input-lg" value={name} onChange={(e) => setName(e.target.value)} maxLength={40} placeholder="Mom, Dad, Sam" autoComplete="given-name" />
        </label>
        <label className="field"><span>Birth date <em>optional</em></span>
          <input className="input input-lg" type="date" value={birth} max={today} onChange={(e) => setBirth(e.target.value)} />
        </label>
      </div>
      {error && <p className="notice notice-error" role="alert">{error}</p>}
      <button className="btn btn-lg btn-block" type="submit" aria-busy={busy || undefined}>Start the log</button>
      <p className="hint center">Free. No sign-up. Invite your partner with a link after.</p>
    </form>
  );
}

/** The landing hero's card: a returning family goes straight in; a new one starts here. */
export function StartCard() {
  const returning = Boolean(read<{ family?: string }>("baby:active", {}).family);
  const [fresh, setFresh] = useState(false);
  if (returning && !fresh) {
    return (
      <div className="start-form card">
        <p className="eyebrow">Welcome back</p>
        <a className="btn btn-lg btn-block" href="/app" onClick={onLink}>Open your log</a>
        <button type="button" className="btn btn-quiet btn-block" onClick={() => setFresh(true)}>Start a log for another baby or family</button>
      </div>
    );
  }
  return <StartForm />;
}

export function JoinPage() {
  const key = decodeURIComponent(location.hash.replace(/^#/, ""));
  const [myName, setMyName] = useMyName();
  const [name, setName] = useState(myName);
  const [savedName] = useState(myName);
  const [state, setState] = useState<"checking" | "ready" | "joining">("checking");
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const { alreadyIn, joinFamily } = await import("./lib/family");
        if (await alreadyIn(key)) {
          await joinFamily(key, savedName || "Me");
          if (!cancelled) navigate("/app", true);
          return;
        }
      } catch { /* show the form; joining explains any problem */ }
      if (!cancelled) setState("ready");
    })();
    return () => { cancelled = true; };
  }, [key]);

  const join = async (e: FormEvent) => {
    e.preventDefault();
    const who = name.trim();
    if (!who) return setError("Add your name, so the family sees who logged what.");
    setState("joining");
    setError(null);
    try {
      setMyName(who);
      const { joinFamily } = await import("./lib/family");
      await joinFamily(key, who.slice(0, 40));
      navigate("/app", true);
    } catch (err) {
      setError(explain(err));
      setState("ready");
    }
  };

  return (
    <div className="page narrow">
      <div className="join card">
        <img className="art join-art" src="/art/card-baby.webp" alt="" width="200" height="170" />
        <h1 className="page-title">Join the family log</h1>
        <p className="muted">Someone shared their baby log with you. You will see every feed, sleep and diaper as it is logged, and can log too.</p>
        {state === "checking" ? <div className="stack" aria-busy="true" aria-label="Opening the link"><span className="skeleton" style={{ height: 76 }} /><span className="skeleton" style={{ height: 54 }} /></div> : (
          <form className="stack" onSubmit={join} noValidate>
            <label className="field"><span>Your name</span>
              <input className="input input-lg" value={name} onChange={(e) => setName(e.target.value)} maxLength={40} placeholder="Grandma, Dad, Sam" autoFocus autoComplete="given-name" />
            </label>
            {error && <p className="notice notice-error" role="alert">{error}</p>}
            <button className="btn btn-lg btn-block" type="submit" aria-busy={state === "joining" || undefined}>Join the log</button>
            <p className="hint">Anyone with this link can see and add to the log, so only open links from people you know.</p>
          </form>
        )}
      </div>
    </div>
  );
}

Change history

Quality pass: undo on timers, sleep by hour chart, diaper color, fixes
c84ed88213 · 5 hours ago
Bounded source sync
1ed137d95d · 16 hours ago
Bounded source sync
851999f793 · 17 hours ago
Bounded source sync
9623bc231c · 17 hours ago