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 334801de44cb0fe911d5a88b
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/Start.tsx
// The landing page's two doors: join an event with its code, or start one. Plus "Your events" for returning hosts.
import { useEffect, useState } from "react";
import { setMany } from "@bounded-sh/client";
import { me, explain } from "./lib/bounded";
import { navigate, usePath } from "./lib/router";
import { listMine, forget, remember, type Mine } from "./lib/mine";
import { findEvent } from "./Event";
import { checkTitle, eventWrites, randomCode, randomId, formatCode, LIMITS } from "../functions/lib/polls";
export function Start() {
return (
<div className="start">
<JoinBox />
<CreateBox />
</div>
);
}
function JoinBox() {
const [code, setCode] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const digits = code.replace(/\D/g, "");
const join = async () => {
if (!/^[1-9]\d{5}$/.test(digits)) { setError("Codes have 6 digits, like 482 913."); return; }
setBusy(true);
setError("");
try {
navigate(`/e/${await findEvent(digits)}`);
} catch (err) {
setError(err instanceof Error ? err.message : explain(err));
setBusy(false);
}
};
return (
<form className="card start-card" id="join" onSubmit={(e) => { e.preventDefault(); join(); }}>
<h2 className="h3">Joining an event?</h2>
<p className="muted small">Type the code on the presenter's screen.</p>
<div className="code-row">
<input className="input code-input" value={code} inputMode="numeric" autoComplete="off" maxLength={7} placeholder="123 456" aria-label="Event code"
onChange={(e) => { setCode(e.target.value.replace(/[^\d ]/g, "")); setError(""); }} />
<button className="btn btn-lg" type="submit" disabled={busy}>{busy ? "Joining..." : "Join"}</button>
</div>
{error && <p className="q-error" role="alert">{error}</p>}
</form>
);
}
function CreateBox() {
const [title, setTitle] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const create = async () => {
const checked = checkTitle(title);
if ("error" in checked) { setError(checked.error); return; }
setBusy(true);
setError("");
try {
const uid = await me();
const eventId = randomId(20);
const key = randomId(24);
// Codes are never reused, so a taken code refuses the batch; try a few fresh ones.
for (let attempt = 0; ; attempt++) {
const code = randomCode();
try {
await setMany(eventWrites({ eventId, code, key, me: uid, title: checked.title, polls: [] }));
remember(eventId, key, checked.title, code);
break;
} catch (err) {
if (attempt === 4) throw err;
}
}
try { localStorage.setItem(`polls:new:${eventId}`, "1"); } catch { /* the notice is optional */ }
navigate(`/e/${eventId}/admin`);
} catch (err) {
setError(explain(err));
setBusy(false);
}
};
return (
<form className="card start-card" onSubmit={(e) => { e.preventDefault(); create(); }}>
<h2 className="h3">Running one?</h2>
<p className="muted small">Name it, then add polls. No sign-up.</p>
<div className="code-row">
<input className="input" value={title} maxLength={LIMITS.title} placeholder="Team meeting, Monday" aria-label="Event name"
onChange={(e) => { setTitle(e.target.value); setError(""); }} />
<button className="btn btn-lg" type="submit" disabled={busy}>{busy ? "Creating..." : "Create event"}</button>
</div>
{error && <p className="q-error" role="alert">{error}</p>}
</form>
);
}
export function YourEvents() {
const path = usePath();
const [mine, setMine] = useState<Mine[]>(listMine);
useEffect(() => setMine(listMine()), [path]);
if (!mine.length) return null;
return (
<section className="section-tight your-events">
<div className="wrap">
<h2 className="h2">Your events</h2>
<p className="muted small" style={{ marginTop: 8 }}>Events you made or opened with an admin link in this browser.</p>
<ul className="mine-list">
{mine.map((m) => (
<li key={m.id}>
<a className="mine-title" href={`/e/${m.id}/admin`} data-nav>{m.title || "Untitled event"}<span className="muted"> · {formatCode(m.code || "")}</span></a>
<span className="row mine-actions">
<a className="btn btn-sm btn-line" href={`/e/${m.id}/admin`} data-nav>Open</a>
<a className="btn btn-sm btn-quiet" href={`/e/${m.id}/present`} target="_blank" rel="noopener">Present</a>
<button className="btn btn-sm btn-quiet" type="button"
onClick={() => { if (confirm(`Remove "${m.title}" from this list? The event keeps working. You will need its admin link to run it again.`)) { forget(m.id); setMine(listMine()); } }}>
Remove
</button>
</span>
</li>
))}
</ul>
</div>
</section>
);
}
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
Bounded source sync
Bounded source sync