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 70bace54357d4b57b674108f
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/store.ts
// What Resume remembers on this device: the resumes you are working on (each with its cover letter and the
// link it is shared at, if any), which one is open, and the resumes you shared from here with their admin keys.
// Nothing here leaves the browser.
import { blankResume, randomId, toJsonResume, LISTS, SECTIONS, type PaperSize, type Resume } from "../functions/lib/resume";
export interface Mine { id: string; key: string; name: string; label: string; at: number }
/** The shared copy of a draft: savedAt is the updatedAt the server stored for this device's last save,
* hash the fingerprint of the resume it holds (links shared before fingerprints have none). */
export interface Linked { id: string; key: string; savedAt: number; hash?: string }
/** A cover letter: it prints under the resume's own header, in the same template. */
export interface Letter { to: string; body: string }
/** A resume on this device. title is the person's name for it in the list ("Acme application"). */
export interface Draft { id: string; title: string; resume: Resume; letter: Letter; linked: Linked | null; at: number }
function read<T>(key: string): T | null {
try {
const raw = localStorage.getItem(key);
return raw ? (JSON.parse(raw) as T) : null;
} catch {
return null;
}
}
function write(key: string, value: unknown) {
try {
if (value == null) localStorage.removeItem(key);
else localStorage.setItem(key, JSON.stringify(value));
} catch { /* storage full or blocked: the app still works for this visit */ }
}
/** US-style Letter paper where it is the norm, A4 everywhere else. */
export function localPaper(): PaperSize {
try {
const region = new Intl.Locale(navigator.language).maximize().region || "US";
return ["US", "CA", "MX", "PH", "CL", "CO", "VE", "PR", "GT", "CR", "PA", "DO", "SV", "NI", "HN", "BO"].includes(region) ? "Letter" : "A4";
} catch {
return "Letter";
}
}
export const newResume = () => blankResume(localPaper());
/** Fills anything a saved resume lacks, so an older or partial one still opens. */
export function normalize(d: Partial<Resume> | null): Resume | null {
if (!d || typeof d !== "object" || !d.basics || !d.settings) return null;
const base = newResume();
const out: Resume = { ...base, basics: { ...base.basics, ...d.basics }, settings: { ...base.settings, ...d.settings } };
for (const k of LISTS) if (Array.isArray(d[k])) (out[k] as unknown[]) = d[k] as unknown[];
const order = out.settings.order.filter((k) => SECTIONS.includes(k));
out.settings.order = [...order, ...SECTIONS.filter((k) => !order.includes(k))];
return out;
}
// ---- Drafts: one key per resume, so typing writes only the one being edited ----
const DOC = "resume:doc:";
const blankLetter = (): Letter => ({ to: "", body: "" });
function readDraft(id: string): Draft | null {
const d = read<Partial<Draft>>(DOC + id);
const resume = normalize(d?.resume ?? null);
if (!d || !resume) return null;
return { id, title: d.title ?? "", resume, letter: { ...blankLetter(), ...d.letter }, linked: d.linked ?? null, at: d.at ?? 0 };
}
export function saveDraft(d: Draft) {
write(DOC + d.id, d);
write("resume:current", d.id);
}
export function removeDraft(id: string) {
write(DOC + id, null);
}
export function listDrafts(): Draft[] {
const ids: string[] = [];
try {
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
if (k?.startsWith(DOC)) ids.push(k.slice(DOC.length));
}
} catch { /* storage blocked */ }
return ids.map(readDraft).filter((d): d is Draft => d != null).sort((a, b) => b.at - a.at);
}
export function makeDraft(resume: Resume = newResume(), title = "", letter: Letter = blankLetter()): Draft {
return { id: randomId(10), title, resume, letter, linked: null, at: Date.now() };
}
/** The resume to open: the one open last time, else the newest; a resume from before drafts becomes the first. */
export function openDraft(): Draft {
const current = read<string>("resume:current");
const open = (current && readDraft(current)) || listDrafts()[0];
if (open) return open;
const old = normalize(read<Resume>("resume:draft"));
const d = old ? { ...makeDraft(old), linked: read<Linked>("resume:linked") } : makeDraft();
if (old) saveDraft(d);
return d;
}
/** A short fingerprint of what a link shows, so the editor knows when the draft has moved on.
* It hashes the JSON Resume form, whose key order is fixed, so reopening a draft does not change it. */
export function hashOf(r: Resume): string {
const s = JSON.stringify(toJsonResume(r));
let h = 5381;
for (let i = 0; i < s.length; i++) h = ((h << 5) + h + s.charCodeAt(i)) | 0;
return (h >>> 0).toString(36);
}
/** The name a draft goes by in the list. */
export const draftName = (d: Draft) => d.title.trim() || d.resume.basics.label.trim() || d.resume.basics.name.trim() || "Untitled resume";
// ---- Shared from this device ----
export function loadMine(): Mine[] {
const list = read<Mine[]>("resume:mine");
return Array.isArray(list) ? list : [];
}
export function rememberMine(entry: Mine) {
write("resume:mine", [entry, ...loadMine().filter((m) => m.id !== entry.id)].slice(0, 100));
window.dispatchEvent(new Event("resume:mine"));
}
export function keyFor(id: string): string | null {
return loadMine().find((m) => m.id === id)?.key ?? null;
}
Change history
Quality pass: several resumes, cover letters, phone-readable shared pages
Bounded source sync
Bounded source sync
Bounded source sync