Writing a Chrome Extension to help complete my Pokémon collection

Nick Felker
7 min readMay 3, 2024

--

Some of the best cards in my collection, framed

I got my first Pokémon cards a long time ago, maybe around 2001 or so. Back then, they were booster packs of “Base Set” and “Jungle” and I traded them with friends at school and on the bus.

I still have those cards. And now that the market has been falling, now seems like a great time for me to get back into the space. I’ve gone back through my collection and am looking to complete the sets.

The secondary market is fairly easy to access these days with a plethora of online stores. That’s a great start, and I can use that market information to make smart purchases.

But how do I know what I need? For that, I spent a bit of time using TCG Collector. Card by card, I entered in all 3000+ cards that I’ve collected over the past twenty-three years. You can view my collection on the website as well.

Now that I have what I am missing on the record, I can go through and pick up the remaining ones. For completion, I want to place every card into a binder in order: from set order and then card order.

My Base set cards in numerical order

Geometric placement matters here. And it’s fairly easy to know card 9 goes in the bottom-right part of the first page. However, when you start getting up in card count to 214, suddenly it becomes a bit harder to understand the placement.

Even worse is when a set adds a bunch of secret rare cards that are numbered outside the bounds of the set. For instance, Giratina VSTAR is part of Crown Zenith with a card number of GG69/GG70, which is a different counter from the normal 159 cards.

As I ran into these kinds of issues, I found an number of pain points I wanted addressed. So I created an open-source Chrome Extension: Master Collection for TCG Collector. Putting this all together took about an hour from start-to-finish.

This extension works with TCG Collector to add 3 key features that I think will make my collecting experience a lot better:

  • Add a dark theme
  • Enable grid view
  • Copy TCG Player format to clipboard

This blog post will elaborate on how I built all three points, and hopefully serve as a helpful article for others who are interested in building browser extensions.

Manifest

This is basically a plugin for a particular website. I only want it to run on a particular website and never on a different website. When I’m on TCG Collector, I want to inject some code onto the webpage to add my features.

From a Chrome extension perspective, this can be distilled into a small manifest.json file, one of the required files for any extension.

{
"name": "Master Collection for TCG Collector",
"description": "Organizational tooling for TCGCollector",
"version": "1.1.0",
"manifest_version": 3,
"permissions": [],
"host_permissions": ["https://*.tcgcollector.com/*"],
"content_scripts": [
{
"matches": ["https://*.tcgcollector.com/*"],
"run_at": "document_end",
"js": ["content-script.js"],
"all_frames": true
}
]
}

As you can see, when the document is loaded on the browser, my script (content-script.js) then takes effect. I don’t know if this is always the right approach, as it does mean my dark theme takes a bit long to load and there’s a flash when that happens.

Dark Theme

The simple approach to adding a dark theme is just using some CSS. Browsers already have a dark-theme CSS media query that websites can voluntarily handle by changing the look of their page.

When my script loads, I inject a CSS element into the document’s head:

const styles = document.createElement('style')
styles.innerHTML = `...`
document.head.appendChild(styles)

The specific styles I added had to be hand-checked to ensure they were thorough. I did miss a few elements on my first pass. Basically I just tried to swap colors on every element that needed it:

@media (prefers-color-scheme: dark) {
body {
background-color: #313131;
color: #efefef;
}

.list-group>.list-group-heading, .list-group>.list-group-item {
background-color: #424242;
border-color: #757575;
color: #cecece;
}
...
}

And there’s about 130 lines of different CSS changes that will only take effect if the browser is already set to a dark theme.

Adding Grid Views

Card GG69 should be on the backside of the 13th page, in the center-left slot

For collecting in a 9-grid binder, I want all the cards to be reformatted not for the dynamic webpage windows but as how they would physically appear.

I added a few buttons to the top row on card set pages. These can be injected by constructing the buttons and adding a click event:

function attachOrganizerButtons() {
const organizer = document.querySelector('#card-display-options-container')
if (!organizer) return // Bail early
const gridStyles = document.createElement('style')
const gridRow = document.createElement('div')

...

const btn3x3 = document.createElement('a')
btn3x3.className = 'button button-plain collector'
btn3x3.role = 'button'
btn3x3.onclick = () => { handleGridAction(3, gridStyles) }
const icon3x3 = document.createElement('span')
icon3x3.className = 'fa-solid fa-table-cells button-icon'
btn3x3.appendChild(icon3x3)
gridRow.appendChild(btn3x3)

...

gridRow.className = 'card-display-option'
organizer.append(gridStyles, gridRow)
}

This is generally how I did it, making sure that each button looked like other buttons on the site already including using the existing icon library

After observing the HTML for the site, I found a way to select not just the top row but the individual cards. The handleGridAction modifies some page CSS to change the grid layout. Additionally, it goes through the page and adds titles with page numbers:

