Problem
My list of games had outgrown being a list. It could tell me whether I’d played something, but not what the whole hobby looked like: what sits in the backlog, which games I rate above the crowd, what I keep walking away from. The tracking sites didn’t fit either. I wanted room for a game I’m done with but never finished, one that runs in the background, and the same game bought again on a third platform. I wanted my own record of all of it, shaped the way I think about play, and a dataset worth doing real data visualization on. That list had never been written down. I’d kept it in my head since 1986, and its first written form was four platform lists, typed from memory to seed this app.
Constraints
- Local-first. No account and no server database. The library lives in the browser it was entered in, and a JSON export is the backup.
- Keys stay on the server. The Xbox and IGDB credentials are secrets on one Cloudflare Worker, and the browser calls same-origin proxies that never hand them back.
- A public URL, private data. A visitor gets an empty app and the option to load a sample library. Nothing they enter leaves their browser.
- Curated after the fact. Hundreds of games were logged long after they were played, so any feature that needs tracking as you play wouldn’t hold. Start and end dates shipped, then came out for exactly that reason.
Design decisions
A chart for every view
Each status raises its own question, so the games view swaps its hero chart to answer it. All Games is a timeline of release date against public score, colored by status. Backlog, In Play, Paused and Wishlist break down by platform or genre. Completed and Done With plot my score against the public score, with a toggle to a histogram. Abandoned shows the dislikes that drove me away, and Favorites is a genre treemap. Every card on the dashboard links into the view with its filter and chart already set.
Passed on · one stats page that charts everything at once.

Variants and collections are different things
A remaster or a port is the same game again. A compilation bundles different games. Each gets its own link: a variant points at its original, and a collection’s members point at the bundle and can be left out of the stats, so The Ezio Collection doesn’t count Assassin’s Creed II twice. Every playthrough still counts, but variants fold onto their original for the Unique count and surface as Repeats, and “Group by original” clusters them: Minecraft, plus four repeats.
Passed on · guessing repeats by matching titles.

Imports land in a table you review
Bulk Add takes a pasted or uploaded CSV or JSON list, and Xbox sync hands its title history to the same staging table. Every row is checked for duplicates against the library and within the batch, on the current title and the title it was first imported under, so a re-sync after metadata renamed a game doesn’t create a twin. Platforms are ignored because they drift. Fetched metadata applies itself only when the normalized titles match; a loose match gets a “check match” flag instead, because Street Fighter 6 is not the 1987 Street Fighter. Records hidden as junk stay in the database, so the next sync still skips them.
Passed on · writing an import straight into the library.

Build notes
React 18, Vite and TypeScript, with Mantine components, CSS Modules and Recharts. The library lives in IndexedDB through Dexie. RAWG fills in covers, genres and release dates with a key kept on each device, and Wikipedia’s open API finds each game’s reference article. One Cloudflare Worker serves the static build and two stateless proxies: Xbox title history through OpenXBL, and public scores from IGDB through a Twitch app token. Routes load lazily, so the charting library arrives only on the games views, and a push to main deploys the site.
A Vitest suite of 93 tests across 18 files, with fake-indexeddb standing in for the browser’s database, covers the stats, bulk edit, import dedupe, labels, backups, the API clients and both proxies. The ones I lean on most guard the import: a game renamed by fetched metadata must still match its original title, or every Xbox re-sync would quietly make a twin.
Outcome
Live at gamer-career.nineafterten.com. It starts empty, and Load sample library brings in my own library of over 500 games, so anyone can look around without typing in a thing. I learned to design for how people actually keep records: after the fact, in bulk, and messily. Every feature that assumed otherwise got cut.
What’s next
A merge tool for records that are already duplicated, CSV export, and an Xbox sync that runs daily on its own. Sync across devices and a Nintendo Switch import are further off: one needs a backend, the other an API Nintendo doesn’t offer.
