const app = { dom: {}, state: { recipes: [], confirmedRecipeProducts: [], }, api: { async getRecipes() { try { const response = await fetch('api/get_recipes.php'); const data = await response.json(); if (data.success) { app.state.recipes = data.recipes; } else { console.error('Failed to load recipes:', data.error); app.dom.recipeCardsContainer.innerHTML = '

Error loading recipes.

'; } } catch (error) { console.error('Error:', error); app.dom.recipeCardsContainer.innerHTML = '

Could not connect to the server.

'; } }, async saveRecipe(recipeData) { try { const response = await fetch('api/save_recipe.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(recipeData) }); return await response.json(); } catch (error) { alert('Error: ' + error.message); return { success: false, error: error.message }; } }, async deleteRecipe(id) { try { const response = await fetch('api/delete_recipe.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: id }) }); const data = await response.json(); if (data.success) { await app.api.getRecipes(); app.ui.renderRecipeCards(app.state.recipes); app.ui.updateShoppingList(); } else { alert('Failed to delete recipe: ' + data.error); } } catch (error) { alert('Error: ' + error.message); } } }, ui: { renderRecipeCards(recipes) { app.dom.recipeCardsContainer.innerHTML = ''; if (!recipes || recipes.length === 0) { app.dom.recipeCardsContainer.innerHTML = '

Your saved recipes will appear here.

'; return; } recipes.forEach(recipe => { const cardCol = document.createElement('div'); cardCol.className = 'col-12 mb-3'; cardCol.setAttribute('data-id', recipe.id); const card = document.createElement('div'); card.className = 'card h-100'; if (recipe.category) { const categoryLabel = document.createElement('div'); categoryLabel.className = 'recipe-category-label'; categoryLabel.textContent = recipe.category; card.appendChild(categoryLabel); } const cardBody = document.createElement('div'); cardBody.className = 'card-body d-flex flex-column'; const title = document.createElement('h5'); title.className = 'card-title'; title.textContent = recipe.name; const text = document.createElement('p'); text.className = 'card-text text-muted'; text.textContent = `${recipe.ingredients.length} ingredients`; const buttonGroup = document.createElement('div'); buttonGroup.className = 'mt-auto pt-2'; buttonGroup.innerHTML = ` `; cardBody.appendChild(title); cardBody.appendChild(text); cardBody.appendChild(buttonGroup); card.appendChild(cardBody); cardCol.appendChild(card); app.dom.recipeCardsContainer.appendChild(cardCol); }); }, updateShoppingList() { const guestCount = parseInt(app.dom.guestCountInput.value, 10) || 1; const portionsPerGuest = parseInt(app.dom.portionsPerGuestInput.value, 10) || 1; const totalMultiplier = guestCount * portionsPerGuest; const combinedIngredients = new Map(); // 1. Process recipe ingredients app.state.recipes.forEach(recipe => { if (recipe.ingredients) { recipe.ingredients.forEach(ing => { const name = ing.name.trim(); const unit = ing.unit.trim(); if (!name) return; const key = `${name.toLowerCase()}|${unit.toLowerCase()}`; if (!combinedIngredients.has(key)) { combinedIngredients.set(key, { name: name, unit: unit, recipeQty: 0, additionalQty: 0, sources: [], category: null // For 'pack' items }); } const item = combinedIngredients.get(key); item.recipeQty += (ing.quantity || 0); if (!item.sources.includes(recipe.name)) { item.sources.push(recipe.name); } }); } }); // 2. Process additional products if (app.state.additionalProducts) { app.state.additionalProducts.forEach(prod => { const name = prod.name.trim(); const unit = prod.unit.trim(); if (!name) return; const key = `${name.toLowerCase()}|${unit.toLowerCase()}`; if (!combinedIngredients.has(key)) { combinedIngredients.set(key, { name: name, unit: unit, recipeQty: 0, additionalQty: 0, sources: [], category: null }); } const item = combinedIngredients.get(key); item.additionalQty += prod.quantity; const source = prod.source || 'Additional product'; if (!item.sources.includes(source)) { item.sources.push(source); } if (prod.unit === 'pack' && prod.category) { item.category = prod.category; } }); } // 3. Group for display const groups = { Food: { units: ['g', 'kg'], ingredients: [] }, Drinks: { units: ['ml', 'l'], ingredients: [] }, Count: { units: ['piece'], ingredients: [] }, "Tableware and consumables": { units: [], ingredients: [] }, "Cooking and serving": { units: [], ingredients: [] }, Other: { units: [], ingredients: [] } }; combinedIngredients.forEach((item, key) => { let groupName = 'Other'; if (item.unit === 'pack' && item.category) { groupName = item.category; } else { for (const name in groups) { if (groups[name].units.includes(item.unit)) { groupName = name; break; } } } if (!groups[groupName]) { // handle dynamic categories from 'pack' groups[groupName] = { units: [], ingredients: [] }; } groups[groupName].ingredients.push(item); }); // 4. Render HTML let html = ''; let totalIngredients = 0; for (const groupName in groups) { const group = groups[groupName]; if (group.ingredients.length > 0) { totalIngredients += group.ingredients.length; html += `

${groupName}

`; html += ''; } } if (totalIngredients === 0) { html += '

Your shopping list is empty. Add a recipe, and its ingredients will appear here.

'; } app.dom.shoppingListContainer.innerHTML = html; const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl); }); }, addIngredientRow(ingredient = { name: '', quantity: '', unit: 'g' }) { const row = document.createElement('div'); row.className = 'ingredient-row mb-3'; const units = ['g', 'kg', 'ml', 'l', 'piece', 'pack']; const unitButtons = units.map(u => `` ).join(''); row.innerHTML = `
${unitButtons}
`; app.dom.ingredientsContainer.appendChild(row); }, clearForm() { app.dom.recipeIdInput.value = ''; app.dom.recipeNameInput.value = ''; app.dom.recipeCategoryInput.value = ''; app.dom.guestCountInput.value = '1'; app.dom.ingredientsContainer.innerHTML = ''; app.ui.addIngredientRow(); app.dom.newRecipeBtn.textContent = 'Save Recipe'; app.dom.cancelEditBtn.style.display = 'none'; document.getElementById('recipe-form-modal-label').textContent = 'Add a Recipe'; }, populateFormForEdit(recipeId) { const recipe = app.state.recipes.find(r => r.id == recipeId); if (!recipe) return; app.dom.recipeIdInput.value = recipe.id; app.dom.recipeNameInput.value = recipe.name; app.dom.recipeCategoryInput.value = recipe.category || ''; app.dom.guestCountInput.value = recipe.guests; app.dom.ingredientsContainer.innerHTML = ''; if (recipe.ingredients) { recipe.ingredients.forEach(ing => app.ui.addIngredientRow(ing)); } else { app.ui.addIngredientRow(); } app.dom.newRecipeBtn.textContent = 'Update Recipe'; app.dom.cancelEditBtn.style.display = 'block'; document.getElementById('recipe-form-modal-label').textContent = 'Edit Recipe'; app.dom.recipeFormModal.show(); app.dom.recipeNameInput.focus(); }, getRecipeDataFromForm() { const recipeName = app.dom.recipeNameInput.value.trim(); const guests = parseInt(app.dom.guestCountInput.value, 10) || 0; const category = app.dom.recipeCategoryInput.value; const ingredients = []; const rows = app.dom.ingredientsContainer.querySelectorAll('.ingredient-row'); rows.forEach(row => { const name = row.querySelector('input[placeholder="Ingredient Name"]').value.trim(); const qty = parseFloat(row.querySelector('input[placeholder="Qty"]').value); const activeButton = row.querySelector('.unit-selector .btn-secondary'); const unit = activeButton ? activeButton.textContent.trim() : 'g'; if (name && !isNaN(qty) && qty > 0) { ingredients.push({ name, quantity: qty, unit }); } }); if (recipeName && guests > 0 && ingredients.length > 0) { return { name: recipeName, guests, ingredients, category }; } return null; }, createSnowflakes() { const snowContainer = document.getElementById('snow-container'); if (!snowContainer) return; snowContainer.innerHTML = ''; const numberOfSnowflakes = 50; for (let i = 0; i < numberOfSnowflakes; i++) { const snowflake = document.createElement('div'); snowflake.className = 'snowflake'; const size = Math.random() * 4 + 2; snowflake.style.width = `${size}px`; snowflake.style.height = `${size}px`; snowflake.style.left = Math.random() * 100 + 'vw'; const animationDuration = Math.random() * 5 + 5; snowflake.style.animationDuration = `${animationDuration}s`; const animationDelay = Math.random() * 5; snowflake.style.animationDelay = `${animationDelay}s`; snowflake.style.opacity = Math.random() * 0.7 + 0.3; snowContainer.appendChild(snowflake); } } }, events: { attachEventListeners() { app.dom.addIngredientBtn.addEventListener('click', () => app.ui.addIngredientRow()); app.dom.ingredientsContainer.addEventListener('click', function(e) { if (e.target.classList.contains('remove-ingredient')) { e.target.closest('.ingredient-row').remove(); } else if (e.target.classList.contains('unit-btn')) { const group = e.target.closest('.unit-selector'); if (group) { group.querySelectorAll('.unit-btn').forEach(btn => { btn.classList.remove('btn-secondary'); btn.classList.add('btn-outline-secondary'); }); e.target.classList.remove('btn-outline-secondary'); e.target.classList.add('btn-secondary'); } } }); app.dom.newRecipeBtn.addEventListener('click', async function() { const recipeData = app.ui.getRecipeDataFromForm(); if (!recipeData) { alert('Please fill out the recipe name, category, guests, and at least one ingredient before saving.'); return; } const recipeId = app.dom.recipeIdInput.value; if (recipeId) { recipeData.id = recipeId; } const data = await app.api.saveRecipe(recipeData); if (data.success && data.recipe) { app.api.getRecipes().then(() => { const currentCategory = app.dom.categoryFilters.querySelector('.active').dataset.category; if (currentCategory === 'all') { app.ui.renderRecipeCards(app.state.recipes); } else { const filteredRecipes = app.state.recipes.filter(recipe => recipe.category === currentCategory); app.ui.renderRecipeCards(filteredRecipes); } app.ui.updateShoppingList(); app.ui.clearForm(); app.dom.recipeFormModal.hide(); }); } else { alert('Failed to save recipe: ' + data.error); } }); app.dom.recipeCardsContainer.addEventListener('click', function(e) { const target = e.target; const card = target.closest('.col-12[data-id]'); if (!card) return; const recipeId = card.getAttribute('data-id'); if (target.classList.contains('delete-recipe')) { if (confirm('Are you sure you want to delete this recipe?')) { app.api.deleteRecipe(recipeId); } } if (target.classList.contains('edit-recipe')) { app.ui.populateFormForEdit(recipeId); } }); app.dom.shoppingListContainer.addEventListener('click', function(e) { if (e.target.matches('.form-check-input')) { const listItem = e.target.closest('.list-group-item'); if (listItem) { listItem.classList.toggle('checked', e.target.checked); } } else if (e.target.matches('.increment-item')) { const key = e.target.dataset.key; const [name, unit] = key.split('|'); if (!app.state.additionalProducts) { app.state.additionalProducts = []; } let productToModify = app.state.additionalProducts.find(p => p.name.toLowerCase() === name.toLowerCase() && p.unit.toLowerCase() === unit.toLowerCase()); if (!productToModify) { const properName = name.charAt(0).toUpperCase() + name.slice(1); productToModify = { name: properName, quantity: 0, unit: unit, source: 'Additional product' }; app.state.additionalProducts.push(productToModify); } productToModify.quantity++; app.ui.updateShoppingList(); } else if (e.target.matches('.decrement-item')) { const key = e.target.dataset.key; const [name, unit] = key.split('|'); if (!app.state.additionalProducts) { app.state.additionalProducts = []; } let productToModify = app.state.additionalProducts.find(p => p.name.toLowerCase() === name.toLowerCase() && p.unit.toLowerCase() === unit.toLowerCase()); if (productToModify && productToModify.quantity > 0) { productToModify.quantity--; app.ui.updateShoppingList(); } else { // It's a recipe ingredient, show confirmation let recipeName = ''; let ingredientName = ''; let recipeId = null; let ingredientIndex = -1; for (const recipe of app.state.recipes) { const foundIngredientIndex = recipe.ingredients.findIndex(ing => ing.name.toLowerCase() === name.toLowerCase() && ing.unit.toLowerCase() === unit.toLowerCase()); if (foundIngredientIndex !== -1) { recipeName = recipe.name; ingredientName = recipe.ingredients[foundIngredientIndex].name; recipeId = recipe.id; ingredientIndex = foundIngredientIndex; break; } } if (recipeId !== null) { const confirmationKey = `${recipeId}-${name.toLowerCase()}-${unit.toLowerCase()}`; const recipeToUpdate = app.state.recipes.find(r => r.id === recipeId); const proceedWithRemoval = () => { if (recipeToUpdate && recipeToUpdate.ingredients[ingredientIndex].quantity > 0) { recipeToUpdate.ingredients[ingredientIndex].quantity--; } app.ui.updateShoppingList(); }; if (app.state.confirmedRecipeProducts.includes(confirmationKey)) { proceedWithRemoval(); } else { document.getElementById('modal-recipe-name').textContent = recipeName; document.getElementById('modal-ingredient-name').textContent = ingredientName; const confirmModal = new bootstrap.Modal(document.getElementById('confirmRemoveModal')); confirmModal.show(); document.getElementById('confirm-remove-btn').onclick = () => { app.state.confirmedRecipeProducts.push(confirmationKey); proceedWithRemoval(); confirmModal.hide(); }; } } } } }); app.dom.guestCountInput.addEventListener('input', app.ui.updateShoppingList); app.dom.portionsPerGuestInput.addEventListener('input', app.ui.updateShoppingList); app.dom.cancelEditBtn.addEventListener('click', function() { app.ui.clearForm(); app.dom.recipeFormModal.hide(); }); app.dom.recipeSearchInput.addEventListener('input', function() { const searchTerm = app.dom.recipeSearchInput.value.toLowerCase(); const filteredRecipes = app.state.recipes.filter(recipe => recipe.name.toLowerCase().includes(searchTerm)); app.ui.renderRecipeCards(filteredRecipes); }); app.dom.categoryFilters.addEventListener('click', function(e) { if (e.target.tagName === 'BUTTON') { const category = e.target.dataset.category; app.dom.categoryFilters.querySelectorAll('button').forEach(btn => { btn.classList.remove('active', 'btn-secondary'); btn.classList.add('btn-outline-secondary'); }); e.target.classList.add('active', 'btn-secondary'); e.target.classList.remove('btn-outline-secondary'); if (category === 'all') { app.ui.renderRecipeCards(app.state.recipes); } else { const filteredRecipes = app.state.recipes.filter(recipe => recipe.category === category); app.ui.renderRecipeCards(filteredRecipes); } } }); app.dom.printShoppingListBtn.addEventListener('click', function() { window.print(); }); app.dom.addProductModal._element.addEventListener('click', function(e) { if (e.target.classList.contains('unit-btn')) { const group = e.target.closest('.unit-selector'); if (group) { group.querySelectorAll('.unit-btn').forEach(btn => { btn.classList.remove('btn-secondary'); btn.classList.add('btn-outline-secondary'); }); e.target.classList.remove('btn-outline-secondary'); e.target.classList.add('btn-secondary'); if (e.target.textContent.trim() === 'pack') { app.dom.productCategoryWrapper.style.display = 'block'; } else { app.dom.productCategoryWrapper.style.display = 'none'; } } } }); app.dom.addProductForm.addEventListener('submit', (e) => { e.preventDefault(); const name = app.dom.productNameInput.value.trim(); const quantity = parseFloat(app.dom.productQuantityInput.value); const unitButton = app.dom.addProductModal._element.querySelector('.unit-selector .btn-secondary'); const unit = unitButton ? unitButton.textContent.trim() : 'g'; const category = app.dom.productCategory.value; if (!name || isNaN(quantity) || quantity <= 0 || !unit) { alert('Please fill out all fields with valid values.'); return; } if (unit === 'pack' && category === 'Choose...') { alert('Please select a category for products in packs.'); return; } if (!app.state.additionalProducts) { app.state.additionalProducts = []; } const key = `${name.toLowerCase()}|${unit.toLowerCase()}`; const existingProduct = app.state.additionalProducts.find(p => `${p.name.toLowerCase()}|${p.unit.toLowerCase()}` === key); if (existingProduct) { existingProduct.quantity += quantity; } else { const newProduct = { name, quantity, unit, source: 'Additional product' }; if (unit === 'pack') { newProduct.category = category; } app.state.additionalProducts.push(newProduct); } app.ui.updateShoppingList(); // Reset form app.dom.productNameInput.value = ''; app.dom.productQuantityInput.value = '1'; app.dom.productCategory.value = 'Choose...'; app.dom.productCategoryWrapper.style.display = 'none'; const unitButtons = app.dom.addProductModal._element.querySelectorAll('.unit-selector .unit-btn'); unitButtons.forEach((btn, index) => { if (index === 0) { btn.classList.add('btn-secondary'); btn.classList.remove('btn-outline-secondary'); } else { btn.classList.remove('btn-secondary'); btn.classList.add('btn-outline-secondary'); } }); app.dom.addProductModal.hide(); }); document.getElementById('recipe-form-modal').addEventListener('show.bs.modal', function () { if (!app.dom.recipeIdInput.value) { app.ui.clearForm(); } }); } }, init() { app.dom = { recipeNameInput: document.getElementById('recipeName'), guestCountInput: document.getElementById('guestCount'), portionsPerGuestInput: document.getElementById('portionsPerGuest'), ingredientsContainer: document.getElementById('ingredients-container'), addIngredientBtn: document.getElementById('add-ingredient'), newRecipeBtn: document.getElementById('new-recipe-btn'), cancelEditBtn: document.getElementById('cancel-edit-btn'), shoppingListContainer: document.getElementById('shopping-list-container'), recipeCardsContainer: document.getElementById('recipe-cards-container'), recipeIdInput: document.getElementById('recipeId'), recipeCategoryInput: document.getElementById('recipeCategory'), recipeSearchInput: document.getElementById('recipe-search'), categoryFilters: document.getElementById('category-filters'), addProductBtn: document.getElementById('add-product-btn'), printShoppingListBtn: document.getElementById('print-shopping-list-btn'), recipeFormModal: new bootstrap.Modal(document.getElementById('recipe-form-modal')), addProductModal: new bootstrap.Modal(document.getElementById('add-product-modal')), addProductForm: document.getElementById('add-product-form'), productNameInput: document.getElementById('productName'), productQuantityInput: document.getElementById('productQuantity'), productCategoryWrapper: document.getElementById('product-category-wrapper'), productCategory: document.getElementById('productCategory'), }; app.ui.createSnowflakes(); app.events.attachEventListeners(); app.dom.cancelEditBtn.style.display = 'none'; app.ui.addIngredientRow(); app.api.getRecipes().then(() => { app.ui.renderRecipeCards(app.state.recipes); app.ui.updateShoppingList(); }); } }; document.addEventListener('DOMContentLoaded', app.init);