← home ← blog

Building a Vue JS Application w/ Pokemon TCG API

In the last post related to VueJS, we discussed installing, creating, and running a project locally; now let’s have some fun with a new project incorporating what we’ve already learned and calling a remote API.

Pokemon TCG API

What are we building?

We’re going to be taking a 3rd party API (Pokemon TCG Developers) that is meant for the Pokemon Trading Card game and allow visitors to do the following:

  • See 100 most recent trading cards within our homepage.
  • Ability to click on a single card and see details / extra information.
  • View all “types” and “sets” of cards within their routes.

Let’s get ‘er going:

# vue create pokemon-tcg-with-vue-js
# cd pokemon-tcg-with-vue-js
# npm install vue-router
# npm install --save axios

Much like last time, we’re going to need to do a couple things to go ahead and build out our routes/views before connecting to any API; let’s start with the following in src/main.js (note the new line comments):

import Vue from 'vue'
import VueRouter from 'vue-router' // NEW LINE #1
import App from './App.vue'

Vue.use(VueRouter) // NEW LINE #2
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

Remember, this change doesn’t necessarily do anything yet, so if you run npm run serve you’ll just see the default Vue JS homepage. Let’s start working on our components and we’ll circle back to routes.

Views & Components

To get us started on our way to getting some Pokemon trading cards, let’s go ahead and create a couple files:

  • src/components/Home.vue
  • src/components/Sets.vue
  • src/components/Types.vue
  • src/components/Card.vue
  • src/components/templates/header.vue
  • src/components/templates/footer.vue

Inside of the Home.vue, Sets.vue, Types.vue, and Card.vue files, let’s just add the following super simple code (replacing [page_name] with Home, Sets, Types, or Card) just to test things out in a few minutes.

<template>
    <div>
        <br />
        [page_name]
        <br /><br /><br />
    </div> 
</template>
<script>
export default {
    data () {
        return {}
    },
}
</script>

Now, let’s go ahead and create those global components for header.vue and footer.vue with the following content (this time replacing [section_name] with header or footer):

<template>
    <div>
        [section_name]
    </div>
</template>
<script>
export default {
    data() {
        return {}
    },
}
</script>

Next, let’s go ahead and get our routes working and sample templates outputting; first up, replace the contents of your src/App.vue with the following content (if you refresh before updating routes in a moment, you’ll see a blank screen):

<template>
  <div id="app">
    <Header />

    <router-view />

    <Footer />
  </div>
</template>

<script>
import Header from './components/templates/header.vue'
import Footer from './components/templates/footer.vue'

export default {
  name: 'App',
  components: {
    Header,
    Footer
  }
}
</script>

Go ahead and create a new file called src/routes.js and add/save the following content:

import Home from './components/Home.vue';
import Card from './components/Card.vue';
import Sets from './components/Sets.vue';
import Types from './components/Types.vue';

const routes = [
    { path: '/', component: Home },
    { path: '/card/:id', component: Card },
    { path: '/sets', component: Sets },
    { path: '/types', component: Types },
];

export default routes;

To get these new routes to actually output, let’s make the following changes to our src/main.js file:

  1. Import Routes
import VueRouter from 'vue-router'; // Existing Line
import routes from './routes'; // New Line
  1. Initialize VueRouter
Vue.use(VueRouter); // Existing Line

const router = new VueRouter({mode: 'history', routes}); // New Line
  1. Enable Router

(Almost) Last, but not least, replace this:

new Vue({
  render: h => h(App),
}).$mount('#app')

With this:

new Vue({
    router,
    render: h => h(App)
}).$mount('#app');

Now, one last step (don’t worry, almost time for fun), let’s add this into our src/components/templates/header.vue file in order to be able to click around the site:

<router-link to="/">Home</router-link> | 
<router-link to="/sets">Sets</router-link> | 
<router-link to="/types">Types</router-link> | 
<router-link to="/card/1">Sample Card</router-link>

Time For Fun

