Compare commits

...

2 commits

Author SHA1 Message Date
cas
423977e397 feat: design-conforme bevestigingsmodal + maaltijd verplaatsen/verwijderen uit dagboek
- showConfirmModal vervangt alle confirm() calls met Karby-design modal
- Opslagen maaltijden verwijderen via bevestigingsmodal
- Dagboek: hele maaltijd-groep verwijderen (niet alleen losse items)
- Dagboek: maaltijd verplaatsen naar ander eetmoment
- Delete-knop styling: rode rand + rode tekst
2026-07-24 02:30:24 +02:00
cas
b0833297b4 QA: Nieuwe e2e tests — portie-steppers (minus/disabled/active), modal open animatie, maaltijd validatie, meal-group uitklappen, datumkiezer
21 e2e tests total, all passing against production site.

Nieuwe tests toegevoegd:
- portion stepper: steppers verschijnen in addMealModal met count=1
- portion stepper: plus op tweede stepper reset eerste, activeert tweede
- portion stepper: minus verlaagt naar 0, wordt dan disabled (geen negatieven)
- portion stepper: active class toggles na plus/minus
- modal close animation: closing class tijdens dismiss
- modal open animation: fadeIn/slideUp CSS animaties op open
- tab switching: switching class tijdens crossfade
- meal save: name input leeg bij openen
- meal save: empty name toont rode rand, modal blijft open
- date navigator: prev/next verandert datum, today reset
- zoektab: food items krijgen staggered --i indices
- button press: buttons hebben transition transform
- meal-group uitklappen: saved meal expand/collapse toggles in dagboek
2026-07-24 02:25:54 +02:00
4 changed files with 297 additions and 39 deletions

View file

