Move import state into ImportPage; share translation list via hook
The session-list import form, preview, and runEpisodeImport now live in ImportPage. useAvailableTranslations fetches the translation list once per visit for both Import and Setup. Drops the never-read importFile state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d7bbbe60de
commit
8bb3b104c8
5 files changed
+189
-190
No files matched your search
-174
@@ -48,8 +48,6 @@ import {
|
||||
} from './utils/exportBuilders.js';
|
||||
import {
|
||||
parseCrossRefString,
|
||||
parseEpisodePassage,
|
||||
parseEpisodeListDocx,
|
||||
} from './utils/importParsers.js';
|
||||
|
||||
import {
|
||||
@@ -83,7 +81,6 @@ const PronunciationPage = lazy(() => import('./pages/PronunciationPage.jsx'));
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const App = () => {
|
||||
const [availableTranslations, setAvailableTranslations] = useState(['BSB']);
|
||||
const [setup, setSetup] = useState({
|
||||
translation: 'BSB',
|
||||
book: 'Titus',
|
||||
@@ -138,13 +135,6 @@ const App = () => {
|
||||
});
|
||||
const [renamingId, setRenamingId] = useState(null);
|
||||
const [renameValue, setRenameValue] = useState('');
|
||||
const [importBookAbbrev, setImportBookAbbrev] = useState(bookOptions[0].abbrev);
|
||||
const [importTranslation, setImportTranslation] = useState('BSB');
|
||||
const [importTitle, setImportTitle] = useState('');
|
||||
const [importFile, setImportFile] = useState(null);
|
||||
const [importPreview, setImportPreview] = useState(null); // parsed episode specs, before fetching chapters
|
||||
const [importBusy, setImportBusy] = useState(false);
|
||||
const [importError, setImportError] = useState('');
|
||||
const reader = useBibleReader(currentPage);
|
||||
const audio = useAudio(reader.readerBookAbbrev, reader.readerChapter);
|
||||
const { authUser, setAuthUser, authServerDown, signIn, logout } = useAuth({
|
||||
@@ -322,17 +312,6 @@ const App = () => {
|
||||
return () => { cancelled = true; };
|
||||
}, [authUser]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://bible.helloao.org/api/available_translations.json')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
setAvailableTranslations(data);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!titleEdited) {
|
||||
setSetup((current) => ({
|
||||
@@ -1530,151 +1509,9 @@ const App = () => {
|
||||
|
||||
const openImportProject = () => {
|
||||
setProject(null);
|
||||
setImportBookAbbrev(bookOptions[0].abbrev);
|
||||
setImportTranslation('BSB');
|
||||
setImportTitle('');
|
||||
setImportFile(null);
|
||||
setImportPreview(null);
|
||||
setImportError('');
|
||||
setCurrentPage('import');
|
||||
};
|
||||
|
||||
const handleImportFileChange = async (file) => {
|
||||
setImportFile(file);
|
||||
setImportPreview(null);
|
||||
setImportError('');
|
||||
if (!file) return;
|
||||
setImportBusy(true);
|
||||
try {
|
||||
const episodes = await parseEpisodeListDocx(file);
|
||||
if (episodes.length === 0) {
|
||||
throw new Error('No "Session # / Title / Passage" table found in that document.');
|
||||
}
|
||||
const specs = episodes.map((ep) => ({ ...ep, parsed: parseEpisodePassage(ep.passage) }));
|
||||
setImportPreview(specs);
|
||||
} catch (error) {
|
||||
setImportError(error.message || 'Failed to read that document.');
|
||||
} finally {
|
||||
setImportBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const runEpisodeImport = async () => {
|
||||
if (!importPreview || importPreview.length === 0) {
|
||||
setImportError('Choose a .docx file first.');
|
||||
return;
|
||||
}
|
||||
setImportBusy(true);
|
||||
setImportError('');
|
||||
try {
|
||||
const selectedBook = bookOptions.find((b) => b.abbrev === importBookAbbrev) ?? bookOptions[0];
|
||||
|
||||
const chapterNumbersNeeded = new Set();
|
||||
importPreview.forEach((spec) => {
|
||||
if (spec.parsed && spec.parsed !== 'invalid') {
|
||||
chapterNumbersNeeded.add(spec.parsed.startChapter);
|
||||
chapterNumbersNeeded.add(spec.parsed.endChapter);
|
||||
}
|
||||
});
|
||||
const sortedChapters = Array.from(chapterNumbersNeeded).sort((a, b) => a - b);
|
||||
if (sortedChapters.length === 0) {
|
||||
throw new Error('No verse passages found to import.');
|
||||
}
|
||||
|
||||
const versesByChapter = {};
|
||||
for (const chNum of sortedChapters) {
|
||||
const response = await fetch(
|
||||
`https://bible.helloao.org/api/${importTranslation}/${selectedBook.abbrev}/${chNum}.json`,
|
||||
);
|
||||
if (!response.ok) throw new Error(`Unable to load ${selectedBook.name} ${chNum}.`);
|
||||
const data = await response.json();
|
||||
const verses = parseBibleChapter(data);
|
||||
if (!verses.length) throw new Error(`No verses returned for ${selectedBook.name} ${chNum}.`);
|
||||
versesByChapter[chNum] = verses;
|
||||
}
|
||||
|
||||
const chapterIndexByNum = new Map(sortedChapters.map((n, idx) => [n, idx]));
|
||||
const chapters = sortedChapters.map((chNum) => ({
|
||||
book: selectedBook.name,
|
||||
bookAbbrev: selectedBook.abbrev,
|
||||
chapter: chNum,
|
||||
verses: versesByChapter[chNum],
|
||||
chunks: [],
|
||||
}));
|
||||
|
||||
// Markers (rows with no passage) attach to the nearest upcoming chapter with a
|
||||
// real passage, or fall back to the previous one for trailing markers.
|
||||
const anchorChapterFor = importPreview.map((spec, i) => {
|
||||
if (spec.parsed && spec.parsed !== 'invalid') return spec.parsed.startChapter;
|
||||
for (let j = i + 1; j < importPreview.length; j += 1) {
|
||||
const next = importPreview[j].parsed;
|
||||
if (next && next !== 'invalid') return next.startChapter;
|
||||
}
|
||||
for (let j = i - 1; j >= 0; j -= 1) {
|
||||
const prev = importPreview[j].parsed;
|
||||
if (prev && prev !== 'invalid') return prev.startChapter;
|
||||
}
|
||||
return sortedChapters[0];
|
||||
});
|
||||
|
||||
importPreview.forEach((spec, i) => {
|
||||
if (spec.parsed === 'invalid') return; // skip unparseable rows entirely
|
||||
const chIdx = chapterIndexByNum.get(anchorChapterFor[i]);
|
||||
if (chIdx == null) return;
|
||||
const target = chapters[chIdx];
|
||||
|
||||
const baseChunk = {
|
||||
id: makeId(),
|
||||
observation: '',
|
||||
interpretation: '',
|
||||
application: '',
|
||||
crossReferences: [],
|
||||
greekWords: [],
|
||||
generalNotes: '',
|
||||
episodeNumber: spec.episodeNumber,
|
||||
episodeTitle: spec.title,
|
||||
finalScript: '',
|
||||
tags: [],
|
||||
};
|
||||
|
||||
if (!spec.parsed) {
|
||||
// No passage (e.g. a part intro/review) -> zero-length marker chunk.
|
||||
target.chunks.push({ ...baseChunk, startVerse: 0, endVerse: 0, spilloverEndVerse: null });
|
||||
return;
|
||||
}
|
||||
|
||||
const { startChapter, startVerse, endChapter, endVerse } = spec.parsed;
|
||||
if (endChapter === startChapter) {
|
||||
target.chunks.push({ ...baseChunk, startVerse, endVerse, spilloverEndVerse: null });
|
||||
} else {
|
||||
const lastVerseOfStartChapter = versesByChapter[startChapter]?.at(-1)?.number ?? endVerse;
|
||||
target.chunks.push({
|
||||
...baseChunk,
|
||||
startVerse,
|
||||
endVerse: lastVerseOfStartChapter,
|
||||
spilloverEndVerse: endVerse,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const newProject = {
|
||||
id: makeId(),
|
||||
title: importTitle.trim() || `${selectedBook.name} Sessions`,
|
||||
translation: importTranslation,
|
||||
lastEdited: Date.now(),
|
||||
selectedChunkId: null,
|
||||
chapters,
|
||||
};
|
||||
setProject(newProject);
|
||||
setActiveChapterIndex(0);
|
||||
setCurrentPage('setup');
|
||||
} catch (error) {
|
||||
setImportError(error.message || 'Import failed.');
|
||||
} finally {
|
||||
setImportBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resumeProject = (id) => {
|
||||
const loaded = loadProjectById(id);
|
||||
if (!loaded) return;
|
||||
@@ -1957,21 +1794,10 @@ const deleteProject = (id) => {
|
||||
deleteProject,
|
||||
// Audio (book player on Home + chapter player in the Reader)
|
||||
...audio,
|
||||
// Import
|
||||
importBookAbbrev, setImportBookAbbrev,
|
||||
importTranslation, setImportTranslation,
|
||||
importTitle, setImportTitle,
|
||||
importFile,
|
||||
importPreview,
|
||||
importBusy,
|
||||
importError,
|
||||
handleImportFileChange,
|
||||
runEpisodeImport,
|
||||
// Reader
|
||||
...reader,
|
||||
copyVerse,
|
||||
// Setup
|
||||
availableTranslations,
|
||||
project, setProject,
|
||||
setup,
|
||||
titleEdited, setTitleEdited,
|
||||
|
||||
Reference in new issue
Block a user