Let’s take a small step back and talk about what we’ve done so far;

  • Created necessary routes to allow navigation around the site
  • Created our components to allow our content to output
  • Tested to validate we have a basic working site

Now, let’s dive into our actual project…

Tailwind

# npm install tailwindcss

Create a file called postcss.config.js with the following basic settings:

const autoprefixer = require('autoprefixer');
const tailwindcss = require('tailwindcss');

module.exports = {
  plugins: [
    tailwindcss,
    autoprefixer,
  ],
};

Create one more file, src/assets/css/index.css, with the following content:

@tailwind base;
@tailwind components;
@tailwind utilities;

Next up, add this new CSS file into our src/main.js file:

import App from './App.vue'

import './assets/styles/index.css'; // NEW LINE

Design

TailWind CSS is amazing, but it’s love it or hate it for a lot of folks - for today though, we’re going to be using some free components from the amazing site Tailwind Components. Specifically, we’ll be using the following snippets of code:

  1. Navigation
  2. Product Card
  3. Card for Listing
  4. Paragraph w/ Image

Let’s start with the navigation so you can see this start to come together (design wise); open up your src/components/templates/header.vue and replace with the following:

<template>
    <nav class="bg-white shadow" role="navigation">
        <div class="container mx-auto p-4 flex flex-wrap items-center md:flex-no-wrap">
            <div class="mr-4 md:mr-8">
                <router-link to="/">
                    <img src="https://i.ibb.co/z6CBc2D/pokemon.png" class="h-10 w-10" />
                </router-link>
            </div>
            
            <div class="ml-auto md:hidden">
                <button class="flex items-center px-3 py-2 border rounded" type="button">
                    <svg class="h-3 w-3" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
                        <title>Menu</title>
                        <path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"/>
                    </svg>
                </button>
            </div>
            
            <div class="w-full md:w-auto md:flex-grow md:flex md:items-center">
                <ul class="flex flex-col mt-4 -mx-4 pt-4 border-t md:flex-row md:items-center md:mx-0 md:ml-auto md:mt-0 md:pt-0 md:border-0">
                    <li>
                        <router-link class="block px-4 py-1 md:p-2 lg:px-4" to="/">Home</router-link>
                    </li>

                    <li>
                        <router-link class="block px-4 py-1 md:p-2 lg:px-4" to="/types">Types</router-link>
                    </li>

                    <li>
                        <router-link class="block px-4 py-1 md:p-2 lg:px-4" to="/sets">Sets</router-link>
                    </li>
                </ul>
            </div>
        </div>
    </nav>
</template>
<script>
export default {
    data() {
        return {}
    },
}
</script>

While we’re at it, let’s give credit where credit is due (and cover our butts with the almighty Nintendo) and add the following into our src/components/templates/footer.vue:

<template>
    <footer class="text-center text-xs pb-10 pt-10">
        Not Affiliated w/ Nintendo or Pokemon Company<br />
        <a class="text-red-700" href="https://www.flaticon.com/free-icon/pokeball_528101?term=pokeball">Pokeball Icon</a> made by <a class="text-red-700" href="https://www.flaticon.com/authors/those-icons">Those Icons</a> at <a class="text-red-700" href="https://www.flaticon.com">www.flaticon.com</a>.
    </footer>
</template>
<script>
export default {
    data() {
        return {}
    },
}
</script>

API

Finally, we’re ready to… do more configs, kidding, let’s get our hands dirty. On the API we’re using, we’re going to be using four specific end points:

  • /cards
  • /cards/:id
  • /sets
  • /types

To make this work, let’s create the following file:

src/services/api/pokemontcg.js

Note: This folder/path setup is not required, just personal preference, change as you’d wish! :)

Inside of this file, let’s go ahead and add the following base code:

import axios from 'axios'

export default {

    getCards() {

    },

    getCard(id) {

    },

    getSets() {

    },

    getTypes() {

    }

}