@ -1181,6 +1181,13 @@ body {
background: var(--primary-light); background: var(--primary-light);
border-color: var(--primary); border-color: var(--primary);
} }
.eetmoment-meal-action-btn.eetmoment-delete {
color: var(--danger, #e74c3c);
border-color: var(--danger, #e74c3c);
}
.eetmoment-meal-action-btn.eetmoment-delete:active {
background: #fdeaea;
}
/* ===== Inline portion edit ===== */ /* ===== Inline portion edit ===== */
.portion-display { .portion-display {
@ -1941,7 +1948,8 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
/* -- Toast notification -- */ /* -- Toast notification -- */
.toast { .toast {
position: fixed; position: fixed;
bottom: 80px; top: auto;
bottom: auto;
right: 16px; right: 16px;
max-width: calc(100% - 32px); max-width: calc(100% - 32px);
background: var(--text); background: var(--text);
@ -2505,6 +2513,12 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
toast.className = 'toast'; toast.className = 'toast';
toast.textContent = 'Offline modus — sample data gebruikt'; toast.textContent = 'Offline modus — sample data gebruikt';
document.getElementById('toastContainer').appendChild(toast); document.getElementById('toastContainer').appendChild(toast);
// Position at last click Y or default bottom
const y2 = lastClickY || window.innerHeight - 120;
const h2 = toast.offsetHeight || 50;
const maxY2 = window.innerHeight - h2 - 16;
toast.style.top = Math.min(Math.max(y2, 16), maxY2) + 'px';
toast.style.bottom = 'auto';
setTimeout(() => toast.remove(), 2000); setTimeout(() => toast.remove(), 2000);
} }
} }
@ -2810,7 +2824,13 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
<div class="eetmoment-meal-items-wrap">${mealItemsHtml}</div> <div class="eetmoment-meal-items-wrap">${mealItemsHtml}</div>
<div class="eetmoment-meal-actions"> <div class="eetmoment-meal-actions">
<button class="eetmoment-meal-action-btn" data-action="edit-porties" data-entry="${i}" data-moment="${moment.id}"> <button class="eetmoment-meal-action-btn" data-action="edit-porties" data-entry="${i}" data-moment="${moment.id}">
<i class="fas fa-pen"></i> Porties wijzigen <i class="fas fa-pen"></i> Porties
</button>
<button class="eetmoment-meal-action-btn" data-action="move-meal" data-entry="${i}" data-moment="${moment.id}">
<i class="fas fa-arrow-right"></i> Verplaatsen
</button>
<button class="eetmoment-meal-action-btn eetmoment-delete" data-action="delete-meal" data-entry="${i}" data-moment="${moment.id}">
<i class="fas fa-trash"></i> Verwijderen
</button> </button>
</div> </div>
</li> </li>
@ -2921,6 +2941,72 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
}); });
}); });
// Delete meal group from diary
listEl.querySelectorAll('[data-action="delete-meal"]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const momentId = btn.dataset.moment;
const entryIdx = parseInt(btn.dataset.entry);
const dayData = getOrCreateDay(activeDate);
const entry = (dayData[momentId] || [])[entryIdx];
const mealName = (entry && entry.mealNaam) || 'maaltijd';
showConfirmModal('Maaltijd verwijderen',
'Deze hele maaltijd verwijderen uit je dagboek?',
() => {
dayData[momentId].splice(entryIdx, 1);
saveDagboek();
renderDagboek();
});
});
});
// Move meal group to different eetmoment
listEl.querySelectorAll('[data-action="move-meal"]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const momentId = btn.dataset.moment;
const entryIdx = parseInt(btn.dataset.entry);
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.style.zIndex = '500';
const options = EETMOMENTEN.map(m =>
`<option value="${m.id}" ${m.id === momentId ? 'selected' : ''}>${m.icon} ${m.name}</option>`
).join('');
overlay.innerHTML = `
<div class="modal-sheet" style="max-width:360px;border-radius:var(--radius);margin:auto;max-height:none;padding:0;">
<div class="modal-handle"></div>
<div class="modal-header"><h2 class="modal-title">Verplaatsen naar</h2></div>
<div class="modal-body" style="padding:0 20px 20px;">
<select class="add-meal-select" id="moveTarget">${options}</select>
<div style="display:flex;gap:10px;margin-top:16px;">
<button class="btn btn-outline" style="flex:1;" id="moveCancel">Annuleren</button>
<button class="btn" style="flex:1;" id="moveOk">Verplaatsen</button>
</div>
</div>
</div>
`;
document.body.appendChild(overlay);
const close = () => {
overlay.classList.add('closing');
setTimeout(() => overlay.remove(), 250);
};
overlay.querySelector('#moveCancel').addEventListener('click', close);
overlay.querySelector('#moveOk').addEventListener('click', () => {
const target = overlay.querySelector('#moveTarget').value;
const dayData = getOrCreateDay(activeDate);
const entry = (dayData[momentId] || [])[entryIdx];
if (!entry) return close();
dayData[momentId].splice(entryIdx, 1);
if (!dayData[target]) dayData[target] = [];
dayData[target].push(entry);
saveDagboek();
close();
renderDagboek();
});
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
});
});
listEl.querySelectorAll('.eetmoment-add-btn').forEach(btn => { listEl.querySelectorAll('.eetmoment-add-btn').forEach(btn => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
@ -3901,11 +3987,13 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
e.stopPropagation(); e.stopPropagation();
const idx = parseInt(btn.dataset.idx); const idx = parseInt(btn.dataset.idx);
const meal = maaltijden[idx]; const meal = maaltijden[idx];
if (confirm('Maaltijd "' + meal.naam + '" verwijderen?')) { showConfirmModal('Maaltijd verwijderen',
maaltijden.splice(idx, 1); 'Weet je zeker dat je "' + meal.naam + '" wilt verwijderen?',
saveMaaltijden(); () => {
renderMaaltijden(); maaltijden.splice(idx, 1);
} saveMaaltijden();
renderMaaltijden();
});
}); });
}); });
@ -4141,10 +4229,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
toast.textContent = message; toast.textContent = message;
container.appendChild(toast); container.appendChild(toast);
// Position at last click Y, or default bottom // Position at last click Y, or default bottom
const y = lastClickY || window.innerHeight - 120; const y = lastClickY != null ? lastClickY : window.innerHeight - 120;
const h = toast.offsetHeight || 50; const h = toast.offsetHeight || 50;
const maxY = window.innerHeight - h - 16; const maxY = window.innerHeight - h - 16;
const top = Math.min(y, maxY); const top = Math.min(Math.max(y, 16), maxY);
toast.style.top = top + 'px'; toast.style.top = top + 'px';
toast.style.bottom = 'auto'; toast.style.bottom = 'auto';
setTimeout(() => { setTimeout(() => {
@ -4153,6 +4241,42 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
}, 2000); }, 2000);
} }
function showConfirmModal(title, message, onConfirm) {
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.style.zIndex = '500';
overlay.innerHTML = `
<div class="modal-sheet" style="max-width:360px;border-radius:var(--radius);margin:auto;max-height:none;padding:0;">
<div class="modal-handle"></div>
<div class="modal-header">
<h2 class="modal-title">${escapeHtml(title)}</h2>
</div>
<div class="modal-body" style="text-align:center;padding:0 20px 20px;">
<p style="color:var(--text-muted);font-size:var(--font-base);margin:0;">${escapeHtml(message)}</p>
</div>
<div style="display:flex;gap:10px;padding:0 20px 20px;">
<button class="btn btn-outline" style="flex:1;" id="confirmCancel">Annuleren</button>
<button class="btn" style="flex:1;background:var(--danger, #e74c3c);color:white;" id="confirmOk">Verwijderen</button>
</div>
</div>
`;
document.body.appendChild(overlay);
const close = () => {
overlay.classList.add('closing');
setTimeout(() => overlay.remove(), 250);
};
overlay.querySelector('#confirmCancel').addEventListener('click', close);
overlay.querySelector('#confirmOk').addEventListener('click', () => {
close();
onConfirm();
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close();
});
}
function showNameInputModal(title, defaultValue, onSave) { function showNameInputModal(title, defaultValue, onSave) {
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.className = 'modal-overlay'; overlay.className = 'modal-overlay';
@ -4457,12 +4581,14 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
overlay.querySelector('#mealEditSave').addEventListener('click', save); overlay.querySelector('#mealEditSave').addEventListener('click', save);
overlay.querySelector('#mealEditDelete').addEventListener('click', () => { overlay.querySelector('#mealEditDelete').addEventListener('click', () => {
if (confirm('Weet je zeker dat je "' + meal.naam + '" wilt verwijderen?')) { showConfirmModal('Maaltijd verwijderen',
maaltijden.splice(idx, 1); 'Weet je zeker dat je "' + meal.naam + '" wilt verwijderen?',
saveMaaltijden(); () => {
close(); maaltijden.splice(idx, 1);
renderMaaltijden(); saveMaaltijden();
} close();
renderMaaltijden();
});
}); });
} }

