paint-brush
How to Build a Ping Pong Ranking App using Zipper and TypeScript Functionsby@thawkin3
239 reads

How to Build a Ping Pong Ranking App using Zipper and TypeScript Functions

by Tyler HawkinsSeptember 29th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

While my ping pong app isn’t perfect, I hope the takeaway here is how easy it is to get up and running with a product like Zipper. You don’t have to spend time agonizing over your app’s infrastructure when you have a simple idea that you just want to see working in production. Just get out there, start building, and deploy!
featured image - How to Build a Ping Pong Ranking App using Zipper and TypeScript Functions
Tyler Hawkins HackerNoon profile picture

Seasoned software engineers long for the good old days when web development was simple. You just needed a few files and a server to get up and running. No complicated infrastructure, no endless amount of frameworks and libraries, no build tools. Just some ideas and some code hacked together to make an app come to life.


Whether or not this romanticized past was actually as great as we think it was, developers today agree that software engineering has gotten complicated. There are too many choices with too much setup involved.


In response to this sentiment, many products are providing off-the-shelf starter kits and zero config toolchains to try to abstract away the complexity of software development.


One such startup is Zipper, a company which offers an online IDE where you can create applets that run as serverless TypeScript functions in the cloud. With Zipper, you don’t have to spend time worrying about your toolchain — you can just start writing code and deploy your app within minutes.


Today, we’ll be looking at a ping pong ranking app I built — once in 2018 with jQuery, MongoDB, Node.js, and Express; and once in 2023 with Zipper. We’ll examine the development process for each and see just how easy it is to build a powerful app using Zipper.


Backstory

First, a little context: I love to play ping pong. Every office in which I’ve worked has had a ping pong table, and for many years ping pong was an integral part of my afternoon routine. It’s a great game to relax, blow off some steam, strengthen friendships with coworkers, and reset your brain for a half hour.


Those who played ping pong every day began to get a feel for who was good and who wasn’t. People would talk. A handful of people were known as the best in the office, and it was always a challenge to take them on.


Being both highly competitive and a software engineer, I wanted to build an app to track who was the best ping pong player in the office. This wouldn’t be for bracket-style tournaments, but just for recording the games that were played every day by anybody. With that, we’d have a record of all the games played, and we’d be able to see who was truly the best.


This was 2018, and I had a background in the MEAN/MERN stack (MongoDB, Express, Angular, React, and Node.js) and experience with jQuery before that. After dedicating a week’s worth of lunch breaks and nights to this project, I had a working ping pong ranking app. I didn’t keep close track of my time spent working on the app, but I’d estimate it took about 10–20 hours to build.


Here’s what that version of the app looked like.


There was a login and signup page:


Office Competition Ranking System — Home page

The login page asked for your username and password to authenticate:


Office Competition Ranking System — Login page

Once authenticated, you could record your match by selecting your opponent and who won:


Office Competition Ranking System — Record game results page

You could view the leaderboard to see the current office rankings. I even included an Elo rating algorithm like they use in chess:


Office Competition Ranking System — Leaderboard page

Finally, you could click on any of the players to see their specific game history of wins and losses:


Office Competition Ranking System — Player history page

That was the app I created back in 2018 with jQuery, MongoDB, Node.js, and Express. And, I hosted it on an AWS EC2 server.


Now let’s look at my experience recreating this app in 2023 using only Zipper.


About Zipper

Zipper is an online tool for creating applets. It uses TypeScript and Deno, so JavaScript and TypeScript users will feel right at home. You can use Zipper to build web services, web UIs, scheduled jobs, and even Slack or GitHub integrations. Zipper even includes auth.


In short, what I find most appealing about Zipper is how quickly you can take an idea from conception to execution. It’s perfect for side projects or internal-facing apps to quickly improve a business process.


Demo app

Here’s the ping pong ranking app I built with Zipper in just three hours. And that’s including time reading through the docs and getting up to speed with an unfamiliar platform!


First, the app requires authentication. In this case, I’m requiring users to sign in to their Zipper account:

Ping pong ranking app — Authentication page

