David Torres
Posted on November 4, 2021
Why Build This App
Working as a web designer and developer is something I have loved since first tinkering with computers as a teenager. Even longer than that, video games have been an important hobby and part of my life. As a veteran World of Warcraft player, I had played that game for literally hundreds of days in-game.
Passion
When I look to build a side project as a web developer, I want to build something that I am passionate about. I figured a video game web app would be a great idea. Combining multiple things that I enjoy was a great motivator to keep me going and complete the project.
Practicality
Just building an app is one thing, but it should probably be useful. The idea that came to mind was a website where you could just enter your in-game characters information, and get back a list of the best gear, dungeons, quests, whatever was important to your character. Even as a veteran of the game, it's useful to have an idea of what gear to look out for. This seemed like a practical and achievable goal so I went for it!
Brainstorming & Roadblocks
Competition
There was one big problem with my idea. This basically exists already in the form of one of the biggest WoW websites: Wowhead.com. Along with them, there is no shortage of other fan-made World of Warcraft databases online. However, I felt that I wanted to invert the "searching" process inherent to these online databases. You are searching through all of the items on this site, and you can filter out what you don't want... but what if that was all done for you?
Killer Features
With the goal of character-specific feedback in a web app, I had to decide what features would be needed. Of course, you needed a way to enter your own character's information such as Name, Class, Level, etc. I also wanted it to be possible to create multiple characters, as well as edit, or delete them! You should also be able to "choose a character" and see what gear would be best for them, or what zone to go to next. This flips the database-model on it's head and tailors the data specifically to what's important to the player.
Data Gathering
If there was one massive hurdle to overcome in this project, it was realizing and actually gathering all of the data I needed to make this happen. Wowhead.com exists, in part, because third-party websites used in-game addons to aggregate what items existed in the game, their stats, where they drop from and many important data points. This was not easily available in the game source code itself (at least not for my skill level!)
item_data = [];
elements = document.getElementsByClassName("listview-row");
elementsArray = Array.prototype.slice.call(elements, 0);
cells = elementsArray.forEach((e) => {
let cellArray = Array.prototype.slice.call(e.cells, 0);
cellArray.map((e, index) => {
if (index === 2) {
let nameCell = e.children[0].childNodes[0];
console.log(nameCell);
let id = nameCell.pathname && nameCell.pathname.match(/\d/g).join("");
!nameCell.text.includes("Test") && item_data.push(parseFloat(id));
}
});
});
item_data;
Due to the specificity I needed in what data I needed, I opted to manually scrape lists of items based on my heavily filtered Wowhead searches. To gather data on thousands of items, into specific lists, it took many hours of Javascript scraping. My aim was not to "recreate" a database, but to curate sorted lists of items with any relevant item data.
Building with React & Next.js
Create-React-App Beginnings
I originally started this project, before World of Warcraft Classic was released mid-2019. The idea was this would be available to people who haven't played in over 10 years, or ever. The first version I worked on would basically "create" Wowhead search URLs for the user. This was a compromise to not have to actually build my own database... But this would have been a bad user experience, every click takes you off of my site!
Since this was a side project, I ended up never completing version 1.0, my original vision of the project. Other life priorities took over and this got put on the back burner until March of 2021, shortly before WoW Burning Crusade Classic was to be released.
Next.js
A year and a half later, I was a bit more confident in my React skills, and had actually just rebuilt my own development portfolio using Next.js. Realizing the upgrade that Next gave me, and over a year since I last worked on the project, I decided to scrap the whole thing! I salvaged what I could from the original build and started fresh on Next.js.
Data & SEO
With the data fetching, static rendering, and other features now taken care of for me, I was more comfortable tackling the problem of having my own data store for this project. I wanted you to be able to land on a static class guide page, for example. To do that, I still needed to build my data base and actually get the data to my Next app.
FaunaDB & GraphQL
Learning FQL
My build has been focused on the front-end, with a heavy use of the Jamstack for deployment. When looking for a database solution I came across FaunaDB. It fits right into that mold of being a modern "serverless" database I can leverage for my use case. One caveat, I now had to learn how to interact with a database like Fauna, and specifically Fauna Query Language (FQL). I had very little prior experience with databases at all, so this was completely new to me.
Call(Function("addItemArray"), [
{
armorType: "Plate",
imgURL: "inv_chest_chain_15",
quality: "Epic",
name: "Justicar Breastplate",
id: 29071,
iLvl: 120,
reqLvl: 70,
slot: "Chest",
drop: "Magtheridon - Magtheridon's Lair",
phase: "1"
}
])
Well, I was able to use it well enough to build out what I needed for my site. I began using FQL to create the structure I needed. One gets comfortable after running thousands of "item" uploads into a database.
Data Entry and Indexing
With so many items to enter, I had to create functions to easily add multiple items (like the one in the code above). I also had to get that data out of the database using Indexes and Functions like this one:
Query(
Lambda(
["playerClass"],
Let(
{
gearDoc: Get(Match(Index("gearByClassName"), Var("playerClass"))),
gearList: Select(["data", "gear"], Var("gearDoc"))
},
Map(
Var("gearList"),
Lambda(
"itemID",
Let(
{ itemRef: Match(Index("itemByID"), Var("itemID")) },
If(Exists(Var("itemRef")), Get(Var("itemRef")), false)
)
)
)
)
)
)
With that I was able to build out the data and relationships between Items, Classes, Races, and Factions in the game.
Implementing GraphQL
Built into FaunaDB is GraphQL. Actually, this was made very easy thanks to Fauna, it auto-generates GraphQL schema based on the Collections, Indexes, and Documents you add to the database, all I had to do was write out the queries for the data I wanted. This one, actually required that Function shown earlier as a resolver to get it done.
const queryItemsByClassName = (className) =>
`{
getItemsByClass(class: "${className}") {
armorType
drop
faction {
name
}
id
iLvl
imgURL
name
phase
quality
reqLvl
slot
wepType
}
}`;
While I had not used GraphQL before this project, I enjoyed it. Accessing the data was easy from Next.js and I could parse out the data however I wanted on the client-side.
Blizzard API
API History
Blizzard already had a public API available in 2019 when I began this project. At first, that was my initial plan: use the Blizzard API, get my item data, done! Wrong. There was, and currently isn't a way to get a "list" of items back from the API. You can query specific item ID's... one by one. If you don't know what item you're looking for, you would be forced to run a throttled script to gather item data based on random/ascending numbers then clean up and sort the items afterwards.
By the time I had progressed the project into 2021, the API now has more functionality including Classic Vanilla and Burning Crusade-specific endpoints and more relevant item data. This still included the limit of querying one item at a time though so I had to build out a data fetching strategy that would work with this public API.
Next.js Implementation
To interact with the Blizzard API, I used Next.js getStaticProps() to authorize my app. This authorization would then be revalidated on the front-end with useSWR() and Next.js API routes.
import fetch from "isomorphic-unfetch";
export default async function useBlizzAuth() {
const blizzardID = process.env.NEXT_PUBLIC_BLIZZ_ID;
const blizzardSecret = process.env.NEXT_PUBLIC_BLIZZ_SECRET;
const auth = Buffer.from(`${blizzardID}:${blizzardSecret}`).toString("base64");
const tokenResponse = await fetch("https://us.battle.net/oauth/token", {
body: "grant_type=client_credentials",
headers: {
Authorization: `Basic ${auth}`,
"Content-Type": "application/x-www-form-urlencoded",
},
method: "POST",
});
const accessBlizz = await tokenResponse.json();
return accessBlizz;
}
const blizzFetch = (url) => fetch(url).then((r) => r.json());
const { data: blizzToken } = useSWR("/api/blizzauth", blizzFetch, {
initialData: props.accessBlizz,
refreshInterval: 86399,
});
With that, I could call Blizzard's API for any item I wanted, but I don't want to just load all of the items for a given character, until the user is actually looking for it.
Lazy Loading Item Data
This solution involved only showing the item data I have in my own database at first, then if the user expands the item data, then call Blizzard's API for the details. My component ended up looking something like this:
const getItemData = (item, token) => {
const url = "https://us.api.blizzard.com/data/wow";
const namespace = "?namespace=static-classic-us";
const locale = "&locale=en_US";
const { data, isPending, error } = useSWR(
`${url}/item/${item}${namespace}${locale}&access_token=${token}`,
fetcher
);
return { data, isPending, error };
};
const ToggleItemStats = ({ id, drop, itemFaction, faction, reqLvl }) => {
const { token } = useContext(BlizzContext);
const { data: itemBlizzData, isPending, error } = getItemData(id, token);
return (
<>
{error && <small>Error loading item stats.</small>}
{isPending && <small>Loading item data...</small>}
{itemBlizzData ? <BlizzStats data={itemBlizzData} reqLvl={reqLvl} /> : null}
{drop && (
<h4>
Dropped by:
<br />
{drop}
</h4>
)}
{itemFaction && (
<img
className="faction-icon"
src={`${factionURL}${faction.toLowerCase()}.png`}
alt={`${faction} Only`}
title={`${faction} Only`}
/>
)}
</>
);
};
Perfect, now we're only loading single items when the user requests them. Of course, then I would have to parse the data in <BlizzStats>
to mimic the way item stats are displayed in-game.
Framer Motion
Why Add Animations
Games are, by definition, interactive and heavily rely on animations, both 3D and 2D. With that in mind, and the quality standards of the modern web, I needed to add in animations to the site. I looked into multiple animation libraries like GSAP and Frame Motion, and ended up using the latter. This would mean I could smoothly transition elements, page changes, and more, to provide a better user experience.
Page Transitions
Luckily, it's fairly easy to implement page transition animations with Framer Motion and Next.js. Basically, I just use <AnimatePresence>
from Framer Motion in order to animate the exit and enter of page routes. Check out this example:
/* _app.js */
import { AnimatePresence } from "framer-motion";
const MyApp = ({ Component, pageProps }) => {
return (
<>
<Head>
<title>WoW TBC Classic Character Guide</title>
</Head>
<Navbar {...pageProps} />
<SWRConfig value={{refreshInterval: 3000000}}>
<AnimatePresence exitBeforeEnter>
<Component {...pageProps} key={router.route} />
</AnimatePresence>
</SWRConfig>
<Footer />
</>
);
};
export default MyApp;
/* index.js */
import { motion } from "framer-motion";
const Home = () => {
/* ---- Motion Variants ---- */
const transition = {
duration: 0.3,
ease: "easeInOut",
};
const pageVariants = {
exit: { opacity: 0, transition },
enter: {
opacity: 1,
transition,
},
initial: {
opacity: 0,
transition,
},
};
return (
<motion.section
key={"home"}
initial="initial"
animate="enter"
exit="exit"
variants={pageVariants}
>
{/* Page Content... */}
</motion.section>
);
};
export default Home;
Stagger Items
Another effect I was looking for was to stagger the entry and exit of items in a Gear list or Zone list. Again, this is made easy with Framer Motion. I was pretty happy overall with that library, here's another example of it in action, animating the Gear Slots on my Gear page.
const slotVariants = {
animate: {
opacity: 1,
scale: 1,
transition: {
staggerChildren: 1,
delayChildren: 0.5
},
},
initial: {
opacity: 0,
scale: 0.8,
},
transition: {
duration: 0.5
},
};
<motion.div
key={type}
variants={slotVariants}
animate="animate"
initial="initial"
transition="transition"
layout
>
{items.map((itemType, index) => {
const typeName = Object.keys(itemType)[0];
const weaponTypeMatch =
type === "Weapons" &&
classWepTypes.some(
(wepType) => wepType.name === weaponToFullName(typeName)
);
if (weaponTypeMatch || type !== "Weapons") {
return (
<GearSlot {...props} />
);
}
})}
</motion.div>
Conclusion
What I Learned
My goal with this project was to have a successful web app that I could put on my portfolio and be proud of. The byproduct I got was all of the learning I had to do to finish the project. From the first time I tried to scrape HTML data, to deploying a Jamstack site, to learning FaunaDB and GraphQL, working with a public API like Blizzard's, and even implementing design aspects like Framer Motion... it was all a massive learning experience.
Even if nothing else happens with the site, I will have learned so much and boosted my confidence as a developer by building out my own app!
Going Forward
I plan on sharing the site with the general World of Warcraft community, wish me luck there! Some other features I thought of could be automatic character data import, login and saving characters to an account, zone & dungeon guides, and even character talent trees. Many of these features are just ideas, but they could one day become reality.
Want to visit the site? It can be found at wowcharacter.guide, feel free to check it out! I welcome any feedback I can get, especially from a technical standpoint.
Thanks for reading all about how I built a WoW Web App, here's to many more!
World of Warcraft Developer Portal
Next.js
FaunaDB
Framer Motion
Posted on November 4, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.