Weekly Card Chart · Live
Holo Holdings
The trading-card market, live.
Pokémon · One Piece · Riftbound
TRACKED 11,240
▲ 31 SKYROCKETING
▼ 43 CRASHING
LIVE
Charizard ex SAR+12.4%Black Lotus+8.9%Lillie FA-3.2%Roronoa Zoro SP+21.1%Rayquaza VMAX+5.7%Mewtwo V-UNION-7.4%Riftbound #001+44.0%Umbreon VMAX+3.1%Nami SP-2.8%Pikachu Illustrator+0.9%Charizard ex SAR+12.4%Black Lotus+8.9%Lillie FA-3.2%Roronoa Zoro SP+21.1%Rayquaza VMAX+5.7%Mewtwo V-UNION-7.4%Riftbound #001+44.0%Umbreon VMAX+3.1%Nami SP-2.8%Pikachu Illustrator+0.9%
USD · JUN 24

Product Design · Full-Stack Dev · 2026

Designing a data-driven TCG market tracker
so hobbyist collectors can buy with confidence

Solo Designer & Developer4 WeeksNext.js 16 · Tailwind v4 · Neon Postgres · Recharts

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

The research happened on the trade floor, not in a survey.

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.

Vancity Card ShowThe Vault Card ShowBossa Metro Vancouver Card ShowWest Van Card ShowNext Gen Card ShowGuildford Card ShowVancity Card ShowThe Vault Card ShowBossa Metro Vancouver Card ShowWest Van Card ShowNext Gen Card ShowGuildford Card Show

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

"Pokédex Bright" — editorial, not clinical.

Colour Tokens

Base#fcfcfawarm white
Ink#15171c
Gain#1fb257green
Loss#ff4136red
Base#0d1017cool slate — Pokédex Night
Gain#2fd27ebrightened
Loss#ff5a4dbrightened

Typography

Gill Sans — display & body

RegularMediumBold

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

Four routes. Every user job fits one of them.

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.

01

Home

/

The Weekly Card Chart — editorial price guide, biggest movers

02

The Board

/browse

Full catalog browser — gallery or index, filtered by game/set/kind

03

Sets

/sets

Market sector view — average move, hot/cold card count, top value

04

Card Detail

/card/[id]

Deep dive — price history, Market Depth panel, watch/alert

Pages & Key UX Decisions

Every decision has a reason that lived in the data.

/

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.

/browse

Browse

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.

/sets

Sets

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

Six free APIs. Zero paid infrastructure. Built to run forever.

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

Free

pokemontcg.io

HD card images + set logos for Pokémon

Free

optcgapi.com

HD card images for One Piece

Free

open.er-api.com

Live currency exchange rates

Free

flagcdn.com

Country flag images for the currency selector

Free

TCGplayer

External buy link destination (product ID → URL)

Free

Ingestion Pipeline — GitHub Actions cron · 06:00 UTC daily

01npm run ingest

Fetches 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.

02npm run trends

SQL 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.

03npm run alerts

Single 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

cards

Static metadata — name, set, rarity, game, image URLs

prices

Daily time-series — market / low / mid / high per card × sub-type. Only table that grows.

card_trends

Computed deltas + tags. Updated nightly, not appended. (card_id, sub_type) PK.

wishlists

User-set price targets per card × sub-type

notifications

Alert 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.