Skip to content

hmjatt/Tenzies-ReactJS

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tenzies-ReactJS 🎲

Create a Tenzies Game using ReactJS

This is an image This is an image

About ℹ️

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 👾

Technologies Used 💻

javascript html5 css3 ES6 reactJS figma


Includes the following features/components ⚙️

- ReactJS
- create-react-app
- Figma Design Template
- Event Listeners in React
- React State
- Conditional Rendering in React
- React Hooks(useEffect)
- github-pages

Usage 🤓

cd tenzies

npm install

npm start


Steps I followed to complete this project 🪜

1. Initialize Project 🎍

  • 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 the App component.

2. Organize Project 🗄️

  • Create a components folder inside the src directory.
  • Create custom components inside the components folder.
  • Create a styles folder inside the src directory and add .css files inside it.

3. Clean Directory🧹

  • Delete unnecessary files and code from the directory.

4. App Component 🧩

  • Create a App component and basic JSX elements for it.

  • Add appropriate classNames to elements in the App component.

  • Import App component inside index.js. Code inside index.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 editing App.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;
    }

    Output :- This is an image

5. Dice Component 🧩

  • Create a Dice component and basic JSX elements for it.

  • Add appropriate classNames to elements in the Dice 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 editing App.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 */

    Output :- This is an image

6. Footer Component 🧩

  • Create Footer component and basic JSX elements for it.
  • Import Footer component inside App component.
  • Style Footer component.

7. Generate Array of 10 Random Numbers 🔃

  • 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 inside App 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());

8. Replace Numbers with Dots (CSS Challenge) 🔢

  • 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 in App.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 in Dice.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;

    Output :- This is an image

9. Map Array to Dice Component 🗺️

  • 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 our allNewDice 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.

10. Roll Dice Button 🎢

  • Create a Roll dice button inside App component that will re-roll all 10 dice.

    <button className="roll-dice" onClick="{rollDice}">Roll</button>
  • Clicking the Roll dice button runs rollDice() function, which should generate a new array of numbers and set the dice 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 to App.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);
    }

11. Change Dice to Objects 🪢

  • 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 }. Updated allNewDice() 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 the value key from our array of objects. Updated diceElements 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 package nanoid which lets us generate unique ID's on the fly. Here are the code changes we need to make to the App component to make this work :-

    • Import nanoid package at the top of App.js :
    import { nanoid } from "nanoid";
    • Create an id property and assign nanoid() function as it's value :
    value: Math.ceil(Math.random() * 6),
    isHeld: false,
    id: nanoid()
    • Assign the key prop the value of id:
    const diceElements = dice.map((dice) => (
        <Dice key={dice.id} value={dice.value} />
    ));

12. Styling Held Dice 🎨

  • Pass a isHeld prop inside App component, in diceElement when rendering our Dice 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>
    );

13. Hold Dice ✋

  • In App component, create a function holdDice that takes id 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 accept holdDice prop and bound it to onClick event.

    <div className="dice-face" style={styles} onClick={props.holdDice}>
        {diceSpanEles}
    </div>
  • Update the holdDice function to flip the isHeld property on the object in the array that was clicked, based on the id prop passed into the function. In App component, we will use setDice 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 new Dice object, when we call it. Let's use helper function to create Dice object inside allNewDice 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 being held. Same as holdDice function, we will use setDice state function then .map() over the array of objects. When calling helper function generateNewDice(), 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;
    }

    Output -> This is an image

14. End Game 🔚

  • In App component add new state called tenzies, 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 the dice 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 the non-unused-vars warnings for now.

    import { useState, useEffect } from "react";
    
    useEffect(() => {
        console.log("Dice state changed");
    }, [dice]);
  • We will use .every() array method, which returns true if every item in the array is same else it returns false. 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 the react-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>;

    Output -> This is an image

15. New Game 🆕

  • Allow the user to play a new game when the New Game button is clicked and they've already won. In App component, let's update rollDice() function such that user can only roll the dice if tenzies === false. Else tenzies === true(if they've won the game), set tenzies === 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());
        }
    }

16. Track Number Of Rolls (JS Challenge) #️⃣

  • Track the number of Rolls it took to win the game. Inside App component, let's define a state called numOfRolls and set it's default value to 0.

    const [numOfRolls, setNumOfRolls] = useState(0);
  • Inside rollDice() function add a couple of statements that change numOfRolls state, such that when Roll button is clicked (game is not won) it increases numOfRolls state by 1. And when game is won and New 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 of numOfRolls state inside it.

    <h2 className="track-rolls">Number of Rolls: {numOfRolls}</h2>

    Output -> This is an image

17. Track The Time (JS Challenge) ⌚

  • Track the time it took to win the game. In App component initiate two states [time], [running] and set their default states to 0, 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 the timer.

    // 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 when New 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>

    Output -> This is an image

18. Save Best Time (JS Challenge) 💾

  • Save Best Time to localStorage and try to beat the record. Inside App component initiate a state [bestTime]and set it's default value to 23450(just a random value).

    const [bestTime, setBestTime] = useState(23450);
  • Using useEffect Hook that gets bestTime 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 the currentTime in localStorage if(currentTime < bestTime) and also make changes to the dependency array( add time, 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 for bestTime. Also, add some styling to timer 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;
    }

    Output -> This is an image

19. Make App Responsive 📱

  • Change Absolute units to Relative.

  • Make App responsive for mobile by adding media query. 😃

20. Prepare for Deployment 🪢

  • 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.

21. Deploy 📤

  • Use Official Documentation(link) to push the project to GitHub Pages 🎆🎆🎆

Future Changes ♾️

  • 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. ✅

Links to content that helped me with this project 🔗

  1. The Odin Project

  2. Figma Design

  3. Scrimba

  4. React Official Documentation


Quote ✒️

“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

♾️❇️🔥