From fa00b6cdafec8cb5dcc8979579c1befb6ba205c5 Mon Sep 17 00:00:00 2001 From: Marpfie <24807827+Marpfie@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:17:48 +0100 Subject: [PATCH] feat: Added timer functionality (start/stop/reset) --- package.json | 1 + src/lib/utils.ts | 18 ++++++ src/main.tsx | 5 ++ src/routes/timer/components/timerDisplay.tsx | 26 +++++++-- src/routes/timer/timer.tsx | 58 ++++++++++++++++++-- yarn.lock | 7 +++ 6 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 src/lib/utils.ts diff --git a/package.json b/package.json index fa7ea84..08d51ff 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@types/react-dom": "^18.3.5", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", + "dayjs": "^1.11.13", "eslint": "^9.17.0", "eslint-plugin-react-hooks": "^5.0.0", "eslint-plugin-react-refresh": "^0.4.16", diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..6f3bd36 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,18 @@ +import dayjs from "dayjs" + +/** + * @param timestamp1 ISO string + * @param timestamp2 ISO string + * @returns Difference in milliseconds + */ +export const diffBetweenTimestamps = ( + timestamp1: string, + timestamp2: string +): number => { + // Of course this isn't exactly a complicated function. It's put into this file just to demonstrate + // the use of library functions that may be reused across the application. + const date1 = dayjs(timestamp1) + const date2 = dayjs(timestamp2) + + return date1.diff(date2) +} diff --git a/src/main.tsx b/src/main.tsx index bb7ee29..2162d0c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,9 +1,14 @@ import { StrictMode } from "react" import { createRoot } from "react-dom/client" +import dayjs from "dayjs" +import duration from "dayjs/plugin/duration" import { App } from "./App.tsx" import "./index.scss" +// Add the duration plugin to dayjs +dayjs.extend(duration) + createRoot(document.getElementById("root")!).render( diff --git a/src/routes/timer/components/timerDisplay.tsx b/src/routes/timer/components/timerDisplay.tsx index c8c788d..d47e135 100644 --- a/src/routes/timer/components/timerDisplay.tsx +++ b/src/routes/timer/components/timerDisplay.tsx @@ -1,7 +1,25 @@ -import { useState } from "react" +import dayjs from "dayjs" +import { useMemo } from "react" -export const TimerDisplay = () => { - const [time] = useState(0) +interface ITimerDisplayProps { + elapsedTime: number +} + +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") + }, [elapsedTime]) - return
{time}
+ return
{formattedTime}
} diff --git a/src/routes/timer/timer.tsx b/src/routes/timer/timer.tsx index 91f1072..8586e0d 100644 --- a/src/routes/timer/timer.tsx +++ b/src/routes/timer/timer.tsx @@ -1,15 +1,65 @@ +import { useCallback, useEffect, useRef, useState } from "react" +import dayjs from "dayjs" + import { Button } from "../../components/button" +import { diffBetweenTimestamps } from "../../lib/utils" + import { TimerDisplay } from "./components/timerDisplay" 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 interval = useRef(undefined) + + const addElapsedTime = useCallback(() => { + // Adds elapsed time since last timestamp to the elapsedTime counter + setElapsedTime( + elapsedTime + diffBetweenTimestamps(dayjs().toISOString(), timestamp) + ) + }, [elapsedTime, timestamp]) + + useEffect(() => { + if (timerActive) { + interval.current = setInterval(() => { + // Updates the elapsed time every 100 milliseconds. + // We do this instead of just a 1 second interval so the timer remains accurate after pausing + addElapsedTime() + setTimestamp(dayjs().toISOString()) + }, 100) + } + + return () => clearInterval(interval.current) + }, [addElapsedTime, elapsedTime, timerActive, timestamp]) + + const startTimer = () => { + setTimestamp(dayjs().toISOString()) + setTimerActive(true) + } + + const stopTimer = () => { + setTimerActive(false) + addElapsedTime() + } + + const toggleTimer = () => (timerActive ? stopTimer() : startTimer()) + + const resetTimer = () => { + setTimerActive(false) + setElapsedTime(0) + } + return ( <>

Trial Timer

- +

{timestamp}

+

{elapsedTime}

+
- - - + + +
) diff --git a/yarn.lock b/yarn.lock index ed63da5..0f68db1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -918,6 +918,11 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +dayjs@^1.11.13: + version "1.11.13" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.13.tgz#92430b0139055c3ebb60150aa13e860a4b5a366c" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== + debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: version "4.4.0" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" @@ -1927,6 +1932,7 @@ source-map-js@^1.2.1: integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -1945,6 +1951,7 @@ string-width@^5.0.1, string-width@^5.1.2: strip-ansi "^7.0.1" "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: + name strip-ansi-cjs version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==