resume-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 3f16b2375723
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.
← All files
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
3f16b23757 · 5 hours ago
Bounded source sync
b7eb5b26b5 · 1 day ago
Bounded source sync
adae97536e · 1 day ago
Bounded source sync
3b0cf36b39 · 1 day ago