Introducing Grimoire
TL;DR: I'm building Deckbox but for Pokémon cards. Headless WordPress app with a Next.js/React frontend. You can browse the catalog now; you can also request a beta invite if you want to try it out. Want to learn more? Read on!
My first job out of college was for Blackbaud working on their next-generation platform. It was a software-as-a-service built with an API-first design: every action taken by a user, even through the official app, went through the API. During my time there, the primary app went from a Windows application to a Javascript application (something that made my Mac-loving heart happy), and this was possible because the user interface was decoupled from the application logic.
I think this architecture has stuck with me more than I realized. As headless WordPress took off, I had the chance to learn how to properly build a API-based application. Now all I needed was a problem to solve...
A problem like the massive amount of Pokémon cards in my collection. I've started selling some of them on TCGplayer, and while they have a decent app, it didn't quite fit my needs. I needed an application I could store my catalog in and quickly check it for cards that have increased in value. It also needed to be able to tag different versions of the same card for when it came time to build a deck.
I'd worked on something for this before, even wrote a blog post about it, but now it's time to finish the job. To that end, let me introduce Grimoire.
Yeah, it doesn't look like much. In the interest of finishing, this is a minimally viable product. In this case, lots of Bootstrap. But let me show you what there is!
The Features
One one level, Grimoire is just a catalog of Pokémon cards. It uses the TCGplayer API to get the different cards. TCGplayer is already invested in having an extensive catalog of all the Pokémon cards printed, so that is thankfully work I do not have to do. For Grimoire, I wanted to add two things to their catalog:
Unique, Discoverable IDs
A Grimoire ID (pkm-evs-49-r
in the screenshot) consists of up to 4 parts:
- The game the card is from. In this case,
pkm
denotes a Pokémon card. This part is mostly in place for when I inevitably support for Magic the Gathering. - The set the card is from. This card is from the Evolving Skies set, so it's abbreviated
evs
. - The card number, including any descriptors and ignoring any total numbers.
- One last part for any extra modifiers that are not part of the card number. The card in the screenshot is a reverse holographic printing, so its ID has an extra
r
.
The idea is that by looking at the card, you can infer the ID as long as you know the patterns. This is the part of the project that's going to take the longest, as there is a major manual process to all this. Most cards can fit in this pattern, but there are always exceptions. There are deck-exclusive variants, league staff variants, and a bunch of other cards that will have to be manually assigned IDs.
It's okay. It's not like I have a full-time job or anything.
Identify Alternate Printings
The card in the screenshot above is a reverse-holographic printing. There's also a normal, non-holographic printing. These cards are the same from a gameplay perspective, but they have different collection values. With Grimoire, alternate printings are all linked together:
Different card, different price, but same text. The two versions of this card link to each other. This is largely in place so that, in the future, it can be easier to find out which cards you have as you're building a deck. Some desirable cards may have more inexpensive versions. That's why it was important for this feature not just to work within a set, as shown for the Pikachu card, but between different sets.
One of the headline cards for Evolving Skies was Umbreon VMAX, shown in this screenshot:
There was also a "secret" version of this card with alternate artwork:
Very cool! And very expensive. But that wasn't the last time they printed this card. In the Brilliant Stars set, there is a special collection called the Trainer Gallery featuring Pokémon posing with their trainers. And here's the gigantic Umbreon:
All three of these are different cards with (very!) different prices. But when building a deck, all three are functionally the same.
Personal Collections
But I set out to build a personal catalog, not just a list. So once I've logged in, how does that change things?
At the bottom of each card's page, there is a list of the different collections I've made. I can change the quantity of this card in each of those collections. In this case, it's a pretty rare card, so I've only got one.
On my profile page, I can see all my collections and the current value of those cards:
And because entering this data can take a long time, it was important for me to have a CSV export so that I can download my cards and their quantities in a standard format.
Tech Specs
I could write several blog posts about the tech problems I solved making this app. And in fact, I probably will, sometime in the next... time. If you want to see a writeup on any of the features, leave a comment!
At a high level, the frontend website is a fully static Next.js application. This means that the website is written in React and TypeScript with anything that can be rendered ahead of time written to static HTML. It's currently hosted on Vercel, but I could just as easily host it anywhere else because, again, it's static HTML. If Geocities was still around, I could host it there.
That would be a bad idea, I would not host it there.
The backend is a WordPress theme hosted on Smolblog. Remember that? The static rendering uses GraphQL to get the cards and sets, while the more interactive features use custom endpoints in the WordPress REST API. The only reason for the separation is... that... I couldn't figure out how to make the custom endpoints I wanted in GraphQL and I didn't feel like taking the time to learn it just yet.
But there were plenty of fun problems I did solve, including
- Excluding browser-only Javascript from static rendering in Next
- Setting up OAuth so that it works with WordPress multisite correctly
- Writing TypeScript types for a React component that didn't include them
- Using basic card data and an MD5 hash to find different printings
- Store authentication credentials in a cookie
- Use React context to access authentication details throughout the application
- Set up custom tables in WordPress and use them with the REST API and GraphQL
The Future
As I get to the end of the first version of this project, I learned an important lesson:
WordPress was a bad choice.
I don't say that lightly. I've spent the last few years of my life immersed in the WordPress world, and I truly believe it can be used for almost anything.
But in the case of Grimoire, the data does not lend itself to custom post types as easily as custom tables. While sets and cards could conceivably be custom post types, they would rely heavily on custom metadata and taxonomies. The data is much more suited for a traditional relational database. At this point in the project, WordPress is only being used for authentication and as an administration tool. For the future of Grimoire, the benefits of a fully-featured platform like WordPress are outweighed by the difficulties in working directly with the database.
I have a few plans for Grimoire moving forward:
- Rewrite backend in a modern framework like Laravel or Ruby on Rails. This will making with the database much easier.
- Consider using Next.js' server-side capabilities. This could take some pressure off of the backend by moving some functionality closer to the edge.
- Add detailed card information. This will only need to be stored once per printing and can enable some fun features like finding recent cards that work well together.
- Sync inventory to TCGplayer. I'd love to use Grimoire to manage my inventory that I have for sale.
- Offer a "Pro" subscription with access to historical pricing data and store inventory management. Because the people most willing to pay for something are the ones making money.
I've rambled long enough. Go check out Grimoire and let me know what you think!