Competitive Stats Hub for the CJ League
Raw API data turned into real rivalry — a platform built for the community, run by the community.
- Role
- Product Designer + Dev
- Timeline
- 2026
- Platform
- Web + Mobile
- Team
- Solo project

Overview
CJ League is a private CS2 competitive league with around 40 recurring players, organized inside a private FACEIT hub. The group had real engagement and competitive drive — but zero visibility into individual or collective performance beyond what FACEIT natively offers.
The challenge
What FACEIT doesn't deliver for private hubs:
- A consolidated seasonal leaderboard for the league
- Cross-player performance comparison within the group
- Map pool distribution data across the season
- Rank evolution over time
- Visual identity for the league — everything was "generic FACEIT"
Players had to leave the platform, manually cross-reference data, or rely on collective memory in Discord to understand who was performing best in any given season.
Design problem statement:
"How might we create an experience that transforms raw API data into a stats hub capable of generating competition, belonging, and continuous engagement — and that can be operated autonomously by the community itself?"
Research & users
Who uses it and what they expect
Because the group is closed and accessible, research was direct — behavioral observation in Discord and informal conversations during matches. Three main profiles emerged:
- The competitive player
- Tracks every match closely. Wants to know exactly where they rank and how much they need to improve to climb.
- The casual player
- Plays for fun but enjoys checking the group's stats afterward. Engages if the data is easy to read.
- The organizer
- Needs data for concrete decisions: seeding pots, prizes, balancing the next season.
Key insights gathered:
- Players opened FACEIT after every match — but left frustrated by the lack of league context
- Manual rankings were created in Discord, validating the desire for structured data
- The CJ League identity created belonging that FACEIT didn't reflect visually
- Seeding pots were managed from memory — data that needed to be visible and queryable
Player card anatomy
The central reading unit of the leaderboard
The player card is the most recurring visual element in the platform — it appears on the season leaderboard and condenses the most relevant information for each player into a scannable format. Each card was designed to work both as a compact list item and as an expanded detailed view.
Card elements:
- Leaderboard position with visual highlight for top 3
- Player avatar (pulled from their FACEIT profile)
- Pot indicator (1 to 5) — skill tier reference within the league
- K/D Ratio as primary performance metric
- Total matches played in the season
- Win rate percentage
- Internally calculated rating (league composite metric)
Player page anatomy
Each league player has a dedicated page, accessible via the link on their card. The player page consolidates the player's complete season history and provides a deeper read than the leaderboard card alone.
Player page sections:
- Player header
- Featured avatar, nickname, current pot, leaderboard position, and a summary of key season metrics. Functions as the profile's visual identity.
- Season stats overview
- Panel with K/D ratio, total matches, win rate, total kills, deaths, assists, and headshot percentage — all scoped to the active season.
- Rank evolution chart
- Timeline showing how the player's leaderboard position fluctuated across the season. Reveals trends of improvement or performance decline.
- Comparison with other players
- Table or visualization positioning the player relative to the league average and players in the same pot — context the ranking alone can't provide.
- Individual match history
- List of season matches with map, result (win/loss), K/D, kills, deaths, and assists per match. Sorted newest to oldest.
Design decision: the player page exists for the player who wants to go beyond the leaderboard snapshot.
The rank evolution chart is especially valuable for showing consistency — not just where a player stands, but how they got there.

Admin panel
One of the project's core premises was that the site needed to work without the original developer. The admin panel was designed so that a league organizer — with no technical knowledge — could keep the platform updated, manage seasons, and understand what's happening under the hood.
Data update panel
Main button to fetch new data from FACEIT and update the database. Shows last update status, number of indexed matches, and visual feedback for success or error. Eliminates the need for code or server access to keep the site alive.
Cache control
Interface to clear the Redis cache per season or force a full refresh. Includes indicators for when the cache was last generated and how much data is stored — necessary context for the operator to understand whether data is fresh or stale.
Season management
Create and activate new seasons without touching code. Each season has its own isolated namespace, ensuring previous season history is preserved while the new one starts clean.
Player management
Add, remove, and edit league players — including their FACEIT nickname, pot assignment, and profile details. Lets the organizer keep the roster updated between seasons without a deploy.
Site guide (embedded documentation)
A complete guide written directly inside the admin panel, explaining what each section does, when to use each action, and how to interpret status indicators. Not a technical README — it's documentation written for the human operator, in plain language.
Design decision: the admin panel was treated as a product, not an internal tool. Every action has clear feedback, every state has visual context, and the embedded guide ensures autonomy for anyone on the organizing team — with or without the developer available.

The solution
A dedicated web stats hub for the CJ League, consuming the FACEIT API and presenting season data in a fast, visual, and branded experience — with an administration layer that makes the platform self-sufficient.
Other notable design decisions:
- Smart Redis caching: instant loading — the audience opens the site on mobile right after a match and won't tolerate slow loads
- Branded visual identity: dark theme, FACEIT orange, typographic hierarchy — not generic FACEIT, this is CJ League
- New data badge: visual indicator placed in the bottom-right corner on mobile for thumb reach
- Seasons as namespaces: history preserved, new season starts clean without losing previous data

Design process
The project started pragmatic — no extensive wireframing before building. The cycle was direct, driven by real group feedback:
Identify pain → Functional MVP → Group feedback → Iterate → Add visual layer
Documented iterations:
- V1: Simple table with raw API data. Functional, no visual identity.
- V2: Cards with FACEIT-inspired design. Dark theme, typographic hierarchy, orange as accent.
- V3: Added pot filters, search, map chart, update badge, and individual player pages.
- V4 (Planned): Rank evolution charts via historical Redis snapshots.
Success metrics
Quantitative metrics:
- Load time — via Vercel Analytics / Lighthouse | Baseline: Raw API: >2s | Goal: <500ms with cache
- Result: 274ms
- Cache hit rate — via Redis logs | Baseline: 0% (no cache) | Goal: >80% of requests
- Goal achieved
- Matches indexed — via FACEIT API data | Baseline: 0 | Goal: 76+ matches in Season 1
- Result: 411 matches
- Players in ranking — via Hub data | Baseline: 0 | Goal: 28–40 active players
- Result: 53 players
- Uptime — via Vercel monitoring | Baseline: — | Goal: 99%+
- Result: 100%
Qualitative metrics (real value proxies):
- Organic adoption - Players sharing site screenshots in Discord without being asked ✅
- Decision reference - Organizer using hub data to seed pots for the next season ✅
- Post-match retention - Players opening the site immediately after each league match ✅
- Operator autonomy - Admin panel used successfully without calling the developer ✅
- Feature requests - List of requests received — rank evolution, per-match history ✅
Results & learnings
What worked:
- Cache as UX: the perceived speed was immediate — the group started opening the site on mobile right after matches
- Admin panel as a product: the organizer updated data, created a new season, and managed players without any technical help
- Own identity: having a CJ League branded platform increased belonging and pride in the league
- Data that generates conversation: the leaderboard became the reference in Discord, replacing manual tables
What was learned:
- Historical rank tracking needs to be planned from day one — snapshots must be saved before they're needed
- Embedded documentation beats an external README — the admin panel guide was actually used
- Performance is a product feature: Redis was a UX decision as much as an engineering one
Next steps:
- Weekly rank evolution chart (from Season 2 onward)
- Profile page with cross-season career history
- Automatic Discord notifications via webhook when new data is available