Reading plans: integrated into the Reader, redesigned, with per-plan translation
Build and Push Docker Image / build-and-push (push) Failing after 19s

Reader: plan strip with a dot per chapter, plan sheet (day picker, chapter
ticks, translation, streak, other plans), end-of-day card that steps through
the day and rolls into the next, and chapter-level auto-marking.
Each plan remembers its own translation (BSB, ESV, NLT, CSB, NASB, NET) and the
Reader follows it while a plan is open. Plans home redesigned in the reader's
themes with a Continue card, in-progress plans, a filterable catalog, and
sheets for details and the custom-plan builder.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-09-29 09:47:51 -04:00
1 parent c11b6a60bf
commit b1bc1c8ea4
17 files changed
+1205 -863

No files matched your search

+33 -2
View File
@@ -580,15 +580,46 @@ describe('Home page', () => {
expect(await screen.findByDisplayValue('Title')).toBeInTheDocument();
});
test('Continue reading opens the Reader inside the plan, in the plan\'s own translation', async () => {
seedProject();
localStorage.setItem('readingPlanProgress', JSON.stringify({
progress: [{ id: 'rp1', planId: 'gospels-30', startedAt: Date.now(), completedDays: [], chaptersRead: {}, completedAt: null, lastActivityAt: Date.now(), translation: 'ESV' }],
customPlans: [], activity: [],
}));
const esvChapter = { translation: 'ESV', verses: [{ number: 1, text: 'In the beginning was the Word.' }], copyright: '' };
const fetchMock = vi.fn((url) => {
if (String(url).startsWith('/api/bible/ESV/')) return Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(esvChapter) });
return buildFetchMock()(url);
});
vi.stubGlobal('fetch', fetchMock);
const user = userEvent.setup();
await renderApp();
await user.click(screen.getByRole('button', { name: /reading plans/i }));
await user.click(await screen.findByRole('button', { name: /continue reading/i }));
// The plan strip is up, on the plan's translation, and the chapter came from the ESV route
expect(await screen.findByText(/In the beginning was the Word/)).toBeInTheDocument();
expect(screen.getAllByText(/The Gospels in 30 Days · Day 1/).length).toBeGreaterThan(0);
expect(screen.getByText('ESV')).toBeInTheDocument(); // plan strip shows the plan's translation
expect(fetchMock.mock.calls.some(([u]) => String(u).startsWith('/api/bible/ESV/MAT/1'))).toBe(true);
// Leaving the reader ends the plan context: the reader's own translation applies again
await user.click(screen.getByRole('button', { name: /back to studies/i }));
await user.click(screen.getByRole('button', { name: /read bible/i }));
expect(await screen.findByText(/Matthew 1 · BSB/)).toBeInTheDocument();
});
test('a reading plan can be started from the Reading Plans page', async () => {
seedProject(); // the nav button shows once there's at least one project
const user = userEvent.setup();
await renderApp();
await user.click(screen.getByRole('button', { name: /reading plans/i }));
await user.click((await screen.findAllByRole('button', { name: /^start plan$/i }))[0]);
// Tap a plan in the catalog, pick a translation for it, and start it.
await user.click(await screen.findByRole('button', { name: /The Gospels in 30 Days/ }));
await user.selectOptions(screen.getByRole('combobox'), 'ESV');
await user.click(screen.getByRole('button', { name: /^start plan$/i }));
await waitFor(() => {
const stored = JSON.parse(localStorage.getItem('readingPlanProgress'));
expect(stored?.progress?.[0]?.planId).toBeTruthy();
expect(stored?.progress?.[0]?.planId).toBe('gospels-30');
expect(stored?.progress?.[0]?.translation).toBe('ESV');
});
});
});