Ultirzr's a web application that let's users easily search for Ultimate frisbee tournaments, teams, and players, as well as save teams for easy access.
It's safe to say that Ultimate Frisbee's a part of my identity. I started playing circa 2008. The Ultimate "scene" is comprised of several levels of intensity, ranging anywhere from Intramural to Pro. I've been playing in one form or another every year since I started. While the USAU1 provides a great service to us Ultimate aficionados, their tournament scheduling and general online presence leaves something to be desired. Ergo, Ultirzr2 was born.
One of the tough parts about this project is managing the data. On the web, they have an "Archives" tab that lists the years 1979 - 2018, but every link is straight busted.
The web interface has always been a real trip to experience, it also appears like the data they render is pre-rendered, so the only way forward would be to scrape HTML from the page(s). However, they also have a mobile app, which fetches data via API endpoints, and you can view the data without an account. Using the handy Mac app Charles, I unearthed a handful of open3 API endpoints, like this guy which fetches the players on a team by the teamid
:
# fetch a team's players
curl --location 'https://play.usaultimate.org/ajax/api.aspx?f=getpersonnelbyteam&teamid=36037'
Then I spun up a MongoDB database and pulled as much historical data as I could. I didn't want to completely hammer their servers with requests (for both obfuscation and courtesy reasons), so the process took a few sessions. One of the difficult parts of this process was retrieving past team data. As far as I could tell, there wasn't an explicit endpoint to specify a team's year, and what's more, a team receives a new `teamid` every year. A team's payload, however, looks something like this:
{
"TeamId": 36037,
"TeamName": "Hodags",
"TeamLogo": "/assets/TeamLogos/Hodag.png",
"City": "Madison",
"State": "WI",
"SchoolName": "Wisconsin",
"DivisionName": "Division I",
"CompetitionLevel": "College",
"TeamDesignation": "",
"PreviousSeasonTeamId": 32632,
"Year": "2024"
}
They do conveniently provide `PreviousSeasonTeamId` for you. Using that field, I had a script recursively fetch the team's past data if it existed.
One of the primary goals for this website was to make it easier to find ultimate-related stuff, in particular event schedules but also teams and players. It is not easy to do that on USAU, nor has it ever been easy. Using the `mongodb` module, you can achieve a relatively effective search query4 directly from the db:
export const searchDb = async (query?: string | null) => {
const pipeline = [
{ $match: { $text: { $search: query } } },
{ $sort: { score: { $meta: 'textScore' }, posts: -1 } },
{
$project: {
...excludedFields,
score: {
$meta: 'textScore',
},
},
},
{ $limit: 50 },
];
if (query) {
const results = (
await Promise.all([
(await teamsDb.aggregate<ScoreObject<ObjectIdTeam>>(pipeline)).map((t) => mapScore(t, formatTeam)),
(await eventsDb.aggregate<ScoreObject<ObjectIdEvent>>(pipeline)).map((e) => mapScore(e, formatEvent)),
(await playersDb.aggregate<ScoreObject<ObjectIdPlayer>>(pipeline)).map((p) => mapScore(p, formatPlayer)),
])
)
.flat()
.sort((a, b) => b.score - a.score);
return results;
}
return [];
};
Ultirzr's search API lives at /api/search
, give it a whirl!
# it me!
curl --location 'https://www.ultirzr.app/api/search?q=tanner%20marshall'
Once I had the API more-or-less dialed, along with the data and search, the rest of the app fell into place pretty quickly. The only authentication on the app is currently used for saving teams to your dashboard. I'll likely add some more features as the Ultimate season rolls around.
I wanted to be able to easily find tournaments, teams, and players. So a lot of the app is designed around quick access to information. There're just a handful of pages that user's can see and interact with. There's the dashboard which is conditionally shown only if the user's signed in. It displays the team's they follow and any current or upcoming tournaments those teams have.
Next there's the team page, which shows a team's previous, current, and upcoming events, i.e. tournaments, and their results. You can also tab over to see the individual players on each team5, as well as go back in time to previous iterations of the team. One thing that bothered the living frick out of me with USAU's interface is that it's hard to navigate around, so I tried to make almost every listed team, event, or player clickable and linked to their respective pages and data.
Lastly, of course you gotta be able to peep scores and schedules for events, so we have an event page that shows schedules, pools, and brackets, as well as live (well, as live as the API decides to give us) results.
The frontend is React with TypeScript, rendered via NextJS. I used TailwindCSS for the styling, react-query for the frontend data fetching, and fontawesome for the icons.
As mentioned, NextJS has been my go-to framework for JavaScript/Typescript work these days. Since I'd recently migrated some of our work repos over to a Monorepo with Nx, I figured I'd do that as well with this one. This was particularly useful for writing the Node scripts that ran bulk fetches from the API, since you can the share types and functions between the scripts and the NextJS app. Finally, it's deployed via Vercel6.
Made with 🥒 by T. 2024