Family board: tap chores to take, finish or undo them; style number inputs
build-container / build (push) Successful in 4m42s
build-container / build (push) Successful in 4m42s
- Chores on the board are tappable; pick your name (PIN unless a child) and it happens as you - Up-for-grabs chores offer "I did it" or "Take it for later" - Number inputs (chore points, time limit) use the dark input style so they are readable - Tests and README updated Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
870c7129b9
commit
0df1ff5705
7 files changed
+55
-13
No files matched your search
+27
-2
@@ -15,14 +15,17 @@ function draw() {
|
||||
$('#tiles').innerHTML = d.people.map(p => `<button class="tile" data-id="${esc(p.id)}"><span class="av" style="background:${p.color}">${esc((p.name.trim()[0] || '?').toUpperCase())}</span><b>${esc(p.name.split(' ')[0])}</b><small>${p.kid ? '<span class="kid">KID</span>' : ''}</small></button>`).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 `<small class="sub">${label}</small>` + (l.map(e => `<div class="ln"><span class="dot" style="background:${esc(e.color || 'var(--accent)')}"></span><span class="g">${esc(e.title)}</span><small>${e.allDay ? 'All day' : new Date(e.start).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}</small></div>`).join('') || none('Nothing')); };
|
||||
const per = {}; d.chores.forEach(c => { const k = c.userName; (per[k] = per[k] || []).push(c); });
|
||||
const chores = Object.keys(per).map(k => { const l = per[k], dn = l.filter(c => c.done).length; return `<div class="ln" style="display:block"><div style="display:flex;justify-content:space-between"><b>${esc(k)}</b><small>${dn} of ${l.length}</small></div><div class="bar2"><i style="width:${Math.round(dn / l.length * 100)}%"></i></div><small>${esc(l.filter(c => !c.done).slice(0, 3).map(c => c.title).join(' · ') || 'All done 🎉')}</small></div>`; }).join('');
|
||||
const row = c => `<button class="ln chrow ${c.done ? 'dn' : ''}" data-c="${esc(c.chore)}"><span class="chk">${c.done ? '✓' : ''}</span><span class="g">${esc(c.title)}${c.done ? ` <small>${esc(c.doneBy)}</small>` : c.claimed ? ` <small>${esc(c.userName)} took it</small>` : ''}</span>${c.points ? `<small>${c.points} pts</small>` : ''}</button>`;
|
||||
const pool = d.chores.filter(c => c.claimable && !c.done), per = {}; d.chores.filter(c => !(c.claimable && !c.done)).forEach(c => { (per[c.userName] = per[c.userName] || []).push(c); });
|
||||
const chores = (pool.length ? `<small class="sub">Up for grabs: tap to take one</small>${pool.map(row).join('')}` : '') + Object.keys(per).map(k => { const l = per[k], dn = l.filter(c => c.done).length; return `<div style="margin-top:8px"><div style="display:flex;justify-content:space-between"><b>${esc(k)}</b><small class="sub">${dn} of ${l.length}</small></div><div class="bar2"><i style="width:${Math.round(dn / l.length * 100)}%"></i></div>${l.map(row).join('')}</div>`; }).join('');
|
||||
$('#grid').innerHTML = card('Today and tomorrow', day('Today', t0) + day('Tomorrow', t1))
|
||||
+ card('Chores today', chores || none('No chores today'))
|
||||
+ card('Shopping list', d.shopping.map(i => `<div class="ln"><span class="g">${esc(i.text)}</span></div>`).join('') || none('Nothing to buy'))
|
||||
+ card('Family tasks', d.tasks.map(t => `<div class="ln"><span class="g">${esc(t.title)}</span><small style="${t.due && t.due < t0 ? 'color:var(--danger)' : ''}">${t.due === t0 ? 'Today' : esc(t.due)}</small></div>`).join('') || none('Nothing due'))
|
||||
+ card('Family notes and lists', d.notes.map(n => `<div class="ln"><span class="g"><b>${esc(n.title)}</b>${n.type === 'list' ? ` <small>${n.done} of ${n.total} done${n.next ? ' · next: ' + esc(n.next) : ''}</small>` : ` <small>${esc(n.body)}</small>`}</span></div>`).join('') || none('No shared notes'));
|
||||
wireChores();
|
||||
}
|
||||
function wireChores() { document.querySelectorAll('.chrow').forEach(b => b.onclick = () => choreTap(data.chores.find(c => c.chore === b.dataset.c))); }
|
||||
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) {
|
||||
@@ -40,3 +43,25 @@ function pick(p) {
|
||||
}
|
||||
clock(); setInterval(clock, 15000); load(); setInterval(() => { if (!document.hidden && $('#ov').hidden) load(); }, 60000);
|
||||
document.addEventListener('visibilitychange', () => { if (!document.hidden) load(); });
|
||||
|
||||
// ---- tap a chore: (if it is up for grabs, choose what you are doing), pick your name, PIN if you are not a child, then it happens as you
|
||||
const avatar = p => `<span class="av" style="background:${p.color}">${esc((p.name.trim()[0] || '?').toUpperCase())}</span>`;
|
||||
function sheet(html) { const ov = $('#ov'); ov.hidden = false; $('#ovb').innerHTML = html; return { box: $('#ovb'), close: () => { ov.hidden = true; $('#ovb').innerHTML = ''; } }; }
|
||||
function choreTap(c) {
|
||||
if (!c) return; const title = `<h3 style="margin:0 0 4px">${esc(c.title)}</h3>`;
|
||||
const who = action => { // step: which person
|
||||
const list = c.user ? data.people.filter(p => p.id === c.user) : data.people, { box, close } = sheet(`${title}<p class="sub">${c.user && !c.done ? 'This one is ' + esc(c.userName) + "'s. Tap your name" : 'Who are you?'}</p><div class="tiles" style="justify-content:center">${list.map(p => `<button class="tile" data-id="${esc(p.id)}">${avatar(p)}<b>${esc(p.name.split(' ')[0])}</b><small>${p.kid ? '<span class="kid">KID</span>' : ''}</small></button>`).join('')}</div><p><button class="btn ghost" id="no" style="width:100%;justify-content:center">Cancel</button></p>`);
|
||||
box.querySelector('#no').onclick = close; box.querySelectorAll('.tile').forEach(t => t.onclick = () => proof(data.people.find(p => p.id === t.dataset.id), action, close));
|
||||
};
|
||||
const proof = (p, action, close) => { // step: PIN if needed, then do it
|
||||
const send = async pin => { const r = await fetch('/api/board/chore', { method: 'POST', body: JSON.stringify({ uid: p.id, pin, chore: c.chore, action, date: ymd(new Date()) }) }), j = await r.json().catch(() => ({})); if (!r.ok) throw new Error(j.error || 'That did not work'); close(); await load(); };
|
||||
if (p.kid && !p.pin) return send().catch(e => { const x = sheet(`${title}<p class="err">${esc(e.message)}</p><button class="btn ghost" id="no" style="width:100%;justify-content:center">OK</button>`); x.box.querySelector('#no').onclick = x.close; });
|
||||
if (!p.pin) { const x = sheet(`${title}<p>${esc(p.name)} has not set a PIN yet. Set one in Settings on your own device.</p><button class="btn ghost" id="no" style="width:100%;justify-content:center">OK</button>`); x.box.querySelector('#no').onclick = x.close; return; }
|
||||
let pin = ''; const { box } = sheet(`${title}<p class="sub">${esc(p.name)}, enter your PIN</p><div class="dots" id="dots"></div><div class="err" id="e"></div><div class="pad">${[1, 2, 3, 4, 5, 6, 7, 8, 9, '⌫', 0, '✓'].map(k => `<button data-k="${k}">${k}</button>`).join('')}</div><p><button class="btn ghost" id="no" style="width:100%;justify-content:center">Cancel</button></p>`);
|
||||
const paint = () => { box.querySelector('#dots').innerHTML = Array.from({ length: Math.max(4, pin.length) }, (_, i) => `<i class="${i < pin.length ? 'on' : ''}"></i>`).join(''); }; paint(); box.querySelector('#no').onclick = close;
|
||||
box.querySelectorAll('.pad button').forEach(b => b.onclick = async () => { const k = b.dataset.k; box.querySelector('#e').textContent = ''; if (k === '⌫') pin = pin.slice(0, -1); else if (k === '✓') { try { await send(pin); return; } catch (e) { box.querySelector('#e').textContent = e.message; pin = ''; } } else if (pin.length < 8) pin += k; paint(); });
|
||||
};
|
||||
if (c.done) return who('undo');
|
||||
if (c.claimable) { const { box, close } = sheet(`${title}<p class="sub">${c.points ? c.points + ' points' : 'Up for grabs'}</p><button class="btn" id="did" style="width:100%;min-height:52px;justify-content:center;margin-bottom:10px">I did it</button><button class="btn ghost" id="take" style="width:100%;min-height:52px;justify-content:center">Take it for later</button><p><button class="btn ghost" id="no" style="width:100%;justify-content:center">Cancel</button></p>`); box.querySelector('#no').onclick = close; box.querySelector('#did').onclick = () => who('done'); box.querySelector('#take').onclick = () => who('claim'); return; }
|
||||
who('done');
|
||||
}
|
||||
Reference in new issue
Block a user