Skip to content

Commit

Permalink
feat: Split components, Add rudimentary lap functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
Marpfie committed Jan 17, 2025
1 parent fa00b6c commit ff443b8
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 25 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
"preview": "vite preview"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
Expand All @@ -19,6 +23,7 @@
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"classnames": "^2.5.1",
"dayjs": "^1.11.13",
"eslint": "^9.17.0",
"eslint-plugin-react-hooks": "^5.0.0",
Expand Down
17 changes: 15 additions & 2 deletions src/components/button.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import { IconDefinition } from "@fortawesome/fontawesome-svg-core"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { PropsWithChildren } from "react"
import classnames from "classnames"

interface IButtonProps extends PropsWithChildren {
onClick?: () => void
text?: string
icon?: IconDefinition
className?: string
disabled?: boolean
}

export const Button = (props: IButtonProps) => {
const { onClick, children } = props
const { children, className, disabled, icon, onClick, text } = props

return (
<button className="font-bold text-xl" onClick={onClick}>
<button
className={classnames("font-bold text-xl", className)}
onClick={onClick}
disabled={disabled}
>
{icon && <FontAwesomeIcon icon={icon} />}
{text && <span className="ml-2">{text}</span>}
{children}
</button>
)
Expand Down
17 changes: 17 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,20 @@ export const diffBetweenTimestamps = (

return date1.diff(date2)
}
/**
*
* @param elapsedTime in milliseconds
* @returns Formatted string in `HH:MM:ss` / `MM:ss` format
*/
export const formatDuration = (elapsedTime: number) => {
const duration = dayjs.duration(elapsedTime)

if (duration.hours()) {
//TODO Check requirements for hour display
// This depends on the requirements. The `MM:ss` format has been defined,
// but not what should happen with the edge case of reaching an hour or more
return duration.format("HH:MM:ss")
}

return duration.format("MM:ss")
}
50 changes: 50 additions & 0 deletions src/routes/timer/components/controls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {
faArrowRotateLeft,
faFlagCheckered,
faPause,
faPlay,
} from "@fortawesome/free-solid-svg-icons"

import classNames from "classnames"

import { Button } from "../../../components/button"

interface ITimerControlsProps {
addLap: () => void
elapsedTime: number
resetTimer: () => void
timerActive: boolean
toggleTimer: () => void
}

export const TimerControls = (props: ITimerControlsProps) => {
const { addLap, elapsedTime, resetTimer, timerActive, toggleTimer } = props

return (
<div className="grid grid-cols-3 gap-3">
<Button
onClick={resetTimer}
className="bg-orange-400"
disabled={!elapsedTime}
icon={faArrowRotateLeft}
text="Reset"
/>
<Button
onClick={toggleTimer}
className={classNames({
"bg-red-500": timerActive,
"bg-green-400": !timerActive,
"text-black": !timerActive,
})}
icon={timerActive ? faPause : faPlay}
text={timerActive ? "Pause" : "Start"}
/>
<Button
className="bg-blue-500"
onClick={addLap}
icon={faFlagCheckered}
text="Lap"
/>
</div>
)
}
19 changes: 18 additions & 1 deletion src/routes/timer/components/lap.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
export const Lap = () => <b>I'm going to display a lap time in the future</b>
interface ITimerDisplayProps {
formattedTime: string
index: number
onClick: (index: number) => void
}

export const Lap = (props: ITimerDisplayProps) => {
const { formattedTime, onClick, index } = props

//TODO Add a proper button/icon for removal functionality

return (
<div onClick={() => onClick(index)} className="text-2xl m-2 cursor-pointer">
<span>#{index + 1} </span>
{formattedTime}
</div>
)
}
19 changes: 19 additions & 0 deletions src/routes/timer/components/laps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Lap } from "./lap"

interface ILapsProps {
laps: string[]
removeLap: (index: number) => void
}

export const Laps = (props: ILapsProps) => {
const { laps, removeLap } = props

return laps.map((lap, i) => (
<Lap
key={`lap-${lap}-${i}`}
onClick={removeLap}
formattedTime={lap}
index={i}
/>
))
}
13 changes: 2 additions & 11 deletions src/routes/timer/components/timerDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import dayjs from "dayjs"
import { useMemo } from "react"
import { formatDuration } from "../../../lib/utils"

