Create a football results feed with Go and React
You will need Go, dep, Node and npm installed on your machine.
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.
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. This is done by visiting the Pusher dashboard and logging in, creating a new account if needed. Next click on Channels apps on the sidebar, followed by Create Channels 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.
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:
# Mac and Linux
$ mkdir -p $GOPATH/src/pusher/football-feed
$ cd $GOPATH/src/pusher/football-feed
# Windows Powershell
mkdir -path $env:GOPATH/src/pusher/football-feed
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:
// internal/notifier/notifier.go
package notifier
import (
"github.com/pusher/pusher-http-go"
)
type Message interface{}
type MatchMessage struct {
Event string `json:event`
HomeTeam string `json:homeTeam`
AwayTeam string `json:awayTeam`
HomeScore uint16 `json:homeScore`
AwayScore uint16 `json_awayScore`
}
type GoalMessage struct {
Player string `json:player`
ForTeam string `json:forTeam`
HomeTeam string `json:homeTeam`
AwayTeam string `json:awayTeam`
HomeScore uint16 `json:homeScore`
AwayScore uint16 `json_awayScore`
OwnGoal bool `json:ownGoal`
}
type CardMessage struct {
Team string `json:team`
Player string `json:player`
Card string `json:card`
}
type Notifier struct {
notifyChannel chan<- Message
}
func notifier(notifyChannel <-chan Message) {
client := pusher.Client{
AppId: "PUSHER_APP_ID",
Key: "PUSHER_KEY",
Secret: "PUSHER_SECRET",
Cluster: "PUSHER_CLUSTER",
Secure: true,
}
for {
message := <-notifyChannel
switch payload := message.(type) {
case GoalMessage:
client.Trigger("match", "goal", payload)
case CardMessage:
client.Trigger("match", "card", payload)
case MatchMessage:
client.Trigger("match", "match", payload)
}
}
}
func New() Notifier {
notifyChannel := make(chan Message)
go notifier(notifyChannel)
return Notifier{notifyChannel}
}
func (notifier *Notifier) Notify(msg Message) {
notifier.notifyChannel <- msg
}
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:
// internal/webapp/webapp.go
package webapp
import (
"net/http"
"pusher/football-feed/internal/notifier"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
func StartServer(notify *notifier.Notifier) {
r := gin.Default()
r.Use(cors.Default())
r.POST("/match", func(c *gin.Context) {
var json notifier.MatchMessage
if err := c.BindJSON(&json); err == nil {
notify.Notify(json)
c.JSON(http.StatusCreated, json)
} else {
c.JSON(http.StatusBadRequest, gin.H{})
}
})
r.POST("/goal", func(c *gin.Context) {
var json notifier.GoalMessage
if err := c.BindJSON(&json); err == nil {
notify.Notify(json)
c.JSON(http.StatusCreated, json)
} else {
c.JSON(http.StatusBadRequest, gin.H{})
}
})
r.POST("/card", func(c *gin.Context) {
var json notifier.CardMessage
if err := c.BindJSON(&json); err == nil {
notify.Notify(json)
c.JSON(http.StatusCreated, json)
} else {
c.JSON(http.StatusBadRequest, gin.H{})
}
})
r.Run()
}
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:
// football-feed.go
package main
import (
"pusher/football-feed/internal/notifier"
"pusher/football-feed/internal/webapp"
)
func main() {
notifier := notifier.New()
webapp.StartServer(¬ifier)
}
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
:
$ go run football-feed.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /match --> pusher/football-feed/internal/webapp.StartServer.func1 (4 handlers)
[GIN-debug] POST /goal --> pusher/football-feed/internal/webapp.StartServer.func2 (4 handlers)
[GIN-debug] POST /card --> pusher/football-feed/internal/webapp.StartServer.func3 (4 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[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:
> $ curl -v -X POST http://localhost:8080/card -H "Content-Type: application-json" --data '{"team": "Russia", "player": "Aleksandr Golovin", "card": "yellow"}'
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> POST /card HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> Content-Type: application-json
> Content-Length: 67
>
* upload completely sent off: 67 out of 67 bytes
< HTTP/1.1 201 Created
< Content-Type: application/json; charset=utf-8
< Date: Mon, 25 Jun 2018 13:09:21 GMT
< Content-Length: 62
<
* Connection #0 to host localhost left intact
{"Team":"Russia","Player":"Aleksandr Golovin","Card":"yellow"}
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:
$ create-react-app pundit-ui
$ cd pundit-ui
$ 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:
// src/UnstartedGame.js
import React from 'react';
import { Segment, Grid, Form, Header, Button } from 'semantic-ui-react';
export default function UnstartedGame({game, onTeamUpdated, onPlayerUpdated, onCancel, onStart}) {
const homePlayers = [];
const awayPlayers = [];
for (let i = 1; i <= 11; ++i) {
homePlayers.push(<input placeholder={`Home Player ${i}`}
value={game.home.players[`player_${i}`] || ''}
onChange={(e) => onPlayerUpdated('home', `player_${i}`, e.target.value)}
key={`home.players.player_${i}`} />);
awayPlayers.push(<input placeholder={`Away Player ${i}`}
value={game.away.players[`player_${i}`] || ''}
onChange={(e) => onPlayerUpdated('away', `player_${i}`, e.target.value)}
key={`away.players.player_${i}`} />);
}
return (
<Segment>
<Form>
<Grid>
<Grid.Row columns={1}>
<Grid.Column>
<Header as='h2' textAlign='center'>New Match</Header>
</Grid.Column>
</Grid.Row>
<Grid.Row columns={2}>
<Grid.Column>
<input placeholder="Home Team"
value={game.home.team}
onChange={(e) => onTeamUpdated('home', e.target.value)} />
</Grid.Column>
<Grid.Column>
<input placeholder="Away Team"
value={game.away.team}
onChange={(e) => onTeamUpdated('away', e.target.value)} />
</Grid.Column>
</Grid.Row>
<Grid.Row columns={1}>
<Grid.Column>
<Header as='h2' textAlign='center'>Players</Header>
</Grid.Column>
</Grid.Row>
<Grid.Row columns={2}>
<Grid.Column>{homePlayers}</Grid.Column>
<Grid.Column>{awayPlayers}</Grid.Column>
</Grid.Row>
<Grid.Row columns={1}>
<Grid.Column textAlign="right">
<Button.Group>
<Button primary onClick={onStart}>Start Game</Button>
<Button.Or />
<Button negative onClick={onCancel}>Cancel</Button>
</Button.Group>
</Grid.Column>
</Grid.Row>
</Grid>
</Form>
</Segment>
);
}
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:
// src/StartedGame.js
import React from 'react';
import { Segment, Grid, Header, Button, Label, Dropdown, Menu } from 'semantic-ui-react';
const gameState = {
'first half': 'First Half',
'second half': 'Second Half',
'finished': 'Full Time',
'extra time': 'Extra Time'
};
export default function StartedGame({ game, onGoal, onCard, onGameEvent }) {
const homePlayers = [];
const awayPlayers = [];
for (let i = 1; i <= 11; ++i) {
const playerId = `player_${i}`;
let homeLabel;
if (game.home.cards[playerId]) {
homeLabel=<Label color={game.home.cards[playerId]} ribbon>{game.home.players[playerId]}</Label>;
} else {
homeLabel = game.home.players[playerId];
}
let awayLabel;
if (game.away.cards[playerId]) {
awayLabel=<Label color={game.away.cards[playerId]} ribbon>{game.away.players[playerId]}</Label>;
} else {
awayLabel = game.away.players[playerId];
}
homePlayers.push(
<Dropdown text={homeLabel}
pointing="left"
className="link item"
key={`home.players.${playerId}}`}>
<Dropdown.Menu>
<Dropdown.Item onClick={() => onGoal('home', playerId, 'home')}>Goal</Dropdown.Item>
<Dropdown.Item onClick={() => onGoal('home', playerId, 'away')}>Own Goal</Dropdown.Item>
<Dropdown.Item onClick={() => onCard('home', playerId, 'yellow')}>Yellow Card</Dropdown.Item>
<Dropdown.Item onClick={() => onCard('home', playerId, 'red')}>Red Card</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
);
awayPlayers.push(
<Dropdown text={awayLabel}
pointing="left"
className="link item"
key={`away.players.${playerId}}`}>
<Dropdown.Menu>
<Dropdown.Item onClick={() => onGoal('away', playerId, 'away')}>Goal</Dropdown.Item>
<Dropdown.Item onClick={() => onGoal('away', playerId, 'home')}>Own Goal</Dropdown.Item>
<Dropdown.Item onClick={() => onCard('away', playerId, 'yellow')}>Yellow Card</Dropdown.Item>
<Dropdown.Item onClick={() => onCard('away', playerId, 'red')}>Red Card</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
);
}
return (
<Segment>
<Grid>
<Grid.Row columns={1}>
<Grid.Column>
<Header as='h2' textAlign='center'>Match</Header>
</Grid.Column>
</Grid.Row>
<Grid.Row columns={2}>
<Grid.Column textAlign="right">
<Label>
{game.home.team}
<Label.Detail>{game.home.score}</Label.Detail>
</Label>
</Grid.Column>
<Grid.Column>
<Label>
{game.away.team}
<Label.Detail>{game.away.score}</Label.Detail>
</Label>
</Grid.Column>
</Grid.Row>
<Grid.Row columns={1}>
<Grid.Column textAlign='center'>
{gameState[game.state]}
</Grid.Column>
</Grid.Row>
<Grid.Row columns={1}>
<Grid.Column>
<Header as='h2' textAlign='center'>Players</Header>
</Grid.Column>
</Grid.Row>
<Grid.Row columns={2}>
<Grid.Column>
<Menu vertical borderless secondary style={{width: "100%"}}>{homePlayers}</Menu>
</Grid.Column>
<Grid.Column>
<Menu vertical borderless secondary style={{width: "100%"}}>{awayPlayers}</Menu>
</Grid.Column>
</Grid.Row>
<Grid.Row columns={1}>
<Grid.Column textAlign="right">
<Button.Group>
<Button primary onClick={() => onGameEvent('finished')}>Finish Game</Button>
<Button onClick={() => onGameEvent('second half')}>Half Time</Button>
<Button onClick={() => onGameEvent('extra time')}>Extra Time</Button>
</Button.Group>
</Grid.Column>
</Grid.Row>
</Grid>
</Segment>
);
}
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:
// src/Games.js
import React from 'react';
import { Container, Segment, Button } from 'semantic-ui-react';
import uuid from 'uuid/v4';
import StartedGame from './StartedGame';
import UnstartedGame from './UnstartedGame';
export default class Games extends React.Component {
state = {
games: []
}
newGameHandler = this.newGame.bind(this)
updateTeamHandler = this.updateTeam.bind(this)
updatePlayerHandler = this.updatePlayer.bind(this)
startGameHandler = this.startGame.bind(this)
cancelGameHandler = this.cancelGame.bind(this)
goalHandler = this.goalScored.bind(this)
cardHandler = this.cardGiven.bind(this)
gameEventHandler = this.gameEvent.bind(this)
render() {
const renderedGames = this.state.games
.map((game, index) => {
if (game.state !== 'unstarted') {
return <StartedGame game={game}
key={game.id}
onGoal={(team, player, goalFor) => this.goalHandler(game.id, team, player, goalFor)}
onCard={(team, player, card) => this.cardHandler(game.id, team, player, card)}
onGameEvent={(event) => this.gameEventHandler(game.id, event)} />;
} else {
return <UnstartedGame game={game}
key={game.id}
onTeamUpdated={(team, value) => this.updateTeamHandler(game.id, team, value)}
onPlayerUpdated={(team, player, value) => this.updatePlayerHandler(game.id, team, player, value)}
onCancel={() => this.cancelGameHandler(game.id)}
onStart={() => this.startGameHandler(game.id)} />;
}
});
return (
<Container>
<Segment.Group>
{renderedGames}
</Segment.Group>
<Button onClick={this.newGameHandler}>New Match</Button>
</Container>
)
}
goalScored(gameId, team, player, goalFor) {
const { games } = this.state;
const newGames = games.map((game) => {
if (game.id === gameId) {
game[goalFor].score++;
}
return game;
});
this.setState({
games: newGames
});
}
cardGiven(gameId, team, player, card) {
const { games } = this.state;
const newGames = games.map((game) => {
if (game.id === gameId) {
game[team].cards[player] = card;
}
return game;
});
this.setState({
games: newGames
});
}
gameEvent(gameId, event) {
const { games } = this.state;
const newGames = games.map((game) => {
if (game.id === gameId) {
game.state = event;
}
return game;
});
this.setState({
games: newGames
});
}
newGame() {
const { games } = this.state;
const newGames = [
...games,
{
id: uuid(),
state: 'unstarted',
home: {
team: '',
score: 0,
players: {},
cards: {}
},
away: {
team: '',
score: 0,
players: {},
cards: {}
}
}
];
this.setState({
games: newGames
});
}
updateTeam(id, team, value) {
const { games } = this.state;
const newGames = games.map((game) => {
if (game.id === id) {
game[team].team = value;
}
return game;
});
this.setState({
games: newGames
});
}
updatePlayer(id, team, player, value) {
const { games } = this.state;
const newGames = games.map((game) => {
if (game.id === id) {
game[team].players[player] = value;
}
return game;
});
this.setState({
games: newGames
});
}
startGame(id) {
const { games } = this.state;
const newGames = games.map((game) => {
if (game.id === id) {
game.state = 'first half';
}
return game;
});
this.setState({
games: newGames
});
}
cancelGame(id) {
const { games } = this.state;
const newGames = games.filter((game) => game.id !== id);
this.setState({
games: newGames
});
}
}
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:
// src/App.js
import React, { Component } from 'react';
import Games from './Games';
class App extends Component {
render() {
return (
<div className="App">
<Games />
</div>
);
}
}
export default App;
And the main index of the entire page, in src/index.js
, ensuring that our styles are loaded correctly:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import 'semantic-ui-css/semantic.min.css';
import App from './App';
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:
// src/Games.js
import axios from 'axios';
const axiosClient = axios.create({
baseURL: 'http://localhost:8080'
});
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:
// src/Games.js
goalScored(gameId, team, player, goalFor) {
const { games } = this.state;
const newGames = games.map((game) => {
if (game.id === gameId) {
game[goalFor].score++;
}
axiosClient.post('/goal', {
player: game[team].players[player],
forTeam: goalFor,
homeTeam: game.home.team,
awayTeam: game.away.team,
homeScore: game.home.score,
awayScore: game.away.score,
ownGoal: team !== goalFor
});
return game;
});
this.setState({
games: newGames
});
}
cardGiven(gameId, team, player, card) {
const { games } = this.state;
const newGames = games.map((game) => {
if (game.id === gameId) {
game[team].cards[player] = card;
}
axiosClient.post('/card', {
team: game[team].team,
player: game[team].players[player],
card
});
return game;
});
this.setState({
games: newGames
});
}
gameEvent(gameId, event) {
const { games } = this.state;
const newGames = games.map((game) => {
if (game.id === gameId) {
game.state = event;
}
axiosClient.post('/match', {
event,
homeTeam: game.home.team,
awayTeam: game.away.team,
homeScore: game.home.score,
awayScore: game.away.score
});
return game;
});
this.setState({
games: newGames
});
}
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.
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:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Football Feed</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u"
crossorigin="anonymous">
</head>
<body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mouse0270-bootstrap-notify/3.1.7/bootstrap-notify.js" integrity="sha256-ZfyZUBGHlJunePNMsBqgGX3xHMv4kaCZ5Hj+8Txwd9c="
crossorigin="anonymous"></script>
<script src="https://js.pusher.com/4.2/pusher.min.js"></script>
<script>
const pusher = new Pusher('PUSHER_KEY', {
cluster: 'PUSHER_CLUSTER'
});
const channel = pusher.subscribe('match');
channel.bind('goal', function(data) {
let message = data.Player + ' scored!';
if (data.OwnGoal) {
message += ' (OG)';
}
$.notify({
title: message,
message: `${data.HomeTeam} ${data.HomeScore} - ${data.AwayScore} ${data.AwayTeam}`
}, {
type: 'success',
allow_dismiss: true,
newest_on_top: false,
});
});
channel.bind('card', function(data) {
let message;
let type;
if (data.Card === 'yellow') {
message = `Yellow card for ${data.Player} (${data.Team})`;
type = 'warning';
} else {
message = `Red card for ${data.Player} (${data.Team})`;
type = 'danger';
}
$.notify({
message: message
}, {
type: type,
allow_dismiss: true,
newest_on_top: false,
});
});
channel.bind('match', function(data) {
let message;
if (data.Event === 'finished') {
message = 'Full Time';
} else if (data.Event === 'second half') {
message = 'Half Time';
} else if (data.Event === 'extra time') {
message = 'Extra Time';
}
$.notify({
title: message,
message: `${data.HomeTeam} ${data.HomeScore} - ${data.AwayScore} ${data.AwayTeam}`
}, {
type: 'info',
allow_dismiss: true,
newest_on_top: false,
});
});
</script>
</body>
</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.
# run backend
$ go run football-feed.go
# run pundit UI
$ npm start
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.
2 July 2018
by Graham Cox