First commit
This commit is contained in:
commit
43fed81a62
438
app.js
Normal file
438
app.js
Normal file
@ -0,0 +1,438 @@
|
||||
// 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;
|
||||
130
index.html
Normal file
130
index.html
Normal file
@ -0,0 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pour-Over Coffee Timer</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- Welcome Screen -->
|
||||
<div id="welcome-screen" class="screen active">
|
||||
<div class="coffee-header">
|
||||
<div class="coffee-icon">☕</div>
|
||||
<h1>Pour-Over Coffee Timer</h1>
|
||||
<p class="subtitle">Barista-Recommended Recipe</p>
|
||||
</div>
|
||||
|
||||
<div class="recipe-overview card">
|
||||
<div class="recipe-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-label">Coffee</span>
|
||||
<span class="stat-value">21g</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Water</span>
|
||||
<span class="stat-value">340g</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Ratio</span>
|
||||
<span class="stat-value">1:16</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Time</span>
|
||||
<span class="stat-value">3:30-4:00</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recipe-details">
|
||||
<p><strong>Water Temperature:</strong> 94-96°C</p>
|
||||
<p><strong>Grind Size:</strong> Medium-fine</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn--primary btn--lg btn--full-width" onclick="startBrewing()">
|
||||
Start Brewing
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Brewing Screen -->
|
||||
<div id="brewing-screen" class="screen">
|
||||
<div class="brewing-header">
|
||||
<div class="step-indicator">
|
||||
<span id="current-step">Step 1</span> of 5
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progress-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step-content">
|
||||
<div class="step-icon" id="step-icon">🔧</div>
|
||||
<h2 id="step-title">Preparation</h2>
|
||||
<p id="step-description">Set up your equipment and ingredients</p>
|
||||
|
||||
<div class="water-indicator" id="water-indicator">
|
||||
<span class="water-label">Water Amount</span>
|
||||
<span class="water-amount" id="water-amount">0g</span>
|
||||
</div>
|
||||
|
||||
<ul class="step-details" id="step-details">
|
||||
<li>Grind 21g coffee to medium-fine consistency</li>
|
||||
<li>Heat 340g filtered water to 94-96°C</li>
|
||||
<li>Place metal filter in Chemex</li>
|
||||
<li>Zero your scale with Chemex on it</li>
|
||||
</ul>
|
||||
|
||||
<div class="timer-section" id="timer-section" style="display: none;">
|
||||
<div class="timer-display" id="timer-display">00:45</div>
|
||||
<div class="timer-controls">
|
||||
<button class="btn btn--outline timer-btn" id="play-pause-btn" onclick="toggleTimer()">
|
||||
<span id="play-pause-text">▶</span>
|
||||
</button>
|
||||
<button class="btn btn--outline timer-btn" onclick="resetTimer()">
|
||||
<span>↻</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn--primary btn--lg btn--full-width" id="next-step-btn" onclick="nextStep()">
|
||||
Ready
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Completion Screen -->
|
||||
<div id="completion-screen" class="screen">
|
||||
<div class="completion-content">
|
||||
<div class="completion-icon">🎉</div>
|
||||
<h1>Perfect Brew!</h1>
|
||||
<p class="completion-message">Your pour-over coffee is ready to enjoy.</p>
|
||||
|
||||
<div class="completion-stats card">
|
||||
<h3>Brewing Complete</h3>
|
||||
<p>You've successfully completed the barista-recommended pour-over process.</p>
|
||||
<div class="completion-tips">
|
||||
<h4>Pro Tips:</h4>
|
||||
<ul>
|
||||
<li>Give your brew a gentle swirl before serving</li>
|
||||
<li>Serve immediately for best flavor</li>
|
||||
<li>Clean your equipment while the coffee is hot</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="completion-actions">
|
||||
<button class="btn btn--primary btn--lg" onclick="restartBrewing()">
|
||||
Brew Another
|
||||
</button>
|
||||
<button class="btn btn--outline btn--lg" onclick="goToWelcome()">
|
||||
Back to Recipe
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user