const calendarId = new URLSearchParams(window.location.search).get('calendarId'); let calendar = null; const weekdayBoard = document.getElementById('weekdayBoard'); const weekendWrap = document.getElementById('weekendWrap'); const legend = document.getElementById('legend'); const toast = document.getElementById('toast'); function showToast(msg) { toast.textContent = msg; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 1800); } function debounce(fn, ms) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; } const handlers = { onToggleDone: (taskId, done) => api.patch(`/api/calendars/${calendarId}/tasks/${taskId}`, { done }).catch(() => showToast('Could not save')), onTextEdit: (taskId, text) => api.patch(`/api/calendars/${calendarId}/tasks/${taskId}`, { text }).catch(() => showToast('Could not save')), onLabelEdit: (blockKey, label) => api.patch(`/api/calendars/${calendarId}/blocks/${blockKey}`, { label }).catch(() => showToast('Could not save')), onAddTask: async (dayOfWeek, blockKey, text) => { const { task } = await api.post(`/api/calendars/${calendarId}/tasks`, { dayOfWeek, blockKey, text }); return task; }, onDeleteTask: (taskId) => api.del(`/api/calendars/${calendarId}/tasks/${taskId}`).catch(() => showToast('Could not delete')), onDurationEdit: (taskId, durationMinutes) => api.patch(`/api/calendars/${calendarId}/tasks/${taskId}`, { durationMinutes }).catch(() => showToast('Could not save')), }; function todayName() { return new Date().toLocaleDateString('en-US', { weekday: 'long' }); } function draw() { const childNameEl = document.getElementById('childName'); if (document.activeElement !== childNameEl) childNameEl.textContent = calendar.childName || ''; const titleEl = document.getElementById('calTitle'); if (document.activeElement !== titleEl) titleEl.textContent = calendar.title; renderCalendar({ calendar, editable: true, weekdayBoard, weekendWrap, handlers, todayName: todayName(), }); renderLegend(legend, calendar); weekendWrap.classList.toggle('hidden', !calendar.showWeekend); document.getElementById('toggleWeekend').textContent = calendar.showWeekend ? 'Hide weekend' : 'Show weekend'; buildPeriodToggles(); } function buildPeriodToggles() { const el = document.getElementById('periodToggles'); el.querySelectorAll('label.opt').forEach((n) => n.remove()); calendar.blocks.forEach((block) => { const label = document.createElement('label'); label.className = 'opt'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = !document.body.classList.contains('hide-' + block.key); cb.onchange = () => document.body.classList.toggle('hide-' + block.key, !cb.checked); const span = document.createElement('span'); span.textContent = block.label; label.appendChild(cb); label.appendChild(span); el.appendChild(label); }); } async function load() { if (!calendarId) { document.body.innerHTML = '
No calendar selected. Back to dashboard
'; return; } try { const data = await api.get(`/api/calendars/${calendarId}`); calendar = data.calendar; } catch (err) { if (err.status === 401) { window.location.href = '/login.html'; return; } document.body.innerHTML = `${err.message}. Back to dashboard
`; return; } draw(); } document.getElementById('toggleWeekend').addEventListener('click', async () => { calendar.showWeekend = !calendar.showWeekend; weekendWrap.classList.toggle('hidden', !calendar.showWeekend); document.getElementById('toggleWeekend').textContent = calendar.showWeekend ? 'Hide weekend' : 'Show weekend'; await api.patch(`/api/calendars/${calendarId}`, { showWeekend: calendar.showWeekend }); }); document.getElementById('resetChecks').addEventListener('click', async () => { const data = await api.post(`/api/calendars/${calendarId}/reset-checks`); calendar = data.calendar; draw(); showToast('Checkboxes reset'); }); document.getElementById('printBtn').addEventListener('click', () => window.print()); const titleEl = document.getElementById('calTitle'); titleEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); titleEl.blur(); } }); const debouncedTitleSave = debounce(() => { if (titleEl.textContent.trim()) api.patch(`/api/calendars/${calendarId}`, { title: titleEl.textContent.trim() }); }, 500); titleEl.addEventListener('input', debouncedTitleSave); const childNameEl = document.getElementById('childName'); childNameEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); childNameEl.blur(); } }); const debouncedChildNameSave = debounce(() => { if (childNameEl.textContent.trim()) api.patch(`/api/children/${calendar.childId}`, { name: childNameEl.textContent.trim() }); }, 500); childNameEl.addEventListener('input', debouncedChildNameSave); // Polling keeps this view in sync with kiosk checkbox taps (and the other // parent's edits) without a manual reload. Paused while the parent is mid-edit // in any contenteditable field, so an incoming poll can't yank their cursor. function isEditableTarget(el) { // isContentEditable covers task/label/title/child-name text; duration is a // plain instead (isContentEditable is always false for // form fields), so it needs its own explicit check — a narrow class match // rather than a blanket tag check, so unrelated inputs elsewhere (e.g. the // period-toggle checkboxes, which commit instantly and don't need this) // aren't also paused. return !!el && (el.isContentEditable || el.classList.contains('duration-input')); } let isEditingFocused = false; document.addEventListener('focusin', (e) => { if (isEditableTarget(e.target)) isEditingFocused = true; }); document.addEventListener('focusout', (e) => { if (isEditableTarget(e.target)) { setTimeout(() => { isEditingFocused = isEditableTarget(document.activeElement); }, 0); } }); async function poll() { if (!calendar || isEditingFocused || document.hidden) return; try { const data = await api.get(`/api/calendars/${calendarId}/poll?since=${encodeURIComponent(calendar.updatedAt)}`); if (data.changed) { calendar = data.calendar; draw(); } } catch (err) { if (err.status === 401) window.location.href = '/login.html'; } } setInterval(poll, 4000); document.addEventListener('visibilitychange', () => { if (!document.hidden) poll(); }); load();