Family board: one compact header band (greeting, everyone's tile, clock); tapping a child opens their chores, with Open my account inside
build-container / build (push) Canceled after 0s

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-29 09:06:08 -04:00
1 parent 2e6ee15ef2
commit 0991185eb4
2 files changed
+10 -17

No files matched your search

+5 -10
View File
@@ -46,10 +46,10 @@
<style> <style>
body { padding: 18px; } body { padding: 18px; }
.bd { max-width: 1280px; margin: 0 auto; } .bd { max-width: 1280px; margin: 0 auto; }
.bdtop { display: flex; justify-content: space-between; align-items: flex-end; gap: 14px; flex-wrap: wrap; } .bdtop { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; } .bdl { flex: none; } .pickrow { flex: 1 1 320px; min-width: 0; display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; } .bdclock { flex: none; margin-left: auto; }
.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; }
.pickrow .pk { display: flex; align-items: center; gap: 10px; padding: 8px 14px; border-radius: 18px; background: var(--panel); border: 2px solid var(--line); color: var(--text); font: inherit; text-align: left; cursor: pointer; } .pickrow .pk.has { border-color: var(--accent); } .pickrow .pk:active { transform: scale(.97); } .pickrow .pk .av { width: 46px; height: 46px; border-radius: 50%; color: #fff; display: grid; place-items: center; font-size: 21px; font-weight: 700; flex: none; } .pickrow .pk b { display: block; font-size: 18px; } .pickrow .pk small { color: var(--muted); font-size: 13px; white-space: nowrap; }
.bdtop h1 { margin: 0; font-size: 34px; } .bdclock { font-size: 40px; font-weight: 700; font-variant-numeric: tabular-nums; } .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; }
.topz { display: flex; flex-wrap: wrap; align-items: flex-start; column-gap: 28px; } .topz .pickrow { flex: 1 1 380px; min-width: 0; } .topz .who { flex: 0 1 auto; min-width: 0; margin-top: 14px; } .topz .who h2 { margin-bottom: 8px; }
.who { margin: 16px 0; } .who h2 { margin: 0 0 8px; font-size: 18px; } .who { margin: 16px 0; } .who h2 { margin: 0 0 8px; font-size: 18px; }
.tiles { display: flex; gap: 12px; flex-wrap: wrap; } .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; } .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; }
@@ -78,11 +78,10 @@
@media (min-width: 700px) and (min-height: 700px) { @media (min-width: 700px) and (min-height: 700px) {
html, body { height: 100%; overflow: hidden; } body { padding: 14px 18px; } html, body { height: 100%; overflow: hidden; } body { padding: 14px 18px; }
.bd { height: 100%; display: flex; flex-direction: column; max-width: none; } .bd { height: 100%; display: flex; flex-direction: column; max-width: none; }
.pickrow { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; } .pickrow .pk { flex: none; padding: 6px 12px; } .pickrow .pk .av { width: 38px; height: 38px; font-size: 18px; } .pickrow .pk b { font-size: 16px; }
.bdtop h1 { font-size: 28px; } .bdclock { font-size: 32px; } .bdtop p { margin: 0; } .bdtop h1 { font-size: 28px; } .bdclock { font-size: 32px; } .bdtop p { margin: 0; }
.ann { margin: 8px 0; padding: 8px 14px; font-size: 20px; } .who { margin: 8px 0; } .who h2 { margin-bottom: 6px; font-size: 15px; } .ann { margin: 8px 0; padding: 8px 14px; font-size: 20px; } .who { margin: 8px 0; } .who h2 { margin-bottom: 6px; font-size: 15px; }
.tiles { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; gap: 10px; } .tile { width: 92px; padding: 8px 4px; border-radius: 16px; flex: none; } .tile .av { width: 44px; height: 44px; font-size: 20px; } .tile b { font-size: 14px; } .tiles { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; gap: 10px; } .tile { width: 92px; padding: 8px 4px; border-radius: 16px; flex: none; } .tile .av { width: 44px; height: 44px; font-size: 20px; } .tile b { font-size: 14px; }
.topz .who { margin-top: 6px; } .who .tile small { display: none; } .who .tile { width: 78px; padding: 6px 4px; } .who .tile .av { width: 36px; height: 36px; font-size: 17px; }
.pickrow { margin: 6px 0 2px; } .pickrow h2 { margin-bottom: 4px; font-size: 15px; } .pickrow .tiles { flex-wrap: nowrap; overflow-x: auto; } .pickrow .pk { padding: 6px 12px; min-width: 0; flex: none; gap: 10px; border-radius: 16px; } .pickrow .pk .av { width: 38px; height: 38px; font-size: 18px; } .pickrow .pk b { font-size: 16px; } .pickrow .pk small { font-size: 12px; }
.grid { flex: 1; min-height: 0; grid-template-columns: repeat(2, 1fr); grid-template-rows: minmax(0, 1.4fr) repeat(3, minmax(0, 1fr)); gap: 10px; } .grid { flex: 1; min-height: 0; grid-template-columns: repeat(2, 1fr); grid-template-rows: minmax(0, 1.4fr) repeat(3, minmax(0, 1fr)); gap: 10px; }
.card.wide { grid-column: 1 / -1; } .card.wide { grid-column: 1 / -1; }
.card.wide { display: flex; flex-direction: column; } .chcols { flex: 1; min-height: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; align-items: stretch; } .chcols .chp { display: flex; flex-direction: column; min-height: 0; } .chl { flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; display: flex; flex-direction: column; align-items: stretch; overscroll-behavior: contain; } .chl .pill { margin: 3px 0; justify-content: flex-start; } .chl .pill.more { justify-content: center; } .chcols .chp { margin-top: 6px; min-width: 0; } .card.wide { display: flex; flex-direction: column; } .chcols { flex: 1; min-height: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; align-items: stretch; } .chcols .chp { display: flex; flex-direction: column; min-height: 0; } .chl { flex: 1; min-height: 0; overflow-y: auto; -webkit-overflow-scrolling: touch; display: flex; flex-direction: column; align-items: stretch; overscroll-behavior: contain; } .chl .pill { margin: 3px 0; justify-content: flex-start; } .chl .pill.more { justify-content: center; } .chcols .chp { margin-top: 6px; min-width: 0; }
@@ -96,12 +95,8 @@
</head> </head>
<body> <body>
<div class="bd"> <div class="bd">
<div class="bdtop"><div><h1 id="hi">Family board</h1><p class="sub" id="date"></p></div><div class="bdclock" id="clock"></div></div> <div class="bdtop"><div class="bdl"><h1 id="hi">Family board</h1><p class="sub" id="date"></p></div><section class="pickrow" id="pickrow"></section><div class="bdclock" id="clock"></div></div>
<div id="annc"></div> <div id="annc"></div>
<div class="topz">
<section class="pickrow" id="pickrow"></section>
<section class="who"><h2>Who's using this? Tap your name</h2><div class="tiles" id="tiles"></div></section>
</div>
<div class="grid" id="grid"></div> <div class="grid" id="grid"></div>
<p class="sub" style="text-align:center;margin-top:18px"><a href="/login" style="color:var(--accent)">Sign in with a passkey instead</a></p> <p class="sub" style="text-align:center;margin-top:18px"><a href="/login" style="color:var(--accent)">Sign in with a passkey instead</a></p>
</div> </div>
+5 -7
View File
@@ -29,11 +29,9 @@ const none = t => `<p class="none">${t}</p>`;
function draw() { function draw() {
const t0 = ymd(new Date()), t1 = ymd(new Date(Date.now() + 864e5)), d = data; const t0 = ymd(new Date()), t1 = ymd(new Date(Date.now() + 864e5)), d = data;
$('#annc').innerHTML = d.announcement ? `<div class="ann">${esc(d.announcement.text)}<div class="sub" style="font-size:14px;font-weight:400">${esc(d.announcement.by)}</div></div>` : ''; $('#annc').innerHTML = d.announcement ? `<div class="ann">${esc(d.announcement.text)}<div class="sub" style="font-size:14px;font-weight:400">${esc(d.announcement.by)}</div></div>` : '';
$('#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)}${e.by ? ` <small>${esc(e.by)}</small>` : ''}</span><small>${e.allDay ? 'All day' : new Date(e.start).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}</small></div>`).join('') || none('Nothing')); }; 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)}${e.by ? ` <small>${esc(e.by)}</small>` : ''}</span><small>${e.allDay ? 'All day' : new Date(e.start).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}</small></div>`).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)); const left = p => d.chores.filter(c => c.user === p.id && !c.done).length, grabs = d.chores.filter(c => c.claimable && !c.done).length, has = p => p.kid || d.chores.some(c => c.user === p.id);
$('#pickrow').innerHTML = pk.length ? `<h2>🧹 Pick your chores. Tap your name</h2><div class="tiles">${pk.map(p => `<button class="pk" data-hub="${esc(p.id)}"><span class="av" style="background:${p.color}">${esc((p.name.trim()[0] || '?').toUpperCase())}</span><span><b>${esc(p.name.split(' ')[0])}</b><small>${left(p) ? left(p) + ' to do' : 'All done'}${grabs ? ' · ' + grabs + ' to grab' : ''}</small></span></button>`).join('')}</div>` : ''; $('#pickrow').innerHTML = d.people.map(p => `<button class="pk${has(p) ? ' has' : ''}" data-hub="${esc(p.id)}"><span class="av" style="background:${p.color}">${esc((p.name.trim()[0] || '?').toUpperCase())}</span><span><b>${esc(p.name.split(' ')[0])}</b><small>${has(p) ? (left(p) ? left(p) + ' to do' : 'All done') + (grabs ? ' · ' + grabs + ' to grab' : '') : 'Sign in'}</small></span></button>`).join('');
const pill = (c, cls) => `<button class="pill ${cls || ''}" data-c="${esc(c.chore)}" data-u="${esc(c.user || '')}">${esc(c.title)}${c.points ? ` <small>${c.points} pts</small>` : ''}</button>`; const pill = (c, cls) => `<button class="pill ${cls || ''}" data-c="${esc(c.chore)}" data-u="${esc(c.user || '')}">${esc(c.title)}${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.user || c.userName] = per[c.user || c.userName] || []).push(c); }); 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 ? `<button class="pill pool" id="poolall" style="width:100%;justify-content:space-between;font-size:17px"><span>Pick a chore: ${pool.length} up for grabs</span><span>See all ›</span></button>` : '') + '<div class="chcols">' + Object.keys(per).map(k => { const l = per[k], dn = l.filter(c => c.done).length, left = l.filter(c => !c.done); return `<div class="chp"><div class="top"><b>${esc(l[0].userName)}</b><small class="sub">${dn} of ${l.length}</small></div><div class="bar2"><i style="width:${Math.round(dn / l.length * 100)}%"></i></div><div class="chl">${left.map(c => pill(c)).join('')}${left.length ? '' : '<small class="sub">All done 🎉</small>'}</div></div>`; }).join('') + '</div>'; const SHOW = 6, chores = (pool.length ? `<button class="pill pool" id="poolall" style="width:100%;justify-content:space-between;font-size:17px"><span>Pick a chore: ${pool.length} up for grabs</span><span>See all ›</span></button>` : '') + '<div class="chcols">' + Object.keys(per).map(k => { const l = per[k], dn = l.filter(c => c.done).length, left = l.filter(c => !c.done); return `<div class="chp"><div class="top"><b>${esc(l[0].userName)}</b><small class="sub">${dn} of ${l.length}</small></div><div class="bar2"><i style="width:${Math.round(dn / l.length * 100)}%"></i></div><div class="chl">${left.map(c => pill(c)).join('')}${left.length ? '' : '<small class="sub">All done 🎉</small>'}</div></div>`; }).join('') + '</div>';
@@ -48,7 +46,7 @@ function poolAll() {
const list = data.chores.filter(c => c.claimable && !c.done), { box, close } = sheet(`<h3 style="margin:0 0 4px">Up for grabs</h3><p class="sub">${list.length} chores. Tap one to pick it</p><div style="text-align:left;max-height:58dvh;overflow:auto">${list.map(c => `<button class="pill pool" data-c="${esc(c.chore)}" style="width:100%;justify-content:space-between;font-size:18px;padding:12px 14px;margin:4px 0"><span>${esc(c.title)}</span>${c.points ? `<small>${c.points} pts</small>` : ''}</button>`).join('')}</div><p><button class="btn ghost" id="no" style="width:100%;justify-content:center">Close</button></p>`); const list = data.chores.filter(c => c.claimable && !c.done), { box, close } = sheet(`<h3 style="margin:0 0 4px">Up for grabs</h3><p class="sub">${list.length} chores. Tap one to pick it</p><div style="text-align:left;max-height:58dvh;overflow:auto">${list.map(c => `<button class="pill pool" data-c="${esc(c.chore)}" style="width:100%;justify-content:space-between;font-size:18px;padding:12px 14px;margin:4px 0"><span>${esc(c.title)}</span>${c.points ? `<small>${c.points} pts</small>` : ''}</button>`).join('')}</div><p><button class="btn ghost" id="no" style="width:100%;justify-content:center">Close</button></p>`);
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 || '')))); 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-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 || '')))); } function wireChores() { document.querySelectorAll('[data-hub]').forEach(b => b.onclick = () => { const p = data.people.find(x => x.id === b.dataset.hub); if (p && (p.kid || data.chores.some(c => c.user === p.id))) choreHub(p); else pick(p); }); 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 {} } 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) { function pick(p) {
@@ -128,8 +126,8 @@ function choreHub(p) {
+ h('To do today') + (todo.map(c => row(c, `<button class="btn" data-a="done" data-c="${esc(c.chore)}" data-u="${esc(c.user || '')}">Done ✓</button>${c.claimed ? `<button class="btn ghost" data-a="unclaim" data-c="${esc(c.chore)}" data-u="${esc(c.user || '')}">Put back</button>` : ''}`)).join('') || '<p class="none" style="text-align:left">Nothing left. Nice! 🎉</p>') + h('To do today') + (todo.map(c => row(c, `<button class="btn" data-a="done" data-c="${esc(c.chore)}" data-u="${esc(c.user || '')}">Done ✓</button>${c.claimed ? `<button class="btn ghost" data-a="unclaim" data-c="${esc(c.chore)}" data-u="${esc(c.user || '')}">Put back</button>` : ''}`)).join('') || '<p class="none" style="text-align:left">Nothing left. Nice! 🎉</p>')
+ (pool.length ? h('Up for grabs. Take one!') + pool.map(c => row(c, `<button class="btn ghost" data-a="claim" data-c="${esc(c.chore)}" data-u="${esc(c.user || '')}">Take it</button><button class="btn" data-a="done" data-c="${esc(c.chore)}" data-u="${esc(c.user || '')}">I did it</button>`)).join('') : '') + (pool.length ? h('Up for grabs. Take one!') + pool.map(c => row(c, `<button class="btn ghost" data-a="claim" data-c="${esc(c.chore)}" data-u="${esc(c.user || '')}">Take it</button><button class="btn" data-a="done" data-c="${esc(c.chore)}" data-u="${esc(c.user || '')}">I did it</button>`)).join('') : '')
+ (done.length ? h('Finished') + done.map(c => row(c, `<button class="btn ghost" data-a="undo" data-c="${esc(c.chore)}" data-u="${esc(c.user || '')}">Undo</button>`, 'dn')).join('') : '') + (done.length ? h('Finished') + done.map(c => row(c, `<button class="btn ghost" data-a="undo" data-c="${esc(c.chore)}" data-u="${esc(c.user || '')}">Undo</button>`, 'dn')).join('') : '')
+ `</div><div class="err" id="e"></div><p><button class="btn ghost" id="no" style="width:100%;justify-content:center">Close</button></p>`; + `</div><div class="err" id="e"></div><div style="display:flex;gap:10px;margin-top:10px"><button class="btn ghost" id="sgn" style="flex:1;justify-content:center">Open my account</button><button class="btn" id="no" style="flex:1;justify-content:center">Close</button></div>`;
box.querySelector('#no').onclick = close; box.querySelector('#no').onclick = close; box.querySelector('#sgn').onclick = () => { close(); pick(p); };
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); } }); 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 => { const askPin = msg => {