From c26ab053a68bf261762453f0e5b3ed67c754ea7d Mon Sep 17 00:00:00 2001 From: Mimi Flynn <414934+mimiflynn@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:21:03 -0500 Subject: [PATCH 1/4] various code improvements and added keyboard shortcuts --- src/App.jsx | 153 ++++++++++++++++++++++++++---------------- src/lib/time-utils.js | 68 +++++++++++++++---- 2 files changed, 150 insertions(+), 71 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 6eb8bb5..7410b86 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import classNames from 'classnames'; import './styles/App.css'; import { DEFAULT_LIMIT_SEC, DEFAULT_WARNING_SEC } from './constants'; @@ -12,15 +12,30 @@ const App = () => { const [timeWarning, setTimeWarning] = useState(DEFAULT_WARNING_SEC); useEffect(() => { - if (timerStarted) { - const timeout = setTimeout(() => { - setTime(timer(timeElapsed)); - }, 1000); - return () => clearTimeout(timeout); - } else { - setTime(timeElapsed); - } - }, [setTime, timeElapsed, timerStarted]); + if (!timerStarted) return; + + const timeout = setTimeout(() => { + setTime(timer(timeElapsed)); + }, 1000); + + return () => clearTimeout(timeout); + }, [timeElapsed, timerStarted]); + + // Handle keyboard shortcuts + useEffect(() => { + const handleKeyPress = (event) => { + if (event.code === 'Space') { + event.preventDefault(); + setTimerStatus((prev) => !prev); + } else if (event.code === 'KeyR') { + event.preventDefault(); + setTime(0); + } + }; + + window.addEventListener('keydown', handleKeyPress); + return () => window.removeEventListener('keydown', handleKeyPress); + }, []); const handleLimitUpdate = useCallback((value) => { setTimeLimit(value); @@ -30,73 +45,97 @@ const App = () => { setTimeWarning(value); }, []); - const renderControls = useMemo(() => { - return ( -
- - + const handleReset = useCallback(() => { + setTime(0); + }, []); + + const handleToggleTimer = useCallback(() => { + setTimerStatus((prev) => !prev); + }, []); + + const isWarning = timeElapsed > timeLimit - timeWarning; + const isOverLimit = timeElapsed > timeLimit; + + return ( +
+
+
+ {formatTime(timeElapsed)} +
+ +
+ + +
- ); - }, [timerStarted]); - const renderSettings = useMemo(() => { - return (
- + + />
+ + Timer stops when limit is reached +
- + /> +
+ + Warning shown before time limit + +
+
+
+
+ + Keyboard shortcuts: Space to start/stop, R to reset +
- ); - }, [handleLimitUpdate, handleWarningUpdate, timeLimit, timeWarning]); - - return ( -
-
timeLimit - timeWarning, - stop: timeElapsed > timeLimit, - })} - > -
{formatTime(timeElapsed)}
- {renderControls} -
- {renderSettings}
); }; diff --git a/src/lib/time-utils.js b/src/lib/time-utils.js index 169c093..1e48b05 100644 --- a/src/lib/time-utils.js +++ b/src/lib/time-utils.js @@ -1,25 +1,49 @@ +/** + * Increments the timer by one second + * @param {number} secondsElapsed - The current number of seconds elapsed + * @returns {number} The incremented seconds value + */ export function timer(secondsElapsed) { - if (!secondsElapsed) return 1; + if (!secondsElapsed || typeof secondsElapsed !== 'number') return 1; return secondsElapsed + 1; } +/** + * Formats seconds into MM:SS format + * @param {number|string} elapsedSeconds - The number of seconds to format, or already formatted string + * @returns {string} Time formatted as MM:SS + */ export function formatTime(elapsedSeconds) { if (!elapsedSeconds) return '0:00'; - if (elapsedSeconds.length > 2) return elapsedSeconds; - if (isNaN(elapsedSeconds)) return elapsedSeconds; + + // If already formatted as a string, return as-is + if (typeof elapsedSeconds === 'string') return elapsedSeconds; + + // If not a valid number, return the value as-is + if (typeof elapsedSeconds !== 'number' || isNaN(elapsedSeconds)) { + return String(elapsedSeconds); + } + const minutes = Math.floor(elapsedSeconds / 60); const seconds = elapsedSeconds % 60; - const displaySeconds = - seconds < 10 ? `0${seconds.toString()}` : seconds.toString(); + const displaySeconds = seconds < 10 ? `0${seconds}` : `${seconds}`; return `${minutes}:${displaySeconds}`; } +/** + * Formats user input into MM:SS time format + * Removes non-numeric characters and adds colon separator + * @param {string} inputTime - The raw user input + * @returns {string} Formatted time as MM:SS + */ export function formatInputTime(inputTime) { if (!inputTime) return '0:00'; - const removeNonnumeric = inputTime.replace(/\D/g, ''); - const numberString = removeNonnumeric.replace(/\b0+/g, ''); + // Remove all non-numeric characters + const numericOnly = inputTime.replace(/\D/g, ''); + // Remove leading zeros + const numberString = numericOnly.replace(/^0+/, '') || '0'; if (numberString.length === 1) { return `0:0${numberString}`; @@ -28,15 +52,31 @@ export function formatInputTime(inputTime) { return `0:${numberString}`; } - // add colon - const timeArr = String(numberString).split(''); - timeArr.splice(timeArr.length - 2, 0, ':'); - return timeArr.join(''); + // Insert colon before the last 2 digits (seconds) + const timeArray = numberString.split(''); + timeArray.splice(timeArray.length - 2, 0, ':'); + return timeArray.join(''); } +/** + * Converts formatted time (MM:SS) to total seconds + * @param {string|number} formattedTime - Time in MM:SS format or raw seconds + * @returns {number} Total seconds + */ export function convertToSeconds(formattedTime) { if (!formattedTime) return 0; - if (!isNaN(formattedTime)) return formattedTime; - const value = formattedTime.split(':'); - return Number(value[0]) * 60 + Number(value[1]); + + // If already a number, return it + if (typeof formattedTime === 'number') return formattedTime; + + // If not a valid number as string, try to parse as MM:SS + if (!isNaN(formattedTime)) return Number(formattedTime); + + const parts = String(formattedTime).split(':'); + if (parts.length !== 2) return 0; + + const minutes = Number(parts[0]) || 0; + const seconds = Number(parts[1]) || 0; + + return minutes * 60 + seconds; } From 49f68ce8fc6650e1787c327140416b37ddb5c811 Mon Sep 17 00:00:00 2001 From: Mimi Flynn <414934+mimiflynn@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:28:04 -0500 Subject: [PATCH 2/4] update tests --- package.json | 3 +++ src/components/time-input.jsx | 7 ++++--- src/lib/time-utils.js | 10 ++++++++++ src/setupTests.js | 20 ++++++++++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index f60a186..b394c96 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,9 @@ "type": "module", "jest": { "testEnvironment": "jsdom", + "setupFilesAfterEnv": [ + "/src/setupTests.js" + ], "moduleNameMapper": { "^.+\\.svg$": "jest-svg-transformer", "^.+\\.(css|less|scss)$": "identity-obj-proxy" diff --git a/src/components/time-input.jsx b/src/components/time-input.jsx index f3f1c77..5d752ba 100644 --- a/src/components/time-input.jsx +++ b/src/components/time-input.jsx @@ -14,10 +14,11 @@ export const TimeInput = ({ const [value, setValue] = useState(formatTime(placeholderSec)); const handleChange = useCallback( ({ target }) => { - setValue(formatInputTime(formatTime(target.value))); - onChange(convertToSeconds(target.value)); + const formattedValue = formatInputTime(target.value); + setValue(formattedValue); + onChange(convertToSeconds(formattedValue)); }, - [onChange] + [onChange], ); return ( diff --git a/src/lib/time-utils.js b/src/lib/time-utils.js index 1e48b05..ca37021 100644 --- a/src/lib/time-utils.js +++ b/src/lib/time-utils.js @@ -34,6 +34,7 @@ export function formatTime(elapsedSeconds) { /** * Formats user input into MM:SS time format * Removes non-numeric characters and adds colon separator + * Handles conversion when seconds >= 60 * @param {string} inputTime - The raw user input * @returns {string} Formatted time as MM:SS */ @@ -49,6 +50,15 @@ export function formatInputTime(inputTime) { return `0:0${numberString}`; } if (numberString.length === 2) { + // Check if this represents seconds >= 60, convert to minutes + const seconds = parseInt(numberString, 10); + if (seconds >= 60) { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + const displaySeconds = + remainingSeconds < 10 ? `0${remainingSeconds}` : `${remainingSeconds}`; + return `${minutes}:${displaySeconds}`; + } return `0:${numberString}`; } diff --git a/src/setupTests.js b/src/setupTests.js index 8f2609b..a416ad1 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -3,3 +3,23 @@ // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; +import * as React from 'react'; + +// Suppress ReactDOMTestUtils.act deprecation warning +// Use React.act instead of ReactDOMTestUtils.act +const originalError = console.error; +beforeAll(() => { + console.error = (...args) => { + if ( + typeof args[0] === 'string' && + args[0].includes('ReactDOMTestUtils.act') + ) { + return; + } + originalError.call(console, ...args); + }; +}); + +afterAll(() => { + console.error = originalError; +}); From 12a355c988e5dc270a437185662c4a2c728a8516 Mon Sep 17 00:00:00 2001 From: Mimi Flynn <414934+mimiflynn@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:33:03 -0500 Subject: [PATCH 3/4] fix time input field --- src/components/time-input.test.js | 12 ++++----- src/lib/time-utils.js | 44 +++++++++++++++++-------------- src/lib/time-utils.test.js | 10 +++---- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/components/time-input.test.js b/src/components/time-input.test.js index 6398fe1..79e8054 100644 --- a/src/components/time-input.test.js +++ b/src/components/time-input.test.js @@ -10,7 +10,7 @@ const setup = () => { placeholder="0:30" onChange={(event) => console.log('change', event)} value={90} - > + >, ); const input = screen.getByLabelText('time'); return { @@ -22,17 +22,17 @@ const setup = () => { test('Input should set display value', () => { const { input } = setup(); fireEvent.change(input, { target: { value: '23' } }); - expect(input.value).toBe('0:23'); + expect(input.value).toBe('23:00'); fireEvent.change(input, { target: { value: 'y15efg' } }); - expect(input.value).toBe('0:15'); + expect(input.value).toBe('15:00'); fireEvent.change(input, { target: { value: '8' } }); fireEvent.change(input, { target: { value: '80' } }); - fireEvent.change(input, { target: { value: '800' } }); + fireEvent.change(input, { target: { value: '8:00' } }); expect(input.value).toBe('8:00'); fireEvent.change(input, { target: { value: '9' } }); - fireEvent.change(input, { target: { value: '90' } }); - expect(input.value).toBe('1:30'); + fireEvent.change(input, { target: { value: '9:30' } }); + expect(input.value).toBe('9:30'); }); diff --git a/src/lib/time-utils.js b/src/lib/time-utils.js index ca37021..668edbe 100644 --- a/src/lib/time-utils.js +++ b/src/lib/time-utils.js @@ -33,39 +33,43 @@ export function formatTime(elapsedSeconds) { /** * Formats user input into MM:SS time format - * Removes non-numeric characters and adds colon separator - * Handles conversion when seconds >= 60 + * Plain numbers are treated as minutes (e.g., "5" = "5:00") + * Colons can be used for MM:SS format (e.g., "5:30" = "5:30") * @param {string} inputTime - The raw user input * @returns {string} Formatted time as MM:SS */ export function formatInputTime(inputTime) { if (!inputTime) return '0:00'; + // If input already contains a colon, treat as MM:SS format + if (inputTime.includes(':')) { + const parts = inputTime.split(':'); + const minutes = parts[0].replace(/\D/g, '') || '0'; + const seconds = parts[1] ? parts[1].replace(/\D/g, '') : '0'; + + const mins = parseInt(minutes, 10); + const secs = parseInt(seconds, 10); + + // Clamp seconds to 0-59 + const displaySeconds = Math.min(secs, 59); + const displaySecondsPadded = + displaySeconds < 10 ? `0${displaySeconds}` : `${displaySeconds}`; + + return `${mins}:${displaySecondsPadded}`; + } + // Remove all non-numeric characters const numericOnly = inputTime.replace(/\D/g, ''); // Remove leading zeros const numberString = numericOnly.replace(/^0+/, '') || '0'; - if (numberString.length === 1) { - return `0:0${numberString}`; - } - if (numberString.length === 2) { - // Check if this represents seconds >= 60, convert to minutes - const seconds = parseInt(numberString, 10); - if (seconds >= 60) { - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; - const displaySeconds = - remainingSeconds < 10 ? `0${remainingSeconds}` : `${remainingSeconds}`; - return `${minutes}:${displaySeconds}`; - } - return `0:${numberString}`; + if (numberString.length === 0) { + return '0:00'; } - // Insert colon before the last 2 digits (seconds) - const timeArray = numberString.split(''); - timeArray.splice(timeArray.length - 2, 0, ':'); - return timeArray.join(''); + // Treat plain numbers as minutes (not seconds) + const minutes = parseInt(numberString, 10); + return `${minutes}:00`; } /** diff --git a/src/lib/time-utils.test.js b/src/lib/time-utils.test.js index 8a4eb2c..53e16b8 100644 --- a/src/lib/time-utils.test.js +++ b/src/lib/time-utils.test.js @@ -21,12 +21,12 @@ it('formats time', () => { it('formats input time', () => { expect(formatInputTime()).toBe('0:00'); - expect(formatInputTime('3')).toBe('0:03'); - expect(formatInputTime('30')).toBe('0:30'); - expect(formatInputTime('130')).toBe('1:30'); + expect(formatInputTime('3')).toBe('3:00'); + expect(formatInputTime('30')).toBe('30:00'); + expect(formatInputTime('130')).toBe('130:00'); expect(formatInputTime('1:30')).toBe('1:30'); - expect(formatInputTime('y15efg')).toBe('0:15'); - expect(formatInputTime('y300efg')).toBe('3:00'); + expect(formatInputTime('y15efg')).toBe('15:00'); + expect(formatInputTime('y3:45efg')).toBe('3:45'); }); it('converts minutes to seconds', () => { From e02525b59b74505b264a3ee8bb38e2901874975b Mon Sep 17 00:00:00 2001 From: Mimi Flynn <414934+mimiflynn@users.noreply.github.com> Date: Sat, 17 Jan 2026 15:41:39 -0500 Subject: [PATCH 4/4] fix time input better --- src/components/time-input.jsx | 30 +++++++++++----- src/components/time-input.test.js | 25 +++++++++----- src/lib/time-utils.js | 57 +++++++++++++++++++------------ src/lib/time-utils.test.js | 13 +++---- 4 files changed, 80 insertions(+), 45 deletions(-) diff --git a/src/components/time-input.jsx b/src/components/time-input.jsx index 5d752ba..80d177f 100644 --- a/src/components/time-input.jsx +++ b/src/components/time-input.jsx @@ -12,14 +12,26 @@ export const TimeInput = ({ onChange, }) => { const [value, setValue] = useState(formatTime(placeholderSec)); - const handleChange = useCallback( - ({ target }) => { - const formattedValue = formatInputTime(target.value); - setValue(formattedValue); - onChange(convertToSeconds(formattedValue)); - }, - [onChange], - ); + const [isEditing, setIsEditing] = useState(false); + + const handleChange = useCallback(({ target }) => { + // While editing, just store the raw value (digits only) + setValue(target.value); + }, []); + + const handleFocus = useCallback(() => { + setIsEditing(true); + // Clear the formatted value to let user type fresh + setValue(''); + }, []); + + const handleBlur = useCallback(() => { + setIsEditing(false); + // Format the value when done editing + const formattedValue = formatInputTime(value); + setValue(formattedValue); + onChange(convertToSeconds(formattedValue)); + }, [value, onChange]); return ( ); diff --git a/src/components/time-input.test.js b/src/components/time-input.test.js index 79e8054..7ad3d4a 100644 --- a/src/components/time-input.test.js +++ b/src/components/time-input.test.js @@ -7,7 +7,7 @@ const setup = () => { console.log('change', event)} value={90} >, @@ -21,18 +21,25 @@ const setup = () => { test('Input should set display value', () => { const { input } = setup(); + + // Focus, type, then blur to trigger formatting + fireEvent.focus(input); fireEvent.change(input, { target: { value: '23' } }); - expect(input.value).toBe('23:00'); + fireEvent.blur(input); + expect(input.value).toBe('0:23'); + fireEvent.focus(input); fireEvent.change(input, { target: { value: 'y15efg' } }); - expect(input.value).toBe('15:00'); + fireEvent.blur(input); + expect(input.value).toBe('0:15'); - fireEvent.change(input, { target: { value: '8' } }); - fireEvent.change(input, { target: { value: '80' } }); - fireEvent.change(input, { target: { value: '8:00' } }); + fireEvent.focus(input); + fireEvent.change(input, { target: { value: '800' } }); + fireEvent.blur(input); expect(input.value).toBe('8:00'); - fireEvent.change(input, { target: { value: '9' } }); - fireEvent.change(input, { target: { value: '9:30' } }); - expect(input.value).toBe('9:30'); + fireEvent.focus(input); + fireEvent.change(input, { target: { value: '90' } }); + fireEvent.blur(input); + expect(input.value).toBe('1:30'); }); diff --git a/src/lib/time-utils.js b/src/lib/time-utils.js index 668edbe..63dd4cf 100644 --- a/src/lib/time-utils.js +++ b/src/lib/time-utils.js @@ -33,31 +33,14 @@ export function formatTime(elapsedSeconds) { /** * Formats user input into MM:SS time format - * Plain numbers are treated as minutes (e.g., "5" = "5:00") - * Colons can be used for MM:SS format (e.g., "5:30" = "5:30") + * Last 2 digits are treated as seconds, remaining digits as minutes + * e.g., "130" = "1:30", "800" = "8:00", "90" = "1:30" * @param {string} inputTime - The raw user input * @returns {string} Formatted time as MM:SS */ export function formatInputTime(inputTime) { if (!inputTime) return '0:00'; - // If input already contains a colon, treat as MM:SS format - if (inputTime.includes(':')) { - const parts = inputTime.split(':'); - const minutes = parts[0].replace(/\D/g, '') || '0'; - const seconds = parts[1] ? parts[1].replace(/\D/g, '') : '0'; - - const mins = parseInt(minutes, 10); - const secs = parseInt(seconds, 10); - - // Clamp seconds to 0-59 - const displaySeconds = Math.min(secs, 59); - const displaySecondsPadded = - displaySeconds < 10 ? `0${displaySeconds}` : `${displaySeconds}`; - - return `${mins}:${displaySecondsPadded}`; - } - // Remove all non-numeric characters const numericOnly = inputTime.replace(/\D/g, ''); // Remove leading zeros @@ -67,9 +50,39 @@ export function formatInputTime(inputTime) { return '0:00'; } - // Treat plain numbers as minutes (not seconds) - const minutes = parseInt(numberString, 10); - return `${minutes}:00`; + if (numberString.length === 1) { + // Single digit: treat as seconds + return `0:0${numberString}`; + } + + if (numberString.length === 2) { + // Two digits: treat as seconds, convert if >= 60 + const seconds = parseInt(numberString, 10); + if (seconds >= 60) { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + const displaySeconds = + remainingSeconds < 10 ? `0${remainingSeconds}` : `${remainingSeconds}`; + return `${minutes}:${displaySeconds}`; + } + return `0:${numberString}`; + } + + // 3+ digits: last 2 are seconds, rest are minutes + const secondsPart = numberString.slice(-2); + const minutesPart = numberString.slice(0, -2); + + let minutes = parseInt(minutesPart, 10); + let seconds = parseInt(secondsPart, 10); + + // If seconds >= 60, convert to minutes + if (seconds >= 60) { + minutes += Math.floor(seconds / 60); + seconds = seconds % 60; + } + + const displaySeconds = seconds < 10 ? `0${seconds}` : `${seconds}`; + return `${minutes}:${displaySeconds}`; } /** diff --git a/src/lib/time-utils.test.js b/src/lib/time-utils.test.js index 53e16b8..1569fd7 100644 --- a/src/lib/time-utils.test.js +++ b/src/lib/time-utils.test.js @@ -21,12 +21,13 @@ it('formats time', () => { it('formats input time', () => { expect(formatInputTime()).toBe('0:00'); - expect(formatInputTime('3')).toBe('3:00'); - expect(formatInputTime('30')).toBe('30:00'); - expect(formatInputTime('130')).toBe('130:00'); - expect(formatInputTime('1:30')).toBe('1:30'); - expect(formatInputTime('y15efg')).toBe('15:00'); - expect(formatInputTime('y3:45efg')).toBe('3:45'); + expect(formatInputTime('3')).toBe('0:03'); + expect(formatInputTime('30')).toBe('0:30'); + expect(formatInputTime('130')).toBe('1:30'); + expect(formatInputTime('800')).toBe('8:00'); + expect(formatInputTime('90')).toBe('1:30'); + expect(formatInputTime('y15efg')).toBe('0:15'); + expect(formatInputTime('y300efg')).toBe('3:00'); }); it('converts minutes to seconds', () => {