Family board: fit a 12.9-inch iPad without scrolling; See all for the chore pool
build-container / build (push) Successful in 5m40s
build-container / build (push) Successful in 5m40s
- Fixed-height grid for tablets (3x2 landscape, 2x3 portrait), larger text on big screens - Compact chores card with tappable pills and +N overflow - "See all" opens the full list of up-for-grabs chores to pick from Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
17a282054a
commit
472e7bec7e
2 files changed
+25
-6
No files matched your search
@@ -65,6 +65,21 @@
|
|||||||
#ov { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: grid; place-items: center; padding: 18px; } #ov[hidden] { display: none; }
|
#ov { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: grid; place-items: center; padding: 18px; } #ov[hidden] { display: none; }
|
||||||
#ovb { background: var(--panel); border: 1px solid var(--line); border-radius: 20px; padding: 20px; width: 100%; max-width: 380px; text-align: center; }
|
#ovb { background: var(--panel); border: 1px solid var(--line); border-radius: 20px; padding: 20px; width: 100%; max-width: 380px; text-align: center; }
|
||||||
.err { color: var(--danger); min-height: 20px; }
|
.err { color: var(--danger); min-height: 20px; }
|
||||||
|
.pill { display: inline-flex; align-items: center; gap: 6px; margin: 3px 6px 3px 0; padding: 6px 12px; border-radius: 16px; background: var(--panel2); border: 1px solid var(--line); color: var(--text); font: inherit; font-size: 15px; cursor: pointer; } .pill.pool { border-color: var(--accent); } .pill small { color: var(--muted); font-size: 12px; } .pill.more { background: none; border-style: dashed; color: var(--muted); cursor: default; }
|
||||||
|
.chp { margin-top: 8px; } .chp .top { display: flex; justify-content: space-between; align-items: baseline; }
|
||||||
|
/* a tablet (like a 12.9-inch iPad) shows the whole board at once, with no scrolling */
|
||||||
|
@media (min-width: 700px) and (min-height: 700px) {
|
||||||
|
html, body { height: 100%; overflow: hidden; } body { padding: 14px 18px; }
|
||||||
|
.bd { height: 100%; display: flex; flex-direction: column; max-width: none; }
|
||||||
|
.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; }
|
||||||
|
.tiles { flex-wrap: nowrap; overflow: hidden; 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; }
|
||||||
|
.grid { flex: 1; min-height: 0; grid-template-columns: repeat(2, 1fr); grid-template-rows: repeat(3, minmax(0, 1fr)); gap: 10px; }
|
||||||
|
.card { overflow: hidden; padding: 10px 14px; } .card h2 { margin-bottom: 4px; font-size: 13px; } .ln { padding: 4px 0; font-size: 16px; }
|
||||||
|
.bd > p.sub { display: none; }
|
||||||
|
}
|
||||||
|
@media (min-width: 900px) and (min-height: 1100px) { .ln { font-size: 20px; padding: 6px 0; } .pill { font-size: 18px; padding: 8px 14px; } .card h2 { font-size: 15px; } .tile { width: 108px; } .tile .av { width: 54px; height: 54px; font-size: 24px; } .tile b { font-size: 16px; } .bdtop h1 { font-size: 34px; } .bdclock { font-size: 40px; } .card small, .chp small { font-size: 14px; } }
|
||||||
|
@media (min-width: 1200px) and (min-height: 700px) { .grid { grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(2, minmax(0, 1fr)); } }
|
||||||
.chrow { width: 100%; background: none; border: 0; border-bottom: 1px solid var(--line); color: var(--text); font: inherit; font-size: 17px; text-align: left; cursor: pointer; } .chrow .chk { width: 24px; height: 24px; border: 2px solid var(--line); border-radius: 7px; display: grid; place-items: center; flex: none; font-size: 15px; } .chrow.dn .chk { background: var(--accent); border-color: var(--accent); color: #fff; } .chrow.dn .g { text-decoration: line-through; opacity: .6; }
|
.chrow { width: 100%; background: none; border: 0; border-bottom: 1px solid var(--line); color: var(--text); font: inherit; font-size: 17px; text-align: left; cursor: pointer; } .chrow .chk { width: 24px; height: 24px; border: 2px solid var(--line); border-radius: 7px; display: grid; place-items: center; flex: none; font-size: 15px; } .chrow.dn .chk { background: var(--accent); border-color: var(--accent); color: #fff; } .chrow.dn .g { text-decoration: line-through; opacity: .6; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
+10
-6
@@ -19,17 +19,21 @@ 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').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)));
|
$('#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 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 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 pill = (c, cls) => `<button class="pill ${cls || ''}" data-c="${esc(c.chore)}">${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.userName] = per[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.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('');
|
const SHOW = 4, chores = (pool.length ? `<small class="sub">Up for grabs: tap one</small><div>${pool.slice(0, 6).map(c => pill(c, 'pool')).join('')}${pool.length > 6 ? `<button class="pill more" id="poolall">See all ${pool.length}</button>` : ''}</div>` : '') + 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(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><div>${left.slice(0, SHOW).map(c => pill(c)).join('')}${left.length > SHOW ? `<span class="pill more">+${left.length - SHOW}</span>` : ''}${left.length ? '' : '<small class="sub">All done 🎉</small>'}</div></div>`; }).join('');
|
||||||
$('#grid').innerHTML = card('Today and tomorrow', day('Today', t0) + day('Tomorrow', t1))
|
$('#grid').innerHTML = card('Today and tomorrow', day('Today', t0) + day('Tomorrow', t1))
|
||||||
+ card('Chores today', chores || none('No chores today'))
|
+ card('Chores today', chores || none('No chores today'))
|
||||||
+ card('This week', weekCard(d.week)) + card('Shopping list', d.shopping.map(i => `<div class="ln"><span class="g">${esc(i.text)}</span></div>`).join('') || none('Nothing to buy'))
|
+ card('This week', weekCard(d.week)) + card('Shopping list', d.shopping.slice(0, 10).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 tasks', d.tasks.slice(0, 6).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'));
|
+ card('Family notes and lists', d.notes.slice(0, 4).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();
|
wireChores();
|
||||||
}
|
}
|
||||||
function wireChores() { document.querySelectorAll('.chrow').forEach(b => b.onclick = () => choreTap(data.chores.find(c => c.chore === b.dataset.c))); }
|
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>`);
|
||||||
|
box.querySelector('#no').onclick = close; box.querySelectorAll('.pill[data-c]').forEach(b => b.onclick = () => choreTap(data.chores.find(c => c.chore === b.dataset.c)));
|
||||||
|
}
|
||||||
|
function wireChores() { 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))); }
|
||||||
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) {
|
||||||
|
|||||||
Reference in new issue
Block a user