Creating World Cup 2022 betting system with React

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Technologies used: React, Redux, MUI, REST API (axios library), Cloudflare KV and Cloudflare Pages.

In these post I will briefly go though the app explaining some core moments of app development. Github repository of the project can be found here .

Let’s create react app with store and reducer containing initialState of the app.

export const initialState: NewDataState= {
  selectionState: [
    {
      name: "A",
      countries: ["Qatar", "Ecuador", "Senegal", "Netherlands"],
    },
    {
      name: "B",
      countries: ["England", "Iran", "USA", "Wales"],
    },
    {
      name: "C",
      countries: ["Argentina", "Saudi Arabia", "Mexico", "Poland"],
    },
    {
      name: "D",
      countries: ["France", "Australia", "Denmark", "Tunisia"],
    },
    {
      name: "E",
      countries: ["Spain", "Costa Rica", "Germany", "Japan"],
    },
    {
      name: "F",
      countries: ["Belgium", "Canada", "Morocco", "Croatia"],
    },
    {
      name: "G",
      countries: ["Brazil", "Serbia", "Switzerland", "Cameroon"],
    },
    {
      name: "H",
      countries: ["Portugal", "Ghana", "Uruguay", "South Korea"],
    },
  ],
  roundState: [],
  quarterFinalState: [],
  semiFinalState: [],
  thirdPlaceState: [],
  finalState: [],
  roundIndexToDisplay: 0,
};

Once done we need to create (and render) a row in which countries divided into groups (A, B, C, D, E, F, G, H) will be showed. In GroupRow component we map on 8 groups and render Group component 8 times (groups come from the reducer, we get them with the help of useSelector in Table component that is parent component for all rows).

We should be able to choose 2 finalists in each group. Based on the selection done next row should be rendered and another round of games should be determined. This logic repeats 5 times meaning that starting from the second row we can reuse components and logic in it.

To render data from reducer I created two components: Group and GroupRow (GroupRow renders first row in the app and is a parent of Group).

In Group comp we determine how a single block (group) will look like. I took Select component from MUI. We need to store the choice from the Select somewhere so that is why we refer to useState hook:

const[firstFinalist, setFirstFinalist] = useState<string>("");
const[secondFinalist, setSecondFinalist] = useState<string>("");

On each select we need to put onChange handler, mine for first winner looks like this:

const onFirstWinnerChangeHandler = (e:SelectChangeEvent) => {
  setFirstFinalist(e.target.value);
  selectGroupFinalist(name, "firstFinalist", e.target.value);
};

With the help of setFirstFinalist we set firstFinalist locally within the Group comp. Then we pass user selection to selectGroupFinalist function which is a callback we get from the parent component. In selectGroupFinalist we pass name of the Group (A, B, C, D, E, F, G, H), whether the value is firstFinalist or setSecondFinalist and user selection (country who took first or second place in group). Then this function generates an object of the following type:

type GroupRowSelection= {
  [key:string]: { firstFinalist:string; secondFinalist:string};
};

where key is the name of the Group (A, B, C, D, E, F, G, H) and value is object containing user choice. Once object is generated we dispatch it to reducer and then next row will be rendered using Round and RoundRow components.

In each round we need to select only one winner. The logic for catching round winner and passing it to parent component is similar to what we had in the GroupRow. As we reuse RoundRow many times switch case was added to dispatch userSelection depending on the game stage that was predicted:

switch(roundKey) {
    case"roundState":
        dispatch(generateQuarterFinal(roundSelection));
        break;
    case"quarterFinalState":
    dispatch(generateSemiFinal(roundSelection));
        break;
    case"semiFinalState":
    dispatch(generateFinalAndThirdPlace(roundSelection));
        break;
    case"thirdPlaceState":
    dispatch(saveThirdPlace(Object.values(roundSelection)[0]));
        break;
    case"finalState":
    dispatch(saveFinal(Object.values(roundSelection)[0]));
    handleOpenModal();
        break;
  }
};

When all selections are done, user presses on “Save” button in the last row (Final) and modal window popups asking user to enter his/her name and then after he/she presses “Submit” button in the modal his/her selection appears on the left side of the screen.

So where did the data go? Where do we store it?

The short answer is: We store it in Cloudflare KV.

So how can we get there? For sure creating Cloudflare account is involved. Once you have it you should see in the menu on the left side of the screen Workers ⇒ KV ⇒ Create namespace

This namespace is where all data will be stored in the form of key value;

How do we send all data there?

We need to create Cloudflare worker and connect it to our namespace. For that you should go to Workers and press on Create Service button.

Some documentation about Cloudflare Workers can be found here

Personally I did not write worker using wrangler, I did everything within Cloudflare.

Here what I did: Workers ⇒ Create Service button ⇒ HTTP router ⇒ Create Service ⇒ Settings ⇒ Add binding: here you should connect your worker with namespace (KV storage) you made ⇒ Quick edit

What I liked about writing Cloudflare Workers is that its interface reminded me Postman and it was very user friendly. URL to which you can send all your requests is on the right side on the screen located between send button and a request type:

I highly recommend to read this article to feel more confident with writing Worker logic.

From the app I made my requests to worker with the help of axios library.

Honestly, this app was the most challenging thing I made so far and I really enjoyed the time working on it.