Let’s break down how we’re going to be using these four functions:

  1. Home.vue - In this component, we’ll be using the getCards() function to output 100 recent cards on the homepage.

  2. Sets.vue - In this component, we’ll be using the getSets() function to output all of the sets from the API.

  3. Types.vue - Same as Sets.vue, we’ll be using getTypes() function to output all of the types from the API.

  4. Card.vue - Last, but not least, the getCard() function (with a card id passed as an argument) will be used to output card details.

getCards()

We want to display a few things on the homepage; the card image, hit points, and a link to more details - to accomplish that, let’s add the following code to our getCards function so that it looks like the following:

    getCards() {

        return axios.get('https://api.pokemontcg.io/v1/cards').
        then(response => { 
            console.table(response.data.cards)
            return response.data.cards
        }) 

    }

Then, we’ll need to update our src/component/Home.vue to the following code which will loop through the cards, output a basic loading message, and allow users to click on a card:


<template>
    <div>
        <div class="w-1/2 mx-auto text-center pb-20 pt-20" v-if="loading">
            Loading...
        </div>

        <div class="flex flex-wrap max-w-screen-xl mx-auto">
            <div class="w-1/4" v-for="card in cards" :key="card.id">
                <div class="max-w-xs bg-white shadow-lg rounded-lg overflow-hidden my-10 border border-gray-400 m-3 p-2 mb-2">
                    <router-link :to="`/card/${ card.id }`">
                        <img class="h-75 w-full object-cover mt-2 mb-2" :src="card.imageUrl" />
                        <div class="flex items-center justify-between px-4 py-2 bg-red-700 rounded">
                            <h1 class="text-gray-200 font-bold text-xl" v-if="card.hp">HP: {{ card.hp }}</h1>
                            <h1 class="text-gray-200 font-bold text-xl" v-else>&nbsp;</h1>
                            <button class="px-3 py-1 bg-gray-200 text-sm text-gray-900 font-semibold rounded">Details</button>
                        </div>
                    </router-link>
                </div>
            </div>
        </div>
    </div> 
</template>
<script>
import PokemonAPI from "@/services/api/pokemontcg"

export default {    
    data () {
        return {
            loading: true,
            cards: []
        }
    },

    created() {
         PokemonAPI.getCards()
            .then(cards => {
                this.cards = cards
            })
            .catch(error => console.log(error))
            .finally(() => {
                this.loading = false;
            })
    }
}
</script>

getSets()

For sets, the code will be very similar with the following as our JavaScript function:

getSets() {

    return axios.get('https://api.pokemontcg.io/v1/sets').
    then(response => { 
        console.table(response.data.sets)
        return response.data.sets
    }) 

}

For the view, let’s use the following code within our src/components/Sets.vue to output basic information such as the logo, symbol, name, series, release date, etc:


<template>
    <div>
        <div class="w-1/2 mx-auto text-center pb-20 pt-20" v-if="loading">
            Loading...
        </div>

        <div class="w-full mt-4">
            <h1 class="text-center">Sets:</h1>
        </div>

        <div class="flex flex-wrap max-w-screen-xl mx-auto">

            <div class="w-1/4" v-for="set in sets" :key="set.code">
                <div class="overflow-hidden rounded border bg-white shadow bg-white shadow-lg rounded-lg overflow-hidden m-2 border border-gray-400 m-3 p-2">
                    <div class="relative">
                        <div class="h-48 bg-cover bg-no-repeat bg-contain bg-center" :style="{ backgroundImage: `url('${set.logoUrl}')` }"></div>
                        <div style="background-color: rgba(0,0,0,0.6)" class="absolute bottom-0 mb-2 ml-3 px-2 py-1 rounded text-sm text-white">Total Cards: {{ set.totalCards }}</div>
                        <div style="bottom: -20px;" class="absolute right-0 w-10 mr-2">
                            <a href="#">
                                <img class="rounded-full border-2 border-white" :src="set.symbolUrl" >
                            </a>
                        </div>
                    </div>
                    
                    <div class="p-3">
                        <h3 class="mr-10 text-sm truncate-2nd">
                            {{ set.name }}
                        </h3>
                        <div class="flex items-start justify-between">
                            <p class="text-xs text-gray-500">Series: {{ set.series }}</p>
                            <button class="outline text-xs text-gray-500 hover:text-blue-500" title="Bookmark this ad"><i class="far fa-bookmark"></i></button>
                        </div>
                        
                        <p class="text-xs text-gray-500">
                            Standard: <span v-if="set.standardLegal == false">No</span><span v-else>True</span> • 
                            Expanded: <span v-if="set.expandedLegal == false">No</span><span v-else>True</span> • 
                            Released: {{ set.releaseDate }} 
                        </p>
                    </div>
                </div>
            </div>

        </div>
    </div> 
