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
+26
-2
@@ -5,6 +5,7 @@ import { AppContext, useApp } from './context/AppContext.js';
|
||||
import { ToastProvider, useToast } from './components/Toasts.jsx';
|
||||
import CommandPalette from './components/CommandPalette.jsx';
|
||||
import ShortcutsHelp from './components/ShortcutsHelp.jsx';
|
||||
import SharedPasscodeForm from './components/SharedPasscodeForm.jsx';
|
||||
import SharedOutlineView from './components/SharedOutlineView.jsx';
|
||||
import Tooltips from './components/Tooltips.jsx';
|
||||
import { useAudio } from './hooks/useAudio.js';
|
||||
@@ -12,11 +13,12 @@ import { useAuth } from './hooks/useAuth.js';
|
||||
import { useBibleReader } from './hooks/useBibleReader.js';
|
||||
import { useProjectLibrary } from './hooks/useProjectLibrary.js';
|
||||
import { useHistoryNav } from './hooks/useHistoryNav.js';
|
||||
import { useReadingHistory } from './hooks/useReadingHistory.js';
|
||||
import { useReadingPlans } from './hooks/useReadingPlans.js';
|
||||
import { useSharedView } from './hooks/useSharedView.js';
|
||||
import HomePage from './pages/HomePage.jsx';
|
||||
import { themeVars } from './components/reader/readerThemes.js';
|
||||
import { loadRemoteProject, saveRemoteProject } from './syncService.js';
|
||||
import { joinStudy, loadRemoteProject, saveRemoteProject } from './syncService.js';
|
||||
import { getSyncBase, setSyncBase } from './utils/syncBase.js';
|
||||
import {
|
||||
bookOptions, buildExportHtml, loadProjectIndex, makeId, parseBibleChapter, switchStorageUser,
|
||||
@@ -73,6 +75,7 @@ const AppInner = () => {
|
||||
const [readerPlan, setReaderPlan] = useState(null);
|
||||
const readerPlanProgress = readerPlan ? readingPlans.planProgress.find((p) => p.id === readerPlan.progressId) ?? null : null;
|
||||
const reader = useBibleReader(currentPage, readerPlanProgress?.translation ?? null);
|
||||
const readingHistory = useReadingHistory({ active: currentPage === 'reader', book: reader.readerBookAbbrev, chapter: reader.readerChapter });
|
||||
const audio = useAudio(reader.readerBookAbbrev, reader.readerChapter, reader.readerTranslation);
|
||||
useEffect(() => { if (currentPage !== 'reader') setReaderPlan(null); }, [currentPage]);
|
||||
// A new page starts at the top. Without this the scroll position of the previous page carries over, and on
|
||||
@@ -99,11 +102,14 @@ const AppInner = () => {
|
||||
reader.pullServerMarks();
|
||||
// Theme, default translation, layout, commentary and reading position.
|
||||
reader.pullServerPrefs();
|
||||
// What you've read, merged with this device's.
|
||||
readingHistory.pullReadingHistory();
|
||||
},
|
||||
onSignOut: () => {
|
||||
reader.stopMarksSync();
|
||||
reader.stopPrefsSync();
|
||||
reader.stopInkSync();
|
||||
readingHistory.stopReadingHistory();
|
||||
readingPlans.stopReadingPlansSync();
|
||||
setProjectIndex(switchStorageUser(null));
|
||||
goStudies();
|
||||
@@ -117,6 +123,22 @@ const AppInner = () => {
|
||||
setCurrentPage,
|
||||
});
|
||||
|
||||
// Opening an invite link (?join=TOKEN) while signed in adds that study to your list, ready to edit.
|
||||
useEffect(() => {
|
||||
if (!authUser) return;
|
||||
const token = new URLSearchParams(window.location.search).get('join');
|
||||
if (!token) return;
|
||||
window.history.replaceState({}, '', window.location.pathname);
|
||||
joinStudy(token).then(async (joined) => {
|
||||
if (!joined.ok) { toast(joined.error ?? 'Could not use that invite link.', { tone: 'error' }); return; }
|
||||
const full = await loadRemoteProject(joined.data.projectId);
|
||||
if (full.ok) { saveProjectToStorage(full.data); setSyncBase(full.data.id, full.data.lastEdited); setProjectIndex(loadProjectIndex()); }
|
||||
library.refreshLibrary();
|
||||
toast(`You can now edit “${joined.data.title}”. It's in My Studies.`);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [authUser]);
|
||||
|
||||
// ⌘K search/go-to, the "?" shortcut list, and requests other pages make of the Reader.
|
||||
const [paletteOpen, setPaletteOpen] = useState(false);
|
||||
const [helpOpen, setHelpOpen] = useState(false);
|
||||
@@ -479,6 +501,7 @@ const AppInner = () => {
|
||||
...audio,
|
||||
// Reader
|
||||
...reader,
|
||||
readingHistory: readingHistory.readingHistory,
|
||||
readerPlan, setReaderPlan,
|
||||
readerPlanActive: !!readerPlanProgress,
|
||||
openPlanReading,
|
||||
@@ -523,13 +546,14 @@ function PageLoading() {
|
||||
|
||||
function AppRouter() {
|
||||
const { currentPage, authUser, authServerDown } = useApp();
|
||||
const { token: sharedViewToken, project: sharedProject, error: sharedError } = useSharedView();
|
||||
const { token: sharedViewToken, project: sharedProject, error: sharedError, needsPasscode, passcodeError, checking, submitPasscode } = useSharedView();
|
||||
const sharedOutlineToken = new URLSearchParams(window.location.search).get('outline');
|
||||
|
||||
// A read-only outline link: no sign-in, nothing editable
|
||||
if (sharedOutlineToken) return <SharedOutlineView token={sharedOutlineToken} />;
|
||||
|
||||
if (sharedViewToken) {
|
||||
if (needsPasscode) return <SharedPasscodeForm dark error={passcodeError} busy={checking} onSubmit={submitPasscode} />;
|
||||
if (sharedError) {
|
||||
return (
|
||||
<div className="flex min-h-dvh items-center justify-center bg-slate-900 px-4">
|
||||
|
||||
Reference in new issue
Block a user