import { useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import DOMPurify from 'dompurify'; import { useApp } from '../context/AppContext.js'; import { useCrossRefs } from '../hooks/useCrossRefs.js'; import { useStudyResources } from '../hooks/useStudyResources.js'; import { useWordStudy } from '../hooks/useWordStudy.js'; import { buildGreekDefinitionHtml, externalLookupLinks, speakOriginalWord } from '../utils/wordStudy.js'; import ProjectHeader from '../components/ProjectHeader.jsx'; import { STUDY_TABS, COMMENTARY_OPTIONS, NT_BOOK_NUMBER, formatChunkReference, getChunkVerseEntries, CrossRefChip, } from '../utils/bibleUtils.jsx'; export default function StudyPage() { const { project, allChunks, projectIndex, updateProject, updateChunk } = useApp(); // Short-lived status line (word suggestions, DOCX import, lookups, …) const [statusMessage, setStatusMessage] = useState(''); const statusTimerRef = useRef(null); const notify = (message, ms = 2000) => { window.clearTimeout(statusTimerRef.current); setStatusMessage(message); statusTimerRef.current = window.setTimeout(() => setStatusMessage(''), ms); }; useEffect(() => () => window.clearTimeout(statusTimerRef.current), []); const selectedChunk = allChunks.find((c) => c.id === project?.selectedChunkId) ?? allChunks[0] ?? null; const selectedChunkChapterIndex = selectedChunk ? project.chapters.findIndex((ch) => ch.chunks.some((c) => c.id === selectedChunk.id)) : -1; const selectedChunkChapter = selectedChunkChapterIndex >= 0 ? project.chapters[selectedChunkChapterIndex] : null; const selectedChunkVerses = selectedChunk ? getChunkVerseEntries(project, selectedChunkChapterIndex, selectedChunk) : []; const selectedChunkGlobalIndex = allChunks.findIndex((c) => c.id === project?.selectedChunkId); const [studyLayout, setStudyLayout] = useState(() => { const saved = localStorage.getItem('studyLayout'); return saved === 'stacked' ? 'stacked' : 'split'; // 'annotate' (Draw) was removed }); // 'stacked' | 'split' const [activeStudyTab, setActiveStudyTab] = useState( () => localStorage.getItem('activeStudyTab') || 'notes', ); const [mobileStudyTab, setMobileStudyTab] = useState('scripture'); useEffect(() => { localStorage.setItem('studyLayout', studyLayout); }, [studyLayout]); useEffect(() => { localStorage.setItem('activeStudyTab', activeStudyTab); }, [activeStudyTab]); const [collapsedSections, setCollapsedSections] = useState({}); const { commentarySource, setCommentarySource, commentaryData, commentaryLoading, commentaryError, interlinearData, interlinearLoading, interlinearError, } = useStudyResources({ project, selectedChunk, selectedChunkChapterIndex, collapsedSections }); const { crossRefInput, setCrossRefInput, suggestingCrossRefs, crossRefSuggestions, addCrossRef, removeCrossRef, suggestCrossRefsForChunk, addSuggestedCrossRef, loadVerseText, } = useCrossRefs({ project, allChunks, selectedChunk, updateChunk, notify }); const { suggestingGreekForChunkId, suggestingHebrewForChunkId, suggestModal, setSuggestModal, suggestSelection, setSuggestSelection, updateChunkWord, addGreekWord, removeGreekWord, lookupWord, suggestGreekWordsForChunk, suggestHebrewWordsForChunk, confirmSuggestWords, } = useWordStudy({ project, allChunks, updateProject, notify }); const [tagInput, setTagInput] = useState(''); const addTag = (chunkId) => { const tag = tagInput.trim(); if (!tag) return; const existing = selectedChunk?.tags ?? []; if (existing.some((t) => t.toLowerCase() === tag.toLowerCase())) { setTagInput(''); return; } updateChunk(chunkId, { tags: [...existing, tag] }); setTagInput(''); }; const removeTag = (chunkId, tag) => { updateChunk(chunkId, { tags: (selectedChunk?.tags ?? []).filter((t) => t !== tag), }); }; useEffect(() => { setTagInput(''); }, [selectedChunk?.id]); const importFinalScriptDocx = async (chunkId, file) => { if (!file) return; try { const arrayBuffer = await file.arrayBuffer(); const { default: mammoth } = await import('mammoth'); const { value } = await mammoth.extractRawText({ arrayBuffer }); updateChunk(chunkId, { finalScript: value.trim() }); notify('Imported script from DOCX.', 1800); } catch { notify('Could not read that DOCX file.', 1800); } }; const goToPreviousChunk = () => { if (selectedChunkGlobalIndex <= 0) return; updateProject((current) => ({ ...current, selectedChunkId: allChunks[selectedChunkGlobalIndex - 1].id, })); }; const goToNextChunk = () => { if (selectedChunkGlobalIndex < 0 || selectedChunkGlobalIndex >= allChunks.length - 1) return; updateProject((current) => ({ ...current, selectedChunkId: allChunks[selectedChunkGlobalIndex + 1].id, })); }; // Keyboard shortcuts: arrow keys navigate chunks on the study page, Esc closes modals useEffect(() => { const handleKeyDown = (event) => { const tag = event.target?.tagName; const isEditable = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || event.target?.isContentEditable; if (event.key === 'Escape') { if (suggestModal) { setSuggestModal(null); setSuggestSelection(new Set()); } return; } if (isEditable || suggestModal) return; if (event.key === 'ArrowLeft') { event.preventDefault(); goToPreviousChunk(); } else if (event.key === 'ArrowRight') { event.preventDefault(); goToNextChunk(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [suggestModal, selectedChunkGlobalIndex, allChunks]); return (

Bible Study Project

{project?.title ?? ''}

{selectedChunk && selectedChunkChapterIndex >= 0 && (

{formatChunkReference(project, selectedChunkChapterIndex, selectedChunk, '–')}

)}
{/* Sidebar */} {/* Chunk editor */}

Chunk editor

{selectedChunk ? `Section ${formatChunkReference(project, selectedChunkChapterIndex, selectedChunk, '–')}` : 'Select a chunk'}

{[ { id: 'stacked', label: '☰ Stacked' }, { id: 'split', label: '◫ Split' }, ].map((m) => ( ))}
{/* Mobile tab bar */}
{[ { id: 'scripture', label: 'Scripture' }, { id: 'notes', label: 'Notes' }, { id: 'crossRefs', label: 'Refs' }, { id: 'wordStudy', label: 'Words' }, { id: 'commentary', label: 'Commentary' }, { id: 'script', label: 'Script' }, ].map((tab) => ( ))}
{selectedChunk ? (
{/* Scripture */}

Scripture

Read-only passage for the selected chunk.

{formatChunkReference(project, selectedChunkChapterIndex, selectedChunk, '–')}
{selectedChunkVerses .map((verse) => (

{verse.chapter}:{verse.number}. {verse.text}

))}
{/* Interlinear */}
{!collapsedSections.interlinear && (
{interlinearLoading &&

Loading interlinear text...

} {interlinearError &&

{interlinearError}

} {!interlinearLoading && !interlinearError && interlinearData && Array.from( { length: selectedChunk.endVerse - selectedChunk.startVerse + 1 }, (_, i) => selectedChunk.startVerse + i, ) .filter((num) => interlinearData[String(num)]) .map((num) => (
{selectedChunkChapter.chapter}:{num}.{' '} {interlinearData[String(num)].map((w, i) => ( {w.o} {w.g || '—'} ))}
))}
)}
{/* Right column (study panels) */}
{studyLayout === 'split' && (
{STUDY_TABS.map((tab) => ( ))}
)} {/* Session metadata */}

Session Info

Optional — only fill this in if this chunk is part of a numbered series (a podcast episode, sermon, or class session). Used to label it in exports and in "Prepare for Podcast."

updateChunk(selectedChunk.id, { episodeNumber: e.target.value })} placeholder="Session #" className="w-full rounded-2xl 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 sm:w-32" /> updateChunk(selectedChunk.id, { episodeTitle: e.target.value })} placeholder="Session title" className="w-full rounded-2xl 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" />
{/* Background / general notes */}
{!collapsedSections.generalNotes && (