</template>
<script>
import PokemonAPI from "@/services/api/pokemontcg"

export default {    
    data () {
        return {
            loading: true,
            sets: []
        }
    },

    created() {
         PokemonAPI.getSets()
            .then(sets => {
                this.sets = sets
            })
            .catch(error => console.log(error))
            .finally(() => {
                this.loading = false;
            })
    }
}
</script>

getTypes()

Same as cards and sets, this code will be very similar with the following as our JavaScript function:

getTypes() {

    return axios.get('https://api.pokemontcg.io/v1/types').
    then(response => { 
        console.table(response.data.types)
        return response.data.types
    }) 

}

Out of all of the templates, this is the most simple, just a list - let’s add the following to our src/components/Types.vue:


<template>
    <div>
        <div class="w-1/2 mx-auto text-center pb-20 pt-20" v-if="loading">
            Loading...
        </div>

        <div class="w-full mt-4">
            <h1 class="text-center">Types</h1>
        </div>

        <div class="flex flex-wrap max-w-screen-xl mx-auto">
            <div class="w-full" v-for="type in types" :key="type">
                <h4>• {{ type }}</h4>
            </div>
        </div>
    </div> 
</template>
<script>
import PokemonAPI from "@/services/api/pokemontcg"

export default {    
    data () {
        return {
            loading: true,
            types: []
        }
    },

    created() {
         PokemonAPI.getTypes()
            .then(types => {
                this.types = types
            })
            .catch(error => console.log(error))
            .finally(() => {
                this.loading = false;
            })
    }
}
</script>
getCard()

For getCard, you’ll need to update your JavaScript to look like the following; notice the id in the URL:

getCard( id ) {

    return axios.get('https://api.pokemontcg.io/v1/cards/' + id).
    then(response => { 
        console.table(response.data)
        return response.data
    }) 

}

There is a lot going on here such as if conditionals, looping through arrays, outputting content, etc all based on the URL route’s id value; let’s go ahead and add the following to our src/components/Card.vue template:


