Redesign the study side: Outline, Study, Script and Export stages
Build and Push Docker Image / build-and-push (push) Successful in 4m15s
Build and Push Docker Image / build-and-push (push) Successful in 4m15s
Studies now move through a stage bar in the reader's themes. Outline splits chunks by clicking between verses (join merges notes) and suggests chunks from section headings. Study is three panes: outline, passage with an Observe / Interpret / Apply accordion, and commentary / word study / cross-refs / people & places for the chunk; original words are added by clicking them in the interlinear. Script drafts an outline from notes; Export holds files and the share link. Home, Import and the Pronunciation guide are restyled. Study projects are BSB only. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b1bc1c8ea4
commit
31abee0695
31 files changed
+1911
-2460
No files matched your search
+33
-90
@@ -1,15 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { useApp } from '../context/AppContext.js';
|
||||
import AuthStatus from '../components/AuthStatus.jsx';
|
||||
import { useAvailableTranslations } from '../hooks/useAvailableTranslations.js';
|
||||
import PageShell from '../components/study/PageShell.jsx';
|
||||
import { bookOptions, makeId, parseBibleChapter } from '../utils/bibleUtils.jsx';
|
||||
import { parseEpisodeListDocx, parseEpisodePassage } from '../utils/importParsers.js';
|
||||
|
||||
export default function ImportPage() {
|
||||
const { setCurrentPage, setProject } = useApp();
|
||||
const availableTranslations = useAvailableTranslations();
|
||||
const [importBookAbbrev, setImportBookAbbrev] = useState(bookOptions[0].abbrev);
|
||||
const [importTranslation, setImportTranslation] = useState('BSB');
|
||||
const importTranslation = 'BSB'; // studies use the Berean Standard Bible
|
||||
const [importTitle, setImportTitle] = useState('');
|
||||
const [importPreview, setImportPreview] = useState(null); // parsed episode specs, before fetching chapters
|
||||
const [importBusy, setImportBusy] = useState(false);
|
||||
@@ -149,31 +147,14 @@ export default function ImportPage() {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 text-slate-900">
|
||||
<header className="border-b border-slate-200 bg-slate-900 text-white shadow-sm">
|
||||
<div className="mx-auto flex max-w-7xl items-center justify-between px-4 py-5 sm:px-6 lg:px-8">
|
||||
<div>
|
||||
<p className="text-sm uppercase tracking-[0.24em] text-slate-300">Bible Study Project</p>
|
||||
<h1 className="mt-2 text-2xl font-semibold">Import Session List</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCurrentPage('home')}
|
||||
className="rounded-xl border border-slate-500 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-slate-700"
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
<AuthStatus />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
const field = 'mt-1 w-full rounded-xl border border-[var(--r-line)] bg-[var(--r-bg)] px-3 py-2 text-sm text-[var(--r-text)] focus:outline-none focus:ring-2 focus:ring-[var(--r-line)]';
|
||||
|
||||
<main className="mx-auto max-w-4xl px-4 py-8 sm:px-6 lg:px-8 space-y-6">
|
||||
<section className="rounded-3xl border border-slate-200 bg-white p-8 shadow-panel space-y-5">
|
||||
<div className="rounded-2xl border border-slate-200 bg-slate-50 p-4 text-sm text-slate-600 space-y-2">
|
||||
<p className="font-semibold text-slate-700">How this works</p>
|
||||
return (
|
||||
<PageShell title="Import Session List" onBack={() => setCurrentPage('home')} maxWidth="max-w-4xl">
|
||||
<main className="mx-auto max-w-3xl space-y-6 px-4 py-8">
|
||||
<section className="space-y-5 rounded-2xl border border-[var(--r-line)] bg-[var(--r-bar)] p-6">
|
||||
<div className="space-y-2 rounded-xl bg-[var(--r-bg)] p-4 text-sm text-[var(--r-sub)]">
|
||||
<p className="font-medium text-[var(--r-text)]">How this works</p>
|
||||
<p>
|
||||
This is a shortcut for setting up a multi-part study — a teaching series, sermon series, class
|
||||
curriculum, or podcast — all at once, instead of building each chapter and chunk by hand.
|
||||
@@ -182,100 +163,62 @@ export default function ImportPage() {
|
||||
Upload a .docx containing a table with three columns: session number, title, and passage
|
||||
(e.g. <span className="font-mono text-xs">1:1–2</span>). Each row becomes one chunk, grouped
|
||||
automatically by chapter. If you don't have a document like this, just use{' '}
|
||||
<span className="font-semibold">+ New Project</span> on the home page instead — this import
|
||||
<span className="font-medium text-[var(--r-text)]">+ New Project</span> on the home page instead — this import
|
||||
step is entirely optional.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<label className="block text-sm font-medium text-slate-700">
|
||||
Book
|
||||
<select
|
||||
value={importBookAbbrev}
|
||||
onChange={(e) => setImportBookAbbrev(e.target.value)}
|
||||
className="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
||||
>
|
||||
{bookOptions.map((b) => (
|
||||
<option key={b.abbrev} value={b.abbrev}>{b.name}</option>
|
||||
))}
|
||||
<label className="block text-sm">Book
|
||||
<select value={importBookAbbrev} onChange={(e) => setImportBookAbbrev(e.target.value)} className={field}>
|
||||
{bookOptions.map((b) => <option key={b.abbrev} value={b.abbrev}>{b.name}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label className="block text-sm font-medium text-slate-700">
|
||||
Translation
|
||||
<select
|
||||
value={importTranslation}
|
||||
onChange={(e) => setImportTranslation(e.target.value)}
|
||||
className="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
||||
>
|
||||
{availableTranslations.map((t) => (
|
||||
<option key={t} value={t}>{t}</option>
|
||||
))}
|
||||
</select>
|
||||
<label className="block text-sm">Project title
|
||||
<input type="text" value={importTitle} onChange={(e) => setImportTitle(e.target.value)}
|
||||
placeholder={`${bookOptions.find((b) => b.abbrev === importBookAbbrev)?.name ?? ''} Sessions`} className={field} />
|
||||
</label>
|
||||
</div>
|
||||
<p className="text-xs text-[var(--r-sub)]">Text: Berean Standard Bible (BSB).</p>
|
||||
|
||||
<label className="block text-sm font-medium text-slate-700">
|
||||
Project title
|
||||
<input
|
||||
type="text"
|
||||
value={importTitle}
|
||||
onChange={(e) => setImportTitle(e.target.value)}
|
||||
placeholder={`${bookOptions.find((b) => b.abbrev === importBookAbbrev)?.name ?? ''} Sessions`}
|
||||
className="mt-1 w-full rounded-xl border border-slate-300 px-3 py-2 text-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
|
||||
/>
|
||||
<label className="block text-sm">Session list (.docx)
|
||||
<input type="file" accept=".docx" onChange={(e) => handleImportFileChange(e.target.files?.[0] ?? null)}
|
||||
className="mt-1 block w-full text-sm text-[var(--r-sub)] file:mr-4 file:rounded-lg file:border-0 file:bg-[var(--r-text)] file:px-4 file:py-2 file:text-sm file:text-[var(--r-bg)]" />
|
||||
</label>
|
||||
|
||||
<label className="block text-sm font-medium text-slate-700">
|
||||
Session list (.docx)
|
||||
<input
|
||||
type="file"
|
||||
accept=".docx"
|
||||
onChange={(e) => handleImportFileChange(e.target.files?.[0] ?? null)}
|
||||
className="mt-1 block w-full text-sm text-slate-600 file:mr-4 file:rounded-xl file:border-0 file:bg-slate-700 file:px-4 file:py-2 file:text-sm file:font-semibold file:text-white hover:file:bg-slate-600"
|
||||
/>
|
||||
</label>
|
||||
|
||||
{importBusy && <p className="text-sm text-slate-500">Working…</p>}
|
||||
{importError && <p className="text-sm text-rose-600">{importError}</p>}
|
||||
{importBusy && <p className="text-sm text-[var(--r-sub)]">Working…</p>}
|
||||
{importError && <p className="text-sm text-red-600">{importError}</p>}
|
||||
|
||||
{importPreview && !importBusy && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm font-semibold text-slate-700">
|
||||
<p className="text-sm font-medium">
|
||||
{importPreview.length} session{importPreview.length === 1 ? '' : 's'} found
|
||||
{' · '}
|
||||
{importPreview.filter((s) => s.parsed && s.parsed !== 'invalid').length} with passages
|
||||
</p>
|
||||
<div className="max-h-80 overflow-y-auto rounded-xl border border-slate-200">
|
||||
<div className="max-h-80 overflow-y-auto rounded-xl border border-[var(--r-line)]">
|
||||
<table className="w-full text-sm">
|
||||
<tbody>
|
||||
{importPreview.map((spec) => (
|
||||
<tr key={spec.episodeNumber} className="border-b border-slate-100 last:border-0">
|
||||
<td className="px-3 py-1.5 text-slate-500">#{spec.episodeNumber}</td>
|
||||
<td className="px-3 py-1.5 text-slate-900">{spec.title}</td>
|
||||
<td className="px-3 py-1.5 text-right text-slate-500">
|
||||
<tr key={spec.episodeNumber} className="border-b border-[var(--r-line)] last:border-0">
|
||||
<td className="px-3 py-1.5 text-[var(--r-sub)]">#{spec.episodeNumber}</td>
|
||||
<td className="px-3 py-1.5">{spec.title}</td>
|
||||
<td className="px-3 py-1.5 text-right text-[var(--r-sub)]">
|
||||
{spec.parsed === 'invalid'
|
||||
? <span className="text-rose-600">unrecognized — skipped</span>
|
||||
: spec.parsed
|
||||
? spec.passage
|
||||
: <span className="text-slate-400">marker (no passage)</span>}
|
||||
? <span className="text-red-600">unrecognized — skipped</span>
|
||||
: spec.parsed ? spec.passage : <span>marker (no passage)</span>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={runEpisodeImport}
|
||||
disabled={importBusy}
|
||||
className="rounded-xl bg-slate-900 px-6 py-2.5 text-sm font-semibold text-white transition hover:bg-slate-800 disabled:opacity-50"
|
||||
>
|
||||
Create Project from Import
|
||||
</button>
|
||||
<button type="button" onClick={runEpisodeImport} disabled={importBusy}
|
||||
className="rounded-full bg-[var(--r-accent)] px-6 py-2.5 text-sm font-medium text-[var(--r-on)] hover:opacity-90 disabled:opacity-50">Create Project from Import</button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user