Settings: a Family tab gathering chat, PIN, chores and rewards, family board people and shared screens
build-container / build (push) Successful in 5m27s
build-container / build (push) Successful in 5m27s
- New Family tab in Settings; each row opens the existing sheet - Admin switches for who appears on the family board - Shared-screen controls moved here from Security - Chore manager can be reopened from Settings after a save Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3c721ef6a5
commit
900e900e53
5 files changed
+33
-8
No files matched your search
@@ -19,6 +19,8 @@ function crWeekPerson(p) {
|
||||
${p.earned.map(id => { const t = w.tiers.find(x => x.id === id), given = p.given.includes(id); return `<div class="crearn"><span>🎁</span><span class="grow"><b>${esc(t.title)}</b><small>${given ? 'Given' : 'Earned, not given yet'}</small></span>${admin ? `<button class="btn ${given ? 'ghost' : ''}" data-give="${esc(p.id)}|${esc(id)}" ${given ? 'data-undo="1"' : ''}>${given ? 'Undo' : 'Mark given'}</button>` : ''}</div>`; }).join('')}
|
||||
${p.done.length ? `<details class="crdet"><summary>Chores done (${p.done.length})</summary>${p.done.map(c => `<div class="crrank" style="border:0;padding:3px 0"><span class="grow">${esc(c.title)}<small style="display:block">${esc(new Date(c.date + 'T12:00:00').toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }))}</small></span><small>${c.pts ? c.pts + ' pts' : ''}</small></div>`).join('')}</details>` : ''}</div>`;
|
||||
}
|
||||
/** After a change in the manage sheets: redraw the Chores page, or (when opened from Settings) reopen the manager. */
|
||||
const crRefresh = () => location.hash.startsWith('#/chores') ? viewChores() : (cr.afterEdit && cr.afterEdit());
|
||||
async function viewChores() {
|
||||
const v = $('#view'); cr.day = cr.day || crYmd(new Date());
|
||||
v.innerHTML = '<div class="wrap"><h1>Chores</h1><p class="empty">Loading…</p></div>';
|
||||
@@ -62,8 +64,8 @@ function crReward(r) {
|
||||
<label>Who is it for?</label><div class="list"><label class="row" style="cursor:pointer"><input type="checkbox" id="rwall"${!r || !Array.isArray(r.for) ? ' checked' : ''}><span class="grow"><b>Everyone</b></span></label></div><div class="list" id="rwpp">${cr.data.people.map(p => `<label class="row" style="cursor:pointer"><input type="checkbox" data-p="${esc(p.id)}"${r && Array.isArray(r.for) && r.for.includes(p.id) ? ' checked' : ''}><span class="grow"><b>${esc(p.name)}</b></span></label>`).join('')}</div><div class="err" id="rwe"></div>
|
||||
<div class="bar">${r ? '<button type="button" class="btn danger" id="rwdel">Delete</button>' : ''}<button type="button" class="btn ghost" id="rwx" style="flex:1;justify-content:center">Cancel</button><button class="btn" style="flex:1;justify-content:center">Save</button></div></form>`);
|
||||
const rsync = () => { el.querySelector('#rwpp').hidden = el.querySelector('#rwall').checked; }; el.querySelector('#rwall').onchange = rsync; rsync();
|
||||
el.querySelector('#rwx').onclick = closeSheet; if (r) el.querySelector('#rwdel').onclick = async () => { try { await crPost('/reward', { id: r.id, remove: true }); closeSheet(); viewChores(); } catch (e) { toast(e.message); } };
|
||||
el.querySelector('#rwf').onsubmit = async e => { e.preventDefault(); try { await crPost('/reward', { id: r && r.id, title: el.querySelector('#rwt').value, points: +el.querySelector('#rwp').value, for: el.querySelector('#rwall').checked ? 'all' : [...el.querySelectorAll('#rwpp [data-p]:checked')].map(i => i.dataset.p) }); closeSheet(); viewChores(); } catch (x) { el.querySelector('#rwe').textContent = x.message; } };
|
||||
el.querySelector('#rwx').onclick = closeSheet; if (r) el.querySelector('#rwdel').onclick = async () => { try { await crPost('/reward', { id: r.id, remove: true }); closeSheet(); crRefresh(); } catch (e) { toast(e.message); } };
|
||||
el.querySelector('#rwf').onsubmit = async e => { e.preventDefault(); try { await crPost('/reward', { id: r && r.id, title: el.querySelector('#rwt').value, points: +el.querySelector('#rwp').value, for: el.querySelector('#rwall').checked ? 'all' : [...el.querySelectorAll('#rwpp [data-p]:checked')].map(i => i.dataset.p) }); closeSheet(); crRefresh(); } catch (x) { el.querySelector('#rwe').textContent = x.message; } };
|
||||
}
|
||||
function crEdit(c) {
|
||||
const d = cr.data, x = c || { title: '', notes: '', repeat: 'daily', days: [], dom: 1, date: crYmd(new Date()), assign: 'anyone', points: 0, active: true }, chosen = new Set(x.assign === 'anyone' ? [] : x.assign);
|
||||
@@ -80,8 +82,8 @@ function crEdit(c) {
|
||||
const sync = () => { const r = el.querySelector('#crr').value; el.querySelector('#crd').hidden = r !== 'weekly'; el.querySelector('#crm').hidden = r !== 'monthly'; el.querySelector('#cro').hidden = r !== 'once'; el.querySelector('#crp').hidden = el.querySelector('[name=crw]:checked').value !== 'people'; };
|
||||
el.querySelector('#crr').onchange = sync; el.querySelectorAll('[name=crw]').forEach(r => r.onchange = sync); sync(); el.querySelectorAll('#crd [data-d]').forEach(a => a.onclick = e => { e.preventDefault(); a.classList.toggle('on'); });
|
||||
el.querySelector('#crx').onclick = closeSheet;
|
||||
if (c) el.querySelector('#crdel').onclick = () => { closeSheet(); confirmSheet('Delete this chore?', c.title + ' and its history.', 'Delete', async () => { try { await crApi('/' + c.id, { method: 'DELETE' }); toast('Deleted'); viewChores(); } catch (e) { toast(e.message); } }); };
|
||||
if (c) el.querySelector('#crdel').onclick = () => { closeSheet(); confirmSheet('Delete this chore?', c.title + ' and its history.', 'Delete', async () => { try { await crApi('/' + c.id, { method: 'DELETE' }); toast('Deleted'); crRefresh(); } catch (e) { toast(e.message); } }); };
|
||||
el.querySelector('#cref').onsubmit = async e => { e.preventDefault(); const people = [...el.querySelectorAll('[data-p]:checked')].map(i => i.dataset.p);
|
||||
try { await crPost('/', { id: c && c.id, title: el.querySelector('#crt').value, notes: el.querySelector('#crn').value, repeat: el.querySelector('#crr').value, days: [...el.querySelectorAll('#crd .on')].map(a => +a.dataset.d), dom: +el.querySelector('#crdom').value, date: el.querySelector('#crdt').value, assign: el.querySelector('[name=crw]:checked').value === 'anyone' ? 'anyone' : people, points: +el.querySelector('#crpt').value, active: el.querySelector('#cra').checked }); closeSheet(); toast('Saved'); viewChores(); }
|
||||
try { await crPost('/', { id: c && c.id, title: el.querySelector('#crt').value, notes: el.querySelector('#crn').value, repeat: el.querySelector('#crr').value, days: [...el.querySelectorAll('#crd .on')].map(a => +a.dataset.d), dom: +el.querySelector('#crdom').value, date: el.querySelector('#crdt').value, assign: el.querySelector('[name=crw]:checked').value === 'anyone' ? 'anyone' : people, points: +el.querySelector('#crpt').value, active: el.querySelector('#cra').checked }); closeSheet(); toast('Saved'); crRefresh(); }
|
||||
catch (x) { el.querySelector('#cre').textContent = x.message; } };
|
||||
}
|
||||
@@ -103,6 +103,7 @@
|
||||
<script src="/keys.js"></script>
|
||||
<script src="/inbox-view.js"></script>
|
||||
<script src="/device-ui.js"></script>
|
||||
<script src="/settings-family.js"></script>
|
||||
<script src="/boot.js"></script>
|
||||
<script src="/shell.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
// ---- Settings > Family: the family's settings in one place (chat, your PIN, chores and rewards, who is on the family board, shared screens).
|
||||
// Each row opens the same sheet that used to live inside that part of the hub.
|
||||
const famRow = (id, ic, title, sub) => `<button class="row" id="${id}"><span class="ic" style="background:#6a7280">${icon(ic)}</span><span class="grow"><b>${esc(title)}</b><small style="white-space:normal">${esc(sub)}</small></span>${icon('chev', 'chev')}</button>`;
|
||||
async function famManageChores() {
|
||||
try { cr.data = await crApi('/?date=' + crYmd(new Date())); } catch (e) { return toast(e.message); }
|
||||
cr.afterEdit = famManageChores; crManage();
|
||||
}
|
||||
async function famSettings(box) {
|
||||
if (!box) return; const admin = ME && ME.role === 'admin', can = t => typeof allowed === 'function' && allowed(t);
|
||||
box.innerHTML = `<h2>You</h2><div class="list">${can('chat') ? famRow('fs-chat', 'chat', 'Chat', 'Quiet hours for phone alerts' + (admin ? ', and the family announcement' : '')) : ''}</div><div id="fs-dev"></div>
|
||||
${admin && can('chores') ? `<h2>Chores</h2><div class="list">${famRow('fs-chores', 'check', 'Chores and weekly rewards', 'Make chores, choose who does them, set the points and the reward tiers')}</div>` : ''}
|
||||
${admin ? '<h2>Family board</h2><p class="sub" style="margin:0 0 8px">Who has a name tile on the shared family screen and shows in its chore progress.</p><div class="list" id="fs-board"><div class="row"><span class="grow"><small>Loading…</small></span></div></div>' : ''}`;
|
||||
const c = box.querySelector('#fs-chat'); if (c) c.onclick = () => chSettings();
|
||||
const k = box.querySelector('#fs-chores'); if (k) k.onclick = famManageChores;
|
||||
if (typeof devSettings === 'function') devSettings(box.querySelector('#fs-dev'));
|
||||
if (admin) try {
|
||||
const r = await jfetch('/api/users'), b = box.querySelector('#fs-board'); if (!b) return;
|
||||
b.innerHTML = r.users.filter(u => !u.disabled).map(u => `<label class="row" style="cursor:pointer"><span class="ic" style="background:${u.role === 'admin' ? '#4f7cff' : '#6a7280'}">${esc(u.name.slice(0, 1).toUpperCase())}</span><span class="grow"><b>${esc(u.name)}</b>${u.kid ? '<small>Child</small>' : ''}</span><input type="checkbox" data-u="${esc(u.id)}" ${u.board === false ? '' : 'checked'}></label>`).join('');
|
||||
b.querySelectorAll('input[data-u]').forEach(i => i.onchange = async () => { try { await jfetch('/api/users/' + encodeURIComponent(i.dataset.u), { method: 'PUT', body: JSON.stringify({ board: i.checked }) }); } catch (e) { toast(e.message); i.checked = !i.checked; } });
|
||||
} catch (e) { const b = box.querySelector('#fs-board'); if (b) b.innerHTML = `<div class="row"><span class="grow"><small>${esc(e.message)}</small></span></div>`; }
|
||||
}
|
||||
+3
-3
@@ -78,7 +78,7 @@ function viewSettings() {
|
||||
const isAdmin = ME && ME.role === 'admin';
|
||||
const tabNames = ME ? TABS.filter(t => ME.tabs.includes(t[0]) && !['home', 'settings'].includes(t[0])).map(t => t[1]).join(', ') : '';
|
||||
$('#view').innerHTML = `<div class="wrap"><h1>Settings</h1>
|
||||
<div class="sgrp" id="settabs" role="tablist" aria-label="Settings sections">${[['general', 'General'], ['connections', 'Connections'], ['security', 'Security'], ...(isAdmin ? [['admin', 'Admin']] : [])].map(([k, l]) => `<a href="#" role="tab" data-g="${k}">${l}</a>`).join('')}</div>
|
||||
<div class="sgrp" id="settabs" role="tablist" aria-label="Settings sections">${[['general', 'General'], ['connections', 'Connections'], ['family', 'Family'], ['security', 'Security'], ...(isAdmin ? [['admin', 'Admin']] : [])].map(([k, l]) => `<a href="#" role="tab" data-g="${k}">${l}</a>`).join('')}</div>
|
||||
<div class="sgroup" data-g="general">${ME ? `<h2>Signed in as</h2><div class="list"><div class="row"><span class="grow"><b>${esc(ME.name)}</b><small style="white-space:normal">${isAdmin ? 'Admin · can use everything and manage people' : 'Can use: ' + esc(tabNames || 'Home and Settings only')}</small></span></div></div>` : ''}
|
||||
<div id="sec-signal"><h2>Signal alerts</h2><p class="sub">How this computer gets your attention when a Signal message arrives (not while you are looking at that chat). Saved on this device only.</p>
|
||||
<div class="list"><label class="row" style="cursor:pointer"><span class="grow"><b>Play a sound</b><small>A short chime</small></span><input type="checkbox" id="sga-s"></label>
|
||||
@@ -98,9 +98,9 @@ function viewSettings() {
|
||||
<label for="ab-p">Password</label>
|
||||
<input type="password" id="ab-p" autocomplete="new-password" value="${esc(abCreds().p)}"></div>
|
||||
</div></div>
|
||||
<div class="sgroup" data-g="family"><div id="famset"></div></div>
|
||||
<div class="sgroup" data-g="security"><h2>Passkeys</h2><div class="list" id="pks"><div class="row"><span class="grow"><small>Loading…</small></span></div></div>
|
||||
<div class="bar"><button class="btn ghost" id="addpk">Add a passkey</button><button class="btn ghost" id="invpk">Invite a device</button><button class="btn danger" id="hubout">Sign out of NE Hub</button></div>
|
||||
<div id="famdev"></div>
|
||||
<h2>Recovery codes</h2><div class="list"><div class="row"><span class="grow"><b id="rcn">Checking…</b><small style="white-space:normal">One-time codes that let you add a new passkey if you lose every device. Keep them somewhere safe (a password manager or on paper).</small></span><button class="btn ghost" id="rcgen">Make codes</button></div></div>
|
||||
</div>
|
||||
<div class="sgroup" data-g="admin">${isAdmin ? `<h2>People</h2><div class="list" id="people"><div class="row"><span class="grow"><small>Loading…</small></span></div></div>
|
||||
@@ -196,7 +196,7 @@ function viewSettings() {
|
||||
const el = $('#audit'); if (!el) return;
|
||||
el.innerHTML = events.length ? events.slice(0, 40).map(e => `<div class="row"><span class="grow"><b>${esc(e.event)}${e.who ? ' · ' + esc(e.who) : ''}</b><small style="white-space:normal">${esc(new Date(e.t).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' }))}${e.detail ? ' · ' + esc(e.detail) : ''}${e.ip ? ' · ' + esc(e.ip) : ''}</small></span></div>`).join('') : '<div class="row"><span class="grow"><small>Nothing yet</small></span></div>';
|
||||
}).catch(() => { const el = $('#audit'); if (el) el.innerHTML = '<div class="row"><span class="grow"><small>Could not load</small></span></div>'; });
|
||||
if (typeof devSettings === 'function') devSettings($('#famdev'));
|
||||
if (typeof famSettings === 'function') famSettings($('#famset'));
|
||||
$('#invpk').onclick = async () => {
|
||||
try {
|
||||
const { code, minutes } = await authPost('/api/auth/invite');
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
// App-shell cache only; Seafile/API traffic always goes to the network.
|
||||
const CACHE = 'hub-v78';
|
||||
const CACHE = 'hub-v79';
|
||||
const SHARE = 'hub-share'; // files shared into the app from other apps, held until the person picks a folder
|
||||
const SHELL = ['/icon.svg', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/apple-touch-icon.png'];
|
||||
|
||||
|
||||
Reference in new issue
Block a user