<template>
    <div>

        <div class="w-1/2 mx-auto text-center pb-20 pt-20" v-if="loading">
            Loading...
        </div>


        <div v-for="details in card" :key="details.id">
            <div class="lg:py-12 lg:flex lg:justify-center">
                <div class="bg-white max-w-screen-xl w-full lg:shadow-lg lg:rounded-lg flex">
                    <div class="w-1/5">
                        <div class="h-64 bg-cover bg-no-repeat lg:rounded-lg lg:h-full bg-contain" :style="{ backgroundImage: `url('${details.imageUrlHiRes}')` }"></div>
                    </div>
                    
                    <div class="py-12 px-6 w-2/5 pt-2">
                        <h2 class="text-3xl text-gray-800 font-bold"><span class="text-indigo-600">Card: </span> {{ details.name }}</h2>
                        <div class="mt-4 text-gray-600">
                            <span class="font-semibold block" v-if="details.nationalPokedexNumber">
                                Pokedex Number: 
                                <em class="not-italic font-light">{{ details.nationalPokedexNumber }}</em>
                            </span> 

                            <span class="font-semibold block" v-if="details.supertype">
                                Super Type: 
                                <em class="not-italic font-light">{{ details.supertype }}</em>
                            </span>                             

                            <span class="font-semibold block" v-if="details.subtype">
                                Sub Type: 
                                <em class="not-italic font-light">{{ details.subtype }}</em>
                            </span>                             

                            <span class="font-semibold block" v-if="details.nationalPokedexNumber">
                                Evolves From: 
                                <em class="not-italic font-light">{{ details.evolvesFrom }}</em>
                            </span>                              

                            <span class="font-semibold block" v-if="details.hp">
                                Hit Points: 
                                <em class="not-italic font-light">{{ details.hp }}</em>
                            </span>                             
                            
                            <span class="font-semibold block" v-if="details.artist">
                                Artist: 
                                <em class="not-italic font-light">{{ details.artist }}</em>
                            </span>                             

                            <span class="font-semibold block" v-if="details.rarity">
                                Rarity: 
                                <em class="not-italic font-light">{{ details.rarity }}</em>
                            </span>                             

                            <span class="font-semibold block" v-if="details.series">
                                Series: 
                                <em class="not-italic font-light">{{ details.series }}</em>
                            </span>                             

                            <span class="font-semibold block" v-if="details.set">
                                Set: 
                                <em class="not-italic font-light">{{ details.set }}</em>
                            </span>                             

                        </div>
                    </div>

                    <div class="w-2/5 pt-3 pb-5">
                        <div v-if="details.types">
                            <h3 class="text-indigo-600">Type(s):</h3>
                            <div v-for="(type, i) in details.types" :key="'type-' + i">
                                {{ type }}
                            </div> 
                        </div>

                        <div v-if="details.ability">
                            <h3 class="text-indigo-600  mt-3">Abilities:</h3>
                            <div>
                                {{ details.ability.name }} - {{ details.ability.text }}
                            </div>                            
                        </div>

                        <div v-if="details.attacks">
                            <h3 class="text-indigo-600  mt-3">Attacks</h3>
                            <div v-for="(attack, i) in details.attacks" :key="'attack-' + i">
                                <h4 class="text-gray-800">{{ attack.name }}</h4>
                                <h5>Cost:</h5>
                                <ul class="list-disc ml-5">
                                    <li class="text-sm" v-for="cost in attack.cost" :key="cost">{{ cost }}</li>
                                </ul>
                                <p class="text-sm">{{ attack.text}}</p>
                                <span class="text-sm font-bold block mt-1" v-if="attack.damage">Damage: {{ attack.damage }}</span>
                                <span class="text-sm font-bold block mt-1" v-if="attack.convertedEnergyCost">Energy Cost: {{ attack.convertedEnergyCost }}</span>
                            </div>                            
                        </div>
                    </div>
                </div>
            </div> 
        </div>

    </div> 
</template>
<script>
import PokemonAPI from "@/services/api/pokemontcg"

export default {
    data () {
        return {
            loading: true,
            card: [],
        }
    },
    created() {

        PokemonAPI.getCard(this.$route.params.id)
            .then(card => {
                this.card = card
            })
            .catch(error => console.log(error))
            .finally(() => {
                this.loading = false;
            })
          
    },
}
</script>

Ready To Go

Take a breath, save all of your work, and now run the following to see all of your work come together:

# npm run serve

You should now see a few things working; 100 cards listed on your homepage, the ability to click on a single card for details, and then be able to see all types and sets of cards within their designated routes. In the JavaScript code, I’ve left console.table in place so that you can see all fields returned for each set of data in case you want to tweak (documentation works well too - haha).

With all of that said, please give it a try, let me know if you find any bugs, if I missed anything, or if you have any issues!

:: Link to GitHub Repository: Pokemon TCG w/ Vue JS

Ideas

Some time in the near future, I possibly will add a new branch and update this post (or write a new one) on details, but the following would be the next ideas I’d recommend to building if your getting started and want something simple to try out:

  • Add in support to click on a set item and then output the appropriate cards. (/sets/:id)
  • Add in support to search based on hit points (/cards/?hp=XXX)
  • More complicated, but more fun, would be to add a deck builder w/ local storage.

Credits