import { useEffect, useRef, useState } from 'react'; import { useApp } from '../context/AppContext.js'; import { filterStudies, groupBySeries, isTagQuery, seriesList, tagCounts } from '../utils/studyFilter.js'; import BookCover from '../components/BookCover.jsx'; import WeeklyRecall from '../components/WeeklyRecall.jsx'; import RecallHistory from '../components/RecallHistory.jsx'; import IqDailyCard from '../components/iq/IqDailyCard.jsx'; import IqNarratorOptions, { useLazyNarrators } from '../components/iq/IqNarratorOptions.jsx'; import PageShell from '../components/study/PageShell.jsx'; import AudioPlayer from '../components/AudioPlayer.jsx'; import { bookOptions, formatChunkReference, formatRelativeDate, loadProjectIndex, } from '../utils/bibleUtils.jsx'; import { backupAge, lastBackupAt } from '../utils/backup.js'; import { loadProjectById } from '../utils/projectStorage.js'; import { compactPassages } from '../utils/passageSummary.js'; import { getPlan, getPlanSchedule, formatDayReadings, nextUnreadDayIndex, computeStreak } from '../utils/readingPlans.js'; export default function HomePage() { const { autoRestoredCount, staleLocalProjects, sharedStudies, projectIndex, planProgress, readingActivity, openReadingPlans, openPlanReading, studyReadingPlanDay, studyBuildBusy, readingPlanSyncStatus, readingPlanSyncError, retryReadingPlanSync, audioBook, setAudioBook, audioNarrator, setAudioNarrator, audioState, audioProgress, openImportProject, setCurrentPage, openBackup, openNewProject, pullLatestFromServer, resumeProject, renameProjectInStorage, setProjectSeries, deleteProject, handlePlayBookAudio, handleStopBookAudio, handleToggleBookAudioPause, handleSkipBookChapter, handleSeekAudio, handleSeekAudioTo, } = useApp(); const narrators = useLazyNarrators(audioNarrator); const [homeSearch, setHomeSearch] = useState(''); const [homeSort, setHomeSort] = useState(() => { try { return localStorage.getItem('homeSort') || 'recent'; } catch { return 'recent'; } }); // 'recent' | 'title' | 'passage' const [homeTags, setHomeTags] = useState([]); // tag chips switched on: a study must have all of them const [showAllTags, setShowAllTags] = useState(false); const [seriesFilter, setSeriesFilter] = useState(''); // '' = all, '__none' = not in a series, or a series name const [seriesEditingId, setSeriesEditingId] = useState(null); const [seriesValue, setSeriesValue] = useState(''); const toggleTag = (tag) => setHomeTags((cur) => (cur.includes(tag) ? cur.filter((t) => t !== tag) : [...cur, tag])); const [homeView, setHomeView] = useState(() => { try { return localStorage.getItem('homeView') === 'list' ? 'list' : 'grid'; } catch { return 'grid'; } }); // 'grid' | 'list' useEffect(() => { try { localStorage.setItem('homeView', homeView); } catch {} }, [homeView]); 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 || isTagQuery(q)) { 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]); const tagList = tagCounts(projectIndex); const shownTags = showAllTags ? tagList : tagList.slice(0, 12); const seriesNames = seriesList(projectIndex); const visible = filterStudies(projectIndex, { query: homeSearch, tags: homeTags, series: seriesFilter }) .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); }); // With series in use (and no single series chosen) the studies are grouped under their series name const sections = seriesFilter === '' && seriesNames.length > 0 ? groupBySeries(visible) : [{ name: '', items: visible, flat: true }]; const fieldCls = 'rounded-lg border border-[var(--r-line)] bg-[var(--r-bar)] px-2.5 py-1.5 text-sm text-[var(--r-text)] focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]'; const linkBtn = 'rounded-lg px-3 py-1.5 text-sm text-[var(--r-sub)] hover:bg-black/5'; const notice = 'mb-4 rounded-xl border border-[var(--r-line)] bg-[var(--r-bar)] px-4 py-3 text-sm'; return ( )} >
{projectIndex.length > 0 && (() => { const age = backupAge(lastBackupAt()); return (

{age.text}.

); })()} {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) => ( ))}
)} {readingPlanSyncStatus === 'error' && (
Couldn’t sync your reading plans to the server. Changes are only saved on this device until this is fixed.
)} {projectIndex.length === 0 ? (

No projects yet

Start a new Bible study to get going.

) : ( <>
setHomeSearch(e.target.value)} placeholder="Search projects by title, passage or #tag…" className={`${fieldCls} w-full max-w-sm`} /> {seriesNames.length > 0 && ( )}
{[['grid', 'Grid'], ['list', 'List']].map(([id, label]) => ( ))}
{seriesNames.map((sr) => {tagList.length > 0 && (
Tags {shownTags.map(({ tag, count }) => { const on = homeTags.includes(tag); return ( ); })} {tagList.length > 12 && ( )} {homeTags.length > 0 && }
)} {visible.length === 0 && (

No studies match{homeTags.length ? ` ${homeTags.map((t) => `“${t}”`).join(' + ')}` : ''}{homeSearch.trim() ? ` “${homeSearch.trim()}”` : ''}.{' '}

)} {/* 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' : ''}`}

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

{snippet}

))}
))}
)} {sections.map((section) => (
{!section.flat && (

{section.name || 'Not in a series'} {section.items.length}

)}
{section.items.map((entry) => (
{homeView === 'list' ? : }
{renamingId === entry.id ? (
setRenameValue(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); const t = renameValue.trim(); if (t) renameProjectInStorage(entry.id, t); setRenamingId(null); } else if (e.key === 'Escape') setRenamingId(null); }} className={`${fieldCls} flex-1`} />
) : (

{entry.title}

{sharedStudies?.[entry.id] !== undefined && ( Shared{sharedStudies[entry.id] ? ` by ${sharedStudies[entry.id]}` : ''} )}
)} {seriesEditingId === entry.id && (
setSeriesValue(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); setProjectSeries(entry.id, seriesValue); setSeriesEditingId(null); } else if (e.key === 'Escape') setSeriesEditingId(null); }} className={`${fieldCls} flex-1`} />
)} {entry.chapterSummary &&

{compactPassages(entry.chapterSummary)}

} {entry.tags?.length > 0 && ( // A few tags and a "+N" for the rest (hover for all; the Tag filter above finds any of them)
3 ? entry.tags.join(', ') : undefined}> {entry.tags.slice(0, 3).map((tag) => ( ))} {entry.tags.length > 3 && +{entry.tags.length - 3} more}
)}

{formatRelativeDate(entry.lastEdited)}

))}
))} )}
); } // Today's reading across your active plans, each opening straight into the Reader. function TodayCard({ planProgress, readingActivity, openReadingPlans, openPlanReading, studyReadingPlanDay, studyBuildBusy }) { const active = planProgress.filter((p) => !p.completedAt); const streak = computeStreak(readingActivity); const streakText = streak.current > 0 && {streak.current} day{streak.current !== 1 ? 's' : ''} in a row; if (active.length === 0) { return (

Reading Plans

{streakText}

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 dayIndex = nextUnreadDayIndex(plan, progress.completedDays); const refs = dayIndex != null ? getPlanSchedule(progress.planId)[dayIndex] : null; return { progress, plan, dayIndex, refs, pct: plan.days > 0 ? Math.round((progress.completedDays.length / plan.days) * 100) : 0 }; }).filter(Boolean); return (

Today’s Reading

{streakText}
{rows.map(({ progress, plan, dayIndex, refs, pct }) => (

{plan.icon ? `${plan.icon} ` : ''}{plan.title}

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

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