Create a Tenzies Game
using ReactJS
An implementation of Tenzies Game
in ReactJS
. While creating this project I learned about Event Listeners in React
, React State
, Conditional Rendering in React
, React Hooks(useEffect)
, etc. A player could track the number of rolls, current time and best time it took to win the game. Have Fun 😄. After creating the project, it was deployed to GitHub Pages
🐦 Feel free to reach me onTwitter 👾
- ReactJS
- create-react-app
- Figma Design Template
- Event Listeners in React
- React State
- Conditional Rendering in React
- React Hooks(useEffect)
- github-pages
cd tenzies
npm install
npm start
- Initialize the project using
npx create-react-app tenzies
which will create a complete React App pre-configured and pre-installed with all the dependencies. - Import
Karla
font from google fonts and apply it to theApp
component.
- Create a
components
folder inside thesrc
directory. - Create custom components inside the
components
folder. - Create a
styles
folder inside thesrc
directory and add.css
files inside it.
- Delete unnecessary files and code from the directory.
-
Create a
App
component and basic JSX elements for it. -
Add appropriate
className
s to elements in theApp
component. -
Import
App
component insideindex.js
. Code insideindex.js
looks like this :-import React from "react"; import ReactDOM from "react-dom/client"; import "./styles/index.css"; import App from "./App"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <React.StrictMode> <App /> </React.StrictMode> );
-
Add these styles to
index.css
:-body { margin: 0; background-color: #0b2434; } * { box-sizing: border-box; }
-
Style
App
component by editingApp.css
and add these styles :-.App { font-family: "Karla", sans-serif; text-align: center; display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100vh; } main { background-color: #f5f5f5; height: 40em; width: 40em; border-radius: 10px; box-shadow: rgba(254, 254, 254, 0.25) 0px 13px 27px -5px, rgba( 255, 255, 255, 0.3 ) 0px 8px 16px -8px; }
-
Create a
Dice
component and basic JSX elements for it. -
Add appropriate
className
s to elements in theDice
component.-
Update code inside
App.js
and it should look like this :-import "./styles/App.css"; import Dice from "./components/Dice"; import Footer from "./components/Footer"; function App() { function allNewDice() { const newDice = []; for (let i = 0; i < 10; i++) { newDice.push(Math.ceil(Math.random() * 6)); } return newDice; } console.log(allNewDice()); return ( <div className="App"> <main> <div className="dice-container"> <Dice value="1" /> <Dice value="2" /> <Dice value="3" /> <Dice value="4" /> <Dice value="5" /> <Dice value="6" /> <Dice value="1" /> <Dice value="2" /> <Dice value="3" /> <Dice value="4" /> </div> </main> <Footer /> </div> ); } export default App;
-
Code inside
Dice.js
looks like this :-
function Dice(props) { return ( <div className="dice-face"> <h2 className="dice-num">{props.value}</h2> </div> ); } export default Dice;
-
-
Style
Dice
component by editingApp.css
and add these styles :-main { background-color: #f5f5f5; height: 40em; width: 40em; border-radius: 10px; box-shadow: rgba(254, 254, 254, 0.25) 0px 13px 27px -5px, rgba( 255, 255, 255, 0.3 ) 0px 8px 16px -8px; padding: 20px; display: flex; flex-direction: column; justify-content: center; align-items: center; } .dice-container { display: grid; grid-template: auto auto / repeat(5, 1fr); gap: 20px; } /* Dice Component */ .dice-face { height: 50px; width: 50px; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.15); border-radius: 10px; display: flex; justify-content: center; align-items: center; cursor: pointer; background-color: white; } .dice-num { font-size: 2rem; } /* Dice Component */
- Create
Footer
component and basic JSX elements for it. - Import
Footer
component insideApp
component. - Style
Footer
component.
- Write a
allNewDice
function that returns an array of 10 random numbers between 1-6 inclusive. - Log the array of numbers to the console for now.
- Code for
allNewDice
function insideApp
component looks like this :-function allNewDice() { const newDice = []; for (let i = 0; i < 10; i++) { newDice.push(Math.ceil(Math.random() * 6)); } return newDice; } console.log(allNewDice());
-
Put Real Dots on the Dice. Here is a link to an article that helped me with some of the css in
Dice
component Creating Dice in Flexbox in CSS -
Update styles for
Dice
component inApp.css
and it should look like this :-/* Dice Component */ .dice-face { height: 55px; width: 55px; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.15); border-radius: 10px; display: flex; justify-content: center; /* align-items: center; */ cursor: pointer; background-color: white; padding: 12%; } /* .dice-num { font-size: 2rem; } */ .dot { display: block; width: 12px; height: 12px; border-radius: 50%; background-color: rgb(50, 50, 50); } .dice { width: 2.5em; } .first-face { display: flex; justify-content: center; align-items: center; } .second-face, .third-face, .fourth-face, .fifth-face, .sixth-face { display: flex; justify-content: space-between; } .second-face .dot:nth-of-type(2), .third-face .dot:nth-of-type(3) { align-self: flex-end; } .third-face .dot:nth-of-type(1) { align-self: flex-start; } .third-face .dot:nth-of-type(2), .fifth-face .column:nth-of-type(2) { align-self: center; } .fourth-face .column, .fifth-face .column { display: flex; flex-direction: column; justify-content: space-between; } /* Dice Component */
-
Update code for
Dice
component inDice.js
and it should look like this :-function Dice(props) { const diceValue = parseInt(props.value); let diceSpanEles; if (diceValue === 1) { diceSpanEles = ( <div className="dice first-face"> <span className="dot" style={{ backgroundColor: "rgb(255 100 89)" }} > {" "} </span> </div> ); } else if (diceValue === 2) { diceSpanEles = ( <div className="dice second-face"> <span className="dot"> </span> <span className="dot"> </span> </div> ); } else if (diceValue === 3) { diceSpanEles = ( <div className="dice third-face"> <span className="dot"></span> <span className="dot"></span> <span className="dot"></span> </div> ); } else if (diceValue === 4) { diceSpanEles = ( <div className="fourth-face dice"> <div className="column"> <span className="dot"></span> <span className="dot"></span> </div> <div className="column"> <span className="dot"></span> <span className="dot"></span> </div> </div> ); } else if (diceValue === 5) { diceSpanEles = ( <div className="fifth-face dice"> <div className="column"> <span className="dot"></span> <span className="dot"></span> </div> <div className="column"> <span className="dot"></span> </div> <div className="column"> <span className="dot"></span> <span className="dot"></span> </div> </div> ); } else if (diceValue === 6) { diceSpanEles = ( <div className="fourth-face dice"> <div className="column"> <span className="dot"></span> <span className="dot"></span> <span className="dot"></span> </div> <div className="column"> <span className="dot"></span> <span className="dot"></span> <span className="dot"></span> </div> </div> ); } else { diceSpanEles = <h2 className="die-num">{props.value}</h2>; } return <div className="dice-face">{diceSpanEles}</div>; } export default Dice;
-
Import
useState
hook from react using :-import { useState } from "react";
-
Create a state inside
App
component to hold our array of numbers(Initialize the state by calling ourallNewDice
function so it loads all new(random) dice as soon as the app loads). :-const [dice, setDice] = useState(allNewDice());
-
Map over the state numbers array to generate our array of
diceElements
and render those in place of our manually-written 10 Dice elements.const diceElements = dice.map((dice) => <Dice value={dice} />);
-
React will show the following warning, we will fix it in the future(Ignore this for now)
Warning ⚠️ : Each child in a list should have a unique "key" prop.
-
Create a
Roll
dice button insideApp
component that will re-roll all 10 dice.<button className="roll-dice" onClick="{rollDice}">Roll</button>
-
Clicking the
Roll
dice button runsrollDice()
function, which should generate a new array of numbers and set thedice
state to that new array (thus re-rendering the array to the page).function rollDice() { setDice(allNewDice()); }
-
Style
Roll
dice button using styles from figma design template. Add these styles toApp.css
:-.roll-dice { margin-top: 2em; height: 50px; width: 150px; border: none; border-radius: 6px; background-color: #5035ff; color: white; font-size: 1.2rem; font-family: "Karla", sans-serif; cursor: pointer; } .roll-dice:focus { outline: none; } .roll-dice:active { box-shadow: inset 5px 5px 10px -3px rgba(0, 0, 0, 0.7); }
-
Inside
App
component, update the array of numbers in state to be an array of objects instead. Each object should look like:{ value: <random number>, isHeld: false }
. UpdatedallNewDice()
function looks something like this :-function allNewDice() { const newDice = []; for (let i = 0; i < 10; i++) { newDice.push({ value: Math.ceil(Math.random() * 6), isHeld: false, }); } return newDice; }
-
Making this change will break parts of our code, so we need to update
diceElement
variable and access thevalue
key from ourarray of objects
. UpdateddiceElements
variable looks something like this :-const diceElements = dice.map((dice) => <Dice value={dice.value} />);
-
Let's fix this warning ->
Warning ⚠️ : Each child in a list should have a unique "key" prop.
, by using a npm packagenanoid
which lets us generate unique ID's on the fly. Here are the code changes we need to make to theApp
component to make this work :-- Import
nanoid
package at the top ofApp.js
:
import { nanoid } from "nanoid";
- Create an
id
property and assignnanoid()
function as it's value :
value: Math.ceil(Math.random() * 6), isHeld: false, id: nanoid()
- Assign the
key
prop the value ofid
:
const diceElements = dice.map((dice) => ( <Dice key={dice.id} value={dice.value} /> ));
- Import
-
Pass a
isHeld
prop insideApp
component, indiceElement
when rendering ourDice
component.const diceElements = dice.map((dice) => ( <Dice key={dice.id} value={dice.value} isHeld={dice.isHeld} /> ));
-
Add conditional styling to the
Dice
component so that if it's isheld prop is true(isHeld === true)
, its background color changes to a light green(#59E391)
.const styles = { backgroundColor: props.isHeld ? "#59E391" : "white", }; return ( <div className="dice-face" style={styles}> {diceSpanEles} </div> );
-
In
App
component, create a functionholdDice
that takesid
as a parameter. For now, just have the function console.log(id). Pass that function down to each instance of the Die component as a prop, so when each one is clicked, it logs its own unique ID property.function holdDice(id) { console.log(id); } const diceElements = dice.map((dice) => ( <Dice key={dice.id} value={dice.value} isHeld={dice.isHeld} holdDice={() => holdDice(dice.id)} /> ));
-
In
Dice
component acceptholdDice
prop and bound it toonClick
event.<div className="dice-face" style={styles} onClick={props.holdDice}> {diceSpanEles} </div>
-
Update the
holdDice
function to flip theisHeld
property on the object in the array that was clicked, based on theid
prop passed into the function. InApp
component, we will usesetDice
state function then.map()
over the array of objects. Every Dice object will be in exactly the same state as it was, except the ones that has their isHeld property flipped(to true).function holdDice(id) { setDice((oldDice) => oldDice.map((dice) => { return dice.id === id ? { ...dice, isHeld: !dice.isHeld } : dice; }) ); }
-
Create a helper function
generateNewDice()
that allows us to generate newDice
object, when we call it. Let's use helper function to createDice
object insideallNewDice
function.function generateNewDice() { return { value: Math.ceil(Math.random() * 6), isHeld: false, id: nanoid(), }; } function allNewDice() { const newDice = []; for (let i = 0; i < 10; i++) { newDice.push(generateNewDice()); } return newDice; }
-
Update the
rollDice
function to not just roll all new dice, but instead to look through the existing dice to NOT roll any dice that are beingheld
. Same asholdDice
function, we will usesetDice
state function then.map()
over the array of objects. When calling helper functiongenerateNewDice()
, every Dice object's value will be changed, except the ones that has their property isHeld === true.function holdDice(id) { setDice((oldDice) => oldDice.map((dice) => { return dice.id === id ? { ...dice, isHeld: !dice.isHeld } : dice; }) ); }
-
Add Title & Description elements to give some additional information to the users. Style
<h1 className="title">Tenzies</h1> <p className="instructions"> Roll until all dice are the same. Click each die to freeze it at its current value between rolls. </p>
.title { font-size: 40px; margin: 0; } .instructions { font-family: "Inter", sans-serif; font-weight: 400; margin-top: 0; text-align: center; margin-top: 1em; }
-
In
App
component add new state calledtenzies
, default to false. It represents whether the user has won the game yet or not.const [tenzies, setTenzies] = useState(false);
-
Add an Effect Hook
(useEffect)
that runs every time thedice
state array changes. For now, just console.log("Dice state changed"). We are using effect hook(useEffect)
in order to keep two states(Dice & tenzies)
in sync with each other. Ignore thenon-unused-vars
warnings for now.import { useState, useEffect } from "react"; useEffect(() => { console.log("Dice state changed"); }, [dice]);
-
We will use
.every()
array method, which returnstrue
if every item in the array is same else it returnsfalse
. In our case if all dice are held & all dice have the same value,console.log("You won!")
Let's update our Effect Hook(useEffect)
->useEffect(() => { // All dice are held const allHeld = dice.every((die) => die.isHeld); // All dice have the same value const firstValue = dice[0].value; const allSameValue = dice.every((die) => die.value === firstValue); // if `allHeld` and `allSameValue)` === true, we won if (allHeld && allSameValue) { setTenzies(true); console.log("You won!"); } }, [dice]);
-
If tenzies is
true
, change the button text to "New Game" and use thereact-confetti
package to render the component.npm install react-confetti
import Confetti from "react-confetti"; <main> {tenzies && <Confetti />} <button className="roll-dice" onClick={rollDice}> {tenzies ? "New Game" : "Roll"} </button> </main>;
-
Allow the user to play a new game when the
New Game
button is clicked and they've already won. InApp
component, let's updaterollDice()
function such that user can only roll the dice iftenzies === false
. Elsetenzies === true
(if they've won the game), settenzies === false
and generate all new dice.function rollDice() { if (!tenzies) { setDice((oldDice) => oldDice.map((dice) => { return dice.isHeld ? dice : generateNewDice(); }) ); } else { setTenzies(false); setDice(allNewDice()); } }
-
Track the number of Rolls it took to win the game. Inside
App
component, let's define a state callednumOfRolls
and set it's default value to0
.const [numOfRolls, setNumOfRolls] = useState(0);
-
Inside
rollDice()
function add a couple of statements that changenumOfRolls
state, such that whenRoll
button is clicked (game is not won) it increasesnumOfRolls
state by 1. And when game is won andNew Game
button is clicked (game is won),numOfRolls
state is reset back to 0.function rollDice() { if (!tenzies) { setNumOfRolls((prevState) => prevState + 1); } else { setNumOfRolls(0); } }
-
Create
<h2>
element and insert value ofnumOfRolls
state inside it.<h2 className="track-rolls">Number of Rolls: {numOfRolls}</h2>
-
Track the time it took to win the game. In
App
component initiate two states[time]
,[running]
and set their default states to0
,false
respectively.[time]
representing the recorded time and[running]
as if the game is being played or is won.const [time, setTime] = useState(0); const [running, setRunning] = useState(false);
-
Calculate time using
useEffect
Hook &setInterval()
method. Follow this article for detailed information.useEffect(() => { let interval; if (running) { interval = setInterval(() => { setTime((prevTime) => prevTime + 10); }, 10); } else if (!running) { clearInterval(interval); } return () => clearInterval(interval); }, [running]);
-
Update the
useEffect
Hook, that represents game state. Using this hook Start or Stop thetimer
.// useEffect Hook that represents game state useEffect(() => { // Check if some Dice are held(even if it's just one) const someHeld = dice.some((die) => die.isHeld); // if `someHeld` === True, Start counting if (someHeld) { setRunning(true); } // if `allHeld` and `allSameValue)` === true, we won if (allHeld && allSameValue) { // Stop Counter setRunning(false); // Game Won setTenzies(true); } }, [dice]);
-
Update
rollDice()
function such that if game is won, reset the counter whenNew Game
button is clicked.function rollDice() { if (!tenzies) { //... } else { // Reset timer setTime(0); } }
-
Create JSX elements that will hold values for
minutes
,seconds
,milliseconds
.<h3> <div className="timer"> <div className="current-time"> <span> {("0" + Math.floor((time / 60000) % 60)).slice(-2)}: </span> <span>{("0" + Math.floor((time / 1000) % 60)).slice(-2)}:</span> <span>{("0" + ((time / 10) % 100)).slice(-2)}</span> </div> </div> </h3>
-
Save Best Time to
localStorage
and try to beat the record. InsideApp
component initiate a state[bestTime]
and set it's default value to23450
(just a random value).const [bestTime, setBestTime] = useState(23450);
-
Using
useEffect
Hook that getsbestTime
from localStorage . Follow this article for detailed instructions.useEffect(() => { const bestTime = JSON.parse(localStorage.getItem("bestTime")); if (bestTime) { setBestTime(bestTime); } }, []);
-
Update the
useEffect
Hook, that represents game state. Using this hook store thecurrentTime
in localStorageif(currentTime < bestTime)
and also make changes to the dependency array( addtime
,bestTime
to it ).// useEffect Hook that represents game state useEffect(() => { // ... // if `allHeld` and `allSameValue)` === true, we won if (allHeld && allSameValue) { // ... // Store Time at the end of a win in a variable let currentTime = time; // if currentTime > bestTime, store it in localStorage if (currentTime < bestTime) { setBestTime(currentTime); localStorage.setItem("bestTime", JSON.stringify(currentTime)); } // ... } }, [dice, time, bestTime]);
-
Create JSX elements that will hold
minutes
,seconds
,milliseconds
values forbestTime
. Also, add some styling totimer
div.<div className="timer"> <div className="current-time"> <!-- ... --> </div> <div className="best-time"> <h3 className="best">Best</h3> <div> <span> {( "0" + Math.floor((bestTime / 60000) % 60) ).slice(-2)} : </span> <span> {( "0" + Math.floor((bestTime / 1000) % 60) ).slice(-2)} : </span> <span> {("0" + ((bestTime / 10) % 100)).slice(-2)} </span> </div> </div> </div>
Styles ->
.timer { display: flex; justify-content: space-around; width: 25vw; } .timer h3 { margin: 10px; }
-
Change Absolute units to Relative.
-
Make App responsive for mobile by adding
media query
. 😃
-
Delete unnecessary files from directory and format code with
Prettier
. -
Test for Responsiveness and make changes if need be.
-
Add links to
Live Preview
and screenshots.
- Use Official Documentation(link) to push the project to GitHub Pages 🎆🎆🎆
- CSS - Put Real Dots on the Dice. ✅
- JS - Track Number of Rolls it took to win the game. ✅
- JS - Track the time it took to win the game. ✅
- JS - Save Best Time/Rolls to
localStorage
and try to beat the record. ✅
-
The Odin Project
-
Figma Design
-
Scrimba
-
React Official Documentation
“Humans are allergic to change. They love to say, ‘We’ve always done it this way.’ I try to fight that. That’s why I have a clock on my wall that runs counterclockwise.”
— Grace Hopper
♾️❇️🔥