Create a football results feed with Go and React

Introduction

Introduction

The World Cup is with us once again. In this article we are going to show how you can add a real-time football results feed to your site so that your users can keep up with the latest scores without needing to go elsewhere.

We are going to build a system where a football pundit can enter details of matches, and other sites can display a live feed of the results as they are entered.

go-react-football-feed-demo

Prerequisites

This article focuses on using Go and React. As such, it is important that you have Go already installed and configured on your system - including having the GOPATH set up correctly. If you do not know how to do this then the Go documentation can explain this all. A certain level of understanding of Go is assumed to follow along with this article. The “A Tour of Go” tutorial is a fantastic introduction if you are new to the language.

We are also going to use the dep tool to manage the dependencies of our backend application, so make sure that this is correctly installed as well.

Finally, in order to develop and run our pundits web UI you will need to have a recent version of Node.js installed and correctly set up. A certain level of understanding of JavaScript is also assumed to follow along with this article.

Create a Pusher account

In order to follow along, you will need to create a free Pusher account - sign up or sign in. Then go to the dashboardand create a Channels app.

go-react-football-feed-create-pusher-app

Fill out this dialog as needed and then click the Create my app button. Then click on App Keys and note down the credentials for later.

go-react-football-feed-pusher-app-keys

Building the backend service

We are going to write our backend service using the Go language, using the library to power our HTTP service.

Our service is going to offer the following endpoints:

  • POST /match - this will trigger events for half time, extra time and full time.
  • POST /goal - this will trigger events to indicate that a goal has been scored.
  • POST /card - this will trigger events to indicate that a yellow or red card has been given.

To start with, we need to create an area to work with. Create a new directory under your GOPATH in which to work:

1# Mac and Linux
2    $ mkdir -p $GOPATH/src/pusher/football-feed
3    $ cd $GOPATH/src/pusher/football-feed
4    
5    # Windows Powershell
6    mkdir -path $env:GOPATH/src/pusher/football-feed
7    cd $env:GOPATH/src/pusher/football-feed

We can then initialise our work area for this project. This is done using the dep tool:

    $ dep init

Doing this will create the **Gopkg.toml and Gopkg.lock files used to track our dependencies, and the vendor **directory which is used to store vendor dependencies.

The first thing we want is to be able to send Pusher Channels messages. This is the core of our backend application. For this we will be creating a new directory called internal/notifier in the root of rht project area and then writing a file called internal/notifier/notifier.go, as follows:

1// internal/notifier/notifier.go
2    package notifier
3    import (
4        "github.com/pusher/pusher-http-go"
5    )
6    type Message interface{}
7    type MatchMessage struct {
8        Event     string `json:event`
9        HomeTeam  string `json:homeTeam`
10        AwayTeam  string `json:awayTeam`
11        HomeScore uint16 `json:homeScore`
12        AwayScore uint16 `json_awayScore`
13    }
14    type GoalMessage struct {
15        Player    string `json:player`
16        ForTeam   string `json:forTeam`
17        HomeTeam  string `json:homeTeam`
18        AwayTeam  string `json:awayTeam`
19        HomeScore uint16 `json:homeScore`
20        AwayScore uint16 `json_awayScore`
21        OwnGoal   bool   `json:ownGoal`
22    }
23    type CardMessage struct {
24        Team   string `json:team`
25        Player string `json:player`
26        Card   string `json:card`
27    }
28    type Notifier struct {
29        notifyChannel chan<- Message
30    }
31    func notifier(notifyChannel <-chan Message) {
32        client := pusher.Client{
33            AppId:   "PUSHER_APP_ID",
34            Key:     "PUSHER_KEY",
35            Secret:  "PUSHER_SECRET",
36            Cluster: "PUSHER_CLUSTER",
37            Secure:  true,
38        }
39        for {
40            message := <-notifyChannel
41            switch payload := message.(type) {
42            case GoalMessage:
43                client.Trigger("match", "goal", payload)
44            case CardMessage:
45                client.Trigger("match", "card", payload)
46            case MatchMessage:
47                client.Trigger("match", "match", payload)
48            }
49        }
50    }
51    func New() Notifier {
52        notifyChannel := make(chan Message)
53        go notifier(notifyChannel)
54        return Notifier{notifyChannel}
55    }
56    func (notifier *Notifier) Notify(msg Message) {
57        notifier.notifyChannel <- msg
58    }