Once authenticated, users can record a new ping pong match:


Ping pong ranking app — Record a new match page

They can view the leaderboard:


Ping pong ranking app — Leaderboard page

And they can view the game history for any individual player:


Ping pong ranking app — Player history page

Not bad! The best part is that I didn’t have to create any of the UI components for this app. All the inputs and table outputs were handled automatically. And, the auth was created for me just by checking a box in the app settings!


You can find the working app hosted publicly on Zipper.


Ok, now let’s look at how I built this.


Creating a new Zipper app

First, I created my Zipper account by authenticating with GitHub. Then, on the main dashboard page, I clicked the Create Applet button to create my first applet.


Create your first applet

Next, I gave my applet a name, which became its URL. I also chose to make my code public and required users to sign in before they could run the applet.


Applet configuration

Then I chose to generate my app using AI, mostly because I was curious how it would turn out! This was the prompt I gave it:


“I’d like to create a leaderboard ranking app for recording wins and losses in ping pong matches. Users should be able to log into the app. Then they should be able to record a match showing who the two players were and who won and who lost.


“Users should be able to see the leaderboard for all the players, sorted with the best players displayed at the top and the worst players displayed at the bottom.


“Users should also be able to view a single player to see all of their recorded matches and who they played and who won and who lost.”


Applet initialization

I might need to get better at prompt engineering, because the output didn’t include all the features or pages I wanted.


The AI-generated code included two files: a generic “hello world” main.ts file, and a view-player.ts file for viewing the match history of an individual player.


main.ts file generated by AI


view-player.ts file generated by AI

So, the app wasn’t perfect from the get-go, but it was enough to get started.


Writing the ping pong app code

I knew that Zipper would handle the authentication page for me, so that left three pages to write:


  1. A page to record a ping pong match
  2. A page to view the leaderboard
  3. A page to view an individual player’s game history

Record a new ping pong match

I started with the form to record a new ping pong match. Below is the full main.ts file. We’ll break it down line by line right after this.


type Inputs = {
  playerOneID: string;
  playerTwoID: string;
  winnerID: string;
};

export async function handler(inputs: Inputs) {
  const { playerOneID, playerTwoID, winnerID } = inputs;

  if (!playerOneID || !playerTwoID || !winnerID) {
    return "Error: Please fill out all input fields.";
  }

  if (playerOneID === playerTwoID) {
    return "Error: PlayerOne and PlayerTwo must have different IDs.";
  }

  if (winnerID !== playerOneID && winnerID !== playerTwoID) {
    return "Error: Winner ID must match either PlayerOne ID or PlayerTwo ID";
  }

  const matchID = Date.now().toString();
  const matchInfo = {
    matchID,
    winnerID,
    loserID: winnerID === playerOneID ? playerTwoID : playerOneID,
  };

  try {
    await Zipper.storage.set(matchID, matchInfo);
    return `Thanks for recording your match. Player ${winnerID} is the winner!`;
  } catch (e) {
    return `Error: Information was not written to the database. Please try again later.`;
  }
}

export const config: Zipper.HandlerConfig = {
  description: {
    title: "Record New Ping Pong Match",
    subtitle: "Enter who played and who won",
  },
};


Each file in Zipper exports a handler function that accepts inputs as a parameter. Each of the inputs becomes a form in UI, with the input type being determined by its TypeScript type that you give it.


After doing some input validation to ensure that the form is correctly filled out, I stored the match info in Zipper’s key-value storage. Each Zipper applet gets its own storage instance that any of the files in your applet can access. Because it’s a key-value storage, objects work nicely for values since they can be serialized and deserialized as JSON, all of which Zipper handles for you when reading from and writing to the database.


At the bottom of the file, I’ve added a HandlerConfig to add some title and instruction text to the top of the page in the UI.


With that, the first page is done.


Ping pong ranking app - Record a new match page


Leaderboard

Next up is the leaderboard page. I’ve reproduced the leaderboard.ts file below in full:


type PlayerRecord = {
  playerID: string;
  losses: number;
  wins: number;
};

type PlayerRecords = {
  [key: string]: PlayerRecord;
};

