diff --git a/api/save_recipe.php b/api/save_recipe.php index 8fb2603..422f7ca 100644 --- a/api/save_recipe.php +++ b/api/save_recipe.php @@ -2,6 +2,17 @@ header('Content-Type: application/json'); require_once __DIR__ . '/../db/config.php'; +function get_ingredient_category($name) { + $name = strtolower($name); + $drink_keywords = ["water", "juice", "milk", "wine", "beer", "soda", "spirit", "vodka", "gin", "rum", "tequila", "whiskey", "liqueur", "coke", "pepsi", "tea", "coffee"]; + foreach ($drink_keywords as $keyword) { + if (strpos($name, $keyword) !== false) { + return 'drink'; + } + } + return 'food'; +} + // The request is now multipart/form-data, so we use $_POST and $_FILES $data = $_POST; $files = $_FILES; @@ -68,9 +79,10 @@ try { } // Insert ingredients - $stmt = $pdo->prepare("INSERT INTO ingredients (recipe_id, name, quantity, unit) VALUES (?, ?, ?, ?)"); + $stmt = $pdo->prepare("INSERT INTO ingredients (recipe_id, name, quantity, unit, category) VALUES (?, ?, ?, ?, ?)"); foreach ($ingredients as $ing) { - $stmt->execute([$recipeId, $ing['name'], $ing['quantity'], $ing['unit']]); + $ingredientCategory = get_ingredient_category($ing['name']); + $stmt->execute([$recipeId, $ing['name'], $ing['quantity'], $ing['unit'], $ingredientCategory]); } $pdo->commit(); diff --git a/assets/css/custom.css b/assets/css/custom.css index 9c35226..0216418 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -328,26 +328,29 @@ footer.bg-light { } /* Modal Styles */ -#recipe-form-modal .modal-content, #add-product-modal .modal-content, #confirmRemoveModal .modal-content { +#recipe-form-modal .modal-content, #add-product-modal .modal-content, #confirmRemoveModal .modal-content, #view-recipe-modal .modal-content { background-color: #142E35; color: white; } #recipe-form-modal .modal-header, #add-product-modal .modal-header, -#confirmRemoveModal .modal-header { +#confirmRemoveModal .modal-header, +#view-recipe-modal .modal-header { border-bottom: 1px solid rgba(255, 255, 255, 0.2); } #recipe-form-modal .modal-footer, #add-product-modal .modal-footer, -#confirmRemoveModal .modal-footer { +#confirmRemoveModal .modal-footer, +#view-recipe-modal .modal-footer { border-top: 1px solid rgba(255, 255, 255, 0.2); } #recipe-form-modal .btn-close, #add-product-modal .btn-close, -#confirmRemoveModal .btn-close { +#confirmRemoveModal .btn-close, +#view-recipe-modal .btn-close { filter: invert(1); } @@ -375,21 +378,16 @@ footer.bg-light { } /* Recipe Card Category Label */ -.card { - position: relative; -} - .recipe-category-label { - position: absolute; - top: 10px; - right: 10px; background-color: #142E35; color: white; padding: 5px 10px; border-radius: 5px; font-size: 0.8em; font-weight: 600; - z-index: 1; + margin-left: 10px; /* Add some space between title and label */ + white-space: nowrap; /* Prevent the label itself from wrapping */ + align-self: flex-start; /* Align to the top of the flex container */ } /* Shopping List Quantity Controls */ @@ -432,13 +430,7 @@ footer.bg-light { object-fit: cover; } -/* Overlay category label */ -.card .recipe-category-label { - top: 15px; - right: 15px; - background-color: rgba(20, 46, 53, 0.8); - backdrop-filter: blur(5px); -} + /* Recipe Card Animation */ .recipe-card-enter { @@ -561,3 +553,21 @@ footer.bg-light { border-color: transparent; opacity: 0.7; } + +.card-title { + white-space: normal; + word-wrap: break-word; +} + +#view-recipe-ingredients { + background-color: rgba(0, 0, 0, 0.2); /* Darker shade */ + padding: 15px; + border-radius: 10px; + margin-top: 10px; +} + +#view-recipe-ingredients .list-group-item { + background-color: transparent; + border: none; + color: #fff; +} diff --git a/assets/js/main.js b/assets/js/main.js index 70f8a5f..f95301e 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -3,6 +3,8 @@ const app = { state: { recipes: [], confirmedRecipeProducts: [], + checkedItems: [], + additionalProducts: [] }, api: { async getRecipes() { @@ -77,23 +79,40 @@ const app = { 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; - card.appendChild(categoryLabel); + titleWrapper.appendChild(categoryLabel); } - 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`; + text.textContent = `Serves ${recipe.guests} | ${recipe.ingredients.length} ingredients`; + + const guestPortionControls = document.createElement('div'); + guestPortionControls.className = 'row g-2 mb-3'; + guestPortionControls.innerHTML = ` +
+ + +
+
+ + +
+ `; const buttonGroup = document.createElement('div'); buttonGroup.className = 'mt-auto pt-2'; @@ -103,8 +122,9 @@ const app = { `; - cardBody.appendChild(title); + cardBody.appendChild(titleWrapper); cardBody.appendChild(text); + cardBody.appendChild(guestPortionControls); cardBody.appendChild(buttonGroup); card.appendChild(cardBody); cardCol.appendChild(card); @@ -112,14 +132,23 @@ const app = { }); }, updateShoppingList() { - const guestCount = parseInt(app.dom.guestCountInput.value, 10) || 1; - const portionsPerGuest = parseInt(app.dom.portionsPerGuestInput.value, 10) || 1; - const totalMultiplier = guestCount * portionsPerGuest; - const combinedIngredients = new Map(); - // 1. Process recipe ingredients + // 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(); @@ -134,11 +163,12 @@ const app = { recipeQty: 0, additionalQty: 0, sources: [], - category: null // For 'pack' items + category: ing.category }); } const item = combinedIngredients.get(key); - item.recipeQty += (ing.quantity || 0); + item.recipeQty += (ing.quantity || 0) * targetGuests * targetPortions; + if (!item.sources.includes(recipe.name)) { item.sources.push(recipe.name); } @@ -161,7 +191,7 @@ const app = { recipeQty: 0, additionalQty: 0, sources: [], - category: null + category: prod.category }); } const item = combinedIngredients.get(key); @@ -170,7 +200,7 @@ const app = { if (!item.sources.includes(source)) { item.sources.push(source); } - if (prod.unit === 'pack' && prod.category) { + if (prod.category && !item.category) { item.category = prod.category; } }); @@ -178,29 +208,24 @@ const app = { // 3. Group for display const groups = { - Food: { units: ['g', 'kg'], ingredients: [] }, - Drinks: { units: ['ml', 'l'], ingredients: [] }, - Count: { units: ['piece'], ingredients: [] }, - "Tableware and consumables": { units: [], ingredients: [] }, - "Cooking and serving": { units: [], ingredients: [] }, - Other: { units: [], ingredients: [] } + 'Food': { ingredients: [] }, + 'Drinks': { ingredients: [] }, + 'Cooking and serving': { ingredients: [] }, + 'Tableware and consumables': { ingredients: [] } }; combinedIngredients.forEach((item, key) => { - let groupName = 'Other'; - if (item.unit === 'pack' && item.category) { - groupName = item.category; - } else { - for (const name in groups) { - if (groups[name].units.includes(item.unit)) { - groupName = name; - break; - } + 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'; } } - if (!groups[groupName]) { // handle dynamic categories from 'pack' - groups[groupName] = { units: [], ingredients: [] }; - } + groups[groupName].ingredients.push(item); }); @@ -215,17 +240,18 @@ const app = { html += `

${groupName}

`; html += '