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 = '
';
- }
- } 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 += '';
- list.forEach(item => {
- const quantityStr = Number.isInteger(item.qty) ? item.qty : parseFloat(item.qty.toFixed(2));
- html += `-
- ${item.name}
- ${quantityStr} ${item.unit}
-
`;
- });
- 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 = '';
}
+ } 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 @@
-
-
-
-