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 cf614aedcf4cdabfa9c45095
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/Owner.tsx
// The owner area: /f/<id>/results (summary), /f/<id>/responses, /f/<id>/edit and /f/<id>/share. Opening any of them
// with #k=<admin key> makes this browser an owner (the admin link works on any device).
import { useEffect, useMemo, useState } from "react";
import { get, set, setMany, useQuery, serverTimestamp } from "@bounded-sh/client";
import { me, explain } from "./lib/bounded";
import { keyFor, remember } from "./lib/mine";
import { navigate } from "./lib/router";
import { toast, copy, confirmAction, Menu } from "./ui";
import { Missing, Failed } from "./Respond";
import { EditForm, createForm } from "./Builder";
import { Summary, Responses } from "./Results";
import { Share } from "./Share";
import { SITE_URL } from "../functions/lib/config";
import { parseQuestions, type FormDoc, type ResponseDoc } from "../functions/lib/forms";
export type Tab = "results" | "responses" | "edit" | "share";
// Reads and live updates return at most 1,000 rows. Below that the live set is complete;
// past it, each change reloads every response page by page.
const PAGE = 1000;
function useResponses(formId: string): ResponseDoc[] | undefined {
const path = `forms/${formId}/responses`;
const { data: live } = useQuery(path, { limit: PAGE }) as { data: ResponseDoc[] | undefined };
const [all, setAll] = useState<ResponseDoc[]>();
useEffect(() => {
if (!live) return;
if (live.length < PAGE) { setAll(live); return; }
let cancelled = false;
(async () => {
const rows: ResponseDoc[] = [];
let cursor: string | undefined;
do {
const res = (await get(path, { limit: PAGE, ...(cursor ? { cursor } : {}) })) as { data: ResponseDoc[]; nextCursor?: string | null };
rows.push(...res.data);
cursor = res.nextCursor || undefined;
} while (cursor);
if (!cancelled) setAll(rows);
})().catch((e) => {
if (cancelled) return;
toast(`Showing the latest ${PAGE.toLocaleString()} responses. ${explain(e)}`);
setAll(live);
});
return () => { cancelled = true; };
}, [path, live]);
return all;
}
function OwnerSkeleton() {
return (
<div className="owner" aria-busy="true" aria-label="Opening your form">
<header className="owner-head">
<div className="wrap owner-head-inner">
<div className="owner-title-block">
<span className="skeleton skeleton-text" style={{ width: 110 }} />
<span className="skeleton" style={{ height: 40, width: "min(460px, 78vw)", marginTop: 10 }} />
</div>
</div>
<div className="wrap owner-tabs"><span className="skeleton skeleton-text" style={{ width: 260, margin: "16px 0" }} /></div>
</header>
<div className="wrap owner-body"><span className="skeleton" style={{ height: 180, maxWidth: 760, borderRadius: 18 }} /></div>
</div>
);
}
export function Owner({ formId, tab }: { formId: string; tab: Tab }) {
const [state, setState] = useState<"checking" | "ready" | "denied" | "badkey" | "missing" | "error">("checking");
const [error, setError] = useState("");
// Pasting the admin link into a tab already on this page only changes the hash: check again.
const [attempt, setAttempt] = useState(0);
useEffect(() => {
const onHash = () => { if (/(^|[#&])k=/.test(location.hash)) { setState("checking"); setAttempt((n) => n + 1); } };
window.addEventListener("hashchange", onHash);
return () => window.removeEventListener("hashchange", onHash);
}, []);
useEffect(() => {
let cancelled = false;
const done = (s: typeof state) => { if (!cancelled) setState(s); };
(async () => {
try {
const uid = await me();
const linkKey = new URLSearchParams(location.hash.slice(1)).get("k");
// The key never stays in the address bar; this browser keeps it in "Your forms".
if (linkKey) history.replaceState(null, "", location.pathname + location.search);
if (!(await get(`forms/${formId}/joins/${uid}`))) {
try { await set(`forms/${formId}/joins/${uid}`, { at: serverTimestamp() }); } catch { return done("missing"); }
}
const form = (await get(`forms/${formId}`)) as FormDoc | null;
if (!form) return done("missing");
const key = linkKey || keyFor(formId);
let owner = Boolean(await get(`forms/${formId}/owners/${uid}`));
if (!owner && key) {
try {
await setMany([
{ path: `formclaims/${formId}/c/${uid}`, document: { key } },
{ path: `forms/${formId}/owners/${uid}`, document: { at: serverTimestamp() } },
]);
owner = true;
} catch {
return done(linkKey ? "badkey" : "denied");
}
}
if (owner && key) remember(formId, key, form.title);
done(owner ? "ready" : "denied");
} catch (err) {
if (!cancelled) { setError(explain(err)); setState("error"); }
}
})();
return () => { cancelled = true; };
}, [formId, attempt]);
if (state === "checking") return <OwnerSkeleton />;
if (state === "missing") return <Missing />;
if (state === "error") return <Failed message={error} retry={() => { setState("checking"); setAttempt((n) => n + 1); }} />;
if (state === "denied" || state === "badkey") return (
<div className="wrap page-pad narrow">
<div className="empty-state enter">
<h1>{state === "badkey" ? "That admin link is not right" : "Only the form's owner can see this"}</h1>
<p>
{state === "badkey"
? "The key at the end of the link does not match this form. Copy the whole admin link again."
: "To see responses or edit this form on this device, open the admin link you saved when the form was made."}
</p>
<a className="btn" href={`/f/${formId}`} data-nav>Open the form</a>
</div>
</div>
);
return <OwnerView formId={formId} tab={tab} />;
}
function OwnerView({ formId, tab }: { formId: string; tab: Tab }) {
const { data: form } = useQuery(`forms/${formId}`) as { data: FormDoc | null | undefined };
const rows = useResponses(formId);
const questions = useMemo(() => parseQuestions(form?.questions), [form?.questions]);
const [busy, setBusy] = useState(false);
useEffect(() => { if (form?.title) document.title = `${form.title} | Forms`; }, [form?.title]);
if (form === undefined) return <OwnerSkeleton />;
if (!form) return <Missing />;
const toggleClosed = async () => {
if (!form.closed && !(await confirmAction({
title: "Close this form?",
body: "People who open the link will see that it is closed and cannot send responses. You can reopen it any time.",
action: "Close form",
}))) return;
setBusy(true);
try {
await set(`forms/${formId}`, { closed: !form.closed });
toast(form.closed ? "Form reopened. It takes responses again." : "Form closed. It no longer takes responses.");
} catch (e) {
toast(explain(e));
}
setBusy(false);
};
const duplicate = async () => {
setBusy(true);
try {
const id = await createForm({ title: `${form.title} (copy)`.slice(0, 200), description: form.description || "", thankYou: form.thankYou || "", questions });
toast("Copy made. It has no responses yet.");
navigate(`/f/${id}/edit`);
} catch (e) {
toast(explain(e));
setBusy(false);
}
};
const deleteAll = async () => {
const list = rows || [];
if (!list.length || !(await confirmAction({
title: "Delete all responses?",
body: `This deletes ${list.length === 1 ? "the only response" : `all ${list.length.toLocaleString()} responses`} to this form, for every owner. It cannot be undone. Download a CSV first if you might need them.`,
action: "Delete all",
danger: true,
}))) return;
setBusy(true);
if (list.length > 50) toast(`Deleting ${list.length.toLocaleString()} responses. Keep this page open.`);
try {
for (let i = 0; i < list.length; i += 50) {
await setMany(list.slice(i, i + 50).map((r) => ({ path: `forms/${formId}/responses/${r.id}`, document: null })));
}
toast(`Deleted ${list.length.toLocaleString()} ${list.length === 1 ? "response" : "responses"}`);
} catch (e) {
toast(explain(e));
}
setBusy(false);
};
const count = rows?.length;
const tabs: [Tab, string, number?][] = [["results", "Summary"], ["responses", "Responses", count], ["edit", "Edit"], ["share", "Share"]];
return (
<div className="owner">
<header className="owner-head">
<div className="wrap owner-head-inner">
<div className="owner-title-block">
<p className="eyebrow">Your form · <span className={form.closed ? "status-closed" : "status-open"}>{form.closed ? "Closed" : "Open"}</span></p>
<h1 className="owner-title">{form.title}</h1>
</div>
<div className="row owner-actions">
<a className="btn btn-line" href={`/f/${formId}`} target="_blank" rel="noopener">
View form
<svg width="14" height="14" viewBox="0 0 20 20" aria-hidden="true"><path d="M8 5H5v10h10v-3M11 4h5v5M16 4l-7 7" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" /></svg>
</a>
<Menu label="More actions">
<button type="button" role="menuitem" onClick={() => copy(`${SITE_URL}/f/${formId}`, "Form link copied")}>Copy form link</button>
<button type="button" role="menuitem" onClick={toggleClosed} disabled={busy}>{form.closed ? "Reopen form" : "Close form"}</button>
<button type="button" role="menuitem" onClick={duplicate} disabled={busy}>Duplicate form</button>
<hr />
<button type="button" role="menuitem" className="danger" onClick={deleteAll} disabled={busy || !count}>Delete all responses</button>
</Menu>
</div>
</div>
<nav className="wrap owner-tabs" aria-label="Form">
{tabs.map(([t, label, n]) => (
<a key={t} href={`/f/${formId}/${t}`} data-nav aria-current={t === tab ? "page" : undefined}>
{label}{n !== undefined && <span className="tab-count">{n.toLocaleString()}</span>}
</a>
))}
</nav>
</header>
<div className="wrap owner-body enter" key={tab}>
{form.closed && tab !== "edit" && (
<p className="notice closed-note">This form is closed: people see that it no longer takes responses. <button className="link-btn" type="button" onClick={toggleClosed} disabled={busy}>Reopen it</button></p>
)}
{tab === "results" && <Summary formId={formId} form={form} questions={questions} rows={rows} />}
{tab === "responses" && <Responses formId={formId} form={form} questions={questions} rows={rows} />}
{tab === "edit" && (
<EditForm key={formId} formId={formId}
initial={{ title: form.title, description: form.description || "", thankYou: form.thankYou || "", questions }} />
)}
{tab === "share" && <Share formId={formId} form={form} questions={questions} />}
</div>
</div>
);
}
Change history
Quality pass: conditional questions, 0 to 10 scale, QR code, prefill links, results tabs, API docs
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync