polls-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 c06cea4889e9
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.
← All files
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
c06cea4889 · 17 hours ago
Bind agent address in policy constants
a7de116d52 · 19 hours ago
Use OpenApps service URLs and add contribution strip
e8ce0b0bf0 · 19 hours ago
Bounded source sync
dcdc4d56e6 · 1 day ago
Bounded source sync
7acdace1e1 · 1 day ago
Bounded source sync
2140a66ea0 · 1 day ago
Bounded source sync
0db2fe3ca0 · 1 day ago
Bounded source sync
996f633a3c · 1 day ago