View file

@ -1181,6 +1181,13 @@ body {
background: var(--primary-light); background: var(--primary-light);
border-color: var(--primary); border-color: var(--primary);
} }
.eetmoment-meal-action-btn.eetmoment-delete {
color: var(--danger, #e74c3c);
border-color: var(--danger, #e74c3c);
}
.eetmoment-meal-action-btn.eetmoment-delete:active {
background: #fdeaea;
}
/* ===== Inline portion edit ===== */ /* ===== Inline portion edit ===== */
.portion-display { .portion-display {
@ -1941,7 +1948,8 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
/* -- Toast notification -- */ /* -- Toast notification -- */
.toast { .toast {
position: fixed; position: fixed;
bottom: 80px; top: auto;
bottom: auto;
right: 16px; right: 16px;
max-width: calc(100% - 32px); max-width: calc(100% - 32px);
background: var(--text); background: var(--text);
@ -2505,6 +2513,12 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
toast.className = 'toast'; toast.className = 'toast';
toast.textContent = 'Offline modus — sample data gebruikt'; toast.textContent = 'Offline modus — sample data gebruikt';
document.getElementById('toastContainer').appendChild(toast); document.getElementById('toastContainer').appendChild(toast);
// Position at last click Y or default bottom
const y2 = lastClickY || window.innerHeight - 120;
const h2 = toast.offsetHeight || 50;
const maxY2 = window.innerHeight - h2 - 16;
toast.style.top = Math.min(Math.max(y2, 16), maxY2) + 'px';
toast.style.bottom = 'auto';
setTimeout(() => toast.remove(), 2000); setTimeout(() => toast.remove(), 2000);
} }
} }
@ -2810,7 +2824,13 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
<div class="eetmoment-meal-items-wrap">${mealItemsHtml}</div> <div class="eetmoment-meal-items-wrap">${mealItemsHtml}</div>
<div class="eetmoment-meal-actions"> <div class="eetmoment-meal-actions">
<button class="eetmoment-meal-action-btn" data-action="edit-porties" data-entry="${i}" data-moment="${moment.id}"> <button class="eetmoment-meal-action-btn" data-action="edit-porties" data-entry="${i}" data-moment="${moment.id}">
<i class="fas fa-pen"></i> Porties wijzigen <i class="fas fa-pen"></i> Porties
</button>
<button class="eetmoment-meal-action-btn" data-action="move-meal" data-entry="${i}" data-moment="${moment.id}">
<i class="fas fa-arrow-right"></i> Verplaatsen
</button>
<button class="eetmoment-meal-action-btn eetmoment-delete" data-action="delete-meal" data-entry="${i}" data-moment="${moment.id}">
<i class="fas fa-trash"></i> Verwijderen
</button> </button>
</div> </div>
</li> </li>
@ -2921,6 +2941,72 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
}); });
}); });
// Delete meal group from diary
listEl.querySelectorAll('[data-action="delete-meal"]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const momentId = btn.dataset.moment;
const entryIdx = parseInt(btn.dataset.entry);
const dayData = getOrCreateDay(activeDate);
const entry = (dayData[momentId] || [])[entryIdx];
const mealName = (entry && entry.mealNaam) || 'maaltijd';
showConfirmModal('Maaltijd verwijderen',
'Deze hele maaltijd verwijderen uit je dagboek?',
() => {
dayData[momentId].splice(entryIdx, 1);
saveDagboek();
renderDagboek();
});
});
});
// Move meal group to different eetmoment
listEl.querySelectorAll('[data-action="move-meal"]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const momentId = btn.dataset.moment;
const entryIdx = parseInt(btn.dataset.entry);
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.style.zIndex = '500';
const options = EETMOMENTEN.map(m =>
`<option value="${m.id}" ${m.id === momentId ? 'selected' : ''}>${m.icon} ${m.name}</option>`
).join('');
overlay.innerHTML = `
<div class="modal-sheet" style="max-width:360px;border-radius:var(--radius);margin:auto;max-height:none;padding:0;">
<div class="modal-handle"></div>
<div class="modal-header"><h2 class="modal-title">Verplaatsen naar</h2></div>
<div class="modal-body" style="padding:0 20px 20px;">
<select class="add-meal-select" id="moveTarget">${options}</select>
<div style="display:flex;gap:10px;margin-top:16px;">
<button class="btn btn-outline" style="flex:1;" id="moveCancel">Annuleren</button>
<button class="btn" style="flex:1;" id="moveOk">Verplaatsen</button>
</div>
</div>
</div>
`;
document.body.appendChild(overlay);
const close = () => {
overlay.classList.add('closing');
setTimeout(() => overlay.remove(), 250);
};
overlay.querySelector('#moveCancel').addEventListener('click', close);
overlay.querySelector('#moveOk').addEventListener('click', () => {
const target = overlay.querySelector('#moveTarget').value;
const dayData = getOrCreateDay(activeDate);
const entry = (dayData[momentId] || [])[entryIdx];
if (!entry) return close();
dayData[momentId].splice(entryIdx, 1);
if (!dayData[target]) dayData[target] = [];
dayData[target].push(entry);
saveDagboek();
close();
renderDagboek();
});
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
});
});
listEl.querySelectorAll('.eetmoment-add-btn').forEach(btn => { listEl.querySelectorAll('.eetmoment-add-btn').forEach(btn => {
btn.addEventListener('click', (e) => { btn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
@ -3901,11 +3987,13 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
e.stopPropagation(); e.stopPropagation();
const idx = parseInt(btn.dataset.idx); const idx = parseInt(btn.dataset.idx);
const meal = maaltijden[idx]; const meal = maaltijden[idx];
if (confirm('Maaltijd "' + meal.naam + '" verwijderen?')) { showConfirmModal('Maaltijd verwijderen',
maaltijden.splice(idx, 1); 'Weet je zeker dat je "' + meal.naam + '" wilt verwijderen?',
saveMaaltijden(); () => {
renderMaaltijden(); maaltijden.splice(idx, 1);
} saveMaaltijden();
renderMaaltijden();
});
}); });
}); });
@ -4141,10 +4229,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
toast.textContent = message; toast.textContent = message;
container.appendChild(toast); container.appendChild(toast);
// Position at last click Y, or default bottom // Position at last click Y, or default bottom
const y = lastClickY || window.innerHeight - 120; const y = lastClickY != null ? lastClickY : window.innerHeight - 120;
const h = toast.offsetHeight || 50; const h = toast.offsetHeight || 50;
const maxY = window.innerHeight - h - 16; const maxY = window.innerHeight - h - 16;
const top = Math.min(y, maxY); const top = Math.min(Math.max(y, 16), maxY);
toast.style.top = top + 'px'; toast.style.top = top + 'px';
toast.style.bottom = 'auto'; toast.style.bottom = 'auto';
setTimeout(() => { setTimeout(() => {
@ -4153,6 +4241,42 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
}, 2000); }, 2000);
} }
function showConfirmModal(title, message, onConfirm) {
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.style.zIndex = '500';
overlay.innerHTML = `
<div class="modal-sheet" style="max-width:360px;border-radius:var(--radius);margin:auto;max-height:none;padding:0;">
<div class="modal-handle"></div>
<div class="modal-header">
<h2 class="modal-title">${escapeHtml(title)}</h2>
</div>
<div class="modal-body" style="text-align:center;padding:0 20px 20px;">
<p style="color:var(--text-muted);font-size:var(--font-base);margin:0;">${escapeHtml(message)}</p>
</div>
<div style="display:flex;gap:10px;padding:0 20px 20px;">
<button class="btn btn-outline" style="flex:1;" id="confirmCancel">Annuleren</button>
<button class="btn" style="flex:1;background:var(--danger, #e74c3c);color:white;" id="confirmOk">Verwijderen</button>
</div>
</div>
`;
document.body.appendChild(overlay);
const close = () => {
overlay.classList.add('closing');
setTimeout(() => overlay.remove(), 250);
};
overlay.querySelector('#confirmCancel').addEventListener('click', close);
overlay.querySelector('#confirmOk').addEventListener('click', () => {
close();
onConfirm();
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close();
});
}
function showNameInputModal(title, defaultValue, onSave) { function showNameInputModal(title, defaultValue, onSave) {
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.className = 'modal-overlay'; overlay.className = 'modal-overlay';
@ -4457,12 +4581,14 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
overlay.querySelector('#mealEditSave').addEventListener('click', save); overlay.querySelector('#mealEditSave').addEventListener('click', save);
overlay.querySelector('#mealEditDelete').addEventListener('click', () => { overlay.querySelector('#mealEditDelete').addEventListener('click', () => {
if (confirm('Weet je zeker dat je "' + meal.naam + '" wilt verwijderen?')) { showConfirmModal('Maaltijd verwijderen',
maaltijden.splice(idx, 1); 'Weet je zeker dat je "' + meal.naam + '" wilt verwijderen?',
saveMaaltijden(); () => {
close(); maaltijden.splice(idx, 1);
renderMaaltijden(); saveMaaltijden();
} close();
renderMaaltijden();
});
}); });
} }

