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
-
Painstorm → prototype (15 min)
Hard requirement: must work with one hand on the keyboard.
A bare table, a couple of arrays, andwindow.onkeydown
got me input + live score in under an hour. -
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. -
Ship (same night)
Committed to Git, pushed to Vercel, lighthouse auditing.
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).

Build sheet
Choice | Reason |
---|---|
Vanilla JS | Under 5 kB gzipped; no bundler, no npm migraine. |
CSS custom props | Day/night theme = one class toggle. |
localStorage | Survives refresh, works offline, no quota drama at this size. |
Blob + URL.createObjectURL | Client-side CSV download—no server, no GDPR headaches. |
Pure keyboard UX | Muscle memory > mouse travel. Ctrl Z/Y , Backspace , M and T for mode/dark. |
Relevant XKCD

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);
}