canvas-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 93fdaafeda16
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 788ba0fa01083b300ad50913
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/Home.tsx
// The landing page's live parts: starting a board (blank or from a template) and "Your boards".
// Also the /new and /new/<template> pages, which make a board straight away and open it.
import { useEffect, useRef, useState } from "react";
import { forgetBoard, myBoards, type MyBoard } from "./lib/mine";
import { navigate, usePath } from "./lib/router";
import { TEMPLATES, templateById, type TemplateId } from "../functions/lib/canvas";

// Making a board needs the data client, which the landing page loads only once it is idle (or on click).
const boards = () => import("./lib/boards");
const describe = (err: unknown) => import("./lib/bounded").then(({ explain }) => explain(err));
(window.requestIdleCallback ?? ((f: () => void) => setTimeout(f, 1500)))(() => void boards());

async function start(template: TemplateId | null) {
  const t = template ? templateById(template) : undefined;
  return (await boards()).createBoard(t?.title ?? "Untitled board", t ? { template: t.id } : null);
}

export function StartBoard() {
  const [busy, setBusy] = useState<string | null>(null);
  const [error, setError] = useState("");
  const go = async (template: TemplateId | null) => {
    setBusy(template ?? "blank");
    setError("");
    try {
      navigate(await start(template));
    } catch (err) {
      setError(await describe(err));
      setBusy(null);
    }
  };
  return (
    <div className="card start">
      <button type="button" className="btn btn-lg btn-block" disabled={Boolean(busy)} onClick={() => void go(null)}>
        {busy === "blank" ? <><span className="spinner spinner-light" /> Making your board...</> : "Start a whiteboard"}
      </button>
      <p className="hint start-hint">Free, no sign-up. You get a link to share.</p>
      <div className="start-templates" role="group" aria-label="Or start from a template">
        <p className="label">Or start from a template</p>
        <div className="tpl-grid">
          {TEMPLATES.map((t) => (
            <button key={t.id} type="button" className="tpl" disabled={Boolean(busy)} onClick={() => void go(t.id)}>
              <span className="tpl-name">{busy === t.id ? "Making..." : t.name}</span>
              <span className="tpl-sum">{t.summary}</span>
            </button>
          ))}
        </div>
      </div>
      {error && <p className="notice notice-error" role="alert">{error}</p>}
    </div>
  );
}

export function YourBoards() {
  const path = usePath();
  const [boards, setBoards] = useState<MyBoard[]>(myBoards);
  const [all, setAll] = useState(false);
  useEffect(() => setBoards(myBoards()), [path]);
  if (!boards.length) return null;
  const shown = all ? boards : boards.slice(0, 5);
  return (
    <section className="your-boards" aria-label="Your boards">
      <h2 className="label">Your boards</h2>
      <ul>
        {shown.map((b) => {
          const href = b.edit ? `/b/${b.id}#edit=${b.edit}` : `/b/${b.id}`;
          return (
            <li key={b.id}>
              <a href={href} onClick={(e) => { e.preventDefault(); navigate(href); }}>{b.title || "Untitled board"}</a>
              <span className="hint">{b.admin ? "Owner" : b.edit ? "Can edit" : "Can view"}</span>
              <button type="button" className="icon-btn icon-btn-sm" aria-label={`Remove ${b.title || "board"} from this list`} title="Remove from this list"
                onClick={() => {
                  if (!confirm(b.admin ? "Remove this board from the list? It keeps working. You will need its admin link to manage it again." : "Remove this board from the list? It keeps working for everyone with its link.")) return;
                  forgetBoard(b.id);
                  setBoards(myBoards());
                }}>
                <svg width="14" height="14" viewBox="0 0 20 20" aria-hidden="true"><path d="M5 5l10 10M15 5 5 15" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" /></svg>
              </button>
            </li>
          );
        })}
      </ul>
      {boards.length > shown.length && <button className="link-btn" type="button" onClick={() => setAll(true)}>Show all {boards.length}</button>}
      <p className="hint">Listed on this device only.</p>
    </section>
  );
}

/** /new and /new/<template>: makes the board and replaces this page with it, so Back does not make another. */
export function NewBoard({ template }: { template: string | null }) {
  const [error, setError] = useState("");
  const started = useRef(false);
  useEffect(() => {
    if (started.current) return;
    started.current = true;
    const t = template ? templateById(template) : undefined;
    if (template && !t) { setError("There is no template with that name."); return; }
    start(t?.id ?? null).then((to) => {
      history.replaceState(null, "", to);
      window.dispatchEvent(new PopStateEvent("popstate"));
    }).catch(async (err) => setError(await describe(err)));
  }, [template]);
  if (error) return (
    <div className="wrap page-pad narrow">
      <p className="notice notice-error">{error}</p>
      <a className="btn" style={{ marginTop: 20 }} href="/" onClick={(e) => { e.preventDefault(); navigate("/"); }}>Back to Canvas</a>
    </div>
  );
  return <div className="board-loading"><span className="spinner" /> Making your board...</div>;
}

Change history

Point OpenApps links to openapps.free
93fdaafeda · 18 hours ago
Bind AGENT_ADDRESS in policy constants
bf8f43b096 · 20 hours ago
Use functions.openapps.xyz alias, add OpenApp contribution strip
56f1a381b2 · 20 hours ago
Bounded source sync
9ec707aeb8 · 1 day ago
Bounded source sync
5645481c23 · 1 day ago
Bounded source sync
c38fe71874 · 1 day ago
Bounded source sync
28c0350b40 · 1 day ago