diff --git a/docker/index.html b/docker/index.html index 90af93a..778f555 100644 --- a/docker/index.html +++ b/docker/index.html @@ -800,12 +800,6 @@ body { display: flex; align-items: flex-end; justify-content: center; - animation: fadeIn 0.2s; -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } } .modal-sheet { @@ -816,15 +810,9 @@ body { max-height: 85vh; max-height: 85dvh; overflow-y: auto; - animation: slideUp 0.25s ease-out; padding-bottom: env(safe-area-inset-bottom, 0); } -@keyframes slideUp { - from { transform: translateY(100%); } - to { transform: translateY(0); } -} - .modal-handle { width: 36px; height: 4px; @@ -1824,34 +1812,6 @@ body { cursor: not-allowed; } -/* ===== Toast Notification ===== */ -.toast { - position: fixed; - bottom: 80px; - left: 50%; - transform: translateX(-50%); - background: var(--text); - color: var(--white); - padding: 10px 20px; - border-radius: 20px; - font-size: var(--font-sm); - font-weight: 500; - z-index: 300; - animation: toastIn 0.3s, toastOut 0.3s 1.7s forwards; - pointer-events: none; - max-width: calc(100% - 32px); - text-align: center; -} - -@keyframes toastIn { - from { opacity: 0; transform: translateX(-50%) translateY(10px); } - to { opacity: 1; transform: translateX(-50%) translateY(0); } -} - -@keyframes toastOut { - from { opacity: 1; } - to { opacity: 0; } -} /* ===== Utility ===== */ .hidden { @@ -1958,26 +1918,45 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- animation: slideDown 0.25s ease-in both; } -/* -- Tab content transition -- */ +/* -- Tab content transition — crossfade effect on switch only -- */ .tab-content { - animation: fadeIn 0.25s ease both; + /* no default animation; applied via JS .switching class during tab switches */ } .tab-content.switching { - animation: fadeIn 0.2s ease both; + animation: fadeIn 0.25s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +/* -- Page content transition on tab switch -- */ +.main-content.page-transition { + animation: fadeIn 0.3s ease both; } /* -- Staggered list fade-in -- */ -.search-result-item, .food-item, .meal-card, .dagboek-item, .maaltijd-item { - animation: fadeIn 0.3s ease both; +.search-result-item, .food-item, .meal-card, .dagboek-item, .maaltijd-item, +.eetmoment-card, .eetmoment-item, .meal-add-option { + animation: fadeIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; animation-delay: calc(var(--i, 0) * 50ms); } /* -- Toast notification -- */ .toast { - animation: slideInRight 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; + position: fixed; + bottom: 80px; + left: 16px; + right: 16px; + background: var(--text); + color: var(--white); + padding: 10px 20px; + border-radius: 20px; + font-size: var(--font-sm); + font-weight: 500; + z-index: 300; + animation: slideUp 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; + text-align: center; + pointer-events: none; } .toast.hiding { - animation: slideOutRight 0.3s ease-in both; + animation: slideDown 0.25s ease-in both; } /* -- Stepper -- */ @@ -2782,7 +2761,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Eating moments const listEl = document.getElementById('eetmomentList'); - listEl.innerHTML = EETMOMENTEN.map(moment => { + listEl.innerHTML = EETMOMENTEN.map((moment, mi) => { const items = dayData[moment.id] || []; const momentKh = berekenEetmomentKh(activeDate, moment.id); const roundedKh = Math.round(momentKh); @@ -2864,7 +2843,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- const cardOverLimit = overLimit && momentKh > 0; return ` -
  • +
  • ${moment.icon} @@ -3128,7 +3107,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- const countEl = stepper.querySelector('.portie-stepper-count'); const grams = parseInt(stepper.dataset.grams, 10); - stepper.querySelector('.portie-stepper-plus').addEventListener('click', () => { + stepper.querySelector('.portie-stepper-plus').addEventListener('click', function() { // Reset all other steppers to 0 steppers.forEach(s => { if (s !== stepper) { s.querySelector('.portie-stepper-count').textContent = '0'; } @@ -3136,12 +3115,18 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- let count = parseInt(countEl.textContent, 10) || 0; countEl.textContent = count + 1; updatePortionFromSteppers(); + // Bounce feedback + this.classList.add('tapped'); + setTimeout(() => this.classList.remove('tapped'), 300); }); - stepper.querySelector('.portie-stepper-minus').addEventListener('click', () => { + stepper.querySelector('.portie-stepper-minus').addEventListener('click', function() { let count = parseInt(countEl.textContent, 10) || 0; if (count > 0) countEl.textContent = count - 1; updatePortionFromSteppers(); + // Bounce feedback + this.classList.add('tapped'); + setTimeout(() => this.classList.remove('tapped'), 300); }); }); @@ -3298,7 +3283,6 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- document.body.appendChild(overlay); document.body.style.overflow = 'hidden'; - function closeDialog() { overlay.remove(); document.body.style.overflow = ''; } overlay.querySelector('#portieEditClose').addEventListener('click', closeDialog); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDialog(); }); @@ -3343,15 +3327,19 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- } steppers.forEach(stepper => { const countEl = stepper.querySelector('.portie-stepper-count'); - stepper.querySelector('.portie-stepper-plus').addEventListener('click', () => { + stepper.querySelector('.portie-stepper-plus').addEventListener('click', function() { steppers.forEach(s => { if (s !== stepper) s.querySelector('.portie-stepper-count').textContent = '0'; }); countEl.textContent = (parseInt(countEl.textContent, 10) || 0) + 1; updateStepperTotal(); + this.classList.add('tapped'); + setTimeout(() => this.classList.remove('tapped'), 300); }); - stepper.querySelector('.portie-stepper-minus').addEventListener('click', () => { + stepper.querySelector('.portie-stepper-minus').addEventListener('click', function() { const c = parseInt(countEl.textContent, 10) || 0; if (c > 0) countEl.textContent = c - 1; updateStepperTotal(); + this.classList.add('tapped'); + setTimeout(() => this.classList.remove('tapped'), 300); }); }); } @@ -3478,7 +3466,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- document.body.appendChild(overlay); document.body.style.overflow = 'hidden'; - function closeDialog() { overlay.remove(); document.body.style.overflow = ''; } + function closeDialog() { + overlay.classList.add('closing'); + setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 280); + } overlay.querySelector('#bulkEditClose').addEventListener('click', closeDialog); overlay.querySelector('#bulkCancel').addEventListener('click', closeDialog); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDialog(); }); @@ -3553,6 +3544,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- document.getElementById('navDagboek').classList.toggle('active', tab === 'dagboek'); document.getElementById('navMaaltijden').classList.toggle('active', tab === 'maaltijden'); + const mainContent = document.getElementById('mainContent'); + mainContent.classList.add('page-transition'); + setTimeout(() => mainContent.classList.remove('page-transition'), 350); + if (tab === 'zoeken') { renderZoeken(); } else if (tab === 'dagboek') { @@ -3876,7 +3871,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- listEl.innerHTML = maaltijden.map((meal, idx) => { const itemCount = meal.items.length; return ` -
    +
    ${escapeHtml(meal.naam)}
    @@ -4079,7 +4074,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- document.body.appendChild(overlay); document.body.style.overflow = 'hidden'; - function close() { overlay.remove(); document.body.style.overflow = ''; } + function close() { + overlay.classList.add('closing'); + setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 280); + } function applyChanges(toPermanent) { const newItems = []; editItems.forEach((e, i) => { @@ -4170,7 +4168,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- document.body.appendChild(overlay); document.body.style.overflow = 'hidden'; const input = overlay.querySelector('#nameInputField'); - function close() { overlay.remove(); document.body.style.overflow = ''; } + function close() { + overlay.classList.add('closing'); + setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 280); + } function submit() { const val = input.value.trim(); close(); onSave(val || null); } overlay.querySelector('#nameInputClose').addEventListener('click', close); overlay.querySelector('#nameInputCancel').addEventListener('click', close); @@ -4231,7 +4232,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- document.body.appendChild(overlay); document.body.style.overflow = 'hidden'; - function close() { overlay.remove(); document.body.style.overflow = ''; } + function close() { + overlay.classList.add('closing'); + setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 280); + } function save() { const newItems = []; @@ -4381,7 +4385,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- `; document.body.appendChild(po); document.body.style.overflow = 'hidden'; - function closePicker() { po.remove(); document.body.style.overflow = ''; } + function closePicker() { + po.classList.add('closing'); + setTimeout(() => { po.remove(); document.body.style.overflow = ''; }, 280); + } po.querySelector('#mealPortionClose').addEventListener('click', closePicker); po.querySelector('#mealPortionCancel').addEventListener('click', closePicker); po.addEventListener('click', (e) => { if (e.target === po) closePicker(); }); @@ -4407,15 +4414,19 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- } poSteppers.forEach(stepper => { const ce = stepper.querySelector('.portie-stepper-count'); - stepper.querySelector('.portie-stepper-plus').addEventListener('click', () => { + stepper.querySelector('.portie-stepper-plus').addEventListener('click', function() { poSteppers.forEach(s => { if (s !== stepper) s.querySelector('.portie-stepper-count').textContent = '0'; }); ce.textContent = (parseInt(ce.textContent, 10) || 0) + 1; updatePoTotal(); + this.classList.add('tapped'); + setTimeout(() => this.classList.remove('tapped'), 300); }); - stepper.querySelector('.portie-stepper-minus').addEventListener('click', () => { + stepper.querySelector('.portie-stepper-minus').addEventListener('click', function() { const c = parseInt(ce.textContent, 10) || 0; if (c > 0) ce.textContent = c - 1; updatePoTotal(); + this.classList.add('tapped'); + setTimeout(() => this.classList.remove('tapped'), 300); }); }); po.querySelector('#mealPortionConfirm').addEventListener('click', () => { @@ -4656,6 +4667,17 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- } // Start the app + // Haptic feedback: vibrate on every button press (native Vibration API — Android only, iOS silently ignores) + (function setupHaptics() { + if (typeof navigator !== 'undefined' && navigator.vibrate) { + document.addEventListener('click', (e) => { + if (e.target.tagName === 'BUTTON' || e.target.closest('button')) { + navigator.vibrate(8); + } + }, { passive: true }); + } + })(); + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { diff --git a/index.html b/index.html index 90af93a..778f555 100644 --- a/index.html +++ b/index.html @@ -800,12 +800,6 @@ body { display: flex; align-items: flex-end; justify-content: center; - animation: fadeIn 0.2s; -} - -@keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } } .modal-sheet { @@ -816,15 +810,9 @@ body { max-height: 85vh; max-height: 85dvh; overflow-y: auto; - animation: slideUp 0.25s ease-out; padding-bottom: env(safe-area-inset-bottom, 0); } -@keyframes slideUp { - from { transform: translateY(100%); } - to { transform: translateY(0); } -} - .modal-handle { width: 36px; height: 4px; @@ -1824,34 +1812,6 @@ body { cursor: not-allowed; } -/* ===== Toast Notification ===== */ -.toast { - position: fixed; - bottom: 80px; - left: 50%; - transform: translateX(-50%); - background: var(--text); - color: var(--white); - padding: 10px 20px; - border-radius: 20px; - font-size: var(--font-sm); - font-weight: 500; - z-index: 300; - animation: toastIn 0.3s, toastOut 0.3s 1.7s forwards; - pointer-events: none; - max-width: calc(100% - 32px); - text-align: center; -} - -@keyframes toastIn { - from { opacity: 0; transform: translateX(-50%) translateY(10px); } - to { opacity: 1; transform: translateX(-50%) translateY(0); } -} - -@keyframes toastOut { - from { opacity: 1; } - to { opacity: 0; } -} /* ===== Utility ===== */ .hidden { @@ -1958,26 +1918,45 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- animation: slideDown 0.25s ease-in both; } -/* -- Tab content transition -- */ +/* -- Tab content transition — crossfade effect on switch only -- */ .tab-content { - animation: fadeIn 0.25s ease both; + /* no default animation; applied via JS .switching class during tab switches */ } .tab-content.switching { - animation: fadeIn 0.2s ease both; + animation: fadeIn 0.25s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +/* -- Page content transition on tab switch -- */ +.main-content.page-transition { + animation: fadeIn 0.3s ease both; } /* -- Staggered list fade-in -- */ -.search-result-item, .food-item, .meal-card, .dagboek-item, .maaltijd-item { - animation: fadeIn 0.3s ease both; +.search-result-item, .food-item, .meal-card, .dagboek-item, .maaltijd-item, +.eetmoment-card, .eetmoment-item, .meal-add-option { + animation: fadeIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; animation-delay: calc(var(--i, 0) * 50ms); } /* -- Toast notification -- */ .toast { - animation: slideInRight 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; + position: fixed; + bottom: 80px; + left: 16px; + right: 16px; + background: var(--text); + color: var(--white); + padding: 10px 20px; + border-radius: 20px; + font-size: var(--font-sm); + font-weight: 500; + z-index: 300; + animation: slideUp 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; + text-align: center; + pointer-events: none; } .toast.hiding { - animation: slideOutRight 0.3s ease-in both; + animation: slideDown 0.25s ease-in both; } /* -- Stepper -- */ @@ -2782,7 +2761,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- // Eating moments const listEl = document.getElementById('eetmomentList'); - listEl.innerHTML = EETMOMENTEN.map(moment => { + listEl.innerHTML = EETMOMENTEN.map((moment, mi) => { const items = dayData[moment.id] || []; const momentKh = berekenEetmomentKh(activeDate, moment.id); const roundedKh = Math.round(momentKh); @@ -2864,7 +2843,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- const cardOverLimit = overLimit && momentKh > 0; return ` -
  • +
  • ${moment.icon} @@ -3128,7 +3107,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- const countEl = stepper.querySelector('.portie-stepper-count'); const grams = parseInt(stepper.dataset.grams, 10); - stepper.querySelector('.portie-stepper-plus').addEventListener('click', () => { + stepper.querySelector('.portie-stepper-plus').addEventListener('click', function() { // Reset all other steppers to 0 steppers.forEach(s => { if (s !== stepper) { s.querySelector('.portie-stepper-count').textContent = '0'; } @@ -3136,12 +3115,18 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- let count = parseInt(countEl.textContent, 10) || 0; countEl.textContent = count + 1; updatePortionFromSteppers(); + // Bounce feedback + this.classList.add('tapped'); + setTimeout(() => this.classList.remove('tapped'), 300); }); - stepper.querySelector('.portie-stepper-minus').addEventListener('click', () => { + stepper.querySelector('.portie-stepper-minus').addEventListener('click', function() { let count = parseInt(countEl.textContent, 10) || 0; if (count > 0) countEl.textContent = count - 1; updatePortionFromSteppers(); + // Bounce feedback + this.classList.add('tapped'); + setTimeout(() => this.classList.remove('tapped'), 300); }); }); @@ -3298,7 +3283,6 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- document.body.appendChild(overlay); document.body.style.overflow = 'hidden'; - function closeDialog() { overlay.remove(); document.body.style.overflow = ''; } overlay.querySelector('#portieEditClose').addEventListener('click', closeDialog); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDialog(); }); @@ -3343,15 +3327,19 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- } steppers.forEach(stepper => { const countEl = stepper.querySelector('.portie-stepper-count'); - stepper.querySelector('.portie-stepper-plus').addEventListener('click', () => { + stepper.querySelector('.portie-stepper-plus').addEventListener('click', function() { steppers.forEach(s => { if (s !== stepper) s.querySelector('.portie-stepper-count').textContent = '0'; }); countEl.textContent = (parseInt(countEl.textContent, 10) || 0) + 1; updateStepperTotal(); + this.classList.add('tapped'); + setTimeout(() => this.classList.remove('tapped'), 300); }); - stepper.querySelector('.portie-stepper-minus').addEventListener('click', () => { + stepper.querySelector('.portie-stepper-minus').addEventListener('click', function() { const c = parseInt(countEl.textContent, 10) || 0; if (c > 0) countEl.textContent = c - 1; updateStepperTotal(); + this.classList.add('tapped'); + setTimeout(() => this.classList.remove('tapped'), 300); }); }); } @@ -3478,7 +3466,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- document.body.appendChild(overlay); document.body.style.overflow = 'hidden'; - function closeDialog() { overlay.remove(); document.body.style.overflow = ''; } + function closeDialog() { + overlay.classList.add('closing'); + setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 280); + } overlay.querySelector('#bulkEditClose').addEventListener('click', closeDialog); overlay.querySelector('#bulkCancel').addEventListener('click', closeDialog); overlay.addEventListener('click', (e) => { if (e.target === overlay) closeDialog(); }); @@ -3553,6 +3544,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- document.getElementById('navDagboek').classList.toggle('active', tab === 'dagboek'); document.getElementById('navMaaltijden').classList.toggle('active', tab === 'maaltijden'); + const mainContent = document.getElementById('mainContent'); + mainContent.classList.add('page-transition'); + setTimeout(() => mainContent.classList.remove('page-transition'), 350); + if (tab === 'zoeken') { renderZoeken(); } else if (tab === 'dagboek') { @@ -3876,7 +3871,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- listEl.innerHTML = maaltijden.map((meal, idx) => { const itemCount = meal.items.length; return ` -
    +
    ${escapeHtml(meal.naam)}
    @@ -4079,7 +4074,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- document.body.appendChild(overlay); document.body.style.overflow = 'hidden'; - function close() { overlay.remove(); document.body.style.overflow = ''; } + function close() { + overlay.classList.add('closing'); + setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 280); + } function applyChanges(toPermanent) { const newItems = []; editItems.forEach((e, i) => { @@ -4170,7 +4168,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- document.body.appendChild(overlay); document.body.style.overflow = 'hidden'; const input = overlay.querySelector('#nameInputField'); - function close() { overlay.remove(); document.body.style.overflow = ''; } + function close() { + overlay.classList.add('closing'); + setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 280); + } function submit() { const val = input.value.trim(); close(); onSave(val || null); } overlay.querySelector('#nameInputClose').addEventListener('click', close); overlay.querySelector('#nameInputCancel').addEventListener('click', close); @@ -4231,7 +4232,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- document.body.appendChild(overlay); document.body.style.overflow = 'hidden'; - function close() { overlay.remove(); document.body.style.overflow = ''; } + function close() { + overlay.classList.add('closing'); + setTimeout(() => { overlay.remove(); document.body.style.overflow = ''; }, 280); + } function save() { const newItems = []; @@ -4381,7 +4385,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- `; document.body.appendChild(po); document.body.style.overflow = 'hidden'; - function closePicker() { po.remove(); document.body.style.overflow = ''; } + function closePicker() { + po.classList.add('closing'); + setTimeout(() => { po.remove(); document.body.style.overflow = ''; }, 280); + } po.querySelector('#mealPortionClose').addEventListener('click', closePicker); po.querySelector('#mealPortionCancel').addEventListener('click', closePicker); po.addEventListener('click', (e) => { if (e.target === po) closePicker(); }); @@ -4407,15 +4414,19 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- } poSteppers.forEach(stepper => { const ce = stepper.querySelector('.portie-stepper-count'); - stepper.querySelector('.portie-stepper-plus').addEventListener('click', () => { + stepper.querySelector('.portie-stepper-plus').addEventListener('click', function() { poSteppers.forEach(s => { if (s !== stepper) s.querySelector('.portie-stepper-count').textContent = '0'; }); ce.textContent = (parseInt(ce.textContent, 10) || 0) + 1; updatePoTotal(); + this.classList.add('tapped'); + setTimeout(() => this.classList.remove('tapped'), 300); }); - stepper.querySelector('.portie-stepper-minus').addEventListener('click', () => { + stepper.querySelector('.portie-stepper-minus').addEventListener('click', function() { const c = parseInt(ce.textContent, 10) || 0; if (c > 0) ce.textContent = c - 1; updatePoTotal(); + this.classList.add('tapped'); + setTimeout(() => this.classList.remove('tapped'), 300); }); }); po.querySelector('#mealPortionConfirm').addEventListener('click', () => { @@ -4656,6 +4667,17 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food- } // Start the app + // Haptic feedback: vibrate on every button press (native Vibration API — Android only, iOS silently ignores) + (function setupHaptics() { + if (typeof navigator !== 'undefined' && navigator.vibrate) { + document.addEventListener('click', (e) => { + if (e.target.tagName === 'BUTTON' || e.target.closest('button')) { + navigator.vibrate(8); + } + }, { passive: true }); + } + })(); + if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { diff --git a/tests/e2e.spec.js b/tests/e2e.spec.js index 14dd42b..8c1d666 100644 --- a/tests/e2e.spec.js +++ b/tests/e2e.spec.js @@ -40,7 +40,6 @@ test.describe('Karby Eetdagboek', () => { await page.waitForTimeout(800); await page.locator('.food-item').first().click(); await page.waitForTimeout(500); - // Detail modal with Add button await expect(page.locator('#detailAddBtn')).toBeVisible({ timeout: 3000 }); }); @@ -55,11 +54,9 @@ test.describe('Karby Eetdagboek', () => { test('settings modal opens', async ({ page }) => { await page.goto(BASE); - // Click settings gear icon await page.locator('#settingsBtn').click(); await page.waitForTimeout(500); await expect(page.locator('#settingsModal')).not.toHaveClass(/hidden/, { timeout: 3000 }); - // Close await page.locator('#settingsModal .modal-close').click(); await expect(page.locator('#settingsModal')).toHaveClass(/hidden/, { timeout: 2000 }); }); @@ -73,7 +70,6 @@ test.describe('Karby Eetdagboek', () => { test('contrast: key elements have visible colors', async ({ page }) => { await page.goto(BASE); - // Nav buttons should have visible text color (not transparent) const navBtns = page.locator('.nav-btn'); const count = await navBtns.count(); expect(count).toBeGreaterThan(0); @@ -83,4 +79,313 @@ test.describe('Karby Eetdagboek', () => { } }); + // ========== NIEUWE TESTS — portie-steppers, animaties, maaltijden, datumkiezer, meal-groups ========== + + // ---------- Stepper: basis ---------- + + test('portion stepper: steppers appear in addMealModal with first count=1', async ({ page }) => { + await page.goto(BASE); + await page.locator('.search-input').fill('brood'); + await page.waitForTimeout(800); + await page.locator('.food-item').first().click(); + await page.waitForTimeout(500); + // Open add-to-meal modal (hier zitten de steppers) + await page.locator('#detailAddBtn').click(); + await page.waitForTimeout(500); + const steppers = page.locator('.portie-stepper'); + await expect(steppers.first()).toBeVisible({ timeout: 3000 }); + expect(await steppers.count()).toBeGreaterThan(0); + // Eerste stepper heeft count=1 + await expect(steppers.first().locator('.portie-stepper-count')).toHaveText('1'); + }); + + test('portion stepper: plus op tweede activeert die en reset eerste naar 0', async ({ page }) => { + await page.goto(BASE); + await page.locator('.search-input').fill('brood'); + await page.waitForTimeout(800); + await page.locator('.food-item').first().click(); + await page.waitForTimeout(500); + await page.locator('#detailAddBtn').click(); + await page.waitForTimeout(500); + const steppers = page.locator('.portie-stepper'); + const count = await steppers.count(); + if (count < 2) { test.skip(); return; } + // + op tweede stepper + await steppers.nth(1).locator('.portie-stepper-plus').click(); + await page.waitForTimeout(100); + // Eerste moet 0 zijn (gereset) + await expect(steppers.first().locator('.portie-stepper-count')).toHaveText('0'); + // Tweede moet 1 zijn + await expect(steppers.nth(1).locator('.portie-stepper-count')).toHaveText('1'); + }); + + test('portion stepper: minus verlaagt count naar 0 en wordt dan disabled', async ({ page }) => { + await page.goto(BASE); + await page.locator('.search-input').fill('brood'); + await page.waitForTimeout(800); + await page.locator('.food-item').first().click(); + await page.waitForTimeout(500); + await page.locator('#detailAddBtn').click(); + await page.waitForTimeout(500); + const firstStepper = page.locator('.portie-stepper').first(); + // Default count = 1 + await expect(firstStepper.locator('.portie-stepper-count')).toHaveText('1'); + // Minus klikken → 0 + await firstStepper.locator('.portie-stepper-minus').click(); + await expect(firstStepper.locator('.portie-stepper-count')).toHaveText('0'); + // Minus moet nu disabled zijn (niet negatief) + await expect(firstStepper.locator('.portie-stepper-minus')).toBeDisabled(); + }); + + test('portion stepper: active class na plus/minus toggles', async ({ page }) => { + await page.goto(BASE); + await page.locator('.search-input').fill('brood'); + await page.waitForTimeout(800); + await page.locator('.food-item').first().click(); + await page.waitForTimeout(500); + await page.locator('#detailAddBtn').click(); + await page.waitForTimeout(500); + const firstStepper = page.locator('.portie-stepper').first(); + // active class is pas aanwezig na updatePortionFromSteppers (triggered door +/-) + // Eerst: plus klikken om active te krijgen + await firstStepper.locator('.portie-stepper-plus').click(); + await page.waitForTimeout(100); + await expect(firstStepper).toHaveClass(/active/); + // Minus naar 0 → active verliezen + await firstStepper.locator('.portie-stepper-minus').click(); + await expect(firstStepper).not.toHaveClass(/active/); + }); + + // ---------- Modal animaties ---------- + + test('modal close animation: closing class applied during dismiss', async ({ page }) => { + await page.goto(BASE); + await page.locator('#settingsBtn').click(); + await page.waitForTimeout(500); + await expect(page.locator('#settingsModal')).not.toHaveClass(/hidden/); + await page.locator('#settingsModal .modal-close').click(); + await page.waitForTimeout(50); + await expect(page.locator('#settingsModal')).toHaveClass(/closing/); + await page.waitForTimeout(600); + await expect(page.locator('#settingsModal')).toHaveClass(/hidden/); + }); + + test('modal open animation: fadeIn en slideUp CSS animations gestart', async ({ page }) => { + await page.goto(BASE); + await page.locator('#settingsBtn').click(); + await page.waitForTimeout(100); + const overlay = page.locator('#settingsModal'); + await expect(overlay).toBeVisible(); + await expect(overlay).not.toHaveClass(/closing/); + // .modal-overlay heeft animation: fadeIn + const overlayAnim = await overlay.evaluate(el => getComputedStyle(el).animationName); + expect(overlayAnim).toBe('fadeIn'); + // .modal-sheet heeft animation: slideUp + const sheet = overlay.locator('.modal-sheet'); + const sheetAnim = await sheet.evaluate(el => getComputedStyle(el).animationName); + expect(sheetAnim).toBe('slideUp'); + // Sluiten voor opschoning + await page.locator('#settingsModal .modal-close').click(); + await page.waitForTimeout(500); + }); + + test('tab switching: switching class verschijnt tijdens crossfade', async ({ page }) => { + await page.goto(BASE); + await page.locator('#navDagboek').click(); + await page.waitForTimeout(100); + const tabEl = page.locator('#tabDagboek'); + await expect(tabEl).toHaveClass(/switching/); + await page.waitForTimeout(500); + await expect(tabEl).toBeVisible(); + }); + + // ---------- Maaltijden ---------- + + test('meal save: name input is empty by default', async ({ page }) => { + await page.goto(BASE); + // Zoek + open detail + await page.locator('.search-input').fill('brood'); + await page.waitForTimeout(800); + await page.locator('.food-item').first().click(); + await page.waitForTimeout(500); + // Toevoegen aan eetmoment + await page.locator('#detailAddBtn').click(); + await page.waitForTimeout(500); + // Terug naar dagboek (sluit addMealModal eerst) + await page.locator('#addMealModal .modal-close').click(); + await page.waitForTimeout(300); + // Sluit detail modal + await page.locator('#detailModal .modal-close').click(); + await page.waitForTimeout(300); + // Naar dagboek tab + await page.locator('#navDagboek').click(); + await page.waitForTimeout(600); + // Zoek save-meal btn en klik + const saveBtn = page.locator('.save-meal-btn'); + if (await saveBtn.count() === 0) { + test.skip('geen items in dagboek'); + return; + } + await saveBtn.first().click(); + await page.waitForTimeout(500); + const nameInput = page.locator('#mealNameInput'); + await expect(nameInput).toBeVisible({ timeout: 5000 }); + // Naamveld moet leeg zijn + await expect(nameInput).toHaveValue(''); + // Annuleren + await page.locator('#mealNameCancel').click(); + await page.waitForTimeout(300); + }); + + test('meal save: empty name toont rode rand en modal blijft open', async ({ page }) => { + await page.goto(BASE); + // Zoek + open detail + await page.locator('.search-input').fill('brood'); + await page.waitForTimeout(800); + await page.locator('.food-item').first().click(); + await page.waitForTimeout(500); + // Toevoegen aan eetmoment + await page.locator('#detailAddBtn').click(); + await page.waitForTimeout(500); + // Sluit addMealModal + await page.locator('#addMealModal .modal-close').click(); + await page.waitForTimeout(300); + // Sluit detail modal + await page.locator('#detailModal .modal-close').click(); + await page.waitForTimeout(300); + // Naar dagboek + await page.locator('#navDagboek').click(); + await page.waitForTimeout(600); + // Opslaan als maaltijd + const saveBtn = page.locator('.save-meal-btn'); + if (await saveBtn.count() === 0) { + test.skip('geen items in dagboek'); + return; + } + await saveBtn.first().click(); + await page.waitForTimeout(500); + const nameInput = page.locator('#mealNameInput'); + await expect(nameInput).toBeVisible({ timeout: 5000 }); + await expect(nameInput).toHaveValue(''); + // Opslaan zonder naam + await page.locator('#mealNameSave').click(); + await page.waitForTimeout(300); + // Modal moet nog open zijn (niet gesloten) + await expect(nameInput).toBeVisible(); + // Input border moet rood zijn (validation) + const borderColor = await nameInput.evaluate(el => getComputedStyle(el).borderColor); + const rgb = borderColor.match(/\d+/g).map(Number); + expect(rgb[0]).toBeGreaterThan(150); // R > 150 + expect(rgb[1]).toBeLessThan(100); // G < 100 + expect(rgb[2]).toBeLessThan(100); // B < 100 + // Annuleren + await page.locator('#mealNameCancel').click(); + await page.waitForTimeout(300); + }); + + // ---------- Datumkiezer ---------- + + test('date navigator: next/prev verandert datum en today reset', async ({ page }) => { + await page.goto(BASE); + await page.locator('#navDagboek').click(); + await page.waitForTimeout(500); + const initialDay = await page.locator('#dayLabel').textContent(); + // Next day (tweede .day-nav-btn) + const nextBtn = page.locator('.day-nav-btn').nth(1); + await nextBtn.click(); + await page.waitForTimeout(300); + const nextDay = await page.locator('#dayLabel').textContent(); + expect(nextDay).not.toBe(initialDay); + // Today reset + await page.locator('.day-nav-today').click(); + await page.waitForTimeout(300); + const todayDay = await page.locator('#dayLabel').textContent(); + expect(todayDay).toBe(initialDay); + }); + + // ---------- Zoek animaties ---------- + + test('zoektab: food items krijgen staggered animation indices', async ({ page }) => { + await page.goto(BASE); + await page.locator('.search-input').fill('k'); + await page.waitForTimeout(800); + const items = page.locator('.food-item'); + const count = await items.count(); + if (count >= 2) { + const i0 = await items.first().evaluate(el => el.style.getPropertyValue('--i')); + expect(i0.trim()).toBe('0'); + const i1 = await items.nth(1).evaluate(el => el.style.getPropertyValue('--i')); + expect(i1.trim()).toBe('1'); + } + }); + + test('button press: buttons hebben transition transform bij indrukken', async ({ page }) => { + await page.goto(BASE); + const btn = page.locator('button').first(); + const transition = await btn.evaluate(el => getComputedStyle(el).transition); + expect(transition).toBeTruthy(); + expect(transition).toContain('transform'); + }); + + // ---------- Meal-group uitklappen in dagboek ---------- + + test('meal-group uitklappen: saved meal toggles expand/collapse in dagboek', async ({ page }) => { + await page.goto(BASE); + // Stap 1: Zoek voedingsmiddel + await page.locator('.search-input').fill('brood'); + await page.waitForTimeout(800); + await page.locator('.food-item').first().click(); + await page.waitForTimeout(500); + // Stap 2: Voeg toe aan eetmoment + await page.locator('#detailAddBtn').click(); + await page.waitForTimeout(500); + // Sluit addMealModal + await page.locator('#addMealModal .modal-close').click(); + await page.waitForTimeout(300); + // Sluit detail modal (anders overlay intercepteert nav knoppen) + await page.locator('#detailModal .modal-close').click(); + await page.waitForTimeout(300); + // Stap 3: Naar dagboek + await page.locator('#navDagboek').click(); + await page.waitForTimeout(600); + // Stap 4: Opslaan als maaltijd + const saveBtn = page.locator('.save-meal-btn'); + if (await saveBtn.count() === 0) { + test.skip('geen items in dagboek — kan geen maaltijd opslaan'); + return; + } + await saveBtn.first().click(); + await page.waitForTimeout(500); + // Stap 5: Naam invullen en opslaan + const nameInput = page.locator('#mealNameInput'); + await expect(nameInput).toBeVisible({ timeout: 5000 }); + await nameInput.fill('Test ontbijt'); + await page.locator('#mealNameSave').click(); + await page.waitForTimeout(600); + // De meal wordt opgeslagen, toast verschijnt + // Stap 6: Ga terug naar dagboek om meal-group te zien + await page.locator('#navDagboek').click(); + await page.waitForTimeout(800); + // Stap 7: Zoek de meal-group + const mealGroup = page.locator('.eetmoment-meal-group').first(); + await expect(mealGroup).toBeVisible({ timeout: 5000 }); + // Stap 8: Items-wrap begint collapsed (hidden class) + const itemsWrap = mealGroup.locator('.eetmoment-meal-items-wrap'); + await expect(itemsWrap).toHaveClass(/hidden/); + // Stap 9: Klik op 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/); + // Chevron moet naar boven wijzen + const chevronUp = header.locator('i.fa-chevron-up'); + await expect(chevronUp).toBeVisible(); + // Stap 10: Klik opnieuw om in te klappen + await header.click(); + await page.waitForTimeout(300); + await expect(itemsWrap).toHaveClass(/hidden/); + const chevronDown = header.locator('i.fa-chevron-down'); + await expect(chevronDown).toBeVisible(); + }); + });