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