feat: toast positioned dynamically at click Y-coordinate — slides out right from interaction point

This commit is contained in:
cas 2026-07-24 02:22:55 +02:00
parent 1bb0a94bdd
commit e09cf22b77
3 changed files with 50 additions and 16 deletions

View file

@ -2341,6 +2341,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
let dagboek = {}; // { "2026-07-23": { "Ontbijt": [{item, portie}, ...], ... } }
let activeDate = formatDate(new Date());
let activeTab = 'zoeken';
let lastClickY = null; // tracked for toast positioning
let selectedCategory = null; // null = alle
let searchQuery = '';
let searchIndex = null; // Map<item, expanded search tokens string>
@ -4139,6 +4140,13 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
toast.className = 'toast';
toast.textContent = message;
container.appendChild(toast);
// Position at last click Y, or default bottom
const y = lastClickY || window.innerHeight - 120;
const h = toast.offsetHeight || 50;
const maxY = window.innerHeight - h - 16;
const top = Math.min(y, maxY);
toast.style.top = top + 'px';
toast.style.bottom = 'auto';
setTimeout(() => {
toast.classList.add('hiding');
setTimeout(() => toast.remove(), 300);
@ -4667,15 +4675,16 @@ 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)
// Track last click position for toast positioning + haptic feedback
(function setupHaptics() {
if (typeof navigator !== 'undefined' && navigator.vibrate) {
document.addEventListener('click', (e) => {
document.addEventListener('click', (e) => {
lastClickY = e.clientY;
if (typeof navigator !== 'undefined' && navigator.vibrate) {
if (e.target.tagName === 'BUTTON' || e.target.closest('button')) {
navigator.vibrate(8);
}
}, { passive: true });
}
}
}, { passive: true });
})();
if (document.readyState === 'loading') {

View file

@ -2341,6 +2341,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
let dagboek = {}; // { "2026-07-23": { "Ontbijt": [{item, portie}, ...], ... } }
let activeDate = formatDate(new Date());
let activeTab = 'zoeken';
let lastClickY = null; // tracked for toast positioning
let selectedCategory = null; // null = alle
let searchQuery = '';
let searchIndex = null; // Map<item, expanded search tokens string>
@ -4139,6 +4140,13 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
toast.className = 'toast';
toast.textContent = message;
container.appendChild(toast);
// Position at last click Y, or default bottom
const y = lastClickY || window.innerHeight - 120;
const h = toast.offsetHeight || 50;
const maxY = window.innerHeight - h - 16;
const top = Math.min(y, maxY);
toast.style.top = top + 'px';
toast.style.bottom = 'auto';
setTimeout(() => {
toast.classList.add('hiding');
setTimeout(() => toast.remove(), 300);
@ -4667,15 +4675,16 @@ 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)
// Track last click position for toast positioning + haptic feedback
(function setupHaptics() {
if (typeof navigator !== 'undefined' && navigator.vibrate) {
document.addEventListener('click', (e) => {
document.addEventListener('click', (e) => {
lastClickY = e.clientY;
if (typeof navigator !== 'undefined' && navigator.vibrate) {
if (e.target.tagName === 'BUTTON' || e.target.closest('button')) {
navigator.vibrate(8);
}
}, { passive: true });
}
}
}, { passive: true });
})();
if (document.readyState === 'loading') {

View file

@ -290,29 +290,45 @@ test.describe('Karby Eetdagboek', () => {
test('meal-group uitklappen: saved meal toggles expand/collapse in dagboek', async ({ page }) => {
await page.goto(BASE);
// Stap 1: Zoek voedingsmiddel
// Stap 1: Zoek voedingsmiddel en voeg toe aan dagboek
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 via detailAddBtn → addMealModal
await page.locator('#detailAddBtn').click();
await page.waitForTimeout(500);
// Stap 3: Klik Toevoegen in addMealModal → item in dagboek
await page.locator('#addMealSubmit').click();
await page.waitForTimeout(800);
// Stap 4: Opslaan als maaltijd
// Stap 2: Sla op als maaltijd
const saveBtn = page.locator('.save-meal-btn');
await expect(saveBtn.first()).toBeVisible({ timeout: 5000 });
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);
// Stap 6: Ga naar dagboek om meal-group te zien
// Stap 3: Ga naar maaltijden tab om de maaltijd toe te voegen aan dagboek
await page.locator('#navMaaltijden').click();
await page.waitForTimeout(600);
// Stap 4: Klik "Toevoegen aan dagboek" op de opgeslagen maaltijd
const mealCardAdd = page.locator('.meal-card-add').first();
await expect(mealCardAdd).toBeVisible({ timeout: 5000 });
await mealCardAdd.click();
await page.waitForTimeout(500);
// Stap 5: Klik op een eetmoment-optie in de modal (bv. Ontbijt)
const mealOption = page.locator('.meal-add-option').first();
await expect(mealOption).toBeVisible({ timeout: 3000 });
await mealOption.click();
await page.waitForTimeout(600);
// Sluit eventuele resteerde modals door op de overlay te klikken
const anyOverlay = page.locator('.modal-overlay:not(.hidden)').first();
if (await anyOverlay.isVisible().catch(() => false)) {
await anyOverlay.click({ position: { x: 10, y: 10 } });
await page.waitForTimeout(300);
}
// Stap 6: Ga naar dagboek om de meal-group te zien
await page.locator('#navDagboek').click();
await page.waitForTimeout(800);
// Stap 7: Zoek meal-group