Invite-only sign-up, present mode, series, handouts, templates, dictation, progress, sharing
Build and Push Docker Image / build-and-push (push) Successful in 6m47s
Build and Push Docker Image / build-and-push (push) Successful in 6m47s
- Admin: sign-up Open / Invite only / Closed with single-use invite links - Present mode for scripts (auto-scroll) and outlines (step through, pace vs planned minutes) - Series: group studies on the dashboard, filter by series, calendar file for a study's sessions - Lesson handout: printable page per session (passage, outline, questions, writing lines) - Save a chunk's notes as your own study template (synced) - Voice dictation for notes and scripts (browser speech recognition) - Reading progress: map of the 66 books and a calendar of days read, synced and merged across devices - Export highlights and bookmarks as Markdown or Word, grouped by named colour - Verse of the day push to the ntfy topic (needs IQBIBLE_API_KEY) - Sharing: co-teacher editing via single-use invite links; read-only links can expire and take a passcode Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
88bc59bac0
commit
54cb1c217d
73 files changed
+2739
-134
No files matched your search
@@ -0,0 +1,52 @@
|
||||
// A calendar file (.ics) any calendar app can import: one event per entry. Dates are plain calendar days
|
||||
// (all-day events) unless a time is given, in which case the event is at that clock time wherever the file is opened.
|
||||
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
const day = (d) => `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`;
|
||||
const stamp = (d) => `${day(d)}T${pad(d.getHours())}${pad(d.getMinutes())}00`;
|
||||
const utcStamp = (d) => `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}T${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}Z`;
|
||||
|
||||
// Escapes text for an ICS field, and folds long lines the way the format requires (75 characters).
|
||||
export const icsText = (s) => String(s ?? '').replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\r?\n/g, '\\n');
|
||||
export function fold(line) {
|
||||
const out = [];
|
||||
let rest = line;
|
||||
while (rest.length > 75) { out.push(rest.slice(0, 75)); rest = ` ${rest.slice(75)}`; }
|
||||
out.push(rest);
|
||||
return out.join('\r\n');
|
||||
}
|
||||
|
||||
// events: [{ date: Date, title, description?, time?: 'HH:MM', minutes?: number }] -> the file's text
|
||||
export function buildIcs(events, { name = 'Bible study', now = new Date() } = {}) {
|
||||
const lines = ['BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//Bible Study Project//EN', 'CALSCALE:GREGORIAN', `X-WR-CALNAME:${icsText(name)}`];
|
||||
events.forEach((e, i) => {
|
||||
lines.push('BEGIN:VEVENT', `UID:${utcStamp(now)}-${i}@bible-study-project`, `DTSTAMP:${utcStamp(now)}`);
|
||||
const m = /^(\d{1,2}):(\d{2})$/.exec(e.time ?? '');
|
||||
if (m) {
|
||||
const start = new Date(e.date.getFullYear(), e.date.getMonth(), e.date.getDate(), Number(m[1]), Number(m[2]));
|
||||
const end = new Date(start.getTime() + (e.minutes ?? 60) * 60_000);
|
||||
lines.push(`DTSTART:${stamp(start)}`, `DTEND:${stamp(end)}`);
|
||||
} else {
|
||||
const next = new Date(e.date.getFullYear(), e.date.getMonth(), e.date.getDate() + 1);
|
||||
lines.push(`DTSTART;VALUE=DATE:${day(e.date)}`, `DTEND;VALUE=DATE:${day(next)}`);
|
||||
}
|
||||
lines.push(`SUMMARY:${icsText(e.title)}`);
|
||||
if (e.description) lines.push(`DESCRIPTION:${icsText(e.description)}`);
|
||||
lines.push('END:VEVENT');
|
||||
});
|
||||
lines.push('END:VCALENDAR');
|
||||
return `${lines.map(fold).join('\r\n')}\r\n`;
|
||||
}
|
||||
|
||||
// The date of each of `count` sessions: the first on `start`, then every `everyDays` days.
|
||||
export function sessionDates(start, count, everyDays = 7) {
|
||||
return Array.from({ length: count }, (_, i) => new Date(start.getFullYear(), start.getMonth(), start.getDate() + i * everyDays));
|
||||
}
|
||||
|
||||
export function downloadIcs(text, fileName) {
|
||||
const url = URL.createObjectURL(new Blob([text], { type: 'text/calendar;charset=utf-8' }));
|
||||
const a = document.createElement('a');
|
||||
a.href = url; a.download = fileName;
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}
|
||||
Reference in new issue
Block a user