forms-free.openapps.xyz

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.
Owned app · rev ce303909347f
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.
← All files
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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[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
ce30390934 · 4 hours ago
Bounded source sync
7d82add8ae · 1 day ago
Bounded source sync
8230fd7d59 · 1 day ago
Bounded source sync
d92a6e3b44 · 1 day ago
Bounded source sync
b548641922 · 1 day ago
Bounded source sync
b89074ffd2 · 1 day ago