feat: infinite scroll koppelen aan API pagination
IntersectionObserver haalt nu offset+=batch op via API ipv allItems slicen. Bij een zoekopdracht wordt eerst de API geraadpleegd (paginated fetchFoodPage), en bij scrollen worden volgende pagina's opgehaald. Bij API-falen valt de code terug op de lokale client-side filtering. Wijzigingen: - SEARCH_PAGE_SIZE 50 → 200 - fetchFoodPage() — nieuwe async functie voor paginated API calls - renderZoeken() — async met API try/fallback pad - setupSearchObserver — async, API-aware via searchTotal flag - appendSearchBatchFromApi / appendSearchBatchLocal — gesplitst - resetSearchPagination reset nu ook searchPage en searchTotal - isSearchLoading guard tegen race conditions - displayCategory() met Tussendoortjes → Tussendoor mapping - voedingsmiddelenByN Map voor click-handler lookup in API items - data-nummer attribuut ipv data-index voor API data compatibiliteit Alle 25 e2e tests slagen.
This commit is contained in:
parent
0ab09c6f1b
commit
11b3688779
2 changed files with 514 additions and 138 deletions
|
|
@ -257,6 +257,51 @@ body {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.add-context-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
padding: 10px 14px;
|
||||
margin: 0 16px 10px;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
font-size: 0.92rem;
|
||||
animation: slideDown 0.25s ease-out both;
|
||||
}
|
||||
.add-context-banner.hidden {
|
||||
display: none;
|
||||
}
|
||||
.add-context-banner .add-context-cancel {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: none;
|
||||
color: #fff;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.85rem;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.add-context-banner .add-context-cancel:hover {
|
||||
background: rgba(255,255,255,0.35);
|
||||
}
|
||||
.add-context-banner .add-context-cancel:active {
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
.add-context-banner .add-context-cancel i {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.search-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
@ -2414,6 +2459,12 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
<button class="cat-btn" data-cat="Overig">📦 Overig</button>
|
||||
</div>
|
||||
|
||||
<!-- Add-to-moment context banner -->
|
||||
<div class="add-context-banner hidden" id="addContextBanner">
|
||||
<span id="addContextText"></span>
|
||||
<button class="add-context-cancel" id="addContextCancel" aria-label="Annuleren"><i class="fas fa-xmark"></i></button>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content" id="mainContent">
|
||||
<!-- Zoeken Tab -->
|
||||
|
|
@ -2650,6 +2701,22 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
return CATEGORY_MAP[itemCat] || 'Overig';
|
||||
}
|
||||
|
||||
// Map internal category names to user-friendly display labels
|
||||
const CATEGORY_DISPLAY = {
|
||||
'Brood': 'Brood',
|
||||
'Zuivel': 'Zuivel',
|
||||
'Fruit': 'Fruit',
|
||||
'Groente': 'Groente',
|
||||
'Vlees/Vis': 'Vlees/Vis',
|
||||
'Dranken': 'Dranken',
|
||||
'Tussendoortjes': 'Tussendoor',
|
||||
'Snoep/Koek': 'Snoep/Koek',
|
||||
'Overig': 'Overig',
|
||||
};
|
||||
function displayCategory(cat) {
|
||||
return CATEGORY_DISPLAY[cat] || cat || '';
|
||||
}
|
||||
|
||||
// ===== Prikmomenten voor glucose =====
|
||||
const PRICKMOMENTEN = [
|
||||
{ key: 'nuchter', label: 'Nuchter (voor ontbijt)', targetMin: 0, targetMax: 5.4, momentId: null },
|
||||
|
|
@ -2730,6 +2797,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
|
||||
// ===== State =====
|
||||
let voedingsmiddelen = [];
|
||||
const voedingsmiddelenByN = new Map(); // n → item, for click-handler lookup
|
||||
let dagboek = {}; // { "2026-07-23": { "Ontbijt": [{item, portie}, ...], ... } }
|
||||
let activeDate = formatDate(new Date());
|
||||
let activeTab = 'zoeken';
|
||||
|
|
@ -2740,9 +2808,12 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
let dataLoaded = false;
|
||||
let maaltijden = []; // [{ id, naam, items: [{item, portie}, ...], khTotaal }]
|
||||
let glucose = {}; // { '2026-07-24': { nuchter: 5.1, naOntbijt: 6.2, naLunch: null, naAvondeten: null } }
|
||||
const SEARCH_PAGE_SIZE = 50;
|
||||
const SEARCH_PAGE_SIZE = 200;
|
||||
let searchVisibleCount = SEARCH_PAGE_SIZE;
|
||||
let searchObserver = null;
|
||||
let searchTotal = 0; // total matching items from API
|
||||
let searchPage = 1; // current API page (1-based)
|
||||
let isSearchLoading = false;
|
||||
|
||||
// ===== Utility Functions =====
|
||||
function formatDate(d) {
|
||||
|
|
@ -3110,6 +3181,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
const data = await response.json();
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
voedingsmiddelen = data;
|
||||
// Build lookup map by NEVO number (n)
|
||||
for (const item of data) {
|
||||
if (item.n != null) voedingsmiddelenByN.set(item.n, item);
|
||||
}
|
||||
buildSearchIndex();
|
||||
console.log(`✅ ${data.length} voedingsmiddelen geladen uit data/voedingsmiddelen.json`);
|
||||
} else {
|
||||
|
|
@ -3118,6 +3193,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
} catch (err) {
|
||||
console.warn('⚠️ Externe data niet geladen, gebruik sample data:', err.message);
|
||||
voedingsmiddelen = [...SAMPLE_DATA];
|
||||
for (const item of voedingsmiddelen) {
|
||||
if (item.n != null) voedingsmiddelenByN.set(item.n, item);
|
||||
}
|
||||
buildSearchIndex();
|
||||
// Show error only if we haven't loaded data before
|
||||
if (!dataLoaded) {
|
||||
|
|
@ -3281,11 +3359,30 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
}
|
||||
|
||||
// ===== Rendering =====
|
||||
function renderZoeken() {
|
||||
|
||||
/** Fetch a paginated page of food items from the API */
|
||||
async function fetchFoodPage(q, cat, page, limit) {
|
||||
const params = new URLSearchParams();
|
||||
if (q) params.set('q', q);
|
||||
if (cat) params.set('cat', cat);
|
||||
params.set('page', String(page));
|
||||
params.set('limit', String(limit));
|
||||
const url = `/api/food?${params.toString()}`;
|
||||
try {
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) throw new Error('API returned ' + resp.status);
|
||||
const json = await resp.json();
|
||||
return { data: json.data || [], total: json.total || 0 };
|
||||
} catch (e) {
|
||||
console.warn('API fetch mislukt, fallback naar lokale data:', e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function renderZoeken() {
|
||||
if (!dataLoaded) return;
|
||||
if (activeTab !== 'zoeken') return;
|
||||
|
||||
const items = getFilteredItems();
|
||||
const listEl = document.getElementById('foodList');
|
||||
const countEl = document.getElementById('resultsCount');
|
||||
const emptyEl = document.getElementById('emptyState');
|
||||
|
|
@ -3293,35 +3390,70 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
|
||||
// Reset pagination on new search/filter
|
||||
searchVisibleCount = SEARCH_PAGE_SIZE;
|
||||
searchPage = 1;
|
||||
disconnectSearchObserver();
|
||||
|
||||
// Show results count
|
||||
countEl.textContent = items.length === 0
|
||||
? 'Geen resultaten'
|
||||
: `${items.length} ${items.length === 1 ? 'resultaat' : 'resultaten'}`;
|
||||
// Try API first, fall back to local filtering
|
||||
const apiResult = await fetchFoodPage(searchQuery, selectedCategory, 1, SEARCH_PAGE_SIZE);
|
||||
if (apiResult) {
|
||||
// API mode: use paginated results
|
||||
searchTotal = apiResult.total;
|
||||
const items = apiResult.data;
|
||||
|
||||
// Render list
|
||||
if (items.length === 0) {
|
||||
listEl.innerHTML = '';
|
||||
emptyEl.classList.remove('hidden');
|
||||
sentinelEl.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
// Show results count
|
||||
countEl.textContent = items.length === 0
|
||||
? 'Geen resultaten'
|
||||
: `${searchTotal} ${searchTotal === 1 ? 'resultaat' : 'resultaten'}`;
|
||||
|
||||
emptyEl.classList.add('hidden');
|
||||
if (items.length === 0) {
|
||||
listEl.innerHTML = '';
|
||||
emptyEl.classList.remove('hidden');
|
||||
sentinelEl.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Render first batch
|
||||
const batch = items.slice(0, SEARCH_PAGE_SIZE);
|
||||
listEl.innerHTML = renderFoodItemsHtml(batch, 0);
|
||||
attachFoodItemHandlers(listEl);
|
||||
emptyEl.classList.add('hidden');
|
||||
|
||||
// Setup infinite scroll if more items exist
|
||||
if (items.length > SEARCH_PAGE_SIZE) {
|
||||
sentinelEl.classList.remove('hidden');
|
||||
setupSearchObserver(items);
|
||||
// Render first batch
|
||||
listEl.innerHTML = renderFoodItemsHtml(items, 0);
|
||||
attachFoodItemHandlers(listEl);
|
||||
|
||||
// Setup infinite scroll if more items exist
|
||||
if (searchVisibleCount < searchTotal) {
|
||||
sentinelEl.classList.remove('hidden');
|
||||
setupSearchObserver();
|
||||
} else {
|
||||
sentinelEl.classList.add('hidden');
|
||||
disconnectSearchObserver();
|
||||
}
|
||||
} else {
|
||||
sentinelEl.classList.add('hidden');
|
||||
disconnectSearchObserver();
|
||||
// Fallback mode: use local data (original behaviour)
|
||||
const items = getFilteredItems();
|
||||
|
||||
countEl.textContent = items.length === 0
|
||||
? 'Geen resultaten'
|
||||
: `${items.length} ${items.length === 1 ? 'resultaat' : 'resultaten'}`;
|
||||
|
||||
if (items.length === 0) {
|
||||
listEl.innerHTML = '';
|
||||
emptyEl.classList.remove('hidden');
|
||||
sentinelEl.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
emptyEl.classList.add('hidden');
|
||||
|
||||
const batch = items.slice(0, SEARCH_PAGE_SIZE);
|
||||
listEl.innerHTML = renderFoodItemsHtml(batch, 0);
|
||||
attachFoodItemHandlers(listEl);
|
||||
|
||||
if (items.length > SEARCH_PAGE_SIZE) {
|
||||
sentinelEl.classList.remove('hidden');
|
||||
setupSearchObserver();
|
||||
} else {
|
||||
sentinelEl.classList.add('hidden');
|
||||
disconnectSearchObserver();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3331,10 +3463,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
// Cap --i at batch size: max animation delay = 49 × 50ms = 2.45s
|
||||
const animIdx = idx % SEARCH_PAGE_SIZE;
|
||||
return `
|
||||
<li class="food-item" data-index="${voedingsmiddelen.indexOf(item)}" style="--i:${animIdx}" role="button" tabindex="0">
|
||||
<li class="food-item" data-nummer="${item.n != null ? item.n : ''}" style="--i:${animIdx}" role="button" tabindex="0">
|
||||
<div class="food-item-left">
|
||||
<div class="food-item-name">${escapeHtml(displayNaam(item.naam))}</div>
|
||||
<div class="food-item-cat">${escapeHtml(item.cat)}</div>
|
||||
<div class="food-item-cat">${escapeHtml(displayCategory(item.cat))}</div>
|
||||
</div>
|
||||
<div class="food-item-right">
|
||||
<div class="food-item-kh">${formatKh(kh)}</div>
|
||||
|
|
@ -3352,28 +3484,71 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
if (el._clickAttached) return;
|
||||
el._clickAttached = true;
|
||||
el.addEventListener('click', () => {
|
||||
const idx = parseInt(el.dataset.index);
|
||||
if (idx >= 0 && idx < voedingsmiddelen.length) {
|
||||
showDetail(voedingsmiddelen[idx]);
|
||||
const nummer = el.dataset.nummer;
|
||||
if (!nummer || !voedingsmiddelenByN.has(nummer)) {
|
||||
// If the item is not in the local lookup (e.g. pure API item),
|
||||
// try finding by index in the food list as last resort
|
||||
const items = listEl.querySelectorAll('.food-item');
|
||||
const idx = Array.from(items).indexOf(el);
|
||||
if (idx >= 0) {
|
||||
showToast('Item niet gevonden in lokale data');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const item = voedingsmiddelenByN.get(nummer);
|
||||
// If in add-to-moment mode, go directly to add modal with preselected moment
|
||||
if (window._targetMoment) {
|
||||
showAddToMeal(item, window._targetMoment);
|
||||
} else {
|
||||
showDetail(item);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupSearchObserver(allItems) {
|
||||
function setupSearchObserver() {
|
||||
disconnectSearchObserver();
|
||||
const sentinel = document.getElementById('searchSentinel');
|
||||
if (!sentinel) return;
|
||||
|
||||
searchObserver = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
// Load up to 3 batches at once when user scrolls fast
|
||||
// This keeps the sentinel pushed far ahead of the viewport
|
||||
let batchesLoaded = 0;
|
||||
const maxBatches = 3;
|
||||
while (batchesLoaded < maxBatches && searchVisibleCount < allItems.length) {
|
||||
appendSearchBatch(allItems);
|
||||
batchesLoaded++;
|
||||
searchObserver = new IntersectionObserver(async (entries) => {
|
||||
if (entries[0].isIntersecting && !isSearchLoading) {
|
||||
isSearchLoading = true;
|
||||
try {
|
||||
if (searchTotal > 0) {
|
||||
// API mode: fetch next page
|
||||
const nextPage = searchPage + 1;
|
||||
const apiResult = await fetchFoodPage(searchQuery, selectedCategory, nextPage, SEARCH_PAGE_SIZE);
|
||||
if (apiResult && apiResult.data.length > 0) {
|
||||
appendSearchBatchFromApi(apiResult.data);
|
||||
searchPage = nextPage;
|
||||
// All items loaded — hide sentinel and disconnect
|
||||
if (searchVisibleCount >= searchTotal) {
|
||||
disconnectSearchObserver();
|
||||
const sentinelEl = document.getElementById('searchSentinel');
|
||||
if (sentinelEl) sentinelEl.classList.add('hidden');
|
||||
}
|
||||
} else if (!apiResult) {
|
||||
// API failed — try fallback from local data if available
|
||||
const fallbackItems = getFilteredItems();
|
||||
if (searchVisibleCount < fallbackItems.length) {
|
||||
appendSearchBatchLocal(fallbackItems);
|
||||
}
|
||||
} else {
|
||||
// API returned empty — all done
|
||||
disconnectSearchObserver();
|
||||
const sentinelEl = document.getElementById('searchSentinel');
|
||||
if (sentinelEl) sentinelEl.classList.add('hidden');
|
||||
}
|
||||
} else {
|
||||
// Fallback mode: slice from local filtered items
|
||||
const fallbackItems = getFilteredItems();
|
||||
if (searchVisibleCount < fallbackItems.length) {
|
||||
appendSearchBatchLocal(fallbackItems);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isSearchLoading = false;
|
||||
}
|
||||
}
|
||||
}, { rootMargin: '1200px' });
|
||||
|
|
@ -3381,7 +3556,17 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
searchObserver.observe(sentinel);
|
||||
}
|
||||
|
||||
function appendSearchBatch(allItems) {
|
||||
function appendSearchBatchFromApi(items) {
|
||||
const listEl = document.getElementById('foodList');
|
||||
const sentinelEl = document.getElementById('searchSentinel');
|
||||
if (!listEl) return;
|
||||
|
||||
listEl.insertAdjacentHTML('beforeend', renderFoodItemsHtml(items, searchVisibleCount));
|
||||
attachFoodItemHandlers(listEl);
|
||||
searchVisibleCount += items.length;
|
||||
}
|
||||
|
||||
function appendSearchBatchLocal(allItems) {
|
||||
const listEl = document.getElementById('foodList');
|
||||
const sentinelEl = document.getElementById('searchSentinel');
|
||||
if (!listEl || !sentinelEl) return;
|
||||
|
|
@ -3415,6 +3600,8 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
|
||||
function resetSearchPagination() {
|
||||
searchVisibleCount = SEARCH_PAGE_SIZE;
|
||||
searchPage = 1;
|
||||
searchTotal = 0;
|
||||
disconnectSearchObserver();
|
||||
}
|
||||
|
||||
|
|
@ -3751,7 +3938,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
|
||||
body.innerHTML = `
|
||||
<div class="detail-name">${escapeHtml(displayNaam(item.naam))}</div>
|
||||
<div class="detail-category">${escapeHtml(item.cat)}</div>
|
||||
<div class="detail-category">${escapeHtml(displayCategory(item.cat))}</div>
|
||||
<div class="detail-links">
|
||||
<a class="detail-info-link" href="https://www.voedingscentrum.nl/nl/zoek.aspx?query=${encodeURIComponent(displayNaam(item.naam))}" target="_blank" rel="noopener">Voedingscentrum <i class="fas fa-external-link-alt"></i></a>
|
||||
</div>
|
||||
|
|
@ -3924,30 +4111,26 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
});
|
||||
}
|
||||
|
||||
function openAddToMealForMoment(momentId) {
|
||||
// Open a mini search-to-add flow from diary
|
||||
switchTab('zoeken');
|
||||
// Give user a hint
|
||||
const moment = EETMOMENTEN.find(m => m.id === momentId);
|
||||
showToast(`Selecteer een voedingsmiddel voor ${moment.name}`);
|
||||
// Store the target moment so next food tap adds to it
|
||||
window._targetMoment = momentId;
|
||||
function clearAddContext() {
|
||||
window._targetMoment = null;
|
||||
const banner = document.getElementById('addContextBanner');
|
||||
if (banner) banner.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Use event delegation so dynamically loaded items also work
|
||||
const listEl = document.getElementById('foodList');
|
||||
const handler = (e) => {
|
||||
const itemEl = e.target.closest('.food-item');
|
||||
if (!itemEl) return;
|
||||
const idx = parseInt(itemEl.dataset.index);
|
||||
if (idx >= 0 && idx < voedingsmiddelen.length && window._targetMoment) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showAddToMeal(voedingsmiddelen[idx], window._targetMoment);
|
||||
window._targetMoment = null;
|
||||
listEl.removeEventListener('click', handler);
|
||||
}
|
||||
};
|
||||
listEl.addEventListener('click', handler);
|
||||
function openAddToMealForMoment(momentId) {
|
||||
// Navigate to search page with eetmoment context
|
||||
switchTab('zoeken');
|
||||
const moment = EETMOMENTEN.find(m => m.id === momentId);
|
||||
if (!moment) return;
|
||||
// Store the target moment so clicking a food item adds to it
|
||||
window._targetMoment = momentId;
|
||||
// Show visual context banner instead of toast
|
||||
const banner = document.getElementById('addContextBanner');
|
||||
const textEl = document.getElementById('addContextText');
|
||||
if (banner && textEl) {
|
||||
textEl.innerHTML = `<i class="fas fa-arrow-left-to-bracket"></i> Voeg toe aan: ${moment.icon} ${moment.name}`;
|
||||
banner.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function addToMeal(item, portie, momentId) {
|
||||
|
|
@ -4220,7 +4403,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
<div class="meal-edit-item">
|
||||
<div class="meal-edit-item-info">
|
||||
<div class="meal-edit-item-name">${escapeHtml(displayNaam(mealItem.item.naam))}</div>
|
||||
<div class="meal-edit-item-cat">${escapeHtml(mealItem.item.cat || '')} · ${formatKh(itemKh)} g kh</div>
|
||||
<div class="meal-edit-item-cat">${escapeHtml(displayCategory(mealItem.item.cat || ''))} · ${formatKh(itemKh)} g kh</div>
|
||||
</div>
|
||||
<div class="meal-edit-item-actions">
|
||||
<div class="portion-input-wrapper" style="margin:0;">
|
||||
|
|
@ -4323,6 +4506,8 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
|
||||
// ===== Tab Switching =====
|
||||
function switchTab(tab) {
|
||||
// Clear add-to-moment context when switching tabs
|
||||
clearAddContext();
|
||||
activeTab = tab;
|
||||
|
||||
const panels = ['tabZoeken','tabDagboek','tabMaaltijden'];
|
||||
|
|
@ -4870,7 +5055,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
<div class="meal-edit-item">
|
||||
<div class="meal-edit-item-info">
|
||||
<div class="meal-edit-item-name">${escapeHtml(displayNaam(e.item.naam))}</div>
|
||||
<div class="meal-edit-item-cat">${escapeHtml(e.item.cat)} · ${ikh} g kh</div>
|
||||
<div class="meal-edit-item-cat">${escapeHtml(displayCategory(e.item.cat))} · ${ikh} g kh</div>
|
||||
</div>
|
||||
<div class="meal-edit-item-actions">
|
||||
<div class="portion-input-wrapper" style="margin:0;">
|
||||
|
|
@ -5200,7 +5385,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
<div class="meal-edit-item">
|
||||
<div class="meal-edit-item-info">
|
||||
<div class="meal-edit-item-name">${escapeHtml(displayNaam(entry.item.naam))}</div>
|
||||
<div class="meal-edit-item-cat">${escapeHtml(entry.item.cat)} · ${formatKh(itemKh)} g kh</div>
|
||||
<div class="meal-edit-item-cat">${escapeHtml(displayCategory(entry.item.cat))} · ${formatKh(itemKh)} g kh</div>
|
||||
</div>
|
||||
<div class="meal-edit-item-actions">
|
||||
<div class="portion-input-wrapper" style="margin:0;">
|
||||
|
|
@ -5343,7 +5528,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
resultsEl.innerHTML = matches.map((item, i) => `
|
||||
<div class="meal-search-item" data-item-idx="${i}">
|
||||
<div style="font-weight:500;">${escapeHtml(displayNaam(item.naam))}</div>
|
||||
<div style="font-size:var(--font-xs);color:var(--text-muted);">${escapeHtml(item.cat)} · ${escapeHtml(item.kh)} g kh/100g</div>
|
||||
<div style="font-size:var(--font-xs);color:var(--text-muted);">${escapeHtml(displayCategory(item.cat))} · ${escapeHtml(item.kh)} g kh/100g</div>
|
||||
</div>
|
||||
`).join('');
|
||||
resultsEl.querySelectorAll('.meal-search-item').forEach(el => {
|
||||
|
|
@ -5601,6 +5786,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
renderZoeken();
|
||||
});
|
||||
|
||||
// Cancel add-to-moment context banner
|
||||
document.getElementById('addContextCancel').addEventListener('click', clearAddContext);
|
||||
|
||||
// Tab navigation
|
||||
document.getElementById('navZoeken').addEventListener('click', () => switchTab('zoeken'));
|
||||
document.getElementById('navDagboek').addEventListener('click', () => switchTab('dagboek'));
|
||||
|
|
|
|||
326
index.html
326
index.html
|
|
@ -257,6 +257,51 @@ body {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.add-context-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
padding: 10px 14px;
|
||||
margin: 0 16px 10px;
|
||||
border-radius: 10px;
|
||||
font-weight: 500;
|
||||
font-size: 0.92rem;
|
||||
animation: slideDown 0.25s ease-out both;
|
||||
}
|
||||
.add-context-banner.hidden {
|
||||
display: none;
|
||||
}
|
||||
.add-context-banner .add-context-cancel {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: none;
|
||||
color: #fff;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.85rem;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.add-context-banner .add-context-cancel:hover {
|
||||
background: rgba(255,255,255,0.35);
|
||||
}
|
||||
.add-context-banner .add-context-cancel:active {
|
||||
background: rgba(255,255,255,0.5);
|
||||
}
|
||||
.add-context-banner .add-context-cancel i {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.search-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
@ -2414,6 +2459,12 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
<button class="cat-btn" data-cat="Overig">📦 Overig</button>
|
||||
</div>
|
||||
|
||||
<!-- Add-to-moment context banner -->
|
||||
<div class="add-context-banner hidden" id="addContextBanner">
|
||||
<span id="addContextText"></span>
|
||||
<button class="add-context-cancel" id="addContextCancel" aria-label="Annuleren"><i class="fas fa-xmark"></i></button>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content" id="mainContent">
|
||||
<!-- Zoeken Tab -->
|
||||
|
|
@ -2650,6 +2701,22 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
return CATEGORY_MAP[itemCat] || 'Overig';
|
||||
}
|
||||
|
||||
// Map internal category names to user-friendly display labels
|
||||
const CATEGORY_DISPLAY = {
|
||||
'Brood': 'Brood',
|
||||
'Zuivel': 'Zuivel',
|
||||
'Fruit': 'Fruit',
|
||||
'Groente': 'Groente',
|
||||
'Vlees/Vis': 'Vlees/Vis',
|
||||
'Dranken': 'Dranken',
|
||||
'Tussendoortjes': 'Tussendoor',
|
||||
'Snoep/Koek': 'Snoep/Koek',
|
||||
'Overig': 'Overig',
|
||||
};
|
||||
function displayCategory(cat) {
|
||||
return CATEGORY_DISPLAY[cat] || cat || '';
|
||||
}
|
||||
|
||||
// ===== Prikmomenten voor glucose =====
|
||||
const PRICKMOMENTEN = [
|
||||
{ key: 'nuchter', label: 'Nuchter (voor ontbijt)', targetMin: 0, targetMax: 5.4, momentId: null },
|
||||
|
|
@ -2730,6 +2797,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
|
||||
// ===== State =====
|
||||
let voedingsmiddelen = [];
|
||||
const voedingsmiddelenByN = new Map(); // n → item, for click-handler lookup
|
||||
let dagboek = {}; // { "2026-07-23": { "Ontbijt": [{item, portie}, ...], ... } }
|
||||
let activeDate = formatDate(new Date());
|
||||
let activeTab = 'zoeken';
|
||||
|
|
@ -2740,9 +2808,12 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
let dataLoaded = false;
|
||||
let maaltijden = []; // [{ id, naam, items: [{item, portie}, ...], khTotaal }]
|
||||
let glucose = {}; // { '2026-07-24': { nuchter: 5.1, naOntbijt: 6.2, naLunch: null, naAvondeten: null } }
|
||||
const SEARCH_PAGE_SIZE = 50;
|
||||
const SEARCH_PAGE_SIZE = 200;
|
||||
let searchVisibleCount = SEARCH_PAGE_SIZE;
|
||||
let searchObserver = null;
|
||||
let searchTotal = 0; // total matching items from API
|
||||
let searchPage = 1; // current API page (1-based)
|
||||
let isSearchLoading = false;
|
||||
|
||||
// ===== Utility Functions =====
|
||||
function formatDate(d) {
|
||||
|
|
@ -3110,6 +3181,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
const data = await response.json();
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
voedingsmiddelen = data;
|
||||
// Build lookup map by NEVO number (n)
|
||||
for (const item of data) {
|
||||
if (item.n != null) voedingsmiddelenByN.set(item.n, item);
|
||||
}
|
||||
buildSearchIndex();
|
||||
console.log(`✅ ${data.length} voedingsmiddelen geladen uit data/voedingsmiddelen.json`);
|
||||
} else {
|
||||
|
|
@ -3118,6 +3193,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
} catch (err) {
|
||||
console.warn('⚠️ Externe data niet geladen, gebruik sample data:', err.message);
|
||||
voedingsmiddelen = [...SAMPLE_DATA];
|
||||
for (const item of voedingsmiddelen) {
|
||||
if (item.n != null) voedingsmiddelenByN.set(item.n, item);
|
||||
}
|
||||
buildSearchIndex();
|
||||
// Show error only if we haven't loaded data before
|
||||
if (!dataLoaded) {
|
||||
|
|
@ -3281,11 +3359,30 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
}
|
||||
|
||||
// ===== Rendering =====
|
||||
function renderZoeken() {
|
||||
|
||||
/** Fetch a paginated page of food items from the API */
|
||||
async function fetchFoodPage(q, cat, page, limit) {
|
||||
const params = new URLSearchParams();
|
||||
if (q) params.set('q', q);
|
||||
if (cat) params.set('cat', cat);
|
||||
params.set('page', String(page));
|
||||
params.set('limit', String(limit));
|
||||
const url = `/api/food?${params.toString()}`;
|
||||
try {
|
||||
const resp = await fetch(url);
|
||||
if (!resp.ok) throw new Error('API returned ' + resp.status);
|
||||
const json = await resp.json();
|
||||
return { data: json.data || [], total: json.total || 0 };
|
||||
} catch (e) {
|
||||
console.warn('API fetch mislukt, fallback naar lokale data:', e.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function renderZoeken() {
|
||||
if (!dataLoaded) return;
|
||||
if (activeTab !== 'zoeken') return;
|
||||
|
||||
const items = getFilteredItems();
|
||||
const listEl = document.getElementById('foodList');
|
||||
const countEl = document.getElementById('resultsCount');
|
||||
const emptyEl = document.getElementById('emptyState');
|
||||
|
|
@ -3293,35 +3390,70 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
|
||||
// Reset pagination on new search/filter
|
||||
searchVisibleCount = SEARCH_PAGE_SIZE;
|
||||
searchPage = 1;
|
||||
disconnectSearchObserver();
|
||||
|
||||
// Show results count
|
||||
countEl.textContent = items.length === 0
|
||||
? 'Geen resultaten'
|
||||
: `${items.length} ${items.length === 1 ? 'resultaat' : 'resultaten'}`;
|
||||
// Try API first, fall back to local filtering
|
||||
const apiResult = await fetchFoodPage(searchQuery, selectedCategory, 1, SEARCH_PAGE_SIZE);
|
||||
if (apiResult) {
|
||||
// API mode: use paginated results
|
||||
searchTotal = apiResult.total;
|
||||
const items = apiResult.data;
|
||||
|
||||
// Render list
|
||||
if (items.length === 0) {
|
||||
listEl.innerHTML = '';
|
||||
emptyEl.classList.remove('hidden');
|
||||
sentinelEl.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
// Show results count
|
||||
countEl.textContent = items.length === 0
|
||||
? 'Geen resultaten'
|
||||
: `${searchTotal} ${searchTotal === 1 ? 'resultaat' : 'resultaten'}`;
|
||||
|
||||
emptyEl.classList.add('hidden');
|
||||
if (items.length === 0) {
|
||||
listEl.innerHTML = '';
|
||||
emptyEl.classList.remove('hidden');
|
||||
sentinelEl.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Render first batch
|
||||
const batch = items.slice(0, SEARCH_PAGE_SIZE);
|
||||
listEl.innerHTML = renderFoodItemsHtml(batch, 0);
|
||||
attachFoodItemHandlers(listEl);
|
||||
emptyEl.classList.add('hidden');
|
||||
|
||||
// Setup infinite scroll if more items exist
|
||||
if (items.length > SEARCH_PAGE_SIZE) {
|
||||
sentinelEl.classList.remove('hidden');
|
||||
setupSearchObserver(items);
|
||||
// Render first batch
|
||||
listEl.innerHTML = renderFoodItemsHtml(items, 0);
|
||||
attachFoodItemHandlers(listEl);
|
||||
|
||||
// Setup infinite scroll if more items exist
|
||||
if (searchVisibleCount < searchTotal) {
|
||||
sentinelEl.classList.remove('hidden');
|
||||
setupSearchObserver();
|
||||
} else {
|
||||
sentinelEl.classList.add('hidden');
|
||||
disconnectSearchObserver();
|
||||
}
|
||||
} else {
|
||||
sentinelEl.classList.add('hidden');
|
||||
disconnectSearchObserver();
|
||||
// Fallback mode: use local data (original behaviour)
|
||||
const items = getFilteredItems();
|
||||
|
||||
countEl.textContent = items.length === 0
|
||||
? 'Geen resultaten'
|
||||
: `${items.length} ${items.length === 1 ? 'resultaat' : 'resultaten'}`;
|
||||
|
||||
if (items.length === 0) {
|
||||
listEl.innerHTML = '';
|
||||
emptyEl.classList.remove('hidden');
|
||||
sentinelEl.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
emptyEl.classList.add('hidden');
|
||||
|
||||
const batch = items.slice(0, SEARCH_PAGE_SIZE);
|
||||
listEl.innerHTML = renderFoodItemsHtml(batch, 0);
|
||||
attachFoodItemHandlers(listEl);
|
||||
|
||||
if (items.length > SEARCH_PAGE_SIZE) {
|
||||
sentinelEl.classList.remove('hidden');
|
||||
setupSearchObserver();
|
||||
} else {
|
||||
sentinelEl.classList.add('hidden');
|
||||
disconnectSearchObserver();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3331,10 +3463,10 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
// Cap --i at batch size: max animation delay = 49 × 50ms = 2.45s
|
||||
const animIdx = idx % SEARCH_PAGE_SIZE;
|
||||
return `
|
||||
<li class="food-item" data-index="${voedingsmiddelen.indexOf(item)}" style="--i:${animIdx}" role="button" tabindex="0">
|
||||
<li class="food-item" data-nummer="${item.n != null ? item.n : ''}" style="--i:${animIdx}" role="button" tabindex="0">
|
||||
<div class="food-item-left">
|
||||
<div class="food-item-name">${escapeHtml(displayNaam(item.naam))}</div>
|
||||
<div class="food-item-cat">${escapeHtml(item.cat)}</div>
|
||||
<div class="food-item-cat">${escapeHtml(displayCategory(item.cat))}</div>
|
||||
</div>
|
||||
<div class="food-item-right">
|
||||
<div class="food-item-kh">${formatKh(kh)}</div>
|
||||
|
|
@ -3352,28 +3484,71 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
if (el._clickAttached) return;
|
||||
el._clickAttached = true;
|
||||
el.addEventListener('click', () => {
|
||||
const idx = parseInt(el.dataset.index);
|
||||
if (idx >= 0 && idx < voedingsmiddelen.length) {
|
||||
showDetail(voedingsmiddelen[idx]);
|
||||
const nummer = el.dataset.nummer;
|
||||
if (!nummer || !voedingsmiddelenByN.has(nummer)) {
|
||||
// If the item is not in the local lookup (e.g. pure API item),
|
||||
// try finding by index in the food list as last resort
|
||||
const items = listEl.querySelectorAll('.food-item');
|
||||
const idx = Array.from(items).indexOf(el);
|
||||
if (idx >= 0) {
|
||||
showToast('Item niet gevonden in lokale data');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const item = voedingsmiddelenByN.get(nummer);
|
||||
// If in add-to-moment mode, go directly to add modal with preselected moment
|
||||
if (window._targetMoment) {
|
||||
showAddToMeal(item, window._targetMoment);
|
||||
} else {
|
||||
showDetail(item);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupSearchObserver(allItems) {
|
||||
function setupSearchObserver() {
|
||||
disconnectSearchObserver();
|
||||
const sentinel = document.getElementById('searchSentinel');
|
||||
if (!sentinel) return;
|
||||
|
||||
searchObserver = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
// Load up to 3 batches at once when user scrolls fast
|
||||
// This keeps the sentinel pushed far ahead of the viewport
|
||||
let batchesLoaded = 0;
|
||||
const maxBatches = 3;
|
||||
while (batchesLoaded < maxBatches && searchVisibleCount < allItems.length) {
|
||||
appendSearchBatch(allItems);
|
||||
batchesLoaded++;
|
||||
searchObserver = new IntersectionObserver(async (entries) => {
|
||||
if (entries[0].isIntersecting && !isSearchLoading) {
|
||||
isSearchLoading = true;
|
||||
try {
|
||||
if (searchTotal > 0) {
|
||||
// API mode: fetch next page
|
||||
const nextPage = searchPage + 1;
|
||||
const apiResult = await fetchFoodPage(searchQuery, selectedCategory, nextPage, SEARCH_PAGE_SIZE);
|
||||
if (apiResult && apiResult.data.length > 0) {
|
||||
appendSearchBatchFromApi(apiResult.data);
|
||||
searchPage = nextPage;
|
||||
// All items loaded — hide sentinel and disconnect
|
||||
if (searchVisibleCount >= searchTotal) {
|
||||
disconnectSearchObserver();
|
||||
const sentinelEl = document.getElementById('searchSentinel');
|
||||
if (sentinelEl) sentinelEl.classList.add('hidden');
|
||||
}
|
||||
} else if (!apiResult) {
|
||||
// API failed — try fallback from local data if available
|
||||
const fallbackItems = getFilteredItems();
|
||||
if (searchVisibleCount < fallbackItems.length) {
|
||||
appendSearchBatchLocal(fallbackItems);
|
||||
}
|
||||
} else {
|
||||
// API returned empty — all done
|
||||
disconnectSearchObserver();
|
||||
const sentinelEl = document.getElementById('searchSentinel');
|
||||
if (sentinelEl) sentinelEl.classList.add('hidden');
|
||||
}
|
||||
} else {
|
||||
// Fallback mode: slice from local filtered items
|
||||
const fallbackItems = getFilteredItems();
|
||||
if (searchVisibleCount < fallbackItems.length) {
|
||||
appendSearchBatchLocal(fallbackItems);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isSearchLoading = false;
|
||||
}
|
||||
}
|
||||
}, { rootMargin: '1200px' });
|
||||
|
|
@ -3381,7 +3556,17 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
searchObserver.observe(sentinel);
|
||||
}
|
||||
|
||||
function appendSearchBatch(allItems) {
|
||||
function appendSearchBatchFromApi(items) {
|
||||
const listEl = document.getElementById('foodList');
|
||||
const sentinelEl = document.getElementById('searchSentinel');
|
||||
if (!listEl) return;
|
||||
|
||||
listEl.insertAdjacentHTML('beforeend', renderFoodItemsHtml(items, searchVisibleCount));
|
||||
attachFoodItemHandlers(listEl);
|
||||
searchVisibleCount += items.length;
|
||||
}
|
||||
|
||||
function appendSearchBatchLocal(allItems) {
|
||||
const listEl = document.getElementById('foodList');
|
||||
const sentinelEl = document.getElementById('searchSentinel');
|
||||
if (!listEl || !sentinelEl) return;
|
||||
|
|
@ -3415,6 +3600,8 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
|
||||
function resetSearchPagination() {
|
||||
searchVisibleCount = SEARCH_PAGE_SIZE;
|
||||
searchPage = 1;
|
||||
searchTotal = 0;
|
||||
disconnectSearchObserver();
|
||||
}
|
||||
|
||||
|
|
@ -3751,7 +3938,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
|
||||
body.innerHTML = `
|
||||
<div class="detail-name">${escapeHtml(displayNaam(item.naam))}</div>
|
||||
<div class="detail-category">${escapeHtml(item.cat)}</div>
|
||||
<div class="detail-category">${escapeHtml(displayCategory(item.cat))}</div>
|
||||
<div class="detail-links">
|
||||
<a class="detail-info-link" href="https://www.voedingscentrum.nl/nl/zoek.aspx?query=${encodeURIComponent(displayNaam(item.naam))}" target="_blank" rel="noopener">Voedingscentrum <i class="fas fa-external-link-alt"></i></a>
|
||||
</div>
|
||||
|
|
@ -3924,30 +4111,26 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
});
|
||||
}
|
||||
|
||||
function openAddToMealForMoment(momentId) {
|
||||
// Open a mini search-to-add flow from diary
|
||||
switchTab('zoeken');
|
||||
// Give user a hint
|
||||
const moment = EETMOMENTEN.find(m => m.id === momentId);
|
||||
showToast(`Selecteer een voedingsmiddel voor ${moment.name}`);
|
||||
// Store the target moment so next food tap adds to it
|
||||
window._targetMoment = momentId;
|
||||
function clearAddContext() {
|
||||
window._targetMoment = null;
|
||||
const banner = document.getElementById('addContextBanner');
|
||||
if (banner) banner.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Use event delegation so dynamically loaded items also work
|
||||
const listEl = document.getElementById('foodList');
|
||||
const handler = (e) => {
|
||||
const itemEl = e.target.closest('.food-item');
|
||||
if (!itemEl) return;
|
||||
const idx = parseInt(itemEl.dataset.index);
|
||||
if (idx >= 0 && idx < voedingsmiddelen.length && window._targetMoment) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
showAddToMeal(voedingsmiddelen[idx], window._targetMoment);
|
||||
window._targetMoment = null;
|
||||
listEl.removeEventListener('click', handler);
|
||||
}
|
||||
};
|
||||
listEl.addEventListener('click', handler);
|
||||
function openAddToMealForMoment(momentId) {
|
||||
// Navigate to search page with eetmoment context
|
||||
switchTab('zoeken');
|
||||
const moment = EETMOMENTEN.find(m => m.id === momentId);
|
||||
if (!moment) return;
|
||||
// Store the target moment so clicking a food item adds to it
|
||||
window._targetMoment = momentId;
|
||||
// Show visual context banner instead of toast
|
||||
const banner = document.getElementById('addContextBanner');
|
||||
const textEl = document.getElementById('addContextText');
|
||||
if (banner && textEl) {
|
||||
textEl.innerHTML = `<i class="fas fa-arrow-left-to-bracket"></i> Voeg toe aan: ${moment.icon} ${moment.name}`;
|
||||
banner.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function addToMeal(item, portie, momentId) {
|
||||
|
|
@ -4220,7 +4403,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
<div class="meal-edit-item">
|
||||
<div class="meal-edit-item-info">
|
||||
<div class="meal-edit-item-name">${escapeHtml(displayNaam(mealItem.item.naam))}</div>
|
||||
<div class="meal-edit-item-cat">${escapeHtml(mealItem.item.cat || '')} · ${formatKh(itemKh)} g kh</div>
|
||||
<div class="meal-edit-item-cat">${escapeHtml(displayCategory(mealItem.item.cat || ''))} · ${formatKh(itemKh)} g kh</div>
|
||||
</div>
|
||||
<div class="meal-edit-item-actions">
|
||||
<div class="portion-input-wrapper" style="margin:0;">
|
||||
|
|
@ -4323,6 +4506,8 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
|
||||
// ===== Tab Switching =====
|
||||
function switchTab(tab) {
|
||||
// Clear add-to-moment context when switching tabs
|
||||
clearAddContext();
|
||||
activeTab = tab;
|
||||
|
||||
const panels = ['tabZoeken','tabDagboek','tabMaaltijden'];
|
||||
|
|
@ -4870,7 +5055,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
<div class="meal-edit-item">
|
||||
<div class="meal-edit-item-info">
|
||||
<div class="meal-edit-item-name">${escapeHtml(displayNaam(e.item.naam))}</div>
|
||||
<div class="meal-edit-item-cat">${escapeHtml(e.item.cat)} · ${ikh} g kh</div>
|
||||
<div class="meal-edit-item-cat">${escapeHtml(displayCategory(e.item.cat))} · ${ikh} g kh</div>
|
||||
</div>
|
||||
<div class="meal-edit-item-actions">
|
||||
<div class="portion-input-wrapper" style="margin:0;">
|
||||
|
|
@ -5200,7 +5385,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
<div class="meal-edit-item">
|
||||
<div class="meal-edit-item-info">
|
||||
<div class="meal-edit-item-name">${escapeHtml(displayNaam(entry.item.naam))}</div>
|
||||
<div class="meal-edit-item-cat">${escapeHtml(entry.item.cat)} · ${formatKh(itemKh)} g kh</div>
|
||||
<div class="meal-edit-item-cat">${escapeHtml(displayCategory(entry.item.cat))} · ${formatKh(itemKh)} g kh</div>
|
||||
</div>
|
||||
<div class="meal-edit-item-actions">
|
||||
<div class="portion-input-wrapper" style="margin:0;">
|
||||
|
|
@ -5343,7 +5528,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
resultsEl.innerHTML = matches.map((item, i) => `
|
||||
<div class="meal-search-item" data-item-idx="${i}">
|
||||
<div style="font-weight:500;">${escapeHtml(displayNaam(item.naam))}</div>
|
||||
<div style="font-size:var(--font-xs);color:var(--text-muted);">${escapeHtml(item.cat)} · ${escapeHtml(item.kh)} g kh/100g</div>
|
||||
<div style="font-size:var(--font-xs);color:var(--text-muted);">${escapeHtml(displayCategory(item.cat))} · ${escapeHtml(item.kh)} g kh/100g</div>
|
||||
</div>
|
||||
`).join('');
|
||||
resultsEl.querySelectorAll('.meal-search-item').forEach(el => {
|
||||
|
|
@ -5601,6 +5786,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
|
|||
renderZoeken();
|
||||
});
|
||||
|
||||
// Cancel add-to-moment context banner
|
||||
document.getElementById('addContextCancel').addEventListener('click', clearAddContext);
|
||||
|
||||
// Tab navigation
|
||||
document.getElementById('navZoeken').addEventListener('click', () => switchTab('zoeken'));
|
||||
document.getElementById('navDagboek').addEventListener('click', () => switchTab('dagboek'));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue