const app = { dom: {}, state: { recipes: [], confirmedRecipeProducts: [], checkedItems: [], additionalProducts: [], selectedRecipeIds: [], user: null }, api: { async checkAuth() { try { const response = await fetch('api/check_auth.php'); const data = await response.json(); if (data.success && data.logged_in) { app.state.user = data.user; if (data.user.shopping_list) { app.state.checkedItems = data.user.shopping_list.checkedItems || []; app.state.additionalProducts = data.user.shopping_list.additionalProducts || []; app.state.confirmedRecipeProducts = data.user.shopping_list.confirmedRecipeProducts || []; app.state.selectedRecipeIds = data.user.shopping_list.selectedRecipeIds || []; } } else { app.state.user = null; } } catch (error) { console.error('Auth check failed:', error); } }, async saveShoppingList() { if (!app.state.user) return; try { await fetch('api/save_shopping_list.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ shopping_list: { checkedItems: app.state.checkedItems, additionalProducts: app.state.additionalProducts, confirmedRecipeProducts: app.state.confirmedRecipeProducts, selectedRecipeIds: app.state.selectedRecipeIds } }) }); } catch (error) { console.error('Failed to save shopping list:', error); } }, async login(email, password) { try { const response = await fetch('api/login.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }); const data = await response.json(); if (data.success) { await app.api.checkAuth(); await app.api.getRecipes(); app.ui.updateAuthNav(); app.ui.renderRecipeCards(app.state.recipes); app.ui.updateShoppingList(); return { success: true }; } return data; } catch (error) { return { success: false, error: error.message }; } }, async register(email, password) { try { const response = await fetch('api/register.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }); const data = await response.json(); if (data.success) { await app.api.checkAuth(); await app.api.getRecipes(); app.ui.updateAuthNav(); app.ui.renderRecipeCards(app.state.recipes); app.ui.updateShoppingList(); return { success: true }; } return data; } catch (error) { return { success: false, error: error.message }; } }, async logout() { try { const response = await fetch('api/logout.php'); const data = await response.json(); if (data.success) { app.state.user = null; app.state.recipes = []; app.state.checkedItems = []; app.state.additionalProducts = []; app.state.confirmedRecipeProducts = []; await app.api.getRecipes(); app.ui.updateAuthNav(); app.ui.renderRecipeCards(app.state.recipes); app.ui.updateShoppingList(); } } catch (error) { console.error('Logout failed:', error); } }, 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 = '

Failed to connect to the server.

'; } }, async saveRecipe(formData) { try { const response = await fetch('api/save_recipe.php', { method: 'POST', body: formData }); 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) { // Remove from selectedRecipeIds if present const index = app.state.selectedRecipeIds.indexOf(id.toString()); if (index > -1) { app.state.selectedRecipeIds.splice(index, 1); } const indexNum = app.state.selectedRecipeIds.indexOf(Number(id)); if (indexNum > -1) { app.state.selectedRecipeIds.splice(indexNum, 1); } await app.api.getRecipes(); app.ui.renderRecipeCards(app.state.recipes); app.ui.updateShoppingList(); app.api.saveShoppingList(); } 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, index) => { const cardCol = document.createElement('div'); cardCol.className = 'col-md-6 mb-4 recipe-card-enter'; cardCol.setAttribute('data-id', recipe.id); cardCol.style.animationDelay = `${index * 0.1}s`; const isSelected = app.state.selectedRecipeIds.includes(recipe.id.toString()) || app.state.selectedRecipeIds.includes(Number(recipe.id)); const card = document.createElement('div'); card.className = `card h-100 recipe-selection-card ${isSelected ? 'selected' : ''}`; if (recipe.image_url) { const img = document.createElement('img'); img.src = recipe.image_url; img.className = 'card-img-top'; img.alt = recipe.name; card.appendChild(img); } const cardBody = document.createElement('div'); cardBody.className = 'card-body d-flex flex-column align-items-center'; const titleWrapper = document.createElement('div'); titleWrapper.className = 'd-flex flex-column align-items-center mb-2 gap-1 w-100'; const selectionWrapper = document.createElement('div'); selectionWrapper.className = 'form-check mb-0 d-flex align-items-center justify-content-center'; selectionWrapper.innerHTML = ` `; titleWrapper.appendChild(selectionWrapper); if (recipe.category) { const categoryLabel = document.createElement('div'); categoryLabel.className = 'recipe-category-label'; categoryLabel.textContent = recipe.category === 'Drinks' ? 'Drinks' : recipe.category === 'Breakfast' ? 'Breakfast' : recipe.category === 'Dinner' ? 'Lunch/Dinner' : recipe.category === 'Appetizers' ? 'Appetizers' : recipe.category; titleWrapper.appendChild(categoryLabel); } const text = document.createElement('p'); text.className = 'card-text text-muted mb-2 small'; text.textContent = `${recipe.ingredients.length} ingredients`; const controlsWrapper = document.createElement('div'); controlsWrapper.className = 'recipe-controls mb-2 p-2 bg-light rounded'; controlsWrapper.innerHTML = `
`; const buttonGroup = document.createElement('div'); buttonGroup.className = 'mt-auto pt-2 d-flex gap-2 justify-content-center w-100'; buttonGroup.innerHTML = ` `; cardBody.appendChild(titleWrapper); cardBody.appendChild(text); cardBody.appendChild(controlsWrapper); cardBody.appendChild(buttonGroup); card.appendChild(cardBody); cardCol.appendChild(card); app.dom.recipeCardsContainer.appendChild(cardCol); }); }, updateShoppingList() { const combinedIngredients = new Map(); // 1. Process recipe ingredients and calculate total based on per-recipe inputs app.state.recipes.forEach(recipe => { // Only include if selected if (!app.state.selectedRecipeIds.includes(recipe.id.toString()) && !app.state.selectedRecipeIds.includes(Number(recipe.id))) { return; } const card = app.dom.recipeCardsContainer.querySelector(`[data-id="${recipe.id}"]`); if (!card) return; const guestsInput = card.querySelector('.recipe-guests-input'); const portionsInput = card.querySelector('.recipe-portions-input'); const targetGuests = guestsInput ? parseInt(guestsInput.value, 10) : recipe.guests; const targetPortions = portionsInput ? parseInt(portionsInput.value, 10) : 1; if (isNaN(targetGuests) || targetGuests <= 0) return; const baseGuests = parseInt(recipe.guests, 10) || 1; 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: ing.category }); } const item = combinedIngredients.get(key); item.recipeQty += (ing.quantity || 0) * targetGuests * targetPortions; 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: prod.category }); } const item = combinedIngredients.get(key); item.additionalQty += prod.quantity; const source = prod.source || 'Add. product'; if (!item.sources.includes(source)) { item.sources.push(source); } if (prod.category && !item.category) { item.category = prod.category; } }); } // 3. Group for display const groups = { 'Food': { label: 'Food', ingredients: [] }, 'Drinks': { label: 'Drinks', ingredients: [] }, 'Cooking and serving': { label: 'Kitchen & serving', ingredients: [] }, 'Tableware and consumables': { label: 'Tableware & consumables', ingredients: [] } }; combinedIngredients.forEach((item, key) => { let groupName = 'Food'; // Default to Food if (item.category) { const normalizedCategory = item.category.toLowerCase(); if (groups.hasOwnProperty(item.category)) { groupName = item.category; } else if (normalizedCategory === 'drinks' || normalizedCategory === 'drink') { groupName = 'Drinks'; } } 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 += `

${group.label}

`; 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', 'pcs', '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.recipeInstructionsInput.value = ''; app.dom.recipeCategoryInput.value = ''; app.dom.recipeImage.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 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.recipeInstructionsInput.value = recipe.instructions || ''; 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(); }, populateViewModal(recipeId) { const recipe = app.state.recipes.find(r => r.id == recipeId); if (!recipe) return; const modal = document.getElementById('view-recipe-modal'); const img = modal.querySelector('img'); if (img) { img.remove(); } if (recipe.image_url) { const newImg = document.createElement('img'); newImg.src = recipe.image_url; newImg.className = 'card-img-top mb-3'; newImg.alt = recipe.name; modal.querySelector('.modal-body').prepend(newImg); } document.getElementById('view-recipe-name').textContent = recipe.name; document.getElementById('view-recipe-category').textContent = recipe.category === 'Drinks' ? 'Drinks' : recipe.category === 'Breakfast' ? 'Breakfast' : recipe.category === 'Dinner' ? 'Lunch/Dinner' : recipe.category === 'Appetizers' ? 'Appetizers' : 'No category'; document.getElementById('view-recipe-guests').textContent = recipe.guests; document.getElementById('view-recipe-instructions').textContent = recipe.instructions || 'No instructions provided.'; const ingredientsList = document.getElementById('view-recipe-ingredients'); ingredientsList.innerHTML = ''; if (recipe.ingredients) { recipe.ingredients.forEach(ing => { const li = document.createElement('li'); li.className = 'list-group-item'; li.textContent = `${ing.name} - ${ing.quantity} ${ing.unit}`; ingredientsList.appendChild(li); }); } const viewRecipeModal = new bootstrap.Modal(modal); viewRecipeModal.show(); }, getRecipeDataFromForm() { const recipeName = app.dom.recipeNameInput.value.trim(); const instructions = app.dom.recipeInstructionsInput.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 nameInput = row.querySelector('input[placeholder="Ingredient name"]'); const name = nameInput ? nameInput.value.trim() : ''; const qtyInput = row.querySelector('input[placeholder="Qty"]'); const qty = qtyInput ? parseFloat(qtyInput.value) : NaN; 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, instructions, guests, ingredients, category }; } return null; }, loadCheckedItems() { const stored = localStorage.getItem('checkedItems'); if (stored) { app.state.checkedItems = JSON.parse(stored); } }, saveCheckedItems() { localStorage.setItem('checkedItems', JSON.stringify(app.state.checkedItems)); app.api.saveShoppingList(); }, updateAuthNav() { const nav = document.getElementById('auth-nav'); const guestView = document.getElementById('guest-view'); const appView = document.getElementById('app-view'); if (!nav) return; if (app.state.user) { nav.innerHTML = ` `; if (guestView) guestView.classList.add('d-none'); if (appView) appView.classList.remove('d-none'); } else { nav.innerHTML = ''; // Landing page handles login/register if (guestView) guestView.classList.remove('d-none'); if (appView) appView.classList.add('d-none'); } } }, events: { attachEventListeners() { // Auth events (Landing Page) const loginFormLanding = document.getElementById('login-form-landing'); if (loginFormLanding) { loginFormLanding.addEventListener('submit', async (e) => { e.preventDefault(); const email = loginFormLanding.querySelector('[name="email"]').value; const password = loginFormLanding.querySelector('[name="password"]').value; const result = await app.api.login(email, password); if (result.success) { loginFormLanding.reset(); } else { alert(result.error); } }); } const registerFormLanding = document.getElementById('register-form-landing'); if (registerFormLanding) { registerFormLanding.addEventListener('submit', async (e) => { e.preventDefault(); const email = registerFormLanding.querySelector('[name="email"]').value; const password = registerFormLanding.querySelector('[name="password"]').value; const result = await app.api.register(email, password); if (result.success) { registerFormLanding.reset(); } else { alert(result.error); } }); } // Toggle login/register on landing const showRegister = document.getElementById('show-register'); const showLogin = document.getElementById('show-login'); const loginContainer = document.getElementById('login-container'); const registerContainer = document.getElementById('register-container'); if (showRegister && showLogin) { showRegister.addEventListener('click', (e) => { e.preventDefault(); loginContainer.classList.add('d-none'); registerContainer.classList.remove('d-none'); }); showLogin.addEventListener('click', (e) => { e.preventDefault(); registerContainer.classList.add('d-none'); loginContainer.classList.remove('d-none'); }); } document.getElementById('auth-nav').addEventListener('click', (e) => { if (e.target.id === 'logout-btn') { app.api.logout(); } }); app.dom.addIngredientBtn.addEventListener('click', () => app.ui.addIngredientRow()); app.dom.aiScanBtn.addEventListener('click', async function() { const file = app.dom.recipeImage.files[0]; if (!file) { alert('Please select an image first.'); return; } app.dom.aiScanBtn.disabled = true; app.dom.aiScanLoading.classList.remove('d-none'); const formData = new FormData(); formData.append('image', file); try { const response = await fetch('api/scan_recipe.php', { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { const data = result.data; if (data.name) app.dom.recipeNameInput.value = data.name; if (data.instructions) app.dom.recipeInstructionsInput.value = data.instructions; if (data.category) app.dom.recipeCategoryInput.value = data.category; if (data.guests) app.dom.guestCountInput.value = data.guests; if (data.ingredients && Array.isArray(data.ingredients)) { app.dom.ingredientsContainer.innerHTML = ''; data.ingredients.forEach(ing => { app.ui.addIngredientRow({ name: ing.name || '', quantity: ing.quantity || '', unit: ing.unit || 'g' }); }); } } else { alert('AI scan error: ' + result.error); } } catch (error) { console.error('Error during AI scan:', error); alert('An error occurred during AI scan.'); } finally { app.dom.aiScanBtn.disabled = false; app.dom.aiScanLoading.classList.add('d-none'); } }); 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 in recipe name, category, guest count, and at least one ingredient before saving.'); return; } const formData = new FormData(); formData.append('name', recipeData.name); formData.append('instructions', recipeData.instructions); formData.append('guests', recipeData.guests); formData.append('category', recipeData.category); formData.append('ingredients', JSON.stringify(recipeData.ingredients)); const recipeId = app.dom.recipeIdInput.value; if (recipeId) { formData.append('id', recipeId); } const imageInput = document.getElementById('recipeImage'); if (imageInput.files[0]) { formData.append('image', imageInput.files[0]); } const data = await app.api.saveRecipe(formData); 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.closest('button'); const checkbox = e.target.closest('.select-recipe'); const cardCol = e.target.closest('.col-12[data-id]'); if (!cardCol) return; const recipeId = cardCol.getAttribute('data-id'); if (checkbox) { const card = cardCol.querySelector('.card'); if (checkbox.checked) { if (!app.state.selectedRecipeIds.includes(recipeId)) { app.state.selectedRecipeIds.push(recipeId); } card.classList.add('selected'); } else { const index = app.state.selectedRecipeIds.indexOf(recipeId); if (index > -1) { app.state.selectedRecipeIds.splice(index, 1); } const indexNum = app.state.selectedRecipeIds.indexOf(Number(recipeId)); if (indexNum > -1) { app.state.selectedRecipeIds.splice(indexNum, 1); } card.classList.remove('selected'); } app.ui.updateShoppingList(); app.api.saveShoppingList(); return; // Don't trigger other actions } if (target && target.classList.contains('delete-recipe')) { if (confirm('Are you sure you want to delete this recipe?')) { app.api.deleteRecipe(recipeId); } } if (target && target.classList.contains('edit-recipe')) { app.ui.populateFormForEdit(recipeId); } if (target && target.classList.contains('view-recipe')) { app.ui.populateViewModal(recipeId); } }); app.dom.recipeCardsContainer.addEventListener('input', function(e) { if (e.target.classList.contains('recipe-guests-input') || e.target.classList.contains('recipe-portions-input')) { app.ui.updateShoppingList(); } }); 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); const itemKey = e.target.dataset.key; if (e.target.checked) { if (!app.state.checkedItems.includes(itemKey)) { app.state.checkedItems.push(itemKey); } } else { const index = app.state.checkedItems.indexOf(itemKey); if (index > -1) { app.state.checkedItems.splice(index, 1); } } app.ui.saveCheckedItems(); } } 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: 'Add. product' }; app.state.additionalProducts.push(productToModify); } productToModify.quantity++; app.ui.updateShoppingList(); app.api.saveShoppingList(); } else if (e.target.matches('.decrement-item')) { const key = e.target.dataset.key; const [name, unit] = key.split('|'); // Find or create the item in additionalProducts to track adjustments 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: 'Add. product' }; app.state.additionalProducts.push(productToModify); } // Check if this ingredient is part of any recipe let isRecipeIngredient = false; let recipeNameForModal = ''; for (const recipe of app.state.recipes) { if (recipe.ingredients.some(ing => ing.name.toLowerCase() === name.toLowerCase() && ing.unit.toLowerCase() === unit.toLowerCase())) { isRecipeIngredient = true; recipeNameForModal = recipe.name; // Just need one name for the modal break; } } // If it's a recipe ingredient and we're about to remove from the recipe's contribution if (isRecipeIngredient && productToModify.quantity <= 0) { const confirmationKey = `${name.toLowerCase()}-${unit.toLowerCase()}`; if (app.state.confirmedRecipeProducts.includes(confirmationKey)) { productToModify.quantity--; app.ui.updateShoppingList(); app.api.saveShoppingList(); } else { document.getElementById('modal-recipe-name').textContent = recipeNameForModal; document.getElementById('modal-ingredient-name').textContent = productToModify.name; const confirmModal = new bootstrap.Modal(document.getElementById('confirmRemoveModal')); confirmModal.show(); document.getElementById('confirm-remove-btn').onclick = () => { app.state.confirmedRecipeProducts.push(confirmationKey); productToModify.quantity--; app.ui.updateShoppingList(); app.api.saveShoppingList(); confirmModal.hide(); }; } } else if (productToModify.quantity > 0) { // It's not a recipe ingredient about to be removed, but has been added via '+' productToModify.quantity--; app.ui.updateShoppingList(); app.api.saveShoppingList(); } // If not a recipe ingredient and quantity is 0, do nothing. } }); 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 => { const recipeName = recipe.name.toLowerCase(); const ingredients = recipe.ingredients.map(ing => ing.name.toLowerCase()).join(' '); return recipeName.includes(searchTerm) || ingredients.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'); } } }); 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 in all fields with valid values.'); 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: 'Add. product', category: category }; app.state.additionalProducts.push(newProduct); } app.ui.updateShoppingList(); app.api.saveShoppingList(); // Reset form app.dom.productNameInput.value = ''; app.dom.productQuantityInput.value = '1'; app.dom.productCategory.value = 'Food'; 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'), recipeInstructionsInput: document.getElementById('recipeInstructions'), 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'), recipeImage: document.getElementById('recipeImage'), aiScanBtn: document.getElementById('ai-scan-btn'), aiScanLoading: document.getElementById('ai-scan-loading'), 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.loadCheckedItems(); app.events.attachEventListeners(); app.dom.cancelEditBtn.style.display = 'none'; app.ui.addIngredientRow(); app.api.checkAuth().then(() => { app.ui.updateAuthNav(); app.api.getRecipes().then(() => { app.ui.renderRecipeCards(app.state.recipes); app.ui.updateShoppingList(); }); }); } }; document.addEventListener('DOMContentLoaded', app.init);