Fix missing verses and add cross-chapter chunk spanning
This commit is contained in:
+211
-56
@@ -129,7 +129,10 @@ export function migrateChunk(chunk) {
|
||||
function chunkSpansNextChapter(project, startChapterIndex, chunk) {
|
||||
if (!project || !Array.isArray(project.chapters)) return false;
|
||||
if (!Number.isInteger(chunk?.spilloverEndVerse)) return false;
|
||||
return startChapterIndex >= 0 && startChapterIndex < project.chapters.length - 1;
|
||||
if (startChapterIndex < 0 || startChapterIndex >= project.chapters.length - 1) return false;
|
||||
const startChapter = project.chapters[startChapterIndex];
|
||||
const nextChapter = project.chapters[startChapterIndex + 1];
|
||||
return startChapter?.bookAbbrev && startChapter.bookAbbrev === nextChapter?.bookAbbrev;
|
||||
}
|
||||
|
||||
function formatChunkReference(project, startChapterIndex, chunk, separator = '-') {
|
||||
@@ -323,15 +326,12 @@ export function buildExportHtml(project) {
|
||||
|
||||
const chapters = Array.isArray(project.chapters) ? project.chapters : [];
|
||||
|
||||
const chunksHtml = chapters.map((ch) => {
|
||||
const chunksHtml = chapters.map((ch, chapterIndex) => {
|
||||
const chapterHeader = `<h2 class="chapter-heading">${ch.book} ${ch.chapter}</h2>`;
|
||||
const chunkSections = ch.chunks.map((chunk) => {
|
||||
const scripture = chunk.startVerse === chunk.endVerse
|
||||
? `${ch.book} ${ch.chapter}:${chunk.startVerse}`
|
||||
: `${ch.book} ${ch.chapter}:${chunk.startVerse}-${chunk.endVerse}`;
|
||||
const versesText = ch.verses
|
||||
.filter((verse) => verse.number >= chunk.startVerse && verse.number <= chunk.endVerse)
|
||||
.map((verse) => `<p class="verse"><strong>${verse.number}</strong> ${verse.text}</p>`)
|
||||
const scripture = formatChunkReference(project, chapterIndex, chunk, '-');
|
||||
const versesText = getChunkVerseEntries(project, chapterIndex, chunk)
|
||||
.map((verse) => `<p class="verse"><strong>${verse.chapter}:${verse.number}</strong> ${verse.text}</p>`)
|
||||
.join('');
|
||||
|
||||
const observation = (chunk.observation ?? '').trim().replace(/\n/g, '<br />') || '<em>No observation.</em>';
|
||||
@@ -516,16 +516,13 @@ PASSAGE: ${chapterLabel}
|
||||
`;
|
||||
|
||||
let chunkIndex = 0;
|
||||
const chunks = chapters.map((ch) => {
|
||||
const chunks = chapters.map((ch, chapterIndex) => {
|
||||
return ch.chunks.map((chunk) => {
|
||||
chunkIndex += 1;
|
||||
const ref = chunk.startVerse === chunk.endVerse
|
||||
? `${ch.book} ${ch.chapter}:${chunk.startVerse}`
|
||||
: `${ch.book} ${ch.chapter}:${chunk.startVerse}–${chunk.endVerse}`;
|
||||
const ref = formatChunkReference(project, chapterIndex, chunk, '–');
|
||||
|
||||
const verses = ch.verses
|
||||
.filter((v) => v.number >= chunk.startVerse && v.number <= chunk.endVerse)
|
||||
.map((v) => `${v.number} ${v.text}`)
|
||||
const verses = getChunkVerseEntries(project, chapterIndex, chunk)
|
||||
.map((v) => `${v.chapter}:${v.number} ${v.text}`)
|
||||
.join('\n');
|
||||
|
||||
const observation = (chunk.observation ?? '').trim() || 'No observation.';
|
||||
@@ -629,6 +626,8 @@ const App = () => {
|
||||
const [statusMessage, setStatusMessage] = useState('');
|
||||
const [typedChunkStart, setTypedChunkStart] = useState('');
|
||||
const [typedChunkEnd, setTypedChunkEnd] = useState('');
|
||||
const [typedChunkNextEnd, setTypedChunkNextEnd] = useState('');
|
||||
const [clickedSpanNextEnd, setClickedSpanNextEnd] = useState('');
|
||||
const [typedChunkBulk, setTypedChunkBulk] = useState('');
|
||||
const saveTimerRef = useRef(null);
|
||||
const [syncStatus, setSyncStatus] = useState(''); // '' | 'syncing' | 'synced' | 'error'
|
||||
@@ -685,9 +684,15 @@ const App = () => {
|
||||
const allChunks = project ? project.chapters.flatMap((ch) => ch.chunks) : [];
|
||||
const activeChapter = project?.chapters[activeChapterIndex] ?? null;
|
||||
const selectedChunk = allChunks.find((c) => c.id === project?.selectedChunkId) ?? allChunks[0] ?? null;
|
||||
const selectedChunkChapter = selectedChunk
|
||||
? project.chapters.find((ch) => ch.chunks.some((c) => c.id === selectedChunk.id))
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -845,16 +850,21 @@ const App = () => {
|
||||
// Chunk CRUD (operates on activeChapter in setup view, on selectedChunkChapter in study)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const addChunkToChapter = (chapterIndex, start, end) => {
|
||||
const addChunkToChapter = (chapterIndex, start, end, spilloverEndVerse = null) => {
|
||||
updateProject((current) => {
|
||||
const chapters = current.chapters.map((ch, idx) => {
|
||||
if (idx !== chapterIndex) return ch;
|
||||
const overlap = ch.chunks.find((c) => c.startVerse === start && c.endVerse === end);
|
||||
const overlap = ch.chunks.find(
|
||||
(c) => c.startVerse === start
|
||||
&& c.endVerse === end
|
||||
&& (c.spilloverEndVerse ?? null) === (spilloverEndVerse ?? null),
|
||||
);
|
||||
if (overlap) return ch;
|
||||
const newChunk = {
|
||||
id: makeId(),
|
||||
startVerse: start,
|
||||
endVerse: end,
|
||||
spilloverEndVerse,
|
||||
observation: '',
|
||||
interpretation: '',
|
||||
application: '',
|
||||
@@ -869,17 +879,21 @@ const App = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const addChunk = (start, end) => {
|
||||
const addChunk = (start, end, spilloverEndVerse = null) => {
|
||||
if (!project) return;
|
||||
const chapter = activeChapter;
|
||||
if (!chapter) return;
|
||||
const overlap = chapter.chunks.find((c) => c.startVerse === start && c.endVerse === end);
|
||||
const overlap = chapter.chunks.find(
|
||||
(c) => c.startVerse === start
|
||||
&& c.endVerse === end
|
||||
&& (c.spilloverEndVerse ?? null) === (spilloverEndVerse ?? null),
|
||||
);
|
||||
if (overlap) {
|
||||
setStatusMessage('That chunk already exists.');
|
||||
window.setTimeout(() => setStatusMessage(''), 1800);
|
||||
return;
|
||||
}
|
||||
addChunkToChapter(activeChapterIndex, start, end);
|
||||
addChunkToChapter(activeChapterIndex, start, end, spilloverEndVerse);
|
||||
};
|
||||
|
||||
const addChunkRanges = (ranges) => {
|
||||
@@ -894,11 +908,13 @@ const App = () => {
|
||||
updateProject((current) => {
|
||||
const chapters = current.chapters.map((ch, idx) => {
|
||||
if (idx !== activeChapterIndex) return ch;
|
||||
const existing = new Set(ch.chunks.map((c) => `${c.startVerse}-${c.endVerse}`));
|
||||
const existing = new Set(
|
||||
ch.chunks.map((c) => `${c.startVerse}-${c.endVerse}-${c.spilloverEndVerse ?? ''}`),
|
||||
);
|
||||
const newChunks = [];
|
||||
|
||||
ranges.forEach(({ start, end }) => {
|
||||
const key = `${start}-${end}`;
|
||||
ranges.forEach(({ start, end, spilloverEndVerse = null }) => {
|
||||
const key = `${start}-${end}-${spilloverEndVerse ?? ''}`;
|
||||
if (existing.has(key)) {
|
||||
skippedCount += 1;
|
||||
return;
|
||||
@@ -907,6 +923,7 @@ const App = () => {
|
||||
id: makeId(),
|
||||
startVerse: start,
|
||||
endVerse: end,
|
||||
spilloverEndVerse,
|
||||
observation: '',
|
||||
interpretation: '',
|
||||
application: '',
|
||||
@@ -938,7 +955,11 @@ const App = () => {
|
||||
if (!activeChapter) return;
|
||||
const start = Number.parseInt(typedChunkStart, 10);
|
||||
const end = Number.parseInt(typedChunkEnd, 10);
|
||||
const nextEndRaw = typedChunkNextEnd.trim();
|
||||
const nextEnd = nextEndRaw ? Number.parseInt(nextEndRaw, 10) : null;
|
||||
const maxVerse = activeChapter.verses.at(-1)?.number ?? 0;
|
||||
const nextChapter = project?.chapters?.[activeChapterIndex + 1] ?? null;
|
||||
const nextMaxVerse = nextChapter?.verses?.at(-1)?.number ?? 0;
|
||||
|
||||
if (!Number.isInteger(start) || !Number.isInteger(end)) {
|
||||
setStatusMessage('Type a valid start and end verse.');
|
||||
@@ -956,7 +977,30 @@ const App = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const { addedCount } = addChunkRanges([{ start, end }]);
|
||||
if (nextEndRaw) {
|
||||
if (!nextChapter) {
|
||||
setStatusMessage('Add the next chapter first to span chunks across chapters.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2400);
|
||||
return;
|
||||
}
|
||||
if (nextChapter.bookAbbrev !== activeChapter.bookAbbrev) {
|
||||
setStatusMessage('Chapter spanning only works into the next chapter of the same book.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2400);
|
||||
return;
|
||||
}
|
||||
if (!Number.isInteger(nextEnd) || nextEnd < 1 || nextEnd > nextMaxVerse) {
|
||||
setStatusMessage(`Next chapter end must be between 1 and ${nextMaxVerse}.`);
|
||||
window.setTimeout(() => setStatusMessage(''), 2400);
|
||||
return;
|
||||
}
|
||||
if (end !== maxVerse) {
|
||||
setStatusMessage('To span chapters, set End to the last verse of this chapter.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2400);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { addedCount } = addChunkRanges([{ start, end, spilloverEndVerse: nextEnd }]);
|
||||
if (addedCount === 0) {
|
||||
setStatusMessage('That chunk already exists.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2200);
|
||||
@@ -965,6 +1009,7 @@ const App = () => {
|
||||
|
||||
setTypedChunkStart('');
|
||||
setTypedChunkEnd('');
|
||||
setTypedChunkNextEnd('');
|
||||
};
|
||||
|
||||
const addBulkTypedChunks = () => {
|
||||
@@ -977,6 +1022,8 @@ const App = () => {
|
||||
}
|
||||
|
||||
const maxVerse = activeChapter.verses.at(-1)?.number ?? 0;
|
||||
const nextChapter = project?.chapters?.[activeChapterIndex + 1] ?? null;
|
||||
const nextMaxVerse = nextChapter?.verses?.at(-1)?.number ?? 0;
|
||||
const parts = raw
|
||||
.split(/[\n,;]+/)
|
||||
.map((part) => part.trim())
|
||||
@@ -997,7 +1044,7 @@ const App = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const range = /^(\d+)\s*-\s*(\d+)$/.exec(part);
|
||||
const range = /^(\d+)\s*-\s*(\d+)(?:\s*:\s*(\d+))?$/.exec(part);
|
||||
if (!range) {
|
||||
invalidCount += 1;
|
||||
return;
|
||||
@@ -1005,11 +1052,22 @@ const App = () => {
|
||||
|
||||
const start = Number.parseInt(range[1], 10);
|
||||
const end = Number.parseInt(range[2], 10);
|
||||
const spilloverEndVerse = range[3] ? Number.parseInt(range[3], 10) : null;
|
||||
if (start < 1 || end < 1 || start > end || start > maxVerse || end > maxVerse) {
|
||||
invalidCount += 1;
|
||||
return;
|
||||
}
|
||||
validRanges.push({ start, end });
|
||||
if (spilloverEndVerse !== null) {
|
||||
if (!nextChapter
|
||||
|| nextChapter.bookAbbrev !== activeChapter.bookAbbrev
|
||||
|| spilloverEndVerse < 1
|
||||
|| spilloverEndVerse > nextMaxVerse
|
||||
|| end !== maxVerse) {
|
||||
invalidCount += 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
validRanges.push({ start, end, spilloverEndVerse });
|
||||
});
|
||||
|
||||
if (validRanges.length === 0) {
|
||||
@@ -1031,6 +1089,49 @@ const App = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const addClickSpanChunk = () => {
|
||||
if (!activeChapter || rangeStart === null) {
|
||||
setStatusMessage('Click a start verse first, then set next chapter end.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2400);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextChapter = project?.chapters?.[activeChapterIndex + 1] ?? null;
|
||||
if (!nextChapter) {
|
||||
setStatusMessage('Add the next chapter first to span into it.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2400);
|
||||
return;
|
||||
}
|
||||
if (nextChapter.bookAbbrev !== activeChapter.bookAbbrev) {
|
||||
setStatusMessage('Click-based spanning only works into the next chapter of the same book.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2600);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextMaxVerse = nextChapter.verses.at(-1)?.number ?? 0;
|
||||
const spilloverEndVerse = Number.parseInt(clickedSpanNextEnd, 10);
|
||||
if (!Number.isInteger(spilloverEndVerse) || spilloverEndVerse < 1 || spilloverEndVerse > nextMaxVerse) {
|
||||
setStatusMessage(`Next chapter end must be between 1 and ${nextMaxVerse}.`);
|
||||
window.setTimeout(() => setStatusMessage(''), 2400);
|
||||
return;
|
||||
}
|
||||
|
||||
const start = rangeStart;
|
||||
const end = activeChapter.verses.at(-1)?.number ?? start;
|
||||
const { addedCount } = addChunkRanges([{ start, end, spilloverEndVerse }]);
|
||||
if (addedCount === 0) {
|
||||
setStatusMessage('That cross-chapter chunk already exists.');
|
||||
window.setTimeout(() => setStatusMessage(''), 2400);
|
||||
return;
|
||||
}
|
||||
|
||||
setClickedSpanNextEnd('');
|
||||
setRangeStart(null);
|
||||
setRangeEnd(null);
|
||||
setStatusMessage('Cross-chapter chunk added.');
|
||||
window.setTimeout(() => setStatusMessage(''), 1800);
|
||||
};
|
||||
|
||||
const handleVerseClick = (verseNumber, event) => {
|
||||
if (event.shiftKey && rangeStart !== null) {
|
||||
const start = Math.min(rangeStart, verseNumber);
|
||||
@@ -1243,9 +1344,10 @@ const App = () => {
|
||||
|
||||
const suggestGreekWordsForChunk = async (chunkId) => {
|
||||
const chunk = allChunks.find((c) => c.id === chunkId);
|
||||
const chapter = project?.chapters.find((ch) =>
|
||||
const chapterIndex = project?.chapters.findIndex((ch) =>
|
||||
ch.chunks.some((c) => c.id === chunkId)
|
||||
);
|
||||
) ?? -1;
|
||||
const chapter = chapterIndex >= 0 ? project.chapters[chapterIndex] : null;
|
||||
if (!chunk || !chapter) return;
|
||||
if (!NT_BOOK_NUMBER[chapter.bookAbbrev]) {
|
||||
setStatusMessage('Auto-suggest only works for New Testament books.');
|
||||
@@ -1257,11 +1359,19 @@ const App = () => {
|
||||
const [concordance, dict, gloss] = await Promise.all([loadNtConcordance(), loadGreekDict(), loadNtGloss()]);
|
||||
const bookData = concordance[chapter.bookAbbrev] ?? {};
|
||||
const chapterData = bookData[chapter.chapter] ?? {};
|
||||
const nextChapterData = chunkSpansNextChapter(project, chapterIndex, chunk)
|
||||
? bookData[project.chapters[chapterIndex + 1].chapter] ?? {}
|
||||
: {};
|
||||
|
||||
const strongsInRange = new Set();
|
||||
for (let v = chunk.startVerse; v <= chunk.endVerse; v++) {
|
||||
for (const s of chapterData[String(v)] ?? []) strongsInRange.add(s);
|
||||
}
|
||||
if (chunkSpansNextChapter(project, chapterIndex, chunk)) {
|
||||
for (let v = 1; v <= chunk.spilloverEndVerse; v++) {
|
||||
for (const s of nextChapterData[String(v)] ?? []) strongsInRange.add(s);
|
||||
}
|
||||
}
|
||||
|
||||
if (strongsInRange.size === 0) {
|
||||
setStatusMessage("No Strong's data found for this passage.");
|
||||
@@ -1323,9 +1433,10 @@ const App = () => {
|
||||
|
||||
const suggestHebrewWordsForChunk = async (chunkId) => {
|
||||
const chunk = allChunks.find((c) => c.id === chunkId);
|
||||
const chapter = project?.chapters.find((ch) =>
|
||||
const chapterIndex = project?.chapters.findIndex((ch) =>
|
||||
ch.chunks.some((c) => c.id === chunkId)
|
||||
);
|
||||
) ?? -1;
|
||||
const chapter = chapterIndex >= 0 ? project.chapters[chapterIndex] : null;
|
||||
if (!chunk || !chapter) return;
|
||||
if (NT_BOOK_NUMBER[chapter.bookAbbrev]) {
|
||||
setStatusMessage('Hebrew suggestions are for Old Testament passages.');
|
||||
@@ -1336,8 +1447,7 @@ const App = () => {
|
||||
setSuggestingHebrewForChunkId(chunkId);
|
||||
try {
|
||||
const dict = await loadHebrewDict();
|
||||
const versesInChunk = (chapter.verses ?? [])
|
||||
.filter((v) => v.number >= chunk.startVerse && v.number <= chunk.endVerse)
|
||||
const versesInChunk = getChunkVerseEntries(project, chapterIndex, chunk)
|
||||
.map((v) => v.text)
|
||||
.join(' ')
|
||||
.toLowerCase();
|
||||
@@ -1622,20 +1732,16 @@ const App = () => {
|
||||
}),
|
||||
];
|
||||
|
||||
project.chapters.forEach((ch) => {
|
||||
project.chapters.forEach((ch, chapterIndex) => {
|
||||
children.push(new Paragraph({ text: `${ch.book} ${ch.chapter}`, heading: HeadingLevel.HEADING_1 }));
|
||||
ch.chunks.forEach((chunk) => {
|
||||
const scriptureHeading = chunk.startVerse === chunk.endVerse
|
||||
? `${ch.book} ${ch.chapter}:${chunk.startVerse}`
|
||||
: `${ch.book} ${ch.chapter}:${chunk.startVerse}-${chunk.endVerse}`;
|
||||
const scriptureHeading = formatChunkReference(project, chapterIndex, chunk, '-');
|
||||
|
||||
children.push(new Paragraph({ text: scriptureHeading, heading: HeadingLevel.HEADING_2 }));
|
||||
ch.verses
|
||||
.filter((verse) => verse.number >= chunk.startVerse && verse.number <= chunk.endVerse)
|
||||
.forEach((verse) => {
|
||||
getChunkVerseEntries(project, chapterIndex, chunk).forEach((verse) => {
|
||||
children.push(new Paragraph({
|
||||
children: [
|
||||
new TextRun({ text: `${verse.number}. `, bold: true }),
|
||||
new TextRun({ text: `${verse.chapter}:${verse.number}. `, bold: true }),
|
||||
new TextRun({ text: verse.text }),
|
||||
],
|
||||
}));
|
||||
@@ -1773,8 +1879,6 @@ const restoreRemoteProject = async (id) => {
|
||||
setStatusMessage('');
|
||||
};
|
||||
|
||||
const verseLabel = (start, end) => (start === end ? `${start}` : `${start}-${end}`);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared header
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -2081,9 +2185,18 @@ const restoreRemoteProject = async (id) => {
|
||||
rangeStart !== null &&
|
||||
verse.number >= Math.min(rangeStart, rangeEnd) &&
|
||||
verse.number <= Math.max(rangeStart, rangeEnd);
|
||||
const inChunk = activeChapter.chunks.some(
|
||||
const inOwnChapterChunk = activeChapter.chunks.some(
|
||||
(chunk) => verse.number >= chunk.startVerse && verse.number <= chunk.endVerse,
|
||||
);
|
||||
const prevChapter = project?.chapters?.[activeChapterIndex - 1] ?? null;
|
||||
const inPrevChapterSpillover = prevChapter
|
||||
? prevChapter.chunks.some((chunk) =>
|
||||
Number.isInteger(chunk.spilloverEndVerse)
|
||||
&& prevChapter.bookAbbrev === activeChapter.bookAbbrev
|
||||
&& verse.number <= chunk.spilloverEndVerse
|
||||
)
|
||||
: false;
|
||||
const inChunk = inOwnChapterChunk || inPrevChapterSpillover;
|
||||
return (
|
||||
<button
|
||||
key={verse.number}
|
||||
@@ -2138,6 +2251,18 @@ const restoreRemoteProject = async (id) => {
|
||||
className="mt-1 block w-full rounded-xl border border-slate-300 bg-slate-50 px-2.5 py-1.5 text-sm text-slate-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex-1 text-xs text-slate-500">
|
||||
Next ch end (optional)
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max={project?.chapters?.[activeChapterIndex + 1]?.verses?.at(-1)?.number ?? 1}
|
||||
value={typedChunkNextEnd}
|
||||
onChange={(e) => setTypedChunkNextEnd(e.target.value)}
|
||||
placeholder="e.g. 5"
|
||||
className="mt-1 block w-full rounded-xl border border-slate-300 bg-slate-50 px-2.5 py-1.5 text-sm text-slate-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addTypedChunk}
|
||||
@@ -2148,12 +2273,12 @@ const restoreRemoteProject = async (id) => {
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<label className="text-xs text-slate-500">
|
||||
Bulk ranges (comma or new line)
|
||||
Bulk ranges (comma/new line; use `start-end:nextEnd` to span)
|
||||
<textarea
|
||||
rows={2}
|
||||
value={typedChunkBulk}
|
||||
onChange={(e) => setTypedChunkBulk(e.target.value)}
|
||||
placeholder="1-6, 7-12, 13-20"
|
||||
placeholder="1-6, 7-31:5, 6"
|
||||
className="mt-1 block w-full resize-y rounded-xl border border-slate-300 bg-slate-50 px-2.5 py-2 text-sm text-slate-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
||||
/>
|
||||
</label>
|
||||
@@ -2166,6 +2291,37 @@ const restoreRemoteProject = async (id) => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-3">
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-500">Click-based chapter span</p>
|
||||
<p className="mt-1 text-xs text-slate-500">Click a start verse in this chapter, then choose where to end in the next chapter.</p>
|
||||
<div className="mt-2 flex items-end gap-2">
|
||||
<div className="flex-1 text-xs text-slate-500">
|
||||
Start
|
||||
<div className="mt-1 rounded-xl border border-slate-300 bg-slate-50 px-2.5 py-2 text-sm text-slate-900">
|
||||
{rangeStart ?? 'Not selected'}
|
||||
</div>
|
||||
</div>
|
||||
<label className="flex-1 text-xs text-slate-500">
|
||||
Next ch end
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max={project?.chapters?.[activeChapterIndex + 1]?.verses?.at(-1)?.number ?? 1}
|
||||
value={clickedSpanNextEnd}
|
||||
onChange={(e) => setClickedSpanNextEnd(e.target.value)}
|
||||
placeholder="e.g. 5"
|
||||
className="mt-1 block w-full rounded-xl border border-slate-300 bg-slate-50 px-2.5 py-1.5 text-sm text-slate-900 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={addClickSpanChunk}
|
||||
className="rounded-xl border border-slate-300 bg-white px-3 py-2 text-xs font-semibold text-slate-700 transition hover:border-slate-400 hover:bg-slate-50"
|
||||
>
|
||||
Span Into Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 max-h-[520px] overflow-y-auto scrollbar-thin">
|
||||
{activeChapter.chunks.length === 0 ? (
|
||||
<div className="rounded-3xl border border-dashed border-slate-300 bg-white p-4 text-sm text-slate-500">
|
||||
@@ -2183,7 +2339,7 @@ const restoreRemoteProject = async (id) => {
|
||||
className="mb-3 w-full text-left"
|
||||
>
|
||||
<p className="text-sm font-semibold text-slate-900">
|
||||
{activeChapter.book} {activeChapter.chapter}:{verseLabel(chunk.startVerse, chunk.endVerse)}
|
||||
{formatChunkReference(project, activeChapterIndex, chunk, '–')}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-slate-600 truncate">
|
||||
{activeChapter.verses.find((v) => v.number === chunk.startVerse)?.text || ''}
|
||||
@@ -2264,7 +2420,7 @@ const restoreRemoteProject = async (id) => {
|
||||
<h2 className="mt-2 text-xl font-semibold text-slate-900">{allChunks.length} chunks</h2>
|
||||
</div>
|
||||
<div className="space-y-6 max-h-[calc(100vh-260px)] overflow-y-auto scrollbar-thin">
|
||||
{project?.chapters.map((ch) => (
|
||||
{project?.chapters.map((ch, chapterIndex) => (
|
||||
<div key={`${ch.bookAbbrev}-${ch.chapter}`}>
|
||||
{project.chapters.length > 1 && (
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-widest text-slate-400">
|
||||
@@ -2297,7 +2453,7 @@ const restoreRemoteProject = async (id) => {
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-sm font-semibold text-slate-900">
|
||||
{ch.book} {ch.chapter}:{verseLabel(chunk.startVerse, chunk.endVerse)}
|
||||
{formatChunkReference(project, chapterIndex, chunk, '–')}
|
||||
</span>
|
||||
{hasContent ? <span className="text-emerald-600">✓</span> : null}
|
||||
</div>
|
||||
@@ -2321,7 +2477,7 @@ const restoreRemoteProject = async (id) => {
|
||||
<p className="text-sm font-medium text-slate-500">Chunk editor</p>
|
||||
<h2 className="mt-2 text-xl font-semibold text-slate-900">
|
||||
{selectedChunk
|
||||
? `Section ${verseLabel(selectedChunk.startVerse, selectedChunk.endVerse)}`
|
||||
? `Section ${formatChunkReference(project, selectedChunkChapterIndex, selectedChunk, '–')}`
|
||||
: 'Select a chunk'}
|
||||
</h2>
|
||||
</div>
|
||||
@@ -2342,15 +2498,14 @@ const restoreRemoteProject = async (id) => {
|
||||
<p className="text-xs text-slate-500">Read-only passage for the selected chunk.</p>
|
||||
</div>
|
||||
<span className="rounded-full bg-slate-100 px-3 py-1 text-xs uppercase tracking-[0.18em] text-slate-500">
|
||||
{selectedChunkChapter?.book} {selectedChunkChapter?.chapter}:{verseLabel(selectedChunk.startVerse, selectedChunk.endVerse)}
|
||||
{formatChunkReference(project, selectedChunkChapterIndex, selectedChunk, '–')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-3 font-serif text-slate-800">
|
||||
{selectedChunkChapter?.verses
|
||||
.filter((v) => v.number >= selectedChunk.startVerse && v.number <= selectedChunk.endVerse)
|
||||
{selectedChunkVerses
|
||||
.map((verse) => (
|
||||
<p key={verse.number} className="leading-relaxed">
|
||||
<span className="font-semibold text-slate-700">{verse.number}.</span> {verse.text}
|
||||
<p key={`${verse.chapter}-${verse.number}`} className="leading-relaxed">
|
||||
<span className="font-semibold text-slate-700">{verse.chapter}:{verse.number}.</span> {verse.text}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user