/** * Work Break Timer Logic */ // Constants const DEFAULT_TIME_MINUTES = 22; const MAX_TIME_MINUTES = 180; const SECONDS_PER_MINUTE = 60; // State let timeLeft = DEFAULT_TIME_MINUTES * SECONDS_PER_MINUTE; // in seconds let isRunning = false; let intervalId: number | null = null; let audioContext: AudioContext | null = null; // DOM Elements const timerDisplay = document.getElementById('timerDisplay') as HTMLElement; const startBtn = document.getElementById('startBtn') as HTMLButtonElement; const resetBtn = document.getElementById('resetBtn') as HTMLButtonElement; const adjustButtons = document.querySelectorAll('.adjust-btn'); /** * Formats seconds into MM:SS string */ function formatTime(seconds: number): string { const m = Math.floor(seconds / 60); const s = seconds % 60; return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; } /** * Updates the timer display */ function updateDisplay(): void { timerDisplay.textContent = formatTime(timeLeft); // Update document title document.title = `${formatTime(timeLeft)} - Work Timer`; } /** * Plays a simple beep sound using AudioContext */ function playAlarm(): void { if (!audioContext) { audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); } // Create oscillator const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.type = 'sine'; oscillator.frequency.setValueAtTime(880, audioContext.currentTime); // A5 oscillator.frequency.exponentialRampToValueAtTime(440, audioContext.currentTime + 0.5); // Drop to A4 gainNode.gain.setValueAtTime(0.5, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.start(); oscillator.stop(audioContext.currentTime + 0.5); } /** * Handles the timer tick */ function onTick(): void { if (timeLeft > 0) { timeLeft--; updateDisplay(); } else { // Time is up pauseTimer(); timerDisplay.classList.add('time-up'); playAlarm(); // Play alarm 3 times setTimeout(playAlarm, 1000); setTimeout(playAlarm, 2000); } } /** * Starts or resumes the timer */ function startTimer(): void { if (timeLeft === 0) { // If time is 0, reset to default before starting? // Or just do nothing? Requirement says: "Start from currently set time". // If currently set is 0, we can't really start. // Let's assume if 0, we reset to default for convenience, or just return. // Let's just return to be safe, or maybe the user wants to add time first. if (timeLeft === 0) return; } if (!isRunning) { isRunning = true; startBtn.textContent = 'Pause'; timerDisplay.classList.remove('time-up'); // Use window.setInterval to avoid TypeScript confusion with NodeJS.Timeout intervalId = window.setInterval(onTick, 1000); } else { // Pause pauseTimer(); } } /** * Pauses the timer */ function pauseTimer(): void { isRunning = false; startBtn.textContent = 'Resume'; if (intervalId !== null) { clearInterval(intervalId); intervalId = null; } } /** * Resets the timer */ function resetTimer(): void { pauseTimer(); timeLeft = DEFAULT_TIME_MINUTES * SECONDS_PER_MINUTE; startBtn.textContent = 'Start'; timerDisplay.classList.remove('time-up'); updateDisplay(); document.title = 'Work Break Timer'; } /** * Adjusts the time by adding minutes */ function adjustTime(minutes: number): void { const newTime = timeLeft + (minutes * SECONDS_PER_MINUTE); const maxSeconds = MAX_TIME_MINUTES * SECONDS_PER_MINUTE; if (newTime <= maxSeconds) { timeLeft = newTime; updateDisplay(); // If timer finished and we add time, remove the alarm style if (timeLeft > 0) { timerDisplay.classList.remove('time-up'); } } else { // Cap at max timeLeft = maxSeconds; updateDisplay(); } } // Event Listeners startBtn.addEventListener('click', startTimer); resetBtn.addEventListener('click', resetTimer); adjustButtons.forEach(btn => { btn.addEventListener('click', () => { const minutes = parseInt(btn.getAttribute('data-minutes') || '0', 10); adjustTime(minutes); }); }); // Initialize updateDisplay();