Let's Build a Sports Team Sentiment Analyzer Using Next.js, Twitter, and Machine Learning
Tags: machinelearning, apis, twitter, nextjs
Published: 2020-05-18 17:27:24

---
title: Let's Build a Sports Team Sentiment Analyzer Using Next.js, Twitter, and Machine Learning
published: true
description: I built an app that analyzes tweets about sports teams so that I could better learn Next.js, the Twitter API, and put some Machine Learning to good use.
tags: Machine Learning, APIs, Twitter, Next.js
---

I love React. Love it. It has always felt like the cool kid at the lunch table. Mature, smart, not so opinionated to make you frustrated. But I keep hearing about this new kid in school. This cooler, smoother new kid. A new kid with shoes that make her run faster and jump higher. That new kid is [Next.js](https://nextjs.org/). The best thing you can do when someone new comes to school is learn more about them. So, let's do that. 

Before we get started, let's talk about what you'll actually build in this tutorial—which, by the way, is a tutorial for me too since I've only ever experimented with Next.js. I thought it'd be interesting to combine a few useful learning paths into this tutorial. Because I'm a long-suffering San Diego Padres fan, I want to build a sentiment analysis app that takes tweets about the Padres (or any other team) by week and analyzes the sentiment of the tweets. To do this, we're going to use Next.js, the Twitter API, and Machine Learning. 

Here's what you'll need to get started: 

* Node  
* NPM  
* A text editor
* A team you want to analyze. 

Pretty simple, huh? Let's dive in. 

### Getting Started With Next.js

Next.js is well-documented, so feel free to go through their Getting Started guide and their tutorial to get your feet wet. Or, if you're feeling dangerous, just jump right in here. We need to create a directory in our development folder for our new project. You can call this directory whatever you'd like, but I'll be calling mine `team-sentiment`. It's also worth noting that I'm working on MacOS, so some of the terminal commands may be slightly different for Windows command line.

Create the directory: `mkdir team-sentiment`

Change into that directory: `cd team-sentiment`

Cool, first two were super easy! Now, let's create a Next.js project. Fortunately, that's super easy as well. 

`npm init next-app`

You'll be presented with two prompts: providing a project name and deciding if you want to start with the default template or choose one from the Next.js repo. 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/g5y9id1zzmr03q34wylp.png) 

Keep it simple. Just name the project the same thing you named your project directory, and choose the default template. That's it! Next.js is installed and ready to go. 

Now you will switch into the Next.js project (same name as the parent folder): `cd team-sentitment`. From there, we can test whether or not Next.js installed properly. Run `npm run dev` to launch the server and serve the app on localhost:3000. Open up localhost:3000 in your browser and you should see this: 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/pxd94rwqccnvt7s2q0wz.png)

We're not going to go into too much detail about the inner workings of Next.js in this tutorial. We just want to get up and running with it and poke around a bit. But I think it's important to take a look at the file structure and what you get out of the box. So, let's open our project in a text editor. You should see something like this: 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/apupu821sqalk8ewucks.png)

There is a `pages` folder and an `api` folder right out of the box. That's pretty awesome. The pages folder houses any pages for your app. Think of this like you would a `components` folder in React. The `api` folder houses all of your api routes, much like you might find in an Express app. You'll notice there is already a test route in the api folder. The file is called `hello.js` and the convention for accessing api routes in Next.js is to go to `root_domain/folder_path/route`. In this case, it will look like `http://localhost:3000/api/hello`. Give it a shot and see what you get back. 

Did you get `{"name":"John Doe"}` printed? Pretty cool right? To prove this is a true, working API, let's try it in [Postman](https://www.postman.com/) (you can simply use curl on the command line/terminal if you prefer). If I make a GET request to that route in Postman, I get this: 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/z5gwsvtr0jdytb43m5kh.png)

Alright, this is pretty great. But we need to get to work now. If you look back at the pages folder, you'll find `index.js`. This is the root of your html file. All other pages will be rendered within here just like you'd expect from React. Now we want to create a folder to house our sub-pages (there will only be one). 

