Work · case study

Gamer Career

A library of every game I’ve played, curated by status and score, where each way of looking at it gets its own chart.

Year
2026
Role
Design and build
Stack
React, TypeScript, Mantine, Recharts
AI
Assisted, heavily
Status
Live
Links
The All Games timeline, over 500 games by release year and public score

OutcomeA sample library of over 500 games, nine play statuses, a chart for every view, and 93 tests.

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.

A scatter of my score versus the public score for completed games, with a dashed parity line
Dots above the dashed line are games I rate higher than the crowd does.

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.

The games view grouped by original and searched for Minecraft: one group of five editions, four of them marked as variants
Each edition keeps its playthrough; the Unique count sees one game.

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.

The Bulk Add staging table: seven pasted rows, three flagged as already in the library or repeated in the batch, and four ticked to add
Nothing reaches the library until the batch has been reviewed.

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.