From 8a449fc5009739fc97d4454c7c8e71c0a98e04d2 Mon Sep 17 00:00:00 2001 From: Chelsea Dwarika Date: Mon, 29 Jan 2024 13:52:27 -0500 Subject: [PATCH 1/9] initial CSS styling --- public/index.html | 14 +++++++------- src/App.tsx | 1 + src/StopWatch.css | 26 ++++++++++++++++++++++++++ src/StopWatch.tsx | 15 +++++++++++---- src/index.tsx | 5 ++++- 5 files changed, 49 insertions(+), 12 deletions(-) create mode 100644 src/StopWatch.css diff --git a/public/index.html b/public/index.html index 5953d96e..824cd131 100755 --- a/public/index.html +++ b/public/index.html @@ -1,9 +1,9 @@ - - Template Site - - -
- - \ No newline at end of file + + Chelsea's Countdown + + +
+ + diff --git a/src/App.tsx b/src/App.tsx index 95351af9..8d193a79 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import React from 'react' + export default function App() { return(
diff --git a/src/StopWatch.css b/src/StopWatch.css new file mode 100644 index 00000000..425f14a4 --- /dev/null +++ b/src/StopWatch.css @@ -0,0 +1,26 @@ +body { + background-color: lavenderblush; + margin: 0; + display: flex; + height: 100vh; + justify-content: center; + align-items: center; +} + +.container { + color: #DA70D6; + display: flex; + letter-spacing: 2px; +} + +.container span { + position: relative; + font-size: 5rem; + bottom: 3px; +} + +.text { + margin: 0; + font-size: 5rem; +} + diff --git a/src/StopWatch.tsx b/src/StopWatch.tsx index d58edc78..12c467da 100644 --- a/src/StopWatch.tsx +++ b/src/StopWatch.tsx @@ -1,7 +1,14 @@ -import React from 'react' +import React from 'react'; +import './StopWatch.css'; export default function StopWatch() { - return( -
- ) + return ( +
+

0

+ : +

0

+ : +

0

+
+ ) } \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index 48342b59..5812d00b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,10 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; +import StopWatch from './StopWatch'; const container = document.getElementById('root'); const root = createRoot(container); -root.render(); +root.render( + +); From 0a7bdf0b6b51adf5412de8b2f2a0c17f492e14f1 Mon Sep 17 00:00:00 2001 From: Chelsea Dwarika Date: Mon, 29 Jan 2024 17:05:23 -0500 Subject: [PATCH 2/9] components folder created --- src/App.tsx | 5 ++++- src/{ => components}/StopWatch.css | 0 src/{ => components}/StopWatch.tsx | 6 +++--- src/{ => components}/StopWatchButton.tsx | 0 src/index.tsx | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) rename src/{ => components}/StopWatch.css (100%) rename src/{ => components}/StopWatch.tsx (65%) rename src/{ => components}/StopWatchButton.tsx (100%) diff --git a/src/App.tsx b/src/App.tsx index 8d193a79..c5cf71b9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,11 @@ import React from 'react' +import StopWatch from './components/StopWatch' export default function App() { return( -
+
+ +
) } \ No newline at end of file diff --git a/src/StopWatch.css b/src/components/StopWatch.css similarity index 100% rename from src/StopWatch.css rename to src/components/StopWatch.css diff --git a/src/StopWatch.tsx b/src/components/StopWatch.tsx similarity index 65% rename from src/StopWatch.tsx rename to src/components/StopWatch.tsx index 12c467da..8af3b582 100644 --- a/src/StopWatch.tsx +++ b/src/components/StopWatch.tsx @@ -4,11 +4,11 @@ import './StopWatch.css'; export default function StopWatch() { return (
-

0

+

00

: -

0

+

00

: -

0

+

00

) } \ No newline at end of file diff --git a/src/StopWatchButton.tsx b/src/components/StopWatchButton.tsx similarity index 100% rename from src/StopWatchButton.tsx rename to src/components/StopWatchButton.tsx diff --git a/src/index.tsx b/src/index.tsx index 5812d00b..ca8d723f 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; import App from './App'; -import StopWatch from './StopWatch'; +import StopWatch from './components/StopWatch'; const container = document.getElementById('root'); const root = createRoot(container); From 261ed0296863ff9d37b93ea9fbfdc535e43526c1 Mon Sep 17 00:00:00 2001 From: Chelsea Dwarika Date: Mon, 29 Jan 2024 17:57:06 -0500 Subject: [PATCH 3/9] basic timer set-up --- src/App.tsx | 17 ++++---- src/components/StopWatch.css | 18 ++++++++- src/components/StopWatch.tsx | 63 ++++++++++++++++++++++++------ src/components/StopWatchButton.tsx | 38 +++++++++++++++--- 4 files changed, 107 insertions(+), 29 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index c5cf71b9..2f599ad8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,10 @@ -import React from 'react' -import StopWatch from './components/StopWatch' - +import React from 'react'; +import StopWatch from './components/StopWatch'; export default function App() { - return( -
- -
- ) -} \ No newline at end of file + return ( +
+ +
+ ); +} diff --git a/src/components/StopWatch.css b/src/components/StopWatch.css index 425f14a4..98673594 100644 --- a/src/components/StopWatch.css +++ b/src/components/StopWatch.css @@ -15,12 +15,26 @@ body { .container span { position: relative; - font-size: 5rem; + font-size: 10rem; bottom: 3px; } .text { margin: 0; - font-size: 5rem; + font-size: 10rem; } +.btn { + background-color: #D8BFD8; + color: black; + padding: 10px 30px; + font-size: 2rem; + margin: 30px; + cursor: pointer; + border: none; + border-radius: 4px; +} + +.btn:hover { + background-color: #DA70D6; +} \ No newline at end of file diff --git a/src/components/StopWatch.tsx b/src/components/StopWatch.tsx index 8af3b582..46560aaf 100644 --- a/src/components/StopWatch.tsx +++ b/src/components/StopWatch.tsx @@ -1,14 +1,53 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; +import StopWatchButton from './StopWatchButton'; import './StopWatch.css'; -export default function StopWatch() { - return ( -
-

00

- : -

00

- : -

00

-
- ) -} \ No newline at end of file +const StopWatch: React.FC = () => { + const [time, setTime] = useState(0); + const [isRunning, setIsRunning] = useState( false ); + + useEffect(() => { + let interval: NodeJS.Timeout; + + if (isRunning) { + interval = setInterval(() => { + setTime((prevTime) => prevTime + 1); + }, 1000); + } + + return () => clearInterval(interval); + }, [isRunning]); + + const startTimer = () => { + setIsRunning(true); + }; + + const stopTimer = () => { + setIsRunning(false); + }; + + const resetTimer = () => { + setTime(0); + setIsRunning(false); + }; + + return ( +
+
+

{Math.floor(time / 3600).toString().padStart(2, '0')}

+ : +

{Math.floor((time % 3600) / 60).toString().padStart(2, '0')}

+ : +

{(time % 60).toString().padStart(2, '0')}

+
+ +
+ ); +}; + +export default StopWatch; diff --git a/src/components/StopWatchButton.tsx b/src/components/StopWatchButton.tsx index dbd7c174..9f8b9987 100644 --- a/src/components/StopWatchButton.tsx +++ b/src/components/StopWatchButton.tsx @@ -1,7 +1,33 @@ -import React from 'react' +// StopWatchButton.tsx +import React from 'react'; -export default function StopWatchButton() { - return( -
- ) -} \ No newline at end of file +interface StopWatchButtonProps { + onStart: () => void; + onStop: () => void; + onReset: () => void; + isRunning: boolean; +} + +const StopWatchButton: React.FC = ({ + onStart, + onStop, + onReset, + isRunning, +}) => { + + return ( +
+ + + +
+ ); +}; + +export default StopWatchButton; From f535b1896516e6e7ae8b295891ff11fb63955e78 Mon Sep 17 00:00:00 2001 From: Chelsea Dwarika Date: Mon, 29 Jan 2024 18:01:01 -0500 Subject: [PATCH 4/9] btn-selected styles --- src/components/StopWatch.css | 4 ++++ src/components/StopWatchButton.tsx | 28 ++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/components/StopWatch.css b/src/components/StopWatch.css index 98673594..8cc6fb1a 100644 --- a/src/components/StopWatch.css +++ b/src/components/StopWatch.css @@ -37,4 +37,8 @@ body { .btn:hover { background-color: #DA70D6; +} + +.btn.selected { + background-color: #DA70D6; } \ No newline at end of file diff --git a/src/components/StopWatchButton.tsx b/src/components/StopWatchButton.tsx index 9f8b9987..7c86c96e 100644 --- a/src/components/StopWatchButton.tsx +++ b/src/components/StopWatchButton.tsx @@ -1,5 +1,4 @@ -// StopWatchButton.tsx -import React from 'react'; +import React, { useState } from 'react'; interface StopWatchButtonProps { onStart: () => void; @@ -14,16 +13,33 @@ const StopWatchButton: React.FC = ({ onReset, isRunning, }) => { - + const [selectedButton, setSelectedButton] = useState(null); + + const handleButtonClick = (action: string, callback: () => void) => { + setSelectedButton(action); + callback(); + }; + return (
- - -
From aa2bb2150251b710c2396399af4bf496942de78f Mon Sep 17 00:00:00 2001 From: Chelsea Dwarika Date: Mon, 29 Jan 2024 18:31:40 -0500 Subject: [PATCH 5/9] gradient background --- src/components/StopWatch.css | 38 ++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/components/StopWatch.css b/src/components/StopWatch.css index 8cc6fb1a..f3de221b 100644 --- a/src/components/StopWatch.css +++ b/src/components/StopWatch.css @@ -1,35 +1,37 @@ body { - background-color: lavenderblush; - margin: 0; + background: rgb(218,112,214); + background: radial-gradient(circle, rgba(218,112,214,1) 0%, rgba(148,187,233,1) 100%); margin: 0; display: flex; - height: 100vh; + min-height: 100vh; justify-content: center; align-items: center; } .container { - color: #DA70D6; + color: #f0a7ee; display: flex; letter-spacing: 2px; + flex-wrap: wrap; + text-align: center; } .container span { position: relative; - font-size: 10rem; + font-size: 6rem; bottom: 3px; } .text { margin: 0; - font-size: 10rem; + font-size: 6rem; } .btn { background-color: #D8BFD8; color: black; - padding: 10px 30px; - font-size: 2rem; - margin: 30px; + padding: 10px 20px; + font-size: 1.5rem; + margin: 10px; cursor: pointer; border: none; border-radius: 4px; @@ -41,4 +43,20 @@ body { .btn.selected { background-color: #DA70D6; -} \ No newline at end of file +} + +@media only screen and (max-width: 600px) { + .container span { + font-size: 4rem; + } + + .text { + font-size: 4rem; + } + + .btn { + padding: 8px 16px; + font-size: 1rem; + margin: 5px; + } +} From f53651495985b0ce9a4ee360a0942ae1b95a565d Mon Sep 17 00:00:00 2001 From: Chelsea Dwarika Date: Mon, 29 Jan 2024 20:57:11 -0500 Subject: [PATCH 6/9] lap functionality --- src/components/StopWatch.css | 10 +++++++++- src/components/StopWatch.tsx | 20 +++++++++++++++++++- src/components/StopWatchButton.tsx | 9 +++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/components/StopWatch.css b/src/components/StopWatch.css index f3de221b..3049f531 100644 --- a/src/components/StopWatch.css +++ b/src/components/StopWatch.css @@ -33,7 +33,7 @@ body { font-size: 1.5rem; margin: 10px; cursor: pointer; - border: none; + border: 1px solid purple; border-radius: 4px; } @@ -45,6 +45,12 @@ body { background-color: #DA70D6; } +.lap-list { + font-size: 2rem; + text-align: center; + margin-top: 2rem; +} + @media only screen and (max-width: 600px) { .container span { font-size: 4rem; @@ -60,3 +66,5 @@ body { margin: 5px; } } + + diff --git a/src/components/StopWatch.tsx b/src/components/StopWatch.tsx index 46560aaf..d6f52a2c 100644 --- a/src/components/StopWatch.tsx +++ b/src/components/StopWatch.tsx @@ -4,7 +4,8 @@ import './StopWatch.css'; const StopWatch: React.FC = () => { const [time, setTime] = useState(0); - const [isRunning, setIsRunning] = useState( false ); + const [isRunning, setIsRunning] = useState(false); + const [laps, setLaps] = useState([]); useEffect(() => { let interval: NodeJS.Timeout; @@ -29,8 +30,13 @@ const StopWatch: React.FC = () => { const resetTimer = () => { setTime(0); setIsRunning(false); + setLaps([]); }; + const recordLap = () => { + setLaps((prevLaps) => [...prevLaps, time]); + }; + return (
@@ -44,8 +50,20 @@ const StopWatch: React.FC = () => { onStart={startTimer} onStop={stopTimer} onReset={resetTimer} + onLap={recordLap} isRunning={isRunning} /> + +
+ {laps.map((lap, index) => ( +
+ Lap {index + 1}: + {Math.floor(lap / 3600).toString().padStart(2, '0')}: + {Math.floor((lap % 3600) / 60).toString().padStart(2, '0')}: + {(lap % 60).toString().padStart(2, '0')} +
+ ))} +
); }; diff --git a/src/components/StopWatchButton.tsx b/src/components/StopWatchButton.tsx index 7c86c96e..9546bd4f 100644 --- a/src/components/StopWatchButton.tsx +++ b/src/components/StopWatchButton.tsx @@ -4,6 +4,7 @@ interface StopWatchButtonProps { onStart: () => void; onStop: () => void; onReset: () => void; + onLap: () => void; isRunning: boolean; } @@ -11,6 +12,7 @@ const StopWatchButton: React.FC = ({ onStart, onStop, onReset, + onLap, isRunning, }) => { const [selectedButton, setSelectedButton] = useState(null); @@ -42,6 +44,13 @@ const StopWatchButton: React.FC = ({ > Reset + ); }; From b2f0c2de276eb8e2c2a9eb55b09541c190911808 Mon Sep 17 00:00:00 2001 From: Chelsea Dwarika Date: Mon, 29 Jan 2024 21:09:46 -0500 Subject: [PATCH 7/9] more styling fixes, milliseconds added --- src/components/StopWatch.css | 19 +++++++++++-------- src/components/StopWatch.tsx | 30 +++++++++++++++++------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/components/StopWatch.css b/src/components/StopWatch.css index 3049f531..5333dcd4 100644 --- a/src/components/StopWatch.css +++ b/src/components/StopWatch.css @@ -1,6 +1,11 @@ body { - background: rgb(218,112,214); - background: radial-gradient(circle, rgba(218,112,214,1) 0%, rgba(148,187,233,1) 100%); margin: 0; + background: rgb(218, 112, 214); + background: radial-gradient( + circle, + rgba(218, 112, 214, 1) 0%, + rgba(148, 187, 233, 1) 100% + ); + margin: 0; display: flex; min-height: 100vh; justify-content: center; @@ -27,22 +32,22 @@ body { } .btn { - background-color: #D8BFD8; + background-color: #d8bfd8; color: black; padding: 10px 20px; font-size: 1.5rem; - margin: 10px; + margin: 2px 13px 10px; cursor: pointer; border: 1px solid purple; border-radius: 4px; } .btn:hover { - background-color: #DA70D6; + background-color: #da70d6; } .btn.selected { - background-color: #DA70D6; + background-color: #da70d6; } .lap-list { @@ -66,5 +71,3 @@ body { margin: 5px; } } - - diff --git a/src/components/StopWatch.tsx b/src/components/StopWatch.tsx index d6f52a2c..b977227a 100644 --- a/src/components/StopWatch.tsx +++ b/src/components/StopWatch.tsx @@ -5,15 +5,15 @@ import './StopWatch.css'; const StopWatch: React.FC = () => { const [time, setTime] = useState(0); const [isRunning, setIsRunning] = useState(false); - const [laps, setLaps] = useState([]); + const [laps, setLaps] = useState([]); useEffect(() => { let interval: NodeJS.Timeout; if (isRunning) { interval = setInterval(() => { - setTime((prevTime) => prevTime + 1); - }, 1000); + setTime((prevTime) => prevTime + 10); + }, 10); } return () => clearInterval(interval); @@ -30,27 +30,29 @@ const StopWatch: React.FC = () => { const resetTimer = () => { setTime(0); setIsRunning(false); - setLaps([]); + setLaps([]); }; const recordLap = () => { - setLaps((prevLaps) => [...prevLaps, time]); + setLaps((prevLaps) => [...prevLaps, time]); }; - + return (
-

{Math.floor(time / 3600).toString().padStart(2, '0')}

+

{Math.floor(time / 3600000).toString().padStart(2, '0')}

: -

{Math.floor((time % 3600) / 60).toString().padStart(2, '0')}

+

{Math.floor((time % 3600000) / 60000).toString().padStart(2, '0')}

: -

{(time % 60).toString().padStart(2, '0')}

+

{Math.floor((time % 60000) / 1000).toString().padStart(2, '0')}

+ . +

{((time % 1000) / 10).toFixed(0).toString().padStart(2, '0')}

@@ -58,9 +60,11 @@ const StopWatch: React.FC = () => { {laps.map((lap, index) => (
Lap {index + 1}: - {Math.floor(lap / 3600).toString().padStart(2, '0')}: - {Math.floor((lap % 3600) / 60).toString().padStart(2, '0')}: - {(lap % 60).toString().padStart(2, '0')} + {Math.floor(lap / 3600000).toString().padStart(2, '0')}: + {Math.floor((lap % 3600000) / 60000).toString().padStart(2, '0')}: + {Math.floor((lap % 60000) / 1000).toString().padStart(2, '0')} + . + {((lap % 1000) / 10).toFixed(0).toString().padStart(2, '0')}
))}
From c72bff009f8d859886479c1da97c440eccc677eb Mon Sep 17 00:00:00 2001 From: Chelsea Dwarika Date: Mon, 29 Jan 2024 21:19:23 -0500 Subject: [PATCH 8/9] create testing folder/files --- src/tests/StopWatch.test.tsx | 0 src/tests/StopWatchButton.test.tsx | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/tests/StopWatch.test.tsx create mode 100644 src/tests/StopWatchButton.test.tsx diff --git a/src/tests/StopWatch.test.tsx b/src/tests/StopWatch.test.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/tests/StopWatchButton.test.tsx b/src/tests/StopWatchButton.test.tsx new file mode 100644 index 00000000..e69de29b From d6ef55cf8960c9e432e04e7e393f596fb21cc001 Mon Sep 17 00:00:00 2001 From: Chelsea Dwarika Date: Tue, 30 Jan 2024 00:01:25 -0500 Subject: [PATCH 9/9] testing incomplete - will learn from this --- package-lock.json | 724 +++++++++++++++++++++++++++++ package.json | 11 +- src/components/StopWatch.css | 2 + src/components/StopWatch.tsx | 93 ++-- src/components/StopWatchButton.tsx | 21 +- src/setupTests.js | 1 + src/tests/StopWatch.test.tsx | 18 + test/jest/stylemock.js | 3 + tsconfig.json | 3 +- 9 files changed, 838 insertions(+), 38 deletions(-) create mode 100644 src/setupTests.js create mode 100644 test/jest/stylemock.js diff --git a/package-lock.json b/package-lock.json index ef8315e5..542966e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@babel/preset-env": "^7.23.8", "@babel/preset-react": "^7.23.3", "@babel/preset-typescript": "^7.23.3", + "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^14.1.2", "@types/jest": "^29.5.11", "@types/node": "^16.18.30", @@ -29,6 +30,7 @@ "html-loader": "^4.2.0", "html-webpack-plugin": "^5.5.1", "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "react-test-renderer": "^18.2.0", "style-loader": "^3.3.2", "ts-jest": "^29.1.1", @@ -2785,6 +2787,138 @@ "node": ">=8" } }, + "node_modules/@testing-library/jest-dom": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-4.2.4.tgz", + "integrity": "sha512-j31Bn0rQo12fhCWOUWy9fl7wtqkp7In/YP2p5ZFyRuiiB9Qs3g+hS4gAmDWONbAHcRmVooNJ5eOHQDCOmUFXHg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.5.1", + "chalk": "^2.4.1", + "css": "^2.2.3", + "css.escape": "^1.5.1", + "jest-diff": "^24.0.0", + "jest-matcher-utils": "^24.0.0", + "lodash": "^4.17.11", + "pretty-format": "^24.0.0", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=8", + "npm": ">=6" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/@jest/types": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^13.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/@types/istanbul-reports": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", + "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/@types/yargs": { + "version": "13.0.12", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.12.tgz", + "integrity": "sha512-qCxJE1qgz2y0hA4pIxjBR+PelCH0U5CK1XJXFwCNqfmliatKp47UCXXE9Dyk1OXBDLvsCF57TqQEJaeLfDYEOQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/diff-sequences": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", + "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/jest-diff": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", + "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", + "dev": true, + "dependencies": { + "chalk": "^2.0.1", + "diff-sequences": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/jest-get-type": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/jest-matcher-utils": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz", + "integrity": "sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==", + "dev": true, + "dependencies": { + "chalk": "^2.0.1", + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/pretty-format": { + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", + "dev": true, + "dependencies": { + "@jest/types": "^24.9.0", + "ansi-regex": "^4.0.0", + "ansi-styles": "^3.2.0", + "react-is": "^16.8.4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, "node_modules/@testing-library/react": { "version": "14.1.2", "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.1.2.tgz", @@ -2803,6 +2937,15 @@ "react-dom": "^18.0.0" } }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -3034,6 +3177,17 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/@types/jsdom": { + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", + "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -3169,6 +3323,12 @@ "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", "dev": true }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true + }, "node_modules/@types/uglify-js": { "version": "3.17.1", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.1.tgz", @@ -3430,6 +3590,13 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3455,6 +3622,16 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, "node_modules/acorn-import-assertions": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", @@ -3464,6 +3641,27 @@ "acorn": "^8" } }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3626,6 +3824,24 @@ "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -4278,6 +4494,18 @@ "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "10.0.1", "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", @@ -4525,6 +4753,18 @@ "node": ">= 8" } }, + "node_modules/css": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", + "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "source-map": "^0.6.1", + "source-map-resolve": "^0.5.2", + "urix": "^0.1.0" + } + }, "node_modules/css-loader": { "version": "6.8.1", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", @@ -4612,6 +4852,12 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4624,12 +4870,50 @@ "node": ">=4" } }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + }, "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", "dev": true }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -4647,6 +4931,21 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, "node_modules/dedent": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", @@ -4760,6 +5059,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4871,6 +5179,19 @@ } ] }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/domhandler": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", @@ -5060,6 +5381,36 @@ "node": ">=0.8.0" } }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -5444,6 +5795,20 @@ "is-callable": "^1.1.3" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5775,6 +6140,18 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/html-entities": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", @@ -5961,6 +6338,20 @@ "node": ">=8.0.0" } }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/http-proxy-middleware": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", @@ -5985,6 +6376,19 @@ } } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -6046,6 +6450,15 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -6320,6 +6733,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -7337,6 +7756,33 @@ "node": ">=8" } }, + "node_modules/jest-environment-jsdom": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", + "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/jsdom": "^20.0.0", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0", + "jsdom": "^20.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jest-environment-node": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", @@ -8643,6 +9089,51 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -8941,6 +9432,15 @@ "node": ">=6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -9090,6 +9590,12 @@ "url": "https://github.com/fb55/nth-check?sponsor=1" } }, + "node_modules/nwsapi": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", + "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==", + "dev": true + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -9588,6 +10094,12 @@ "node": ">= 0.10" } }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -9628,6 +10140,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -9770,6 +10288,19 @@ "node": ">= 0.10" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -9942,6 +10473,13 @@ "node": ">=8" } }, + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "dev": true + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -10001,6 +10539,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -10342,6 +10892,20 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -10352,6 +10916,13 @@ "source-map": "^0.6.0" } }, + "node_modules/source-map-url": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", + "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", + "dev": true + }, "node_modules/spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", @@ -10496,6 +11067,18 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -10548,6 +11131,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -10689,6 +11278,33 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/ts-jest": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", @@ -10980,6 +11596,15 @@ "node": ">=4" } }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -11028,6 +11653,23 @@ "punycode": "^2.1.0" } }, + "node_modules/urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", + "dev": true + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -11087,6 +11729,18 @@ "node": ">= 0.8" } }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -11124,6 +11778,15 @@ "integrity": "sha512-4S6PQKwW8L2kPoZilvpNFYFFqZxfYvCWaGyVMXgETLfrqjbrD0RzNIlcAZPOiJx1/GqtNM9ibje47kSSd+Jn/w==", "dev": true }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/webpack": { "version": "5.88.2", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", @@ -11529,6 +12192,52 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11690,6 +12399,21 @@ } } }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index cf0d970a..a8425617 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@babel/preset-env": "^7.23.8", "@babel/preset-react": "^7.23.3", "@babel/preset-typescript": "^7.23.3", + "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^14.1.2", "@types/jest": "^29.5.11", "@types/node": "^16.18.30", @@ -32,6 +33,7 @@ "html-loader": "^4.2.0", "html-webpack-plugin": "^5.5.1", "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "react-test-renderer": "^18.2.0", "style-loader": "^3.3.2", "ts-jest": "^29.1.1", @@ -46,6 +48,13 @@ "jest": { "transform": { "^.+\\.(js|jsx|ts|tsx)$": "babel-jest" - } + }, + "moduleNameMapper": { + "\\.css$": "/test/jest/stylemock.js" + }, + "testEnvironment": "jsdom", + "setupFilesAfterEnv": [ + "/src/setupTests.js" + ] } } diff --git a/src/components/StopWatch.css b/src/components/StopWatch.css index 5333dcd4..01d74058 100644 --- a/src/components/StopWatch.css +++ b/src/components/StopWatch.css @@ -56,6 +56,8 @@ body { margin-top: 2rem; } +/* smaller screens */ + @media only screen and (max-width: 600px) { .container span { font-size: 4rem; diff --git a/src/components/StopWatch.tsx b/src/components/StopWatch.tsx index b977227a..6df241aa 100644 --- a/src/components/StopWatch.tsx +++ b/src/components/StopWatch.tsx @@ -1,53 +1,73 @@ -import React, { useState, useEffect } from 'react'; -import StopWatchButton from './StopWatchButton'; -import './StopWatch.css'; +import React, { useState, useEffect } from "react"; +import StopWatchButton from "./StopWatchButton"; +import "./StopWatch.css"; +//StopWatch functional component const StopWatch: React.FC = () => { - const [time, setTime] = useState(0); - const [isRunning, setIsRunning] = useState(false); - const [laps, setLaps] = useState([]); + const [time, setTime] = useState(0); // Track elapsed time + const [isRunning, setIsRunning] = useState(false); // Track running/paused + const [laps, setLaps] = useState([]); // Record lap times useEffect(() => { let interval: NodeJS.Timeout; - if (isRunning) { interval = setInterval(() => { - setTime((prevTime) => prevTime + 10); + // Update time every 10 milliseconds + setTime((prevTime) => prevTime + 10); // Add 10 milliseconds to prevTime }, 10); } - - return () => clearInterval(interval); - }, [isRunning]); + return () => clearInterval(interval); // Clear interval when paused/stopped + }, [isRunning]); // Run effect when isRunning state changed const startTimer = () => { - setIsRunning(true); + setIsRunning(true); // Set isRunning state to true }; const stopTimer = () => { - setIsRunning(false); + setIsRunning(false); // Set isRunning state to false }; const resetTimer = () => { - setTime(0); - setIsRunning(false); - setLaps([]); + setTime(0); // Reset time to 0 + setIsRunning(false); // Set isRunning state to false + setLaps([]); // Clear lap times }; const recordLap = () => { - setLaps((prevLaps) => [...prevLaps, time]); + setLaps((prevLaps) => [...prevLaps, time]); // Add time to Laps array }; return (
-
-

{Math.floor(time / 3600000).toString().padStart(2, '0')}

+
+ {/* hours */} +

+ {Math.floor(time / 3600000) + .toString() + .padStart(2, "0")} +

: -

{Math.floor((time % 3600000) / 60000).toString().padStart(2, '0')}

+ {/* mins */} +

+ {Math.floor((time % 3600000) / 60000) + .toString() + .padStart(2, "0")} +

: -

{Math.floor((time % 60000) / 1000).toString().padStart(2, '0')}

+ {/* seconds */} +

+ {Math.floor((time % 60000) / 1000) + .toString() + .padStart(2, "0")} +

. -

{((time % 1000) / 10).toFixed(0).toString().padStart(2, '0')}

+ {/*milliseconds */} +

+ {((time % 1000) / 10).toFixed(0).toString().padStart(2, "0")} +

+ + {/* StopWatchButton & props */} { onLap={recordLap} isRunning={isRunning} /> - +
+ {/* Map thru laps array and render times */} {laps.map((lap, index) => (
Lap {index + 1}: - {Math.floor(lap / 3600000).toString().padStart(2, '0')}: - {Math.floor((lap % 3600000) / 60000).toString().padStart(2, '0')}: - {Math.floor((lap % 60000) / 1000).toString().padStart(2, '0')} + {/* format and display lap time */} + + {Math.floor(lap / 3600000) + .toString() + .padStart(2, "0")} + : + + + {Math.floor((lap % 3600000) / 60000) + .toString() + .padStart(2, "0")} + : + + + {Math.floor((lap % 60000) / 1000) + .toString() + .padStart(2, "0")} + . - {((lap % 1000) / 10).toFixed(0).toString().padStart(2, '0')} + {/* add mlliseconds */} + + {((lap % 1000) / 10).toFixed(0).toString().padStart(2, "0")} +
))}
diff --git a/src/components/StopWatchButton.tsx b/src/components/StopWatchButton.tsx index 9546bd4f..35caa6a9 100644 --- a/src/components/StopWatchButton.tsx +++ b/src/components/StopWatchButton.tsx @@ -1,5 +1,6 @@ -import React, { useState } from 'react'; +import React, { useState } from "react"; +//props for StopWatchButton interface StopWatchButtonProps { onStart: () => void; onStop: () => void; @@ -15,8 +16,10 @@ const StopWatchButton: React.FC = ({ onLap, isRunning, }) => { + // track currently selected button const [selectedButton, setSelectedButton] = useState(null); + // button clicks, set selectedbutton state based on clicked const handleButtonClick = (action: string, callback: () => void) => { setSelectedButton(action); callback(); @@ -25,28 +28,28 @@ const StopWatchButton: React.FC = ({ return (