sign-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 528326727848
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 1da91c033c738ab74538d13d
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 start box in the landing hero: choose or drop a PDF, or write text; and "Your documents" for returning owners.
// It stays light: the data client and the PDF library load on first use.
import { useEffect, useRef, useState, type DragEvent } from "react";
import { navigate, usePath } from "./lib/router";
import { myDocs } from "./lib/mydocs";
import type { ListStatus } from "./lib/actions";

const loadActions = () => import("./lib/actions");
const loadWrite = () => import("./Write");
const SHOWN = 5;

export function StartDocument() {
  const [step, setStep] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);
  const [over, setOver] = useState(false);
  const input = useRef<HTMLInputElement>(null);

  const upload = async (file: File | undefined) => {
    if (!file || step) return;
    setError(null);
    setStep("Opening the PDF");
    try {
      const { startFromPdf } = await loadActions();
      navigate(`/d/${await startFromPdf(file, setStep)}`);
    } catch (e) {
      setError(e instanceof Error ? e.message : String(e));
      setStep(null);
    }
  };
  const drop = (e: DragEvent) => {
    e.preventDefault();
    setOver(false);
    upload(e.dataTransfer.files?.[0]);
  };
  const warm = () => { loadActions(); };

  return (
    <div className="start">
      <div className={`drop card${over ? " over" : ""}`} onPointerEnter={warm} onFocus={warm} onTouchStart={warm}
        onDragEnter={warm} onDragOver={(e) => { e.preventDefault(); setOver(true); }} onDragLeave={() => setOver(false)} onDrop={drop}>
        <img className="art drop-art" src="/art/card-sign.webp" alt="" width="150" height="125" />
        <p className="drop-title"><span className="on-fine">Drop a PDF here</span><span className="on-coarse">Add the document to sign</span></p>
        <div className="drop-actions">
          <button className="btn btn-lg" type="button" onClick={() => input.current?.click()} aria-busy={step ? true : undefined}>Choose a PDF</button>
          <button className="btn btn-lg btn-line" type="button" onPointerEnter={() => loadWrite()} onClick={() => navigate("/new")} disabled={Boolean(step)}>Write or paste text</button>
        </div>
        <input ref={input} type="file" accept="application/pdf,.pdf" hidden onChange={(e) => { upload(e.target.files?.[0]); e.target.value = ""; }} />
        <p className="hint" role="status">{step ? `${step}...` : "Free. No sign-up. PDFs up to 10 MB."}</p>
        {error && <p className="notice notice-error enter" role="alert">{error}</p>}
      </div>
      <YourDocuments />
    </div>
  );
}

/** Only on the landing: document pages hide the hero, and should not spend requests on this list. */
function YourDocuments() {
  return usePath() === "/" ? <DocList /> : null;
}

function DocList() {
  const docs = myDocs();
  const [all, setAll] = useState(false);
  const [status, setStatus] = useState<Record<string, ListStatus | "loading">>({});
  const shown = all ? docs : docs.slice(0, SHOWN);
  const ids = shown.map((d) => d.id).join(",");

  useEffect(() => {
    const want = shown.filter((d) => !(d.id in status)).map((d) => d.id);
    if (!want.length) return;
    setStatus((s) => ({ ...s, ...Object.fromEntries(want.map((id) => [id, "loading" as const])) }));
    loadActions().then(({ docStatus }) => {
      // Four at a time, so a long list does not flood the connection.
      const worker = async () => {
        for (let id = want.shift(); id; id = want.shift()) {
          const st = await docStatus(id).catch(() => null);
          setStatus((s) => ({ ...s, [id]: st }));
        }
      };
      for (let i = 0; i < 4; i++) worker();
    }).catch(() => setStatus((s) => ({ ...s, ...Object.fromEntries(want.map((id) => [id, null])) })));
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [ids]);

  if (!docs.length) return null;
  return (
    <section className="your-docs" aria-labelledby="your-docs-title">
      <h2 className="eyebrow" id="your-docs-title">Your documents</h2>
      <ul>
        {shown.map((d, i) => {
          const st = status[d.id];
          return (
            <li key={d.id} className="enter" style={{ ["--i" as string]: i }}>
              <a href={`/d/${d.id}`} onClick={(e) => { e.preventDefault(); navigate(`/d/${d.id}`); }}>
                <span className="doc-name">{d.title}</span>
                <span className="doc-date">{new Date(d.at).toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" })}</span>
              </a>
              {st === "loading" || st === undefined ? <span className="skeleton doc-chip-skeleton" aria-hidden="true" />
                : st ? <span className={`chip tone-${st.tone}`}>{st.label}</span> : null}
            </li>
          );
        })}
      </ul>
      {docs.length > SHOWN && <button type="button" className="btn btn-quiet btn-sm" onClick={() => setAll(!all)}>{all ? "Show fewer" : `Show all ${docs.length}`}</button>}
      <p className="hint">Kept in this browser. To open a document elsewhere, copy its admin link from the document's menu.</p>
    </section>
  );
}

Change history

Quality pass: phone signing, edit signers, copy to send again
5283267278 · 5 hours ago
Bounded source sync
15f0fa2e27 · 1 day ago
Bounded source sync
d3aa1ca65b · 1 day ago
Bounded source sync
8e2ca0b773 · 1 day ago
Bounded source sync
5cfa84715c · 1 day ago
Bounded source sync
c849e7b286 · 1 day ago
Bounded source sync
68c1cabe03 · 1 day ago
Bounded source sync
62ae8944f0 · 1 day ago
Bounded source sync
7ed54e7381 · 1 day ago