Add project dashboard, OIA notes, cross-references, and multi-chapter support

- Home page (project list/dashboard): projects stored in localStorage index,
  Resume/Delete per project, sorted by last-edited date
- OIA notes: replace single `notes` field with observation/interpretation/application
  textareas on each chunk following inductive study method
- Cross-references: string tag array per chunk with inline add/remove UI,
  included in HTML and DOCX exports and Claude prompt
- Multi-chapter projects: chapters array on project, chapter tabs in setup view,
  chapter-grouped sidebar in study view, global chunk navigation across chapters
- Migration: migrateChunk/migrateProject exported helpers auto-upgrade old
  localStorage entries on startup
- Tests updated: new baseProject fixture, 11 additional tests (99 total)
This commit is contained in:
Claude
2026-05-27 12:28:41 +00:00
parent 00d9dbd7c8
commit dff6ec2983
3 changed files with 1474 additions and 841 deletions
+25 -2
View File
@@ -62,6 +62,7 @@ afterEach(() => {
async function loadChapter() {
const user = userEvent.setup();
render(<App />);
await user.click(screen.getAllByRole('button', { name: /new project/i })[0]);
await user.click(screen.getByRole('button', { name: /load chapter/i }));
await screen.findByText(/Scripture & Chunks/i);
return user;
@@ -97,14 +98,33 @@ function findChunkCounter(n, total) {
// Initial render
// ---------------------------------------------------------------------------
describe('Initial render', () => {
test('shows the project setup form', () => {
test('shows the home page with "My Studies" heading', () => {
render(<App />);
expect(screen.getByText('My Studies')).toBeInTheDocument();
});
test('shows "No projects yet" when storage is empty', () => {
render(<App />);
expect(screen.getByText(/no projects yet/i)).toBeInTheDocument();
});
test('shows "New Project" button on home page', () => {
render(<App />);
expect(screen.getAllByRole('button', { name: /new project/i }).length).toBeGreaterThan(0);
});
test('clicking "New Project" shows the project setup form', async () => {
const user = userEvent.setup();
render(<App />);
await user.click(screen.getAllByRole('button', { name: /new project/i })[0]);
expect(screen.getByText('Project Setup')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /load chapter/i })).toBeInTheDocument();
});
test('shows translation, book, and chapter inputs', () => {
test('setup form shows translation, book, and chapter inputs', async () => {
const user = userEvent.setup();
render(<App />);
await user.click(screen.getAllByRole('button', { name: /new project/i })[0]);
expect(screen.getByText('Translation')).toBeInTheDocument();
expect(screen.getByText('Book')).toBeInTheDocument();
expect(screen.getByText('Chapter')).toBeInTheDocument();
@@ -139,6 +159,7 @@ describe('Loading a chapter', () => {
}));
const user = userEvent.setup();
render(<App />);
await user.click(screen.getAllByRole('button', { name: /new project/i })[0]);
await user.click(screen.getByRole('button', { name: /load chapter/i }));
await screen.findByText(/unable to load chapter/i);
});
@@ -147,8 +168,10 @@ describe('Loading a chapter', () => {
vi.stubGlobal('fetch', buildFetchMock({ chapterData: { chapter: { content: [] } } }));
const user = userEvent.setup();
render(<App />);
await user.click(screen.getAllByRole('button', { name: /new project/i })[0]);
await user.click(screen.getByRole('button', { name: /load chapter/i }));
await screen.findByText(/invalid bible data/i);
});
});