import { useEffect, useRef, useState } from 'react'; import { useApp } from '../context/AppContext.js'; import { bookOptions, CHAPTER_COUNTS, formatChunkReference, formatRelativeDate, loadProjectIndex, } from '../utils/bibleUtils.jsx'; import { loadProjectById } from '../utils/projectStorage.js'; export default function HomePage() { const { authStatus, autoRestoredCount, staleLocalProjects, projectIndex, readingPlan, createReadingPlan, clearReadingPlan, audioBook, setAudioBook, audioNarrator, setAudioNarrator, audioState, openBibleReader, openImportProject, openNewProject, pullLatestFromServer, resumeProject, renameProjectInStorage, deleteProject, handlePlayBookAudio, handleStopBookAudio, handleToggleBookAudioPause, } = useApp(); const [homeSearch, setHomeSearch] = useState(''); const [homeSort, setHomeSort] = useState(() => { try { return localStorage.getItem('homeSort') || 'recent'; } catch { return 'recent'; } }); // 'recent' | 'title' | 'passage' const [homeTagFilter, setHomeTagFilter] = useState(''); const [homeFullTextResults, setHomeFullTextResults] = useState(null); // null=idle, []=no match, [...]= results const fullTextTimerRef = useRef(null); const [renamingId, setRenamingId] = useState(null); const [renameValue, setRenameValue] = useState(''); useEffect(() => { try { localStorage.setItem('homeSort', homeSort); } catch {} }, [homeSort]); // Full-text search across all project notes (debounced, synchronous localStorage read) useEffect(() => { clearTimeout(fullTextTimerRef.current); const q = homeSearch.trim().toLowerCase(); if (q.length < 3) { setHomeFullTextResults(null); return; } fullTextTimerRef.current = window.setTimeout(() => { const index = loadProjectIndex(); const results = []; for (const entry of index) { const proj = loadProjectById(entry.id); if (!proj) continue; const matches = []; for (const [chIdx, ch] of (proj.chapters ?? []).entries()) { for (const chunk of (ch.chunks ?? [])) { for (const field of ['observation', 'interpretation', 'application', 'generalNotes', 'finalScript']) { const text = (chunk[field] ?? '').toLowerCase(); if (!text.includes(q)) continue; const idx = text.indexOf(q); const raw = chunk[field] ?? ''; const start = Math.max(0, idx - 40); const snippet = (start > 0 ? '…' : '') + raw.slice(start, idx + q.length + 60) + (idx + q.length + 60 < raw.length ? '…' : ''); matches.push({ chunkId: chunk.id, ref: formatChunkReference(proj, chIdx, chunk, '–'), field, snippet }); break; // one match per chunk } } } if (matches.length > 0) results.push({ projectId: entry.id, projectTitle: entry.title, matches }); } setHomeFullTextResults(results); }, 300); }, [homeSearch]); return (

Bible Study Project

My Studies

{authStatus}
{autoRestoredCount !== null && (

📥 Synced {autoRestoredCount} project{autoRestoredCount > 1 ? 's' : ''} from another device.

)} {staleLocalProjects.length > 0 && (

☁️ {staleLocalProjects.length} project{staleLocalProjects.length > 1 ? 's have' : ' has'} a newer version on the server:

{staleLocalProjects.map((entry) => ( ))}
)} {projectIndex.length === 0 ? (

No projects yet

Start a new Bible study to get going.

) : ( <>
setHomeSearch(e.target.value)} placeholder="Search projects by title or passage…" className="w-full max-w-sm rounded-xl border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200" /> {(() => { const allTags = Array.from( new Set(projectIndex.flatMap((entry) => entry.tags ?? [])), ).sort((a, b) => a.localeCompare(b)); if (allTags.length === 0) return null; return ( ); })()}
{/* Full-text note search results */} {homeFullTextResults !== null && (

{homeFullTextResults.length === 0 ? 'No notes match your search.' : `Notes matching "${homeSearch.trim()}" — ${homeFullTextResults.reduce((n, r) => n + r.matches.length, 0)} result${homeFullTextResults.reduce((n, r) => n + r.matches.length, 0) !== 1 ? 's' : ''} across ${homeFullTextResults.length} project${homeFullTextResults.length !== 1 ? 's' : ''}`}

{homeFullTextResults.map(({ projectId, projectTitle, matches }) => (
{matches.map(({ chunkId, ref, field, snippet }) => (
{ref} · {field}

{snippet}

))}
))}
)} {/* Reading plan card */}

Listen to BSB Audio

{audioState.status === 'idle' || audioState.status === 'error' ? 'Play a full book of the Berean Standard Bible.' : `Playing ${bookOptions.find((b) => b.abbrev === audioBook)?.name} — chapter ${audioState.chapter} of ${audioState.total}`}

{audioState.status === 'error' && (

Couldn't load audio for this book/narrator.

)}
{audioState.status === 'playing' || audioState.status === 'paused' ? ( <> ) : ( )}
{projectIndex .slice() .filter((entry) => { if (homeTagFilter && !(entry.tags ?? []).includes(homeTagFilter)) return false; const q = homeSearch.trim().toLowerCase(); if (!q) return true; return entry.title?.toLowerCase().includes(q) || entry.chapterSummary?.toLowerCase().includes(q); }) .sort((a, b) => { if (homeSort === 'title') return (a.title ?? '').localeCompare(b.title ?? ''); if (homeSort === 'passage') return (a.chapterSummary ?? '').localeCompare(b.chapterSummary ?? ''); return (b.lastEdited ?? 0) - (a.lastEdited ?? 0); }) .map((entry) => (
{renamingId === entry.id ? (
setRenameValue(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); const trimmed = renameValue.trim(); if (trimmed) renameProjectInStorage(entry.id, trimmed); setRenamingId(null); } else if (e.key === 'Escape') { setRenamingId(null); } }} className="flex-1 rounded-lg border border-slate-300 px-2 py-1 text-base font-semibold text-slate-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200" />
) : (

{entry.title}

)} {entry.chapterSummary && (

{entry.chapterSummary}

)} {entry.tags?.length > 0 && (
{entry.tags.map((tag) => ( {tag} ))}
)}

{formatRelativeDate(entry.lastEdited)}

))}
)}
); } function ReadingPlanCard({ readingPlan, createReadingPlan, clearReadingPlan, openBibleReader }) { const [showForm, setShowForm] = useState(false); const [planBook, setPlanBook] = useState(bookOptions[0].abbrev); const [planWeeks, setPlanWeeks] = useState(4); if (readingPlan) { const pct = readingPlan.totalChapters > 0 ? Math.round((readingPlan.chaptersRead.length / readingPlan.totalChapters) * 100) : 0; const daysLeft = Math.max(0, Math.ceil((readingPlan.targetDate - Date.now()) / 86400000)); const chapLeft = readingPlan.totalChapters - readingPlan.chaptersRead.length; const paceNeeded = daysLeft > 0 ? (chapLeft / daysLeft).toFixed(1) : '—'; return (

Reading Plan

{readingPlan.bookName}

{readingPlan.chaptersRead.length} / {readingPlan.totalChapters} chapters · {daysLeft} day{daysLeft !== 1 ? 's' : ''} left · {paceNeeded} ch/day needed

{pct}%

{readingPlan.chaptersRead.length > 0 && (

Read: ch. {readingPlan.chaptersRead.slice(0, 12).join(', ')}{readingPlan.chaptersRead.length > 12 ? '…' : ''}

)}
); } return (
{showForm ? (
Read in
) : (

Reading Plan

Set a goal to read through a book, track chapters as you go.

)}
); }