cod;nncode. learn. thrive.

React Countdown Timer - days | hours | minutes | seconds

Posted Dec 24, 2022

Welcome to the #react10 Day 4 challenge.

In this challenge we will develop a react stopwatch & countdown timer(days, hours, minutes & seconds) using basic React concepts with little JavaScript programming logic around that.

Demo

Get Github Code

Pre-requisites

  • Code Editor (like VS Code)
  • React basic knowledge (as in this challenge we are not focusing on explaining about the react concepts)
  • Basic knowledge of HTML & CSS (mobile responsive)

Lets Begin!

Open terminal in the vs code and run these commands,

npx create-react-app stopwatch
cd stopwatch
npm start

After doing all of this, your UI would look like,

react countdown timer days hours minutes seconds

Run these commands in a new terminal (let the other terminal run our project),

mkdir src\components\common
mkdir src\components\common\button
mkdir src\components\common\header
mkdir src\components\stopwatch
mkdir src\components\timer
type NUL > src\components\stopwatch\Stopwatch.js
type NUL > src\components\stopwatch\Stopwatch.css
type NUL > src\components\timer\Timer.js
type NUL > src\components\timer\Timer.css
type NUL > src\components\common\button\Button.js
type NUL > src\components\common\button\Button.css
type NUL > src\components\common\header\Header.js
type NUL > src\components\common\header\Header.css

Don't get confused with the above commands, you can also create files or folders using file explorer manually.

Folder Structure

Note - I have removed few files which 'create-react-app' util gives us by default.
You can also remove those but that is not mandatory step for this challenge.

react countdown timer days hours minutes seconds

Lets add some code

For better understanding, I have added few comments in the code itself.

Stopwatch.js

import React, { useEffect, useState } from "react";
import Button from "../common/button/Button";
import "./Stopwatch.css";

const Stopwatch = () => {
  //state to track whether stopwatch has started or not - to switch b/w 'pause' & 'start' text on jsx button
  //and if not started then to avoid the calculation - in useEffect
  const [isStarted, setIsStarted] = useState(false);
  //state to track millisecond(ms), minute(m), second(s) values in the UI
  const [[m, s, ms], setTimer] = useState([0, 0, 0]);

  //we are using JS's setInterval method to set [m,s,ms] values every 10ms
  //we need to clear the interval on component unmount otherwise useEffect will create infinite intervals to
  //update the [m,s,ms] state.
  //so the flow here is -> create an interval -> call onStart() -> setTimer done -> clear the interval
  // -> create new interval -> and so on.
  useEffect(() => {
    const timerId = setInterval(() => onStart(), 10);
    return () => clearInterval(timerId);
  });

  //logic to update minute based on seconds & seconds based on milliseconds value
  const onStart = () => {
    if (!isStarted) return;
    if (ms === 99 && s === 60) {
      //3rd
      setTimer([m + 1, 0, 0]);
    } else if (ms === 99) {
      //2nd
      setTimer([m, s + 1, 0]);
    } else {
      //1st
      setTimer([m, s, ms + 1]);
    }
  };

  const onReset = () => {
    setIsStarted(false);
    setTimer([0, 0, 0]);
  };

  return (
    <div className="stopwatch">
      <div className="container">
        <div className="clock" onClick={() => setIsStarted(true)}>
          <div className="clock-border-ring">
            <div className="clock-border-ring-inner"></div>
          </div>
          <div className="clock-timer">
            {m < 10 && 0}
            {m}:{s < 10 && 0}
            {s}:{ms < 10 && 0}
            {ms}
          </div>
        </div>
        <div className="button-row">
          <Button onClickHandler={onReset}>Reset</Button>
          <Button onClickHandler={() => setIsStarted(!isStarted)}>
            {isStarted ? <>Pause</> : <>Start</>}
          </Button>
        </div>
      </div>
    </div>
  );
};

export default Stopwatch;

Stopwatch.css

.stopwatch {
  width: 100%;
}

.clock {
  width: 210px;
  height: 210px;
  font-size: 2rem;
  font-weight: 500;
  border-radius: 50%;
  display: flex;
  justify-content: center;
  align-items: center;
  box-shadow: var(--shadow);
  margin: 10px;
  flex-direction: column;
  position: relative;
  cursor: pointer;
}

