Version history, notes on highlights and bookmarks, "you've studied this", prayer journal
Build and Push Docker Image / build-and-push (push) Successful in 5m13s

- Earlier versions of a study: the server keeps compressed copies (about one per ten minutes of
  editing, the latest 30); Export shows what differs and puts one back (the replaced copy is kept)
- Notes on highlights and bookmarks: synced, merged without loss, in exports and backups
- Reader verse panel lists your studies and chunks that cover the verse, and opens them
- Prayer journal: requests, who for, linked verses, days prayed, answers; synced, in backups
  and the account export; daily prayer reminder through ntfy
- TODO.md lists the ideas not built yet

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-06 10:45:32 -04:00
1 parent 54cb1c217d
commit 207bfcb919
45 files changed
+1669 -75

No files matched your search

+25 -17
View File
@@ -3,6 +3,7 @@
import { useEffect, useState } from 'react';
import Sheet from './Sheet.jsx';
import { CloseIcon } from './icons.jsx';
import MarkNote from './MarkNote.jsx';
import { buildMarksDocxBlob, buildMarksModel, downloadMarksFile, marksToMarkdown } from '../../utils/marksExport.js';
import { COLOR_NAMES, COLOR_SUGGESTIONS, HIGHLIGHT_COLORS, colorTitle } from '../../utils/highlightColors.js';
@@ -10,6 +11,7 @@ const tabBtn = (active) => `rounded-full border px-3 py-1 text-xs font-medium ${
export default function MarksSheet({
open, onClose, initialTab = 'highlights', bookmarks, highlights, colorLabels, onSetLabel, onJump, onRemoveBookmark, onRemoveHighlight,
bookmarkNotes = {}, onSetHighlightNote, onSetBookmarkNote,
}) {
const [tab, setTab] = useState(initialTab);
// Opening from the Highlights button shows highlights; from the Bookmarks button, bookmarks
@@ -24,7 +26,7 @@ export default function MarksSheet({
const [exportError, setExportError] = useState('');
const exportAs = async (kind) => {
setExportError('');
const model = buildMarksModel({ highlights, bookmarks, colorLabels });
const model = buildMarksModel({ highlights, bookmarks, colorLabels, bookmarkNotes });
try {
if (kind === 'md') downloadMarksFile(new Blob([marksToMarkdown(model)], { type: 'text/markdown;charset=utf-8' }), 'highlights-and-bookmarks.md');
else downloadMarksFile(await buildMarksDocxBlob(model), 'highlights-and-bookmarks.docx');
@@ -73,15 +75,18 @@ export default function MarksSheet({
) : shown.length === 0 ? (
<p className="text-sm text-[var(--r-sub)]">Nothing highlighted in that colour yet.</p>
) : shown.map((h) => (
<div key={h.id} className="flex items-start gap-2 rounded-lg px-1 py-1.5 hover:bg-black/5">
<span className="mt-1 h-3 w-3 shrink-0 rounded-full border border-black/10" style={{ backgroundColor: h.color }} title={colorTitle(h.color, colorLabels)} />
<button type="button" onClick={() => onJump(h.book, h.chapter, h.verse)} className="min-w-0 flex-1 text-left text-sm">
<span className="font-medium">{h.bookName} {h.chapter}:{h.verse}</span>
{colorLabels[h.color] && <span className="ml-1.5 text-[11px] text-[var(--r-sub)]">{colorLabels[h.color]}</span>}
{h.text && <span className="mt-0.5 block font-serif text-[var(--r-sub)]">“{h.text}”</span>}
</button>
<button type="button" onClick={() => onRemoveHighlight(h)} aria-label={`Remove highlight on ${h.bookName} ${h.chapter}:${h.verse}`}
className="rounded p-1 text-[var(--r-sub)] hover:text-[var(--r-danger,#b42318)]"><CloseIcon /></button>
<div key={h.id} className="rounded-lg px-1 py-1.5 hover:bg-black/5">
<div className="flex items-start gap-2">
<span className="mt-1 h-3 w-3 shrink-0 rounded-full border border-black/10" style={{ backgroundColor: h.color }} title={colorTitle(h.color, colorLabels)} />
<button type="button" onClick={() => onJump(h.book, h.chapter, h.verse)} className="min-w-0 flex-1 text-left text-sm">
<span className="font-medium">{h.bookName} {h.chapter}:{h.verse}</span>
{colorLabels[h.color] && <span className="ml-1.5 text-[11px] text-[var(--r-sub)]">{colorLabels[h.color]}</span>}
{h.text && <span className="mt-0.5 block font-serif text-[var(--r-sub)]">“{h.text}”</span>}
</button>
<button type="button" onClick={() => onRemoveHighlight(h)} aria-label={`Remove highlight on ${h.bookName} ${h.chapter}:${h.verse}`}
className="rounded p-1 text-[var(--r-sub)] hover:text-[var(--r-danger,#b42318)]"><CloseIcon /></button>
</div>
{onSetHighlightNote && <MarkNote className="ml-5 mt-1" note={h.note} label={`note on ${h.bookName} ${h.chapter}:${h.verse}`} onSave={(text) => onSetHighlightNote(h.id, text)} />}
</div>
))}
</>
@@ -91,13 +96,16 @@ export default function MarksSheet({
bookmarks.length === 0 ? (
<p className="text-sm text-[var(--r-sub)]">No bookmarks yet. Tap a verse, then the bookmark icon, to save it here.</p>
) : bookmarks.map((e) => (
<div key={e.key} className="flex items-center gap-2 rounded-lg px-1 py-1.5 hover:bg-black/5">
<span className="h-3 w-3 shrink-0 rounded-full" style={{ backgroundColor: e.color }} />
<button type="button" onClick={() => onJump(e.bookAbbrev, e.chapter, e.verse)} className="flex-1 truncate text-left text-sm">
{e.bookName} {e.chapter}:{e.verse}
</button>
<button type="button" onClick={() => onRemoveBookmark(e)} aria-label={`Remove bookmark on ${e.bookName} ${e.chapter}:${e.verse}`}
className="rounded p-1 text-[var(--r-sub)] hover:text-[var(--r-danger,#b42318)]"><CloseIcon /></button>
<div key={e.key} className="rounded-lg px-1 py-1.5 hover:bg-black/5">
<div className="flex items-center gap-2">
<span className="h-3 w-3 shrink-0 rounded-full" style={{ backgroundColor: e.color }} />
<button type="button" onClick={() => onJump(e.bookAbbrev, e.chapter, e.verse)} className="flex-1 truncate text-left text-sm">
{e.bookName} {e.chapter}:{e.verse}
</button>
<button type="button" onClick={() => onRemoveBookmark(e)} aria-label={`Remove bookmark on ${e.bookName} ${e.chapter}:${e.verse}`}
className="rounded p-1 text-[var(--r-sub)] hover:text-[var(--r-danger,#b42318)]"><CloseIcon /></button>
</div>
{onSetBookmarkNote && <MarkNote className="ml-5 mt-1" note={bookmarkNotes[e.key]} label={`note on ${e.bookName} ${e.chapter}:${e.verse}`} onSave={(text) => onSetBookmarkNote(e.key, text)} />}
</div>
))
)}