This commit is contained in:
Flatlogic Bot 2026-01-30 22:54:39 +00:00
parent 3830ef477f
commit 7b0ece6750
3 changed files with 119 additions and 16 deletions

View File

@ -294,6 +294,43 @@ footer {
padding: 20px 24px; padding: 20px 24px;
} }
/* Recipe Card Selection */
.recipe-selection-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 2px solid transparent;
}
.recipe-selection-card.selected {
border-color: var(--brand-primary);
box-shadow: 0 15px 45px rgba(45, 106, 79, 0.15);
background-color: #F7FAF9;
}
.recipe-selection-card .select-recipe:checked {
background-color: var(--brand-primary);
border-color: var(--brand-primary);
}
.recipe-controls {
border: 1px solid #EAEAEA;
transition: all 0.3s ease;
}
.recipe-selection-card.selected .recipe-controls {
background-color: #ffffff !important;
border-color: var(--brand-primary);
}
.recipe-selection-card .form-check-input {
width: 1.5em;
height: 1.5em;
}
.recipe-selection-card .form-check-label {
cursor: pointer;
user-select: none;
}
/* Category Label */ /* Category Label */
.recipe-category-label { .recipe-category-label {
background-color: #E9F5EF; background-color: #E9F5EF;

View File

@ -5,6 +5,7 @@ const app = {
confirmedRecipeProducts: [], confirmedRecipeProducts: [],
checkedItems: [], checkedItems: [],
additionalProducts: [], additionalProducts: [],
selectedRecipeIds: [],
user: null user: null
}, },
api: { api: {
@ -18,6 +19,7 @@ const app = {
app.state.checkedItems = data.user.shopping_list.checkedItems || []; app.state.checkedItems = data.user.shopping_list.checkedItems || [];
app.state.additionalProducts = data.user.shopping_list.additionalProducts || []; app.state.additionalProducts = data.user.shopping_list.additionalProducts || [];
app.state.confirmedRecipeProducts = data.user.shopping_list.confirmedRecipeProducts || []; app.state.confirmedRecipeProducts = data.user.shopping_list.confirmedRecipeProducts || [];
app.state.selectedRecipeIds = data.user.shopping_list.selectedRecipeIds || [];
} }
} else { } else {
app.state.user = null; app.state.user = null;
@ -36,7 +38,8 @@ const app = {
shopping_list: { shopping_list: {
checkedItems: app.state.checkedItems, checkedItems: app.state.checkedItems,
additionalProducts: app.state.additionalProducts, additionalProducts: app.state.additionalProducts,
confirmedRecipeProducts: app.state.confirmedRecipeProducts confirmedRecipeProducts: app.state.confirmedRecipeProducts,
selectedRecipeIds: app.state.selectedRecipeIds
} }
}) })
}); });
@ -141,9 +144,20 @@ const app = {
}); });
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
// Remove from selectedRecipeIds if present
const index = app.state.selectedRecipeIds.indexOf(id.toString());
if (index > -1) {
app.state.selectedRecipeIds.splice(index, 1);
}
const indexNum = app.state.selectedRecipeIds.indexOf(Number(id));
if (indexNum > -1) {
app.state.selectedRecipeIds.splice(indexNum, 1);
}
await app.api.getRecipes(); await app.api.getRecipes();
app.ui.renderRecipeCards(app.state.recipes); app.ui.renderRecipeCards(app.state.recipes);
app.ui.updateShoppingList(); app.ui.updateShoppingList();
app.api.saveShoppingList();
} else { } else {
alert('Failed to delete recipe: ' + data.error); alert('Failed to delete recipe: ' + data.error);
} }
@ -166,8 +180,10 @@ const app = {
cardCol.setAttribute('data-id', recipe.id); cardCol.setAttribute('data-id', recipe.id);
cardCol.style.animationDelay = `${index * 0.1}s`; cardCol.style.animationDelay = `${index * 0.1}s`;
const isSelected = app.state.selectedRecipeIds.includes(recipe.id.toString()) || app.state.selectedRecipeIds.includes(Number(recipe.id));
const card = document.createElement('div'); const card = document.createElement('div');
card.className = 'card h-100'; card.className = `card h-100 recipe-selection-card ${isSelected ? 'selected' : ''}`;
if (recipe.image_url) { if (recipe.image_url) {
const img = document.createElement('img'); const img = document.createElement('img');
@ -183,10 +199,15 @@ const app = {
const titleWrapper = document.createElement('div'); const titleWrapper = document.createElement('div');
titleWrapper.className = 'd-flex justify-content-between align-items-start mb-2 gap-2'; titleWrapper.className = 'd-flex justify-content-between align-items-start mb-2 gap-2';
const title = document.createElement('h5'); const selectionWrapper = document.createElement('div');
title.className = 'card-title mb-0'; selectionWrapper.className = 'form-check mb-0';
title.textContent = recipe.name; selectionWrapper.innerHTML = `
titleWrapper.appendChild(title); <input class="form-check-input select-recipe" type="checkbox" value="${recipe.id}" id="select-recipe-${recipe.id}" ${isSelected ? 'checked' : ''}>
<label class="form-check-label fw-bold h5 mb-0 ms-1" for="select-recipe-${recipe.id}">
${recipe.name}
</label>
`;
titleWrapper.appendChild(selectionWrapper);
if (recipe.category) { if (recipe.category) {
const categoryLabel = document.createElement('div'); const categoryLabel = document.createElement('div');
@ -199,19 +220,35 @@ const app = {
} }
const text = document.createElement('p'); const text = document.createElement('p');
text.className = 'card-text text-muted'; text.className = 'card-text text-muted mb-3';
text.textContent = `${recipe.ingredients.length} ingredients`; text.textContent = `${recipe.ingredients.length} ingredients`;
const controlsWrapper = document.createElement('div');
controlsWrapper.className = 'recipe-controls mb-3 p-2 bg-light rounded';
controlsWrapper.innerHTML = `
<div class="row g-2 align-items-center">
<div class="col-6">
<label class="small text-muted mb-1 d-block">Guests</label>
<input type="number" class="form-control form-control-sm recipe-guests-input" value="${recipe.guests}" min="1">
</div>
<div class="col-6">
<label class="small text-muted mb-1 d-block">Portions</label>
<input type="number" class="form-control form-control-sm recipe-portions-input" value="1" min="1">
</div>
</div>
`;
const buttonGroup = document.createElement('div'); const buttonGroup = document.createElement('div');
buttonGroup.className = 'mt-auto pt-2'; buttonGroup.className = 'mt-auto pt-2 d-flex gap-2';
buttonGroup.innerHTML = ` buttonGroup.innerHTML = `
<button class="btn btn-light btn-sm view-recipe"><i class="bi bi-eye"></i> View</button> <button class="btn btn-light btn-sm view-recipe flex-grow-1"><i class="bi bi-eye"></i> View</button>
<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></button>
<button class="btn btn-danger btn-sm delete-recipe" title="Delete"><i class="bi bi-trash"></i></button> <button class="btn btn-outline-danger btn-sm delete-recipe" title="Delete"><i class="bi bi-trash"></i></button>
`; `;
cardBody.appendChild(titleWrapper); cardBody.appendChild(titleWrapper);
cardBody.appendChild(text); cardBody.appendChild(text);
cardBody.appendChild(controlsWrapper);
cardBody.appendChild(buttonGroup); cardBody.appendChild(buttonGroup);
card.appendChild(cardBody); card.appendChild(cardBody);
cardCol.appendChild(card); cardCol.appendChild(card);
@ -223,6 +260,11 @@ const app = {
// 1. Process recipe ingredients and calculate total based on per-recipe inputs // 1. Process recipe ingredients and calculate total based on per-recipe inputs
app.state.recipes.forEach(recipe => { app.state.recipes.forEach(recipe => {
// Only include if selected
if (!app.state.selectedRecipeIds.includes(recipe.id.toString()) && !app.state.selectedRecipeIds.includes(Number(recipe.id))) {
return;
}
const card = app.dom.recipeCardsContainer.querySelector(`[data-id="${recipe.id}"]`); const card = app.dom.recipeCardsContainer.querySelector(`[data-id="${recipe.id}"]`);
if (!card) return; if (!card) return;
@ -699,10 +741,34 @@ const app = {
app.dom.recipeCardsContainer.addEventListener('click', function(e) { app.dom.recipeCardsContainer.addEventListener('click', function(e) {
const target = e.target.closest('button'); const target = e.target.closest('button');
const card = e.target.closest('.col-12[data-id]'); const checkbox = e.target.closest('.select-recipe');
if (!card) return; const cardCol = e.target.closest('.col-12[data-id]');
if (!cardCol) return;
const recipeId = card.getAttribute('data-id'); const recipeId = cardCol.getAttribute('data-id');
if (checkbox) {
const card = cardCol.querySelector('.card');
if (checkbox.checked) {
if (!app.state.selectedRecipeIds.includes(recipeId)) {
app.state.selectedRecipeIds.push(recipeId);
}
card.classList.add('selected');
} else {
const index = app.state.selectedRecipeIds.indexOf(recipeId);
if (index > -1) {
app.state.selectedRecipeIds.splice(index, 1);
}
const indexNum = app.state.selectedRecipeIds.indexOf(Number(recipeId));
if (indexNum > -1) {
app.state.selectedRecipeIds.splice(indexNum, 1);
}
card.classList.remove('selected');
}
app.ui.updateShoppingList();
app.api.saveShoppingList();
return; // Don't trigger other actions
}
if (target && target.classList.contains('delete-recipe')) { if (target && target.classList.contains('delete-recipe')) {
if (confirm('Are you sure you want to delete this recipe?')) { if (confirm('Are you sure you want to delete this recipe?')) {

View File

@ -27,7 +27,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>_v16"> <link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>_v17">
</head> </head>
<body> <body>
@ -380,7 +380,7 @@
<!-- 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(); ?>_v6"></script> <script src="assets/js/main.js?v=<?php echo time(); ?>_v7"></script>
<!-- Confirmation Modal --> <!-- Confirmation Modal -->
<div class="modal fade" id="confirmRemoveModal" tabindex="-1" aria-labelledby="confirmRemoveModalLabel" aria-hidden="true"> <div class="modal fade" id="confirmRemoveModal" tabindex="-1" aria-labelledby="confirmRemoveModalLabel" aria-hidden="true">