Harden sync and security; add IQ Bible, templates, password reset, backups
Build and Push Docker Image / build-and-push (push) Successful in 7m14s
Build and Push Docker Image / build-and-push (push) Successful in 7m14s
Sync and data safety - Autosave flushes on leaving a study or the tab; storage-full errors are reported - Unsynced studies, deletes and ink are queued and retried when back online - Deletes propagate across devices (with a "Keep it" undo) and no longer resurrect - Server assigns study versions (no client-clock conflicts); "Combine both" merges a conflicted study chunk by chunk - Reading-plan progress made offline is merged, not overwritten; plans, ink and marks are cleared from the device on sign-out Security - Block SSRF through the ntfy reminder URL; failure-only rate limiting per IP and account; constant-time login; TOTP replay protection; input validation; SESSION_SECRET required in production; admin user delete fixed - Forgot password: one-time code sent to the account's ntfy topic Features - IQ Bible proxy (route allow-list, text-only caching per its terms): more commentaries, lexicons, study aids, daily verse and devotional, plan calendar export, narrations, podcast background - Study templates, reading-plan catch-up, account export, daily database snapshots Tests and tooling - ESLint config; App tests split into src/test; tests for sync queues, library hook, merge, templates, IQ Bible UI and reset form Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6bb392933a
commit
f274bc9c8a
67 files changed
+4734
-1569
No files matched your search
@@ -9,12 +9,18 @@ import { READER_TRANSLATIONS, fetchChapter } from '../../utils/bibleText.js';
|
||||
import { diffVerses, differenceLabel } from '../../utils/textDiff.js';
|
||||
import { DiffText } from './DiffText.jsx';
|
||||
import { HIGHLIGHT_COLORS, colorTitle } from '../../utils/highlightColors.js';
|
||||
import { useIqAvailable } from '../../hooks/useIq.js';
|
||||
import IqLexicon from '../iq/IqLexicon.jsx';
|
||||
import IqStudyAids from '../iq/IqStudyAids.jsx';
|
||||
|
||||
const TABS = [['commentary', 'Commentary'], ['compare', 'Compare'], ['original', 'Original'], ['refs', 'Cross-refs'], ['people', 'People & places']];
|
||||
const BASE_TABS = [['commentary', 'Commentary'], ['compare', 'Compare'], ['original', 'Original'], ['refs', 'Cross-refs'], ['people', 'People & places']];
|
||||
|
||||
function OriginalTab({ words }) {
|
||||
const iq = useIqAvailable();
|
||||
const [lookup, setLookup] = useState(null); // the Strong's number open in the lexicon panel
|
||||
if (!words) return <p className="text-sm text-[var(--r-sub)]">No original-language data for this verse.</p>;
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{words.map((w, i) => {
|
||||
const isHebrew = w.s?.startsWith('H');
|
||||
@@ -31,10 +37,16 @@ function OriginalTab({ words }) {
|
||||
className="mt-1.5 rounded-lg bg-[var(--r-bar)] px-2 py-0.5 text-[11px] hover:bg-black/5"
|
||||
title={`Pronounce in ${isHebrew ? 'Hebrew' : 'Greek'}`}>Speak</button>
|
||||
)}
|
||||
{iq && w.s && (
|
||||
<button type="button" onClick={() => setLookup((cur) => (cur === w.s ? null : w.s))} aria-pressed={lookup === w.s}
|
||||
className="mt-1.5 ml-1 rounded-lg bg-[var(--r-bar)] px-2 py-0.5 text-[11px] hover:bg-black/5" title="Look this word up in the lexicons">Lexicon</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{lookup && <IqLexicon strongs={lookup} onClose={() => setLookup(null)} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -144,6 +156,8 @@ export default function VerseSheet({
|
||||
bookmarkColor, onToggleBookmark, onCycleColor, onJump, onClose, request,
|
||||
verseHighlights = [], colorLabels = {}, onHighlightVerse, onClearVerseHighlights, panelRef, resizeHandle,
|
||||
}) {
|
||||
const iqOn = useIqAvailable();
|
||||
const TABS = iqOn ? [...BASE_TABS, ['aids', 'Study aids']] : BASE_TABS;
|
||||
const [tab, setTab] = useState('commentary');
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [preview, setPreview] = useState(null); // the cross-reference being read in the panel
|
||||
@@ -230,6 +244,7 @@ export default function VerseSheet({
|
||||
preview={preview && <RefPreview refItem={preview} translation={translation} onOpen={onJump} onClose={() => setPreview(null)} />} />
|
||||
)}
|
||||
{tab === 'people' && <PeoplePlacesTab book={book} chapter={chapter} verse={verse.number} />}
|
||||
{tab === 'aids' && iqOn && <IqStudyAids book={book} chapter={chapter} verse={verse.number} />}
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user