Devotion: reading plan with check-ins, and a family prayer list
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d7ee6f1339
commit
b0ca8daa2e
4 files changed
+89
-6
No files matched your search
@@ -0,0 +1,47 @@
|
||||
'use strict';
|
||||
// ---- Devotion (inside Bible): a verse for today, a read-through-the-Bible-in-a-year plan with a check-in each day, and a prayer list the whole family shares.
|
||||
const DV_VERSES = ['JHN 3:16', 'PSA 23:1-3', 'PRO 3:5-6', 'ROM 8:28', 'PHP 4:6-7', 'ISA 41:10', 'JER 29:11', 'MAT 6:33', 'PSA 46:1', 'JOS 1:9', 'ROM 12:2', 'GAL 5:22-23', '1CO 13:4-7', 'EPH 2:8-9', 'PSA 119:105', 'MAT 11:28-30', 'COL 3:23', 'PSA 27:1', '2TI 1:7', 'HEB 11:1', 'PSA 121:1-2', 'MIC 6:8', 'JHN 14:27', 'ROM 5:8', '1JN 4:19', 'PSA 37:4-5', 'LAM 3:22-23', 'ISA 40:31', 'MAT 5:14-16', 'JAS 1:5', '1PE 5:7', 'PSA 34:8', 'DEU 31:6', 'PRO 22:6', 'EPH 6:1-3', '1TH 5:16-18', 'PSA 139:13-14', 'JHN 15:12-13', 'ROM 15:13', 'NUM 6:24-26', 'PSA 103:1-2', 'HEB 13:5-6', 'PRO 16:3', '2CO 12:9', 'JHN 1:1-5', 'PSA 100:4-5', 'MAT 28:19-20', 'ZEP 3:17', 'PSA 51:10', 'COL 3:12-14'];
|
||||
const dvDay = (d = new Date()) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
const dvDiff = (a, b) => Math.round((new Date(b + 'T12:00:00') - new Date(a + 'T12:00:00')) / 864e5);
|
||||
/** The text of verses v..ve in a chapter, footnote markers left out. */
|
||||
function dvText(book, c, v, ve) {
|
||||
const out = []; for (const b of book.chapters[c - 1]) if (b[0] === 'v' && b[1] >= v && b[1] <= (ve || v)) out.push(b[2].map(p => typeof p === 'string' ? p : Array.isArray(p) ? p[1] : '').join('').replace(/\s+/g, ' ').trim());
|
||||
return out.join(' ');
|
||||
}
|
||||
function dvVerseOfDay() {
|
||||
const now = new Date(), n = Math.floor((now - new Date(now.getFullYear(), 0, 0)) / 864e5), m = /^(\w{3}) (\d+):(\d+)(?:-(\d+))?$/.exec(DV_VERSES[n % DV_VERSES.length]), book = bbBook(m[1]);
|
||||
return book ? { ref: bbRef(m[1], +m[2], +m[3], m[4] ? +m[4] : 0), text: dvText(book, +m[2], +m[3], m[4] ? +m[4] : 0), link: `#/bible/${m[1]}/${m[2]}/${m[3]}` } : null;
|
||||
}
|
||||
/** Every chapter of the Bible in order, split into 365 daily readings. */
|
||||
function dvReadings(day) {
|
||||
const all = []; for (const b of bb.books) for (let c = 1; c <= b.chapters.length; c++) all.push([b.id, c]);
|
||||
const from = Math.floor(day * all.length / 365), to = Math.floor((day + 1) * all.length / 365); return all.slice(from, to);
|
||||
}
|
||||
async function bbToday() {
|
||||
const v = $('#view'); let plan = bbSt.plan || null, prayers = [], showAnswered = false;
|
||||
try { prayers = await bbApi('/prayers'); } catch {}
|
||||
const draw = () => {
|
||||
const vod = dvVerseOfDay(), today = dvDay(), idx = plan ? dvDiff(plan.start, today) : -1, day = Math.min(364, Math.max(0, idx)), done = new Set(plan ? plan.done : []);
|
||||
let streak = 0; if (plan) for (let d = Math.min(idx, 364) - (done.has(Math.min(idx, 364)) ? 0 : 1); d >= 0 && done.has(d); d--) streak++;
|
||||
const open = prayers.filter(p => !p.answered), answered = prayers.filter(p => p.answered);
|
||||
const prayRow = p => `<div class="row"><span class="grow"><span style="display:block;white-space:normal">${esc(p.text)}</span><small>${esc(p.byName)} · ${new Date(p.answered ? p.answeredAt : p.at).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}${p.answered ? ' · answered' : ''}</small></span><button class="ibtn" data-pa="${p.id}" data-on="${p.answered ? '' : '1'}" aria-label="${p.answered ? 'Mark as still praying' : 'Mark as answered'}" title="${p.answered ? 'Still praying' : 'Answered'}">${icon('check')}</button>${p.mine || (typeof ME !== 'undefined' && ME && ME.role === 'admin') ? `<button class="ibtn" data-pd="${p.id}" aria-label="Remove" title="Remove">${icon('trash')}</button>` : ''}</div>`;
|
||||
v.innerHTML = `<div class="wrap"><div class="ptop"><h1>Devotion</h1><a class="btn ghost" href="#/bible">${icon('bible')}Bible</a></div>
|
||||
${vod ? `<h2>Verse for today</h2><a class="card dvverse" href="${vod.link}"><p>“${esc(vod.text)}”</p><small>${esc(vod.ref)} · BSB</small></a>` : ''}
|
||||
<h2>Reading plan</h2>${plan ? `<div class="card" style="max-width:none"><b>Day ${day + 1} of 365</b><span class="sub"> · ${done.size} day${done.size === 1 ? '' : 's'} done${streak > 1 ? ` · ${streak} in a row` : ''}</span>
|
||||
<div class="dvread">${idx < 0 ? `<p class="sub">Your plan starts ${esc(plan.start)}.</p>` : dvReadings(day).map(([b, c]) => `<a class="btn ghost" href="#/bible/${b}/${c}">${esc(bbBook(b).name)} ${c}</a>`).join('')}</div>
|
||||
${idx >= 0 ? `<div class="bar"><button class="btn${done.has(day) ? ' ghost' : ''}" id="dvck">${icon('check')}${done.has(day) ? 'Done today' : 'Mark today as read'}</button><button class="btn ghost" id="dvstop">Stop plan</button></div>` : ''}</div>`
|
||||
: `<div class="card" style="max-width:none"><p class="sub" style="margin-top:0">Read the whole Bible in a year, a few chapters a day, and check in each day.</p><div class="bar"><button class="btn" id="dvstart">Start today</button></div></div>`}
|
||||
<h2>Family prayer list</h2><form class="bar" id="dvpf" style="flex-wrap:nowrap"><input type="text" id="dvpt" placeholder="Add a prayer request" maxlength="500" autocomplete="off" style="flex:1"><button class="btn">Add</button></form>
|
||||
<div class="list">${open.map(prayRow).join('') || '<div class="row"><span class="grow"><small>Nothing yet. Add what the family is praying about.</small></span></div>'}</div>
|
||||
${answered.length ? `<div class="bar"><button class="btn ghost" id="dvans">${showAnswered ? 'Hide' : 'Show'} answered (${answered.length})</button></div>${showAnswered ? `<div class="list">${answered.map(prayRow).join('')}</div>` : ''}` : ''}</div>`;
|
||||
const save = async (fn) => { try { await fn(); draw(); } catch (e) { toast(e.message); } };
|
||||
const st = $('#dvstart'); if (st) st.onclick = () => save(async () => { plan = (await bbApi('/plan', bbOpt('POST', { start: dvDay() }))).plan; bbSt.plan = plan; });
|
||||
const sp = $('#dvstop'); if (sp) sp.onclick = () => confirmSheet('Stop the reading plan?', 'Your check-ins are cleared.', 'Stop plan', () => save(async () => { await bbApi('/plan', bbOpt('POST', { stop: true })); plan = bbSt.plan = null; }));
|
||||
const ck = $('#dvck'); if (ck) ck.onclick = () => save(async () => { plan = (await bbApi('/plan/check', bbOpt('POST', { day, on: !done.has(day) }))).plan; bbSt.plan = plan; });
|
||||
$('#dvpf').onsubmit = e => { e.preventDefault(); const t = $('#dvpt').value.trim(); if (t) save(async () => { prayers = await bbApi('/prayers', bbOpt('POST', { text: t })); }); };
|
||||
v.querySelectorAll('[data-pa]').forEach(b => b.onclick = () => save(async () => { prayers = await bbApi('/prayers/answer', bbOpt('POST', { id: b.dataset.pa, on: !!b.dataset.on })); }));
|
||||
v.querySelectorAll('[data-pd]').forEach(b => b.onclick = () => save(async () => { prayers = await bbApi('/prayers/delete', bbOpt('POST', { id: b.dataset.pd })); }));
|
||||
const an = $('#dvans'); if (an) an.onclick = () => { showAnswered = !showAnswered; draw(); };
|
||||
};
|
||||
draw();
|
||||
}
|
||||
Reference in new issue
Block a user