+ 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', () => {