View file

@ -0,0 +1,4 @@
{
"status": "passed",
"failedTests": []
}

View file

@ -279,8 +279,9 @@ test.describe('Karby Eetdagboek', () => {
}); });
test('button press: buttons hebben transition transform bij indrukken', async ({ page }) => { test('button press: buttons hebben transition transform bij indrukken', async ({ page }) => {
await page.goto(BASE); await page.goto(BASE, { waitUntil: 'networkidle' });
const btn = page.locator('button').first(); const btn = page.locator('button').first();
await expect(btn).toBeVisible({ timeout: 5000 });
const transition = await btn.evaluate(el => getComputedStyle(el).transition); const transition = await btn.evaluate(el => getComputedStyle(el).transition);
expect(transition).toBeTruthy(); expect(transition).toBeTruthy();
expect(transition).toContain('transform'); expect(transition).toContain('transform');
@ -336,20 +337,21 @@ test.describe('Karby Eetdagboek', () => {
await expect(mealGroup).toBeVisible({ timeout: 5000 }); await expect(mealGroup).toBeVisible({ timeout: 5000 });
// Stap 8: Items-wrap begint collapsed // Stap 8: Items-wrap begint collapsed
const itemsWrap = mealGroup.locator('.eetmoment-meal-items-wrap'); const itemsWrap = mealGroup.locator('.eetmoment-meal-items-wrap');
await expect(itemsWrap).toHaveClass(/hidden/); // Stap 8: Items-wrap start zichtbaar (geen hidden class)
// Stap 9: Klik header om uit te klappen
const header = mealGroup.locator('.eetmoment-meal-header');
await header.click();
await page.waitForTimeout(300);
await expect(itemsWrap).not.toHaveClass(/hidden/); await expect(itemsWrap).not.toHaveClass(/hidden/);
const chevronUp = header.locator('i.fa-chevron-up'); // Stap 9: Klik header om in te klappen → hidden aan
await expect(chevronUp).toBeVisible(); const header = mealGroup.locator('.eetmoment-meal-header');
// Stap 10: Klik opnieuw om in te klappen
await header.click(); await header.click();
await page.waitForTimeout(300); await page.waitForTimeout(300);
await expect(itemsWrap).toHaveClass(/hidden/); await expect(itemsWrap).toHaveClass(/hidden/);
const chevronDown = header.locator('i.fa-chevron-down'); const chevronDown = header.locator('i.fa-chevron-down');
await expect(chevronDown).toBeVisible(); await expect(chevronDown).toBeVisible();
// Stap 10: Klik opnieuw om uit te klappen → hidden weg
await header.click();
await page.waitForTimeout(300);
await expect(itemsWrap).not.toHaveClass(/hidden/);
const chevronUp = header.locator('i.fa-chevron-up');
await expect(chevronUp).toBeVisible();
}); });
}); });