SocialCards
A FIFA-style player card generated from your own platform data — parsed in a Web Worker on your machine, with the zero-server guarantee enforced by CI rather than promised in a README.
Overview
SocialCards turns a data export into a football-card-style rating card: GitHub, Spotify, Discord, Instagram, Claude, and Messages each get an adapter that reads the platform's real export format and scores it. Every stat on a card traces back to something in the file you uploaded. Nothing is fabricated, and for every upload-based platform, nothing is transmitted.
Context
This began as a fork of GitFut (MIT licensed, original notice preserved), which scores a single platform. The work here was extracting its scoring philosophy into a platform-agnostic engine — a shared card model, formula primitives, and a theme contract — and then proving the abstraction by adding five more platforms to it, each with a genuinely different source format.
Adapter pipeline
Export file
Validate
Parse
Metrics
Stats
Overall
Card
Every adapter implements the same six-step shape. The renderer never branches on platform — it branches on the CardTheme an adapter hands it, which is why adding a platform doesn't touch rendering code.
Decisions
Zero-server is a CI gate, not a convention
The build log is parsed for Next.js's dynamic-route marker; if any route stops being static, the workflow fails with an explanatory error. The architectural property is enforced on every push rather than trusted to reviewer memory.
A note in the README, which decays the first time someone adds a convenient API route.
Query parameters instead of dynamic route segments
Gallery detail, comparison, and share views all take
?id=or?d=rather than/[id]. A dynamic segment would turn each into a serverless function and break the guarantee above. Sharing state through the URL keeps every route in the static table.Parsing in a Web Worker, not on the main thread
A Spotify extended-streaming-history export is large enough to freeze the tab. Worker parsing keeps the UI responsive and has a useful side effect: the parsing code has no ambient access to the network layer.
Spotify OAuth via Authorization Code + PKCE
PKCE has no client secret, so there is nothing that needs a server to hold it — the browser talks to Spotify directly. Without a configured client id, the OAuth tab degrades to an explanatory message instead of erroring.
A standard authorization-code flow, which needs a secret, which needs a backend, which would have broken the zero-server property for one optional feature.
Date ranges recompute rather than filter
Selecting a range re-runs the calculation over cached parsed data instead of masking pre-computed totals, so a range's stats are the stats for that range. Boundary behaviour — inclusive end-of-day, invalid dates, missing timestamps — has its own test file.
Accessibility pass
Measured rather than eyeballed. Three of these were real bugs, not polish.
hiddenon a file input removes it from the tab order entirely — switched tosr-only, which stays visually hidden but keyboard-reachable- The card exposes one composed
aria-labelviarole="img"instead of making screen readers walk dozens of unlabelled nested elements - Several selects and custom date inputs had
outline-nonewith no replacement indicator; every interactive control now has a visible focus ring - Secondary text was checked against WCAG AA by computing contrast, not by looking: one token measured about 3:1 and was raised to roughly 5.3:1
- At 320–390px, several button rows had no
flex-wrap— no page-level overflow, but buttons shrank and wrapped their own labels internally, which is broken in a way an overflow check doesn't catch
Results
- 6
- 0
- 11
- 0
Lesson
If an architectural property matters, encode it as a failing build. A README promise decays; a CI gate argues back.