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 (
{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) => (
pullLatestFromServer(entry.id)}
className="rounded-xl bg-amber-700 px-4 py-2 text-sm font-semibold text-white transition hover:bg-amber-600"
>
Pull latest "{entry.title}"
))}
)}
{projectIndex.length === 0 ? (
No projects yet
Start a new Bible study to get going.
+ New Project
) : (
<>
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"
/>
Sort by{' '}
setHomeSort(e.target.value)}
className="ml-1 rounded-xl border border-slate-300 bg-white px-2 py-1.5 text-sm text-slate-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
>
Last edited
Title
Passage
{(() => {
const allTags = Array.from(
new Set(projectIndex.flatMap((entry) => entry.tags ?? [])),
).sort((a, b) => a.localeCompare(b));
if (allTags.length === 0) return null;
return (
Tag{' '}
setHomeTagFilter(e.target.value)}
className="ml-1 rounded-xl border border-slate-300 bg-white px-2 py-1.5 text-sm text-slate-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
>
All tags
{allTags.map((tag) => (
{tag}
))}
);
})()}
{/* 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 }) => (
resumeProject(projectId)}
className="mb-1.5 text-sm font-semibold text-violet-700 hover:underline"
>
{projectTitle} →
{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.
)}
setAudioBook(e.target.value)}
disabled={audioState.status === 'playing' || audioState.status === 'paused'}
className="rounded-xl border border-slate-300 bg-white px-2 py-1.5 text-sm text-slate-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200 disabled:opacity-60"
>
{bookOptions.map((book) => (
{book.name}
))}
setAudioNarrator(e.target.value)}
disabled={audioState.status === 'playing' || audioState.status === 'paused'}
className="rounded-xl border border-slate-300 bg-white px-2 py-1.5 text-sm text-slate-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200 disabled:opacity-60"
>
David
Hays
Souer
{audioState.status === 'playing' || audioState.status === 'paused' ? (
<>
{audioState.status === 'paused' ? 'Resume' : 'Pause'}
Stop
>
) : (
Play book
)}
{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"
/>
{
const trimmed = renameValue.trim();
if (trimmed) renameProjectInStorage(entry.id, trimmed);
setRenamingId(null);
}}
className="text-sm font-semibold text-emerald-600 hover:text-emerald-700"
>
Save
setRenamingId(null)}
className="text-sm text-slate-400 hover:text-slate-600"
>
×
) : (
{entry.title}
{ setRenamingId(entry.id); setRenameValue(entry.title ?? ''); }}
className="shrink-0 text-xs text-slate-400 hover:text-slate-600"
title="Rename project"
>
✎ Rename
)}
{entry.chapterSummary && (
{entry.chapterSummary}
)}
{entry.tags?.length > 0 && (
{entry.tags.map((tag) => (
{tag}
))}
)}
{formatRelativeDate(entry.lastEdited)}
resumeProject(entry.id)}
className="flex-1 rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white transition hover:bg-slate-800"
>
Resume
deleteProject(entry.id)}
className="rounded-xl border border-rose-200 bg-rose-50 px-4 py-2 text-sm font-semibold text-rose-700 transition hover:bg-rose-100"
>
Delete
))}
>
)}
);
}
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
Read
{ if (window.confirm('Clear reading plan?')) clearReadingPlan(); }}
className="rounded-xl border border-emerald-300 px-3 py-1.5 text-sm text-emerald-700 hover:bg-emerald-100"
>
Clear
{pct}%
{readingPlan.chaptersRead.length > 0 && (
Read: ch. {readingPlan.chaptersRead.slice(0, 12).join(', ')}{readingPlan.chaptersRead.length > 12 ? '…' : ''}
)}
);
}
return (
{showForm ? (
Read
setPlanBook(e.target.value)}
className="rounded-xl border border-slate-300 bg-slate-50 px-2 py-1.5 text-sm text-slate-900 focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
>
{bookOptions.map((b) => (
{b.name} ({CHAPTER_COUNTS[b.abbrev] ?? '?'} ch)
))}
in
setPlanWeeks(Number(e.target.value))}
className="rounded-xl border border-slate-300 bg-slate-50 px-2 py-1.5 text-sm text-slate-900 focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
>
{[1,2,3,4,6,8,12,16,26,52].map((w) => {w} week{w !== 1 ? 's' : ''} )}
{
const book = bookOptions.find((b) => b.abbrev === planBook);
createReadingPlan(planBook, book?.name ?? planBook, planWeeks);
setShowForm(false);
}}
className="rounded-xl bg-emerald-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-emerald-500"
>
Start plan
setShowForm(false)} className="text-sm text-slate-400 hover:text-slate-600">Cancel
) : (
Reading Plan
Set a goal to read through a book, track chapters as you go.
setShowForm(true)}
className="shrink-0 rounded-xl border border-slate-300 px-3 py-1.5 text-sm font-semibold text-slate-700 hover:bg-slate-50"
>
Set goal
)}
);
}