import { useEffect, useRef, useState } from 'react'; import { useApp } from '../context/AppContext.js'; import AuthStatus from '../components/AuthStatus.jsx'; import AudioPlayer from '../components/AudioPlayer.jsx'; import { bookOptions, formatChunkReference, formatRelativeDate, loadProjectIndex, } from '../utils/bibleUtils.jsx'; import { loadProjectById } from '../utils/projectStorage.js'; import { getPlan, getPlanSchedule, formatDayReadings, nextUnreadDayIndex, computeStreak } from '../utils/readingPlans.js'; export default function HomePage() { const { autoRestoredCount, staleLocalProjects, projectIndex, planProgress, readingActivity, toggleDay, openReadingPlans, readReadingPlanDay, listenReadingPlanDay, studyReadingPlanDay, studyBuildBusy, audioBook, setAudioBook, audioNarrator, setAudioNarrator, audioState, audioProgress, openBibleReader, openImportProject, openNewProject, pullLatestFromServer, resumeProject, renameProjectInStorage, deleteProject, handlePlayBookAudio, handleStopBookAudio, handleToggleBookAudioPause, handleSkipBookChapter, handleSeekAudio, handleSeekAudioTo, } = 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

{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) => ( ))}
)} {/* Reading plans summary โ€” shown regardless of whether there are any study projects yet */} {/* BSB audio player โ€” shown regardless of whether there are any study projects yet, so "Listen" from a reading plan always has somewhere visible to land. */}
b.abbrev === audioBook)?.name} โ€” chapter ${audioState.chapter} of ${audioState.total}` } errorMessage="Couldn't load audio for this book/narrator." status={audioState.status} currentTime={audioProgress.currentTime} duration={audioProgress.duration} onPlay={handlePlayBookAudio} onTogglePause={handleToggleBookAudioPause} onSeekBack={() => handleSeekAudio(-10)} onSeekForward={() => handleSeekAudio(10)} onSeekTo={handleSeekAudioTo} onNext={handleSkipBookChapter} nextDisabled={audioState.chapter >= audioState.total} onStop={handleStopBookAudio} controls={( <> )} />
{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}

))}
))}
)}
{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 ReadingPlansSummaryCard({ planProgress, readingActivity, openReadingPlans, readReadingPlanDay, listenReadingPlanDay, studyReadingPlanDay, studyBuildBusy, toggleDay, }) { const active = planProgress.filter((p) => !p.completedAt); const streak = computeStreak(readingActivity); const streakBadge = streak.current > 0 && ( ๐Ÿ”ฅ {streak.current} day{streak.current !== 1 ? 's' : ''} ); if (active.length === 0) { return (

Reading Plans

{streakBadge}

Read through the Bible, the Gospels, Psalms, and more โ€” one day at a time.

); } const rows = active .map((progress) => { const plan = getPlan(progress.planId); if (!plan) return null; const schedule = getPlanSchedule(progress.planId); const pct = plan.days > 0 ? Math.round((progress.completedDays.length / plan.days) * 100) : 0; const dayIndex = nextUnreadDayIndex(plan, progress.completedDays); const refs = dayIndex != null ? schedule[dayIndex] : null; return { progress, plan, pct, dayIndex, refs }; }) .filter(Boolean); return (

๐Ÿ“… Today's Reading

{streakBadge}
{rows.map(({ progress, plan, pct, dayIndex, refs }) => (
{refs ? ( toggleDay(progress.id, dayIndex, plan.days)} className="h-4 w-4 shrink-0 rounded border-slate-300 text-indigo-600 focus:ring-indigo-500" /> ) : ( โœ“ )} {plan.icon ?? '๐Ÿ“–'}

{plan.title}

{refs ? formatDayReadings(refs) : `${progress.completedDays.length} / ${plan.days} days`}

{refs && (
)}
))}
); }