2025-07-20 10:32:21 +00:00

438 lines
12 KiB
JavaScript

// 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;