The Website: Link

So I made a stopwatch with almost completely copied Google's stopwatch design :p (if you have some suggestions about design, let me know!).
The whole code is located down below.

Few Notes

❔How the time is calculated

Let's say we just started the stopwatch. We have the startTime, elapsedTime is counting the time now, time too, but time equals elapsedTime only at the beginning.
Now we click pause.
We don't care about startTime now. elapsedTime stops. time too. But stopTime now equals time.
We click play.
startTime is set. elapsedTime is counting from 0. time is counting from the value of stopTime. And time is the time we see on the screen.
Think variables' names are a bit confusing here. If u have some ideas how to rename them, please let me know.

❕A question for you

There's some trouble. In a row if(s == 0 && ms == 1){ in playTime() I wanted to type if(s == 0){. Has some logic, right? instead of calculating minutes at 00s 01ms function could just calculate it at 00s. But! After waiting 1min you'll have 1m00s00ms. If you click reset button it's gonna be 00s00ms. And if there's shorter version of condition, it'll show 1m00s00ms on a new play. I had hard time trying understand why it happens so, but still no clue.
Also it would be great if you have some others code improvements or so.

The Code

JavaScript

import { useEffect, useState } from "react"



function App() {
  const [isPlaying, setIsPlaying] = useState(false)
  // time
  const [startTime, setStartTime] = useState(Date.now())
  const [elapsedTime, setElapsedTime] = useState(0)
  const [stopTime, setStopTime] = useState(0)
  const [time, setTime] = useState(null)
  let sec, msec, min
  const [s, setS] = useState("00")
  const [ms, setMs] = useState("00")
  const [m, setM] = useState("")
  // laps
  const [laps, setLaps] = useState([])



  function handleStart(){
    // onPause
    if(isPlaying){
      setIsPlaying(false)
      setStopTime(time)
    }
    // onPlay
    else{
      setIsPlaying(true)
      document.body.querySelector('#resetBtn').style.visibility = "visible"
      setStartTime(Date.now())
      playTime()
    }
  }

  function playTime(){
    setTimeout(()=>{
      // set elapset time
      setElapsedTime(Date.now() - startTime)
      // set time
      setTime(stopTime + elapsedTime)
      // set sec
      sec = Math.floor(time/1000%60)
      if(sec < 10) setS("0" + sec)
      else setS(sec)
      // set msec
      msec = Math.floor(time/10%100)
      if(msec < 10) setMs("0" + msec)
      else setMs(msec)
      // set min
      if(s == 0 && ms == 1){
        min = Math.floor(time/60000)
        if(min > 0) setM(min + ":")
      }
    }, 1)
  }

  useEffect(()=>{
    if(isPlaying) playTime()
  }, [time])

  function resetTime(){
    document.body.querySelector('#startBtn').checked = false
    document.body.querySelector('#resetBtn').style.visibility = "hidden"
    setTimeout(()=>{
      setIsPlaying(false)
      setTime(null)
      setStopTime(0)
      setS("00")
      setMs("00")
      setM("")
      setLaps([])
    }, 1)
  }

  function lap(){
    setLaps(["#" + (laps.length + 1) + " " + m + s + "." + ms, ...laps])
  }


  return (
    <div className="stopwatch">

      {/* Time Container */}
      <div className="time-cont">
        <div className="time">
          <div>{ m + s }</div>
          <div>{ ms }</div>
        </div>
      </div>

      {/* Laps List */}
      <div className="laps">{
        laps.map((lap)=>(
          <div key={lap}>{lap}</div>
        ))
      }</div>

      {/* Navigation */}
      <nav>
        <div id="resetBtn" onClick={resetTime}></div>
        <input id="startBtn" type="checkbox" />
        <label htmlFor="startBtn" onClick={handleStart}>
          <div className="play"></div>
          <div className="pause"></div>
        </label>
        <div id="lapBtn" onClick={lap}></div>
      </nav>

    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

SCSS

*{
  margin: 0;
  box-sizing: border-box;
}

$primary: #fbeab2;
$secondary: #34353a;
$text: rgba($color: #fff, $alpha: .8);
$stopwatch-width: 40vh;
$btn-opacity: .5;

body{
  background: $secondary;
  font-family: sans-serif;
  height: fill-available;
}



.stopwatch{
  max-width: $stopwatch-width;
  margin: 0 auto; // delete if the app is used somewhere
  position: relative; // same
  top: 50vh; // same
  transform: translateY(-50%); // same
  color: $text;
  display: flex;
  flex-flow: column;
  align-items: center;

  .time-cont{
    width: $stopwatch-width;
    height: $stopwatch-width;
    border: 1.2vh solid rgba($color: #000, $alpha: .4);
    border-radius: 50%;
    display: flex;
    flex-flow: column;
    justify-content: center;
    align-items: center;
    .time{
      font-size: 7vh;
      line-height: 5.4vh;
      text-align: right;
      :nth-child(2){ font-size: 4.8vh; }
    }
  }

  .laps{
    height: 11vh;
    padding: 0 2vh;
    margin: 2vh 0;
    line-height: 3.2vh;
    letter-spacing: .1vh;
    font-size: 2vh;
    overflow-y: scroll;
  }

  nav{
    display: flex;
    justify-content: center;
    align-items: center;
    // input
    input{ display: none; }
    input:checked{
      & + label{
        width: 18vh;
        border-radius: 3vh;
        .play{ display: none; }
        .pause{ display: block; }
      }
      & ~ #lapBtn{ visibility: visible; }
    }
    // play button
    label{
      transition: .2s;
      width: 12vh;
      height: 12vh;
      background: $primary;
      border-radius: 50%;
      margin: 0 3.2vh;
      .play, .pause{ &::after{ background-size: 2vh; } }
      .play::after{ background-image: url('./images/play.svg'); }
      .pause{
        display: none;
        &::after{ background-image: url('./images/pause.svg'); }
      }
    }
    // side nav buttons
    #resetBtn, #lapBtn{
      visibility: hidden;
      width: 7vh;
      height: 7vh;
      background: lighten($color: $secondary, $amount: 50);
      border-radius: 50%;
      &::after{ background-size: 2.5vh; }
    }
    #resetBtn::after{ background-image: url('./images/reset.svg'); }
    #lapBtn::after{ background-image: url('./images/lap.svg'); }
    // all buttons
    label, #resetBtn, #lapBtn{
      cursor: pointer;
      user-select: none;
      position: relative;
      display: flex;
      justify-content: center;
      align-items: center;
      &:hover{ filter: brightness(90%); }
    }
    label .play, label .pause, #resetBtn, #lapBtn{
      &::after{
        content: '';
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background-position: center;
        background-repeat: no-repeat;
        opacity: $btn-opacity;
        border-radius: 50%;
      }
    } 
  }
}






::-webkit-scrollbar {
  width: 1vh;
}
::-webkit-scrollbar-track {
  background: transparent; 
}
::-webkit-scrollbar-thumb {
  background: rgba(110, 110, 110, 0.5);
  border-radius: .5vh;
  background-clip: border-box;
}
::-webkit-scrollbar-thumb:hover {
  background: rgba(110, 110, 110, 0.8);
}
::-webkit-scrollbar-corner {
  background: rgba(0,0,0,0);
}
Enter fullscreen mode Exit fullscreen mode
Logo

React社区为您提供最前沿的新闻资讯和知识内容

更多推荐