Create a folder called `sentiment`. Then create a file within that folder called `team-sentiment.js`. Within that file, we're going to create a very basic React function component like this: 

```
export default function TeamSentiment() {
  return (
    <div>
      <h1>Here's the sentiment about your team</h1>
    </div>
  )
}
```

Save that and try going to `http://localhost:3000/sentiment/team-sentiment`. You should see your text rendered on screen. Now, if you're familiar with React, you're familiar with hot-reload. Next.js has that too. Chance the `h1` to say "Your team's sentiment". 

One cool thing about server-side rendering (which Next.js provides) is that your hot-reload doesn't involve a window refresh on the client. 

This page is what we will use to output the results of the sentiment analysis for our chosen team (poor poor me and my Padres). But how do we get to this page? We will use the built-in router from Next.js. To do so, let's go back to the root `index.js` file in the pages folder. At the top of the file, add the following: `import Link from 'next/link'`. 

Then, let's run a quick test to make sure the router works. Find the line that says `Welcome to <a href="https://nextjs.org">Next.js!</a>`. Change that to: `Check out how people are feeling about <Link href="/sentiment/team-sentiment"><a>your team.</a></Link>`.

If you click the link, you should be navigated to the team sentiment page we created previously. Let's add a link there back to the front page. In the `team-sentiment.js` file, add `import Link from 'next/link'` at the top of the file, then add this: 

`<Link href='/'><a>Try another team</a></Link>`

Now you can navigate back and forth! We can start to build the mechanism for entering our team name and getting the sentiment analysis results. Let's do this in our `index.js` file from within the root `pages` folder. Note: we're not going to be doing any styling in this walkthrough, so feel free to make it as pretty as you'd like on your own. 

Let's think about what we need the front page to do. We need it to: 

* Welcome the person
* Prompt the person to enter a team name 
* Validate the entry 
* Redirect to the team's sentiment score page 

With that in mind, go ahead and clear out everything between the two `<main>` tags. We'll also remove the the entire `<footer>`. Replace all of this with the following: 

```
<main>
  <div>
    <h1>Welcome! Let's see  how the world feels about your team.</h1>
    <form>
      <label>Enter a full team name</label>
      <div>
       <input type="text" placeholder="San Diego Padres" />
      </div>
      <div>
        <button type="submit">Check on Team</button>
      </div>
    </form>         
  </div>
</main>
```

Your page should look like this now: 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/9c5oscyhu2ceo0ov6h5v.png)

This is simple HTML within React, otherwise known as JSX. Nothing out of the norm here. We just need to wire this simple form up to take in the data needed by the API and redirect the user to the team sentiment page.

To do this, we're going to make use of React Hooks. Let's start by importing `useState` from React. Add this to the top of your file: 

`import { useState } from 'react'`

Now, right after your `Home` function, let's use `useState` like this: 

`const [teamName, setTeamName] = useState('');`

We are initializing the `teamName` state to an empty string and we are creating a function called `setTeamName` that will be used to update the state. Now, we need to update our form a little bit: 

```
<form onSubmit={handleSubmit}>
    <label>Enter a full team name</label>
    <div>
     <input type="text" value={teamName} onChange={handleTeamInput} placeholder="San Diego Padres" />
    </div>
    <div>
      <button type="submit">Check on Team</button>
    </div>
</form>
```

We've added an `onSubmit` handler to the form and an `onChange` handler to the input field. This means that when the submit button is clicked, the onSubmit function will be called, and when typing in the input field, we can capture that input. Each of those handlers references a function that we have yet to write. You'll also note that the input field has a `value` attribute initialized to the `teamName` state. 

We can now write the functions our two event handlers are referencing. The first one, `handleTeamInput` is pretty simple: 

```
 const handleTeamInput = (e) => {    
    setTeamName(e.target.value);
 }
```

We are taking the input text and setting it to our `teamName` state variable by calling the function we defined earlier, `setTeamName`.

Now, we can wire up the submit function. We want to do a few things here. We're going to implement some *very* basic validation. We want to check that the team name is greater than 5 characters. You can do significantly more validation here including referencing public APIs of all possible team names. We're not doing that for this lesson, though. So, here's what the function is starting to look like: 

