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 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.
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
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync
Bounded source sync