DEV Community

Nina
Nina

Posted on

Using Reddit's API to find Pokemon Trades

Project Here

Literally all my projects since starting React either are pokemon based or use pokemon art. does she like pokemon maybe??

An odd thing I've found out through coding is that sorting through data is kinda fun? It just so happens that pokemon contains lots of fun data tidbits to sort with, and match to other things. So once again, I am back with an API and some pokemon, but this time we're using Reddit's API.

Goals

  • Get data from various subreddits
  • Learn what Reddit's api terms are
  • Display what pokemon are mentioned in the title and post
  • Search by through trades by pokemon

Did we get all of them? Let's see what happened.

Reddit?

Reddit is a place on the internet, comprised of user run subreddits. There's a subreddit for everything, in fact there are probably multiples of each that all have differing rules. For this project, I want subreddits that are dedicated to the trading of pokemon, are updated frequently, and use similar pre-fixes. For the three subreddits I chose, they use LF (Looking For) and FT (For Trade) in most all of their lookups. Now we move on to how we use reddit's api. Let's break down my first call.

 axios.get(`https://www.reddit.com/r/pokemontrades/search.json?q=LF&q=FT&limit=30&restrict_sr=1&sort=new`)
Enter fullscreen mode Exit fullscreen mode

Boy that's long. Easy part:

  `https://www.reddit.com/r/pokemontrades/search.json?`
Enter fullscreen mode Exit fullscreen mode

We go through reddit, then we specify the subreddit, and then initiate a search to come up with a .json file with all out info.

  `/search.json?q=LF&q=FT`
Enter fullscreen mode Exit fullscreen mode

We ask two question of the search. Does it contain LF? Does it contain FT? If so great. We could also not query and get all posts, but we want to weed out if there's something not following the format to ask a question or mod updates.

  `limit=30&restrict_sr=1`
Enter fullscreen mode Exit fullscreen mode

There's a harder limit of 100, but I didn't really want people to pour through 300 entries total, so we're only going to pull 30 per subreddit, for a total of 90 listings. The nice thing about the limit is that this is all one api call! The next part, we restrict our search to the specific subreddit we've defined. Why? I don't know. It looks in all subreddits if you don't, which we really don't need.

  `&sort=new`
Enter fullscreen mode Exit fullscreen mode

If you're familiar with reddit, this one is easy. Reddit has modes to sort by top of all time, hot for a current time, and then by newest, among others. We want the freshest trades possible, so we want to get them not by relevancy, but newest.

What's in this data?

A lot. And not enough, unfortunately. We can go through the data, into the 30 children that have been requested and get tons of data! We can get the post title, body, flairs, mod actions, and...

Not the date?

The created field's date is when the request was made. As in I just requested it, so that's the created date, or created utc. From what I researched there is no timestamp given to posts available to the api. This threw a wrench in my plans.

The Original Plan

I was going to use queries to look up pokemon related entries! So instead of normal query, we would have this:

axios.get(`https://www.reddit.com/r/pokemontrades/search.json?q=Vulpix&limit=30&restrict_sr=1&sort=new`)
Enter fullscreen mode Exit fullscreen mode

And that does work. It will gather all the posts it can find about vulpix in that subreddit up to 30. Some of them are months old though. While I've had practice in simply serving up data, as a more static resource, I wanted this to be at least somewhat useful. Sure, I could search specific pokemon, but without a way to have a cutoff date it's just white noise to make it look more robust then useful.

The New Plan

Cache the data! Not really like cache in the local browser as for once our content is dynamic, but take our original call and use that data. So instead of finding a bunch of old stuff about some pokemon, you see which pokemon is mentioned in the newest 90 threads.

return general.map((listing, i) => {
        let post = listing.data;
        let pkmnMentioned = this.getPKMNMentioned(post);
        if (pkmnMentioned.includes(pokemon, i)) {
          return <Entry
            key={post.title + i}
            subName={post.subreddit_name_prefixed}
            title={post.title}
            url={post.url}
            text={post.selftext}
            searchPokemon={this.state.searchPokemon}
            setSpecificAndPokemon={this.setSpecificAndPokemon}
          />;
        } else {
          return "";
        }
      });
