792 lines
37 KiB
JavaScript
792 lines
37 KiB
JavaScript
const app = {
|
|
dom: {},
|
|
state: {
|
|
recipes: [],
|
|
confirmedRecipeProducts: [],
|
|
checkedItems: [],
|
|
additionalProducts: []
|
|
},
|
|
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 = '<div class="col-12"><p class="text-center text-danger">Error loading recipes.</p></div>';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
app.dom.recipeCardsContainer.innerHTML = '<div class="col-12"><p class="text-center text-danger">Could not connect to the server.</p></div>';
|
|
}
|
|
},
|
|
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) {
|
|
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 = '<div class="col-12"><p class="text-center text-muted">Your saved recipes will appear here.</p></div>';
|
|
return;
|
|
}
|
|
|
|
recipes.forEach((recipe, index) => {
|
|
const cardCol = document.createElement('div');
|
|
cardCol.className = 'col-12 mb-3 recipe-card-enter';
|
|
cardCol.setAttribute('data-id', recipe.id);
|
|
cardCol.style.animationDelay = `${index * 0.1}s`;
|
|
|
|
const card = document.createElement('div');
|
|
card.className = 'card h-100';
|
|
|
|
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';
|
|
|
|
const titleWrapper = document.createElement('div');
|
|
titleWrapper.className = 'd-flex justify-content-between align-items-start mb-2 gap-2';
|
|
|
|
const title = document.createElement('h5');
|
|
title.className = 'card-title mb-0';
|
|
title.textContent = recipe.name;
|
|
titleWrapper.appendChild(title);
|
|
|
|
if (recipe.category) {
|
|
const categoryLabel = document.createElement('div');
|
|
categoryLabel.className = 'recipe-category-label';
|
|
categoryLabel.textContent = recipe.category;
|
|
titleWrapper.appendChild(categoryLabel);
|
|
}
|
|
|
|
const text = document.createElement('p');
|
|
text.className = 'card-text text-muted';
|
|
text.textContent = `Serves ${recipe.guests} | ${recipe.ingredients.length} ingredients`;
|
|
|
|
const guestPortionControls = document.createElement('div');
|
|
guestPortionControls.className = 'row g-2 mb-3';
|
|
guestPortionControls.innerHTML = `
|
|
<div class="col">
|
|
<label for="recipe-guests-${recipe.id}" class="form-label form-label-sm">Guests</label>
|
|
<input type="number" id="recipe-guests-${recipe.id}" class="form-control form-control-sm recipe-guests-input" value="${recipe.guests}" min="1" data-id="${recipe.id}">
|
|
</div>
|
|
<div class="col">
|
|
<label for="recipe-portions-${recipe.id}" class="form-label form-label-sm">Portions</label>
|
|
<input type="number" id="recipe-portions-${recipe.id}" class="form-control form-control-sm recipe-portions-input" value="1" min="1" data-id="${recipe.id}">
|
|
</div>
|
|
`;
|
|
|
|
const buttonGroup = document.createElement('div');
|
|
buttonGroup.className = 'mt-auto pt-2';
|
|
buttonGroup.innerHTML = `
|
|
<button class="btn btn-light btn-sm view-recipe"><i class="bi bi-eye"></i> View</button>
|
|
<button class="btn btn-light btn-sm edit-recipe"><i class="bi bi-pencil"></i> Edit</button>
|
|
<button class="btn btn-danger btn-sm delete-recipe" title="Delete"><i class="bi bi-trash"></i></button>
|
|
`;
|
|
|
|
cardBody.appendChild(titleWrapper);
|
|
cardBody.appendChild(text);
|
|
cardBody.appendChild(guestPortionControls);
|
|
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 => {
|
|
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 || 'Additional 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': { ingredients: [] },
|
|
'Drinks': { ingredients: [] },
|
|
'Cooking and serving': { ingredients: [] },
|
|
'Tableware and 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 += `<h4 class="mt-3">${groupName}</h4>`;
|
|
html += '<ul class="list-group list-group-flush">';
|
|
group.ingredients.forEach((item, index) => {
|
|
const totalQty = item.recipeQty + item.additionalQty;
|
|
if (totalQty <= 0) return;
|
|
|
|
const quantityStr = Number.isInteger(totalQty) ? totalQty : parseFloat(totalQty.toFixed(2));
|
|
const uniqueId = `shopping-item-${groupName.replace(/\s/g, '-')}-${index}`;
|
|
const tooltipText = item.sources.join(', ');
|
|
const itemKey = `${item.name.toLowerCase()}|${item.unit.toLowerCase()}`;
|
|
const isChecked = app.state.checkedItems.includes(itemKey);
|
|
|
|
html += `<li class="list-group-item d-flex justify-content-between align-items-center ${isChecked ? 'checked' : ''}">
|
|
<div class="form-check d-flex align-items-center">
|
|
<input class="form-check-input" type="checkbox" id="${uniqueId}" data-key="${itemKey}" ${isChecked ? 'checked' : ''}>
|
|
<label class="form-check-label ms-2" for="${uniqueId}">
|
|
${item.name}
|
|
</label>
|
|
<i class="bi bi-info-circle ms-2" data-bs-toggle="tooltip" data-bs-placement="top" title="${tooltipText}"></i>
|
|
</div>`;
|
|
|
|
html += `<div>
|
|
<button class="btn btn-quantity-modifier btn-quantity-minus decrement-item" data-key="${itemKey}">-</button>
|
|
<span class="badge bg-custom-green rounded-pill mx-2">${quantityStr} ${item.unit}</span>
|
|
<button class="btn btn-quantity-modifier btn-quantity-plus increment-item" data-key="${itemKey}">+</button>
|
|
</div>`;
|
|
|
|
html += `</li>`;
|
|
});
|
|
html += '</ul>';
|
|
}
|
|
}
|
|
|
|
if (totalIngredients === 0) {
|
|
html += '<div class="text-center text-muted p-4"><p>Your shopping list is empty. Add a recipe, and its ingredients will appear here.</p></div>';
|
|
}
|
|
|
|
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 =>
|
|
`<button type="button" class="btn ${ingredient.unit === u ? 'btn-secondary' : 'btn-outline-secondary'} unit-btn">${u}</button>`
|
|
).join('');
|
|
|
|
row.innerHTML = `
|
|
<div class="mb-2">
|
|
<input type="text" class="form-control" placeholder="Ingredient Name" aria-label="Ingredient Name" value="${ingredient.name}">
|
|
</div>
|
|
<div class="d-flex align-items-center">
|
|
<input type="number" class="form-control me-2" placeholder="Qty" aria-label="Quantity" min="0" step="any" value="${ingredient.quantity}" style="width: 100px;">
|
|
<div class="btn-group unit-selector me-2" role="group" aria-label="Unit selector">
|
|
${unitButtons}
|
|
</div>
|
|
<button type="button" class="btn btn-danger btn-sm remove-ingredient ms-auto">×</button>
|
|
</div>
|
|
`;
|
|
app.dom.ingredientsContainer.appendChild(row);
|
|
},
|
|
clearForm() {
|
|
app.dom.recipeIdInput.value = '';
|
|
app.dom.recipeNameInput.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 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();
|
|
},
|
|
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 || 'No category';
|
|
document.getElementById('view-recipe-guests').textContent = recipe.guests;
|
|
|
|
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 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);
|
|
}
|
|
},
|
|
loadCheckedItems() {
|
|
const checkedItems = localStorage.getItem('checkedItems');
|
|
if (checkedItems) {
|
|
app.state.checkedItems = JSON.parse(checkedItems);
|
|
}
|
|
},
|
|
saveCheckedItems() {
|
|
localStorage.setItem('checkedItems', JSON.stringify(app.state.checkedItems));
|
|
}
|
|
},
|
|
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 formData = new FormData();
|
|
formData.append('name', recipeData.name);
|
|
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 card = e.target.closest('.col-12[data-id]');
|
|
if (!card) return;
|
|
|
|
const recipeId = card.getAttribute('data-id');
|
|
|
|
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: '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('|');
|
|
|
|
// 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: 'Additional 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();
|
|
} 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();
|
|
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();
|
|
}
|
|
// 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 out 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: 'Additional product', 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 = '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();
|
|
});
|
|
|
|
app.dom.musicToggle.addEventListener('click', function() {
|
|
if (app.dom.christmasMusic.paused) {
|
|
app.dom.christmasMusic.play();
|
|
app.dom.musicToggle.innerHTML = '<i class="bi bi-pause-fill" style="font-size: 1.5rem;"></i>';
|
|
} else {
|
|
app.dom.christmasMusic.pause();
|
|
app.dom.musicToggle.innerHTML = '<i class="bi bi-play-fill" style="font-size: 1.5rem;"></i>';
|
|
}
|
|
});
|
|
|
|
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'),
|
|
recipeImage: document.getElementById('recipeImage'),
|
|
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'),
|
|
christmasMusic: document.getElementById('christmas-music'),
|
|
musicToggle: document.getElementById('music-toggle'),
|
|
|
|
};
|
|
|
|
app.ui.createSnowflakes();
|
|
app.ui.loadCheckedItems();
|
|
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); |