import { useEffect, useRef, useState } from 'react';
import { useApp } from '../context/AppContext.js';
import PageShell from '../components/study/PageShell.jsx';
import AudioPlayer from '../components/AudioPlayer.jsx';
import {
bookOptions,
formatChunkReference,
formatRelativeDate,
loadProjectIndex,
} from '../utils/bibleUtils.jsx';
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,
projectIndex,
planProgress,
readingActivity,
openReadingPlans,
openPlanReading,
studyReadingPlanDay,
studyBuildBusy,
readingPlanSyncStatus,
readingPlanSyncError,
retryReadingPlanSync,
audioBook, setAudioBook,
audioNarrator, setAudioNarrator,
audioState,
audioProgress,
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]);
const allTags = Array.from(new Set(projectIndex.flatMap((entry) => entry.tags ?? []))).sort((a, b) => a.localeCompare(b));
const visible = 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);
});
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 (
>
)}
>
{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 or passage…"
className={`${fieldCls} w-full max-w-sm`} />
{allTags.length > 0 && (
)}
{/* 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}
))}
))}
)}
{visible.map((entry) => (
{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}
)}
{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) => {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 && (
)}
))}
);
}