chore: sync docker/index.html met glucose feature

This commit is contained in:
cas 2026-07-24 19:36:48 +02:00
parent dbe39fded7
commit cfcb77407f

View file

@ -465,6 +465,161 @@ body {
background: var(--progress-red); background: var(--progress-red);
} }
/* ===== Glucose Section ===== */
.glucose-section {
background: var(--white);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
padding: 14px 16px;
margin-bottom: 10px;
}
.glucose-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.glucose-icon {
font-size: 1.1rem;
color: var(--progress-red);
}
.glucose-title {
font-weight: 700;
font-size: var(--font-md);
color: var(--text);
}
.glucose-unit-label {
font-size: var(--font-xs);
color: var(--text-muted);
margin-left: 2px;
}
/* Grid: 4 columns on wide, 2×2 on narrow */
.glucose-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
@media (max-width: 480px) {
.glucose-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.glucose-item {
background: var(--bg);
border-radius: var(--radius-sm);
padding: 10px 8px;
text-align: center;
border: 1px solid var(--border);
transition: border-color 0.2s, background 0.2s;
}
.glucose-item.glucose-good {
border-color: #28a745;
background: #f0fdf4;
}
.glucose-item.glucose-bad {
border-color: var(--progress-red);
background: #fef2f2;
}
.glucose-item.glucose-empty {
border-color: var(--border);
background: var(--bg);
}
.glucose-item-label {
font-size: var(--font-xs);
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 2px;
line-height: 1.3;
}
.glucose-item-range {
font-size: 0.65rem;
color: var(--text-muted);
margin-bottom: 8px;
}
.glucose-input-wrapper {
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
margin-bottom: 6px;
}
.glucose-input {
width: 58px;
padding: 4px 4px;
border: 2px solid var(--border);
border-radius: var(--radius-xs);
font-size: var(--font-md);
font-weight: 600;
text-align: center;
background: var(--white);
color: var(--text);
transition: border-color 0.2s, box-shadow 0.2s;
outline: none;
/* Hide spinners */
-moz-appearance: textfield;
}
.glucose-input::-webkit-outer-spin-button,
.glucose-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.glucose-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(250,232,146,0.25);
}
.glucose-unit {
font-size: var(--font-xs);
color: var(--text-muted);
}
.glucose-status {
display: flex;
align-items: center;
justify-content: center;
gap: 3px;
font-size: var(--font-xs);
line-height: 1.2;
min-height: 20px;
}
.glucose-status-icon {
font-size: 0.85rem;
}
.glucose-status-text {
font-size: 0.65rem;
font-weight: 500;
}
.glucose-good .glucose-status-text {
color: #15803d;
}
.glucose-bad .glucose-status-text {
color: var(--progress-red);
}
.glucose-empty .glucose-status-text {
color: var(--text-muted);
}
/* Warning text */ /* Warning text */
.carb-warning { .carb-warning {
display: none; display: none;
@ -2103,6 +2258,16 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
<div class="carb-warning" id="carbWarning"></div> <div class="carb-warning" id="carbWarning"></div>
</div> </div>
<!-- Glucose Section -->
<div class="glucose-section" id="glucoseSection">
<div class="glucose-header">
<span class="glucose-icon"><i class="fa-solid fa-droplet"></i></span>
<span class="glucose-title">Glucose</span>
<span class="glucose-unit-label">mmol/L</span>
</div>
<div class="glucose-grid" id="glucoseGrid"></div>
</div>
<ul class="eetmoment-list" id="eetmomentList"></ul> <ul class="eetmoment-list" id="eetmomentList"></ul>
<div class="empty-state hidden" id="diaryEmpty"> <div class="empty-state hidden" id="diaryEmpty">
@ -2288,6 +2453,14 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
return CATEGORY_MAP[itemCat] || 'Overig'; return CATEGORY_MAP[itemCat] || 'Overig';
} }
// ===== Prikmomenten voor glucose =====
const PRICKMOMENTEN = [
{ key: 'nuchter', label: 'Nuchter (voor ontbijt)', targetMin: 0, targetMax: 5.4 },
{ key: 'naOntbijt', label: '2 uur na ontbijt', targetMin: 5.4, targetMax: 6.7 },
{ key: 'naLunch', label: '2 uur na lunch', targetMin: 5.4, targetMax: 6.7 },
{ key: 'naAvondeten', label: '2 uur na avondeten', targetMin: 5.4, targetMax: 6.7 }
];
// ===== Standaard portiegroottes per categorie ===== // ===== Standaard portiegroottes per categorie =====
// Gebaseerd op Voedingscentrum / NEVO richtlijnen // Gebaseerd op Voedingscentrum / NEVO richtlijnen
/** Drie praktische porties per categorie: een intuïtieve eenheid + 100g + 50g */ /** Drie praktische porties per categorie: een intuïtieve eenheid + 100g + 50g */
@ -2369,6 +2542,7 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
let searchIndex = null; // Map<item, expanded search tokens string> let searchIndex = null; // Map<item, expanded search tokens string>
let dataLoaded = false; let dataLoaded = false;
let maaltijden = []; // [{ id, naam, items: [{item, portie}, ...], khTotaal }] 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 = 50;
let searchVisibleCount = SEARCH_PAGE_SIZE; let searchVisibleCount = SEARCH_PAGE_SIZE;
let searchObserver = null; let searchObserver = null;
@ -2461,6 +2635,8 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
getOrCreateDay(activeDate); getOrCreateDay(activeDate);
// Load maaltijden // Load maaltijden
loadMaaltijdenFromStorage(); loadMaaltijdenFromStorage();
// Load glucose data
loadGlucoseFromStorage();
} }
function saveMaaltijden() { function saveMaaltijden() {
@ -2482,6 +2658,139 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
} }
} }
// ===== Glucose =====
function isGlucoseInRange(value, moment) {
if (value === null || value === undefined || value === '') return null;
const m = PRICKMOMENTEN.find(p => p.key === moment);
if (!m) return null;
// Nuchter (fasting): only upper bound — < targetMax is good
if (moment === 'nuchter') {
return value < m.targetMax ? 'good' : 'high';
}
// Post-meal: range 5.4 6.7 (inclusive)
if (value < m.targetMin) return 'low';
if (value > m.targetMax) return 'high';
return 'good';
}
function saveGlucoseToStorage() {
try {
localStorage.setItem('koolhydraat-glucose', JSON.stringify(glucose));
} catch (e) {
// Storage full — silently ignore
}
}
function loadGlucoseFromStorage() {
try {
const raw = localStorage.getItem('koolhydraat-glucose');
if (raw) {
glucose = JSON.parse(raw);
}
} catch (e) {
glucose = {};
}
// Ensure today's entry exists
if (!glucose[activeDate]) {
glucose[activeDate] = {};
PRICKMOMENTEN.forEach(m => { glucose[activeDate][m.key] = null; });
}
}
function ensureGlucoseDay(dateStr) {
if (!glucose[dateStr]) {
glucose[dateStr] = {};
PRICKMOMENTEN.forEach(m => { glucose[dateStr][m.key] = null; });
}
}
function renderGlucoseSection() {
const grid = document.getElementById('glucoseGrid');
if (!grid) return;
ensureGlucoseDay(activeDate);
grid.innerHTML = PRICKMOMENTEN.map(m => {
const value = glucose[activeDate][m.key];
const status = isGlucoseInRange(value, m.key);
let statusIcon, statusClass, statusText;
if (value === null || value === undefined || value === '') {
statusIcon = '⏹️';
statusClass = 'glucose-empty';
statusText = 'Nog meten';
} else if (status === 'good') {
statusIcon = '✅';
statusClass = 'glucose-good';
statusText = 'Binnen range';
} else if (status === 'high') {
statusIcon = '🔴';
statusClass = 'glucose-bad';
statusText = 'Te hoog';
} else {
statusIcon = '🔴';
statusClass = 'glucose-bad';
statusText = 'Te laag';
}
const displayValue = (value !== null && value !== undefined) ? value : '';
// Short label: remove parenthetical and "2 uur na" prefix
let shortLabel = m.label;
shortLabel = shortLabel.replace(/\([^)]*\)/g, '').trim();
shortLabel = shortLabel.replace(/^2 uur na /i, '').trim();
// Capitalize first letter
shortLabel = shortLabel.charAt(0).toUpperCase() + shortLabel.slice(1);
return `
<div class="glucose-item ${statusClass}">
<div class="glucose-item-label">${shortLabel}</div>
<div class="glucose-item-range">${m.key === 'nuchter' ? '< ' + m.targetMax : m.targetMin + '' + m.targetMax}</div>
<div class="glucose-input-wrapper">
<input type="number" class="glucose-input"
value="${displayValue}"
data-moment="${m.key}"
step="0.1" min="0" max="30"
placeholder="—"
inputmode="decimal">
<span class="glucose-unit">mmol/L</span>
</div>
<div class="glucose-status ${statusClass}">
<span class="glucose-status-icon">${statusIcon}</span>
<span class="glucose-status-text">${statusText}</span>
</div>
</div>
`;
}).join('');
// Attach save-on-blur handlers
grid.querySelectorAll('.glucose-input').forEach(input => {
input.addEventListener('blur', () => {
const moment = input.dataset.moment;
const val = input.value.trim() === '' ? null : parseFloat(input.value);
// Validate
if (val !== null && (isNaN(val) || val < 0 || val > 30)) {
input.value = glucose[activeDate][moment] !== null && glucose[activeDate][moment] !== undefined ? glucose[activeDate][moment] : '';
return;
}
if (glucose[activeDate][moment] !== val) {
glucose[activeDate][moment] = val;
saveGlucoseToStorage();
renderGlucoseSection();
}
});
// Enter key also saves
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
input.blur();
}
});
});
}
// ===== Daily Carb Limit ===== // ===== Daily Carb Limit =====
const DEFAULT_CARB_LIMIT = 175; // grams per day const DEFAULT_CARB_LIMIT = 175; // grams per day
@ -2875,6 +3184,9 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
// Progress bar // Progress bar
renderProgressBar(totaal); renderProgressBar(totaal);
// Glucose section
renderGlucoseSection();
// Eating moments // Eating moments
const listEl = document.getElementById('eetmomentList'); const listEl = document.getElementById('eetmomentList');
listEl.innerHTML = EETMOMENTEN.map((moment, mi) => { listEl.innerHTML = EETMOMENTEN.map((moment, mi) => {
@ -3664,12 +3976,14 @@ button:active, .btn:active, .meal-card-main:active, .dagboek-item:active, .food-
current.setDate(current.getDate() + direction); current.setDate(current.getDate() + direction);
activeDate = formatDate(current); activeDate = formatDate(current);
getOrCreateDay(activeDate); getOrCreateDay(activeDate);
ensureGlucoseDay(activeDate);
renderDagboek(); renderDagboek();
} }
function goToToday() { function goToToday() {
activeDate = formatDate(new Date()); activeDate = formatDate(new Date());
getOrCreateDay(activeDate); getOrCreateDay(activeDate);
ensureGlucoseDay(activeDate);
renderDagboek(); renderDagboek();
} }