DEV Community

loading...

Build Tic-Tac-Toe with React and Fauna

xuriwork profile image Xuriwork ・11 min read

Git Repo

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

React (Front-end)
Fauna (Database)
Firebase (Authentication)
Node.js (Server)
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

npx create-react-app tic-tac-toe
Enter fullscreen mode Exit fullscreen mode

Install the needed dependencies

yarn add node-sass react-router-dom cors http express concurrently faunadb firebase nanoid socket.io socket.io-client 
Enter fullscreen mode Exit fullscreen mode

Edit your package.json file to like something like this:

  "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"
  },
Enter fullscreen mode Exit fullscreen mode

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

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

Creating a new database

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

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

Creating a new index

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

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

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

Creating a client key

Go to the Firebase console
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

Name your project

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

Adding Firebase to your app

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

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

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;
Enter fullscreen mode Exit fullscreen mode

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

Getting started with Authentication

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

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

Check if a room exists

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

Create a room

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 }],
            },
        })
    );
};
Enter fullscreen mode Exit fullscreen mode

Update the TicTacToe board

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

Add team

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

All of these will be defined in a file called faunaDB.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 };
Enter fullscreen mode Exit fullscreen mode

The express server

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}`));
Enter fullscreen mode Exit fullscreen mode

Back to the React App.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;
Enter fullscreen mode Exit fullscreen mode

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

Navbar.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;
Enter fullscreen mode Exit fullscreen mode

CreateGame.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;
Enter fullscreen mode Exit fullscreen mode

JoinGame.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;
Enter fullscreen mode Exit fullscreen mode

PublicHome.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;
Enter fullscreen mode Exit fullscreen mode

PrivateHome.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;
Enter fullscreen mode Exit fullscreen mode

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

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} />
        )} />
    );
};
Enter fullscreen mode Exit fullscreen mode

The AuthContext to check whether the user is authenticated or not

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;
Enter fullscreen mode Exit fullscreen mode

Board.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;
Enter fullscreen mode Exit fullscreen mode

GameRoom.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;
Enter fullscreen mode Exit fullscreen mode

The Styles (App.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;
    }

  }
}
Enter fullscreen mode Exit fullscreen mode

Discussion

pic
Editor guide