// "My Reading Journal" export — gathers bookmarks, text highlights, and // completed reading plans into one document. These first two are local-only // (see useBibleReader.js: 'reader-bookmarks' and 'readerTextHighlights' in // localStorage, never synced to the server), so this only ever reflects the // current device. Offered as a .docx (matching the study-project exporter) // and a print-to-PDF page (matching printProjectPdf in projectExport.js). import { bookOptions, parseBibleChapter } from './bibleUtils.jsx'; import { getPlan } from './readingPlans.js'; const BOOK_NAME_BY_ABBREV = Object.fromEntries(bookOptions.map((b) => [b.abbrev, b.name])); const BOOK_INDEX_BY_ABBREV = Object.fromEntries(bookOptions.map((b, i) => [b.abbrev, i])); function downloadBlob(blob, filename) { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); } function loadReadingProgress() { try { const raw = JSON.parse(localStorage.getItem('readingPlanProgress') || 'null'); if (Array.isArray(raw)) return { progress: raw, customPlans: [] }; return { progress: raw?.progress ?? [], customPlans: raw?.customPlans ?? [] }; } catch { return { progress: [], customPlans: [] }; } } // One fetch per distinct chapter, however many bookmarks/highlights reference it. async function fetchVersesByChapter(refs) { const cache = new Map(); await Promise.all(refs.map(async ({ book, chapter }) => { const key = `${book}_${chapter}`; if (cache.has(key)) return; cache.set(key, null); // claim the slot so concurrent dupes don't double-fetch try { const res = await fetch(`https://bible.helloao.org/api/BSB/${book}/${chapter}.json`); if (!res.ok) return; const data = await res.json(); cache.set(key, parseBibleChapter(data)); } catch { // leave as null — caller falls back to showing just the reference } })); return cache; } /** * Reads bookmarks/highlights/reading-plan history from local storage and * fetches the verse text they need, ready to hand to buildJournalDocxBlob or * buildJournalHtml. */ export async function gatherJournalData() { const bookmarksRaw = JSON.parse(localStorage.getItem('reader-bookmarks') || '{}'); const highlightsRaw = JSON.parse(localStorage.getItem('readerTextHighlights') || '[]'); const bookmarkEntries = Object.entries(bookmarksRaw).map(([key, color]) => { const [book, chapterStr, verseStr] = key.split('-'); return { book, chapter: Number(chapterStr), verse: Number(verseStr), color }; }); const chapterRefs = [ ...bookmarkEntries.map((b) => ({ book: b.book, chapter: b.chapter })), ...highlightsRaw.map((h) => ({ book: h.book, chapter: h.chapter })), ]; const versesByChapter = await fetchVersesByChapter(chapterRefs); const sortByPassage = (a, b) => ( (BOOK_INDEX_BY_ABBREV[a.book] ?? 999) - (BOOK_INDEX_BY_ABBREV[b.book] ?? 999) || a.chapter - b.chapter || (a.verse ?? 0) - (b.verse ?? 0) ); const bookmarks = bookmarkEntries .map((b) => { const verse = versesByChapter.get(`${b.book}_${b.chapter}`)?.find((v) => v.number === b.verse); return { ...b, bookName: BOOK_NAME_BY_ABBREV[b.book] ?? b.book, text: verse?.text ?? null, }; }) .sort(sortByPassage); const highlights = highlightsRaw .map((h) => { const verse = versesByChapter.get(`${h.book}_${h.chapter}`)?.find((v) => v.number === h.verse); const fullText = verse?.text ?? null; const excerpt = fullText ? fullText.slice(h.startOffset, h.endOffset) : null; return { ...h, bookName: BOOK_NAME_BY_ABBREV[h.book] ?? h.book, fullText, excerpt, }; }) .sort(sortByPassage); const { progress, customPlans } = loadReadingProgress(); const completedPlans = progress .filter((p) => p.completedAt) .map((p) => ({ ...p, plan: getPlan(p.planId) })) .filter((p) => p.plan) .sort((a, b) => b.completedAt - a.completedAt); return { bookmarks, highlights, completedPlans, customPlansCount: customPlans.length }; } export async function exportJournalDocx() { const data = await gatherJournalData(); // docx is large, so it's only downloaded when someone actually exports. const { buildJournalDocxBlob } = await import('./docxExport.js'); const blob = await buildJournalDocxBlob(data); downloadBlob(blob, `my-reading-journal-${new Date().toISOString().slice(0, 10)}.docx`); } function escapeHtml(text) { return String(text ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[c])); } function buildJournalHtml({ bookmarks, highlights, completedPlans }) { const dateStr = new Date().toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' }); const section = (title, rows) => (rows.length === 0 ? '' : `

${title}

${rows.join('')}
`); const planRows = completedPlans.map(({ plan, completedAt }) => `
${plan.icon ?? '✓'} ${escapeHtml(plan.title)} Completed ${new Date(completedAt).toLocaleDateString()} · ${plan.days} days
`); const bookmarkRows = bookmarks.map((b) => `
${escapeHtml(b.bookName)} ${b.chapter}:${b.verse} ${b.text ? `

${escapeHtml(b.text)}

` : ''}
`); const highlightRows = highlights.map((h) => `
${escapeHtml(h.bookName)} ${h.chapter}:${h.verse}

${ h.fullText ? escapeHtml(h.fullText.slice(0, h.startOffset)) + `${escapeHtml(h.excerpt ?? '')}` + escapeHtml(h.fullText.slice(h.endOffset)) : '(verse text unavailable)' }

`); return ` My Reading Journal

My Reading Journal

Generated ${dateStr}

${section('Reading Plans Completed', planRows)} ${section('Bookmarked Verses', bookmarkRows)} ${section('Highlighted Passages', highlightRows)} ${planRows.length + bookmarkRows.length + highlightRows.length === 0 ? '

Nothing saved yet — bookmark a verse, highlight some text, or finish a reading plan to build your journal.

' : ''} `; } export async function printJournalPdf() { const data = await gatherJournalData(); const html = buildJournalHtml(data); const printWindow = window.open('', '_blank'); if (!printWindow) { alert('Please allow pop-ups for this site to print or save as PDF.'); return; } printWindow.document.write(html); printWindow.document.close(); printWindow.onload = () => { printWindow.focus(); printWindow.print(); }; }