Enter fullscreen mode Exit fullscreen mode

The key is important to use a unique identifier, which is turns out the title itself is not, as during testing someone double posted and it really messed up my rendering. Lesson learned. searchPokemon is a state variable that holds which pokemon we're currently searching for so we can pass that down and highlight the sprite we're looking for. We also pass down the ability to set that state and a bool if we're searching for a specific pokemon or not.

I originally had the Entry component determine which pokemon was mentioned, but as seems the case with React, you usually find a reason to move your handling up a level. The getPKMNMentioned just runs through the pokemonArray (an array of all pokemon, seriously it's long) and searches both the title and the self text (the body) to see if the pokemon comes up in it's data. Search returns the number of where the data is, and -1 if it's not in there, so we just see if the number is greater then -1 to push to the pokmemon mentioned array. Since most of the mentioning goes on in the body, I decided to do that first, and if the body doesn't return that particular pokemon, it will then do a search for the pokemon in the title so we don't double up and cover our bases.

{this.state.general[89] ? listings : "Loading..."}
Enter fullscreen mode Exit fullscreen mode

As a quick note, because we know that we're only searching for 90 entries, we can use that as a determination of if we're done loading the three calls we do. I like this api. Note of a note, I don't actually write this.state.value for everything, I just have it in the article code so you can see what's going on. I should do a more involved loading animation, but it seems too trivial compared to how long it actually takes to load.

The Entry Component

Since we took out search for pokemon mentioned out of our Entry (or rather, I forgot to actually take it out as of writing this, thanks reviewing code!) is rather sparse. The most important part is that the name for our images gets rid of special characters and decapitalizes them, so we have to do a bit of RegEx magic to get that to work. A little extra magic to turn what is &amp; in the self text back into an &.

import React from 'react';
import Sprite from './Sprite';

export default function Entry(props) {
  props.pkmnMentioned.forEach(pokemon => {
    let spriteName = pokemon
      .replace(/\s+/g, '-')
      .replace(/[.,':\s]/g, "")
      .replace(/♀/g, "-f")
      .replace(/♂/g, "-m")
      .toLowerCase();
    spritesMentioned.push(
      <Sprite
        key={pokemon}
        fullName={pokemon}
        name={spriteName}
        isSearched={pokemon === props.searchPokemon}
        setSpecificAndPokemon={props.setSpecificAndPokemon}
      />
    );
  })

  return (
    <div className="entry">
      <div className="subname">{props.subName}</div>
      <a
        href={props.url}
        target="_blank"
        rel="noopener noreferrer"
        className="title"
      >
        {props.title.replace(/(&amp;)/g, "&")}
      </a>
      <div className="sprites-container">
        <h3>Pokemon Mentioned:</h3>
        <div className="sprites">
          {spritesMentioned.length ? spritesMentioned : "None"}
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

rel="noopener noreferrer"

Let's take a minute and acknowledge the things I do because React yells at me if I don't. If I open in a target blank, React tells me that doing that without rel="noopener noreferrer" is a security risk. Let me google for a moment and maybe we'll all learn something new.

"The page we're linking to gains partial access to the linking page via the window.opener object.". So it's possible to insert javascript on what looks to be a legit link on hover to redirect to a bad site. The most interesting part of that article is that google just accepts this as "inherent to the current design of web browsers and can't be meaningfully mitigated by any single website".

The Sprite Component

Even smaller then the Entry is the Sprite component. It's possible to just insert this into Entry without it being it's own component, but I feel like the Sprite display is an important enough job to warrant it's own component.

import React from 'react';

export default function Sprite(props) {
  return (
    <div
      className={`circle ${props.isSearched ? 'highlight' : ""}`}
      key={props.name}
      onClick={() => props.setSpecificAndPokemon(true, props.fullName)}
    >
      <img
        src={`${process.env.PUBLIC_URL}/regular/${props.name}.png`}
        alt={props.fullName}
        title={props.fullName}
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the full version, you can select which pokemon to search through via drop-down, but I also wanted to be able to just click on the sprite to search for more with those pokemon as well. We keep the full name for the alt text, title (so you can hover over if you forgot the name of the pokemon but know what it looks like), and most importantly so when we change our search terms, we use the proper name of the pokemon. The circle class and highlight class give us the look for the circles I put around them, and if that pokemon is the current one being searched.


    circle {
      background-color: rgba(white, 0.3);
      border-radius: 50%;
      margin: 5px;
      cursor: pointer;
    }

    .highlight {
      background-color: rgba($link-color, 1);
    }
Enter fullscreen mode Exit fullscreen mode

Small Return vs. Bloat

I've started doing a thing, which I'm not enitrely sure is good? I want small return statements in my render.

    return (
      <div id="app">
        {this.state.specific ? lookSpecific : all}
        {searchAll}
        <div id="listing-container">
          {this.state.general[89] ? listings : "Loading..."}
        </div>
      </div>
Enter fullscreen mode Exit fullscreen mode

That's nice. However, it's preceded by this:

    const { general, specific, searchPokemon } = this.state;
    let listings = this.listings(specific, searchPokemon) || 0;
    const all = <header>Searching last 30 results of 3 subreddits...</header>;
    const lookSpecific = <header>Searching last 90 results for trades containing {searchPokemon}...</header>;
    let cantFind = listings.length ? "" : "Couldn't find any trades!"
    const lookups = pokemonArray.map((pokemon, i) => {
      return (
        <option
          key={pokemon + ` option`}
          value={pokemon}
        >
          {`${pokemon} #${i + 1}`}
        </option>
      );
    });
    const searchAll =
      <select
        onChange={e => this.setSpecificAndPokemon(true, e.target.value)}
      >
        <option
          key='none'
          value={null}
          onClick={() => this.setSpecificAndPokemon(false, null)}
        >
          None
        </option>
        {lookups}
      </select>
Enter fullscreen mode Exit fullscreen mode

The alternative is to just define it in the return statement itself, but then you'd get something like this: which is hard to read in my opinion.

    const { general, specific, searchPokemon } = this.state;
    let listings = this.listings(specific, searchPokemon) || 0;

    return (
      <div id="app">
        {this.state.specific ?
          <header>Searching last 90 results for trades containing {searchPokemon}...</header>
          : <header>Searching last 30 results of 3 subreddits...</header>}
        <select
          onChange={e => this.setSpecificAndPokemon(true, e.target.value)}
        >
          <option
            key='none'
            value={null}
            onClick={() => this.setSpecificAndPokemon(false, null)}
          >
            None
    </option>
          {pokemonArray.map((pokemon, i) => {
            return (
              <option
                key={pokemon + ` option`}
                value={pokemon}
              >
                {`${pokemon} #${i + 1}`}
              </option>
            )
          })}
        </select>
        <div id="listing-container">
          {this.state.general[89] ? this.listings(specific, searchPokemon) || 0 : "Loading..."}
        </div>
      </div >
    );
  }
Enter fullscreen mode Exit fullscreen mode

Is it wrong to separate them like this, or should the render statement just read in order of what's in it? Using an immediately returning function for if statements is pretty ugly, but so is using really long ternary operators.

   {(() => {
     if (ugly) {
       return <div> Man this is {`ugly`}.. </div>
     } else {
       return <div> But it's more informative? </div>
     }
   })()}
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

I feel like I've done variations of the same project three times now, but I did learn something each time. At least for me, coming up with good ideas that are within my reach is hard. I have ideas thanks to friends for some games, but I'm hesitant to jump into React for game purposes, though I think it will help my React skills quite a bit. I guess the ideas are a bit too large, so I need to find a bite sized game idea. Or stop doing React and make Wordpress themes for that specific experience but that's just not as fun. Maybe, just maybe, I'll do something not pokemon related next time.

Top comments (2)

Collapse
 
krmaxwell profile image
Kyle Maxwell

That issue about the API not including the post date really surprised me! Wonder what drove that decision by Reddit? Regardless, thanks for this post. It gave me some similar ideas around Magic the Gathering....

Collapse
 
misnina profile image
Nina

Thank you so much! :) Glad you like it.