interface ITimerDisplayProps {
elapsedTime: number
Expand All @@ -9,16 +9,7 @@ export const TimerDisplay = (props: ITimerDisplayProps) => {
const { elapsedTime } = props

const formattedTime = useMemo(() => {
const duration = dayjs.duration(elapsedTime)

if (duration.hours()) {
//TODO Check requirements for hour display
// This depends on the requirements. The `MM:ss` format has been defined,
// but not what should happen with the edge case of reaching an hour or more
return duration.format("HH:MM:ss")
}

return duration.format("MM:ss")
return formatDuration(elapsedTime)
}, [elapsedTime])

return <div className="flex justify-center text-9xl m-6">{formattedTime}</div>
Expand Down
40 changes: 31 additions & 9 deletions src/routes/timer/timer.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { useCallback, useEffect, useRef, useState } from "react"
import dayjs from "dayjs"

import { Button } from "../../components/button"
import { diffBetweenTimestamps } from "../../lib/utils"
import { diffBetweenTimestamps, formatDuration } from "../../lib/utils"

import { TimerDisplay } from "./components/timerDisplay"
import { TimerControls } from "./components/controls"
import { Laps } from "./components/laps"

export const Timer = () => {
const [elapsedTime, setElapsedTime] = useState(0) // Elapsed time in milliseconds
const [timestamp, setTimestamp] = useState("") // Timestamp of timer start/unpause
const [timerActive, setTimerActive] = useState(false)
const [laps, setLaps] = useState<Array<string>>([])

const interval = useRef<number>(undefined)

Expand All @@ -33,6 +35,7 @@ export const Timer = () => {
return () => clearInterval(interval.current)
}, [addElapsedTime, elapsedTime, timerActive, timestamp])

// No reason to memoize these functions
const startTimer = () => {
setTimestamp(dayjs().toISOString())
setTimerActive(true)
Expand All @@ -48,19 +51,38 @@ export const Timer = () => {
const resetTimer = () => {
setTimerActive(false)
setElapsedTime(0)
setLaps([])
}

const addLap = () => {
// This could be slightly inaccurate as is (within 100 ms, the interval timer) depending on when the button is clicked.
// To fix that this step would need to update elapsed Time and set a new timestamp - I won't go into those details for demo purposes here
// Creates a new array so removeLap stays up to date
setLaps([...laps, formatDuration(elapsedTime)])
}

const removeLap = useCallback(
(index: number) => {
// Don't mutate the original array
const lapsCopy = [...laps]
lapsCopy.splice(index, 1)
setLaps(lapsCopy)
},
[laps]
)

return (
<>
<h1 className="flex justify-center text-5xl m-6">Trial Timer</h1>
<h1>{timestamp}</h1>
<h1>{elapsedTime}</h1>
<TimerDisplay elapsedTime={elapsedTime} />
<div className="grid grid-cols-3 gap-3">
<Button onClick={toggleTimer}>{timerActive ? "Stop" : "Start"}</Button>
<Button onClick={resetTimer}>Reset</Button>
<Button>Lap - Todo :)</Button>
</div>
<TimerControls
addLap={addLap}
toggleTimer={toggleTimer}
resetTimer={resetTimer}
elapsedTime={elapsedTime}
timerActive={timerActive}
/>
<Laps laps={laps} removeLap={removeLap} />
</>
)
}
56 changes: 54 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,39 @@
"@eslint/core" "^0.10.0"
levn "^0.4.1"

"@fortawesome/[email protected]":
version "6.7.2"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz#7123d74b0c1e726794aed1184795dbce12186470"
integrity sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==

"@fortawesome/fontawesome-svg-core@^6.7.2":
version "6.7.2"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz#0ac6013724d5cc327c1eb81335b91300a4fce2f2"
integrity sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==
dependencies:
"@fortawesome/fontawesome-common-types" "6.7.2"

"@fortawesome/free-regular-svg-icons@^6.7.2":
version "6.7.2"
resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz#f1651e55e6651a15589b0569516208f9c65f96db"
integrity sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==
dependencies:
"@fortawesome/fontawesome-common-types" "6.7.2"

"@fortawesome/free-solid-svg-icons@^6.7.2":
version "6.7.2"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz#fe25883b5eb8464a82918599950d283c465b57f6"
integrity sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==
dependencies:
"@fortawesome/fontawesome-common-types" "6.7.2"

"@fortawesome/react-fontawesome@^0.2.2":
version "0.2.2"
resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz#68b058f9132b46c8599875f6a636dad231af78d4"
integrity sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==
dependencies:
prop-types "^15.8.1"

"@humanfs/core@^0.19.1":
version "0.19.1"
resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
Expand Down Expand Up @@ -867,6 +900,11 @@ chokidar@^3.6.0:
optionalDependencies:
fsevents "~2.3.2"

classnames@^2.5.1:
version "2.5.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==

color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
Expand Down Expand Up @@ -1413,7 +1451,7 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==

loose-envify@^1.1.0:
loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
Expand Down Expand Up @@ -1503,7 +1541,7 @@ normalize-range@^0.1.2:
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==

object-assign@^4.0.1:
object-assign@^4.0.1, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
Expand Down Expand Up @@ -1652,6 +1690,15 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==

prop-types@^15.8.1:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
dependencies:
loose-envify "^1.4.0"
object-assign "^4.1.1"
react-is "^16.13.1"

punycode@^2.1.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5"
Expand All @@ -1670,6 +1717,11 @@ react-dom@^18.3.1:
loose-envify "^1.1.0"
scheduler "^0.23.2"

react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==

react-refresh@^0.14.2:
version "0.14.2"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9"
Expand Down

0 comments on commit ff443b8

Please sign in to comment.