Files
KCal/public/js/calendarRender.js
T
ort 2826a3819c Add Web Push notifications, PWA support, and full-screen tablet kiosk
Parents get a real push notification when a kid checks off a task
(false->true transitions only, fire-and-forget, degrades gracefully with
no VAPID keys configured). Dashboard is a fully installable iOS/Android
PWA; each child's kiosk link gets its own dynamic per-token manifest so
"Add to Home Screen" opens straight into their board in standalone mode.

Kiosk view is reworked for tablets: safe-area-aware full-bleed layout,
the whole task row is now tappable (previously only the 24px checkbox
was, well under Apple's touch-target minimum), and app icons are
generated by a small dependency-free PNG encoder (no image tooling
available in this environment).

Push requires real HTTPS (iOS Safari won't allow it otherwise) - README
and UNRAID.md cover VAPID setup and the HTTPS prerequisite.
2026-08-15 16:14:56 -04:00

177 lines
6.0 KiB
JavaScript

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; });
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);
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
// itself — it already handles its own toggle+change natively.
row.addEventListener('click', (e) => {
if (e.target === cb) return;
cb.checked = !cb.checked;
cb.dispatchEvent(new Event('change'));
});
}
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 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 = `<div class="day-name">${dayName}</div>` +
(dayName === todayName ? `<div class="today-badge">TODAY</div>` : '');
card.appendChild(head);
calendar.blocks.forEach((block) => {
const blockEl = document.createElement('div');
blockEl.className = 'block ' + block.key;
const title = document.createElement('div');
title.className = 'block-title';
const iconSpan = document.createElement('span');
iconSpan.className = 'icon';
iconSpan.textContent = BLOCK_ICONS[block.key] || '';
const labelSpan = document.createElement('span');
labelSpan.className = 'label';
labelSpan.dataset.block = block.key;
labelSpan.textContent = currentLabels[block.key];
if (editable) {
labelSpan.contentEditable = 'true';
labelSpan.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); labelSpan.blur(); }
});
const debouncedLabel = debounce(() => {
if (labelSpan.textContent.trim()) {
currentLabels[block.key] = labelSpan.textContent.trim();
handlers.onLabelEdit(block.key, labelSpan.textContent.trim());
}
}, 500);
labelSpan.addEventListener('input', () => { syncLabel(block.key, labelSpan); debouncedLabel(); });
}
title.appendChild(iconSpan);
title.appendChild(labelSpan);
blockEl.appendChild(title);
const list = document.createElement('div');
list.className = 'task-list';
(calendar.days[dayName][block.key] || []).forEach((task) => {
list.appendChild(makeTask(dayName, block.key, task));
});
blockEl.appendChild(list);
if (editable) {
const addBtn = document.createElement('button');
addBtn.className = 'add-task';
addBtn.textContent = '+ add task';
addBtn.onclick = async () => {
const newTask = await handlers.onAddTask(dayName, block.key, 'New task');
const row = makeTask(dayName, block.key, newTask);
list.appendChild(row);
const span = row.querySelector('.task-text');
span.focus();
document.execCommand('selectAll', false, null);
};
blockEl.appendChild(addBtn);
}
card.appendChild(blockEl);
});
return card;
}
WEEKDAYS.forEach((d) => weekdayBoard.appendChild(makeDayCard(d)));
WEEKEND.forEach((d) => weekendWrap.appendChild(makeDayCard(d)));
return { currentLabels };
}
function renderLegend(legendEl, calendar) {
legendEl.innerHTML = '';
calendar.blocks.forEach((block) => {
const item = document.createElement('span');
item.className = 'item';
item.innerHTML = `<span class="dot" style="background:var(--${block.key})"></span>` +
`<span class="legend-label" data-block="${block.key}">${block.label}</span>`;
legendEl.appendChild(item);
});
}