Reading plans: integrated into the Reader, redesigned, with per-plan translation
Build and Push Docker Image / build-and-push (push) Failing after 19s
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:
1 parent
c11b6a60bf
commit
b1bc1c8ea4
17 files changed
+1205
-863
No files matched your search
@@ -25,7 +25,9 @@ const ctx = {
|
||||
handlePlayReaderAudio: noop, handleToggleReaderAudioPause: noop, handleStopBookAudio: noop,
|
||||
handleSeekAudio: noop, handleSeekAudioTo: noop,
|
||||
readerInkByPage: {}, updateReaderPageInk: noop, readerTextHighlights: [], addTextHighlight: noop, removeTextHighlight: noop,
|
||||
planProgress: [], toggleDay: noop,
|
||||
planProgress: [], toggleDay: vi.fn(), toggleChapterRead: vi.fn(), readingActivity: [],
|
||||
readerPlan: null, setReaderPlan: vi.fn(), openPlanReading: vi.fn(), studyReadingPlanDay: vi.fn(), studyBuildBusy: false,
|
||||
setReaderBookAbbrev: vi.fn(), setReaderChapter: vi.fn(),
|
||||
};
|
||||
vi.mock('../context/AppContext.js', () => ({ useApp: () => ctx }));
|
||||
|
||||
@@ -50,3 +52,72 @@ test('tapping a verse selects it', () => {
|
||||
fireEvent.click(screen.getByText(/This man came to Jesus/));
|
||||
expect(ctx.setReaderSelectedVerse).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
// --- reading plan integration -------------------------------------------------
|
||||
import { getPlanSchedule, findDayIndexForRef } from '../utils/readingPlans.js';
|
||||
|
||||
const schedule = getPlanSchedule('gospels-30');
|
||||
const dayIndex = findDayIndexForRef(schedule, 'JHN', 3);
|
||||
const refs = schedule[dayIndex];
|
||||
const keys = refs.map((r) => `${r.book}.${r.chapter}`);
|
||||
const position = keys.indexOf('JHN.3') + 1;
|
||||
const isLast = position === refs.length;
|
||||
|
||||
const withPlan = (over = {}) => {
|
||||
ctx.planProgress = [{ id: 'p1', planId: 'gospels-30', startedAt: Date.now(), completedDays: [], chaptersRead: {}, completedAt: null, translation: 'ESV', ...over }];
|
||||
ctx.readerPlan = { progressId: 'p1', dayIndex };
|
||||
[ctx.setReaderPlan, ctx.toggleChapterRead, ctx.toggleDay, ctx.setReaderChapter, ctx.setReaderBookAbbrev].forEach((f) => f.mockClear());
|
||||
ctx.setReaderTranslation = vi.fn();
|
||||
};
|
||||
const stripButton = () => screen.getAllByText(new RegExp(`Day ${dayIndex + 1} *$`))[0];
|
||||
|
||||
test('in a plan: strip shows the day, a dot per chapter, and the plan translation', () => {
|
||||
withPlan();
|
||||
render(<ReaderPage />);
|
||||
expect(screen.getAllByLabelText(/Go to chapter/)).toHaveLength(refs.length);
|
||||
expect(screen.getByText(`${position} of ${refs.length}`)).toBeTruthy();
|
||||
});
|
||||
|
||||
test('end-of-day card ticks the current chapter and moves on', () => {
|
||||
// every other chapter of the day already read, so finishing this one completes the day
|
||||
withPlan({ chaptersRead: { [dayIndex]: keys.filter((k) => k !== 'JHN.3') } });
|
||||
render(<ReaderPage />);
|
||||
fireEvent.click(screen.getByText(isLast ? /Mark read · Day/ : /^Next: /));
|
||||
expect(ctx.toggleChapterRead).toHaveBeenCalledWith('p1', dayIndex, 'JHN.3', keys, 30, true);
|
||||
if (!isLast) expect(ctx.setReaderChapter).toHaveBeenCalledWith(refs[position].chapter);
|
||||
});
|
||||
|
||||
test('"mark read" on the last chapter completes the whole day even if earlier chapters were skipped', () => {
|
||||
const last = refs.at(-1);
|
||||
expect(refs.length).toBeGreaterThan(1);
|
||||
withPlan(); // nothing read yet
|
||||
ctx.readerBookAbbrev = last.book;
|
||||
ctx.readerChapter = last.chapter;
|
||||
try {
|
||||
render(<ReaderPage />);
|
||||
fireEvent.click(screen.getByText(/Mark read · Day/));
|
||||
expect(ctx.toggleDay).toHaveBeenCalledWith('p1', dayIndex, 30);
|
||||
} finally {
|
||||
ctx.readerBookAbbrev = 'JHN';
|
||||
ctx.readerChapter = 3;
|
||||
}
|
||||
});
|
||||
|
||||
test('plan sheet lists the day, changes the plan translation, and leaves the plan', () => {
|
||||
withPlan();
|
||||
render(<ReaderPage />);
|
||||
fireEvent.click(stripButton());
|
||||
expect(screen.getByText('Mark the whole day read')).toBeTruthy();
|
||||
fireEvent.change(screen.getByDisplayValue(/^ESV/), { target: { value: 'NLT' } });
|
||||
expect(ctx.setReaderTranslation).toHaveBeenCalledWith('NLT');
|
||||
fireEvent.click(screen.getByText(/Leave plan/));
|
||||
expect(ctx.setReaderPlan).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
test('outside a plan but on a plan chapter: offers to open the plan', () => {
|
||||
withPlan();
|
||||
ctx.readerPlan = null;
|
||||
render(<ReaderPage />);
|
||||
fireEvent.click(screen.getByText('Open plan'));
|
||||
expect(ctx.setReaderPlan).toHaveBeenCalledWith({ progressId: 'p1', dayIndex });
|
||||
});
|
||||
Reference in new issue
Block a user