Frontend case study / 2026
CJ Statistics
Building a data-heavy Call of Duty statistics frontend that stayed navigable, accessible, and pleasant to use as the project grew.
CJ Statistics is a web app for browsing and analyzing Call of Duty jumping statistics from Jumpers Heaven and Jump 4 Life.
I started working on the project when it was much smaller. Over time, more data, pages, and features were added. At some point the frontend was no longer just about displaying API responses. It needed its own structure, state management, reusable UI, better routing, and a way to deal with the growing amount of data.
I ended up taking responsibility for most of that frontend work. The backend API and backend services were handled separately. My work was on the frontend and everything needed to turn that API into the application users interact with.
Key Highlights
- Data layer: Added MessagePack decoding and mapping so API responses could be turned into normal domain objects before reaching Redux and the UI. The list mappers run in O(n) time.
- Routing: Moved player and map pages from query-based navigation to Next.js dynamic routes such as
/player/[playerId]and/map/[cpId]. - Frontend architecture: Reworked the application around Next.js App Router, Redux Toolkit, reusable components, API modules, and separate data transformation logic.
- Accessibility and performance: Worked on semantic HTML, keyboard interaction, screen-reader behavior, accessibility checks, caching, image formats, rendering, and application-level performance.
Where the project started
The early version of CJ Statistics was fairly straightforward.
Fetch some data, render it, add a few filters, and make the pages usable.
That approach is fine for a small application. It becomes less comfortable once the same data starts appearing everywhere.
Players were shown in leaderboards, player pages, recent activity, and other parts of the app. Maps had similar relationships. Filters needed to be reflected in the URL. Different leaderboard sources had different fields and behavior.
The problem was not that the code stopped working.
The problem was that every new feature had a little more knowledge about the rest of the application than I wanted it to have.
That is where most of the architectural work started.
Giving the frontend some structure
I gradually separated the application into areas for pages, UI components, API code, data transformation, hooks, utilities, Redux state, and styles.
.
├── public/ # Static files (logos, Open Graph images, map/country assets)
│ ├── assets/
│ └── openGraph/
├── src/
│ ├── app/ # Next.js App Router entry, layouts, metadata, and route handlers
│ │ ├── (pages)/ # Route-group pages (servers, maps, players, favorites, etc.)
│ │ ├── api/ # App Router API endpoints
│ │ ├── layout.js
│ │ ├── page.js
│ │ ├── not-found.js
│ │ └── RootProviders.jsx
│ ├── api/ # External API clients/services
│ ├── components/ # UI components
│ │ ├── Pages/ # Page-specific UI
│ │ ├── Shared/ # Reusable shared UI
│ │ ├── Header/
│ │ ├── Footer/
│ ├── data/ # Constants, metadata, static datasets, and config objects
│ ├── hooks/ # Custom React hooks
│ │ ├── app/
│ │ └── shared/
│ ├── lib/ # Core utilities and low-level modules
│ │ └── api/
│ │ └── mappers/
│ ├── redux/ # Global state management
│ │ ├── features/ # Feature slices/thunks/apis
│ │ ├── msgPack/ # MessagePack base query support
│ │ └── store.js
│ └── styles/ # Global SCSS variables, mixins, and styles
├── next.config.mjs
├── package.json
└── README.mdThere was no point in creating a complicated architecture just for the sake of having one.
The useful part of the split was knowing where different kinds of logic belonged.
- API code handled communication.
- Data mappers handled the shape of the data.
- Redux handled shared application state.
- Components handled presentation and interaction.
That separation made later changes easier because a change in one layer did not automatically have to spread through the whole UI.
Player and map routes
One of the bigger refactors was changing how individual players and maps were represented in the application.
The change affected more than the page itself. Those links existed across leaderboards, map cards, player activity, route completion, run analytics, and other parts of the UI.
Updating the route meant finding and updating all of those references too. The player page also gained dynamic metadata and a clearer page-level data flow. The map page followed the same direction.
This ended up being one of those changes that looks small when written as a Git commit and turns out to touch half the application.
Working with MessagePack
The API eventually introduced a newer MessagePack-based format for several of the main datasets. It was compact, which was useful, but the response format was not something I wanted components to know about.
Some data came back as positional arrays. A component should not have to know that index 0 means one thing and index 4 means another.
The same idea was used for maps, players, and leaderboards. The actual list mapping is linear in the number of records, but the bigger benefit was keeping the transport format out of the rest of the application.
If the API representation changes, there is one place where that change needs to be understood.
Redux and shared state
As the application grew, more of the data needed to be used in more than one place.
I used Redux Toolkit for shared state and organized it around the domains that actually exist in the application:
serversmapsplayersplayerProfileleaderboardglobalThe point was not to put everything in Redux. Local UI state could stay local. Shared data and state that crossed page or component boundaries had a more central place to live.
That distinction helped keep individual pages from becoming responsible for everything.
Building the actual UI
The project has a lot of data, but users do not want to "browse the API". They want to find a player, compare leaderboard results, look at a map, check a run, or find a server.
A lot of the frontend work was therefore about making that data easier to navigate. The application gained things such as:
- player search
- leaderboard sorting
- pagination
- URL-based filters
- FPS filters
- player status filters
- server browsing
- map browsing
- player profiles
- route completion
- run analytics
- badges and rank information
Some of those features were small. Others became shared patterns after I noticed the same problem appearing in several places.
The map preview
Map thumbnails are useful, but sometimes you want to actually look at the map image.
I built a full-screen preview with thumbnail navigation, previous/next controls, counters, and keyboard support.
The modal was moved into the global provider layer so it did not need to be mounted separately on every page.
I also simplified the navigation logic and replaced some repeated inline SVG markup with a shared SVG sprite.
Nothing particularly exotic was happening here. It was mostly a case of building the feature, then cleaning up the parts around it that became obviously repetitive.
Different leaderboard systems
CJ Statistics has more than one leaderboard system, so the frontend cannot assume that every leaderboard has the same meaning.
I added support for the Jump 4 Life Rank XP leaderboard, including rank levels, badges, total XP, and progress toward the next level.
The layout also changes depending on the leaderboard source and type. That meant some filters, columns, and information could only be shown when they actually made sense for the selected leaderboard.
This was one of the points where understanding the domain mattered as much as writing the component.
Accessibility
Accessibility has been part of the frontend work throughout the project.
I worked on semantic HTML, heading structure, keyboard interaction, form labels, custom controls, skip links, landmarks, tooltips, and screen-reader behavior.
I also added accessibility tooling during development.
Some fixes were very small. For example, a custom checkbox still needs to behave like a checkbox. A repeated map name does not always need to be announced twice. A navigation element should have a useful landmark name when there is more than one of the same type.
These things are easy to overlook when a page looks correct in a browser.
Performance
Performance has mostly been a collection of smaller decisions rather than one big optimization.
The application uses Next.js App Router, server rendering where appropriate, caching, MessagePack responses, modern image formats, preconnects, and other application-level optimizations.
Map images were moved toward AVIF/WebP, and I also worked on reducing unnecessary client-side work and hydration.
I tend to prefer simple optimizations that fit the architecture over adding another library every time there is a performance problem.
SEO and social previews
Once player and map pages became proper dynamic routes, it also made sense for them to have their own metadata.
I added dynamic metadata and Open Graph image generation for those pages. That means a player page can represent that player in search and when shared, and a map page can do the same for that map.
The route, page data, and metadata now describe the same resource instead of the page being just another state of a generic route.
Deployment
The project was initially deployed on Vercel. Later, I worked on moving it to Cloudflare Workers using Vinext.
That meant configuring the build and deployment setup, Wrangler, Worker assets, caching, images, and environment variables.
The migration was not completely transparent. Some pages behaved differently in the new environment, which led to debugging issues that did not exist in the original deployment.
That part of the project reminded me that deployment is also an application concern. A frontend can work perfectly in one environment and still make assumptions that do not hold somewhere else.
What I'm working on now
CJ Statistics is still being developed, so some parts of the frontend are not finished yet.
Current work includes responsive improvements, continuing the MessagePack v2 migration, refining data reflow, improving URL-driven fetching on the Players page, and cleaning up parts of the UI.
I am keeping those items separate from the completed work in this case study because the project is still changing.
My role
My work on CJ Statistics has been focused on the frontend.
- Next.js application structure and routing
- React components and UI systems
- Redux state management
- API integration
- MessagePack decoding and data mapping
- caching and data flow
- responsive UI
- accessibility
- performance
- SEO and Open Graph metadata
- deployment configuration
The backend API and backend services were maintained separately.
So the simplest way to describe my role is:
I owned the frontend application and the user-facing experience built on top of the backend API.
Key Takeaways
The most useful thing I got from this project was not learning another React pattern.
It was learning what happens when an application stays alive long enough for its first implementation to become too small for what the product has turned into.
A new feature can expose a problem in routing. A new API format can expose a problem in data handling. A new interaction can expose duplicated UI logic. A performance issue can turn out to be a data-fetching problem rather than a rendering problem.
You end up spending a lot of time deciding where something should live and how much of the rest of the application should know about it. That is the part of frontend engineering I found most useful in this project.
CJ Statistics also gave me hands-on experience with a data-heavy application where the frontend has to deal with a lot of information without making the interface feel complicated.
There is still work to do, and I expect the codebase to keep changing. That is normal.
For me, the interesting part is trying to make the next change easier than the last one.