const BLOCK_ICONS = { morning: '🌅', school: '🎒', after: '⚽', evening: '🌙' }; const WEEKDAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday']; const WEEKEND = ['Saturday', 'Sunday']; function debounce(fn, ms) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; } // Renders the weekly board. `editable` gates every mutation affordance // (contenteditable, delete buttons, add-task buttons) — the kiosk view passes // editable:false and gets a board with nothing but working checkboxes. function renderCalendar({ calendar, editable, weekdayBoard, weekendWrap, handlers, todayName }) { weekdayBoard.innerHTML = ''; weekendWrap.innerHTML = ''; const currentLabels = {}; calendar.blocks.forEach((b) => { currentLabels[b.key] = b.label; }); // Kiosk-only: tasks with a running timer, for kiosk.js's tick loop to // update in place every 250ms without a full re-render. const activeTimers = []; function syncLabel(key, sourceEl) { document.querySelectorAll(`.label[data-block="${key}"]`).forEach((el) => { if (el !== sourceEl) el.textContent = sourceEl.textContent; }); document.querySelectorAll(`.legend-label[data-block="${key}"]`).forEach((el) => { el.textContent = sourceEl.textContent; }); } function makeTask(dayName, blockKey, task) { const row = document.createElement('div'); row.className = 'task' + (task.done ? ' done' : '') + (editable ? '' : ' kiosk'); row.dataset.taskId = task.id; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = task.done; cb.onchange = () => { row.classList.toggle('done', cb.checked); handlers.onToggleDone(task.id, cb.checked); }; const span = document.createElement('div'); span.className = 'task-text'; span.textContent = task.text; row.appendChild(cb); row.appendChild(span); let startBtn = null; if (!editable) { // Kiosk mode: the 24px checkbox alone is a poor touch target, so the // whole row toggles it. Skip when the tap landed on the checkbox or // the start-timer button — those handle themselves. row.addEventListener('click', (e) => { if (e.target === cb || e.target === startBtn) return; cb.checked = !cb.checked; cb.dispatchEvent(new Event('change')); }); if (!task.done) { startBtn = document.createElement('button'); startBtn.className = 'timer-start'; startBtn.textContent = '▶'; startBtn.setAttribute('aria-label', 'Start timer'); startBtn.onclick = () => handlers.onStartTimer(task.id); row.appendChild(startBtn); } if (task.startedAt && !task.done) { const timer = document.createElement('div'); timer.className = 'task-timer'; const track = document.createElement('div'); track.className = 'timer-track'; const fill = document.createElement('div'); fill.className = 'timer-fill'; track.appendChild(fill); const label = document.createElement('span'); label.className = 'timer-label'; timer.appendChild(track); timer.appendChild(label); row.appendChild(timer); activeTimers.push({ taskId: task.id, startedAt: task.startedAt, durationMinutes: task.durationMinutes, fillEl: fill, labelEl: label, }); } } if (editable) { span.contentEditable = 'true'; span.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); span.blur(); } }); const debouncedEdit = debounce(() => { if (span.textContent.trim()) handlers.onTextEdit(task.id, span.textContent.trim()); }, 500); span.addEventListener('input', debouncedEdit); span.addEventListener('blur', () => { if (!span.textContent.trim()) span.textContent = task.text; }); const durationInput = document.createElement('input'); durationInput.type = 'number'; durationInput.className = 'duration-input'; durationInput.min = '1'; durationInput.max = '180'; durationInput.value = task.durationMinutes; durationInput.title = 'Minutes'; const debouncedDuration = debounce(() => { const value = parseInt(durationInput.value, 10); if (Number.isInteger(value) && value >= 1 && value <= 180) { handlers.onDurationEdit(task.id, value); } }, 500); durationInput.addEventListener('input', debouncedDuration); row.appendChild(durationInput); const del = document.createElement('button'); del.className = 'del'; del.textContent = '✕'; del.onclick = () => { row.remove(); handlers.onDeleteTask(task.id); }; row.appendChild(del); } return row; } function makeDayCard(dayName) { const card = document.createElement('div'); card.className = 'day-card' + (dayName === todayName ? ' today' : ''); const head = document.createElement('div'); head.className = 'day-head'; head.innerHTML = `