app.js (3862B)
1 'use strict'; 2 3 let daysOffset = 0; 4 5 // Month (calendar) view 6 let monthOffset = 0; // 0 = current month, -1 = previous month, 1 = next month, ... 7 8 // Labels 9 10 function updateDaysLabel() { 11 const el = document.getElementById('days-offset-label'); 12 const fwd = document.getElementById('days-forward-btn'); 13 el.textContent = daysOffset === 0 ? 'Today ±2' : daysOffset + 'd back'; 14 fwd.disabled = daysOffset <= 0; 15 } 16 17 // Tiles 18 19 async function fetchHTML(url, containerId) { 20 const res = await fetch(url); 21 const html = await res.text(); 22 document.getElementById(containerId).innerHTML = html; 23 } 24 25 async function refreshAllTiles() { 26 await Promise.all([ 27 fetchHTML('/tiles/days?offset=' + daysOffset, 'days-row'), 28 loadMonth(), 29 fetchHTML('/summary', 'summary'), 30 ]); 31 } 32 33 // Navigation 34 35 async function shiftDays(dir) { 36 const next = daysOffset + dir; 37 if (next < 0) return; 38 daysOffset = next; 39 updateDaysLabel(); 40 await fetchHTML('/tiles/days?offset=' + daysOffset, 'days-row'); 41 } 42 43 // Month (calendar) 44 45 // Fetches the full calendar grid + label for the currently selected 46 // monthOffset and swaps it in, updating the Prev/Next button state. 47 async function loadMonth() { 48 const res = await fetch('/tiles/month?offset=' + monthOffset); 49 const data = await res.json(); 50 document.getElementById('month-label').textContent = data.label; 51 document.getElementById('month-grid').innerHTML = data.grid; 52 document.getElementById('month-next-btn').disabled = data.next_disabled; 53 } 54 55 async function shiftMonth(dir) { 56 monthOffset += dir; 57 await loadMonth(); 58 } 59 60 // Days 61 62 function openDay(date) { 63 fetch('/modal?date=' + date) 64 .then(r => r.text()) 65 .then(html => { 66 document.body.insertAdjacentHTML('beforeend', html); 67 updatePreview(); 68 document.getElementById('drink-volume').addEventListener('change', updatePreview); 69 document.getElementById('drink-abv').addEventListener('input', updatePreview); 70 }); 71 } 72 73 function closeModal(e) { 74 if (e.target.id === 'day-modal') { 75 document.getElementById('day-modal').remove(); 76 } 77 } 78 79 function refreshModal(date) { 80 const modal = document.getElementById('day-modal'); 81 if (!modal) return; 82 fetch('/modal?date=' + date) 83 .then(r => r.text()) 84 .then(html => { 85 modal.remove(); 86 document.body.insertAdjacentHTML('beforeend', html); 87 updatePreview(); 88 document.getElementById('drink-volume').addEventListener('change', updatePreview); 89 document.getElementById('drink-abv').addEventListener('input', updatePreview); 90 }); 91 } 92 93 function updatePreview() { 94 const volEl = document.getElementById('drink-volume'); 95 const abvEl = document.getElementById('drink-abv'); 96 const pre = document.getElementById('abv-preview'); 97 if (!volEl || !abvEl || !pre) return; 98 const ml = parseInt(volEl.value, 10); 99 const abv = parseFloat(abvEl.value); 100 if (ml > 0 && abv > 0) { 101 const u = (ml * abv / 1000).toFixed(2); 102 pre.textContent = `= ${u} unit${u === '1.00' ? '' : 's'}`; 103 } else { 104 pre.textContent = ''; 105 } 106 } 107 108 // Drinks 109 110 function post(url, params) { 111 return fetch(url, { 112 method: 'POST', 113 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 114 body: new URLSearchParams(params).toString(), 115 }); 116 } 117 118 async function addDrink(date) { 119 const ml = document.getElementById('drink-volume').value; 120 const abv = document.getElementById('drink-abv').value; 121 if (!ml || !abv || parseFloat(abv) <= 0) return; 122 await post('/drink/add', { date, volume_ml: ml, abv }); 123 refreshModal(date); 124 refreshAllTiles(); 125 } 126 127 async function removeDrink(date, key) { 128 await post('/drink/remove', { date, key }); 129 refreshModal(date); 130 refreshAllTiles(); 131 } 132 133 async function adjustDrink(date, key, delta) { 134 await post('/drink/adjust', { date, key, delta }); 135 refreshModal(date); 136 refreshAllTiles(); 137 } 138 139 // Init 140 updateDaysLabel();