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
112 changes: 50 additions & 62 deletions packages/core/src/bundle/hooks/useStopwatch/useStopwatch.js
Original file line number Diff line number Diff line change
@@ -1,94 +1,82 @@
import { useEffect, useState } from 'react';
const getStopwatchTime = (time) => {
if (!time)
import { useState } from 'react';
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 = (millis) => (Date.now() - millis > 0 ? Date.now() - millis : 0);
/**
* @name useStopwatch
* @description - Hook that creates a stopwatch functionality
* @category 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);
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]);
const updateInterval = options?.updateInterval ?? 1000;
const [milliseconds, setMilliseconds] = useState(initialTime);
const [timestamp, setTimestamp] = useState(Date.now() - initialTime);
const interval = useInterval(
() => setMilliseconds(getMillsDiffOrZero(timestamp)),
updateInterval,
{
immediately
}
);
const start = () => {
if (interval.active) return;
setTimestamp(new Date().getTime() - milliseconds);
interval.resume();
};
const pause = () => {
if (!interval.active) return;
setMilliseconds(getMillsDiffOrZero(timestamp));
interval.pause();
};
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