7
This commit is contained in:
parent
32cc8d6cd2
commit
60e4e3f87d
@ -17,8 +17,9 @@ try {
|
|||||||
if (isset($data['id']) && !empty($data['id'])) {
|
if (isset($data['id']) && !empty($data['id'])) {
|
||||||
// Update existing recipe
|
// Update existing recipe
|
||||||
$recipeId = $data['id'];
|
$recipeId = $data['id'];
|
||||||
$stmt = $pdo->prepare("UPDATE recipes SET name = ?, guests = ? WHERE id = ?");
|
$category = !empty($data['category']) ? $data['category'] : 'No category';
|
||||||
$stmt->execute([$data['name'], $data['guests'], $recipeId]);
|
$stmt = $pdo->prepare("UPDATE recipes SET name = ?, guests = ?, category = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$data['name'], $data['guests'], $category, $recipeId]);
|
||||||
|
|
||||||
// Easiest way to handle ingredients is to delete old ones and insert new ones
|
// Easiest way to handle ingredients is to delete old ones and insert new ones
|
||||||
$stmt = $pdo->prepare("DELETE FROM ingredients WHERE recipe_id = ?");
|
$stmt = $pdo->prepare("DELETE FROM ingredients WHERE recipe_id = ?");
|
||||||
@ -31,8 +32,9 @@ try {
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Insert new recipe
|
// Insert new recipe
|
||||||
$stmt = $pdo->prepare("INSERT INTO recipes (name, guests) VALUES (?, ?)");
|
$category = !empty($data['category']) ? $data['category'] : 'No category';
|
||||||
$stmt->execute([$data['name'], $data['guests']]);
|
$stmt = $pdo->prepare("INSERT INTO recipes (name, guests, category) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([$data['name'], $data['guests'], $category]);
|
||||||
$recipeId = $pdo->lastInsertId();
|
$recipeId = $pdo->lastInsertId();
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO ingredients (recipe_id, name, quantity, unit) VALUES (?, ?, ?, ?)");
|
$stmt = $pdo->prepare("INSERT INTO ingredients (recipe_id, name, quantity, unit) VALUES (?, ?, ?, ?)");
|
||||||
|
|||||||
@ -301,15 +301,72 @@ animation: fall linear infinite;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Modal Styles */
|
/* Modal Styles */
|
||||||
#recipe-form-modal .modal-content {
|
#recipe-form-modal .modal-content, #add-product-modal .modal-content, #confirmRemoveModal .modal-content {
|
||||||
background-color: #013617;
|
background-color: #013617;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
#recipe-form-modal .modal-header {
|
#recipe-form-modal .modal-header, #add-product-modal .modal-header, #confirmationModal .modal-header {
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#recipe-form-modal .btn-close {
|
#recipe-form-modal .btn-close, #add-product-modal .btn-close, #confirmationModal .btn-close {
|
||||||
filter: invert(1);
|
filter: invert(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#category-filters .btn.active {
|
||||||
|
background-color: #012a10;
|
||||||
|
border-color: #012a10;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Recipe Card Category Label */
|
||||||
|
.card {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-category-label {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background-color: #013617;
|
||||||
|
color: white;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
font-weight: 600;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#category-filters .btn {
|
||||||
|
padding: 8px 15px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shopping List Quantity Controls */
|
||||||
|
.btn-quantity-modifier {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50% !important;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: none !important;
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
color: white;
|
||||||
|
background-color: #013617; /* Very dark green */
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-quantity-modifier:hover {
|
||||||
|
background-color: #025c27; /* Slightly lighter on hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
.quantity-controls .quantity {
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ const app = {
|
|||||||
dom: {},
|
dom: {},
|
||||||
state: {
|
state: {
|
||||||
recipes: [],
|
recipes: [],
|
||||||
|
confirmedRecipeProducts: [],
|
||||||
},
|
},
|
||||||
api: {
|
api: {
|
||||||
async getRecipes() {
|
async getRecipes() {
|
||||||
@ -67,6 +68,13 @@ const app = {
|
|||||||
|
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'card h-100';
|
card.className = 'card h-100';
|
||||||
|
|
||||||
|
if (recipe.category) {
|
||||||
|
const categoryLabel = document.createElement('div');
|
||||||
|
categoryLabel.className = 'recipe-category-label';
|
||||||
|
categoryLabel.textContent = recipe.category;
|
||||||
|
card.appendChild(categoryLabel);
|
||||||
|
}
|
||||||
|
|
||||||
const cardBody = document.createElement('div');
|
const cardBody = document.createElement('div');
|
||||||
cardBody.className = 'card-body d-flex flex-column';
|
cardBody.className = 'card-body d-flex flex-column';
|
||||||
@ -83,7 +91,7 @@ const app = {
|
|||||||
buttonGroup.className = 'mt-auto pt-2';
|
buttonGroup.className = 'mt-auto pt-2';
|
||||||
buttonGroup.innerHTML = `
|
buttonGroup.innerHTML = `
|
||||||
<button class="btn btn-light btn-sm edit-recipe"><i class="bi bi-pencil"></i> Edit</button>
|
<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>
|
<button class="btn btn-danger btn-sm delete-recipe" title="Delete"><i class="bi bi-trash"></i></button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
cardBody.appendChild(title);
|
cardBody.appendChild(title);
|
||||||
@ -99,91 +107,129 @@ const app = {
|
|||||||
const portionsPerGuest = parseInt(app.dom.portionsPerGuestInput.value, 10) || 1;
|
const portionsPerGuest = parseInt(app.dom.portionsPerGuestInput.value, 10) || 1;
|
||||||
const totalMultiplier = guestCount * portionsPerGuest;
|
const totalMultiplier = guestCount * portionsPerGuest;
|
||||||
|
|
||||||
const groups = {
|
const combinedIngredients = new Map();
|
||||||
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() }
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// 1. Process recipe ingredients
|
||||||
app.state.recipes.forEach(recipe => {
|
app.state.recipes.forEach(recipe => {
|
||||||
if (recipe.ingredients) {
|
if (recipe.ingredients) {
|
||||||
recipe.ingredients.forEach(ing => {
|
recipe.ingredients.forEach(ing => {
|
||||||
const ingName = ing.name || '';
|
const name = ing.name.trim();
|
||||||
const ingUnit = ing.unit || '';
|
const unit = ing.unit.trim();
|
||||||
const key = `${ingName.trim().toLowerCase()}|${ingUnit.trim().toLowerCase()}`;
|
if (!name) return;
|
||||||
|
const key = `${name.toLowerCase()}|${unit.toLowerCase()}`;
|
||||||
|
|
||||||
let groupName = 'Other';
|
if (!combinedIngredients.has(key)) {
|
||||||
for (const name in groups) {
|
combinedIngredients.set(key, {
|
||||||
if (groups[name].units.includes(ingUnit)) {
|
name: name,
|
||||||
groupName = name;
|
unit: unit,
|
||||||
break;
|
recipeQty: 0,
|
||||||
}
|
additionalQty: 0,
|
||||||
}
|
sources: [],
|
||||||
|
category: null // For 'pack' items
|
||||||
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
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const item = combinedIngredients.get(key);
|
||||||
|
item.recipeQty += (ing.quantity || 0);
|
||||||
|
if (!item.sources.includes(recipe.name)) {
|
||||||
|
item.sources.push(recipe.name);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let html = '';
|
// 2. Process additional products
|
||||||
let totalIngredients = 0;
|
|
||||||
|
|
||||||
if (app.state.additionalProducts) {
|
if (app.state.additionalProducts) {
|
||||||
app.state.additionalProducts.forEach(prod => {
|
app.state.additionalProducts.forEach(prod => {
|
||||||
const key = `${prod.name.trim().toLowerCase()}|${prod.unit.trim().toLowerCase()}`;
|
const name = prod.name.trim();
|
||||||
let groupName = 'Other';
|
const unit = prod.unit.trim();
|
||||||
for (const name in groups) {
|
if (!name) return;
|
||||||
if (groups[name].units.includes(prod.unit)) {
|
const key = `${name.toLowerCase()}|${unit.toLowerCase()}`;
|
||||||
groupName = name;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const existing = groups[groupName].ingredients.get(key);
|
if (!combinedIngredients.has(key)) {
|
||||||
if (existing) {
|
combinedIngredients.set(key, {
|
||||||
existing.qty += prod.quantity;
|
name: name,
|
||||||
} else {
|
unit: unit,
|
||||||
groups[groupName].ingredients.set(key, {
|
recipeQty: 0,
|
||||||
name: prod.name,
|
additionalQty: 0,
|
||||||
qty: prod.quantity,
|
sources: [],
|
||||||
unit: prod.unit
|
category: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
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.unit === 'pack' && prod.category) {
|
||||||
|
item.category = prod.category;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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: [] }
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!groups[groupName]) { // handle dynamic categories from 'pack'
|
||||||
|
groups[groupName] = { units: [], ingredients: [] };
|
||||||
|
}
|
||||||
|
groups[groupName].ingredients.push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Render HTML
|
||||||
|
let html = '';
|
||||||
|
let totalIngredients = 0;
|
||||||
|
|
||||||
for (const groupName in groups) {
|
for (const groupName in groups) {
|
||||||
const group = groups[groupName];
|
const group = groups[groupName];
|
||||||
const ingredientList = Array.from(group.ingredients.values());
|
if (group.ingredients.length > 0) {
|
||||||
|
totalIngredients += group.ingredients.length;
|
||||||
if (ingredientList.length > 0) {
|
|
||||||
totalIngredients += ingredientList.length;
|
|
||||||
html += `<h4 class="mt-3">${groupName}</h4>`;
|
html += `<h4 class="mt-3">${groupName}</h4>`;
|
||||||
html += '<ul class="list-group list-group-flush">';
|
html += '<ul class="list-group list-group-flush">';
|
||||||
ingredientList.forEach((item, index) => {
|
group.ingredients.forEach((item, index) => {
|
||||||
const totalQty = item.qty * totalMultiplier;
|
const totalQty = (item.recipeQty * totalMultiplier) + item.additionalQty;
|
||||||
|
if (totalQty === 0) return;
|
||||||
|
|
||||||
const quantityStr = Number.isInteger(totalQty) ? totalQty : parseFloat(totalQty.toFixed(2));
|
const quantityStr = Number.isInteger(totalQty) ? totalQty : parseFloat(totalQty.toFixed(2));
|
||||||
const uniqueId = `shopping-item-${groupName}-${index}`;
|
const uniqueId = `shopping-item-${groupName.replace(/\s/g, '-')}-${index}`;
|
||||||
|
const tooltipText = item.sources.join(', ');
|
||||||
|
const itemKey = `${item.name.toLowerCase()}|${item.unit.toLowerCase()}`;
|
||||||
|
|
||||||
html += `<li class="list-group-item d-flex justify-content-between align-items-center">
|
html += `<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||||
<div class="form-check">
|
<div class="form-check d-flex align-items-center">
|
||||||
<input class="form-check-input" type="checkbox" id="${uniqueId}">
|
<input class="form-check-input" type="checkbox" id="${uniqueId}">
|
||||||
<label class="form-check-label" for="${uniqueId}">
|
<label class="form-check-label ms-2" for="${uniqueId}">
|
||||||
${item.name}
|
${item.name}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
<i class="bi bi-info-circle ms-2" data-bs-toggle="tooltip" data-bs-placement="top" title="${tooltipText}"></i>
|
||||||
<span class="badge bg-custom-green rounded-pill">${quantityStr} ${item.unit}</span>
|
</div>`;
|
||||||
</li>`;
|
|
||||||
|
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>';
|
html += '</ul>';
|
||||||
}
|
}
|
||||||
@ -194,6 +240,11 @@ const app = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.dom.shoppingListContainer.innerHTML = html;
|
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' }) {
|
addIngredientRow(ingredient = { name: '', quantity: '', unit: 'g' }) {
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
@ -221,6 +272,7 @@ const app = {
|
|||||||
clearForm() {
|
clearForm() {
|
||||||
app.dom.recipeIdInput.value = '';
|
app.dom.recipeIdInput.value = '';
|
||||||
app.dom.recipeNameInput.value = '';
|
app.dom.recipeNameInput.value = '';
|
||||||
|
app.dom.recipeCategoryInput.value = '';
|
||||||
app.dom.guestCountInput.value = '1';
|
app.dom.guestCountInput.value = '1';
|
||||||
app.dom.ingredientsContainer.innerHTML = '';
|
app.dom.ingredientsContainer.innerHTML = '';
|
||||||
app.ui.addIngredientRow();
|
app.ui.addIngredientRow();
|
||||||
@ -234,6 +286,7 @@ const app = {
|
|||||||
|
|
||||||
app.dom.recipeIdInput.value = recipe.id;
|
app.dom.recipeIdInput.value = recipe.id;
|
||||||
app.dom.recipeNameInput.value = recipe.name;
|
app.dom.recipeNameInput.value = recipe.name;
|
||||||
|
app.dom.recipeCategoryInput.value = recipe.category || '';
|
||||||
app.dom.guestCountInput.value = recipe.guests;
|
app.dom.guestCountInput.value = recipe.guests;
|
||||||
|
|
||||||
app.dom.ingredientsContainer.innerHTML = '';
|
app.dom.ingredientsContainer.innerHTML = '';
|
||||||
@ -253,6 +306,7 @@ const app = {
|
|||||||
getRecipeDataFromForm() {
|
getRecipeDataFromForm() {
|
||||||
const recipeName = app.dom.recipeNameInput.value.trim();
|
const recipeName = app.dom.recipeNameInput.value.trim();
|
||||||
const guests = parseInt(app.dom.guestCountInput.value, 10) || 0;
|
const guests = parseInt(app.dom.guestCountInput.value, 10) || 0;
|
||||||
|
const category = app.dom.recipeCategoryInput.value;
|
||||||
|
|
||||||
const ingredients = [];
|
const ingredients = [];
|
||||||
const rows = app.dom.ingredientsContainer.querySelectorAll('.ingredient-row');
|
const rows = app.dom.ingredientsContainer.querySelectorAll('.ingredient-row');
|
||||||
@ -268,7 +322,7 @@ const app = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (recipeName && guests > 0 && ingredients.length > 0) {
|
if (recipeName && guests > 0 && ingredients.length > 0) {
|
||||||
return { name: recipeName, guests, ingredients };
|
return { name: recipeName, guests, ingredients, category };
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@ -316,7 +370,7 @@ const app = {
|
|||||||
app.dom.newRecipeBtn.addEventListener('click', async function() {
|
app.dom.newRecipeBtn.addEventListener('click', async function() {
|
||||||
const recipeData = app.ui.getRecipeDataFromForm();
|
const recipeData = app.ui.getRecipeDataFromForm();
|
||||||
if (!recipeData) {
|
if (!recipeData) {
|
||||||
alert('Please fill out the recipe name, guests, and at least one ingredient before saving.');
|
alert('Please fill out the recipe name, category, guests, and at least one ingredient before saving.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,25 +382,18 @@ const app = {
|
|||||||
const data = await app.api.saveRecipe(recipeData);
|
const data = await app.api.saveRecipe(recipeData);
|
||||||
|
|
||||||
if (data.success && data.recipe) {
|
if (data.success && data.recipe) {
|
||||||
const savedRecipe = data.recipe;
|
app.api.getRecipes().then(() => {
|
||||||
if (recipeId) {
|
const currentCategory = app.dom.categoryFilters.querySelector('.active').dataset.category;
|
||||||
const index = app.state.recipes.findIndex(r => r.id == recipeId);
|
if (currentCategory === 'all') {
|
||||||
if (index !== -1) {
|
app.ui.renderRecipeCards(app.state.recipes);
|
||||||
app.state.recipes[index] = savedRecipe;
|
} else {
|
||||||
|
const filteredRecipes = app.state.recipes.filter(recipe => recipe.category === currentCategory);
|
||||||
|
app.ui.renderRecipeCards(filteredRecipes);
|
||||||
}
|
}
|
||||||
const card = app.dom.recipeCardsContainer.querySelector(`[data-id="${recipeId}"]`);
|
app.ui.updateShoppingList();
|
||||||
if (card) {
|
app.ui.clearForm();
|
||||||
card.querySelector('.card-title').textContent = savedRecipe.name;
|
app.dom.recipeFormModal.hide();
|
||||||
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 {
|
} else {
|
||||||
alert('Failed to save recipe: ' + data.error);
|
alert('Failed to save recipe: ' + data.error);
|
||||||
}
|
}
|
||||||
@ -370,12 +417,95 @@ const app = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.dom.shoppingListContainer.addEventListener('change', function(e) {
|
app.dom.shoppingListContainer.addEventListener('click', function(e) {
|
||||||
if (e.target.matches('.form-check-input')) {
|
if (e.target.matches('.form-check-input')) {
|
||||||
const listItem = e.target.closest('.list-group-item');
|
const listItem = e.target.closest('.list-group-item');
|
||||||
if (listItem) {
|
if (listItem) {
|
||||||
listItem.classList.toggle('checked', e.target.checked);
|
listItem.classList.toggle('checked', e.target.checked);
|
||||||
}
|
}
|
||||||
|
} 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('|');
|
||||||
|
|
||||||
|
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 && productToModify.quantity > 0) {
|
||||||
|
productToModify.quantity--;
|
||||||
|
app.ui.updateShoppingList();
|
||||||
|
} else {
|
||||||
|
// It's a recipe ingredient, show confirmation
|
||||||
|
let recipeName = '';
|
||||||
|
let ingredientName = '';
|
||||||
|
let recipeId = null;
|
||||||
|
let ingredientIndex = -1;
|
||||||
|
|
||||||
|
for (const recipe of app.state.recipes) {
|
||||||
|
const foundIngredientIndex = recipe.ingredients.findIndex(ing => ing.name.toLowerCase() === name.toLowerCase() && ing.unit.toLowerCase() === unit.toLowerCase());
|
||||||
|
if (foundIngredientIndex !== -1) {
|
||||||
|
recipeName = recipe.name;
|
||||||
|
ingredientName = recipe.ingredients[foundIngredientIndex].name;
|
||||||
|
recipeId = recipe.id;
|
||||||
|
ingredientIndex = foundIngredientIndex;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipeId !== null) {
|
||||||
|
const confirmationKey = `${recipeId}-${name.toLowerCase()}-${unit.toLowerCase()}`;
|
||||||
|
const recipeToUpdate = app.state.recipes.find(r => r.id === recipeId);
|
||||||
|
|
||||||
|
const proceedWithRemoval = () => {
|
||||||
|
if (recipeToUpdate && recipeToUpdate.ingredients[ingredientIndex].quantity > 0) {
|
||||||
|
recipeToUpdate.ingredients[ingredientIndex].quantity--;
|
||||||
|
}
|
||||||
|
app.ui.updateShoppingList();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (app.state.confirmedRecipeProducts.includes(confirmationKey)) {
|
||||||
|
proceedWithRemoval();
|
||||||
|
} else {
|
||||||
|
document.getElementById('modal-recipe-name').textContent = recipeName;
|
||||||
|
document.getElementById('modal-ingredient-name').textContent = ingredientName;
|
||||||
|
|
||||||
|
const confirmModal = new bootstrap.Modal(document.getElementById('confirmRemoveModal'));
|
||||||
|
confirmModal.show();
|
||||||
|
|
||||||
|
document.getElementById('confirm-remove-btn').onclick = () => {
|
||||||
|
app.state.confirmedRecipeProducts.push(confirmationKey);
|
||||||
|
proceedWithRemoval();
|
||||||
|
confirmModal.hide();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -393,38 +523,112 @@ const app = {
|
|||||||
app.ui.renderRecipeCards(filteredRecipes);
|
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() {
|
app.dom.printShoppingListBtn.addEventListener('click', function() {
|
||||||
window.print();
|
window.print();
|
||||||
});
|
});
|
||||||
|
|
||||||
app.dom.addProductBtn.addEventListener('click', () => {
|
app.dom.addProductModal._element.addEventListener('click', function(e) {
|
||||||
const name = prompt('Enter product name:');
|
if (e.target.classList.contains('unit-btn')) {
|
||||||
if (!name) return;
|
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');
|
||||||
|
|
||||||
const quantity = parseFloat(prompt('Enter quantity:'));
|
if (e.target.textContent.trim() === 'pack') {
|
||||||
if (isNaN(quantity) || quantity <= 0) {
|
app.dom.productCategoryWrapper.style.display = 'block';
|
||||||
alert('Please enter a valid quantity.');
|
} else {
|
||||||
|
app.dom.productCategoryWrapper.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const unit = prompt('Enter unit (e.g., g, kg, ml, l, piece, pack):');
|
if (unit === 'pack' && category === 'Choose...') {
|
||||||
if (!unit) return;
|
alert('Please select a category for products in packs.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!app.state.additionalProducts) {
|
if (!app.state.additionalProducts) {
|
||||||
app.state.additionalProducts = [];
|
app.state.additionalProducts = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
app.state.additionalProducts.push({
|
const key = `${name.toLowerCase()}|${unit.toLowerCase()}`;
|
||||||
name: name.trim(),
|
const existingProduct = app.state.additionalProducts.find(p => `${p.name.toLowerCase()}|${p.unit.toLowerCase()}` === key);
|
||||||
quantity: quantity,
|
|
||||||
unit: unit.trim()
|
if (existingProduct) {
|
||||||
});
|
existingProduct.quantity += quantity;
|
||||||
|
} else {
|
||||||
|
const newProduct = { name, quantity, unit, source: 'Additional product' };
|
||||||
|
if (unit === 'pack') {
|
||||||
|
newProduct.category = category;
|
||||||
|
}
|
||||||
|
app.state.additionalProducts.push(newProduct);
|
||||||
|
}
|
||||||
|
|
||||||
app.ui.updateShoppingList();
|
app.ui.updateShoppingList();
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
app.dom.productNameInput.value = '';
|
||||||
|
app.dom.productQuantityInput.value = '1';
|
||||||
|
app.dom.productCategory.value = 'Choose...';
|
||||||
|
app.dom.productCategoryWrapper.style.display = 'none';
|
||||||
|
|
||||||
|
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
document.getElementById('recipe-form-modal').addEventListener('show.bs.modal', function () {
|
document.getElementById('recipe-form-modal').addEventListener('show.bs.modal', function () {
|
||||||
if (!app.dom.recipeIdInput.value) {
|
if (!app.dom.recipeIdInput.value) {
|
||||||
app.ui.clearForm();
|
app.ui.clearForm();
|
||||||
@ -444,11 +648,20 @@ const app = {
|
|||||||
shoppingListContainer: document.getElementById('shopping-list-container'),
|
shoppingListContainer: document.getElementById('shopping-list-container'),
|
||||||
recipeCardsContainer: document.getElementById('recipe-cards-container'),
|
recipeCardsContainer: document.getElementById('recipe-cards-container'),
|
||||||
recipeIdInput: document.getElementById('recipeId'),
|
recipeIdInput: document.getElementById('recipeId'),
|
||||||
|
recipeCategoryInput: document.getElementById('recipeCategory'),
|
||||||
recipeSearchInput: document.getElementById('recipe-search'),
|
recipeSearchInput: document.getElementById('recipe-search'),
|
||||||
|
categoryFilters: document.getElementById('category-filters'),
|
||||||
addProductBtn: document.getElementById('add-product-btn'),
|
addProductBtn: document.getElementById('add-product-btn'),
|
||||||
|
|
||||||
printShoppingListBtn: document.getElementById('print-shopping-list-btn'),
|
printShoppingListBtn: document.getElementById('print-shopping-list-btn'),
|
||||||
recipeFormModal: new bootstrap.Modal(document.getElementById('recipe-form-modal'))
|
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'),
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
app.ui.createSnowflakes();
|
app.ui.createSnowflakes();
|
||||||
|
|||||||
BIN
assets/pasted-20251117-200938-7a012c0d.png
Normal file
BIN
assets/pasted-20251117-200938-7a012c0d.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/pasted-20251123-172354-6ed9c79c.png
Normal file
BIN
assets/pasted-20251123-172354-6ed9c79c.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/pasted-20251123-172752-08f5dba8.png
Normal file
BIN
assets/pasted-20251123-172752-08f5dba8.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/vm-shot-2025-11-23T17-18-09-594Z.jpg
Normal file
BIN
assets/vm-shot-2025-11-23T17-18-09-594Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
1
db/migrations/002_add_category_to_recipes.sql
Normal file
1
db/migrations/002_add_category_to_recipes.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `recipes` ADD `category` VARCHAR(255) NULL DEFAULT NULL AFTER `guests`;
|
||||||
96
index.php
96
index.php
@ -58,6 +58,14 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<input type="text" id="recipe-search" class="form-control" placeholder="Search recipes...">
|
<input type="text" id="recipe-search" class="form-control" placeholder="Search recipes...">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3 d-flex flex-wrap gap-2" id="category-filters">
|
||||||
|
<button class="btn btn-secondary active" data-category="all">All</button>
|
||||||
|
<button class="btn btn-outline-secondary" data-category="Drinks">Drinks</button>
|
||||||
|
<button class="btn btn-outline-secondary" data-category="Breakfast">Breakfast</button>
|
||||||
|
<button class="btn btn-outline-secondary" data-category="Dinner">Dinner</button>
|
||||||
|
<button class="btn btn-outline-secondary" data-category="Appetizers">Appetizers</button>
|
||||||
|
<button class="btn btn-outline-secondary" data-category="No category">No category</button>
|
||||||
|
</div>
|
||||||
<div id="recipe-cards-container" class="row">
|
<div id="recipe-cards-container" class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<p class="text-center text-muted">Your saved recipes will appear here.</p>
|
<p class="text-center text-muted">Your saved recipes will appear here.</p>
|
||||||
@ -70,7 +78,7 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<h2 class="text-center mb-0">Shopping List</h2>
|
<h2 class="text-center mb-0">Shopping List</h2>
|
||||||
<div>
|
<div>
|
||||||
<button id="add-product-btn" class="btn btn-primary me-2"><i class="bi bi-plus-lg"></i> Add Product</button>
|
<button id="add-product-btn" class="btn btn-primary me-2" data-bs-toggle="modal" data-bs-target="#add-product-modal"><i class="bi bi-plus-lg"></i> Add Product</button>
|
||||||
<button id="print-shopping-list-btn" class="btn btn-outline-secondary btn-sm"><i class="bi bi-printer"></i> Print</button>
|
<button id="print-shopping-list-btn" class="btn btn-outline-secondary btn-sm"><i class="bi bi-printer"></i> Print</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -101,6 +109,16 @@
|
|||||||
<label for="recipeName" class="form-label">Recipe Name</label>
|
<label for="recipeName" class="form-label">Recipe Name</label>
|
||||||
<input type="text" class="form-control" id="recipeName" placeholder="e.g., Gingerbread Cookies">
|
<input type="text" class="form-control" id="recipeName" placeholder="e.g., Gingerbread Cookies">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="recipeCategory" class="form-label">Category</label>
|
||||||
|
<select class="form-control" id="recipeCategory">
|
||||||
|
<option value="" selected disabled>Choose...</option>
|
||||||
|
<option value="Drinks">Drinks</option>
|
||||||
|
<option value="Breakfast">Breakfast</option>
|
||||||
|
<option value="Dinner">Dinner</option>
|
||||||
|
<option value="Appetizers">Appetizers</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr class="my-4 border-secondary">
|
<hr class="my-4 border-secondary">
|
||||||
|
|
||||||
@ -138,13 +156,87 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Add Product -->
|
||||||
|
<div class="modal fade" id="add-product-modal" tabindex="-1" aria-labelledby="add-product-modal-label" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="add-product-modal-label">Add a Product</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="add-product-form">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="productName" class="form-label">Product Name</label>
|
||||||
|
<input type="text" class="form-control" id="productName" placeholder="e.g., Milk">
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="productQuantity" class="form-label">Quantity</label>
|
||||||
|
<input type="number" class="form-control" id="productQuantity" placeholder="e.g., 1" min="1" value="1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Unit</label>
|
||||||
|
<div class="btn-group unit-selector" role="group" aria-label="Unit selector">
|
||||||
|
<button type="button" class="btn btn-secondary unit-btn">g</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary unit-btn">kg</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary unit-btn">ml</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary unit-btn">l</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary unit-btn">piece</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary unit-btn">pack</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" id="product-category-wrapper" style="display: none;">
|
||||||
|
<label for="productCategory" class="form-label">Category</label>
|
||||||
|
<select class="form-select" id="productCategory">
|
||||||
|
<option selected>Choose...</option>
|
||||||
|
<option value="Tableware and consumables">Tableware and consumables</option>
|
||||||
|
<option value="Cooking and serving">Cooking and serving</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid gap-2 mt-4">
|
||||||
|
<button type="submit" class="btn btn-primary">Add Product</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer class="text-center py-4 mt-5">
|
<footer class="text-center py-4 mt-5">
|
||||||
<p class="mb-0">© <?php echo date("Y"); ?> Christmas Recipe Calculator. Happy Holidays!</p>
|
<p class="mb-0">© <?php echo date("Y"); ?> Christmas Recipe Calculator. Happy Holidays!</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
<script src="assets/js/main.js?v=1700253313"></script>
|
||||||
|
|
||||||
|
<!-- Confirmation Modal -->
|
||||||
|
<div class="modal fade" id="confirmRemoveModal" tabindex="-1" aria-labelledby="confirmRemoveModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="confirmRemoveModalLabel">Confirm Removal</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>You are about to remove an ingredient from a recipe. This will affect the recipe itself. Are you sure you want to continue?</p>
|
||||||
|
<p><strong>Recipe:</strong> <span id="modal-recipe-name"></span></p>
|
||||||
|
<p><strong>Ingredient:</strong> <span id="modal-ingredient-name"></span></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-danger" id="confirm-remove-btn">Remove</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user