function handleGridAction(cols, gridStyles) {
const gridItems = document.querySelectorAll('.card-image-grid-item')
const pastPages = document.querySelectorAll('.newpagegrid')
// Cleanup
pastPages.forEach(el => { el.remove() })

gridStyles.innerHTML = `
@media (min-width: 960px) and (max-width: 1159.98px) {
...
}

@media (min-width: 1160px) {
#card-image-grid {
grid-template-columns: repeat(${cols}, minmax(0, 1fr));
width: 720px;
}

.card-image-grid-item {
width: 200px;
}
}

div.newpagegrid hr {
background-color: white;
}

div.newpagegrid hr.backside {
opacity: 0.7;
}
`
const frontPage = cols * cols
const backPage = frontPage * 2
gridItems.forEach((el, i) => {
if (i % backPage === 0) {
const newPageGrid = document.createElement('div')
const pageNo = document.createElement('span')
const hr = document.createElement('hr')
pageNo.innerText = `Front ${Math.floor(i/backPage) + 1}`
hr.className = 'newpage'
newPageGrid.className = 'newpagegrid'
newPageGrid.appendChild(pageNo)
newPageGrid.appendChild(hr)
el.prepend(newPageGrid)
} else if (i % frontPage === 0) {
const newPageGrid = document.createElement('div')
const pageNo = document.createElement('span')
const hr = document.createElement('hr')
pageNo.innerText = `Backside ${Math.floor(i/backPage) + 1}`
hr.className = 'backside'
newPageGrid.className = 'newpagegrid'
newPageGrid.appendChild(pageNo)
newPageGrid.appendChild(hr)
el.prepend(newPageGrid)
}
})
}

The function takes a columns parameter, allowing me to have a button for standard 3x3 pages but also uncommon 2x2 pages. I have seen a few 3x4 binders. I currently don’t support that layout. It wouldn’t be hard. Since I compute the frontPage as cols * cols , I could add a row parameter to the function. The rest of the content would be the same.

Using native DOM APIs allows the website to maintain full functionality over the individual card components. It also improves performance. I don’t need to rebuild any page elements. Tagging all of my new elements makes it easy to delete them when the page content changes in some other way.

Clipboard support

Now that I have managed to organize all my current cards, spending a lot of time staring at the screen in a dark theme, it’s time to buy more of them!

There’s a website called TCG Player which is basically an online marketplace of various sellers of cards. Their search capabilities are broad, but it can also mean spending a lot of time trying to find each of them.

They have a Mass Entry feature where you can enter a list of cards you want and their quantity. Then the website will try to match up sales inexpensively and from the fewest number of sellers.

Mass Entry feature on TCG Player for the Fossil set

To make it easier to jump between TCG Collector and TCG Player, I added a clipboard button to my row of custom buttons. It takes all of the currently displayed cards, converts them into the Mass Entry format, and writes that directly to your clipboard so that you can drop it right into that form.

  copyBulk = document.createElement('a')
copyBulk.className = 'button button-plain collector'
copyBulk.role = 'button'
copyBulk.onclick = () => {
const setCode = document.querySelector('#card-search-result-title-expansion-code').innerText.trim()
const textEntries = []
const cardsInGrid = document.querySelectorAll('.card-image-grid-item-card-title')
for (const card of cardsInGrid.values()) {
const cardText = card.innerText.trim()
const parser = new RegExp('(.+?)\\(.+\\s(\\d+)/\\d+\\)')
console.log(cardText, parser.exec(cardText))
const [_, title, number] = parser.exec(cardText)
textEntries.push(`1 ${title.trim()} [${setCode}]`)
}
console.log(textEntries.join('\n'))
navigator.clipboard.writeText(textEntries.join('\n'))
alert(`Copied ${cardsInGrid.length} cards to the clipboard`)
}
const iconCopy = document.createElement('span')
iconCopy.className = 'fa-solid fa-copy button-icon'
copyBulk.appendChild(iconCopy)
gridRow.appendChild(copyBulk)

While this has saved me a lot of time, I haven’t managed to work out all the issues. Since some sets have several cards with the same Pokémon, I think there’s an ambiguity that causes problems on TCG Player. I’m still trying to figure out how to solve this.

Do I want the Gengar card, or the Gengar card?

Time for you to try

All of the code for this project is open-source on GitHub if you want to see in more detail what I did.

If you, like me, want to really optimize your Pokémon card collecting, the Master Collection for TCG Collector is available to install to your browser from the Chrome Web Store.

--

--

Nick Felker
Nick Felker

Written by Nick Felker

Social Media Expert -- Rowan University 2017 -- IoT & Assistant @ Google

No responses yet