```
const handleSubmit = (e) => {
  e.preventDefault();
  //  Validate the entry is a string long enough
  if(teamName.length > 5) {
    //  Post the data
  } else {
    console.log("That doesn't appear to be a valid team name");
  }
}
```

We need to prevent the default functionality of submitting a form (which refreshes the page, so we have that at the top of the function. Then, we are doing a simple validation check and if it fails, we console.log the error. In a real application, we'd want to provide some information in the UI for the user, but again, that's outside the scope of this walkthrough. 

Now, how are we going to post the data? There are a few ways to handle the data that will eventually be returned from the server, some more efficient than others. For simplicity, I'm going to have the form submission redirect us to our team sentitment page with a query string parameter. So, we'll end up with something like `http://localhost:3000/sentiment/team-sentiment?teamName=San%20Diego%20Padres`. Then on our team-sentiment page, we can make our data request.

Again, I'm choosing this for simplicity of the walkthrough. Rather than managing global state, passing props, and other complex handling of data returned from the API, I want us to just get to the point where we can fetch and server the results easily. 

Let's wire up the form submission now. First, we'll need to use Next.js's built in router by importing it at the top of our file: `import Router from 'next/router'`. Then, in the `handleSubmit` function, you should update it to look like this: 

```
const handleSubmit = (e) => {
  e.preventDefault();
  //  Validate the entry is a string long enough
  if(teamName.length > 5) {
    //  Post the data
    Router.push(`/sentiment/team-sentiment?teamName=${encodeURI(teamName)}`);
  } else {
    console.log("That doesn't appear to be a valid team name");
  }
}
```

You can see that I am using a template string to represent the path we will be redirecting to, and I am using JavaScript's built-in `encodeURI` function to encode the team name, which will have spaces, to proper URL format. 

Go ahead and save all this and test it out. Enter a team name, click the submit button, and make sure you're taken to the team-sentiment page with a query string parameter in the URL. 

Did it work? Great! Now we can set up our team-sentiment page to handle the API request, parse the data, and display it on the screen. We'll work on the first two items there, then we'll switch to our API code (which we haven't touched up until this point, and then we can come back for the rendering of the data returned. 

In your `team-sentiment.js` file, you'll need to choose how you'd like to make requests to the server. I like to use Axios, so I'll use that, but you can use any http request library you'd like. I'm going to install Axios like this `npm i axios`. Once installed, you can require that at the top of the file: `import axios from 'axios'`. 

While we are importing dependencies, let's go ahead and import the `useEffect` and `useState` dependencies from React: 

`import { useEffect, useState } from 'react'`

Before we move on, let's think about what we need to do on this page. We were just redirected here from the main page. We haven't yet made a request to the API, but we have the necessary information to make the request. The response won't be instant, so we should show the user some indicator of the work being done. 

I think we'll start by creating a `loading` state variable. You can do that right within the function component like this: 

```
export default function TeamSentiment() {
  const [loading, setLoading] = useState(true);
  return (
    <div>
      <h1>Your team's sentiment</h1>

...
```

We want to start by initializing the page as loading since we know we still have to make the API request and get our data. So, let's update our component to handle the loading state: 

```
export default function TeamSentiment() {
  const [loading, setLoading] = useState(true);
  const [teamName, setTeamName] = useState('');

  if(loading) {
    return (
      <div>
        <h1>Checking on all the feels for the {teamName}...</h1>
      </div>
    )
  } else {
    return (
      <div>
        <h1>Your team's sentiment</h1>
        <Link href='/'><a>Try another team</a></Link>
      </div>
    )
  }
}
```

You'll note a couple of changes here. I have updated the component to render conditionally based on the `loading` state variable. I have also created a new state variable called `teamName` so that we can use it both in the UI and for fetching data. 

We need to trigger the request to the API now. This should happen as soon as the redirect to the team-sentiment page is complete, and we'll use the `useEffect` hook for that. Right above the conditional statement, add this: 

```
useEffect(() => {
  //  Get the team name from the URL
  try {
    const encodedTeamName = window.location.href.split('=')[1];
    const teamName = decodeURI(encodedTeamName);
    setTeamName(teamName);
  } catch (error) {
    console.log(error);
  }    
}, [])
```

There are a lot of ways to parse query string parameters, but I'm going to take the quickest and easiest way by just splitting the URL. Once we have the URL-encoded team name, we can decode it and set state. The `useEffect` hook, when passed an empty array at the end of the function, acts very much like the `componentDidMount` lifecycle event in class components in React. So when the page mounts, we will grab the teamName and set state. We should be able to test this before sending any data to the API. Save everything, go back to the home page, enter a team name, and submit. 

You should see something like this: 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2mlpu36uztqfka68t3q0.png) 

We are now ready to post our request to the API (which, again, we haven't set up yet). We will make our request when the page loads, so we need to do two things to get ourselves started. We need to create a function that will handle the request, and we need to call that function from within the `useEffect` hook we already created. 

```
  useEffect(() => {
    //  Get the team name from the URL
    try {
      const encodedTeamName = window.location.href.split('=')[1];
      const teamName = decodeURI(encodedTeamName);
      setTeamName(teamName);

      checkTheFeels(teamName);
    } catch (error) {
      console.log(error);
    }    
  }, []);

  const checkTheFeels = async (teamName) => {
    //  This is where we'll make our request
  }
```

In the function we created to make the request to the API, we will utilize Axios (or whatever you chose to use). We want to send the team name, wait for a response, then eventually use the data returned in the response to update our page. Sounds to me like we need to create a new state variable to hold the response data. At the top of the component function, add this: `const [sentiment, setSentiment] = useState([])`

We are initializing that state variable to an empty array because I expect the data to be returned in array form. With that in place, we can set up the function to make the request. 

```
const checkTheFeels = async (teamName) => {
  //  This is where we'll make our request
  try {
    const payload = {
      teamName
    }
    const res = await axios.post('/api/team', JSON.stringify(payload), { headers: { 'Content-Type': "application/json"}});
    setSentiment(res.data);
    setLoading(false); 
  } catch (error) {
    console.log(error);
  }
}
```

You can see we are setting the response to the payload state and we are updating the loading state to `false`. None of this matters yet since we haven't created our API route, so I think it's time to do that now. 

### Getting Started With The API

If you remember, Next.js has already set us up with an API and routing. So, half the work is already done. Let's find the `pages/api` folder. Inside that is the `hello.js` file. Go ahead and rename that to `team.js`.

Inside that file, we're going to do a few things. Let's outline them here: 

1. Send the team name to the Twitter search API
2. Parse the results for the tweet content (we don't need the names of the people who tweeted) 
3. Takes the parsed content and send it to our Machine Learning API (more on that in a bit)
4. Convert the sentiment analysis into something usable that can be returned to the client

We'll take this all in order. Step One requires the Twitter Search API, so we will need to log into Twitter and register a new application. To do that, go to [Twitter's App Developer Page](https://developer.twitter.com/en/apps). Once there, you will need to click the Create an App button. You'll be taken to a page that looks like this: 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ge56tswk4a9uqxstp90w.png)

You will need to fill in all of the required fields. Here are some tips: 

* Name: Give this a unique name
* Website URL: This will need to be a real URL. You can use your personal site for this. 
* App Usage: Make sure to write a detailed description here. You can summarize the walkthrough and that you are learning how to use the API.

That should be all the required fields. Once you're done with that and have created your app, you should see a page like this: 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ka0cvahs41hybnq0l25y.png)

The keys and tokens tab will be important. We're going to need information from there. But first, let's go back to our project and install another dependency. To make using the Twitter API simpler, I like to use a module simply called Twitter. To install it, run `npm i twitter`. 

Once installed, we will bring it into our project in our `team.js` file. That file should now look like this: 

```
const Twitter = require('twitter');
 
const client = new Twitter({
  consumer_key: '',
  consumer_secret: '',
  access_token_key: '',
  access_token_secret: ''
});

export default (req, res) => {
  res.statusCode = 200
  //  Return our eventual payload
  res.json()
}
```

We will need all of the keys and tokens referenced. **DO NOT PUSH TO PRODUCTION WITH KEYS PLAINLY AVAILABLE IN THE CODE** For testing and experimenting, it's ok to hard-code your keys and tokens. But please don't push these to a Github repository or deploy with them like this. 

You can get all of the necessary keys and tokens from the Keys and Tokens tab for your Twitter app. With your keys and tokens ready for use, we can write our Twitter search query. In the `team.js` file, let's change the request code to look like this: 

```
export default async (req, res) => {
  const { teamName } = req.body;
  const tweets = await client.get('search/tweets', {q: teamName, count: '100'});
  console.log(tweets);
  res.statusCode = 200

  res.json(tweets)
}
```

This is taking the team name payload, passing it through as our search parameter on Twitter and increasing the results to the max of 100. We could page through results, but I think as a snapshot of how people feel about your team, limiting it to 100 is totally fine. 

Right now, we're not quite returning the data we want, but we can at least test that this is all working. If you go back to the web app and search for a team, you should see in the terminal/command prompt the results of the search printed out. This data is also being returned to the client. If you look at the network tab, you can see the GET request, and the response should look the same as the results printed in the terminal. 

Our next step is to parse the tweets and only use the status text. Let's work on that now. 

```
export default async (req, res) => {
  try {
    const { teamName } = req.body;
    const tweets = await client.get('search/tweets', {q: teamName, count: '100'});
    const statuses = tweets.statuses.map(tweet => tweet.text);
    console.log(statuses);
    res.statusCode = 200;

    res.json(statuses);
  } catch (error) {
    console.log(error);
    res.status(500).send('Server error');
  }
}
```

I updated the API function to include a try/catch because that's just what you should do. Catch your errors, handle them properly. But in addition to that, I used the `map` function to return only the status text of all the tweets. Go ahead and try it out. 

Much cleaner, and matches our needs. Next up, we need to send this information to the Machine Learning API. 

### Working With Parallel Dots

If you've never worked with Machine Learning before, don't worry. I hadn't either before this little experiment. Fortunately for us, a lot of other people have not only trained Machine Learning algorithms but they've made accessing those models easy. We're going to use one such solution here called [Parallel Dots](https://www.paralleldots.com/sentiment-analysis).

You'll need to sign up for a free account at the link above, but that free account gives you a generous 1,000 requests per day for free. You'll want to bookmark the [documentation](http://apis.paralleldots.com/text_docs/index.html) as well. We are going to be working with the sentiment API. The Parallel Dots APIs expect a FormData payload when making requests, so we'll need to account for that in our code. 

Fortunately, NodeJS has FormData built in. Let's require it like this: `const FormData = require('form-data')`

You will also need to grab your API key from the Parallel Dots dashboard. Once you have that, you're ready to go. We've already filtered our Twitter data feed down to just the status text. Let's feed that as an array into the Parallel Dots sentiment API to find out if each tweet is positive, negative, or neutral. 

Right below where we left off (filtering our data down to just the status text), add this: 

```
const formData = new FormData();
formData.append('text', JSON.stringify(statuses));
formData.append('api_key', ParallelDotsAPIKey);

const payload = await axios.post('https://apis.paralleldots.com/v4/sentiment_batch', formData, {headers: formData.getHeaders()});
const { data } = payload;
const { sentiment } = data;

const realSentiment = sentiment.filter(s => !s.code);
console.log(realSentiment);
```

You'll not the `realSentiment` variable. This is simply a hack to get around the occasional rate limiting issue on the free API when making a batch request like we are. If you get rate limited, an object is returned in your overall payload array that includes the 429 status code. We're filtering those out for simplicity and because they are few and far between and because for this walkthrough we won't be upgrading to a paid endpoint. You're welcome to upgrade if you want, but I'm not. 

Ok, back to the code. We are making a POST request to the sentiment API endpoint with our FormData. The FormData includes the api_key and the statuses array. 

You can test this out now, and you should see an array of analysis that looks something like this: 

```
[  { "negative": 0.019, "neutral": 0.251, "positive": 0.73 },
  { "negative": 0.429, "neutral": 0.462, "positive": 0.109 },
  { "negative": 0.855, "neutral": 0.103, "positive": 0.042 },
  { "negative": 0.014, "neutral": 0.169, "positive": 0.817 },
  { "negative": 0.496, "neutral": 0.341, "positive": 0.163 },
  { "negative": 0.006, "neutral": 0.097, "positive": 0.897 },
  { "negative": 0.003, "neutral": 0.156, "positive": 0.841 },
  { "negative": 0.038, "neutral": 0.585, "positive": 0.376 },
  { "negative": 0.031, "neutral": 0.223, "positive": 0.746 },
  { "negative": 0.415, "neutral": 0.512, "positive": 0.073 },
...
]
```

Now, we're getting somewhere! The higher the score, the more likely the tweet fell into the particular category. For example, an 8.0 in positive would indicate the tweet seemed overwhelmingly positive. 

For the sake of our app, I think we want to return something a little more filtered, though. What would be nice is an array of objects with two keys: `type` and `score`. This will allows us to better manipulate the data on the client as we see fit. 

To do this, we can loop through the `realSentiment` array and build up our own new custom array. Here's how I've structured this using a simple if/then statement: 

```
let scores = [];
for(const s of realSentiment) { 

  try {
    const { positive, negative, neutral } = s;
    let scoreObject = {};
    if(positive > negative && positive > neutral) {
      scoreObject = {
        type: 'positive', 
        score: positive
      }
      scores.push(scoreObject);
    } else if(negative > positive && negative > neutral) {
      scoreObject = {
        type: 'negative', 
        score: negative
      }
      scores.push(scoreObject);
    } else if(neutral > positive && neutral > negative) {
      scoreObject = {
        type: 'neutral', 
        score: neutral
      }
      scores.push(scoreObject);
    } else {
      console.log("No match")
    }
  } catch (error) {
    console.log(error);
  }             
}
```

We're doing simple comparisons to figure out if the score is positive, negative, or neutral and setting the type. Then we are returning the actual score. 

You can console.log `scores`, and you'll see something like this: 

```
[  { type: 'neutral', score: 0.371 },
  { type: 'positive', score: 0.701 },
  { type: 'neutral', score: 0.598 },
  { type: 'neutral', score: 0.477 },
  { type: 'positive', score: 0.584 },
  { type: 'positive', score: 0.68 },
  { type: 'positive', score: 0.68 },
  { type: 'neutral', score: 0.477 },
  { type: 'positive', score: 0.599 },
  { type: 'neutral', score: 0.396 },
  { type: 'neutral', score: 0.583 },
  { type: 'positive', score: 0.585 },
  { type: 'positive', score: 0.484 },
  { type: 'positive', score: 0.532 },
  { type: 'positive', score: 0.572 },
  { type: 'neutral', score: 0.432 },
  { type: 'neutral', score: 0.523 },
  { type: 'neutral', score: 0.48 },
  { type: 'positive', score: 0.744 },
...
]
```

Now we have the information needed to return to the client. Take that scores array and return it from the API. Here's the full function for the route: 

```
export default async (req, res) => {
  try {
    const { teamName } = req.body;
    const tweets = await client.get('search/tweets', {q: teamName, count: '100'});
    const statuses = tweets.statuses.map(tweet => tweet.text);
    
    const formData = new FormData();
    formData.append('text', JSON.stringify(statuses));
    formData.append('api_key', ParallelDotsAPIKey);

    const payload = await axios.post('https://apis.paralleldots.com/v4/sentiment_batch', formData, {headers: formData.getHeaders()});
    const { data } = payload;
    const { sentiment } = data;

    const realSentiment = sentiment.filter(s => !s.code);

let scores = [];
for(const s of realSentiment) { 

  try {
    const { positive, negative, neutral } = s;
    let scoreObject = {};
    if(positive > negative && positive > neutral) {
      scoreObject = {
        type: 'positive', 
        score: positive
      }
      scores.push(scoreObject);
    } else if(negative > positive && negative > neutral) {
      scoreObject = {
        type: 'negative', 
        score: negative
      }
      scores.push(scoreObject);
    } else if(neutral > positive && neutral > negative) {
      scoreObject = {
        type: 'neutral', 
        score: neutral
      }
      scores.push(scoreObject);
    } else {
      console.log("No match")
    }
  } catch (error) {
    console.log(error);
  }             
}

    console.log(scores);
    res.statusCode = 200;

    res.json(scores);
  } catch (error) {
    console.log(error);
    res.status(500).send('Server error');
  }
}
```

### Finishing Up The Client

I know this is a long walkthrough, but remember, I'm learning with you. It's good for all of us. And, we're almost done. Open up the `team-sentiment.js` file in your `pages/sentiment` folder. Let's take the data returned from the API, do some basic JavaScript manipulation, and display the results. 

In our `checkTheFeels` function, let's take the response data from the API and convert it into something a little more useful. Percentages of the whole, maybe? I think being able to display what percent of positive sentiment there is versus negative versus neutral would be interesting. Let's start there. Below the response from the API, right under this line `const res = await axios.post('/api/team', JSON.stringify(payload), { headers: { 'Content-Type': "application/json"}})`, add: 

```
const scores = res.data;

const positives = scores.filter(score => score.type === 'positive').length;
const negatives = scores.filter(score => score.type === 'negative').length;
const neutral = scores.filter(score => score.type === 'neutral').length;

const positivePercentage = (positives/scores.length) * 100;
const negativePercentage = (negatives/scores.length) * 100;
const neutralPercentage = (neutral/scores.length) * 100; 

const data = { positivePercentage, negativePercentage, neutralPercentage };

setSentiment(data);
``` 

This is pretty straightforward math. We filter the array based on conditions. Then we compare the lengths of each filtered array against the length of the original array. Now we have our percentages and can use them in the client. For convenience, I wrapped each percentage up in an object and am passing that through to our `setSentiment` state function. Which reminds me. We originally set up our payload state variable to expect an array. Let's fix that. Up toward the top of the function component, find the line that includes the payload state variable and the `setSentiment` state function. Change it to this: 

`const [sentiment, setSentiment] = useState({positivePercentage: 0, negativePercentage: 0, neutralPercentage: 0})`

Finally, we need to update our render conditional to display the sentiment scores. Again, not doing much styling here in this walkthrough, but I want to at least lay the scores out in a row. We can do that like so: 

```
if(loading) {
    return (
      <div>
        <h1>Checking on all the feels for the {teamName}...</h1>
      </div>
    )
  } else {
    return (
      <div>
        <h1>Your team's sentiment</h1>
        <div style={{ display: "flex", flexDirection: "row" }}>
          <div style={{margin: "15px"}}>
            <h3>{sentiment.positivePercentage.toFixed(2)}%</h3>
            <p>Positive</p>
          </div>
          <div style={{margin: "15px"}}>
            <h3>{sentiment.negativePercentage.toFixed(2)}%</h3>
            <p>Negative</p>
          </div>
          <div style={{margin: "15px"}}>
            <h3>{sentiment.neutralPercentage.toFixed(2)}%</h3>
            <p>Neutral</p>
          </div>
        </div>
        <Link href='/'><a>Try another team</a></Link>
      </div>
    )
  }
```

And that should do it. If you enter a team name in the app now, you'll be taken to the sentiment page. When the sentiment analysis is done, you'll be given a result like this: 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/qz0e3b8t8jkwbgqvwnx7.png)  

Looks like the Padres are having a good day!

### Wrapping Up

This was a long, but simple example of what you can do by combining cool technologies. I could have built this app with a traditional React frontend and Express backend, but Next.js packages it up so neatly for us, it's a much nicer experience. Not to mention the performance improvements. 

The Parallel Dots API is nice, but it may not be what you want to use. Fortunately, there are other Machine Learning APIs. Google [has one](https://cloud.google.com/products/machine-learning) with decent enough pricing. The choice of products you use is totally up to you, but what I hope you have taken away from this is how accessible even complex-seeming technology can be. 

If you're interested in the source code for this walkthrough, you can [find it here](https://github.com/jehunter5811/team-sentiment).