// Security utility functions function sanitizeText(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function validateElement(element) { return element && element.nodeType === Node.ELEMENT_NODE; } // Secure DOM manipulation function secureSetTextContent(element, text) { if (!validateElement(element)) { console.error('Invalid element provided'); return false; } element.textContent = sanitizeText(text); return true; } // Audio notification function function playTimerCompleteSound() { try { // Create audio context const audioContext = new (window.AudioContext || window.webkitAudioContext)(); // Create a pleasant chime sound using multiple tones const frequencies = [523.25, 659.25, 783.99]; // C5, E5, G5 - major chord const duration = 0.8; // seconds frequencies.forEach((freq, index) => { const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.frequency.setValueAtTime(freq, audioContext.currentTime); oscillator.type = 'sine'; // Create a gentle fade-in and fade-out const startTime = audioContext.currentTime + (index * 0.1); const endTime = startTime + duration; gainNode.gain.setValueAtTime(0, startTime); gainNode.gain.linearRampToValueAtTime(0.1, startTime + 0.1); gainNode.gain.exponentialRampToValueAtTime(0.01, endTime); oscillator.start(startTime); oscillator.stop(endTime); }); console.log('Timer complete sound played'); } catch (error) { console.log('Could not play sound:', error); // Fallback: try to use a simple beep try { const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const oscillator = audioContext.createOscillator(); const gainNode = audioContext.createGain(); oscillator.connect(gainNode); gainNode.connect(audioContext.destination); oscillator.frequency.setValueAtTime(800, audioContext.currentTime); oscillator.type = 'sine'; gainNode.gain.setValueAtTime(0.1, audioContext.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5); oscillator.start(); oscillator.stop(audioContext.currentTime + 0.5); } catch (fallbackError) { console.log('Audio not supported in this browser'); } } } document.addEventListener('DOMContentLoaded', () => { const setupCard = document.getElementById('setup'); const brewingCard = document.getElementById('brewing-process'); const startBtn = document.getElementById('start-btn'); const nextBtn = document.getElementById('next-btn'); const resetBtn = document.getElementById('reset-btn'); const pauseBtn = document.getElementById('pause-btn'); // Debug: Check if pause button exists if (!pauseBtn) { console.error('Pause button not found!'); } else { console.log('Pause button found successfully'); } const timerDisplay = document.getElementById('timer-display'); const totalWaterDisplay = document.getElementById('total-water'); const stepTitle = document.getElementById('step-title'); const stepInstruction = document.getElementById('step-instruction'); const progressRing = document.querySelector('.progress-ring__circle'); const radius = progressRing.r.baseVal.value; const circumference = 2 * Math.PI * radius; progressRing.style.strokeDasharray = `${circumference} ${circumference}`; progressRing.style.strokeDashoffset = circumference; const brewSteps = [ { title: 'Bloom', instruction: 'Pour 50g of water evenly over the coffee grounds. Wait for the coffee to degas.', duration: 45, // seconds water: 50, // grams }, { title: 'Second Pour', instruction: 'Slowly pour in circles until the total weight reaches 200g. Avoid pouring on the edges.', duration: 60, water: 200, }, { title: 'Final Pour', instruction: 'Continue pouring in slow circles until you reach the final weight of 340g.', duration: 60, water: 340, }, { title: 'Let it Drain', instruction: 'Allow all the water to drain through the coffee bed. This should take about 30-60 seconds.', duration: 60, water: 340, }, { title: 'Enjoy!', instruction: 'Your delicious Chemex coffee is ready. Remove the filter, swirl, and serve.', duration: 0, water: 340, } ]; let currentStepIndex = 0; let timerInterval; let secondsElapsed = 0; let totalSeconds = 0; // Step-specific timer states - each step has its own pause state let stepTimerStates = brewSteps.map(() => ({ isPaused: false, pausedTime: 0, secondsElapsed: 0, isCompleted: false })); function setProgress(percent) { const offset = circumference - (percent / 100) * circumference; progressRing.style.strokeDashoffset = offset; } function formatTime(seconds) { const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; } function startTimer() { const step = brewSteps[currentStepIndex]; const stepState = stepTimerStates[currentStepIndex]; totalSeconds = step.duration; // Use step-specific timer state if (!stepState.isPaused) { // Starting fresh or step not paused secondsElapsed = stepState.secondsElapsed; } else { // Resuming from pause secondsElapsed = stepState.pausedTime; stepState.isPaused = false; pauseBtn.classList.remove('paused'); secureSetTextContent(pauseBtn, 'Pause'); } updateTimerDisplay(); const percent = (secondsElapsed / totalSeconds) * 100; setProgress(percent); if (totalSeconds === 0) return; timerInterval = setInterval(() => { const currentStepState = stepTimerStates[currentStepIndex]; if (!currentStepState.isPaused) { secondsElapsed++; currentStepState.secondsElapsed = secondsElapsed; updateTimerDisplay(); const percent = (secondsElapsed / totalSeconds) * 100; setProgress(percent); if (secondsElapsed >= totalSeconds) { clearInterval(timerInterval); currentStepState.isCompleted = true; // Play pleasant sound when timer completes playTimerCompleteSound(); } } }, 1000); } function updateTimerDisplay() { secureSetTextContent(timerDisplay, formatTime(secondsElapsed)); } function updateUIForStep() { const step = brewSteps[currentStepIndex]; const stepState = stepTimerStates[currentStepIndex]; if (!step) { console.error('Invalid step index'); return; } secureSetTextContent(stepTitle, step.title); secureSetTextContent(stepInstruction, step.instruction); secureSetTextContent(totalWaterDisplay, `${step.water}g`); if (currentStepIndex === brewSteps.length - 1) { secureSetTextContent(nextBtn, 'Brew Again'); } else { secureSetTextContent(nextBtn, 'Next Step'); } // Reset pause button state for the new step if (stepState.isPaused) { pauseBtn.classList.add('paused'); secureSetTextContent(pauseBtn, 'Resume'); } else { pauseBtn.classList.remove('paused'); secureSetTextContent(pauseBtn, 'Pause'); } clearInterval(timerInterval); startTimer(); } startBtn.addEventListener('click', () => { setupCard.classList.remove('active'); brewingCard.style.display = 'block'; // A little trick to trigger the animation setTimeout(() => brewingCard.classList.add('active'), 10); updateUIForStep(); }); nextBtn.addEventListener('click', () => { // Bounds checking for security if (currentStepIndex < 0 || currentStepIndex >= brewSteps.length - 1) { if (currentStepIndex >= brewSteps.length - 1) { reset(); return; } } currentStepIndex = Math.max(0, Math.min(currentStepIndex + 1, brewSteps.length - 1)); updateUIForStep(); }); resetBtn.addEventListener('click', reset); // Pause/Resume functionality - now step-specific function togglePause() { const currentStepState = stepTimerStates[currentStepIndex]; console.log('Pause button clicked! Current step state:', currentStepState.isPaused); if (!currentStepState.isPaused) { // Pause the current step's timer currentStepState.isPaused = true; currentStepState.pausedTime = secondsElapsed; pauseBtn.classList.add('paused'); secureSetTextContent(pauseBtn, 'Resume'); console.log('Step', currentStepIndex, 'timer paused at:', currentStepState.pausedTime, 'seconds'); } else { // Resume the current step's timer currentStepState.isPaused = false; pauseBtn.classList.remove('paused'); secureSetTextContent(pauseBtn, 'Pause'); console.log('Step', currentStepIndex, 'timer resumed from:', currentStepState.pausedTime, 'seconds'); } } if (pauseBtn) { pauseBtn.addEventListener('click', togglePause); console.log('Pause button event listener added'); } else { console.error('Cannot add event listener - pause button not found'); } function reset() { clearInterval(timerInterval); currentStepIndex = 0; secondsElapsed = 0; totalSeconds = 0; // Reset all step-specific timer states stepTimerStates = brewSteps.map(() => ({ isPaused: false, pausedTime: 0, secondsElapsed: 0, isCompleted: false })); // Reset pause button state pauseBtn.classList.remove('paused'); secureSetTextContent(pauseBtn, 'Pause'); brewingCard.classList.remove('active'); setupCard.classList.add('active'); setTimeout(() => brewingCard.style.display = 'none', 500); // Match CSS transition updateTimerDisplay(); setProgress(0); } });