import { useRef, useMemo, useState, useEffect } from 'react'; import { IoPlay, IoPause, IoRefresh, IoTrashOutline, } from 'react-icons/io5/index'; import { ReverseTimer } from './reverse-timer'; import { useCountdownTimers } from '@/stores/countdown-timers'; import { useAlarm } from '@/hooks/use-alarm'; import { useSnackbar } from '@/contexts/snackbar'; import { padNumber } from '@/helpers/number'; import { cn } from '@/helpers/styles'; import styles from './timer.module.css'; interface TimerProps { id: string; } export function Timer({ id }: TimerProps) { const intervalRef = useRef | null>(null); const lastActiveTimeRef = useRef(null); const lastStateRef = useRef<{ spent: number; total: number } | null>(null); const [isRunning, setIsRunning] = useState(false); const { name, spent, total } = useCountdownTimers(state => state.getTimer(id), ) || { name: '', spent: 0, total: 0 }; const [isDeleting, setIsDeleting] = useState(false); const [snapshot, setSnapshot] = useState({ spent: 0, total: 0 }); const tick = useCountdownTimers(state => state.tick); const rename = useCountdownTimers(state => state.rename); const reset = useCountdownTimers(state => state.reset); const deleteTimer = useCountdownTimers(state => state.delete); const left = useMemo( () => (isDeleting ? snapshot.total - snapshot.spent : total - spent), [total, spent, isDeleting, snapshot], ); const hours = useMemo(() => Math.floor(left / 3600), [left]); const minutes = useMemo(() => Math.floor((left % 3600) / 60), [left]); const seconds = useMemo(() => left % 60, [left]); const playAlarm = useAlarm(); const showSnackbar = useSnackbar(); const handleStart = () => { if (left > 0) setIsRunning(true); }; const handlePause = () => setIsRunning(false); const handleToggle = () => { if (isRunning) handlePause(); else handleStart(); }; const handleReset = () => { if (spent === 0) return; if (isRunning) return showSnackbar('Please first stop the timer.'); setIsRunning(false); reset(id); }; const handleDelete = () => { if (isRunning) return showSnackbar('Please first stop the timer.'); setIsDeleting(true); setSnapshot({ spent, total }); deleteTimer(id); }; useEffect(() => { if (isRunning) { if (intervalRef.current) clearInterval(intervalRef.current); intervalRef.current = setInterval(() => tick(id), 1000); } return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [isRunning, tick, id]); useEffect(() => { if (left === 0 && isRunning) { setIsRunning(false); playAlarm(); if (intervalRef.current) clearInterval(intervalRef.current); } }, [left, isRunning, playAlarm]); useEffect(() => { const handleBlur = () => { if (isRunning) { lastActiveTimeRef.current = Date.now(); lastStateRef.current = { spent, total }; } }; const handleFocus = () => { if (isRunning && lastActiveTimeRef.current && lastStateRef.current) { const elapsed = Math.floor( (Date.now() - lastActiveTimeRef.current) / 1000, ); const previousLeft = lastStateRef.current.total - lastStateRef.current.spent; const currentLeft = left; const correctedLeft = previousLeft - elapsed; if (correctedLeft < currentLeft) { tick(id, currentLeft - correctedLeft); } lastActiveTimeRef.current = null; lastStateRef.current = null; } }; window.addEventListener('blur', handleBlur); window.addEventListener('focus', handleFocus); return () => { window.removeEventListener('blur', handleBlur); window.removeEventListener('focus', handleFocus); }; }, [isRunning, tick, id, spent, total, left]); return (
{padNumber(hours)} : {padNumber(minutes)} : {padNumber(seconds)}
); }