recipe-manager/public/app.js

813 lines
27 KiB
JavaScript

// GourmetVault client application state
let allRecipes = [];
let activeRecipe = null;
let currentMultiplier = 1;
let selectedCoverImage = '';
let stopwatchInterval = null;
let stopwatchSeconds = 0;
let cookingActiveStep = 0;
// Page elements caching
const elements = {
themeToggle: document.getElementById('theme-toggle'),
urlScrapeForm: document.getElementById('url-scrape-form'),
scrapeUrlInput: document.getElementById('scrape-url-input'),
btnScrapeSubmit: document.getElementById('btn-scrape-submit'),
btnManualAdd: document.getElementById('btn-manual-add'),
recipesGrid: document.getElementById('recipes-grid'),
emptyState: document.getElementById('empty-state'),
librarySearch: document.getElementById('library-search'),
// Scrape / Edit Modal
saveModal: document.getElementById('save-modal'),
recipeEditForm: document.getElementById('recipe-edit-form'),
editRecipeId: document.getElementById('edit-recipe-id'),
editTitle: document.getElementById('edit-title'),
editSource: document.getElementById('edit-source'),
editDescription: document.getElementById('edit-description'),
editServings: document.getElementById('edit-servings'),
editPrepTime: document.getElementById('edit-prep-time'),
editCookTime: document.getElementById('edit-cook-time'),
imageSelectionGrid: document.getElementById('image-selection-grid'),
editImageUrl: document.getElementById('edit-image-url'),
editIngredients: document.getElementById('edit-ingredients'),
editInstructions: document.getElementById('edit-instructions'),
modalActionTitle: document.getElementById('modal-action-title'),
btnSaveConfirm: document.getElementById('btn-save-confirm'),
// Detail Modal
detailModal: document.getElementById('detail-modal'),
recipeDetailBanner: document.getElementById('recipe-detail-banner'),
btnDetailEdit: document.getElementById('btn-detail-edit'),
btnDetailDelete: document.getElementById('btn-detail-delete'),
recipeDetailTitle: document.getElementById('recipe-detail-title'),
tagServings: document.getElementById('tag-servings'),
tagPrep: document.getElementById('tag-prep'),
tagCook: document.getElementById('tag-cook'),
recipeDetailDescription: document.getElementById('recipe-detail-description'),
recipeDetailIngredients: document.getElementById('recipe-detail-ingredients'),
recipeDetailInstructions: document.getElementById('recipe-detail-instructions'),
scalerMinus: document.getElementById('scaler-minus'),
scalerPlus: document.getElementById('scaler-plus'),
currentServingsVal: document.getElementById('current-servings-val'),
btnTriggerCookmode: document.getElementById('btn-trigger-cookmode'),
// Cooking Mode Modal
cookingModeModal: document.getElementById('cooking-mode-modal'),
cookRecipeTitle: document.getElementById('cook-recipe-title'),
cookTimerVal: document.getElementById('cook-timer-val'),
cookTimerPlay: document.getElementById('cook-timer-play'),
cookTimerReset: document.getElementById('cook-timer-reset'),
cookProgressBar: document.getElementById('cook-progress-bar'),
cookStepIndex: document.getElementById('cook-step-index'),
cookStepInstruction: document.getElementById('cook-step-instruction'),
cookBtnPrev: document.getElementById('cook-btn-prev'),
cookBtnNext: document.getElementById('cook-btn-next'),
cookStepDots: document.getElementById('cook-step-dots'),
// Loader
loadingOverlay: document.getElementById('loading-overlay'),
loaderStepFetch: document.getElementById('loader-step-fetch'),
loaderStepParse: document.getElementById('loader-step-parse'),
loaderStepImages: document.getElementById('loader-step-images')
};
// -------------------------------------------------------------
// LIFE CYCLE AND REGISTRATION
// -------------------------------------------------------------
window.addEventListener('DOMContentLoaded', () => {
initTheme();
fetchRecipes();
registerEvents();
});
function registerEvents() {
// Theme Toggle
elements.themeToggle.addEventListener('click', toggleTheme);
// URL Form Submission
elements.urlScrapeForm.addEventListener('submit', handleScrapeSubmit);
// Manual Add Form Opening
elements.btnManualAdd.addEventListener('click', openManualAdd);
// Library Search
elements.librarySearch.addEventListener('input', filterRecipes);
// Recipe Form Save
elements.recipeEditForm.addEventListener('submit', handleRecipeSave);
// Action Buttons Detail Modal
elements.btnDetailEdit.addEventListener('click', editActiveRecipe);
elements.btnDetailDelete.addEventListener('click', deleteActiveRecipe);
// Servings Scaler Buttons
elements.scalerMinus.addEventListener('click', () => adjustMultiplier(-0.5));
elements.scalerPlus.addEventListener('click', () => adjustMultiplier(0.5));
// Cooking Mode Trigger
elements.btnTriggerCookmode.addEventListener('click', enterCookingMode);
// Cooking Controls
elements.cookBtnPrev.addEventListener('click', prevCookingStep);
elements.cookBtnNext.addEventListener('click', nextCookingStep);
elements.cookTimerPlay.addEventListener('click', toggleTimer);
elements.cookTimerReset.addEventListener('click', resetTimer);
// Global Modals click to dismiss on overlay
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', (e) => {
if (e.target === overlay && !overlay.classList.contains('loading-overlay')) {
closeModal(overlay.id);
}
});
});
// Global key bindings for cooking mode
document.addEventListener('keydown', (e) => {
if (elements.cookingModeModal.classList.contains('active')) {
if (e.key === 'ArrowRight' || e.key === ' ') {
e.preventDefault();
nextCookingStep();
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
prevCookingStep();
} else if (e.key === 'Escape') {
exitCookingMode();
}
}
});
}
// -------------------------------------------------------------
// UTILITIES: SYSTEM THEMES
// -------------------------------------------------------------
function initTheme() {
const savedTheme = localStorage.getItem('gourmet-theme') || 'dark';
if (savedTheme === 'dark') {
document.body.classList.add('dark-theme');
} else {
document.body.classList.remove('dark-theme');
}
}
function toggleTheme() {
document.body.classList.toggle('dark-theme');
const isDark = document.body.classList.contains('dark-theme');
localStorage.setItem('gourmet-theme', isDark ? 'dark' : 'light');
}
// -------------------------------------------------------------
// LOADING STATUS MACHINE
// -------------------------------------------------------------
function setLoaderStepIcon(stepElement, iconName, spin = false) {
const wrapper = stepElement.querySelector('.step-icon-wrapper');
if (wrapper) {
wrapper.innerHTML = `<i data-lucide="${iconName}" class="${spin ? 'spinning-icon' : ''}"></i>`;
}
}
function showScrapeLoading() {
elements.loadingOverlay.classList.add('active');
// Reset Steps to default
elements.loaderStepFetch.className = 'loader-step active';
elements.loaderStepParse.className = 'loader-step';
elements.loaderStepImages.className = 'loader-step';
setLoaderStepIcon(elements.loaderStepFetch, 'circle-dashed', true);
setLoaderStepIcon(elements.loaderStepParse, 'circle', false);
setLoaderStepIcon(elements.loaderStepImages, 'circle', false);
lucide.createIcons();
// Simulate process transitions
setTimeout(() => {
if (!elements.loadingOverlay.classList.contains('active')) return;
elements.loaderStepFetch.className = 'loader-step completed';
setLoaderStepIcon(elements.loaderStepFetch, 'check-circle-2', false);
elements.loaderStepParse.className = 'loader-step active';
setLoaderStepIcon(elements.loaderStepParse, 'circle-dashed', true);
lucide.createIcons();
}, 1800);
setTimeout(() => {
if (!elements.loadingOverlay.classList.contains('active')) return;
elements.loaderStepParse.className = 'loader-step completed';
setLoaderStepIcon(elements.loaderStepParse, 'check-circle-2', false);
elements.loaderStepImages.className = 'loader-step active';
setLoaderStepIcon(elements.loaderStepImages, 'circle-dashed', true);
lucide.createIcons();
}, 3500);
}
function hideScrapeLoading() {
elements.loadingOverlay.classList.remove('active');
}
// -------------------------------------------------------------
// CORE MODAL ROUTER
// -------------------------------------------------------------
function openModal(id) {
document.getElementById(id).classList.add('active');
}
function closeModal(id) {
document.getElementById(id).classList.remove('active');
if (id === 'cooking-mode-modal') {
exitCookingMode();
}
}
// -------------------------------------------------------------
// CRUD & API HANDLERS
// -------------------------------------------------------------
async function fetchRecipes() {
try {
const response = await fetch('/api/recipes');
allRecipes = await response.json();
renderLibrary();
} catch (error) {
console.error('Error fetching recipes library:', error);
}
}
// Scrape link form submission
async function handleScrapeSubmit(e) {
e.preventDefault();
const url = elements.scrapeUrlInput.value.trim();
if (!url) return;
showScrapeLoading();
try {
const response = await fetch('/api/scrape', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
if (response.status === 403 || errorData.isBlocked) {
throw new Error('BLOCKED');
}
throw new Error(errorData.error || 'Extraction error.');
}
const data = await response.json();
hideScrapeLoading();
openDraftPreview(data);
elements.scrapeUrlInput.value = '';
} catch (error) {
hideScrapeLoading();
if (error.message === 'BLOCKED') {
alert(`Access Blocked by Site Protection\n\nThis website uses Cloudflare or bot-defense screens to prevent automated scraping.\n\nDon't worry! You can still add this recipe by clicking the "Manual Recipe" button at the top to quickly paste your ingredients and directions.`);
} else {
alert(`Scraping failed: ${error.message}`);
}
}
}
// Save or Edit submit
async function handleRecipeSave(e) {
e.preventDefault();
const id = elements.editRecipeId.value;
const recipePayload = {
title: elements.editTitle.value.trim(),
sourceUrl: elements.editSource.value.trim(),
description: elements.editDescription.value.trim(),
servings: elements.editServings.value.trim(),
prepTime: elements.editPrepTime.value.trim(),
cookTime: elements.editCookTime.value.trim(),
image: elements.editImageUrl.value.trim(),
images: Array.from(elements.imageSelectionGrid.querySelectorAll('.image-option img')).map(img => img.src),
ingredients: elements.editIngredients.value.trim().split(/\r?\n/).map(line => line.trim()).filter(Boolean),
instructions: elements.editInstructions.value.trim().split(/\r?\n/).map(line => line.trim()).filter(Boolean)
};
try {
let response;
if (id) {
// Edit
response = await fetch(`/api/recipes/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(recipePayload)
});
} else {
// Create
response = await fetch('/api/recipes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(recipePayload)
});
}
if (!response.ok) throw new Error('Saving failed.');
const saved = await response.json();
closeModal('save-modal');
fetchRecipes();
// If active view was open, update it
if (activeRecipe && activeRecipe.id === saved.id) {
showRecipeDetails(saved);
}
} catch (error) {
console.error('Error saving recipe:', error);
alert('Error saving recipe.');
}
}
// Delete Active Recipe
async function deleteActiveRecipe() {
if (!activeRecipe) return;
if (!confirm(`Are you sure you want to delete "${activeRecipe.title}"?`)) return;
try {
const response = await fetch(`/api/recipes/${activeRecipe.id}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Deletion failed.');
closeModal('detail-modal');
activeRecipe = null;
fetchRecipes();
} catch (error) {
console.error('Error deleting recipe:', error);
alert('Deletion failed.');
}
}
// -------------------------------------------------------------
// DRAFT / MANUAL CREATOR MODAL POPULATOR
// -------------------------------------------------------------
function openManualAdd() {
elements.modalActionTitle.textContent = 'Create New Recipe';
elements.editRecipeId.value = '';
elements.recipeEditForm.reset();
elements.imageSelectionGrid.innerHTML = '';
selectedCoverImage = '';
openModal('save-modal');
}
function openDraftPreview(data) {
elements.modalActionTitle.textContent = 'Save Extracted Recipe';
elements.editRecipeId.value = '';
elements.editTitle.value = data.title || '';
elements.editSource.value = data.sourceUrl || '';
elements.editDescription.value = data.description || '';
elements.editServings.value = data.servings || '';
elements.editPrepTime.value = data.prepTime || '';
elements.editCookTime.value = data.cookTime || '';
// Ingredients text area
elements.editIngredients.value = data.ingredients ? data.ingredients.join('\n') : '';
// Instructions text area
elements.editInstructions.value = data.instructions ? data.instructions.join('\n') : '';
// Render Image Selector
renderImageSelector(data.images || []);
openModal('save-modal');
}
function editActiveRecipe() {
if (!activeRecipe) return;
elements.modalActionTitle.textContent = 'Edit Recipe Details';
elements.editRecipeId.value = activeRecipe.id;
elements.editTitle.value = activeRecipe.title || '';
elements.editSource.value = activeRecipe.sourceUrl || '';
elements.editDescription.value = activeRecipe.description || '';
elements.editServings.value = activeRecipe.servings || '';
elements.editPrepTime.value = activeRecipe.prepTime || '';
elements.editCookTime.value = activeRecipe.cookTime || '';
elements.editIngredients.value = activeRecipe.ingredients ? activeRecipe.ingredients.join('\n') : '';
elements.editInstructions.value = activeRecipe.instructions ? activeRecipe.instructions.join('\n') : '';
renderImageSelector(activeRecipe.images || []);
// Set active selection
if (activeRecipe.image) {
elements.editImageUrl.value = activeRecipe.image;
selectedCoverImage = activeRecipe.image;
selectImageThumbnail(activeRecipe.image);
}
openModal('save-modal');
}
// -------------------------------------------------------------
// IMAGE EXTRACTION CAROUSEL & SELECTOR
// -------------------------------------------------------------
function renderImageSelector(images) {
elements.imageSelectionGrid.innerHTML = '';
selectedCoverImage = '';
if (!images || images.length === 0) {
elements.editImageUrl.value = '';
return;
}
// Pre-select first image
selectedCoverImage = images[0];
elements.editImageUrl.value = images[0];
images.forEach((imgSrc, index) => {
const option = document.createElement('div');
option.className = `image-option ${index === 0 ? 'selected' : ''}`;
option.innerHTML = `<img src="${imgSrc}" alt="extracted thumbnail">`;
option.addEventListener('click', () => {
elements.imageSelectionGrid.querySelectorAll('.image-option').forEach(el => el.classList.remove('selected'));
option.classList.add('selected');
selectedCoverImage = imgSrc;
elements.editImageUrl.value = imgSrc;
});
elements.imageSelectionGrid.appendChild(option);
});
}
function selectImageThumbnail(url) {
elements.imageSelectionGrid.querySelectorAll('.image-option').forEach(el => {
const img = el.querySelector('img');
if (img && img.src === url) {
el.classList.add('selected');
} else {
el.classList.remove('selected');
}
});
}
// -------------------------------------------------------------
// DASHBOARD RENDERING
// -------------------------------------------------------------
function renderLibrary() {
elements.recipesGrid.innerHTML = '';
const searchVal = elements.librarySearch.value.toLowerCase().trim();
const filtered = allRecipes.filter(recipe => {
return recipe.title.toLowerCase().includes(searchVal) ||
(recipe.description && recipe.description.toLowerCase().includes(searchVal)) ||
(recipe.ingredients && recipe.ingredients.some(i => i.toLowerCase().includes(searchVal)));
});
if (filtered.length === 0) {
elements.emptyState.style.display = 'flex';
return;
}
elements.emptyState.style.display = 'none';
filtered.forEach(recipe => {
const domain = recipe.sourceUrl ? new URL(recipe.sourceUrl).hostname.replace('www.', '') : 'Manual Recipe';
const card = document.createElement('div');
card.className = 'recipe-card';
card.addEventListener('click', () => showRecipeDetails(recipe));
let imgBlock = '';
if (recipe.image) {
imgBlock = `<img src="${recipe.image}" class="recipe-card-img" alt="${recipe.title}" loading="lazy">`;
} else {
imgBlock = `
<div class="recipe-card-img-placeholder">
<i data-lucide="cooking-pot"></i>
<span>No dish photo</span>
</div>
`;
}
card.innerHTML = `
<div class="recipe-card-img-wrapper">
${imgBlock}
</div>
<div class="recipe-card-content">
<span class="recipe-card-origin">${domain}</span>
<h3 class="recipe-card-title">${recipe.title}</h3>
<p class="recipe-card-desc">${recipe.description || 'No description available for this recipe.'}</p>
<div class="recipe-card-meta">
${recipe.servings ? `<span><i data-lucide="users"></i> ${recipe.servings} Servings</span>` : ''}
${recipe.totalTime ? `<span><i data-lucide="clock"></i> ${recipe.totalTime}</span>` : ''}
</div>
</div>
`;
elements.recipesGrid.appendChild(card);
});
lucide.createIcons();
}
function filterRecipes() {
renderLibrary();
}
// -------------------------------------------------------------
// DYNAMIC SERVINGS QUANTITY SCALER
// -------------------------------------------------------------
function scaleIngredientText(text, multiplier) {
if (multiplier === 1) return text;
// Mixed number regex: e.g. "1 1/2" or "2 1/4"
const mixedRegex = /^(\d+)\s+(\d+\/\d+)\s*(.*)/;
const mixedMatch = text.match(mixedRegex);
if (mixedMatch) {
const whole = parseInt(mixedMatch[1], 10);
const fracParts = mixedMatch[2].split('/');
const fracVal = parseInt(fracParts[0], 10) / parseInt(fracParts[1], 10);
const total = (whole + fracVal) * multiplier;
return `${formatScaledNumber(total)} ${mixedMatch[3]}`;
}
// Pure Fraction regex: e.g. "1/2" or "3/4"
const fracRegex = /^(\d+\/\d+)\s*(.*)/;
const fracMatch = text.match(fracRegex);
if (fracMatch) {
const parts = fracMatch[1].split('/');
const val = (parseInt(parts[0], 10) / parseInt(parts[1], 10)) * multiplier;
return `${formatScaledNumber(val)} ${fracMatch[2]}`;
}
// Decimals & Integers regex: e.g. "1.5" or "4"
const numRegex = /^(\d+(?:\.\d+)?)\s*(.*)/;
const numMatch = text.match(numRegex);
if (numMatch) {
const val = parseFloat(numMatch[1]) * multiplier;
return `${formatScaledNumber(val)} ${numMatch[2]}`;
}
return text;
}
function formatScaledNumber(num) {
if (num % 1 === 0) return num.toString();
const rounded = Math.round(num * 100) / 100;
const decimal = rounded % 1;
const integer = Math.floor(rounded);
// Common fraction mappings for nice reading
if (Math.abs(decimal - 0.25) < 0.05) return (integer > 0 ? integer + ' ' : '') + '1/4';
if (Math.abs(decimal - 0.5) < 0.05) return (integer > 0 ? integer + ' ' : '') + '1/2';
if (Math.abs(decimal - 0.75) < 0.05) return (integer > 0 ? integer + ' ' : '') + '3/4';
if (Math.abs(decimal - 0.33) < 0.05) return (integer > 0 ? integer + ' ' : '') + '1/3';
if (Math.abs(decimal - 0.66) < 0.05) return (integer > 0 ? integer + ' ' : '') + '2/3';
return rounded.toString();
}
function adjustMultiplier(change) {
const newMulti = currentMultiplier + change;
if (newMulti < 0.5 || newMulti > 10) return;
currentMultiplier = newMulti;
elements.currentServingsVal.textContent = `${currentMultiplier}x`;
renderScaledIngredients();
}
function renderScaledIngredients() {
elements.recipeDetailIngredients.innerHTML = '';
if (!activeRecipe || !activeRecipe.ingredients) return;
activeRecipe.ingredients.forEach((ing, index) => {
const scaledText = scaleIngredientText(ing, currentMultiplier);
const li = document.createElement('li');
li.className = 'ingredient-checkbox-item';
li.innerHTML = `
<div class="checkbox-custom"><i data-lucide="check" style="width:12px;height:12px;"></i></div>
<span class="ingredient-text">${scaledText}</span>
`;
li.addEventListener('click', () => {
li.classList.toggle('checked');
});
elements.recipeDetailIngredients.appendChild(li);
});
lucide.createIcons();
}
// -------------------------------------------------------------
// RECIPE DETAIL VIEWER COMPONENT
// -------------------------------------------------------------
function showRecipeDetails(recipe) {
activeRecipe = recipe;
currentMultiplier = 1;
elements.currentServingsVal.textContent = '1x';
// Set hero cover banner image
if (recipe.image) {
elements.recipeDetailBanner.style.backgroundImage = `url('${recipe.image}')`;
} else {
elements.recipeDetailBanner.style.backgroundImage = 'none';
elements.recipeDetailBanner.style.backgroundColor = 'var(--bg-app)';
}
elements.recipeDetailTitle.textContent = recipe.title;
elements.recipeDetailDescription.textContent = recipe.description || 'No description available for this recipe.';
// Setup meta tags
if (recipe.servings) {
elements.tagServings.style.display = 'flex';
elements.tagServings.querySelector('.val').textContent = `${recipe.servings} Servings`;
} else {
elements.tagServings.style.display = 'none';
}
if (recipe.prepTime) {
elements.tagPrep.style.display = 'flex';
elements.tagPrep.querySelector('.val').textContent = recipe.prepTime;
} else {
elements.tagPrep.style.display = 'none';
}
if (recipe.cookTime) {
elements.tagCook.style.display = 'flex';
elements.tagCook.querySelector('.val').textContent = recipe.cookTime;
} else {
elements.tagCook.style.display = 'none';
}
// Populate Ingredients with scaler
renderScaledIngredients();
// Populate Directions Steps
elements.recipeDetailInstructions.innerHTML = '';
if (recipe.instructions) {
recipe.instructions.forEach((step, index) => {
const li = document.createElement('li');
// Look for section headers (e.g. ### Sauce)
if (step.startsWith('###')) {
li.className = 'instruction-step-item section-header-step';
li.innerHTML = `
<div class="step-text-block">${step.replace('###', '').trim()}</div>
`;
} else {
li.className = 'instruction-step-item';
li.innerHTML = `
<div class="step-number-bullet">${index + 1}</div>
<div class="step-text-block">${step}</div>
`;
li.addEventListener('click', () => {
li.classList.toggle('checked');
});
}
elements.recipeDetailInstructions.appendChild(li);
});
}
openModal('detail-modal');
}
// -------------------------------------------------------------
// INTERACTIVE FULLSCREEN COOKING ASSISTANT
// -------------------------------------------------------------
function enterCookingMode() {
if (!activeRecipe || !activeRecipe.instructions || activeRecipe.instructions.length === 0) return;
elements.cookRecipeTitle.textContent = activeRecipe.title;
cookingActiveStep = 0;
// Filter out header steps from the cooking carousel to keep steps clean
const cleanInstructions = activeRecipe.instructions.filter(step => !step.startsWith('###'));
if (cleanInstructions.length === 0) return;
elements.cookingModeModal.classList.add('active');
document.body.style.overflow = 'hidden'; // Lock background scrolling
renderCookingStep(cleanInstructions);
// Reset Timer
resetTimer();
startTimer();
}
function exitCookingMode() {
elements.cookingModeModal.classList.remove('active');
document.body.style.overflow = 'auto'; // Unlock background scrolling
stopTimer();
}
function renderCookingStep(instructions) {
const stepText = instructions[cookingActiveStep];
elements.cookStepIndex.textContent = `Step ${cookingActiveStep + 1} of ${instructions.length}`;
elements.cookStepInstruction.textContent = stepText;
// Update progress bar
const progressPercent = ((cookingActiveStep + 1) / instructions.length) * 100;
elements.cookProgressBar.style.width = `${progressPercent}%`;
// Render step navigation dot indicators
elements.cookStepDots.innerHTML = '';
instructions.forEach((_, idx) => {
const dot = document.createElement('div');
dot.className = `dot ${idx === cookingActiveStep ? 'active' : ''}`;
dot.addEventListener('click', () => {
cookingActiveStep = idx;
renderCookingStep(instructions);
});
elements.cookStepDots.appendChild(dot);
});
// Enable/disable buttons
elements.cookBtnPrev.disabled = cookingActiveStep === 0;
const nextBtnIconWrapper = elements.cookBtnNext.querySelector('.btn-icon-wrapper');
if (cookingActiveStep === instructions.length - 1) {
elements.cookBtnNext.querySelector('span').textContent = 'Finish Cooking';
if (nextBtnIconWrapper) nextBtnIconWrapper.innerHTML = '<i data-lucide="check"></i>';
} else {
elements.cookBtnNext.querySelector('span').textContent = 'Next Step';
if (nextBtnIconWrapper) nextBtnIconWrapper.innerHTML = '<i data-lucide="chevron-right"></i>';
}
lucide.createIcons();
}
function prevCookingStep() {
const cleanInstructions = activeRecipe.instructions.filter(step => !step.startsWith('###'));
if (cookingActiveStep > 0) {
cookingActiveStep--;
renderCookingStep(cleanInstructions);
}
}
function nextCookingStep() {
const cleanInstructions = activeRecipe.instructions.filter(step => !step.startsWith('###'));
if (cookingActiveStep < cleanInstructions.length - 1) {
cookingActiveStep++;
renderCookingStep(cleanInstructions);
} else {
// Finished cooking
exitCookingMode();
alert('Congratulations! Enjoy your delicious meal.');
}
}
// Timer/Stopwatch Functions
function startTimer() {
if (stopwatchInterval) return;
elements.cookTimerPlay.innerHTML = '<i data-lucide="pause"></i>';
lucide.createIcons();
stopwatchInterval = setInterval(() => {
stopwatchSeconds++;
const mins = Math.floor(stopwatchSeconds / 60).toString().padStart(2, '0');
const secs = (stopwatchSeconds % 60).toString().padStart(2, '0');
elements.cookTimerVal.textContent = `${mins}:${secs}`;
}, 1000);
}
function stopTimer() {
if (stopwatchInterval) {
clearInterval(stopwatchInterval);
stopwatchInterval = null;
}
elements.cookTimerPlay.innerHTML = '<i data-lucide="play"></i>';
lucide.createIcons();
}
function toggleTimer() {
if (stopwatchInterval) {
stopTimer();
} else {
startTimer();
}
}
function resetTimer() {
stopTimer();
stopwatchSeconds = 0;
elements.cookTimerVal.textContent = '00:00';
}