diff --git a/api/save_recipe.php b/api/save_recipe.php index dbd8455..e6f783b 100644 --- a/api/save_recipe.php +++ b/api/save_recipe.php @@ -14,13 +14,31 @@ $pdo = db(); try { $pdo->beginTransaction(); - $stmt = $pdo->prepare("INSERT INTO recipes (name, guests) VALUES (?, ?)"); - $stmt->execute([$data['name'], $data['guests']]); - $recipeId = $pdo->lastInsertId(); + if (isset($data['id']) && !empty($data['id'])) { + // Update existing recipe + $recipeId = $data['id']; + $stmt = $pdo->prepare("UPDATE recipes SET name = ?, guests = ? WHERE id = ?"); + $stmt->execute([$data['name'], $data['guests'], $recipeId]); - $stmt = $pdo->prepare("INSERT INTO ingredients (recipe_id, name, quantity, unit) VALUES (?, ?, ?, ?)"); - foreach ($data['ingredients'] as $ing) { - $stmt->execute([$recipeId, $ing['name'], $ing['quantity'], $ing['unit']]); + // Easiest way to handle ingredients is to delete old ones and insert new ones + $stmt = $pdo->prepare("DELETE FROM ingredients WHERE recipe_id = ?"); + $stmt->execute([$recipeId]); + + $stmt = $pdo->prepare("INSERT INTO ingredients (recipe_id, name, quantity, unit) VALUES (?, ?, ?, ?)"); + foreach ($data['ingredients'] as $ing) { + $stmt->execute([$recipeId, $ing['name'], $ing['quantity'], $ing['unit']]); + } + + } else { + // Insert new recipe + $stmt = $pdo->prepare("INSERT INTO recipes (name, guests) VALUES (?, ?)"); + $stmt->execute([$data['name'], $data['guests']]); + $recipeId = $pdo->lastInsertId(); + + $stmt = $pdo->prepare("INSERT INTO ingredients (recipe_id, name, quantity, unit) VALUES (?, ?, ?, ?)"); + foreach ($data['ingredients'] as $ing) { + $stmt->execute([$recipeId, $ing['name'], $ing['quantity'], $ing['unit']]); + } } $pdo->commit(); diff --git a/assets/css/custom.css b/assets/css/custom.css index bec5b1d..d3f6b5c 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -1,6 +1,6 @@ /* General Body Styles */ body { - background-color: #0a2e36; /* Dark teal background */ + background-color: #013617; /* Dark green background */ color: #ffffff; /* White text */ font-family: 'Poppins', sans-serif; padding-top: 40px; /* Make space for garland */ @@ -11,67 +11,13 @@ h1, h2, h3, h4, h5, h6 { color: #ffffff !important; /* Use !important to override other styles if necessary */ } -/* Christmas Tree */ -.christmas-tree-container { - position: fixed; - bottom: 0; - width: 100px; - height: 150px; - z-index: 100; +/* Buttons and Inputs */ +input, +button, +.btn { + border-radius: 8px !important; } -.christmas-tree-container.left { - left: 20px; -} - -.christmas-tree-container.right { - right: 20px; -} - -.tree { - position: relative; - width: 100%; - height: 100%; -} - -.tree::before { /* The tree itself */ - content: ''; - position: absolute; - bottom: 30px; /* Height of the trunk */ - left: 0; - width: 0; - height: 0; - border-left: 50px solid transparent; - border-right: 50px solid transparent; - border-bottom: 120px solid #2C5F2D; /* Dark green */ -} - -.tree::after { /* The trunk */ - content: ''; - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - width: 20px; - height: 30px; - background: #5C3D2E; /* Brown */ -} - -.ornament { - position: absolute; - width: 10px; - height: 10px; - border-radius: 50%; - background: #ff6f61; /* Coral red */ -} - -.ornament.o1 { top: 50px; left: 45px; } -.ornament.o2 { top: 70px; left: 30px; background: #ffff24; } -.ornament.o3 { top: 75px; left: 60px; background: #2424ff; } -.ornament.o4 { top: 95px; left: 40px; } -.ornament.o5 { top: 100px; left: 15px; background: #24ff24;} -.ornament.o6 { top: 105px; left: 70px; background: #ff24ff;} - /* Garland */ body::before { @@ -82,9 +28,9 @@ body::before { width: 110%; height: 20px; background: - radial-gradient(circle, #ff2424 4px, transparent 5px), - radial-gradient(circle, #24ff24 4px, transparent 5px), - radial-gradient(circle, #2424ff 4px, transparent 5px), + radial-gradient(circle, #FF3E1F 4px, transparent 5px), + radial-gradient(circle, #013617 4px, transparent 5px), + radial-gradient(circle, #FFAFCA 4px, transparent 5px), radial-gradient(circle, #ffff24 4px, transparent 5px), radial-gradient(circle, #ff24ff 4px, transparent 5px); background-size: 100px 20px; @@ -103,7 +49,7 @@ body::before { /* Navbar */ .navbar { - background-color: rgba(10, 46, 54, 0.8) !important; /* Semi-transparent dark teal */ + background-color: rgba(1, 54, 23, 0.8) !important; /* Semi-transparent dark green */ backdrop-filter: blur(10px); border-bottom: 1px solid rgba(255, 255, 255, 0.1); } @@ -143,13 +89,12 @@ body::before { background-color: rgba(0, 0, 0, 0.2); border: 1px solid rgba(255, 255, 255, 0.1); color: #ffffff; - border-radius: 8px; } .form-control:focus { background-color: rgba(0, 0, 0, 0.3); - border-color: #ff6f61; /* Coral red accent */ - box-shadow: 0 0 0 0.25rem rgba(255, 111, 97, 0.25); + border-color: #FF3E1F; /* Coral red accent */ + box-shadow: 0 0 0 0.25rem rgba(255, 62, 31, 0.25); color: #ffffff; } @@ -159,19 +104,18 @@ body::before { /* Buttons */ .btn-primary { - background-color: #ff6f61; /* Coral red */ - border-color: #ff6f61; + background-color: #FF3E1F; /* Coral red */ + border-color: #FF3E1F; font-weight: 600; padding: 12px 30px; - border-radius: 50px; transition: all 0.3s ease; } .btn-primary:hover { - background-color: #e65a50; - border-color: #e65a50; + background-color: #E6381A; + border-color: #E6381A; transform: translateY(-2px); - box-shadow: 0 4px 15px rgba(255, 111, 97, 0.2); + box-shadow: 0 4px 15px rgba(255, 62, 31, 0.2); } .btn-outline-secondary { @@ -179,7 +123,6 @@ body::before { color: #ffffff; font-weight: 600; padding: 12px 30px; - border-radius: 50px; transition: all 0.3s ease; } @@ -208,6 +151,36 @@ body::before { margin-bottom: 15px; } +/* 3-Column Layout Adjustments */ +.row.g-4 > [class*='col-'] .card { + height: 100%; /* Make cards in columns equal height */ +} + +#recipe-cards-container, #shopping-list-container { + max-height: 60vh; /* Adjust as needed */ + overflow-y: auto; + padding: 10px; +} + +/* Custom scrollbar for webkit browsers */ +#recipe-cards-container::-webkit-scrollbar, #shopping-list-container::-webkit-scrollbar { + width: 8px; +} + +#recipe-cards-container::-webkit-scrollbar-track, #shopping-list-container::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.2); + border-radius: 4px; +} + +#recipe-cards-container::-webkit-scrollbar-thumb, #shopping-list-container::-webkit-scrollbar-thumb { + background: rgba(255, 62, 31, 0.5); /* Coral red, semi-transparent */ + border-radius: 4px; +} + +#recipe-cards-container::-webkit-scrollbar-thumb:hover, #shopping-list-container::-webkit-scrollbar-thumb:hover { + background: #FF3E1F; /* Coral red */ +} + /* Footer */ footer.bg-light { background-color: transparent !important; @@ -234,7 +207,7 @@ footer.bg-light { border-radius: 50%; opacity: 0.8; pointer-events: none; - animation: fall linear infinite; +animation: fall linear infinite; } @keyframes fall { @@ -243,3 +216,100 @@ footer.bg-light { opacity: 0; } } + +/* Shopping List Checkbox */ +.list-group-item.checked .form-check-label { + text-decoration: line-through; + opacity: 0.6; +} + +.unit-btn { + padding: 0.375rem 0.5rem; + font-size: 0.875rem; +} + +/* Adjust padding for the remove button in the ingredient row */ +.ingredient-row .remove-ingredient { + padding: 0.375rem 0.75rem; +} + +/* Custom Shopping List Styles */ +.col-md-4:has(#shopping-list-container) .card { + background-color: transparent; + box-shadow: none !important; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +#shopping-list-container .list-group-item { + background-color: transparent; + color: #ffffff; + border-color: rgba(255, 255, 255, 0.1) !important; +} + +/* Custom Checkbox Styles */ +.form-check-input { + background-color: transparent; + border: 1px solid #ffffff; +} + +.form-check-input:checked { + background-color: #FF3E1F; + border-color: #FF3E1F; +} + +.form-check-input:focus { + border-color: #FF3E1F; + box-shadow: 0 0 0 0.25rem rgba(255, 62, 31, 0.25); +} + +@media print { + body * { + visibility: hidden; + } + #shopping-list-container, #shopping-list-container * { + visibility: visible; + } + #shopping-list-container { + position: absolute; + left: 0; + top: 0; + width: 100%; + } + .card { + border: none !important; + box-shadow: none !important; + } + .list-group-item { + color: #000 !important; + background-color: #fff !important; + } + .badge { + color: #000 !important; + background-color: #fff !important; + border: 1px solid #ccc; + } + .form-check-input { + border: 1px solid #000 !important; + } + h2, h3 { + color: #000 !important; + } +} + +.bg-custom-green { + background-color: #013617 !important; +} + +/* Modal Styles */ +#recipe-form-modal .modal-content { + background-color: #013617; + color: white; +} + +#recipe-form-modal .modal-header { + border-bottom: 1px solid rgba(255, 255, 255, 0.2); +} + +#recipe-form-modal .btn-close { + filter: invert(1); +} diff --git a/assets/js/main.js b/assets/js/main.js index b02f1b0..b1bf3d6 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,319 +1,466 @@ -document.addEventListener('DOMContentLoaded', function () { - - // --- Snowflakes Effect --- - function 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; // size from 2px to 6px - snowflake.style.width = `${size}px`; - snowflake.style.height = `${size}px`; - - snowflake.style.left = Math.random() * 100 + 'vw'; - - const animationDuration = Math.random() * 5 + 5; // 5 to 10 seconds - snowflake.style.animationDuration = `${animationDuration}s`; - - const animationDelay = Math.random() * 5; // 0 to 5 seconds - snowflake.style.animationDelay = `${animationDelay}s`; - - snowflake.style.opacity = Math.random() * 0.7 + 0.3; // 0.3 to 1.0 - - snowContainer.appendChild(snowflake); - } - } - createSnowflakes(); - - // --- DOM Elements --- - const recipeNameInput = document.getElementById('recipeName'); - const guestCountInput = document.getElementById('guestCount'); - const ingredientsContainer = document.getElementById('ingredients-container'); - const addIngredientBtn = document.getElementById('add-ingredient'); - const calculateBtn = document.getElementById('calculate-btn'); - const newRecipeBtn = document.getElementById('new-recipe-btn'); - const shoppingListContainer = document.getElementById('shopping-list-container'); - const recipeCardsContainer = document.getElementById('recipe-cards-container'); - - // --- Core Functions --- - - async function loadRecipes() { - try { - const response = await fetch('api/get_recipes.php'); - const data = await response.json(); - if (data.success) { - renderRecipeCards(data.recipes); - } else { - console.error('Failed to load recipes:', data.error); - recipeCardsContainer.innerHTML = '

Error loading recipes.

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

Could not connect to the server.

'; - } - } - - function addIngredientRow(ingredient = { name: '', quantity: '', unit: '' }) { - const row = document.createElement('div'); - row.className = 'ingredient-row mb-2'; - row.innerHTML = ` - - - - - `; - ingredientsContainer.appendChild(row); - } - - function clearForm() { - recipeNameInput.value = ''; - guestCountInput.value = '1'; - ingredientsContainer.innerHTML = ''; - addIngredientRow(); - shoppingListContainer.innerHTML = ` -
-

Your Shopping List

-

Your calculated list will appear here.

-
- `; - } - - function renderRecipeCards(recipes) { - recipeCardsContainer.innerHTML = ''; - if (!recipes || recipes.length === 0) { - recipeCardsContainer.innerHTML = '

Здесь будут появляться ваши сохраненные рецепты.

'; - return; - } - - recipes.forEach(recipe => { - const cardCol = document.createElement('div'); - cardCol.className = 'col-lg-4 col-md-6 mb-4'; - 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 subtitle = document.createElement('h6'); - subtitle.className = 'card-subtitle mb-2 text-muted'; - subtitle.textContent = `${recipe.guests} guest(s)`; - - const text = document.createElement('p'); - text.className = 'card-text'; - text.textContent = `${recipe.ingredients.length} ingredients`; - - const buttonGroup = document.createElement('div'); - buttonGroup.className = 'mt-auto'; - buttonGroup.innerHTML = ` - - - `; - - cardBody.appendChild(title); - cardBody.appendChild(subtitle); - cardBody.appendChild(text); - cardBody.appendChild(buttonGroup); - card.appendChild(cardBody); - cardCol.appendChild(card); - recipeCardsContainer.appendChild(cardCol); - }); - } - - function renderShoppingList(list) { - let html = '

Общий список покупок


'; - if (list.length === 0) { - html += '

Нет ингредиентов для расчета.

'; - } else { - html += ''; - } - shoppingListContainer.innerHTML = html; - } - - function getRecipeDataFromForm() { - const recipeName = recipeNameInput.value.trim(); - const guests = parseInt(guestCountInput.value, 10) || 0; - - const ingredients = []; - const rows = 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 unit = row.querySelector('input[placeholder="Unit (e.g., grams, ml)"]').value.trim(); - 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; - } - - // --- Event Listeners --- - - addIngredientBtn.addEventListener('click', () => addIngredientRow()); - - ingredientsContainer.addEventListener('click', function(e) { - if (e.target.classList.contains('remove-ingredient')) { - e.target.closest('.ingredient-row').remove(); - } - }); - - recipeCardsContainer.addEventListener('click', async function(e) { - const target = e.target; - const card = target.closest('.col-lg-4'); - if (!card) return; - - const recipeId = card.getAttribute('data-id'); - - if (target.classList.contains('delete-recipe-btn')) { - if (confirm('Are you sure you want to delete this recipe?')) { - try { - const response = await fetch('api/delete_recipe.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id: recipeId }) - }); - const data = await response.json(); - if (data.success) { - card.remove(); - } else { - alert('Failed to delete recipe: ' + data.error); - } - } catch (error) { - alert('Error: ' + error.message); +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 = '

Error loading recipes.

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

Could not connect to the server.

'; } - } - - if (target.classList.contains('edit-recipe-btn')) { - // Find the recipe data from the currently rendered cards - const response = await fetch('api/get_recipes.php'); - const data = await response.json(); - if(!data.success) return; - const recipeToEdit = data.recipes.find(r => r.id == recipeId); - - if (recipeToEdit) { - // Populate form - recipeNameInput.value = recipeToEdit.name; - guestCountInput.value = recipeToEdit.guests; - ingredientsContainer.innerHTML = ''; - recipeToEdit.ingredients.forEach(ing => addIngredientRow(ing)); - - // Delete the old recipe from DB - try { - await fetch('api/delete_recipe.php', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ id: recipeId }) - }); - card.remove(); // Remove from UI immediately - } catch (error) { - alert('Error preparing for edit: ' + error.message); - } - } - } - }); - - newRecipeBtn.addEventListener('click', async function() { - const recipeData = getRecipeDataFromForm(); - if (recipeData) { + }, + 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 loadRecipes(); // Reload all recipes to show the new one - clearForm(); + await app.api.getRecipes(); + app.ui.renderRecipeCards(app.state.recipes); + app.ui.updateShoppingList(); } else { - alert('Failed to save recipe: ' + data.error); + alert('Failed to delete recipe: ' + data.error); } } catch (error) { alert('Error: ' + error.message); } - } else { - alert('Please fill out the recipe name, guests, and at least one ingredient before saving.'); } - }); + }, + ui: { + renderRecipeCards(recipes) { + app.dom.recipeCardsContainer.innerHTML = ''; + if (!recipes || recipes.length === 0) { + app.dom.recipeCardsContainer.innerHTML = '

Your saved recipes will appear here.

'; + return; + } - calculateBtn.addEventListener('click', async function() { - try { - const response = await fetch('api/get_recipes.php'); - const data = await response.json(); - if (!data.success) { - alert('Could not get recipes for calculation.'); - return; - } - - const allRecipesToCalculate = data.recipes; - const currentRecipe = getRecipeDataFromForm(); - - if (currentRecipe) { - // Give it a temporary ID to avoid collisions - currentRecipe.id = 'current'; - allRecipesToCalculate.push(currentRecipe); - } - - if (allRecipesToCalculate.length === 0) { - alert('There are no recipes to calculate. Please fill out the form or save a recipe.'); - return; - } - - const combinedIngredients = new Map(); - - allRecipesToCalculate.forEach(recipe => { - const multiplier = recipe.guests; - recipe.ingredients.forEach(ing => { - const ingName = ing.name || ''; - const ingUnit = ing.unit || ''; - const key = `${ingName.trim().toLowerCase()}|${ingUnit.trim().toLowerCase()}`; - const existing = combinedIngredients.get(key); - + 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 = ` + + + `; + + 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 += (ing.quantity || 0) * multiplier; + existing.qty += prod.quantity; } else { - combinedIngredients.set(key, { - name: ing.name, - qty: (ing.quantity || 0) * multiplier, - unit: ing.unit + 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 += `

${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; + }, + 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.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'); + } + } }); - renderShoppingList(Array.from(combinedIngredients.values())); + 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; + } - } catch(error) { - alert('Calculation Error: ' + error.message); + 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')) + }; - // --- Initial State --- - addIngredientRow(); - loadRecipes(); -}); + 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); \ No newline at end of file diff --git a/assets/pasted-20251109-215032-744d90f1.jpg b/assets/pasted-20251109-215032-744d90f1.jpg new file mode 100644 index 0000000..d65a91f Binary files /dev/null and b/assets/pasted-20251109-215032-744d90f1.jpg differ diff --git a/assets/vm-shot-2025-11-09T21-50-13-547Z.jpg b/assets/vm-shot-2025-11-09T21-50-13-547Z.jpg new file mode 100644 index 0000000..246a408 Binary files /dev/null and b/assets/vm-shot-2025-11-09T21-50-13-547Z.jpg differ diff --git a/index.php b/index.php index eb2908f..d48b91c 100644 --- a/index.php +++ b/index.php @@ -26,6 +26,7 @@ + @@ -33,28 +34,6 @@
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-