.clock-timer {
  position: absolute;
  top: 40%;
  left: 17%;
}

.clock-border-ring {
  border: 5px;
  background-image: linear-gradient(#9d00ff, #9d00ff, #2e91e5);
  border-radius: 50%;
  height: 95%;
  width: 95%;
  display: flex;
  justify-content: center;
  align-items: center;
  animation: rotate-gradient 1s 1s infinite;
}
.clock-border-ring-inner {
  background: white;
  border-radius: 50%;
  height: 94%;
  width: 94%;
}

@keyframes rotate-gradient {
  0% {
    transform: rotate(0deg);
  }
  10% {
    transform: rotate(36deg);
  }
  20% {
    transform: rotate(72deg);
  }
  30% {
    transform: rotate(108deg);
  }
  40% {
    transform: rotate(144deg);
  }
  50% {
    transform: rotate(180deg);
  }
  60% {
    transform: rotate(216deg);
  }
  70% {
    transform: rotate(252deg);
  }
  80% {
    transform: rotate(288deg);
  }
  90% {
    transform: rotate(324deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.button-row {
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  flex-direction: row;
  margin: 25px 0px;
  width: 100%;
}

Timer.js

import React, { useEffect, useState } from "react";
import Button from "../common/button/Button";
import "./Timer.css";

const Timer = () => {
  const [[h, m, s], setTimer] = useState([0, 0, 0]);
  const [isStarted, setIsStarted] = useState(false);

  useEffect(() => {
    const timerID = setInterval(() => onStart(), 1000);
    return () => {
      clearInterval(timerID);
    };
  });

  //same logic as stopwatch's code but here we are decreasing the [m,s,ms] state values
  const onStart = () => {
    if (h === 0 && m === 0 && s === 0) {
      setIsStarted(false);
      return;
    }
    if (!isStarted) return;
    if (m === 0 && s === 0) {
      //3rd
      setTimer([h - 1, 59, 59]);
    } else if (s === 0) {
      //2nd
      setTimer([h, m - 1, 59]);
    } else {
      //1st
      setTimer([h, m, s - 1]);
    }
  };

  const onReset = () => {
    setIsStarted(false);
    setTimer([0, 0, 0]);
  };

  return (
    <div className="timer">
      <div className="timer-row">
        <div className="timer-border-ring">
          <div className="timer-border-ring-inner"></div>
        </div>
        <div className="timer-input-title">
          <input
            type="number"
            className="timer-input"
            value={h}
            onChange={(e) => setTimer([e.target.value, m, s])}
          />
          <span>h</span>
        </div>
        <div className="timer-input-title">
          <input
            type="number"
            className="timer-input"
            value={m}
            onChange={(e) => setTimer([h, e.target.value, s])}
          />
          <span>m</span>
        </div>
        <div className="timer-input-title">
          <input
            type="number"
            className="timer-input"
            value={s}
            onChange={(e) => setTimer([h, m, e.target.value])}
          />
          <span>s</span>
        </div>
      </div>
      <div className="button-row">
        <Button onClickHandler={onReset}>Reset</Button>
        {isStarted ? (
          <Button onClickHandler={() => setIsStarted(false)}>Pause</Button>
        ) : (
          <Button
            onClickHandler={() => setIsStarted(true)}
            disabled={h === 0 && m === 0 && s === 0}
          >
            Start
          </Button>
        )}
      </div>
    </div>
  );
};

export default Timer;

Timer.css

.timer {
  width: 100%;
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  flex-direction: column;
  height: 100%;
}

.button-row {
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  flex-direction: row;
  margin: 25px 0px;
  width: 100%;
}

.timer-row {
  width: 210px;
  height: 120px;
  font-weight: 500;
  border-radius: 7px;
  box-shadow: var(--shadow);
  position: relative;
  cursor: pointer;
  display: flex;
  justify-content: center;
  align-items: center;
}

.timer-input-title {
  display: flex;
  justify-content: center;
  align-items: center;
  margin: 0px 7px;
  z-index: 1;
}

.timer-input {
  font-size: 1.8rem;
  font-weight: 500;
  border: none;
  width: 35px;
  border-bottom: 2px solid rgb(97, 97, 97);
}

.timer-input:focus {
  font-size: 1.8rem;
  font-weight: 500;
  outline: none;
}

span {
  font-size: 1rem;
  font-weight: 400;
  width: 100%;
  color: rgb(97, 97, 97);
}

.timer-border-ring {
  border-radius: 7px;
  position: absolute;
  border: 5px;
  background-image: linear-gradient(#9d00ff, #9d00ff, #2e91e5);
  height: 97%;
  width: 97%;
  display: flex;
  justify-content: center;
  align-items: center;
}

.timer-border-ring-inner {
  background: white;
  height: 92%;
  width: 95%;
  border-radius: 4px;
}

Button.js

import React from "react";
import "./Button.css";

const Button = ({ children, onClickHandler, disabled = false }) => {
  return (
    <button onClick={onClickHandler} className="buttons" disabled={disabled}>
      {children}
    </button>
  );
};

export default Button;

Button.css

.buttons {
  box-shadow: var(--shadow);
  padding: 10px 25px;
  border-radius: 7px;
  font-weight: 500;
  display: flex;
  justify-content: center;
  min-width: 32%;
  font-size: 1.1rem;
}

.buttons:hover {
  box-shadow: rgb(0 0 0 / 45%) 0px 5px 15px;
}

Header.js

import React, { useEffect, useState } from "react";
import { Link, useLocation } from "react-router-dom";
import "./Header.css";

const Header = () => {
  const { pathname } = useLocation();
  const [header, setHeader] = useState(pathname);

  useEffect(() => {
    setHeader(pathname);
  }, [pathname]);

  return (
    <div className="header">
      <Link to="/" className={header === "/" ? "active" : ""}>
        Stopwatch
      </Link>
      <Link to="/timer" className={header === "/timer" ? "active" : ""}>
        Timer
      </Link>
    </div>
  );
};

export default Header;

Header.css

.header {
  display: flex;
  flex-direction: row;
  justify-content: space-evenly;
  align-items: center;
  width: 100%;
  font-size: 1.3rem;
  font-weight: 500;
  height: 10vh;
  color: rgb(78, 78, 78);
}

.active {
  color: black;
  animation: animateFontSize 0.2s linear 0s;
  font-size: 2rem;
}

@keyframes animateFontSize {
  0% {
    font-size: 1.3rem;
  }
  25% {
    font-size: 1.5rem;
  }

  50% {
    font-size: 1.7rem;
  }

  75% {
    font-size: 1.9rem;
  }
  100% {
    font-size: 2rem;
  }
}

Go to App.js and import relavant files and packages

import React from "react";
//we are using react-router-dom package to switch between stopwath <-> timer
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import "./App.css";
import Header from "./common/header/Header";
import Stopwatch from "./stopwatch/Stopwatch";
import Timer from "./timer/Timer";

function App() {
  return (
    <div className="App">
      <Router>
        <Header />
        <div className="container">
          <Switch>
            <Route path="/" exact component={Stopwatch} />
            <Route path="/timer" component={Timer} />
          </Switch>
        </div>
      </Router>
    </div>
  );
}

export default App;

App.css

:root {
  --shadow: rgb(0 0 0 / 35%) 0px 5px 15px;
}

.App {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  width: 100%;
}

.container {
  height: 85vh;
  display: flex;
  justify-content: space-evenly;
  align-items: center;
  flex-direction: column;
  width: 100%;
}

index.css

body {
  font-family: "Poppins", sans-serif;
  display: flex;
  justify-content: center;
  align-items: center;
}

button {
  background-color: inherit;
  border: none;
  font-family: inherit;
  cursor: pointer;
}

a {
  text-decoration: none;
  color: inherit;
}

#root {
  width: 100%;
  max-width: 500px;
}

input,
textarea,
button,
select,
a {
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}

And that's it! You have created a react app with countdown timer hours, minutes & seconds features.

Final UI

react countdown timer days hours minutes seconds

Bonus: To run your localhost project on your Mobile devices -

  1. open cmd and run command
ipconfig
  1. copy IPv4 Address(looks like - 192.168..*)
  2. Connect you mobile device with the same WiFi/Internet networt
  3. Open URL - 192.168..*:3000 (Note : 3000 is your port number in which your project is running)
  4. Enjoy your App :)
Your feedback is our favorite notification! Share your thoughts about this page and make us smile.