English

Try it live


The itch

After a week of AP-prep I had:

  • a cluttered .docx per test,
  • manual tallying that broke my focus,
  • and zero longitudinal data.

I needed a grader that felt like typing in a terminal—not clicking a Scantron on a tablet. Nothing on the web hit that vibe, so I opened a blank .html and started typing.


Process, in three tight loops

  1. Painstorm → prototype (15 min)
    Hard requirement: must work with one hand on the keyboard.
    A bare table, a couple of arrays, and window.onkeydown got me input + live score in under an hour.

  2. Refine → refactor (hour sprint)
    Undo/redo, dark mode, and CSV export all came from “wouldn’t it be nice?” moments during my own practice sets.
    Each feature stayed under 50 loc—otherwise it was cut.

  3. Ship (same night)
    Committed to Git, pushed to Vercel, lighthouse auditing.

    Vercel deploy overview

Why Vercel?

  • Zero-config static hosting — drag-and-drop or push to main.
  • Instant preview URLs — others could hammer new builds without me screen-sharing.
  • Custom domain later — nice-to-have, not a blocker.

The entire app is one HTML file; Vercel serves it straight from /public with compression out of the box. Deploy time hovers around two seconds, which is shorter than my unit-test suite (i.e., nonexistent).

Vercel deploy overview

Build sheet

ChoiceReason
Vanilla JSUnder 5 kB gzipped; no bundler, no npm migraine.
CSS custom propsDay/night theme = one class toggle.
localStorageSurvives refresh, works offline, no quota drama at this size.
Blob + URL.createObjectURLClient-side CSV download—no server, no GDPR headaches.
Pure keyboard UXMuscle memory > mouse travel. Ctrl Z/Y, Backspace, M and T for mode/dark.

Relevant XKCD

https://xkcd.com/1205/

Vercel deploy overview

Key code snippets

// 1. Global constants
const STORAGE = 'quickMcGraderState';
const CHOICES = 6; // answers A-F

// 2. Keyboard dispatcher
window.onkeydown = (e) => {
  if (e.ctrlKey || e.altKey || e.metaKey) return;
  if (/^[1-6]$/.test(e.key)) addChoice(letter(+e.key)); // numeric
  if (/^[a-f]$/i.test(e.key)) addChoice(e.key.toUpperCase());
  if (e.key === 'm' || e.key === 'M') toggleMode();
  if (e.key === 't' || e.key === 'T') toggleDark();
};

// 3. CSV export
function exportCSV() {
  const blob = new Blob([csvString()], { type: 'text/csv' });
  const url = URL.createObjectURL(blob);
  Object.assign(document.createElement('a'), {
    href: url,
    download: `mc-grader-${timestamp()}.csv`,
  }).click();
  URL.revokeObjectURL(url);
}