First commit

This commit is contained in:
Greg 2025-07-20 10:32:21 +00:00
commit 43fed81a62
3 changed files with 1664 additions and 0 deletions

438
app.js Normal file
View 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
View 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>

1096
style.css Normal file

File diff suppressed because it is too large Load Diff