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 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.
src/App.tsx
// The app behind the landing page: Today, Timeline, Charts and Family, with tabs in the header on
// wide screens and at the bottom on phones. The night look turns on after dark (or always, or never).
import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { me as getMe, explain, takeLoginError, type Me } from "./lib/bounded";
import { useFamilies, useFamily, useEvents, finishUpgrade, forgetFamily, type Family, type FamilyRef, type Member } from "./lib/family";
import { useActive, useLook, write, read } from "./lib/prefs";
import { onLink, navigate } from "./lib/router";
import { Icon, AppSkeleton, toast, useNow, localDayStart, addDays, ageText } from "./ui";
import { JoinPage, StartForm } from "./Start";
import { Today } from "./Today";
import { Timeline } from "./Timeline";
import { Charts } from "./Charts";
import { FamilyPage } from "./FamilyPage";
import { Report } from "./Report";
import type { Baby, BabyEvent } from "../functions/lib/baby";
import { set } from "@bounded-sh/client";
export interface FamilyCtx {
h: string;
me: Me;
house: Family;
members: Member[];
owners: Set<string>;
babies: Baby[];
allBabies: Baby[];
baby: Baby | null;
events: BabyEvent[];
familyKey: string | null;
families: FamilyRef[];
isOwner: boolean;
nameOf: (uid: string) => string;
setBaby: (id: string) => void;
}
const TABS = [
{ id: "", label: "Today", icon: "today" },
{ id: "timeline", label: "Timeline", icon: "timeline" },
{ id: "charts", label: "Charts", icon: "charts" },
{ id: "family", label: "Family", icon: "family" },
] as const;
function useNightLook(off: boolean): boolean {
const [look] = useLook();
const now = useNow(60_000);
const [dark, setDark] = useState(() => matchMedia("(prefers-color-scheme: dark)").matches);
useEffect(() => {
const q = matchMedia("(prefers-color-scheme: dark)");
const on = () => setDark(q.matches);
q.addEventListener("change", on);
return () => q.removeEventListener("change", on);
}, []);
const hour = new Date(now * 1000).getHours();
const night = !off && (look === "night" || (look === "auto" && (dark || hour >= 19 || hour < 7)));
useEffect(() => {
document.body.classList.toggle("night", night);
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", night ? "#0e0f11" : "#fdfdfc");
return () => { document.body.classList.remove("night"); };
}, [night]);
return night;
}
/** A line under the header while the phone has no connection: the log is shared live, so nothing saves offline. */
function Offline() {
const [offline, setOffline] = useState(() => !navigator.onLine);
useEffect(() => {
const on = () => setOffline(!navigator.onLine);
window.addEventListener("online", on);
window.addEventListener("offline", on);
return () => { window.removeEventListener("online", on); window.removeEventListener("offline", on); };
}, []);
return offline ? <p className="offline enter" role="status">You are offline. New entries cannot be saved until you reconnect.</p> : null;
}
export default function App({ path }: { path: string }) {
const section = path === "/join" ? "join" : path.split("/")[2] || "";
useNightLook(section === "report");
const [m, setM] = useState<Me | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (section === "join") return;
let cancelled = false;
(async () => {
try {
const who = await getMe();
const note = await finishUpgrade(who).catch((e) => explain(e));
const loginError = takeLoginError();
if (cancelled) return;
setM(who);
if (loginError) toast(loginError); else if (note) toast(note);
} catch (err) {
if (!cancelled) setError(explain(err));
}
})();
return () => { cancelled = true; };
}, [section === "join"]);
if (section === "join") return <JoinPage />;
if (error) return <ErrorPage text={error} />;
if (!m) return <AppSkeleton />;
return <><Offline /><Families me={m} section={section} /></>;
}
function Families({ me, section }: { me: Me; section: string }) {
const { families, error } = useFamilies(me.id);
const [active] = useActive();
if (error) return <ErrorPage text={explain(error)} />;
if (!families) return <AppSkeleton />;
if (!families.length) return <NoFamily />;
const h = families.some((f) => f.id === active.family) ? active.family! : families[0].id;
return <FamilyApp key={h} h={h} me={me} families={families} section={section} />;
}
function NoFamily() {
return (
<div className="page narrow">
<h1 className="page-title">Start your baby log</h1>
<p className="page-lede">Add your baby, then share the family link with your partner or anyone who helps.</p>
<div style={{ marginTop: 22 }}><StartForm /></div>
<p className="muted small" style={{ marginTop: 18 }}>Someone already started a log for your family? Open the family link they sent you.</p>
</div>
);
}
function ErrorPage({ text }: { text: string }) {
return (
<div className="page narrow">
<div className="error-state">
<h2>The log did not open</h2>
<p>{text}</p>
<button className="btn" type="button" onClick={() => location.reload()}>Try again</button>
</div>
</div>
);
}
function Removed({ h, name }: { h: string; name: string }) {
return (
<div className="page narrow">
<div className="empty-state">
<h2>You no longer have access to {name || "this log"}</h2>
<p>The family may have removed you, or changed the link. If you should still be in it, ask someone in the family for the current link.</p>
<button className="btn" type="button" onClick={async () => { await forgetFamily(h).catch(() => null); write("baby:active", {}); }}>Remove it from my list</button>
</div>
</div>
);
}
function FamilyApp({ h, me, families, section }: { h: string; me: Me; families: FamilyRef[]; section: string }) {
const fam = useFamily(h);
const [active, setActive] = useActive();
const [since] = useState(() => addDays(localDayStart(Date.now() / 1000), -7));
const events = useEvents(h, since);
const ref = families.find((f) => f.id === h);
// Keep this person's list of families in step with the family's current name.
useEffect(() => {
if (fam.house?.name && ref && ref.name !== fam.house.name) void set(`users/${me.id}/households/${h}`, { name: fam.house.name }).catch(() => null);
}, [fam.house?.name, ref?.name]);
useEffect(() => { if (fam.house?.name) document.title = section === "report" ? `Baby report | ${fam.house.name}` : `${fam.house.name} | Baby`; }, [fam.house?.name, section]);
// Removed while the log is open: the member list stops including this person before any read fails.
if (fam.error || fam.house === null || (fam.members && !fam.members.some((x) => x.id === me.id))) return <Removed h={h} name={ref?.name || ""} />;
if (!fam.house || !fam.members || !fam.babies || !fam.owners || !events) return <AppSkeleton />;
const babies = fam.babies.filter((b) => !b.archived);
const baby = babies.find((b) => b.id === active.baby?.[h]) ?? babies[0] ?? null;
const members = fam.members;
const ctx: FamilyCtx = {
h, me, house: fam.house, members, owners: fam.owners, babies, allBabies: fam.babies, baby, events,
familyKey: fam.familyKey, families, isOwner: fam.owners.has(me.id),
nameOf: (uid) => (uid === me.id ? "you" : members.find((x) => x.id === uid)?.name ?? "a past member"),
setBaby: (id) => setActive({ ...read("baby:active", {}), family: h, baby: { ...(active.baby || {}), [h]: id } }),
};
const nav = document.getElementById("app-nav");
const tabs = TABS.map((t) => (
<a key={t.id} href={`/app${t.id ? `/${t.id}` : ""}`} onClick={onLink} className="tab" aria-current={section === t.id ? "page" : undefined}>
<Icon name={t.icon} size={22} /><span>{t.label}</span>
</a>
));
if (section === "report") return <Report ctx={ctx} />;
return (
<>
{nav && createPortal(tabs, nav)}
<div className="app-body">
{section !== "family" && <BabyBar ctx={ctx} />}
{section === "" && <Today ctx={ctx} />}
{section === "timeline" && <Timeline ctx={ctx} />}
{section === "charts" && <Charts ctx={ctx} />}
{section === "family" && <FamilyPage ctx={ctx} />}
{!["", "timeline", "charts", "family"].includes(section) && <NotFound />}
</div>
<nav className="tabbar" aria-label="Baby">{tabs}</nav>
</>
);
}
function NotFound() {
useEffect(() => { navigate("/app", true); }, []);
return null;
}
/** The baby in view, a switch between babies, and the day or night look. */
function BabyBar({ ctx }: { ctx: FamilyCtx }) {
const [look, setLook] = useLook();
const night = document.body.classList.contains("night");
if (!ctx.baby) {
return (
<div className="baby-bar wrap-app">
<p className="muted">No baby in this log yet. <a href="/app/family" onClick={onLink}>Add one under Family</a>.</p>
</div>
);
}
return (
<div className="baby-bar wrap-app">
{ctx.babies.length > 1 ? (
<div className="baby-switch" role="tablist" aria-label="Baby">
{ctx.babies.map((b) => (
<button key={b.id} type="button" role="tab" aria-selected={b.id === ctx.baby!.id} onClick={() => ctx.setBaby(b.id)}>{b.name}</button>
))}
</div>
) : (
<div className="baby-name">
<strong>{ctx.baby.name}</strong>
{ageText(ctx.baby.birth) && <span>{ageText(ctx.baby.birth)}</span>}
</div>
)}
<button type="button" className="icon-btn look-btn" onClick={() => setLook(night ? "day" : "night")} aria-label={night ? "Use the day look" : "Use the night look"} title={look === "auto" ? "Night look turns on after 7 pm" : undefined}>
<Icon name={night ? "sun" : "moon"} size={21} />
</button>
</div>
);
}
Change history
Quality pass: undo on timers, sleep by hour chart, diaper color, fixes
Bounded source sync
Bounded source sync
Bounded source sync