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
๐ Read Bible
๐
Reading Plans
๐
Plans
๐ฅ Import Session List
๐ฅ Import
+ New Project
{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}"
))}
)}
{/* 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={(
<>
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
>
)}
/>
{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}
))}
))}
)}
{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 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.
Browse plans
);
}
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}
View all plans โ
{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 && (
listenReadingPlanDay(refs[0].book, refs[0].chapter)}
title="Listen to today's reading"
className="rounded-xl border border-indigo-200 px-2.5 py-1.5 text-sm text-indigo-700 hover:bg-indigo-100"
>
๐ง
studyReadingPlanDay(refs, `${plan.title} โ ${formatDayReadings(refs)}`)}
title="Start a study project from this passage"
className="rounded-xl border border-indigo-200 px-2.5 py-1.5 text-sm text-indigo-700 hover:bg-indigo-100 disabled:cursor-not-allowed disabled:opacity-50"
>
โ
readReadingPlanDay(refs[0].book, refs[0].chapter)}
className="rounded-xl bg-indigo-600 px-3 py-1.5 text-sm font-semibold text-white hover:bg-indigo-500"
>
Read
)}
))}
);
}