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,64 @@
|
||||
// Export → Schedule: the study's sessions (one per chunk) as a calendar file, starting on a date you pick.
|
||||
import { useState } from 'react';
|
||||
import { formatChunkReference } from '../utils/bibleUtils.jsx';
|
||||
import { buildIcs, downloadIcs, sessionDates } from '../utils/icsFile.js';
|
||||
|
||||
const field = 'rounded-lg border border-[var(--r-line)] bg-[var(--r-bg)] px-2.5 py-1.5 text-sm text-[var(--r-text)] focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]';
|
||||
const isoDay = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
|
||||
// The study's sessions in order: { title, description } for each chunk.
|
||||
export function sessionsOf(project) {
|
||||
const list = [];
|
||||
(project?.chapters ?? []).forEach((chapter, chapterIndex) => {
|
||||
for (const chunk of chapter.chunks ?? []) {
|
||||
const ref = formatChunkReference(project, chapterIndex, chunk, '–');
|
||||
const n = list.length + 1;
|
||||
const label = `Session ${(chunk.episodeNumber ?? '').toString().trim() || n}`;
|
||||
const name = (project.series ?? '').trim() || project.title;
|
||||
list.push({ title: `${name} — ${label}: ${(chunk.episodeTitle ?? '').trim() || ref}`, description: `${project.title}\n${ref}` });
|
||||
}
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
export default function SessionSchedule({ project }) {
|
||||
const sessions = sessionsOf(project);
|
||||
const [start, setStart] = useState(() => isoDay(new Date()));
|
||||
const [every, setEvery] = useState('7');
|
||||
const [time, setTime] = useState('');
|
||||
const [minutes, setMinutes] = useState('60');
|
||||
if (sessions.length === 0) return null;
|
||||
|
||||
const download = () => {
|
||||
const [y, m, d] = start.split('-').map(Number);
|
||||
const dates = sessionDates(new Date(y, m - 1, d), sessions.length, Number(every));
|
||||
const events = sessions.map((s, i) => ({ ...s, date: dates[i], time: time || undefined, minutes: Number(minutes) || 60 }));
|
||||
downloadIcs(buildIcs(events, { name: (project.series ?? '').trim() || project.title }), `${(project.title || 'study').replace(/[^\w]+/g, '-').toLowerCase()}-schedule.ics`);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-5" aria-label="Schedule">
|
||||
<h2 className="text-sm font-medium">Schedule</h2>
|
||||
<p className="mb-3 mt-1 text-xs text-[var(--r-sub)]">Put the {sessions.length} session{sessions.length === 1 ? '' : 's'} on your calendar: one per chunk, in order, using each chunk’s session number and title.</p>
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
<label className="text-xs text-[var(--r-sub)]">First session
|
||||
<input type="date" value={start} onChange={(e) => setStart(e.target.value)} className={`${field} mt-1 block`} />
|
||||
</label>
|
||||
<label className="text-xs text-[var(--r-sub)]">Then every
|
||||
<select value={every} onChange={(e) => setEvery(e.target.value)} className={`${field} mt-1 block`}>
|
||||
<option value="1">day</option><option value="7">week</option><option value="14">2 weeks</option><option value="30">30 days</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="text-xs text-[var(--r-sub)]">At (optional)
|
||||
<input type="time" value={time} onChange={(e) => setTime(e.target.value)} className={`${field} mt-1 block`} />
|
||||
</label>
|
||||
{time && (
|
||||
<label className="text-xs text-[var(--r-sub)]">Minutes
|
||||
<input type="number" min="5" max="480" value={minutes} onChange={(e) => setMinutes(e.target.value)} className={`${field} mt-1 block w-20`} />
|
||||
</label>
|
||||
)}
|
||||
<button type="button" onClick={download} disabled={!start} className="rounded-xl border border-[var(--r-line)] px-4 py-2 text-sm hover:bg-black/5 disabled:opacity-50">Download calendar file (.ics)</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user