NOTE: Ensure that PUSHER_APP_ID, PUSHER_KEY, PUSHER_SECRET and PUSHER_CLUSTER are all replaced with values obtained from the Pusher Dashboard when you registered your app.

We start by defining a number of messages that we can handle - MatchMessage, GoalMessage and CardMessage. We then define our Notifier type that will be handling the actual notifications. This works off of a go-routine so that the actual Pusher Channels messages are sent in the background and do not in any way interfere with the performance of the HTTP requests.

When processing a message, we determine the Pusher “event” based on the type of the Message received, and we use the message as-is as the payload.

The next thing we want is the web server. This will be done by writing a file called internal/webapp/webapp.go in our project area, as follows:

1// internal/webapp/webapp.go
2    package webapp
3    import (
4        "net/http"
5        "pusher/football-feed/internal/notifier"
6        "github.com/gin-contrib/cors"
7        "github.com/gin-gonic/gin"
8    )
9    func StartServer(notify *notifier.Notifier) {
10        r := gin.Default()
11        r.Use(cors.Default())
12        r.POST("/match", func(c *gin.Context) {
13            var json notifier.MatchMessage
14            if err := c.BindJSON(&json); err == nil {
15                notify.Notify(json)
16                c.JSON(http.StatusCreated, json)
17            } else {
18                c.JSON(http.StatusBadRequest, gin.H{})
19            }
20        })
21        r.POST("/goal", func(c *gin.Context) {
22            var json notifier.GoalMessage
23            if err := c.BindJSON(&json); err == nil {
24                notify.Notify(json)
25                c.JSON(http.StatusCreated, json)
26            } else {
27                c.JSON(http.StatusBadRequest, gin.H{})
28            }
29        })
30        r.POST("/card", func(c *gin.Context) {
31            var json notifier.CardMessage
32            if err := c.BindJSON(&json); err == nil {
33                notify.Notify(json)
34                c.JSON(http.StatusCreated, json)
35            } else {
36                c.JSON(http.StatusBadRequest, gin.H{})
37            }
38        })
39        r.Run()
40    }

This gives us our three routes, each of which does essentially the same:

  • Parse the request payload as JSON into an appropriate structure
  • Use the Notifier from above to send a Pusher Channels notification for this message

We also need our main application file. This will be /football-feed.go in our project area, as follows:

1// football-feed.go
2    package main
3    import (
4        "pusher/football-feed/internal/notifier"
5        "pusher/football-feed/internal/webapp"
6    )
7    func main() {
8        notifier := notifier.New()
9        webapp.StartServer(&notifier)
10    }

The final thing to do is to ensure that our dependencies are all available. This is done by executing:

    $ dep ensure

We can now start the application by executing go run football-feed.go:

