diff --git a/public/board.html b/public/board.html
index b2f50a4..7e2f95a 100644
--- a/public/board.html
+++ b/public/board.html
@@ -48,6 +48,7 @@
.bd { max-width: 1280px; margin: 0 auto; }
.bdtop { display: flex; justify-content: space-between; align-items: flex-end; gap: 14px; flex-wrap: wrap; }
.bdtop h1 { margin: 0; font-size: 34px; } .bdclock { font-size: 40px; font-weight: 700; font-variant-numeric: tabular-nums; }
+ .pickrow { margin: 14px 0 4px; } .pickrow h2 { margin: 0 0 8px; font-size: 18px; } .pickrow .pk { display: flex; align-items: center; gap: 12px; min-width: 190px; padding: 12px 16px; border-radius: 18px; background: var(--panel); border: 2px solid var(--accent); color: var(--text); font: inherit; text-align: left; cursor: pointer; } .pickrow .pk:active { transform: scale(.97); } .pickrow .pk .av { width: 52px; height: 52px; border-radius: 50%; color: #fff; display: grid; place-items: center; font-size: 24px; font-weight: 700; flex: none; } .pickrow .pk b { display: block; font-size: 19px; } .pickrow .pk small { color: var(--muted); font-size: 14px; } .chrow { display: flex; align-items: center; gap: 8px; padding: 9px 0; border-bottom: 1px solid var(--line); text-align: left; } .chrow .g { flex: 1; min-width: 0; font-size: 18px; } .chrow.dn .g { text-decoration: line-through; opacity: .6; } .chrow .btn { min-height: 44px; padding: 0 14px; }
.who { margin: 16px 0; } .who h2 { margin: 0 0 8px; font-size: 18px; }
.tiles { display: flex; gap: 12px; flex-wrap: wrap; }
.tile { display: flex; flex-direction: column; align-items: center; gap: 6px; width: 104px; padding: 12px 6px; border-radius: 18px; background: var(--panel); border: 1px solid var(--line); color: var(--text); cursor: pointer; font: inherit; }
@@ -94,6 +95,7 @@
+
Who's using this? Tap your name
Sign in with a passkey instead
diff --git a/public/board.js b/public/board.js
index c50bcfd..f43fd0f 100644
--- a/public/board.js
+++ b/public/board.js
@@ -32,6 +32,8 @@ function draw() {
$('#tiles').innerHTML = d.people.map(p => `
`).join('');
$('#tiles').querySelectorAll('.tile').forEach(b => b.onclick = () => pick(d.people.find(p => p.id === b.dataset.id)));
const day = (label, ds) => { const l = d.events.filter(e => e.allDay ? e.start <= ds && e.end > ds : ymd(new Date(e.start)) === ds).sort((a, b) => (b.allDay - a.allDay) || a.start - b.start); return `
${label}` + (l.map(e => `
${esc(e.title)}${e.by ? ` ${esc(e.by)}` : ''}${e.allDay ? 'All day' : new Date(e.start).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
`).join('') || none('Nothing')); };
+ const left = p => d.chores.filter(c => c.user === p.id && !c.done).length, grabs = d.chores.filter(c => c.claimable && !c.done).length, pk = d.people.filter(p => p.kid || d.chores.some(c => c.user === p.id));
+ $('#pickrow').innerHTML = pk.length ? `
๐งน Pick your chores. Tap your name
${pk.map(p => ``).join('')}
` : '';
const pill = (c, cls) => `
`;
const pool = d.chores.filter(c => c.claimable && !c.done), per = {}; d.chores.filter(c => !(c.claimable && !c.done)).forEach(c => { (per[c.user || c.userName] = per[c.user || c.userName] || []).push(c); });
const SHOW = 6, chores = (pool.length ? `
` : '') + '
' + Object.keys(per).map(k => { const l = per[k], dn = l.filter(c => c.done).length, left = l.filter(c => !c.done); return `
${esc(l[0].userName)}${dn} of ${l.length}
${left.map(c => pill(c)).join('')}${left.length ? '' : 'All done ๐'}
`; }).join('') + '
';
@@ -46,7 +48,7 @@ function poolAll() {
const list = data.chores.filter(c => c.claimable && !c.done), { box, close } = sheet(`
Up for grabs
${list.length} chores. Tap one to pick it
${list.map(c => ``).join('')}
`);
box.querySelector('#no').onclick = close; box.querySelectorAll('.pill[data-c]').forEach(b => b.onclick = () => choreTap(data.chores.find(c => c.chore === b.dataset.c && (c.user || '') === (b.dataset.u || ''))));
}
-function wireChores() { document.querySelectorAll('[data-add]').forEach(b => b.onclick = () => addFlow(b.dataset.add)); document.querySelectorAll('[data-goalfor]').forEach(b => b.onclick = () => goalFlow(b.dataset.goalfor)); const all = document.getElementById('poolall'); if (all) all.onclick = poolAll; document.querySelectorAll(".pill[data-c]").forEach(b => b.onclick = () => choreTap(data.chores.find(c => c.chore === b.dataset.c && (c.user || '') === (b.dataset.u || '')))); }
+function wireChores() { document.querySelectorAll('[data-hub]').forEach(b => b.onclick = () => choreHub(data.people.find(p => p.id === b.dataset.hub))); document.querySelectorAll('[data-add]').forEach(b => b.onclick = () => addFlow(b.dataset.add)); document.querySelectorAll('[data-goalfor]').forEach(b => b.onclick = () => goalFlow(b.dataset.goalfor)); const all = document.getElementById('poolall'); if (all) all.onclick = poolAll; document.querySelectorAll(".pill[data-c]").forEach(b => b.onclick = () => choreTap(data.chores.find(c => c.chore === b.dataset.c && (c.user || '') === (b.dataset.u || '')))); }
async function load() { try { const r = await fetch('/api/board?day=' + ymd(new Date())); if (r.status === 403) { location.replace('/login'); return; } data = await r.json(); draw(); } catch {} }
function pick(p) {
@@ -113,3 +115,27 @@ function whoThen(title, payload, doneMsg) {
box.querySelectorAll('.pad button').forEach(b => b.onclick = async () => { let k = b.dataset.k; box.querySelector('#e').textContent = ''; if (k === 'โซ') pin = pin.slice(0, -1); else if (k !== 'โ' && pin.length < 8) { pin += k; if (p.pinLen && pin.length === p.pinLen) k = 'โ'; } if (k === 'โ') { try { await send(p, pin); return; } catch (e) { box.querySelector('#e').textContent = e.message; pin = ''; } } paint(); }); });
}
function toast(msg) { let t = document.getElementById('bt'); if (!t) { t = document.createElement('div'); t.id = 'bt'; t.style.cssText = 'position:fixed;left:50%;bottom:24px;transform:translateX(-50%);background:var(--panel2);border:1px solid var(--line);padding:12px 20px;border-radius:20px;font-size:18px;z-index:99'; document.body.appendChild(t); } t.textContent = msg; t.hidden = false; clearTimeout(t._t); t._t = setTimeout(() => { t.hidden = true; }, 2500); }
+
+// ---- Pick your chores: tap your name, enter your PIN once (a child with no PIN goes straight in), then do, take or put back as many chores as you like
+function choreHub(p) {
+ if (!p) return; let pin = '';
+ const { box, close } = sheet(''), today = ymd(new Date()), head = `
${avatar(p)}
${esc(p.name.split(' ')[0])}'s chores
`;
+ const act = async (c, action) => { const r = await fetch('/api/board/chore', { method: 'POST', body: JSON.stringify({ uid: p.id, pin, chore: c.chore, action, date: today }) }), j = await r.json().catch(() => ({})); if (!r.ok) { if (p.pin && /pin/i.test(j.error || '')) { pin = ''; return askPin(j.error); } box.querySelector('#e').textContent = j.error || 'That did not work'; return; } await load(); paint(); };
+ const paint = () => {
+ const mine = data.chores.filter(c => c.user === p.id), todo = mine.filter(c => !c.done), done = mine.filter(c => c.done), pool = data.chores.filter(c => c.claimable && !c.done), me = (data.week && data.week.people.find(x => x.id === p.id)) || { points: 0 };
+ const row = (c, btns, cls) => `
${esc(c.title)}${c.points ? ` ${c.points} pts` : ''}${btns}
`, h = t => `
${t}
`;
+ box.innerHTML = head + `
${me.points} points this week
`
+ + h('To do today') + (todo.map(c => row(c, `
${c.claimed ? `
` : ''}`)).join('') || '
Nothing left. Nice! ๐
')
+ + (pool.length ? h('Up for grabs. Take one!') + pool.map(c => row(c, `
`)).join('') : '')
+ + (done.length ? h('Finished') + done.map(c => row(c, `
`, 'dn')).join('') : '')
+ + `
`;
+ box.querySelector('#no').onclick = close;
+ box.querySelectorAll('[data-a]').forEach(b => b.onclick = () => { b.disabled = true; const c = data.chores.find(x => x.chore === b.dataset.c && (x.user || '') === b.dataset.u); if (c) act(c, b.dataset.a); else { b.disabled = false; load().then(paint); } });
+ };
+ const askPin = msg => {
+ box.innerHTML = head + `
${esc(p.name)}, enter your PIN
${esc(msg || '')}
${[1, 2, 3, 4, 5, 6, 7, 8, 9, 'โซ', 0, 'โ'].map(k => ``).join('')}
`;
+ const dots = () => { box.querySelector('#dots').innerHTML = Array.from({ length: Math.max(4, pin.length) }, (_, i) => `
`).join(''); }; dots(); box.querySelector('#no').onclick = close;
+ box.querySelectorAll('.pad button').forEach(b => b.onclick = () => { let k = b.dataset.k; box.querySelector('#e').textContent = ''; if (k === 'โซ') pin = pin.slice(0, -1); else if (k !== 'โ' && pin.length < 8) { pin += k; if (p.pinLen && pin.length === p.pinLen) k = 'โ'; } if (k === 'โ') { if (pin) return paint(); } dots(); });
+ };
+ if (p.kid && !p.pin) return paint(); if (!p.pin) { box.innerHTML = head + `
${esc(p.name)} has not set a PIN yet. Set one in Settings on your own device.
`; box.querySelector('#no').onclick = close; return; } askPin();
+}