Coffee_Windsurf/script.js

317 lines
11 KiB
JavaScript

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