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
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);
}

所有评论(0)