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 ee59501e91bae6d2c23a8370
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/Sheets.tsx
// The list's sheets: Share (link, people, assistants, admin link) and Settings (name, aisles, clear,
// leave or delete). People and link changes need a connection; the rest work offline.
import { useState, type FormEvent } from "react";
import { navigate } from "./lib/router";
import { SERVED_APP_ID, explain } from "./lib/bounded";
import {
removeMember, changeLink, leaveList, deleteList, updateList, setMyName,
type ListDoc, type Member,
} from "./lib/data";
import { Modal, copy, share, toast, Icon } from "./ui";
import { SECTIONS, listKey, sectionLabel, sectionOrder, LIMITS, type Item, type Known, type SectionId } from "../functions/lib/lists";
const API_BASE = `https://functions.openapps.xyz/apps/${SERVED_APP_ID}/api`;
const joined = (at: number) => new Date(at * 1000).toLocaleDateString(undefined, { month: "short", day: "numeric" });
export function ShareSheet({ id, uid, list, secret, members, owners, isOwner, ownerKey, onClose }: {
id: string; uid: string | null; list: ListDoc; secret: string | null; members: Member[]; owners: Set<string>;
isOwner: boolean; ownerKey?: string; onClose: () => void;
}) {
const url = secret ? `${location.origin}/l/${id}#${secret}` : null;
const key = secret ? listKey(id, secret) : null;
const mine = members.find((m) => m.id === uid);
const [name, setName] = useState(mine?.name || "");
const [removing, setRemoving] = useState<Member | null>(null);
const [newLink, setNewLink] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const run = async (work: () => Promise<unknown>, done: string) => {
setBusy(true);
setError("");
try {
await work();
toast(done);
setRemoving(null);
} catch (err) {
setError(explain(err));
} finally {
setBusy(false);
}
};
const saveName = (e: FormEvent) => {
e.preventDefault();
if (uid) { setMyName(id, uid, name); toast("Saved your name."); }
};
return (
<Modal title="Share this list" onClose={onClose}>
<div className="sheet">
<section>
<p>Anyone with this link can see {list.name} and change it, live. No sign-up and nothing to install.</p>
{url ? (
<>
<div className="link-row"><code className="link-code">{url}</code></div>
<div className="row">
<button className="btn" type="button" onClick={() => share(url, list.name, `Our ${list.name} list`)}><Icon name="share" size={16} /> Send the link</button>
<button className="btn btn-line" type="button" onClick={() => copy(url, "Link copied.")}>Copy</button>
</div>
</>
) : <p className="notice">The link appears once this list has synced. Connect to the internet for a moment.</p>}
</section>
<section>
<h3 className="h3">On this list</h3>
<ul className="people">
{members.map((m) => (
<li key={m.id}>
<span className="grow">
<strong>{m.name || (m.id === uid ? "You" : "Someone")}</strong>
{m.id === uid && m.name ? <span className="muted"> (you)</span> : null}
{owners.has(m.id) ? <span className="chip">manages</span> : null}
<small className="muted"> joined {joined(m.at)}</small>
</span>
{isOwner && m.id !== uid && (
<button className="btn btn-sm btn-quiet btn-danger" type="button" onClick={() => setRemoving(m)}>Remove</button>
)}
</li>
))}
</ul>
{removing && (
<div className="confirm">
<p>Remove {removing.name || "this person"} from {list.name}?</p>
<label className="check"><input type="checkbox" checked={newLink} onChange={(e) => setNewLink(e.target.checked)} /> Also change the link, so they cannot open the list again with the old one</label>
{owners.has(removing.id) && <p className="muted">They manage this list, so they can still come back with the admin link. The admin link cannot be changed.</p>}
<div className="row">
<button className="btn btn-sm" type="button" disabled={busy} onClick={() => run(() => removeMember(id, removing.id, newLink), newLink ? "Removed. The link changed: send the new one to the others if they need it." : "Removed.")}>Remove</button>
<button className="btn btn-sm btn-quiet" type="button" onClick={() => setRemoving(null)}>Cancel</button>
</div>
</div>
)}
<form className="name-row" onSubmit={saveName}>
<label className="field grow"><span>Your name, so the others know who is on the list</span>
<input className="input" value={name} onChange={(e) => setName(e.target.value)} maxLength={LIMITS.memberName} placeholder="Sam" />
</label>
<button className="btn btn-line" type="submit">Save</button>
</form>
</section>
{key && (
<section>
<h3 className="h3">For assistants</h3>
<p className="muted">Give this key to an AI or voice assistant so it can read the list and add to it ("add milk and eggs"). It works like the link: anyone with it can change the list.</p>
<div className="link-row"><code className="link-code">{key}</code><button className="btn btn-sm btn-line" type="button" onClick={() => copy(key, "Key copied.")}>Copy</button></div>
<pre className="code small-code">{`curl -X POST ${API_BASE}/list/items \\
-H 'x-list-key: ${key}' \\
-H 'content-type: application/json' \\
-d '{"text": "milk and eggs"}'`}</pre>
<p className="hint">Every route is in <a href="/llms.txt" target="_blank" rel="noopener">llms.txt</a> and <a href="/openapi.json" target="_blank" rel="noopener">openapi.json</a>.</p>
</section>
)}
{isOwner && (
<section>
<h3 className="h3">You manage this list</h3>
{ownerKey ? (
<>
<p className="muted">The admin link lets you remove people and delete the list from any device. Anyone with it can do that, so keep it to yourself, for example in your notes.</p>
<div className="link-row"><code className="link-code">{`${location.origin}/l/${id}/admin#${ownerKey}`}</code><button className="btn btn-sm btn-line" type="button" onClick={() => copy(`${location.origin}/l/${id}/admin#${ownerKey}`, "Admin link copied.")}>Copy</button></div>
</>
) : <p className="muted">The admin link is on the device that made this list.</p>}
<p className="muted">Changing the link turns off the old link and the old assistant key. Everyone already on the list stays.</p>
<button className="btn btn-line" type="button" disabled={busy} onClick={() => run(() => changeLink(id), "New link made. Send it to anyone who still needs to join.")}>Change the link</button>
</section>
)}
{error && <p className="notice notice-error" role="alert">{error}</p>}
</div>
</Modal>
);
}
export function SettingsSheet({ id, uid, list, items, known, isOwner, onClose, onClear }: {
id: string; uid: string | null; list: ListDoc; items: Item[]; known: Record<string, Known>; isOwner: boolean; onClose: () => void; onClear: () => void;
}) {
const [view, setView] = useState<"main" | "aisles" | "delete">("main");
const [name, setName] = useState(list.name);
const [order, setOrder] = useState<SectionId[]>(() => sectionOrder(list.order));
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const grocery = list.kind !== "todo";
const checked = items.filter((i) => i.checked).length;
const saveName = (e: FormEvent) => {
e.preventDefault();
const n = name.trim().slice(0, LIMITS.listName);
if (n && n !== list.name) { updateList(id, { name: n }); toast("Renamed."); }
};
const move = (i: number, by: number) => setOrder((o) => {
const next = [...o];
[next[i], next[i + by]] = [next[i + by], next[i]];
return next;
});
const online = async (work: () => Promise<unknown>) => {
setBusy(true);
setError("");
try {
await work();
onClose();
navigate("/lists", true);
} catch (err) {
setError(explain(err));
setBusy(false);
}
};
if (view === "aisles") {
return (
<Modal title="Arrange aisles" onClose={onClose} footer={<>
<button className="btn btn-quiet" type="button" onClick={() => setOrder(SECTIONS.map((s) => s.id))}>Reset</button>
<span className="grow" />
<button className="btn" type="button" onClick={() => { updateList(id, { order: JSON.stringify(order) }); toast("Aisles saved."); onClose(); }}>Save</button>
</>}>
<p className="muted">Put the sections in the order you walk {list.name}. Each list keeps its own order.</p>
<ol className="aisles">
{order.map((s, i) => (
<li key={s}>
<span className="grow">{sectionLabel(s)}</span>
<button className="icon-btn" type="button" disabled={i === 0} onClick={() => move(i, -1)} aria-label={`Move ${sectionLabel(s)} up`}><Icon name="up" /></button>
<button className="icon-btn" type="button" disabled={i === order.length - 1} onClick={() => move(i, 1)} aria-label={`Move ${sectionLabel(s)} down`}><Icon name="down" /></button>
</li>
))}
</ol>
</Modal>
);
}
if (view === "delete") {
return (
<Modal title={`Delete ${list.name}?`} onClose={onClose}>
<div className="sheet">
<p>This deletes the list and its items for everyone on it. It cannot be undone.</p>
{error && <p className="notice notice-error" role="alert">{error}</p>}
<div className="row">
<button className="btn btn-danger-solid" type="button" disabled={busy} onClick={() => online(() => deleteList(id, items.map((i) => i.id), Object.keys(known)))}>{busy ? "Deleting..." : "Delete for everyone"}</button>
<button className="btn btn-quiet" type="button" onClick={() => setView("main")}>Cancel</button>
</div>
</div>
</Modal>
);
}
return (
<Modal title="List settings" onClose={onClose}>
<div className="sheet">
<form className="name-row" onSubmit={saveName}>
<label className="field grow"><span>Name</span><input className="input" value={name} onChange={(e) => setName(e.target.value)} maxLength={LIMITS.listName} /></label>
<button className="btn btn-line" type="submit">Save</button>
</form>
<label className="check">
<input type="checkbox" checked={grocery} onChange={(e) => updateList(id, { kind: e.target.checked ? "grocery" : "todo" })} />
Sort items by store aisle
</label>
<div className="menu">
{grocery && <button type="button" onClick={() => setView("aisles")}>Arrange aisles<small>Match the order of your store</small></button>}
<button type="button" disabled={!checked} onClick={onClear}>Clear checked items<small>{checked ? `${checked} checked off` : "Nothing checked off"}</small></button>
{isOwner
? <button type="button" className="danger" onClick={() => setView("delete")}>Delete this list<small>For everyone on it</small></button>
: <button type="button" className="danger" disabled={busy || !uid} onClick={() => uid && online(() => leaveList(id, uid))}>Leave this list<small>It disappears from this device. You can rejoin with the link.</small></button>}
</div>
{error && <p className="notice notice-error" role="alert">{error}</p>}
</div>
</Modal>
);
}
Change history
Point OpenApps links to openapps.free
Bind agent address in policy constants
Use OpenApps service URLs and add contribution strip
Bounded source sync
Bounded source sync
Bounded source sync