The OutWod Leaderboard – Fun with NodeJS and React

T

Recently Will Lanier from the Out Foundation contacted me because he had found an old blog post about my Crossfit Alerts project and was trying to do something similar for the upcoming 2018 Crossfit Open but for the LGBT+ community.

He explained that he is involved in a project called OutWod to help bring together LGBT+ Crossfit athletes. They were going to have participants from this community raise funds for local charitable organizations and compete against each other during the open.

Now, the games website allows you to create custom leaderboards, but there’s no support for something like a leaderboard based on sexual orientation. Also, Will wanted to be able to support LGBT+ athletes that wanted to participate in this charity event but not formally participate in the open through the games website.

He needed some help.

Fortunately, I happened to have some time on my hands and thought it sounded like an awesome idea, so I offered to build a custom leaderboard to support the project.

Creating a Custom Leaderboard

Since I had some time and I wanted to learn some new technologies, I decided to build out a solution for this project using NodeJS and React.

I’ve been wanting to experiment with isomorphic javascript for a while, but hadn’t had the right opportunity.

I ended up building two NodeJS apps. One for collecting the data I needed automatically and one for the actual UI of the leaderboard.

Importing Participants

Back in 2013 when I created Crossfit Alerts, I had to create a crawler to index the entire games website into my own database. Luckily, the site now supports non-documented search API that responds with JSON.

You can see what this looks like here, Games Search API Endpoint, or in the partial response below.

""version":3,
 "dataType":"LEADERBOARD",
 "query":"query-string",
 "cacheKey":"v3.leaderboard.OPN-2018-DIV02-FIT01000-PRF00-RXD.0.alb.2536",

 "pagination":
 {
  "currentPage":1,
  "totalPages":2993,
  "totalCompetitors":149648
 },
 leaderboardRows":
 [
  {
   "entrant":
   {
    "competitorId":"92567",
    "competitorName":"Andrey Ganin",
    "firstName":"Andrey",
    "lastName":"Ganin",
    ...
   },

   "scores":
   [
    {
    "ordinal":1,
    "rank":"1",
    "score":"14590000",
    "scoreDisplay":"459 reps",
    "mobileScoreDisplay":"",
    "scoreIdentifier":"96048ff1f8b4458918d5",
    "scaled":"0",
    "video":"0",
    "breakdown":"14 rounds +n8 toes-to-barsn3 clean & jerks",
    "judge":"Sergey Chadin",
    "affiliate":"CrossFit MDN",
    "heat":"",
    "lane":""
    }

   ],

   "overallRank":"1",
   "overallScore":"1",
   "nextStage":""
  },

This is fantastic because it makes my job significantly easier. I can automatically update people’s scores by simply querying the API endpoint and copying any new data into my database.

Every person that signs up for the Crossfit Open gets a unique athlete id assigned to them and this can be used to lookup their profile and their scores. I asked Will to collect people’s Crossfit Games athlete id’s when they signed up to participate in the competition.

I then wrote a script to parse the spreadsheet that he collected athlete information with and bring that into MySQL database. This included things like the athlete’s name, their games ID if they have one, what division they are part of, and whether they are doing RX or scaled workouts.

Automatically Collecting Participant Scores

Now that I had all the data about who was participating in the event, I needed to be able to automatically lookup their scores and save it to my database so that it could be displayed in the leaderboard automatically.

It turns out, if you search an athlete by name on the games website and then click their entry that comes up in the dropdown and inspect the HTTP request, you get something like this
https://games.crossfit.com/competitions/api/v1/competitions/open/2018/leaderboards?division=2&athlete=2536.

That returns a list of athletes but one of the athletes is guaranteed to be the athlete with ID 2536 (Sam Briggs in this case).

We can use this type of query to look up an athlete’s scores and update our local database. I simply query to retrieve all athletes and then go through each athlete and hit the API endpoint with their ID, then parse the results to find the athlete I want. The JS code is displayed below. The “gamesApi” simply packages up my object into an HTTP request and returns the JSON response.

gamesApi.performRequest('/competitions/api/v1/competitions/open/2018/leaderboards', 
'GET', 
{
 athlete: athleteId,
 division: division,
 scaled: 0,
 sort: 0,
 fittest: 1,
 fittest1: 0,
 occupation: 0,
 page: 1
}, 
function(data) {
 for(var i = 0; i < data.leaderboardRows.length; i++) {
  if(data.leaderboardRows[i].entrant.competitorId == athleteId) {
   // update the athlete data and scores
   updateAthleteScores(data.leaderboardRows[i], athlete);
   break;
  }
 }
});

I setup this NodeJS app on AWS Lambda so it can run a few times a day to import any new data.

Displaying the Leaderboard

To actually display the results of the leaderboard, I created a NodeJS project with a React front-end. I essentially mimicked the same user experience that you’d find on the Crossfit Games leaderboard.

You can click an athlete to get more details about their scores and profile information. Basic searching and filtering is also supported.

I created a free Heroku account to host the leaderboard for the duration of the open.

Supporting Custom Scores

At this point, everything seemed great, but we needed a way for non-registered athletes to be able to include their scores. On the surface, this seems simple enough, just give them a form where they can enter their scores. However, this complicates matters because up until this point I was relying on the games website’s calculation for score and rank for an athlete. By introducing custom scores that the main site has no knowledge of, it completely invalidates the scores and ranks I was using.

To solve this problem, I had to implement my own ranking and score calculations. Whenever a new score gets entered, whether picked up automatically from the games website or manually entered through a form I built, we need to re-rank all athletes.

The way I do this is I first save any new scores to my database. I then query to get all athletes and I run a custom sort function on each workout to calculate each athlete’s rank for a particular workout. The sort has to take into account RX versus scaled, reps and possibly time.

I use these rankings to calculate the athlete’s overall score and then sort the athletes by score, use this sorted order to calculate their overall rank and update the database entries.

You can see what this code looks like below:

function rankAthletes(athletes) {
 let workoutIds = ["18.1", "18.2", "18.3", "18.4", "18.5"];

 // sort by each workout and calcualte the rank for the workout
 for(var i = 0; i < workoutIds.length; i++) {
  athletes.sort(propComparator(workoutIds[i]));
  
  // saves the rank for this athlete for the given workout
  updateAthleteRanks(athletes, workoutIds[i]);
 }

 // calculate athletes overall score, this is 
 // the sum of their ranks across all workouts
 updateAthleteScores(athletes);

 // sort by score
 athletes.sort(scoreCompare);

 // update athletes overall score and rank
 for(var i = 0; i < athletes.length; i++) {
  let rank = i + 1;
  let score = athletes[i].dataValues.overallscore;

  // update database entry
  models.Athlete.findById(athletes[i].dataValues.id).then(athlete => {
    athlete.update({overallscore: score, overallrank: rank});
  });
 }
}

The Live Result

With support for pulling data from the official games website and custom scores, the leaderboard was ready.

I sent Will some iframe code to embed the leaderboard within the OutWod Squarespace website and we were good to go.

Final Thoughts

I’m really glad that Will reached out. The project was a lot of fun and it’s for a great cause that I’m happy to play some small part in.

These types of side projects are a great way to learn new technologies and approaches to solving problems. I had a great time with my first real NodeJS and React project and I’ll definitely be relying on them more in the future.

Please contact me if you have any questions about this project.

Happy coding!

About the author

Sean Falconer

7 Comments

By Sean Falconer

Sean Falconer

Get in touch

I write about programming, developer relations, technology, startup life, occasionally Survivor, and really anything that interests me.