// ForkFolio Shared Recipe Viewer Logic let activeRecipe = null; let currentMultiplier = 1; let stopwatchInterval = null; let stopwatchSeconds = 0; let cookingActiveStep = 0; // DOM Cache const elements = { themeToggle: document.getElementById('theme-toggle'), shareRecipeBlock: document.getElementById('share-recipe-block'), recipeNotFoundBlock: document.getElementById('recipe-not-found-block'), // Recipe Details recipeDetailBanner: document.getElementById('recipe-detail-banner'), recipeDetailTitle: document.getElementById('recipe-detail-title'), recipeDetailCreatorName: document.getElementById('recipe-detail-creator-name'), tagServings: document.getElementById('tag-servings'), tagPrep: document.getElementById('tag-prep'), tagCook: document.getElementById('tag-cook'), tagTotal: document.getElementById('tag-total'), tagSource: document.getElementById('tag-source'), recipeDetailDescription: document.getElementById('recipe-detail-description'), recipeDetailIngredients: document.getElementById('recipe-detail-ingredients'), recipeDetailInstructions: document.getElementById('recipe-detail-instructions'), // Scaler 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') }; // Start Lifecycle window.addEventListener('DOMContentLoaded', () => { initTheme(); registerEvents(); fetchSharedRecipe(); }); 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'); } function registerEvents() { elements.themeToggle.addEventListener('click', toggleTheme); // Portions Adjustments elements.scalerMinus.addEventListener('click', () => adjustMultiplier(-0.5)); elements.scalerPlus.addEventListener('click', () => adjustMultiplier(0.5)); // Cooking steppers triggers elements.btnTriggerCookmode.addEventListener('click', enterCookingMode); elements.cookBtnPrev.addEventListener('click', prevCookingStep); elements.cookBtnNext.addEventListener('click', nextCookingStep); elements.cookTimerPlay.addEventListener('click', toggleTimer); elements.cookTimerReset.addEventListener('click', resetTimer); // Hotkeys for cooking stepper 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(); } } }); } // Fetch public shared recipe async function fetchSharedRecipe() { const token = new URLSearchParams(window.location.search).get('token'); if (!token) { showErrorBlock(); return; } try { const response = await fetch(`/api/share/recipes/${token}`); if (!response.ok) { throw new Error('Not found'); } activeRecipe = await response.json(); renderRecipePage(activeRecipe); } catch (err) { console.error('Failed to load shared recipe:', err); showErrorBlock(); } } function showErrorBlock() { elements.recipeNotFoundBlock.style.display = 'flex'; elements.shareRecipeBlock.style.display = 'none'; lucide.createIcons(); } function renderRecipePage(recipe) { elements.recipeNotFoundBlock.style.display = 'none'; elements.shareRecipeBlock.style.display = 'block'; // Banner Image Cover 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.recipeDetailCreatorName.textContent = `@${recipe.createdBy || 'gourmet_system'}`; elements.recipeDetailDescription.textContent = recipe.description || 'No description available for this recipe.'; // Servings and timings 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'; } const calculatedTotal = calculateTotalTime(recipe.prepTime, recipe.cookTime, recipe.totalTime); if (calculatedTotal) { elements.tagTotal.style.display = 'flex'; elements.tagTotal.querySelector('.val').textContent = calculatedTotal; } else { elements.tagTotal.style.display = 'none'; } if (recipe.sourceUrl) { elements.tagSource.style.display = 'flex'; elements.tagSource.href = recipe.sourceUrl; } else { elements.tagSource.style.display = 'none'; } // Render lists renderScaledIngredients(); renderInstructions(); lucide.createIcons(); } // DURATION & TOTAL TIME CALCULATION UTILITIES function parseDurationToMinutes(str) { if (!str || typeof str !== 'string') return 0; const s = str.trim(); if (!s) return 0; if (s.startsWith('P') || s.startsWith('p')) { const isoMatch = s.match(/P(?:(\d+)D)?T?(?:(\d+(?:\.\d+)?)H)?(?:(\d+)M)?(?:(\d+)S)?/i); if (isoMatch) { const days = parseInt(isoMatch[1] || '0', 10); const hours = parseFloat(isoMatch[2] || '0'); const mins = parseInt(isoMatch[3] || '0', 10); const secs = parseInt(isoMatch[4] || '0', 10); const totalSecs = (days * 86400) + Math.round(hours * 3600) + (mins * 60) + secs; if (totalSecs > 0) return Math.round(totalSecs / 60); } } let totalMinutes = 0; let matched = false; const hrMatch = s.match(/(\d+(?:\.\d+)?)\s*(?:hours?|hrs?|h\b)/i); if (hrMatch) { totalMinutes += Math.round(parseFloat(hrMatch[1]) * 60); matched = true; } const minMatch = s.match(/(?:(\d+)\s*[-–to]+\s*)?(\d+)\s*(?:minutes?|mins?|m\b)/i); if (minMatch) { const m = parseInt(minMatch[2], 10); if (!isNaN(m)) { totalMinutes += m; matched = true; } } if (!matched && /^\d+$/.test(s)) { return parseInt(s, 10); } return totalMinutes; } function formatMinutes(minutes) { if (!minutes || minutes <= 0) return ''; const hrs = Math.floor(minutes / 60); const mins = minutes % 60; if (hrs > 0 && mins > 0) { return `${hrs} hr${hrs > 1 ? 's' : ''} ${mins} min${mins > 1 ? 's' : ''}`; } if (hrs > 0) { return `${hrs} hr${hrs > 1 ? 's' : ''}`; } return `${mins} min${mins > 1 ? 's' : ''}`; } function calculateTotalTime(prepTime, cookTime, existingTotalTime) { if (existingTotalTime && existingTotalTime.trim()) { if (existingTotalTime.startsWith('P') || existingTotalTime.startsWith('p')) { const mins = parseDurationToMinutes(existingTotalTime); if (mins > 0) return formatMinutes(mins); } return existingTotalTime.trim(); } const prepM = parseDurationToMinutes(prepTime); const cookM = parseDurationToMinutes(cookTime); if (prepM > 0 || cookM > 0) { return formatMinutes(prepM + cookM); } if (cookTime && cookTime.trim()) { const cookMOnly = parseDurationToMinutes(cookTime); return cookMOnly > 0 ? formatMinutes(cookMOnly) : cookTime.trim(); } if (prepTime && prepTime.trim()) { const prepMOnly = parseDurationToMinutes(prepTime); return prepMOnly > 0 ? formatMinutes(prepMOnly) : prepTime.trim(); } return ''; } // Ingredient Scaling Algorithms 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) => { const scaledText = scaleIngredientText(ing, currentMultiplier); const li = document.createElement('li'); li.className = 'ingredient-checkbox-item'; li.innerHTML = `