From 3ec28196d37fd8b4ad6b57569223fcdeaf5eb762 Mon Sep 17 00:00:00 2001 From: amDeimos666 <71735806+amDeimos666@users.noreply.github.com> Date: Tue, 13 Jun 2023 08:33:55 -0400 Subject: [PATCH 1/5] add countdown timer --- .../CountdownStatus/CountdownStatus.tsx | 150 ++++++++++++++++++ src/renderer/components/Header.tsx | 4 +- 2 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 src/renderer/components/CountdownStatus/CountdownStatus.tsx diff --git a/src/renderer/components/CountdownStatus/CountdownStatus.tsx b/src/renderer/components/CountdownStatus/CountdownStatus.tsx new file mode 100644 index 00000000..2aeadf48 --- /dev/null +++ b/src/renderer/components/CountdownStatus/CountdownStatus.tsx @@ -0,0 +1,150 @@ +import { styled } from '@/renderer/globalStyles/styled'; +import { + StyledPopup, + StyledPopupContent, + StyledPopupContainer, +} from '@/renderer/components/styles'; +import { IoMdStopwatch } from 'react-icons/io'; +import { Button } from '@/renderer/components/common/Button'; +import React, { useEffect, FC, useState, ChangeEvent } from 'react'; +import { LabeledInput } from '@/renderer/components/common/LabeledInput'; + +export const CountdownStatus: FC = () => { + const timeDisplayDefault = '00:00'; + + const [duration, setDuration] = useState(35); + const [timeRemaining, setTimeRemaining] = useState(0); + const [isTimerActive, setIsTimerActive] = useState(false); + const [timerDisplay, setTimerDisplay] = useState(timeDisplayDefault); + const [countDownDate, setCountDownDate] = useState(Date.now()); + + const updateDuration = (e: ChangeEvent) => { + setDuration(Number(e.target.value)); + }; + + const startTimer = () => { + setIsTimerActive(false); + setCountDownDate(Date.now() + duration * 60 * 1000); + setIsTimerActive(true); + }; + + const stopTimer = () => { + setIsTimerActive(false); + }; + + const isShowTimerDisplay = () => { + return timerDisplay !== '00:00'; + }; + + useEffect(() => { + let interval: ReturnType | undefined; + const intervalMs = 1000; + if (isTimerActive) { + interval = setInterval(() => { + setTimerDisplay(getTimeRemaining()); + setTimeRemaining(timeRemaining - intervalMs); + }, intervalMs); + } else if (!isTimerActive && timeRemaining !== 0) { + if (interval !== undefined) { + clearInterval(interval); + } + setTimerDisplay(timeDisplayDefault); + setTimerDisplay(timeDisplayDefault); + setTimeRemaining(0); + } + + const getTimeRemaining = () => { + const total = countDownDate - Date.now(); + + const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((total % (1000 * 60)) / 1000); + + const minutesDiplay = + minutes < 10 ? '0' + minutes.toString() : minutes.toString(); + const secondsDiplay = + seconds < 10 ? '0' + seconds.toString() : seconds.toString(); + + if (total < 0) { + setIsTimerActive(false); + } + return `${minutesDiplay}:${secondsDiplay}`; + }; + return () => clearInterval(interval); + }, [isTimerActive, countDownDate, timeRemaining]); + + return ( + + + {isShowTimerDisplay() && timerDisplay} + + + } + on="click" + position="bottom center" + arrow={false} + repositionOnResize={true} + > + + +
+ + + + + +
+ + Time left +

{timerDisplay}

+
+
+
+
+
+ ); +}; + +const StyledDiv = styled.div` + display: flex; + column-gap: 5px; +`; + +const StyledIoMdStopwatch = styled(IoMdStopwatch)` + height: 1.25em; + width: 1.25em; +`; +const StyledDivTimer = styled.div` + margin: 1em; +`; + +const StyledDivDuration = styled.div` + display: flex; + column-gap: 20px; +`; + +const StyledP = styled.p` + margin-right: 20px; +`; + +const StyledDivInfo = styled(StyledDivDuration)` + margin: 8px; +`; diff --git a/src/renderer/components/Header.tsx b/src/renderer/components/Header.tsx index e87153c0..614562ef 100644 --- a/src/renderer/components/Header.tsx +++ b/src/renderer/components/Header.tsx @@ -6,6 +6,7 @@ import { useSelector } from 'react-redux'; import BatteryStatus from './BatteryStatus/BatteryStatus'; import GpioPinsStatus from './GpioPinsStatus/GpioPinsStatus'; import { ExplorationStatus } from './ExplorationStatus/ExplorationStatus'; +import { CountdownStatus } from './CountdownStatus/CountdownStatus'; interface NavLinkDefinition { to: string; @@ -49,6 +50,7 @@ export const Header: FC = () => { ))} + @@ -61,7 +63,7 @@ export const Header: FC = () => { const HeaderGrid = styled.div` display: grid; - grid-template-columns: 1fr 400px; + grid-template-columns: 1fr 500px; box-shadow: 0 3px 2px rgba(0, 0, 0, 0.25); `; From 1c295f6676c23cb4b2cbceabad29bcb419c61907 Mon Sep 17 00:00:00 2001 From: amDeimos666 <71735806+amDeimos666@users.noreply.github.com> Date: Tue, 13 Jun 2023 10:18:13 -0400 Subject: [PATCH 2/5] add countdown && create a component --- .../CountdownStatus/CountdownStatus.tsx | 145 +-------------- .../ExplorationStatus/ExplorationStatus.tsx | 155 ++-------------- src/renderer/components/common/Countdown.tsx | 172 ++++++++++++++++++ 3 files changed, 198 insertions(+), 274 deletions(-) create mode 100644 src/renderer/components/common/Countdown.tsx diff --git a/src/renderer/components/CountdownStatus/CountdownStatus.tsx b/src/renderer/components/CountdownStatus/CountdownStatus.tsx index 2aeadf48..b66daa90 100644 --- a/src/renderer/components/CountdownStatus/CountdownStatus.tsx +++ b/src/renderer/components/CountdownStatus/CountdownStatus.tsx @@ -1,150 +1,19 @@ import { styled } from '@/renderer/globalStyles/styled'; -import { - StyledPopup, - StyledPopupContent, - StyledPopupContainer, -} from '@/renderer/components/styles'; import { IoMdStopwatch } from 'react-icons/io'; -import { Button } from '@/renderer/components/common/Button'; -import React, { useEffect, FC, useState, ChangeEvent } from 'react'; -import { LabeledInput } from '@/renderer/components/common/LabeledInput'; +import { Countdown } from '@/renderer/components/common/Countdown'; +import React, { FC } from 'react'; export const CountdownStatus: FC = () => { - const timeDisplayDefault = '00:00'; - - const [duration, setDuration] = useState(35); - const [timeRemaining, setTimeRemaining] = useState(0); - const [isTimerActive, setIsTimerActive] = useState(false); - const [timerDisplay, setTimerDisplay] = useState(timeDisplayDefault); - const [countDownDate, setCountDownDate] = useState(Date.now()); - - const updateDuration = (e: ChangeEvent) => { - setDuration(Number(e.target.value)); - }; - - const startTimer = () => { - setIsTimerActive(false); - setCountDownDate(Date.now() + duration * 60 * 1000); - setIsTimerActive(true); - }; - - const stopTimer = () => { - setIsTimerActive(false); - }; - - const isShowTimerDisplay = () => { - return timerDisplay !== '00:00'; - }; - - useEffect(() => { - let interval: ReturnType | undefined; - const intervalMs = 1000; - if (isTimerActive) { - interval = setInterval(() => { - setTimerDisplay(getTimeRemaining()); - setTimeRemaining(timeRemaining - intervalMs); - }, intervalMs); - } else if (!isTimerActive && timeRemaining !== 0) { - if (interval !== undefined) { - clearInterval(interval); - } - setTimerDisplay(timeDisplayDefault); - setTimerDisplay(timeDisplayDefault); - setTimeRemaining(0); - } - - const getTimeRemaining = () => { - const total = countDownDate - Date.now(); - - const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((total % (1000 * 60)) / 1000); - - const minutesDiplay = - minutes < 10 ? '0' + minutes.toString() : minutes.toString(); - const secondsDiplay = - seconds < 10 ? '0' + seconds.toString() : seconds.toString(); - - if (total < 0) { - setIsTimerActive(false); - } - return `${minutesDiplay}:${secondsDiplay}`; - }; - return () => clearInterval(interval); - }, [isTimerActive, countDownDate, timeRemaining]); - return ( - - - {isShowTimerDisplay() && timerDisplay} - - - } - on="click" - position="bottom center" - arrow={false} - repositionOnResize={true} - > - - -
- - - - - -
- - Time left -

{timerDisplay}

-
-
-
-
-
+ } + labelPopup={'exploration'} + durationDefault={35} + /> ); }; -const StyledDiv = styled.div` - display: flex; - column-gap: 5px; -`; - const StyledIoMdStopwatch = styled(IoMdStopwatch)` height: 1.25em; width: 1.25em; `; -const StyledDivTimer = styled.div` - margin: 1em; -`; - -const StyledDivDuration = styled.div` - display: flex; - column-gap: 20px; -`; - -const StyledP = styled.p` - margin-right: 20px; -`; - -const StyledDivInfo = styled(StyledDivDuration)` - margin: 8px; -`; diff --git a/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx index bb24df77..f5f7c3c3 100644 --- a/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx +++ b/src/renderer/components/ExplorationStatus/ExplorationStatus.tsx @@ -1,81 +1,25 @@ import { styled } from '@/renderer/globalStyles/styled'; import { ExplorationCancelNavigation } from './ExplorationCancelNavigation'; -import { - StyledPopup, - StyledPopupContent, - StyledPopupContainer, -} from '@/renderer/components/styles'; import { GoTelescope } from 'react-icons/go'; -import { Button } from '@/renderer/components/common/Button'; +import { Countdown } from '@/renderer/components/common/Countdown'; import { rosClient } from '@/renderer/utils/ros/rosClient'; -import React, { useEffect, FC, useState, ChangeEvent } from 'react'; -import { LabeledInput } from '@/renderer/components/common/LabeledInput'; +import React, { FC, useState } from 'react'; import { log } from '@/renderer/logger'; export const ExplorationStatus: FC = () => { - const timeDisplayDefault = '00:00'; + const [isNowStopCountdownTimer, setIsNowStopCountdownTimer] = useState(false); - const [duration, setDuration] = useState(2); - const [timeRemaining, setTimeRemaining] = useState(0); - const [isTimerActive, setIsTimerActive] = useState(false); - const [timerDisplay, setTimerDisplay] = useState(timeDisplayDefault); - const [countDownDate, setCountDownDate] = useState(Date.now()); - - const updateDuration = (e: ChangeEvent) => { - setDuration(Number(e.target.value)); - }; - - const startTimer = () => { - setIsTimerActive(false); - setCountDownDate(Date.now() + duration * 60 * 1000); - setIsTimerActive(true); - startRosExplorationTimer(); + const startTimer = (duration: number) => { + setIsNowStopCountdownTimer(false); + startRosExplorationTimer(duration); }; const stopTimer = () => { - setIsTimerActive(false); + setIsNowStopCountdownTimer(true); stopRosExplorationTimer(); }; - const isShowTimerDisplay = () => { - return timerDisplay !== '00:00'; - }; - - useEffect(() => { - let interval: ReturnType | undefined; - const intervalMs = 1000; - if (isTimerActive) { - interval = setInterval(() => { - setTimerDisplay(getTimeRemaining()); - setTimeRemaining(timeRemaining - intervalMs); - }, intervalMs); - } else if (!isTimerActive && timeRemaining !== 0) { - if (interval !== undefined) { - clearInterval(interval); - } - setTimerDisplay(timeDisplayDefault); - setTimerDisplay(timeDisplayDefault); - setTimeRemaining(0); - } - - const getTimeRemaining = () => { - const total = countDownDate - Date.now(); - - const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((total % (1000 * 60)) / 1000); - - const minutesDiplay = minutes.toString().padStart(2, '0'); - const secondsDiplay = seconds.toString().padStart(2, '0'); - - if (total < 0) { - setIsTimerActive(false); - } - return `${minutesDiplay}:${secondsDiplay}`; - }; - return () => clearInterval(interval); - }, [isTimerActive, countDownDate, timeRemaining]); - - const startRosExplorationTimer = () => { + const startRosExplorationTimer = (duration: number) => { rosClient .callService( { @@ -98,82 +42,21 @@ export const ExplorationStatus: FC = () => { }; return ( - - - - {isShowTimerDisplay() && timerDisplay} - - - } - on="click" - position="bottom center" - arrow={false} - repositionOnResize={true} - > - - -
- - - - - -
- - Time left -

{timerDisplay}

- -
-
-
-
-
+ } + labelPopup={'exploration'} + durationDefault={2} + onStartClick={startTimer} + onStopClick={stopTimer} + sideElement={ + + } + isNowStopCountdownTimer={isNowStopCountdownTimer} + /> ); }; -const StyledDiv = styled.div` - display: flex; - column-gap: 5px; -`; - const StyledGoTelescope = styled(GoTelescope)` height: 1.25em; width: 1.25em; `; -const StyledDivTimer = styled.div` - margin: 1em; -`; - -const StyledDivDuration = styled.div` - display: flex; - column-gap: 20px; -`; - -const StyledP = styled.p` - margin-right: 20px; -`; - -const StyledDivInfo = styled(StyledDivDuration)` - margin: 8px; -`; diff --git a/src/renderer/components/common/Countdown.tsx b/src/renderer/components/common/Countdown.tsx new file mode 100644 index 00000000..a5c7ee62 --- /dev/null +++ b/src/renderer/components/common/Countdown.tsx @@ -0,0 +1,172 @@ +import { styled } from '@/renderer/globalStyles/styled'; +import { + StyledPopup, + StyledPopupContent, + StyledPopupContainer, +} from '@/renderer/components/styles'; +import { Button } from '@/renderer/components/common/Button'; +import React, { useEffect, FC, useState, ChangeEvent } from 'react'; +import { LabeledInput } from '@/renderer/components/common/LabeledInput'; + +interface CountdownProps { + icon: JSX.Element; + labelPopup: string; + durationDefault: number; + onStartClick?: (duration: number) => void; + onStopClick?: () => void; + sideElement?: JSX.Element; + isNowStopCountdownTimer?: boolean; +} + +export const Countdown: FC = ({ + icon, + labelPopup, + durationDefault, + onStartClick, + onStopClick, + sideElement, + isNowStopCountdownTimer, +}) => { + const timeDisplayDefault = '00:00'; + + const [duration, setDuration] = useState(durationDefault); + const [timeRemaining, setTimeRemaining] = useState(0); + const [isTimerActive, setIsTimerActive] = useState(false); + const [timerDisplay, setTimerDisplay] = useState(timeDisplayDefault); + const [countDownDate, setCountDownDate] = useState(Date.now()); + + const updateDuration = (e: ChangeEvent) => { + setDuration(Number(e.target.value)); + }; + + const startTimer = () => { + setIsTimerActive(false); + setCountDownDate(Date.now() + duration * 60 * 1000); + setIsTimerActive(true); + if (onStartClick !== undefined) { + onStartClick(duration); + } + }; + + const stopTimer = () => { + setIsTimerActive(false); + if (onStopClick !== undefined) { + onStopClick(); + } + }; + + const isShowTimerDisplay = () => { + return timerDisplay !== '00:00'; + }; + + useEffect(() => { + let interval: ReturnType | undefined; + const intervalMs = 1000; + if (isTimerActive && !isNowStopCountdownTimer) { + interval = setInterval(() => { + setTimerDisplay(getTimeRemaining()); + setTimeRemaining(timeRemaining - intervalMs); + }, intervalMs); + } else if ( + isNowStopCountdownTimer || + (!isTimerActive && timeRemaining !== 0) + ) { + if (interval !== undefined) { + clearInterval(interval); + } + setTimerDisplay(timeDisplayDefault); + setTimerDisplay(timeDisplayDefault); + setTimeRemaining(0); + } + + const getTimeRemaining = () => { + const total = countDownDate - Date.now(); + + const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((total % (1000 * 60)) / 1000); + + const minutesDiplay = minutes.toString().padStart(2, '0'); + const secondsDiplay = seconds.toString().padStart(2, '0'); + + if (total < 0) { + setIsTimerActive(false); + } + return `${minutesDiplay}:${secondsDiplay}`; + }; + return () => clearInterval(interval); + }, [isTimerActive, isNowStopCountdownTimer, countDownDate, timeRemaining]); + + return ( + + {sideElement} + + {isShowTimerDisplay() && timerDisplay} + {icon} + + } + on="click" + position="bottom center" + arrow={false} + repositionOnResize={true} + > + + +
+ + + + + +
+ + Time left +

{timerDisplay}

+ {sideElement} +
+
+
+
+
+ ); +}; + +const StyledDiv = styled.div` + display: flex; + column-gap: 5px; +`; + +const StyledDivTimer = styled.div` + margin: 1em; +`; + +const StyledDivDuration = styled.div` + display: flex; + column-gap: 20px; +`; + +const StyledDivInfo = styled(StyledDivDuration)` + margin: 8px; +`; + +const StyledP = styled.p` + margin-right: 20px; +`; From cf66e41decb84a0abb4b20552a07a64293f9d1b3 Mon Sep 17 00:00:00 2001 From: amDeimos666 <71735806+amDeimos666@users.noreply.github.com> Date: Wed, 14 Jun 2023 06:05:37 -0400 Subject: [PATCH 3/5] fix typo --- src/renderer/components/CountdownStatus/CountdownStatus.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/components/CountdownStatus/CountdownStatus.tsx b/src/renderer/components/CountdownStatus/CountdownStatus.tsx index b66daa90..36a3a86f 100644 --- a/src/renderer/components/CountdownStatus/CountdownStatus.tsx +++ b/src/renderer/components/CountdownStatus/CountdownStatus.tsx @@ -7,7 +7,7 @@ export const CountdownStatus: FC = () => { return ( } - labelPopup={'exploration'} + labelPopup={'scenario'} durationDefault={35} /> ); From 7677622f425a9cbb6e122d249c3f86a0bc4637fb Mon Sep 17 00:00:00 2001 From: Samuel Lachance Date: Wed, 14 Jun 2023 12:29:37 +0200 Subject: [PATCH 4/5] Refactor the useEffect --- src/renderer/components/common/Countdown.tsx | 142 ++++++++++++------- 1 file changed, 92 insertions(+), 50 deletions(-) diff --git a/src/renderer/components/common/Countdown.tsx b/src/renderer/components/common/Countdown.tsx index a5c7ee62..c317a157 100644 --- a/src/renderer/components/common/Countdown.tsx +++ b/src/renderer/components/common/Countdown.tsx @@ -5,9 +5,11 @@ import { StyledPopupContainer, } from '@/renderer/components/styles'; import { Button } from '@/renderer/components/common/Button'; -import React, { useEffect, FC, useState, ChangeEvent } from 'react'; +import React, { useEffect, FC, useState, ChangeEvent, useRef } from 'react'; import { LabeledInput } from '@/renderer/components/common/LabeledInput'; +const INTERVAL_MS = 1000; + interface CountdownProps { icon: JSX.Element; labelPopup: string; @@ -15,6 +17,9 @@ interface CountdownProps { onStartClick?: (duration: number) => void; onStopClick?: () => void; sideElement?: JSX.Element; + /** + * If true, the timer will be stopped. + */ isNowStopCountdownTimer?: boolean; } @@ -27,74 +32,111 @@ export const Countdown: FC = ({ sideElement, isNowStopCountdownTimer, }) => { - const timeDisplayDefault = '00:00'; + const intervalRef = useRef>(); const [duration, setDuration] = useState(durationDefault); - const [timeRemaining, setTimeRemaining] = useState(0); - const [isTimerActive, setIsTimerActive] = useState(false); - const [timerDisplay, setTimerDisplay] = useState(timeDisplayDefault); - const [countDownDate, setCountDownDate] = useState(Date.now()); + const [timerDisplay, setTimerDisplay] = useState('00:00'); + const countDownDate = useRef(0); - const updateDuration = (e: ChangeEvent) => { - setDuration(Number(e.target.value)); + + const getTimeRemaining = () => { + return countDownDate.current - Date.now(); + }; + + useEffect(() => { + if (isNowStopCountdownTimer) { + stopTimer(); + updateTimerDisplay(); + } + }, [isNowStopCountdownTimer]); + + /** + * Parses a time in milliseconds to a string in the format mm:ss + * + * @param total - time in milliseconds + */ + const formatTime = (total: number): string => { + const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); + const seconds = Math.floor((total % (1000 * 60)) / 1000); + + const minutesDiplay = minutes.toString().padStart(2, '0'); + const secondsDiplay = seconds.toString().padStart(2, '0'); + + return `${minutesDiplay}:${secondsDiplay}`; }; const startTimer = () => { - setIsTimerActive(false); - setCountDownDate(Date.now() + duration * 60 * 1000); - setIsTimerActive(true); + if (intervalRef.current !== undefined) { + stopTimer(); + } + countDownDate.current = Date.now() + duration * 60 * 1000; + intervalRef.current = setInterval(handleTimerTick, INTERVAL_MS); if (onStartClick !== undefined) { onStartClick(duration); } }; const stopTimer = () => { - setIsTimerActive(false); + clearInterval(intervalRef.current); + intervalRef.current = undefined; + countDownDate.current = 0; if (onStopClick !== undefined) { onStopClick(); } }; - const isShowTimerDisplay = () => { - return timerDisplay !== '00:00'; + /** + * Updates the timer display. + * If the timer is negative, it will be set to 0. + */ + const updateTimerDisplay = () => { + let time = getTimeRemaining(); + if (time < 0) time = 0; + const timeDisplay = formatTime(time); + setTimerDisplay(timeDisplay); }; - useEffect(() => { - let interval: ReturnType | undefined; - const intervalMs = 1000; - if (isTimerActive && !isNowStopCountdownTimer) { - interval = setInterval(() => { - setTimerDisplay(getTimeRemaining()); - setTimeRemaining(timeRemaining - intervalMs); - }, intervalMs); - } else if ( - isNowStopCountdownTimer || - (!isTimerActive && timeRemaining !== 0) - ) { - if (interval !== undefined) { - clearInterval(interval); - } - setTimerDisplay(timeDisplayDefault); - setTimerDisplay(timeDisplayDefault); - setTimeRemaining(0); - } + const isTimerActive = () => { + return countDownDate.current > Date.now(); + }; - const getTimeRemaining = () => { - const total = countDownDate - Date.now(); + /** + * This function is called when the user clicks the start button. + */ + const handleStartButtonClick = () => { + startTimer(); + handleTimerTick(); + }; - const minutes = Math.floor((total % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((total % (1000 * 60)) / 1000); + /** + * This function is called when the user clicks the stop button. + */ + const handleStopButtonClick = () => { + stopTimer(); + updateTimerDisplay(); + }; - const minutesDiplay = minutes.toString().padStart(2, '0'); - const secondsDiplay = seconds.toString().padStart(2, '0'); - if (total < 0) { - setIsTimerActive(false); - } - return `${minutesDiplay}:${secondsDiplay}`; - }; - return () => clearInterval(interval); - }, [isTimerActive, isNowStopCountdownTimer, countDownDate, timeRemaining]); + /** + * This function is called when the user changes the duration. + * @param e - the event + */ + const handleDurationChange = (e: ChangeEvent) => { + setDuration(Number(e.target.value)); + }; + + /** + * This function is called every second by the timer. + */ + const handleTimerTick = () => { + const time = getTimeRemaining(); + if (time <= 0) { + countDownDate.current = 0; + stopTimer(); + } + + updateTimerDisplay(); + }; return ( @@ -102,7 +144,7 @@ export const Countdown: FC = ({ - {isShowTimerDisplay() && timerDisplay} + {isTimerActive() && timerDisplay} {icon} } @@ -118,18 +160,18 @@ export const Countdown: FC = ({ label={`Duration of ${labelPopup} (min)`} value={duration.toString()} type="number" - onChange={updateDuration} + onChange={handleDurationChange} />