Invite-only sign-up, present mode, series, handouts, templates, dictation, progress, sharing
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:
nmemmertandClaude Sonnet 5.5 committed 2026-10-06 10:26:43 -04:00
1 parent 88bc59bac0
commit 54cb1c217d
73 files changed
+2739 -134

No files matched your search

+34 -10
View File
@@ -94,8 +94,8 @@ export async function getShareStatus(id) {
}
/** Enables sharing (or returns the existing token if already enabled). Returns { shareToken }. */
export async function enableSharing(id) {
return request('POST', `/projects/${id}/share`);
export async function enableSharing(id, options = {}) {
return request('POST', `/projects/${id}/share`, options);
}
/** Revokes a project's share link. */
@@ -103,24 +103,32 @@ export async function disableSharing(id) {
return request('DELETE', `/projects/${id}/share`);
}
// ---- co-teachers: other accounts that may edit a study ----
export async function getCollaborators(id) { return request('GET', `/projects/${id}/collaborators`); }
/** A single-use link (valid 7 days) that lets whoever opens it, signed in, edit this study. -> { token, expiresAt } */
export async function createEditInvite(id) { return request('POST', `/projects/${id}/collaborators/invite`); }
export async function removeCollaborator(id, userId) { return request('DELETE', `/projects/${id}/collaborators/${userId}`); }
/** Uses an invite link: -> { projectId, title } */
export async function joinStudy(token) { return request('POST', `/join/${encodeURIComponent(token)}`); }
/** The project's read-only outline link: { shareToken } (null when off). A separate link from the study share. */
export async function getOutlineShareStatus(id) {
return request('GET', `/projects/${id}/outline-share`);
}
export async function enableOutlineSharing(id) {
return request('POST', `/projects/${id}/outline-share`);
export async function enableOutlineSharing(id, options = {}) {
return request('POST', `/projects/${id}/outline-share`, options);
}
export async function disableOutlineSharing(id) {
return request('DELETE', `/projects/${id}/outline-share`);
}
/** Public lookup — no session. Returns { title, translation, outlines: [{ scope, label, tree }] }. */
export async function getSharedOutline(token) {
return request('GET', `/share-outline/${token}`);
export async function getSharedOutline(token, passcode) {
return request('GET', `/share-outline/${token}`, undefined, { headers: passcode ? { 'X-Share-Passcode': passcode } : {} });
}
/** Public lookup — no session required. Returns the full project for a valid share token. */
export async function getSharedProject(token) {
return request('GET', `/share/${token}`);
export async function getSharedProject(token, passcode) {
return request('GET', `/share/${token}`, undefined, { headers: passcode ? { 'X-Share-Passcode': passcode } : {} });
}
/**
@@ -152,8 +160,13 @@ export async function getCurrentUser() {
return result;
}
export async function registerUser(email, password) {
return request('POST', '/auth/register', { email, password });
export async function registerUser(email, password, invite) {
return request('POST', '/auth/register', { email, password, invite });
}
/** Who may sign up here: { mode: 'open' | 'invite' | 'closed' }. */
export async function getSignupMode() {
return request('GET', '/auth/signup-mode');
}
export async function loginUser(email, password) {
@@ -209,6 +222,11 @@ export async function saveRemoteMarks(marks) {
return request('PUT', '/reader/marks', marks);
}
/** What the person has read: { chapters, days }. */
export async function getRemoteHistory() { return request('GET', '/reader/history'); }
/** Merges this device's reading history into the account's; returns the combined history. */
export async function saveRemoteHistory(history) { return request('PUT', '/reader/history', history); }
/** Sends a test notification to the given ntfy topic/server (or the saved ones). */
export async function sendReminderTest(settings) {
return request('POST', '/auth/reminder-test', settings);
@@ -255,6 +273,12 @@ export async function adminResetPassword(id) {
return request('POST', `/admin/users/${id}/reset-password`);
}
/** The administrator's sign-up setting and invite codes. */
export async function adminGetSignup() { return request('GET', '/admin/signup'); }
export async function adminSetSignupMode(mode) { return request('PUT', '/admin/signup', { mode }); }
export async function adminCreateInvite({ note, days } = {}) { return request('POST', '/admin/invites', { note, days }); }
export async function adminDeleteInvite(code) { return request('DELETE', `/admin/invites/${encodeURIComponent(code)}`); }
export async function adminListProjects() {
return request('GET', '/admin/projects');
}