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/Share.tsx
// Sharing a form: its public link, an HTML form for any website (posts straight to Forms), the endpoint
// for code, and the private admin link.
import { useState } from "react";
import { copy, share } from "./ui";
import { Qr } from "./Qr";
import { qrMatrix, qrPath } from "./lib/qr";
import { keyFor } from "./lib/mine";
import { SITE_URL, API_URL } from "../functions/lib/config";
import type { FormDoc, Question } from "../functions/lib/forms";
const esc = (s: string) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]!);
/** A plain HTML form with the same questions, posting to the endpoint. Field names are question ids, so later label edits do not break it. */
function htmlSnippet(endpoint: string, questions: Question[]): string {
// A question that other answers can hide is never required here: the HTML cannot hide it.
const req = (q: Question) => (q.required && !q.showIf ? " required" : "");
const fields = questions.length ? questions.map((q) => {
const label = esc(q.label);
const name = q.id;
const opts = (q.options || []).map(esc);
switch (q.type) {
case "long_text": return ` <label>${label}<br>\n <textarea name="${name}" rows="4"${req(q)}></textarea>\n </label>`;
case "email": return ` <label>${label}<br>\n <input type="email" name="${name}"${req(q)}>\n </label>`;
case "number": return ` <label>${label}<br>\n <input type="number" step="any" name="${name}"${req(q)}>\n </label>`;
case "date": return ` <label>${label}<br>\n <input type="date" name="${name}"${req(q)}>\n </label>`;
case "dropdown": return ` <label>${label}<br>\n <select name="${name}"${req(q)}>\n <option value="">Choose</option>\n${opts.map((o) => ` <option>${o}</option>`).join("\n")}\n </select>\n </label>`;
case "choice": return ` <fieldset>\n <legend>${label}</legend>\n${opts.map((o) => ` <label><input type="radio" name="${name}" value="${o}"${req(q)}> ${o}</label>`).join("\n")}\n </fieldset>`;
case "checkboxes": return ` <fieldset>\n <legend>${label}</legend>\n${opts.map((o) => ` <label><input type="checkbox" name="${name}" value="${o}"> ${o}</label>`).join("\n")}\n </fieldset>`;
case "rating":
case "scale": {
const nums = q.type === "rating" ? [1, 2, 3, 4, 5] : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
return ` <fieldset>\n <legend>${label}</legend>\n${nums.map((n) => ` <label><input type="radio" name="${name}" value="${n}"${req(q)}> ${n}</label>`).join("\n")}\n </fieldset>`;
}
case "yes_no": return ` <fieldset>\n <legend>${label}</legend>\n <label><input type="radio" name="${name}" value="yes"${req(q)}> Yes</label>\n <label><input type="radio" name="${name}" value="no"> No</label>\n </fieldset>`;
default: return ` <label>${label}<br>\n <input type="text" name="${name}"${req(q)}>\n </label>`;
}
}) : [
` <label>Name<br>\n <input type="text" name="name" required>\n </label>`,
` <label>Email<br>\n <input type="email" name="email" required>\n </label>`,
` <label>Message<br>\n <textarea name="message" rows="4" required></textarea>\n </label>`,
];
return [
`<form action="${endpoint}" method="POST">`,
...fields,
` <!-- Leave this hidden field empty: bots fill it in and get ignored. -->`,
` <input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off">`,
` <button type="submit">Send</button>`,
`</form>`,
].join("\n");
}
function CodeBox({ code, label }: { code: string; label: string }) {
return (
<div className="codebox">
<div className="codebox-bar">
<span>{label}</span>
<button className="btn btn-sm btn-line" type="button" onClick={() => copy(code, `${label} copied`)}>Copy</button>
</div>
<pre className="code" tabIndex={0} aria-label={`${label} code`}>{code}</pre>
</div>
);
}
/** Saves the QR code as a PNG, which every document and print tool takes. */
function downloadQr(text: string, name: string) {
const { d, size } = qrPath(qrMatrix(text));
const scale = 16;
const canvas = document.createElement("canvas");
canvas.width = canvas.height = size * scale;
const g = canvas.getContext("2d")!;
g.fillStyle = "#fff";
g.fillRect(0, 0, canvas.width, canvas.height);
g.scale(scale, scale);
g.fillStyle = "#0b0b0c";
g.fill(new Path2D(d));
canvas.toBlob((blob) => {
if (!blob) return;
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `${name.replace(/[^\w-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "form"}-qr.png`;
a.click();
URL.revokeObjectURL(a.href);
}, "image/png");
}
export function Share({ formId, form, questions }: { formId: string; form: FormDoc; questions: Question[] }) {
const url = `${SITE_URL}/f/${formId}`;
const endpoint = `${API_URL}/forms/${formId}/responses`;
const key = keyFor(formId);
const adminUrl = key ? `${SITE_URL}/f/${formId}/results#k=${key}` : null;
const [isNew, setIsNew] = useState(() => localStorage.getItem(`forms:new:${formId}`) === "1");
const [showKey, setShowKey] = useState(false);
const example = questions.length
? JSON.stringify({ answers: Object.fromEntries(questions.slice(0, 2).map((q) => [q.label, q.type === "rating" ? 5 : q.type === "scale" ? 9 : q.type === "yes_no" ? "yes" : q.options?.[0] ?? "..."])) })
: JSON.stringify({ answers: { name: "Sam", email: "sam@example.com", message: "Hello" } });
const pick = questions.find((q) => q.type === "choice" || q.type === "dropdown") || questions.find((q) => q.type === "short_text");
const prefillExample = pick && `?${new URLSearchParams({ [pick.label]: pick.options?.[0] ?? "Sam" })}`;
return (
<div className="share">
{isNew && (
<div className="notice notice-ok invite enter">
<span><strong>Your form is live.</strong> Send people the link. Save your admin link below: it is the only way to manage this form from another browser.</span>
<button className="btn btn-sm btn-quiet" type="button" onClick={() => { localStorage.removeItem(`forms:new:${formId}`); setIsNew(false); }}>Dismiss</button>
</div>
)}
<section className="card share-card">
<h2 className="h3">Form link</h2>
<p className="muted small">Anyone with this link can fill in the form, on any phone or computer. No account needed.</p>
<div className="link-row">
<code className="link-code">{url}</code>
<span className="row link-actions">
<button className="btn btn-sm" type="button" onClick={() => share(url, form.title)}>Copy link</button>
<a className="btn btn-sm btn-line" href={url} target="_blank" rel="noopener">Open</a>
</span>
</div>
<div className="qr-row">
<Qr text={url} size={132} label={`QR code for ${form.title}`} />
<div className="qr-text">
<p className="small">Print it or put it on a slide. A phone camera opens the form.</p>
<button className="btn btn-sm btn-line" type="button" onClick={() => downloadQr(url, form.title)}>Download QR code</button>
</div>
</div>
{prefillExample && <p className="hint">To fill in answers ahead, add them to the end of the link: <code className="inline-code">{prefillExample}</code></p>}
{form.closed && <p className="notice">This form is closed. The link shows that it no longer takes responses.</p>}
</section>
<section className="card share-card share-admin">
<h2 className="h3">Admin link</h2>
<p className="muted small">Opens the results and the editor on any device. Anyone with it can read the responses and change the form, so keep it private and save it somewhere safe.</p>
{adminUrl ? (
<div className="link-row">
<code className="link-code">{showKey ? adminUrl : `${SITE_URL}/f/${formId}/results#k=••••••••`}</code>
<span className="row link-actions">
<button className="btn btn-sm btn-line" type="button" onClick={() => setShowKey((v) => !v)} aria-pressed={showKey}>{showKey ? "Hide" : "Show"}</button>
<button className="btn btn-sm" type="button" onClick={() => copy(adminUrl, "Admin link copied. Keep it private.")}>Copy</button>
</span>
</div>
) : <p className="notice">This browser does not have the admin link stored. Use the one you saved when you made the form.</p>}
</section>
<section className="card share-card">
<h2 className="h3">Put it on your website</h2>
<p className="muted small">Paste this HTML into any page. It posts straight to Forms: no script, no backend. People see a short thank-you page with a link back to your site. Style it like the rest of your page.</p>
<CodeBox code={htmlSnippet(endpoint, questions)} label="HTML" />
{!questions.length && <p className="hint">This form has no questions, so it keeps any fields your site sends (up to 50).</p>}
{questions.some((q) => q.showIf) && <p className="hint">This HTML shows every question. Answers to questions that the other answers hide are not saved.</p>}
</section>
<section className="card share-card">
<h2 className="h3">Send responses from code</h2>
<p className="muted small">POST JSON to the endpoint. Use question labels or ids as keys. CORS is open, so this works from a browser too.</p>
<div className="link-row">
<code className="link-code">{endpoint}</code>
<button className="btn btn-sm btn-line" type="button" onClick={() => copy(endpoint, "Endpoint copied")}>Copy</button>
</div>
<CodeBox label="JavaScript" code={`fetch("${endpoint}", {\n method: "POST",\n headers: { "content-type": "application/json" },\n body: JSON.stringify(${example})\n})`} />
</section>
</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