1- import React , { useCallback , useEffect , useMemo , useState } from 'react' ;
1+ import React , { useCallback , useEffect , useState } from 'react' ;
22import classNames from 'classnames' ;
33import './styles/App.css' ;
44import { DEFAULT_LIMIT_SEC , DEFAULT_WARNING_SEC } from './constants' ;
@@ -12,15 +12,30 @@ const App = () => {
1212 const [ timeWarning , setTimeWarning ] = useState ( DEFAULT_WARNING_SEC ) ;
1313
1414 useEffect ( ( ) => {
15- if ( timerStarted ) {
16- const timeout = setTimeout ( ( ) => {
17- setTime ( timer ( timeElapsed ) ) ;
18- } , 1000 ) ;
19- return ( ) => clearTimeout ( timeout ) ;
20- } else {
21- setTime ( timeElapsed ) ;
22- }
23- } , [ setTime , timeElapsed , timerStarted ] ) ;
15+ if ( ! timerStarted ) return ;
16+
17+ const timeout = setTimeout ( ( ) => {
18+ setTime ( timer ( timeElapsed ) ) ;
19+ } , 1000 ) ;
20+
21+ return ( ) => clearTimeout ( timeout ) ;
22+ } , [ timeElapsed , timerStarted ] ) ;
23+
24+ // Handle keyboard shortcuts
25+ useEffect ( ( ) => {
26+ const handleKeyPress = ( event ) => {
27+ if ( event . code === 'Space' ) {
28+ event . preventDefault ( ) ;
29+ setTimerStatus ( ( prev ) => ! prev ) ;
30+ } else if ( event . code === 'KeyR' ) {
31+ event . preventDefault ( ) ;
32+ setTime ( 0 ) ;
33+ }
34+ } ;
35+
36+ window . addEventListener ( 'keydown' , handleKeyPress ) ;
37+ return ( ) => window . removeEventListener ( 'keydown' , handleKeyPress ) ;
38+ } , [ ] ) ;
2439
2540 const handleLimitUpdate = useCallback ( ( value ) => {
2641 setTimeLimit ( value ) ;
@@ -30,73 +45,97 @@ const App = () => {
3045 setTimeWarning ( value ) ;
3146 } , [ ] ) ;
3247
33- const renderControls = useMemo ( ( ) => {
34- return (
35- < div >
36- < button
37- type = "button"
38- className = { classNames ( 'btn' , {
39- 'btn-primary' : ! timerStarted ,
40- 'btn-danger' : timerStarted ,
41- } ) }
42- onClick = { ( ) => setTimerStatus ( ! timerStarted ) }
43- >
44- { timerStarted ? 'Stop' : 'Start' }
45- </ button >
46- < button
47- type = "button"
48- className = "btn btn-dark"
49- onClick = { ( ) => setTime ( 0 ) }
50- >
51- Reset
52- </ button >
48+ const handleReset = useCallback ( ( ) => {
49+ setTime ( 0 ) ;
50+ } , [ ] ) ;
51+
52+ const handleToggleTimer = useCallback ( ( ) => {
53+ setTimerStatus ( ( prev ) => ! prev ) ;
54+ } , [ ] ) ;
55+
56+ const isWarning = timeElapsed > timeLimit - timeWarning ;
57+ const isOverLimit = timeElapsed > timeLimit ;
58+
59+ return (
60+ < div className = "position-relative" >
61+ < div
62+ className = { classNames ( 'timer' , {
63+ warning : isWarning && ! isOverLimit ,
64+ stop : isOverLimit ,
65+ } ) }
66+ >
67+ < div className = "time" role = "timer" aria-live = "polite" >
68+ { formatTime ( timeElapsed ) }
69+ </ div >
70+
71+ < div >
72+ < button
73+ type = "button"
74+ className = { classNames ( 'btn' , {
75+ 'btn-primary' : ! timerStarted ,
76+ 'btn-danger' : timerStarted ,
77+ } ) }
78+ onClick = { handleToggleTimer }
79+ aria-label = { timerStarted ? 'Stop timer' : 'Start timer' }
80+ >
81+ { timerStarted ? 'Stop' : 'Start' }
82+ </ button >
83+ < button
84+ type = "button"
85+ className = "btn btn-dark"
86+ onClick = { handleReset }
87+ aria-label = "Reset timer to zero"
88+ >
89+ Reset
90+ </ button >
91+ </ div >
5392 </ div >
54- ) ;
55- } , [ timerStarted ] ) ;
5693
57- const renderSettings = useMemo ( ( ) => {
58- return (
5994 < div className = "container my-5" >
6095 < div className = "row" >
6196 < div className = "col" >
6297 < div className = "input-group" >
63- < label className = "input-group-text" > Limit</ label >
98+ < label className = "input-group-text" htmlFor = "time-limit-input" >
99+ Limit
100+ </ label >
64101 < TimeInput
65- ariaLabel = "Set Time Limit"
66- ariaDescribedby = "time-limit"
102+ id = "time-limit-input"
103+ ariaLabel = "Set time limit in minutes and seconds"
104+ ariaDescribedby = "time-limit-help"
67105 placeholderSec = { timeLimit }
68106 onChange = { handleLimitUpdate }
69- > </ TimeInput >
107+ / >
70108 </ div >
109+ < small id = "time-limit-help" className = "form-text text-muted" >
110+ Timer stops when limit is reached
111+ </ small >
71112 </ div >
72113 < div className = "col" >
73114 < div className = "input-group" >
74115 < TimeInput
75- ariaLabel = "Set Time Limit"
76- ariaDescribedby = "time-limit"
116+ id = "time-warning-input"
117+ ariaLabel = "Set warning time in minutes and seconds"
118+ ariaDescribedby = "time-warning-help"
77119 placeholderSec = { timeWarning }
78120 onChange = { handleWarningUpdate }
79- > </ TimeInput >
80- < label className = "input-group-text" > Warning</ label >
121+ />
122+ < label className = "input-group-text" htmlFor = "time-warning-input" >
123+ Warning
124+ </ label >
81125 </ div >
126+ < small id = "time-warning-help" className = "form-text text-muted" >
127+ Warning shown before time limit
128+ </ small >
129+ </ div >
130+ </ div >
131+ < div className = "row mt-3" >
132+ < div className = "col text-center" >
133+ < small className = "text-muted" >
134+ Keyboard shortcuts: Space to start/stop, R to reset
135+ </ small >
82136 </ div >
83137 </ div >
84138 </ div >
85- ) ;
86- } , [ handleLimitUpdate , handleWarningUpdate , timeLimit , timeWarning ] ) ;
87-
88- return (
89- < div className = "position-relative" >
90- < div
91- className = { classNames ( 'timer' , {
92- warning : timeElapsed > timeLimit - timeWarning ,
93- stop : timeElapsed > timeLimit ,
94- } ) }
95- >
96- < div className = "time" > { formatTime ( timeElapsed ) } </ div >
97- { renderControls }
98- </ div >
99- { renderSettings }
100139 </ div >
101140 ) ;
102141} ;
0 commit comments