35604-vm/assets/js/main.js
Flatlogic Bot 32cc8d6cd2 4
2025-11-12 19:46:05 +00:00

466 lines
21 KiB
JavaScript

const app = {
dom: {},
state: {
recipes: [],
},
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(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 = '<div class="col-12"><p class="text-center text-muted">Your saved recipes will appear here.</p></div>';
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';
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 = `
<button class="btn btn-light btn-sm edit-recipe"><i class="bi bi-pencil"></i> Edit</button>
<button class="btn btn-outline-danger btn-sm delete-recipe">Delete</button>
`;
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 groups = {
Weight: { units: ['g', 'kg'], ingredients: new Map() },
Volume: { units: ['ml', 'l'], ingredients: new Map() },
Count: { units: ['piece', 'pack'], ingredients: new Map() },
Other: { units: [], ingredients: new Map() }
};
app.state.recipes.forEach(recipe => {
if (recipe.ingredients) {
recipe.ingredients.forEach(ing => {
const ingName = ing.name || '';
const ingUnit = ing.unit || '';
const key = `${ingName.trim().toLowerCase()}|${ingUnit.trim().toLowerCase()}`;
let groupName = 'Other';
for (const name in groups) {
if (groups[name].units.includes(ingUnit)) {
groupName = name;
break;
}
}
const existing = groups[groupName].ingredients.get(key);
if (existing) {
existing.qty += (ing.quantity || 0);
} else {
groups[groupName].ingredients.set(key, {
name: ing.name,
qty: (ing.quantity || 0),
unit: ing.unit
});
}
});
}
});
let html = '';
let totalIngredients = 0;
if (app.state.additionalProducts) {
app.state.additionalProducts.forEach(prod => {
const key = `${prod.name.trim().toLowerCase()}|${prod.unit.trim().toLowerCase()}`;
let groupName = 'Other';
for (const name in groups) {
if (groups[name].units.includes(prod.unit)) {
groupName = name;
break;
}
}
const existing = groups[groupName].ingredients.get(key);
if (existing) {
existing.qty += prod.quantity;
} else {
groups[groupName].ingredients.set(key, {
name: prod.name,
qty: prod.quantity,
unit: prod.unit
});
}
});
}
for (const groupName in groups) {
const group = groups[groupName];
const ingredientList = Array.from(group.ingredients.values());
if (ingredientList.length > 0) {
totalIngredients += ingredientList.length;
html += `<h4 class="mt-3">${groupName}</h4>`;
html += '<ul class="list-group list-group-flush">';
ingredientList.forEach((item, index) => {
const totalQty = item.qty * totalMultiplier;
const quantityStr = Number.isInteger(totalQty) ? totalQty : parseFloat(totalQty.toFixed(2));
const uniqueId = `shopping-item-${groupName}-${index}`;
html += `<li class="list-group-item d-flex justify-content-between align-items-center">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="${uniqueId}">
<label class="form-check-label" for="${uniqueId}">
${item.name}
</label>
</div>
<span class="badge bg-custom-green rounded-pill">${quantityStr} ${item.unit}</span>
</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;
},
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">&times;</button>
</div>
`;
app.dom.ingredientsContainer.appendChild(row);
},
clearForm() {
app.dom.recipeIdInput.value = '';
app.dom.recipeNameInput.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.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 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 };
}
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, 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) {
const savedRecipe = data.recipe;
if (recipeId) {
const index = app.state.recipes.findIndex(r => r.id == recipeId);
if (index !== -1) {
app.state.recipes[index] = savedRecipe;
}
const card = app.dom.recipeCardsContainer.querySelector(`[data-id="${recipeId}"]`);
if (card) {
card.querySelector('.card-title').textContent = savedRecipe.name;
card.querySelector('.card-text').textContent = `${savedRecipe.ingredients.length} ingredients`;
}
} else {
app.state.recipes.unshift(savedRecipe);
app.ui.renderRecipeCards(app.state.recipes);
}
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('change', 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);
}
}
});
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.printShoppingListBtn.addEventListener('click', function() {
window.print();
});
app.dom.addProductBtn.addEventListener('click', () => {
const name = prompt('Enter product name:');
if (!name) return;
const quantity = parseFloat(prompt('Enter quantity:'));
if (isNaN(quantity) || quantity <= 0) {
alert('Please enter a valid quantity.');
return;
}
const unit = prompt('Enter unit (e.g., g, kg, ml, l, piece, pack):');
if (!unit) return;
if (!app.state.additionalProducts) {
app.state.additionalProducts = [];
}
app.state.additionalProducts.push({
name: name.trim(),
quantity: quantity,
unit: unit.trim()
});
app.ui.updateShoppingList();
});
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'),
recipeSearchInput: document.getElementById('recipe-search'),
addProductBtn: document.getElementById('add-product-btn'),
printShoppingListBtn: document.getElementById('print-shopping-list-btn'),
recipeFormModal: new bootstrap.Modal(document.getElementById('recipe-form-modal'))
};
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);