Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 55 additions & 60 deletions packages/core/src/bundle/hooks/useStopwatch/useStopwatch.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { useEffect, useState } from 'react';
const getStopwatchTime = (time) => {
if (!time)
import { useInterval } from '../useInterval/useInterval';
const getStopwatchTime = (count) => {
if (!count)
return {
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
milliseconds: 0,
count: 0
};
const days = Math.floor(time / 86400);
const hours = Math.floor((time % 86400) / 3600);
const minutes = Math.floor((time % 3600) / 60);
const seconds = Math.floor(time % 60);
return { days, hours, minutes, seconds, count: time };
const totalSeconds = Math.floor(count / 1000);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = Math.floor(totalSeconds % 60);
const milliseconds = count % 1000;
return { days, hours, minutes, seconds, milliseconds, count };
};
const getMillsDiffOrZero = (ms) => {
const diff = Date.now() - ms;
return diff > 0 ? diff : 0;
};
/**
* @name useStopwatch
Expand All @@ -21,74 +29,61 @@ const getStopwatchTime = (time) => {
*
* @overload
* @param {number} [initialTime=0] The initial time of the timer
* @param {boolean} [options.enabled=true] The enabled state of the timer
* @param {boolean} [options.immediately=false] The enabled state of the timer
* @param {number} [options.updateInterval=1000] The update interval of the timer
* @returns {UseStopwatchReturn} An object containing the current time and functions to interact with the timer
*
* @example
* const { seconds, minutes, start, pause, reset } = useStopwatch(1000, { enabled: false });
* const { milliseconds, seconds, minutes, start, pause, reset } = useStopwatch(1000, { immediately: false, updateInterval: 1000 });
*
* @overload
* @param {number} [options.initialTime=0] -The initial time of the timer
* @param {boolean} [options.enabled=true] The enabled state of the timer
* @param {boolean} [options.immediately=true] The enabled state of the timer
* @param {number} [options.updateInterval=1000] The update interval of the timer
* @returns {UseStopwatchReturn} An object containing the current time and functions to interact with the timer
*
* @example
* const { seconds, minutes, start, pause, reset } = useStopwatch({ initialTime: 1000, enabled: false });
* const { milliseconds, seconds, minutes, start, pause, reset } = useStopwatch({ initialTime: 1000, immediately: false, updateInterval: 1000 });
*/
export const useStopwatch = (...params) => {
const initialTime = (typeof params[0] === 'number' ? params[0] : params[0]?.initialTime) ?? 0;
const options = typeof params[0] === 'number' ? params[1] : params[0];
const immediately = options?.immediately ?? false;
const [time, setTime] = useState(getStopwatchTime(initialTime));
const [paused, setPaused] = useState(!immediately && !initialTime);
const updateInterval = options?.updateInterval ?? 1000;
const [milliseconds, setMilliseconds] = useState(initialTime);
const [timestamp, setTimestamp] = useState(Date.now() - initialTime);
useEffect(() => {
if (paused) return;
const onInterval = () => {
setTime((prevTime) => {
const updatedCount = prevTime.count + 1;
if (updatedCount % 60 === 0) {
return {
...prevTime,
minutes: prevTime.minutes + 1,
seconds: 0,
count: updatedCount
};
}
if (updatedCount % (60 * 60) === 0) {
return {
...prevTime,
hours: prevTime.hours + 1,
minutes: 0,
seconds: 0,
count: updatedCount
};
}
if (updatedCount % (60 * 60 * 24) === 0) {
return {
...prevTime,
days: prevTime.days + 1,
hours: 0,
minutes: 0,
seconds: 0,
count: updatedCount
};
}
return {
...prevTime,
seconds: prevTime.seconds + 1,
count: updatedCount
};
});
};
const interval = setInterval(() => onInterval(), 1000);
return () => clearInterval(interval);
}, [paused]);
setMilliseconds(initialTime);
setTimestamp(Date.now() - initialTime);
}, [initialTime]);
const interval = useInterval(
() => setMilliseconds(getMillsDiffOrZero(timestamp)),
updateInterval,
{
immediately
}
);
const start = () => {
if (interval.active) return;
setTimestamp(Date.now() - milliseconds);
interval.resume();
};
const pause = () => {
if (!interval.active) return;
interval.pause();
setMilliseconds(getMillsDiffOrZero(timestamp));
};
const reset = () => {
setMilliseconds(initialTime);
setTimestamp(Date.now() - initialTime);
interval.resume();
};
return {
...time,
paused,
pause: () => setPaused(true),
start: () => setPaused(false),
reset: () => setTime(getStopwatchTime(initialTime)),
toggle: () => setPaused((prevPause) => !prevPause)
...getStopwatchTime(milliseconds),
paused: !interval.active,
pause,
start,
reset,
toggle: () => (interval.active ? pause() : start())
};
};
7 changes: 5 additions & 2 deletions packages/core/src/hooks/useStopwatch/useStopwatch.demo.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { useStopwatch } from '@siberiacancode/reactuse';

const Demo = () => {
const stopwatch = useStopwatch();
const stopwatch = useStopwatch({
updateInterval: 100
});

return (
<div>
<p>
<code>{stopwatch.minutes} m</code>:<code>{stopwatch.seconds} s</code>
<code>{stopwatch.minutes} m</code>:<code>{stopwatch.seconds} s</code>:
<code>{String(stopwatch.milliseconds).padStart(3, '0')} ms</code>
</p>
<button type='button' onClick={stopwatch.start}>
Start
Expand Down
14 changes: 7 additions & 7 deletions packages/core/src/hooks/useStopwatch/useStopwatch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,33 +38,33 @@ it('Should start counting when enabled', () => {

expect(result.current.seconds).toBe(1);
expect(result.current.minutes).toBe(0);
expect(result.current.count).toBe(1);
expect(result.current.count).toBe(1000);

act(() => vi.advanceTimersByTime(59000));

expect(result.current.seconds).toBe(0);
expect(result.current.minutes).toBe(1);
expect(result.current.count).toBe(60);
expect(result.current.count).toBe(60_000);
});

it('Should handle initial time correctly', () => {
const { result } = renderHook(() => useStopwatch(90_061));
const { result } = renderHook(() => useStopwatch(90_061_000));

expect(result.current.seconds).toBe(1);
expect(result.current.minutes).toBe(1);
expect(result.current.hours).toBe(1);
expect(result.current.days).toBe(1);
expect(result.current.count).toBe(90061);
expect(result.current.count).toBe(90_061_000);
});

it('Should handle initial time with options object', () => {
const { result } = renderHook(() => useStopwatch({ initialTime: 90_061 }));
const { result } = renderHook(() => useStopwatch({ initialTime: 90_061_000 }));

expect(result.current.seconds).toBe(1);
expect(result.current.minutes).toBe(1);
expect(result.current.hours).toBe(1);
expect(result.current.days).toBe(1);
expect(result.current.count).toBe(90061);
expect(result.current.count).toBe(90_061_000);
});

it('Should respect immediately option', () => {
Expand Down Expand Up @@ -124,7 +124,7 @@ it('Should toggle pause state', () => {
});

it('Should reset to initial time', () => {
const { result } = renderHook(() => useStopwatch(1));
const { result } = renderHook(() => useStopwatch(1000));

expect(result.current.seconds).toBe(1);

Expand Down
Loading