Product Design · Full-Stack Dev · 2026
Product Design · Full-Stack Dev · 2026
The Problem
Collector markets move in real time. Most tools don't.
TCG collectors track card values obsessively, but the existing tools are fragmented — one site per game — visually cluttered, or just raw data tables with no sense of what's actually moving. There's no unified place to see at a glance what surged or crashed over the last 7 days across multiple games.
That's the confidence gap. Knowing a card is up 40% this week tells you to sell. Knowing it's down 30% is a buying signal. Without a clear 7-day read in one place, those calls get made on instinct — or missed entirely.
Role
Solo Designer & Developer
Constraint
$0 budget — free tiers only
Status
Shipped · Live on Vercel
Stack
Next.js 16 · Tailwind v4 · Neon Postgres
Field Discovery
Before writing a line of code, I spent time on the floor at six regional TCG card shows across the Lower Mainland — talking to collectors, sellers, and graders mid-deal.
The core research question: how do you decide whether a card is worth buying right now? Answers converged on three consistent frustrations — blurry card imagery, slow mobile pages on convention Wi-Fi, and the gap between seeing a price move and knowing what to do about it.
Each frustration became a direct design decision in the product.
Insight A · Image Degradation
Sellers were pointing at blurry thumbnails trying to describe foil patterns mid-deal. Collectors couldn't tell a Holofoil from a Reverse Holo. Missing or compressed art erodes trust before a price is even considered.
Design Decision
Sourced high-resolution, unwatermarked scans via open-source community APIs — pokemontcg.io and optcgapi.com — achieving 81–87% HD coverage. A fallback chain handles the remainder: game HD → TCGplayer size-upgraded URL → thumbnail.
Insight B · Trade Floor Velocity
Convention centers run on notoriously congested Wi-Fi. Collectors were mid-deal on their phones — every second waiting on a slow dashboard meant a bad price accepted or a deal lost. Speed was not a nice-to-have.
Design Decision
Prioritized text-first, data-dense components over image-heavy layouts. Pages are server-rendered with Edge caching so critical price data arrives in the first byte — not after a client-side fetch cascade.
Insight C · Passive Friction
Collectors weren't just looking up prices — they were watching for the right moment to buy. But watching meant checking manually, again and again. "I missed it" was a phrase that came up at every show.
Design Decision
Built an automated price alert engine. Set a target price on a card, get notified when the market hits it. A single atomic SQL CTE fires and marks the alert idempotently — no polling, no duplicate notifications.
Design Language
Colour Tokens
Typography
Gill Sans — display & body
The typeface of the Pokémon franchise itself — humanist, approachable, and editorial. Matches the brand DNA without imitation.
JetBrains Mono — price numerals
Numbers on a monospace baseline align cleanly in data-dense rows. Every price, delta, and spread figure uses mono.
Signature Elements
Prismatic foil wordmark
Red → Blue → Yellow gradient built from the three games' shared DNA. Animates on a slow pan — the same loop as a holographic card under light.
Holographic card sheen
Card art gets a subtle sheen on hover using mix-blend-mode: multiply in light mode, screen in dark — same visual arc, adapted per surface.
Light Mode
Pokédex Day
Warm white base, Pokéball red accents. Clean editorial read for daytime browsing.
Dark Mode
Pokédex Night
Cool slate night, neon gain/loss. High contrast for after-hours price watching.
Colors cross-fade via a transient .theme-transition class (0.4s). Preference persists to localStorage; first visit respects prefers-color-scheme.
Information Architecture
Deliberately minimal. The IA was constrained by the user's job: get a fast read on what's moving, browse the full catalog, understand a set as a sector, or deep-dive on a single card.
Home
/The Weekly Card Chart — editorial price guide, biggest movers
The Board
/browseFull catalog browser — gallery or index, filtered by game/set/kind
Sets
/setsMarket sector view — average move, hot/cold card count, top value
Card Detail
/card/[id]Deep dive — price history, Market Depth panel, watch/alert
Pages & Key UX Decisions
/Home
—The Weekly Card Chart
Editorial over live
The initial ticker + panels layout was scrapped — it looked like every other market dashboard. The redesign reframes the homepage as an editorial price guide. The masthead dateline sets expectations: it's curated, not real-time.
Table over cards
Cards look great for browsing art. But the home page is about comparison — which card moved more, how does its spread compare to the next. Tables let the eye scan a column of numbers. Collectors who watch prices think in rows.
FloorLeader spotlight
The single biggest mover of the week gets a featured card position at the top — oversized art, foil sheen animation, price delta called out prominently. One card per week gets the spotlight.
/browseBrowse
—The Board
Gallery ⇄ Index toggle
Two distinct modes for two distinct user types. Gallery for the collector who thinks visually. Index for the analyst who wants to screen by percentage move. View preference persists in the URL so deep links preserve the mode.
Singles vs. Sealed
TCGCSV data mixes individual cards and sealed products (booster boxes, ETBs, deck sets). Singles have a card number and/or rarity; sealed products have neither. This binary is computed at query time — no separate data source needed.
Debounced filter system
Search pushes to the URL at 350ms, no form submit. Game pills, set dropdown (scoped to the selected game), kind toggle, and sort all preserve their state in pageHref for pagination.
/setsSets
—Sector View
Sets as market sectors
"Scarlet & Violet" isn't just a set — it's a sector of the Pokémon market with its own average 7-day move, hot/cold card count, and top value. Framing sets as sectors gives collectors a portfolio-level read.
Booster-fan in pure CSS
Each tile fans its top 3 cards by value on group-hover. Pure rotate() transforms on siblings — no JavaScript needed. motion-reduce kills the spread cleanly.
Set logos, zero API calls
Official set logos are sourced from the pokemontcg.io static CDN. A pre-computed map is committed to the repo so the live site needs no network request to resolve a logo URL at runtime.
/card/[id]Card Detail
—Deep Dive
Sticky 2-column layout
Left column (330px fixed) = art + market data + buy CTA. Right column = price data + chart + related cards. The left column sticks while scrolling the right — card art always stays visible as you read through the data.
Sub-type tabs open on longest history
Pokémon printings come in Normal, Holofoil, and Reverse Holofoil — each with its own price history. The default tab opens on the printing with the longest history, not just [0], so the chart is never empty.
Market Depth panel
An order-book ladder showing High / Market / Mid / Low price rungs. Bar widths scale relative to the ceiling. The Market rung gets a rotated-diamond marker — a signature element that carries through from the spread bar.
Dynamic chart Y-axis
Y-axis width computed dynamically from the widest tick label (~6.5px/char, floor 44px). Long currency labels like CA$3,962 don't get clipped at any viewport. Empty state shows honestly when fewer than 2 data points exist.
Data Layer
The ingestion pipeline, HD image enrichment, set logo generation, and release date mapping all run as offline scripts with local disk caching — so re-runs cost zero network. No paid APIs, end to end.
Source
Use
Cost
TCGCSV
Daily price dumps — all TCGplayer cards + metadata for all 3 games
Freepokemontcg.io
HD card images + set logos for Pokémon
Freeoptcgapi.com
HD card images for One Piece
Freeopen.er-api.com
Live currency exchange rates
Freeflagcdn.com
Country flag images for the currency selector
FreeTCGplayer
External buy link destination (product ID → URL)
FreeIngestion Pipeline — GitHub Actions cron · 06:00 UTC daily
npm run ingestFetches today's TCGCSV price dump for all 3 games, upserts card metadata, appends daily price rows. Idempotent — running twice on the same day is safe.
npm run trendsSQL upsert into card_trends computing 7-day and 30-day deltas. Tags by band: Skyrocketing ≥+20%, Trending Up ≥+5%, Stable, Trending Down >−15%, Crashing ≤−15%, Neutral.
npm run alertsSingle atomic CTE — find all watch targets where market_price ≤ target_price, flip the flag, write a notification row. Idempotent. Re-arms only when the user re-watches the card.
Database Schema — 5 tables · Neon Postgres
cardsStatic metadata — name, set, rarity, game, image URLs
pricesDaily time-series — market / low / mid / high per card × sub-type. Only table that grows.
card_trendsComputed deltas + tags. Updated nightly, not appended. (card_id, sub_type) PK.
wishlistsUser-set price targets per card × sub-type
notificationsAlert events — fired when market ≤ target
Scale
40,430
Cards tracked across Pokémon, One Piece, and Riftbound
299
Sets browsable with logos and release dates
~40k
New price rows appended every day
6
Currencies with live exchange rates — USD, CAD, EUR, GBP, AUD, JPY
HD Image Coverage
Pokémon
81.7%
22,545 / 27,584 cards matched
pokemontcg.io
One Piece
87%
5,622 / 7,055 cards matched
optcgapi.com
Riftbound
—
0 / 1,246 cards matched
Falls back to TCGplayer size-upgraded URL
Outcomes
Shipped end-to-end in ~4 weeks, solo
Zero ongoing infrastructure cost
40,430 cards across 3 TCGs, updated daily
299 sets browsable with logos + release dates
6-currency display with live rates
Full wishlist + price alert pipeline
Deployed on Vercel with GitHub Actions automation
What I'd Do Differently / Future Scope
Auth
v1 is intentionally single-user. Adding Clerk or Auth.js would enable multi-user wishlists without schema changes — a user_id column is already anticipated.
Graded card prices
PSA/BGS pricing isn't on the free tier. Would require PriceCharting's paid API or a scraping layer — both out of scope for $0.
Price retention job
A rolling 120-day window on the prices table to stay within Neon's 0.5 GB free-tier limit. Not yet built; the table will eventually need pruning.
More games
Any game on TCGplayer can be added by adding its category ID to the ingestion client. The architecture is designed for this.
Next Project
EDR Digitalization · Code for Canada × RCMP→