Timer implemented. No working tests.
This commit is contained in:
120
src/hooks/useRestTimer.ts
Normal file
120
src/hooks/useRestTimer.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { playTimeUpSignal } from '../utils/audio';
|
||||
|
||||
export type TimerStatus = 'IDLE' | 'RUNNING' | 'PAUSED' | 'FINISHED';
|
||||
|
||||
interface UseRestTimerProps {
|
||||
defaultTime: number; // in seconds
|
||||
onFinish?: () => void;
|
||||
autoStart?: boolean;
|
||||
}
|
||||
|
||||
export const useRestTimer = ({ defaultTime, onFinish }: UseRestTimerProps) => {
|
||||
const [timeLeft, setTimeLeft] = useState(defaultTime);
|
||||
const [status, setStatus] = useState<TimerStatus>('IDLE');
|
||||
const [duration, setDuration] = useState(defaultTime); // The set duration to reset to
|
||||
|
||||
const endTimeRef = useRef<number | null>(null);
|
||||
const rafRef = useRef<number | null>(null);
|
||||
const prevDefaultTimeRef = useRef(defaultTime);
|
||||
|
||||
// Update internal duration when defaultTime changes
|
||||
useEffect(() => {
|
||||
if (prevDefaultTimeRef.current !== defaultTime) {
|
||||
prevDefaultTimeRef.current = defaultTime;
|
||||
setDuration(defaultTime);
|
||||
// Only update visible time if IDLE. If running, it will apply on next reset.
|
||||
if (status === 'IDLE') {
|
||||
setTimeLeft(defaultTime);
|
||||
}
|
||||
}
|
||||
}, [defaultTime, status]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const tick = useCallback(() => {
|
||||
if (!endTimeRef.current) return;
|
||||
const now = Date.now();
|
||||
const remaining = Math.max(0, Math.ceil((endTimeRef.current - now) / 1000));
|
||||
|
||||
setTimeLeft(remaining);
|
||||
|
||||
if (remaining <= 0) {
|
||||
setStatus('FINISHED');
|
||||
playTimeUpSignal();
|
||||
if (onFinish) onFinish();
|
||||
|
||||
// Auto-reset visuals after 3 seconds of "FINISHED" state?
|
||||
// Requirement says: "The FAB must first change color to red for 3 seconds, and then return to the idle state"
|
||||
// So the hook stays in FINISHED.
|
||||
setTimeout(() => {
|
||||
reset();
|
||||
}, 3000);
|
||||
} else {
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
}
|
||||
}, [onFinish]);
|
||||
|
||||
const start = useCallback(() => {
|
||||
if (status === 'RUNNING') return;
|
||||
|
||||
// If starting from IDLE or PAUSED
|
||||
const targetSeconds = status === 'PAUSED' ? timeLeft : duration;
|
||||
endTimeRef.current = Date.now() + targetSeconds * 1000;
|
||||
|
||||
setStatus('RUNNING');
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
}, [status, timeLeft, duration, tick]);
|
||||
|
||||
const pause = useCallback(() => {
|
||||
if (status !== 'RUNNING') return;
|
||||
setStatus('PAUSED');
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
endTimeRef.current = null;
|
||||
}, [status]);
|
||||
|
||||
const reset = useCallback((newDuration?: number) => {
|
||||
if (rafRef.current) cancelAnimationFrame(rafRef.current);
|
||||
|
||||
const nextDuration = newDuration !== undefined ? newDuration : duration;
|
||||
setDuration(nextDuration);
|
||||
setTimeLeft(nextDuration);
|
||||
setStatus('IDLE');
|
||||
endTimeRef.current = null;
|
||||
}, [duration]);
|
||||
|
||||
const addTime = useCallback((seconds: number) => {
|
||||
setDuration(prev => prev + seconds);
|
||||
if (status === 'IDLE') {
|
||||
setTimeLeft(prev => prev + seconds);
|
||||
} else if (status === 'RUNNING') {
|
||||
// Add to current target
|
||||
if (endTimeRef.current) {
|
||||
endTimeRef.current += seconds * 1000;
|
||||
// Force immediate update to avoid flicker
|
||||
const now = Date.now();
|
||||
setTimeLeft(Math.max(0, Math.ceil((endTimeRef.current - now) / 1000)));
|
||||
}
|
||||
} else if (status === 'PAUSED') {
|
||||
setTimeLeft(prev => prev + seconds);
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
|
||||
return {
|
||||
timeLeft,
|
||||
status,
|
||||
start,
|
||||
pause,
|
||||
reset,
|
||||
addTime,
|
||||
setDuration: (val: number) => {
|
||||
setDuration(val);
|
||||
if (status === 'IDLE') setTimeLeft(val);
|
||||
}
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user