// Application data from provided JSON const recipeData = { "recipe": { "name": "Barista-Recommended Pour-Over", "coffee_weight": "21g", "water_weight": "340g", "ratio": "1:16", "water_temp": "94-96°C", "grind": "Medium-fine", "total_time": "3:30-4:00" }, "steps": [ { "id": 1, "name": "Preparation", "description": "Set up your equipment and ingredients", "details": [ "Grind 21g coffee to medium-fine consistency", "Heat 340g filtered water to 94-96°C", "Place metal filter in Chemex", "Zero your scale with Chemex on it" ], "timer": 0, "water_amount": "0g", "icon": "🔧" }, { "id": 2, "name": "Bloom", "description": "Pour water to bloom the coffee grounds", "details": [ "Add ground coffee to filter", "Pour 50g water slowly over grounds", "Start from center, spiral outward", "Wait for CO2 to release" ], "timer": 45, "water_amount": "50g", "icon": "🌸" }, { "id": 3, "name": "Second Pour", "description": "Continue pouring to 200g total", "details": [ "Pour slowly in circular motion", "Avoid pouring near edges", "Bring total weight to 200g", "Maintain steady pour rate" ], "timer": 30, "water_amount": "200g total", "icon": "💧" }, { "id": 4, "name": "Final Pour", "description": "Complete the pour to 340g total", "details": [ "Continue circular pouring motion", "Pour remaining water to 340g total", "Keep consistent height and speed", "Finish all water by 2:30-3:00" ], "timer": 90, "water_amount": "340g total", "icon": "🫗" }, { "id": 5, "name": "Finish", "description": "Complete the brewing process", "details": [ "Let coffee finish dripping", "Remove filter when dripping stops", "Give brew a gentle swirl", "Serve immediately and enjoy!" ], "timer": 60, "water_amount": "Complete", "icon": "☕" } ] }; // Application state let currentStepIndex = 0; let timerInterval = null; let timeRemaining = 0; let isTimerRunning = false; let isTimerComplete = false; // DOM elements const welcomeScreen = document.getElementById('welcome-screen'); const brewingScreen = document.getElementById('brewing-screen'); const completionScreen = document.getElementById('completion-screen'); // Screen management function showScreen(screenName) { // Hide all screens document.querySelectorAll('.screen').forEach(screen => { screen.classList.remove('active'); }); // Show target screen const targetScreen = document.getElementById(screenName + '-screen'); if (targetScreen) { targetScreen.classList.add('active'); } } // Start brewing process function startBrewing() { currentStepIndex = 0; showScreen('brewing'); loadCurrentStep(); } // Load current step data function loadCurrentStep() { const step = recipeData.steps[currentStepIndex]; const stepNumber = currentStepIndex + 1; const totalSteps = recipeData.steps.length; // Update step indicator document.getElementById('current-step').textContent = `Step ${stepNumber}`; // Update progress bar const progressPercentage = (stepNumber / totalSteps) * 100; document.getElementById('progress-fill').style.width = `${progressPercentage}%`; // Update step content document.getElementById('step-icon').textContent = step.icon; document.getElementById('step-title').textContent = step.name; document.getElementById('step-description').textContent = step.description; document.getElementById('water-amount').textContent = step.water_amount; // Update step details const detailsList = document.getElementById('step-details'); detailsList.innerHTML = ''; step.details.forEach(detail => { const li = document.createElement('li'); li.textContent = detail; detailsList.appendChild(li); }); // Handle timer visibility and setup const timerSection = document.getElementById('timer-section'); const nextStepBtn = document.getElementById('next-step-btn'); if (step.timer > 0) { // Show timer for timed steps timerSection.style.display = 'block'; timeRemaining = step.timer; isTimerComplete = false; updateTimerDisplay(); nextStepBtn.textContent = 'Next Step'; nextStepBtn.style.display = 'none'; // Hide until timer completes // Reset timer controls resetTimerControls(); } else { // Hide timer for preparation step timerSection.style.display = 'none'; nextStepBtn.textContent = 'Ready'; nextStepBtn.style.display = 'block'; } // Update water indicator visibility const waterIndicator = document.getElementById('water-indicator'); if (step.water_amount !== '0g') { waterIndicator.style.display = 'flex'; } else { waterIndicator.style.display = 'none'; } } // Timer functionality function updateTimerDisplay() { const minutes = Math.floor(timeRemaining / 60); const seconds = timeRemaining % 60; const display = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; document.getElementById('timer-display').textContent = display; } function toggleTimer() { if (isTimerRunning) { pauseTimer(); } else { startTimer(); } } function startTimer() { if (timeRemaining <= 0 && !isTimerComplete) { timeRemaining = recipeData.steps[currentStepIndex].timer; } isTimerRunning = true; document.getElementById('play-pause-text').textContent = '⏸'; timerInterval = setInterval(() => { timeRemaining--; updateTimerDisplay(); if (timeRemaining <= 0) { completeTimer(); } }, 1000); } function pauseTimer() { isTimerRunning = false; document.getElementById('play-pause-text').textContent = '▶'; clearInterval(timerInterval); } function resetTimer() { pauseTimer(); timeRemaining = recipeData.steps[currentStepIndex].timer; isTimerComplete = false; updateTimerDisplay(); resetTimerControls(); // Hide next step button until timer completes again const nextStepBtn = document.getElementById('next-step-btn'); nextStepBtn.style.display = 'none'; } function completeTimer() { pauseTimer(); isTimerComplete = true; // Visual feedback const timerDisplay = document.getElementById('timer-display'); timerDisplay.classList.add('timer-complete'); // Audio notification playNotificationSound(); // Show next step button const nextStepBtn = document.getElementById('next-step-btn'); nextStepBtn.style.display = 'block'; nextStepBtn.textContent = 'Next Step'; // Remove visual feedback after animation setTimeout(() => { timerDisplay.classList.remove('timer-complete'); }, 3000); // Auto-advance after a short delay setTimeout(() => { if (currentStepIndex < recipeData.steps.length - 1) { nextStep(); } }, 2000); } function resetTimerControls() { document.getElementById('play-pause-text').textContent = '▶'; const nextStepBtn = document.getElementById('next-step-btn'); if (recipeData.steps[currentStepIndex].timer > 0) { nextStepBtn.style.display = 'none'; } } // Audio notification function playNotificationSound() { // Create a simple beep sound using Web Audio API 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); gainNode.gain.setValueAtTime(0, audioContext.currentTime); gainNode.gain.linearRampToValueAtTime(0.3, audioContext.currentTime + 0.1); gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + 0.3); oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.3); } catch (error) { // Fallback: show visual notification if audio fails console.log('Timer complete!'); } } // Step navigation function nextStep() { // If current step has a timer and it's not complete, start the timer const currentStep = recipeData.steps[currentStepIndex]; if (currentStep.timer > 0 && !isTimerComplete && !isTimerRunning) { startTimer(); return; } // Move to next step currentStepIndex++; if (currentStepIndex >= recipeData.steps.length) { // Brewing complete showScreen('completion'); } else { // Load next step loadCurrentStep(); } } // Completion screen functions function restartBrewing() { currentStepIndex = 0; pauseTimer(); timeRemaining = 0; isTimerComplete = false; showScreen('brewing'); loadCurrentStep(); } function goToWelcome() { currentStepIndex = 0; pauseTimer(); timeRemaining = 0; isTimerComplete = false; showScreen('welcome'); } // Keyboard shortcuts document.addEventListener('keydown', (event) => { if (event.code === 'Space') { event.preventDefault(); const currentScreen = document.querySelector('.screen.active'); if (currentScreen && currentScreen.id === 'brewing-screen') { const currentStep = recipeData.steps[currentStepIndex]; if (currentStep.timer > 0) { toggleTimer(); } } } if (event.key === 'Enter') { const currentScreen = document.querySelector('.screen.active'); if (currentScreen && currentScreen.id === 'welcome-screen') { startBrewing(); } else if (currentScreen && currentScreen.id === 'brewing-screen') { nextStep(); } } if (event.key === 'Escape') { const currentScreen = document.querySelector('.screen.active'); if (currentScreen && currentScreen.id !== 'welcome-screen') { goToWelcome(); } } }); // Initialize app document.addEventListener('DOMContentLoaded', () => { // Show welcome screen by default showScreen('welcome'); // Add some helpful console instructions console.log('☕ Pour-Over Coffee Timer'); console.log('Keyboard shortcuts:'); console.log('- Space: Start/pause timer'); console.log('- Enter: Start brewing or next step'); console.log('- Escape: Return to welcome screen'); }); // Prevent accidental page refresh during brewing window.addEventListener('beforeunload', (event) => { const currentScreen = document.querySelector('.screen.active'); if (currentScreen && currentScreen.id === 'brewing-screen') { event.preventDefault(); event.returnValue = 'Are you sure you want to leave? Your brewing progress will be lost.'; return event.returnValue; } }); // Handle visibility change (when user switches tabs) document.addEventListener('visibilitychange', () => { if (document.hidden && isTimerRunning) { // Optionally pause timer when tab is not visible // pauseTimer(); } }); // Responsive handling for timer display function updateTimerSize() { const timerDisplay = document.getElementById('timer-display'); const containerWidth = timerDisplay.parentElement.offsetWidth; if (containerWidth < 400) { timerDisplay.style.fontSize = '3rem'; } else { timerDisplay.style.fontSize = '4rem'; } } // Call on resize window.addEventListener('resize', updateTimerSize); // Accessibility enhancements function announceStepChange() { const step = recipeData.steps[currentStepIndex]; const announcement = `Step ${currentStepIndex + 1}: ${step.name}. ${step.description}`; // Create and use aria-live region for screen reader announcements let liveRegion = document.getElementById('aria-live-region'); if (!liveRegion) { liveRegion = document.createElement('div'); liveRegion.id = 'aria-live-region'; liveRegion.setAttribute('aria-live', 'polite'); liveRegion.setAttribute('aria-atomic', 'true'); liveRegion.style.position = 'absolute'; liveRegion.style.left = '-10000px'; liveRegion.style.width = '1px'; liveRegion.style.height = '1px'; liveRegion.style.overflow = 'hidden'; document.body.appendChild(liveRegion); } liveRegion.textContent = announcement; } // Export functions for global access (for onclick handlers) window.startBrewing = startBrewing; window.nextStep = nextStep; window.toggleTimer = toggleTimer; window.resetTimer = resetTimer; window.restartBrewing = restartBrewing; window.goToWelcome = goToWelcome;