Build Tic-Tac-Toe with React and Fauna
Tags: react, fauna, node, firebase
Published: 2020-11-30 22:57:50

[Git Repo](https://github.com/Xuriwork/Fauna-Tic-Tac-Toe)

In this tutorial we will be building multiplayer Tic-Tac-Toe, using:

[React](https://reactjs.org) (Front-end)
[Fauna](https://fauna.com) (Database)
[Firebase](https://firebase.google.com) (Authentication)
[Node.js](https://nodejs.org) (Server)
[Socket.io](https://socket.io)

Fauna
The star of the show, FaunaDB is a high-speed serverless NoSQL database. It provides a very simple and easy to use API with various drivers in several programming languages.

# Create a React App
To quickly scaffold our app we will use [create-react-app](https://create-react-app.dev)
```bash
npx create-react-app tic-tac-toe
```

Install the needed dependencies
```
yarn add node-sass react-router-dom cors http express concurrently faunadb firebase nanoid socket.io socket.io-client 
```

Edit your package.json file to like something like this:
```json
  "scripts": {
    "start": "react-scripts start",
    "server": "nodemon -r esm server/index.js",
    "dev": "concurrently \"nodemon ./server/index.js\" \"react-scripts start\"",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
```

Sign up for a FaunaDB account, if you haven't already.
Once you're signed in, go to the FaunaDB dashboard and click on New Database

![FaunaDB Dashboard](https://dev-to-uploads.s3.amazonaws.com/i/1zugmw7d9p5q7ncoc4uy.png)

You can name your database whatever you would like, for the sake of this tutorial we will name ours "TicTacToe".

![Creating a new database](https://dev-to-uploads.s3.amazonaws.com/i/3fmkfwamtj5ej9pqqiio.png)

Click **New collection**, which you can find either on the current page or in the **Collections** tab.
We'll name this collection "Rooms". It will be used to store all of the game rooms created.

![Creating a new collection](https://dev-to-uploads.s3.amazonaws.com/i/cdrcfxb921fl9zvotirb.png)

In the **Rooms** Collections click **New Index** called "room_by_id", with the following values

![Creating a new index](https://dev-to-uploads.s3.amazonaws.com/i/h0j7tmgwb7hrmhvub00a.png)

Click on the **Security** tab and create a new key, choose the role **Server**, and we'll name our key "ServerKey", click save

![Creating a new key](https://dev-to-uploads.s3.amazonaws.com/i/66h3bl3mvdcsjnkiwk0u.png)

You should get the secret key on the next screen. Create a file in the root directory of the react app we created earlier, and save the key in there.

![Secret Key](https://dev-to-uploads.s3.amazonaws.com/i/p5omf9zcsqr96ad5m2gt.png)

Now we'll create a key for the client side, REACT_APP_FAUNADB_CLIENT_KEY and add it to the .env file

Copy your Key's Secret and paste it as a variable called **REACT_APP_FAUNADB_CLIENT_KEY** into a file called .env in the root directory of your project.

*To access Environment variables in Create React App you need to prefix the variable name with **REACT_APP***

In the Security tab go to the Roles section and add a new custom role called Client

![Editting roles](https://dev-to-uploads.s3.amazonaws.com/i/ne0wvhndfoz2bk2cb4tv.png)

![Creating a client key](https://dev-to-uploads.s3.amazonaws.com/i/b8pborltlcxym4g6dkpf.png)

Go to the [Firebase console](https://console.firebase.google.com)
and click add project, give your project a name, we won't need Google Analytics for this project so we will disable it.

![Add project](https://dev-to-uploads.s3.amazonaws.com/i/6v03vbtixiubpblyk9dt.png)

![Name your project](https://dev-to-uploads.s3.amazonaws.com/i/iznry4awem63cn5fj3fs.png)

Let's now add Firebase to our app, get started by selecting the web

![Adding Firebase to your app](https://dev-to-uploads.s3.amazonaws.com/i/twrom3ypg4ko8lj8moze.png)

Once that is finished go to the Authentication tab and click Get Started, give your app a nickname, you can use the same one as when you created the project.

![Register app](https://dev-to-uploads.s3.amazonaws.com/i/ov7ibo5imkm7bc7hbkid.png)

Once you're down, register the app. We will set up Firebase Hosting later.

Copy the content inside of the script tags, and create a file in the src directory called firebase.js

It should look something like this

```js
import firebase from 'firebase';
import '@firebase/auth';

const firebaseConfig = {
    apiKey: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    authDomain: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    databaseURL: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    projectId: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    storageBucket: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    messagingSenderId: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    appId: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
};

firebase.initializeApp(firebaseConfig);

export default firebase;
```

Now back to the Firebase site, continue to the console, go to the Authentication tab.

![Getting started with Authentication](https://dev-to-uploads.s3.amazonaws.com/i/8bmmtn4l4rndy6yi4t0s.png)

We will just use the Gmail Sign-in provider for Authentication, so enable that and save, we're done with the Firebase site for now.

## Let's define 5 queries

### Get a specific room by the roomID
```js
const getRoom = (roomID) => client.query(q.Get(q.Match(q.Index('room_by_id'), roomID)));
```

### Check if a room exists
```js
const checkIfRoomExists = (roomID) => {
	getRoom(roomID)
	.then((ret) => {
		return client.query(q.Exists(q.Ref(q.Collection('Rooms'), ret.ref.value.id)));
	});
};
```

### Create a room
```js
const createRoom = (userID, profilePictureURL) => {
    const id = nanoid();
    const cells = JSON.stringify(Array(9).fill(null));

	return client.query(
		q.Create(q.Collection('Rooms'), {
			data: {
				id,
				cells,
				players: [{ id: userID, profilePictureURL }],
			},
		})
	);
};
```
### Update the TicTacToe board
```js
const updateBoard = (roomID, cells) => {
	return getRoom(roomID)
	.then((ret) => {
		return client
			.query(
				q.Update(ret.ref, {
					data: {
						cells,
					},
				})
			)
			.then((ret) => ret.data.cells)
	})
};
```
### Add team
```js
const updateTeam = (roomID, team, userID) => {
	return getRoom(roomID)
	.then((ret) => {
		return client
			.query(
				q.Update(ret.ref, {
					data: {
						[team]: userID 
					},
				})
			)
			.then((ret) => ret.data)
	})
};
```
### All of these will be defined in a file called **faunaDB.js**

```js
import faunadb from 'faunadb';
import { nanoid } from 'nanoid';

const q = faunadb.query;

const secret = process.env.FAUNADB_SERVER_KEY ? process.env.FAUNADB_SERVER_KEY : process.env.REACT_APP_FAUNADB_CLIENT_KEY;
const client = new faunadb.Client({ secret });

const getRoom = (roomID) => client.query(q.Get(q.Match(q.Index('room_by_id'), roomID)));

const checkIfRoomExists = (roomID) => {
	getRoom(roomID)
	.then((ret) => {
		return client.query(q.Exists(q.Ref(q.Collection('Rooms'), ret.ref.value.id)));
	});
};

const createRoom = (userID, profilePictureURL) => {
    const id = nanoid();
    const cells = JSON.stringify(Array(9).fill(null));

	return client.query(
		q.Create(q.Collection('Rooms'), {
			data: {
				id,
				cells,
				players: [{ id: userID, profilePictureURL }],
			},
		})
	);
};

const updateBoard = (roomID, cells) => {
	return getRoom(roomID)
	.then((ret) => {
		return client
			.query(
				q.Update(ret.ref, {
					data: {
						cells,
					},
				})
			)
			.then((ret) => ret.data.cells)
	})
};

const updateTeam = (roomID, team, userID) => {
	return getRoom(roomID)
	.then((ret) => {
		return client
			.query(
				q.Update(ret.ref, {
					data: {
						[team]: userID 
					},
				})
			)
			.then((ret) => ret.data)
	})
};

export { getRoom, checkIfRoomExists, createRoom, updateBoard, updateTeam };
```

### The express server
```js
const express = require('express');
const http = require('http');
const cors = require('cors');
const socket = require('socket.io');
const { updateBoard, updateTeam } = require('../src/utils/faunaDB');

const app = express();
app.use(cors());

const PORT = process.env.PORT || 8000;

const server = http.createServer(app);

const io = socket(server, {
	cors: {
		origin: '<http://localhost:3000>',
		methods: ['GET', 'POST'],
	},
});

io.on('connection', (socket) => {
	console.log('New client connected');
	socket.leaveAll();
	
	socket.on('JOIN', (roomID) => {
		socket.leaveAll();
		socket.join(roomID);
		socket.roomID = roomID;
	});

	socket.on('CHOOSE_TEAM', ({ roomID, team, userID, players }) => {
		updateTeam(roomID, team, userID)
		.then((ret) => {
			const newPlayers = [...players, {[team]: ret[team]}];
			socket.emit('SET_TEAM', team);
			io.in(roomID).emit('CHOOSE_TEAM', newPlayers);
		})
		.catch((error) => console.log(error));
	});

	socket.on('MAKE_MOVE', ({ roomID, cells, id, player }) => {
		const _cells = cells;
		_cells[id] = player;
		_cells.concat(_cells);

		updateBoard(roomID, JSON.stringify(_cells))
		.then((newCells) => {
			if (player === 'X') player = 'O';
			else player = 'X';
			io.in(roomID).emit('MAKE_MOVE', { newCells: JSON.parse(newCells), newPlayer: player });
		})
		.catch((error) => console.log(error));
	});

	socket.on('REQUEST_RESTART_GAME', ({ roomID, player }) => {
		socket.to(roomID).emit('REQUEST_RESTART_GAME', player);
	});

	socket.on('RESTART_GAME', (roomID) => {
		const newCells = Array(9).fill(null);
		updateBoard(roomID, JSON.stringify(newCells))
		.then(() => io.in(roomID).emit('RESTART_GAME', { newCells }))
		.catch((error) => console.log(error));
	});

	socket.on('disconnect', () => {
		console.log('Client disconnected');
	});
});

server.listen(PORT, () => console.log(`Listening on port ${PORT}`));
```

Back to the React App.js

```js
import { BrowserRouter as Router } from 'react-router-dom';

import { AuthProvider } from './context/AuthContext';
import { PublicRoute, PrivateRoute } from './components/Routes';

import Navbar from './components/Navbar';
import GameRoom from './pages/GameRoom';
import PublicHome from './pages/PublicHome';
import PrivateHome from './pages/PrivateHome';
import JoinGame from './pages/JoinGame';
import CreateGame from './pages/CreateGame';

import './App.scss';

const App = () => {
  return (
    <AuthProvider>
      <Router>
        <Navbar />
        <div className='app-component'>
            <PublicRoute exact path='/' component={PublicHome} restricted={true} />
            <PrivateRoute path='/home' component={PrivateHome} />
            <PrivateRoute path='/create-game' component={CreateGame} />
            <PrivateRoute path='/join-game' component={JoinGame} />
            <PrivateRoute path='/room/:roomID' component={GameRoom} />
        </div>
      </Router>
    </AuthProvider>
  );
};

export default App;
```

Let's create 5 pages called CreateGame.js, JoinGame.js, and Navbar.js, PublicHome.js, and PrivateHome.js

### Navbar.js

```js
import React from 'react';
import { Link } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import firebase from '../utils/firebase';

export const Navbar = () => {
	const { isAuthenticated, handleSignIn } = useAuth();
	const handleSignOut = () => firebase.auth().signOut();

	return (
		<nav className='navbar'>
			<Link to='/'>Tic Tac Toe</Link>
			<div>
				{isAuthenticated ? (
					<button onClick={handleSignOut}>Sign out</button>
				) : (
					<>
						<button
							onClick={handleSignIn}
							style={{ marginRight: 10 }}
						>
							Sign Up
						</button>
						<button className='button-primary' onClick={handleSignIn}>
							Sign In
						</button>
					</>
				)}
			</div>
		</nav>
	);
};

export default Navbar;
```

### CreateGame.js
```js
import { useState } from 'react';
import { useHistory } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { createRoom } from '../utils/faunaDB';

const CreateGame = () => {
    const history = useHistory();
	const [roomName, setRoomName] = useState('');
	const { user } = useAuth();

	const handleCreateGame = (e) => {
        e.preventDefault();
		if (roomName.trim() === '') return;

		createRoom(user.uid, user.photoURL)
		.then((response) => {
			const id = response.data.id;
			history.push(`/room/${id}`);
		});
    };
    
    const handleOnChangeRoomName = (e) => setRoomName(e.target.value);

	return (
		<div className='join-game-page'>
			<div className='form-container'>
				<form>
					<div>
						<label htmlFor='roomName'>Room Name</label>
						<input type='text' name='roomName' id='roomName' value={roomName} onChange={handleOnChangeRoomName} />
					</div>
					<button className='button-primary' onClick={handleCreateGame}>
						Create Game
					</button>
				</form>
			</div>
		</div>
	);
};

export default CreateGame;
```

### JoinGame.js
```js
import { useHistory } from 'react-router-dom';
import { useState } from 'react';
import { checkIfRoomExists } from '../utils/faunaDB';

const JoinGame = () => {
    const history = useHistory();
    const [roomID, setRoomID] = useState('');

    const handleOnChangeRoomID = (e) => setRoomID(e.target.value);

    const handleJoinGame = (e) => {
        if (roomID.trim() === '') return;
        e.preventDefault();

        checkIfRoomExists(roomID)
        .then((ret) => {
            if (ret) history.push(`/room/${roomID}`);
            else alert('Room does not exist');
        });
    };

    return (
        <div className='join-game-page'>
            <div className='form-container'>
                <form>
                    <label htmlFor='roomID'>Room ID</label>
                    <input type='text' name='roomID' id='roomID' value={roomID} onChange={handleOnChangeRoomID} />
                    <button className='button-primary' style={{ marginTop: 10 }} onClick={handleJoinGame}>Join Game</button>
                </form>
            </div>
        </div>
    )
}

export default JoinGame;
```

### PublicHome.js
```js
const PublicHome = () => {
    return (
        <div>
            <h1>Welcome to Fauna Tic-Tac-Toe! 👋</h1>
            <button className='button-primary' style={{ marginTop: 10 }}>Learn the rules</button>
        </div>
    );
};

export default PublicHome;
```

### PrivateHome.js
```js
import { useHistory } from "react-router-dom";

const PrivateHome = () => {
	const history = useHistory();

	return (
		<div className='home-private-page'>
			<div className='container'>
				<button className='button-primary' onClick={() => history.push('/join-game')}>Join game</button>
				<button className='button-secondary' onClick={() => history.push('/create-game')}>Create game</button>
			</div>
		</div>
	);
};

export default PrivateHome;
```

Now let's create a Wrapper to protect certain Routes, in the components, create a Routes.js and add the following code:

```js
import React from 'react';
import { Route, Redirect } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

export const PrivateRoute = ({ component: Component, ...rest }) => {
  const { isAuthenticated, user } = useAuth();
  return (
    <Route {...rest} render={props => isAuthenticated
      ? <Component isAuthenticated={isAuthenticated} user={user} {...props} />
      : <Redirect to={{ pathname: '/' }} />
    }
    />
  )
};

export const PublicRoute = ({ component: Component, restricted, ...rest }) => {
  const { isAuthenticated } = useAuth();
    return (
        <Route {...rest} render={props => (
            isAuthenticated && restricted ? <Redirect to='/home' /> : <Component {...props} />
        )} />
    );
};
```

The AuthContext to check whether the user is authenticated or not

```js
import { useEffect, useState, createContext, useContext } from 'react';
import firebase from '../utils/firebase';
import Loading from '../components/Loading';

export const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);
    const isAuthenticated = !!user;

    useEffect(() => {
        firebase.auth().onAuthStateChanged((user) => {
            setUser(user);
            setLoading(false);
        });
    }, []);

    const handleSignIn = () => {
        const provider = new firebase.auth.GoogleAuthProvider();
        firebase
            .auth()
            .signInWithPopup(provider)
            .then((res) => setUser(res.user))
            .catch((error) => console.log(error.message));
    };

    if (loading) return <Loading />;

    return (
        <AuthContext.Provider value={{ user, isAuthenticated, handleSignIn }}>
            {children}
        </AuthContext.Provider>
    );
};

export const useAuth = () => useContext(AuthContext);

export default AuthContext;
```

### Board.js
```js
const Square = ({ cells, cell, onClick, isActive }) => {
  
	const checkIfIsActive = () => {
    if (!isActive) return;
		if (cells[cell] !== null) return false;
		return true;
	};

	return (
		<td className={checkIfIsActive() ? 'active' : ''} onClick={onClick}>
			{cells[cell]}
		</td>
	);
};

export const Board = ({ cells, onClick, isActive }) => {
	const renderSquare = (cell) => {
		return <Square cell={cell} cells={cells} isActive={isActive} onClick={() => onClick(cell)} />;
	};

	return (
		<table id='board'>
			<tbody>
				<tr>
					{renderSquare(0)}
					{renderSquare(1)}
					{renderSquare(2)}
				</tr>

				<tr>
					{renderSquare(3)}
					{renderSquare(4)}
					{renderSquare(5)}
				</tr>

				<tr>
					{renderSquare(6)}
					{renderSquare(7)}
					{renderSquare(8)}
				</tr>
			</tbody>
		</table>
	);
};

export default Board;
```

### GameRoom.js
```js
import Board from '../components/Board';
import { Component } from 'react';
import io from 'socket.io-client';
import { getRoom } from '../utils/faunaDB';
import Loading from '../components/Loading';

export class GameRoom extends Component {
	state = {
		loading: false,
		cells: Array(9).fill(null),
		players: [],
		player: 'X',
		team: null,
	};

	componentDidMount() {
		const {
			history,
			match: {
				params: { roomID },
			},
		} = this.props;

		getRoom(roomID)
			.then(() => this.onReady())
			.catch((error) => {
				if (error.name === 'NotFound') {
					history.push('/');
				}
			});
	}

	componentWillUnmount() {
		if (this.state.socket) {
			this.state.socket.removeAllListeners();
		}
	}

	onSocketMethods = (socket) => {
		const {
			match: {
				params: { roomID },
			},
		} = this.props;

		socket.on('connect', () => {
			socket.emit('JOIN', roomID);
		});

		socket.on('MAKE_MOVE', ({ newCells, newPlayer }) => {
			this.setState({ cells: newCells });
			this.setState({ player: newPlayer });
		});

		socket.on('CHOOSE_TEAM', (newPlayers) => {
			this.setState({ players: newPlayers });
		});

		socket.on('SET_TEAM', (team) => {
			this.setState({ team });
		});

		socket.on('REQUEST_RESTART_GAME', (player) => {
			if (window.confirm(`${player} would like to restart the game`)) { 
				socket.emit('RESTART_GAME', roomID);
			};
		});

		socket.on('RESTART_GAME', () => {
			this.setState({ players: [] });
		});
	};

	onReady = () => {
		const socket = io('localhost:8000', { transports: ['websocket'] });
		this.setState({ socket });
		this.onSocketMethods(socket);
		this.setState({ loading: false });
	};

	calculateWinner = (cells) => {
		const lines = [
			[0, 1, 2],
			[3, 4, 5],
			[6, 7, 8],
			[0, 3, 6],
			[1, 4, 7],
			[2, 5, 8],
			[0, 4, 8],
			[2, 4, 6],
		];

		for (let i = 0; i < lines.length; i++) {
			const [a, b, c] = lines[i];
			if (cells[a] && cells[a] === cells[b] && cells[a] === cells[c]) {
				return cells[a];
			}
		};
		
		return null;
	};

	handleClick = (id) => {
		const {
			team,
			player,
			players,
			cells,
			socket,
		} = this.state;
		const {
			match: {
				params: { roomID },
			},
		} = this.props;

		if (players.length !== 2) return;
		if (player !== team) return;

		if (this.calculateWinner(cells) || cells[id]) {
			return;
		}

		socket.emit('MAKE_MOVE', { roomID, cells, id, player });
	};

	chooseTeam = (newTeam) => {
		const { team, players, socket } = this.state;
		const {
			match: {
				params: { roomID },
			},
		} = this.props;
		
		if (team !== null) return;

		socket.emit('CHOOSE_TEAM', {
			roomID,
			team: newTeam,
			userID: this.props.userID,
			players,
		});
	};

	restartGame = () => {
		const { socket, team } = this.state;
		const {
			match: {
				params: { roomID },
			},
		} = this.props;

		socket.emit('REQUEST_RESTART_GAME', { roomID, player: team });
	};

	render() {
		const {
			loading,
			cells,
			player,
			team,
			players,
		} = this.state;
		if (loading) return <Loading />;

		const winner = this.calculateWinner(cells);

		let status;
		if (winner) status = 'Winner: ' + winner;
		else status = team === player ? `Turn: ${player} (You)` : `Turn: ${player}`;

		return (
			<div className='game-room'>
				<div>
					<h3 className='status'>{players.length === 2 && status}</h3>
					<Board
						cells={cells}
						isActive={!winner && team === player}
						onClick={(id) => this.handleClick(id)}
					/>
					<div className='buttons-container'>
						{winner ? (
							<button onClick={this.restartGame} className='restart-game-button'>Restart Game</button>
						) : players.length === 2 ? null : (
							<>
								<button onClick={() => this.chooseTeam('X')}>
									Join Team X
								</button>
								<button onClick={() => this.chooseTeam('O')}>
									Join Team O
								</button>
							</>
						)}
					</div>
				</div>
			</div>
		);
	}
}

export default GameRoom;
```

### The Styles (App.scss)
```scss
* {
  margin: 0;
  padding: 0;
  text-decoration: none;
  list-style-type: none;
}

html,
body,
#root {
  height: 100%;
}

body {
  font-family: 'Space Grotesk', sans-serif;
  background-color: #eeeeee;
}

input[type='text'],
input[type='password'],
input[type='email'] {
  height: auto;
  padding: .5rem 1rem;
  font-size: .95rem;
  line-height: 1.5;
  color: #495057;
  background-color: #fff;
  border: 1px solid #becad6;
  font-weight: 300;
  border-radius: .375rem;
  box-shadow: none;
  transition: box-shadow 250ms cubic-bezier(.27, .01, .38, 1.06), border 250ms cubic-bezier(.27, .01, .38, 1.06);
}

button {
  font-weight: 300;
  font-family: 'Space Grotesk', monospace, sans-serif;
  border: 1px solid transparent;
  padding: .75rem 1.25rem;
  font-size: .875rem;
  line-height: 1.125;
  border-radius: 10px;
  transition: all 250ms cubic-bezier(.27, .01, .38, 1.06);
  cursor: pointer;
  font-weight: 500;
}

a {
  color: #ffffff;
}

:root {
 --primary-color: #28df99;
}

%flex-complete-center {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
}

.app-component {
  @extend %flex-complete-center;
  height: calc(100% - 80px);
  width: 100%;
}

.navbar {
  height: 80px;
  background-color: #212121;
  color: #ffffff;
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0 10px;
  box-sizing: border-box;
}

.loading-component {
  @extend %flex-complete-center;
  width: 100%;
  height: 100%;
}

.loading-div {
  border: 3px solid #10442f;
  border-top-color: var(--primary-color);
  border-radius: 50%;
  width: 3em;
  height: 3em;
  animation: spin 1s linear infinite;
  
  @keyframes spin {
    to {
      transform: rotate(360deg);
    }
  }
}

.form-container {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  max-width: 95%;
  box-sizing: border-box;

  form {
    width: 450px;
    max-width: 100%;
    display: flex;
    flex-direction: column;

    > div {
      display: flex;
      flex-direction: column;
      margin-bottom: 20px;
    }

    .switch {
      position: relative;
      display: inline-block;
      width: 54px;
      height: 28px;
    }

    .switch input {
      opacity: 0;
      width: 0;
      height: 0;
    }

    .slider {
      position: absolute;
      cursor: pointer;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: #ccc;
      -webkit-transition: .4s;
      transition: .4s;
    }

    .slider:before {
      position: absolute;
      content: "";
      height: 20px;
      width: 20px;
      left: 4px;
      bottom: 4px;
      background-color: white;
      -webkit-transition: .4s;
      transition: .4s;
    }
    
    input:checked+.slider {
      background-color: var(--primary-color);
    }

    input:focus+.slider {
      box-shadow: 0 0 1px var(--primary-color);
    }

    input:checked+.slider:before {
      -webkit-transform: translateX(26px);
      -ms-transform: translateX(26px);
      transform: translateX(26px);
    }

    /* Rounded sliders */
    .slider.round {
      border-radius: 34px;
    }

    .slider.round:before {
      border-radius: 50%;
    }
  }
}

.button-primary {
  color: #fff;
  background-color: var(--primary-color);
  border-color: var(--primary-color);

  &:hover {
    background-color: #2df3a7;
    border-color: #2df3a7;
  }
}

.button-secondary {
  color: #212121;
  background-color: #ffffff;
  border-color: var(--primary-color);

  color: #fff;
  background-color: #0d7377;
  border-color: #0d7377;

  &:hover {
    background-color: #118b8f;
    border-color: #118b8f;
  }
}

.home-private-page {
  .container {
    @extend %flex-complete-center;
    border-radius: 10px;
    width: 500px;
    max-width: 95%;
    height: 400px;

    button {
      width: 280px;
      height: 50px;
      max-width: 95%;
    }

    button:nth-of-type(2) {
      margin: 15px 0;
    }
  }
}

.game-room {

  .status {
    text-align: center;
    margin-bottom: 20px;
  }

  #board {
    border-collapse: collapse;
    font-family: monospace;
  }
  
  #winner {
    margin-top: 25px;
    width: 168px;
    text-align: center;
  }
  
  td {
    text-align: center;
    font-weight: bold;
    font-size: 25px;
    color: #555;
    width: 100px;
    height: 100px;
    line-height: 50px;
    border: 3px solid #aaa;
    background: #fff;
  }
  
  td.active {
    cursor: pointer;
    background: #eeffe9;
  }
  
  td.active:hover {
    background: #eeffff;
  }

  .buttons-container {
    display: flex;
    justify-content: space-between;
    margin-top: 15px;
    
    button:nth-of-type(1) {
      background-color: #28df99;
      
      &.restart-game-button {
        background-color: #facf5a;
        margin: 0 auto;
      }
    }

    button:nth-of-type(2) {
      background-color: #086972;
      color: #ffffff;
    }

  }
}
```