type Match = {
  matchID: string;
  winnerID: string;
  loserID: string;
};

type Matches = {
  [key: string]: Match;
};

export async function handler() {
  const allMatches: Matches = await Zipper.storage.getAll();
  const matchesArray: Match[] = Object.values(allMatches);

  const players: PlayerRecords = {};

  matchesArray.forEach((match: Match) => {
    const { loserID, winnerID } = match;

    if (players[loserID]) {
      players[loserID].losses++;
    } else {
      players[loserID] = {
        playerID: loserID,
        losses: 0,
        wins: 0,
      };
    }

    if (players[winnerID]) {
      players[winnerID].wins++;
    } else {
      players[winnerID] = {
        playerID: winnerID,
        losses: 0,
        wins: 0,
      };
    }
  });

  return Object.values(players);
}

export const config: Zipper.HandlerConfig = {
  run: true,
  description: {
    title: "Leaderboard",
    subtitle: "See player rankings for all recorded matches",
  },
};


This file contains a lot more TypeScript types than the first file did. I wanted to make sure my data structures were nice and explicit here.


After that, you see our familiar handler function, but this time without any inputs. That’s because the leaderboard page doesn’t need any inputs; it just displays the leaderboard.


We get all of our recorded matches from the database, and then we manipulate the data to get it into an array format of our liking. Then, simply by returning the array, Zipper creates the table UI for us, even including search functionality and column sorting. No UI work needed!


Finally, at the bottom of the file, you’ll see a description setup that’s similar to the one on our main page. You’ll also see the run: true property, which tells Zipper to run the handler function right away without waiting for the user to click the Run button in the UI.


Ping pong ranking app - Leaderboard page


Player history

Alright, two down, one to go. Let’s look at the code for the view-player.ts file, which I ended up renaming to player-history.ts:


type Inputs = {
  playerID: string;
};

type Match = {
  matchID: string;
  winnerID: string;
  loserID: string;
};

type Matches = {
  [key: string]: Match;
};

type FormattedMatch = {
  matchID: string;
  opponent: string;
  result: "Won" | "Lost";
};

export async function handler({ playerID }: Inputs) {
  const allMatches: Matches = await Zipper.storage.getAll();
  const matchesArray: Match[] = Object.values(allMatches);

  const playerMatches = matchesArray.filter((match: Match) => {
    return playerID === match.winnerID || playerID === match.loserID;
  });

  const formattedPlayerMatches = playerMatches.map((match: Match) => {
    const formattedMatch: FormattedMatch = {
      matchID: match.matchID,
      opponent: playerID === match.winnerID ? match.loserID : match.winnerID,
      result: playerID === match.winnerID ? "Won" : "Lost",
    };

    return formattedMatch;
  });

  return formattedPlayerMatches;
}

export const config: Zipper.HandlerConfig = {
  description: {
    title: "Player History",
    subtitle: "See match history for the selected player",
  },
};


The code for this page looks a lot like the code for the leaderboard page. We include some types for our data structures at the top. Next we have our handler function which accepts an input for the player ID that we want to view.


From there, we fetch all the recorded matches and filter them for only matches in which this player participated.


After that, we manipulate the data to get it into an acceptable format to display, and we return that to the UI to get another nice auto-generated table.


Ping pong ranking app - Player history page

Ping pong ranking app - Player history page


Conclusion

That’s it! With just three handler functions, we’ve created a working app for tracking our ping pong game history. This app does have some shortcomings that we could improve, but we’ll leave that as an exercise for the reader.


For example, it would be nice to have a dropdown of users to choose from when recording a new match, rather than entering each player’s ID as text. Maybe we could store each player’s ID in the database and then display those in the UI as a dropdown input type.


Or, maybe we’d like to turn this into a Slack integration to allow users to record their matches directly in Slack. That’s an option too!


While my ping pong app isn’t perfect, I hope the takeaway here is how easy it is to get up and running with a product like Zipper. You don’t have to spend time agonizing over your app’s infrastructure when you have a simple idea that you just want to see working in production. Just get out there, start building, and deploy!