1$ go run football-feed.go
2    [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
3     - using env:        export GIN_MODE=release
4     - using code:        gin.SetMode(gin.ReleaseMode)
5    
6    [GIN-debug] POST   /match                    --> pusher/football-feed/internal/webapp.StartServer.func1 (4 handlers)
7    [GIN-debug] POST   /goal                     --> pusher/football-feed/internal/webapp.StartServer.func2 (4 handlers)
8    [GIN-debug] POST   /card                     --> pusher/football-feed/internal/webapp.StartServer.func3 (4 handlers)
9    [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
10    [GIN-debug] Listening and serving HTTP on :8080

Alternatively, we can build an executable using go build football-feed.go. This executable can then be distributed however we need to do so - for example, copying it into a Docker container or directly onto our production VMs.

If we were to make calls to this manually - e.g. by using cURL - then we would see the Pusher Channels events in the debug dashboard:

1> $ curl -v -X POST http://localhost:8080/card -H "Content-Type: application-json" --data '{"team": "Russia", "player": "Aleksandr Golovin", "card": "yellow"}'
2    *   Trying ::1...
3    * TCP_NODELAY set
4    * Connected to localhost (::1) port 8080 (#0)
5    > POST /card HTTP/1.1
6    > Host: localhost:8080
7    > User-Agent: curl/7.54.0
8    > Accept: */*
9    > Content-Type: application-json
10    > Content-Length: 67
11    >
12    * upload completely sent off: 67 out of 67 bytes
13    < HTTP/1.1 201 Created
14    < Content-Type: application/json; charset=utf-8
15    < Date: Mon, 25 Jun 2018 13:09:21 GMT
16    < Content-Length: 62
17    <
18    * Connection #0 to host localhost left intact
19    {"Team":"Russia","Player":"Aleksandr Golovin","Card":"yellow"}
go-react-football-feed-event-creator

Pundit application

Now that we’ve got our backend that is able to react to messages and send Pusher Channels events, we want to write our Football Pundit application that will actually trigger these messages. This is going to be a simple Create React App application, using Semantic UI to give us some structure to the page.

Firstly, we need to actually create the application. This is done by executing:

1$ create-react-app pundit-ui
2    $ cd pundit-ui
3    $ npm install

NOTE: You can use “yarn” instead of “npm” if you prefer.

We then want to add some dependencies that we need for the system:

    $ npm add --save uuid semantic-ui-css semantic-ui-react

Our UI is going to consist of a list of games that we are reporting on. These games will either be Started - in which case the match is underway - or Unstarted - in which case we are still entering the match details.

Our Unstarted Matches will be rendered by a component defined in src/UnstartedGame.js, as follows:

1// src/UnstartedGame.js
2    import React from 'react';
3    import { Segment, Grid, Form, Header, Button } from 'semantic-ui-react';
4    export default function UnstartedGame({game, onTeamUpdated, onPlayerUpdated, onCancel, onStart}) {
5        const homePlayers = [];
6        const awayPlayers = [];
7        for (let i = 1; i <= 11; ++i) {
8            homePlayers.push(<input placeholder={`Home Player ${i}`}
9                value={game.home.players[`player_${i}`] || ''}
10                onChange={(e) => onPlayerUpdated('home', `player_${i}`, e.target.value)}
11                key={`home.players.player_${i}`} />);
12            awayPlayers.push(<input placeholder={`Away Player ${i}`}
13                value={game.away.players[`player_${i}`] || ''}
14                onChange={(e) => onPlayerUpdated('away', `player_${i}`, e.target.value)}
15                key={`away.players.player_${i}`} />);
16        }
17        return (
18            <Segment>
19                <Form>
20                    <Grid>
21                        <Grid.Row columns={1}>
22                            <Grid.Column>
23                                <Header as='h2' textAlign='center'>New Match</Header>
24                            </Grid.Column>
25                        </Grid.Row>
26                        <Grid.Row columns={2}>
27                                <Grid.Column>
28                                    <input placeholder="Home Team"
29                                        value={game.home.team}
30                                        onChange={(e) => onTeamUpdated('home', e.target.value)} />
31                                </Grid.Column>
32                                <Grid.Column>
33                                    <input placeholder="Away Team"
34                                        value={game.away.team}
35                                        onChange={(e) => onTeamUpdated('away', e.target.value)} />
36                                </Grid.Column>
37                        </Grid.Row>
38                        <Grid.Row columns={1}>
39                            <Grid.Column>
40                                <Header as='h2' textAlign='center'>Players</Header>
41                            </Grid.Column>
42                        </Grid.Row>
43                        <Grid.Row columns={2}>
44                                <Grid.Column>{homePlayers}</Grid.Column>
45                                <Grid.Column>{awayPlayers}</Grid.Column>
46                        </Grid.Row>
47                        <Grid.Row columns={1}>
48                            <Grid.Column textAlign="right">
49                                <Button.Group>
50                                    <Button primary onClick={onStart}>Start Game</Button>
51                                    <Button.Or />
52                                    <Button negative onClick={onCancel}>Cancel</Button>
53                                </Button.Group>
54                            </Grid.Column>
55                        </Grid.Row>
56                    </Grid>
57                </Form>
58            </Segment>
59        );
60    }

This renders a large form that has fields for: home team, away team, 11 home players and 11 away players.

Our Started Matches will be rendered by a component defined in src/StartedGame.js, as follows:

1// src/StartedGame.js
2    import React from 'react';
3    import { Segment, Grid, Header, Button, Label, Dropdown, Menu } from 'semantic-ui-react';
4    const gameState = {
5        'first half': 'First Half',
6        'second half': 'Second Half',
7        'finished': 'Full Time',
8        'extra time': 'Extra Time'
9    };
10    export default function StartedGame({ game, onGoal, onCard, onGameEvent }) {
11        const homePlayers = [];
12        const awayPlayers = [];
13        for (let i = 1; i <= 11; ++i) {
14            const playerId = `player_${i}`;
15            let homeLabel;
16            if (game.home.cards[playerId]) {
17                homeLabel=<Label color={game.home.cards[playerId]} ribbon>{game.home.players[playerId]}</Label>;
18            } else {
19                homeLabel = game.home.players[playerId];
20            }
21            let awayLabel;
22            if (game.away.cards[playerId]) {
23                awayLabel=<Label color={game.away.cards[playerId]} ribbon>{game.away.players[playerId]}</Label>;
24            } else {
25                awayLabel = game.away.players[playerId];
26            }
27            homePlayers.push(
28                <Dropdown text={homeLabel}
29                    pointing="left"
30                    className="link item"
31                    key={`home.players.${playerId}}`}>
32                    <Dropdown.Menu>
33                        <Dropdown.Item onClick={() => onGoal('home', playerId, 'home')}>Goal</Dropdown.Item>
34                        <Dropdown.Item onClick={() => onGoal('home', playerId, 'away')}>Own Goal</Dropdown.Item>
35                        <Dropdown.Item onClick={() => onCard('home', playerId, 'yellow')}>Yellow Card</Dropdown.Item>
36                        <Dropdown.Item onClick={() => onCard('home', playerId, 'red')}>Red Card</Dropdown.Item>
37                    </Dropdown.Menu>
38                </Dropdown>
39            );
40            awayPlayers.push(
41                <Dropdown text={awayLabel}
42                    pointing="left"
43                    className="link item"
44                    key={`away.players.${playerId}}`}>
45                    <Dropdown.Menu>
46                    <Dropdown.Item onClick={() => onGoal('away', playerId, 'away')}>Goal</Dropdown.Item>
47                    <Dropdown.Item onClick={() => onGoal('away', playerId, 'home')}>Own Goal</Dropdown.Item>
48                    <Dropdown.Item onClick={() => onCard('away', playerId, 'yellow')}>Yellow Card</Dropdown.Item>
49                    <Dropdown.Item onClick={() => onCard('away', playerId, 'red')}>Red Card</Dropdown.Item>
50                </Dropdown.Menu>
51                </Dropdown>
52            );
53        }
54        return (
55            <Segment>
56                <Grid>
57                    <Grid.Row columns={1}>
58                        <Grid.Column>
59                            <Header as='h2' textAlign='center'>Match</Header>
60                        </Grid.Column>
61                    </Grid.Row>
62                    <Grid.Row columns={2}>
63                        <Grid.Column textAlign="right">
64                            <Label>
65                                {game.home.team}
66                                <Label.Detail>{game.home.score}</Label.Detail>
67                            </Label>
68                        </Grid.Column>
69                        <Grid.Column>
70                            <Label>
71                                {game.away.team}
72                                <Label.Detail>{game.away.score}</Label.Detail>
73                            </Label>
74                        </Grid.Column>
75                    </Grid.Row>
76                    <Grid.Row columns={1}>
77                        <Grid.Column textAlign='center'>
78                            {gameState[game.state]}
79                        </Grid.Column>
80                    </Grid.Row>
81                    <Grid.Row columns={1}>
82                        <Grid.Column>
83                            <Header as='h2' textAlign='center'>Players</Header>
84                        </Grid.Column>
85                    </Grid.Row>
86                    <Grid.Row columns={2}>
87                        <Grid.Column>
88                            <Menu vertical borderless secondary style={{width: "100%"}}>{homePlayers}</Menu>
89                        </Grid.Column>
90                        <Grid.Column>
91                            <Menu vertical borderless secondary style={{width: "100%"}}>{awayPlayers}</Menu>
92                        </Grid.Column>
93                    </Grid.Row>
94                    <Grid.Row columns={1}>
95                        <Grid.Column textAlign="right">
96                            <Button.Group>
97                                <Button primary onClick={() => onGameEvent('finished')}>Finish Game</Button>
98                                <Button onClick={() => onGameEvent('second half')}>Half Time</Button>
99                                <Button onClick={() => onGameEvent('extra time')}>Extra Time</Button>
100                            </Button.Group>
101                        </Grid.Column>
102                    </Grid.Row>
103                </Grid>
104            </Segment>
105        );
106    }

This renders a view that is similar to the previous, but instead of being a form that can be entered it is read-only and has buttons to click to indicate that events have happened. These events can be match-level events - half time, extra time and finish game - or player events - goal scored or card received.

We then have a single component that displays a list of all the games we are currently working with. This is in src/Games.js as follows:

1// src/Games.js
2    import React from 'react';
3    import { Container, Segment, Button } from 'semantic-ui-react';
4    import uuid from 'uuid/v4';
5    import StartedGame from './StartedGame';
6    import UnstartedGame from './UnstartedGame';
7    export default class Games extends React.Component {
8        state = {
9            games: []
10        }
11        newGameHandler = this.newGame.bind(this)
12        updateTeamHandler = this.updateTeam.bind(this)
13        updatePlayerHandler = this.updatePlayer.bind(this)
14        startGameHandler = this.startGame.bind(this)
15        cancelGameHandler = this.cancelGame.bind(this)
16        goalHandler = this.goalScored.bind(this)
17        cardHandler = this.cardGiven.bind(this)
18        gameEventHandler = this.gameEvent.bind(this)
19        render() {
20            const renderedGames = this.state.games
21                .map((game, index) => {
22                    if (game.state !== 'unstarted') {
23                        return <StartedGame game={game}
24                            key={game.id}
25                            onGoal={(team, player, goalFor) => this.goalHandler(game.id, team, player, goalFor)}
26                            onCard={(team, player, card) => this.cardHandler(game.id, team, player, card)}
27                            onGameEvent={(event) => this.gameEventHandler(game.id, event)} />;
28                    } else {
29                        return <UnstartedGame game={game}
30                            key={game.id}
31                            onTeamUpdated={(team, value) => this.updateTeamHandler(game.id, team, value)}
32                            onPlayerUpdated={(team, player, value) => this.updatePlayerHandler(game.id, team, player, value)}
33                            onCancel={() => this.cancelGameHandler(game.id)}
34                            onStart={() => this.startGameHandler(game.id)} />;
35                    }
36                });
37            return (
38                <Container>
39                    <Segment.Group>
40                        {renderedGames}
41                    </Segment.Group>
42                    <Button onClick={this.newGameHandler}>New Match</Button>
43                </Container>
44            )
45        }
46        goalScored(gameId, team, player, goalFor) {
47            const { games } = this.state;
48            const newGames = games.map((game) => {
49                if (game.id === gameId) {
50                    game[goalFor].score++;
51                }
52                return game;
53            });
54            this.setState({
55                games: newGames
56            });
57        }
58        cardGiven(gameId, team, player, card) {
59            const { games } = this.state;
60            const newGames = games.map((game) => {
61                if (game.id === gameId) {
62                    game[team].cards[player] = card;
63                }
64                return game;
65            });
66            this.setState({
67                games: newGames
68            });
69        }
70        gameEvent(gameId, event) {
71            const { games } = this.state;
72            const newGames = games.map((game) => {
73                if (game.id === gameId) {
74                    game.state = event;
75                }
76                return game;
77            });
78            this.setState({
79                games: newGames
80            });
81        }
82        newGame() {
83            const { games } = this.state;
84            const newGames = [
85                ...games,
86                {
87                    id: uuid(),
88                    state: 'unstarted',
89                    home: {
90                        team: '',
91                        score: 0,
92                        players: {},
93                        cards: {}
94                    },
95                    away: {
96                        team: '',
97                        score: 0,
98                        players: {},
99                        cards: {}
100                    }
101                }
102            ];
103            this.setState({
104                games: newGames
105            });
106        }
107        updateTeam(id, team, value) {
108            const { games } = this.state;
109            const newGames = games.map((game) => {
110                if (game.id === id) {
111                    game[team].team = value;
112                }
113                return game;
114            });
115            this.setState({
116                games: newGames
117            });
118        }
119        updatePlayer(id, team, player, value) {
120            const { games } = this.state;
121            const newGames = games.map((game) => {
122                if (game.id === id) {
123                    game[team].players[player] = value;
124                }
125                return game;
126            });
127            this.setState({
128                games: newGames
129            });
130        }
131        startGame(id) {
132            const { games } = this.state;
133            const newGames = games.map((game) => {
134                if (game.id === id) {
135                    game.state = 'first half';
136                }
137                return game;
138            });
139            this.setState({
140                games: newGames
141            });
142        }
143        cancelGame(id) {
144            const { games } = this.state;
145            const newGames = games.filter((game) => game.id !== id);
146            this.setState({
147                games: newGames
148            });
149        }
150    }

This simply renders a list of games, using the appropriate component to render it depending on whether the game has started or finished. It also handles all of the events that can happen in the game, updating our state and ensuring that the games are re-rendered as needed.

Finally we can update our map App class in src/App.js to render this list of games:

1// src/App.js
2    import React, { Component } from 'react';
3    import Games from './Games';
4    class App extends Component {
5      render() {
6        return (
7          <div className="App">
8            <Games />
9          </div>
10        );
11      }
12    }
13    export default App;

And the main index of the entire page, in src/index.js, ensuring that our styles are loaded correctly:

1// src/index.js
2    import React from 'react';
3    import ReactDOM from 'react-dom';
4    import 'semantic-ui-css/semantic.min.css';
5    import App from './App';
6    ReactDOM.render(<App />, document.getElementById('root'));

At this point we can run our application using npm start and see the Pundit UI that we have built.

Triggering backend events

Now that we’ve got our pundit UI, we want it to trigger messages on our backend. This will be done using the Axios library to make HTTP calls to the backend.

Firstly we need to install Axios:

    npm install --save axios

Then we make use of it in our application. All of this functionality goes in src/Games.js, which is responsible for handling our events.

Firstly we need to actually include Axios and create a client to use. For this, add the following to the top of the file:

1// src/Games.js
2    import axios from 'axios';
3    
4    const axiosClient = axios.create({
5        baseURL: 'http://localhost:8080'
6    });

Then we need to actually make the API calls to trigger the messages. These are done in the goalScored, cardGiven and gameEvent methods, as follows:

1// src/Games.js
2        goalScored(gameId, team, player, goalFor) {
3            const { games } = this.state;
4            const newGames = games.map((game) => {
5                if (game.id === gameId) {
6                    game[goalFor].score++;
7                }
8                axiosClient.post('/goal', {
9                    player: game[team].players[player],
10                    forTeam: goalFor,
11                    homeTeam: game.home.team,
12                    awayTeam: game.away.team,
13                    homeScore: game.home.score,
14                    awayScore: game.away.score,
15                    ownGoal: team !== goalFor
16                });
17                return game;
18            });
19            this.setState({
20                games: newGames
21            });
22        }
23        cardGiven(gameId, team, player, card) {
24            const { games } = this.state;
25            const newGames = games.map((game) => {
26                if (game.id === gameId) {
27                    game[team].cards[player] = card;
28                }
29                axiosClient.post('/card', {
30                    team: game[team].team,
31                    player: game[team].players[player],
32                    card
33                });
34                return game;
35            });
36            this.setState({
37                games: newGames
38            });
39        }
40        gameEvent(gameId, event) {
41            const { games } = this.state;
42            const newGames = games.map((game) => {
43                if (game.id === gameId) {
44                    game.state = event;
45                }
46                axiosClient.post('/match', {
47                    event,
48                    homeTeam: game.home.team,
49                    awayTeam: game.away.team,
50                    homeScore: game.home.score,
51                    awayScore: game.away.score
52                });
53                return game;
54            });
55            this.setState({
56                games: newGames
57            });
58        }

Most of this is simply extracting the data from the current game state to send to the server.

We can now use this UI and see the events appearing in the Pusher debug dashboard.

go-react-football-feed-demo-with-event-creator

Live feed of events

We are going to add our live feed to a Bootstrap enabled page using the Bootstrap Notify plugin. This can be used on any website that uses Bootstrap, but for our example we are going to use a single static HTML file as follows:

1<!DOCTYPE html>
2    <html lang="en">
3    <head>
4        <meta charset="utf-8">
5        <meta http-equiv="X-UA-Compatible" content="IE=edge">
6        <meta name="viewport" content="width=device-width, initial-scale=1">
7        <title>Football Feed</title>
8        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
9            crossorigin="anonymous">
10    </head>
11    <body>
12        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
13        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
14            crossorigin="anonymous"></script>
15        <script src="https://cdnjs.cloudflare.com/ajax/libs/mouse0270-bootstrap-notify/3.1.7/bootstrap-notify.js" integrity="sha256-ZfyZUBGHlJunePNMsBqgGX3xHMv4kaCZ5Hj+8Txwd9c="
16            crossorigin="anonymous"></script>
17        <script src="https://js.pusher.com/4.2/pusher.min.js"></script>
18        <script>
19            const pusher = new Pusher('PUSHER_KEY', {
20              cluster: 'PUSHER_CLUSTER'
21            });
22            const channel = pusher.subscribe('match');
23            channel.bind('goal', function(data) {
24                let message = data.Player + ' scored!';
25                if (data.OwnGoal) {
26                    message += ' (OG)';
27                }
28                $.notify({
29                    title: message,
30                    message: `${data.HomeTeam} ${data.HomeScore} - ${data.AwayScore} ${data.AwayTeam}`
31                }, {
32                    type: 'success',
33                    allow_dismiss: true,
34                    newest_on_top: false,
35                });
36            });
37            channel.bind('card', function(data) {
38                let message;
39                let type;
40                if (data.Card === 'yellow') {
41                    message = `Yellow card for ${data.Player} (${data.Team})`;
42                    type = 'warning';
43                } else {
44                    message = `Red card for ${data.Player} (${data.Team})`;
45                    type = 'danger';
46                }
47                $.notify({
48                    message: message
49                }, {
50                    type: type,
51                    allow_dismiss: true,
52                    newest_on_top: false,
53                });
54            });
55            channel.bind('match', function(data) {
56                let message;
57                if (data.Event === 'finished') {
58                    message = 'Full Time';
59                } else if (data.Event === 'second half') {
60                    message = 'Half Time';
61                } else if (data.Event === 'extra time') {
62                    message = 'Extra Time';
63                }
64                $.notify({
65                    title: message,
66                    message: `${data.HomeTeam} ${data.HomeScore} - ${data.AwayScore} ${data.AwayTeam}`
67                }, {
68                    type: 'info',
69                    allow_dismiss: true,
70                    newest_on_top: false,
71                });
72            });
73        </script>
74    </body>
75    </html>

NOTE: make sure that PUSHER_KEY and PUSHER_CLUSTER are the same values as the backend is using.

The above code can be used on any website that uses Bootstrap, so you can easily include it in an existing site to give your users live football news without leaving.

Ensure that the backend and pundit UI is running, and then open index.html in a web browser to see the messages appearing as you trigger events.

1# run backend
2    $ go run football-feed.go
3    
4    # run pundit UI
5    $ npm start
go-react-football-feed-demo

Summary

This article shows how to use Pusher Channels to trigger a live feed of events on a website. The full source code can be found on GitHub. Why not try extending it to support more actions, or even different games.