@youversion/platform-react-hooks 1.16.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
1
 
2
- > @youversion/platform-react-hooks@1.16.0 build /home/runner/work/platform-sdk-react/platform-sdk-react/packages/hooks
2
+ > @youversion/platform-react-hooks@1.18.0 build /home/runner/work/platform-sdk-react/platform-sdk-react/packages/hooks
3
3
  > tsc -p tsconfig.build.json
4
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @youversion/platform-react-hooks
2
2
 
3
+ ## 1.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b8c6e1b: In our BibleCard component, we've added an error UI to make it more clear when an error has occurred fetching the Bible verse.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [b8c6e1b]
12
+ - @youversion/platform-core@1.18.0
13
+
14
+ ## 1.17.1
15
+
16
+ ### Patch Changes
17
+
18
+ - a7100fd: We've added support for footnotes in Bible book introduction chapters. This is a rare occurance, but an example can be found in Joshua's introduction chapter when using the TPT Bible Version
19
+ - Updated dependencies [a7100fd]
20
+ - @youversion/platform-core@1.17.1
21
+
22
+ ## 1.17.0
23
+
24
+ ### Minor Changes
25
+
26
+ - c3d673e: added error ui for faild verses
27
+
28
+ ### Patch Changes
29
+
30
+ - a5f91bf: Add cross-book chapter navigation to Bible Reader toolbar with prev/next buttons, intro chapter support, and accessible aria-labels
31
+ - Updated dependencies [a5f91bf]
32
+ - Updated dependencies [c3d673e]
33
+ - @youversion/platform-core@1.17.0
34
+
3
35
  ## 1.16.0
4
36
 
5
37
  ### Minor Changes
@@ -7,16 +7,8 @@ interface UseChapterNavigationResult {
7
7
  isLoading: boolean;
8
8
  }
9
9
  /**
10
- * Provides navigation functionality for chapters within a book, allowing the user to move
11
- * to the previous or next chapter, as well as access additional chapter navigation metadata.
12
- *
13
- * @return {UseChapterNavigationResult} An object containing properties and methods for chapter navigation:
14
- * - `canNavigatePrevious` (boolean): Indicates whether navigating to the previous chapter is possible.
15
- * - `canNavigateNext` (boolean): Indicates whether navigating to the next chapter is possible.
16
- * - `navigateToPrevious` (function): Moves to the previous chapter if possible.
17
- * - `navigateToNext` (function): Moves to the next chapter if possible.
18
- * - `currentChapterIndex` (number): The index of the current chapter within the list of chapters.
19
- * - `isLoading` (boolean): Whether the chapter data is still loading.
10
+ * Provides navigation functionality for chapters across book boundaries,
11
+ * including intro chapter support.
20
12
  */
21
13
  export declare function useChapterNavigation(): UseChapterNavigationResult;
22
14
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"useChapterNavigation.d.ts","sourceRoot":"","sources":["../src/useChapterNavigation.ts"],"names":[],"mappings":"AAGA,UAAU,0BAA0B;IAClC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,eAAe,EAAE,OAAO,CAAC;IACzB,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,IAAI,0BAA0B,CAoDjE"}
1
+ {"version":3,"file":"useChapterNavigation.d.ts","sourceRoot":"","sources":["../src/useChapterNavigation.ts"],"names":[],"mappings":"AAMA,UAAU,0BAA0B;IAClC,mBAAmB,EAAE,OAAO,CAAC;IAC7B,eAAe,EAAE,OAAO,CAAC;IACzB,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,0BAA0B,CAsDjE"}
@@ -1,52 +1,54 @@
1
+ 'use client';
2
+ import { getAdjacentChapter } from '@youversion/platform-core';
1
3
  import { useReaderContext } from './context/ReaderContext';
2
- import { useChapters } from './useChapters';
4
+ import { useBooks } from './useBooks';
3
5
  /**
4
- * Provides navigation functionality for chapters within a book, allowing the user to move
5
- * to the previous or next chapter, as well as access additional chapter navigation metadata.
6
- *
7
- * @return {UseChapterNavigationResult} An object containing properties and methods for chapter navigation:
8
- * - `canNavigatePrevious` (boolean): Indicates whether navigating to the previous chapter is possible.
9
- * - `canNavigateNext` (boolean): Indicates whether navigating to the next chapter is possible.
10
- * - `navigateToPrevious` (function): Moves to the previous chapter if possible.
11
- * - `navigateToNext` (function): Moves to the next chapter if possible.
12
- * - `currentChapterIndex` (number): The index of the current chapter within the list of chapters.
13
- * - `isLoading` (boolean): Whether the chapter data is still loading.
6
+ * Provides navigation functionality for chapters across book boundaries,
7
+ * including intro chapter support.
14
8
  */
15
9
  export function useChapterNavigation() {
16
- const { currentChapter, currentVersion, currentBook, setChapter } = useReaderContext();
17
- const bookIdentifier = currentBook?.id ?? '';
18
- const { chapters, loading: chaptersLoading } = useChapters(currentVersion?.id ?? 0, bookIdentifier, { enabled: Boolean(bookIdentifier && currentVersion?.id) });
19
- const currentChapterIndex = chapters?.data.findIndex((c) => c.id === currentChapter.id) ?? -1;
20
- const canNavigatePrevious = Boolean(!chaptersLoading && chapters?.data && currentChapterIndex !== -1 && currentChapterIndex > 0);
21
- const canNavigateNext = Boolean(!chaptersLoading &&
22
- chapters?.data &&
23
- currentChapterIndex !== -1 &&
24
- currentChapterIndex < chapters.data.length - 1);
25
- const navigateToPrevious = () => {
26
- if (canNavigatePrevious && chapters?.data) {
27
- const previousChapter = chapters.data[currentChapterIndex - 1];
28
- if (previousChapter) {
29
- // Use the chapter as-is since it already has all required fields
30
- setChapter(previousChapter);
31
- }
32
- }
33
- };
34
- const navigateToNext = () => {
35
- if (canNavigateNext && chapters?.data) {
36
- const nextChapter = chapters.data[currentChapterIndex + 1];
37
- if (nextChapter) {
38
- // Use the chapter as-is since it already has all required fields
39
- setChapter(nextChapter);
40
- }
10
+ const { currentChapter, currentVersion, currentBook, setChapter, setBook } = useReaderContext();
11
+ const { books, loading: booksLoading } = useBooks(currentVersion?.id ?? 0, {
12
+ enabled: Boolean(currentVersion?.id),
13
+ });
14
+ const booksData = books?.data ?? [];
15
+ const bookId = currentBook?.id ?? '';
16
+ const chapterId = currentChapter?.id ?? '';
17
+ const nextResult = getAdjacentChapter(booksData, bookId, chapterId, 'next');
18
+ const prevResult = getAdjacentChapter(booksData, bookId, chapterId, 'previous');
19
+ const canNavigateNext = !booksLoading && nextResult !== null;
20
+ const canNavigatePrevious = !booksLoading && prevResult !== null;
21
+ const currentChapterIndex = currentBook?.chapters?.findIndex((c) => c.id === currentChapter?.id) ?? -1;
22
+ const navigate = (result) => {
23
+ if (!result || !booksData.length)
24
+ return;
25
+ const targetBook = booksData.find((b) => b.id === result.bookId);
26
+ if (!targetBook)
27
+ return;
28
+ const targetChapter = targetBook.chapters?.find((c) => c.id === result.chapterId) ??
29
+ (targetBook.intro?.id === result.chapterId
30
+ ? {
31
+ id: targetBook.intro.id,
32
+ passage_id: targetBook.intro.passage_id,
33
+ title: targetBook.intro.title,
34
+ }
35
+ : undefined);
36
+ if (!targetChapter)
37
+ return;
38
+ if (targetBook.id !== currentBook?.id) {
39
+ setBook(targetBook);
41
40
  }
41
+ setChapter(targetChapter);
42
42
  };
43
+ const navigateToNext = () => navigate(nextResult);
44
+ const navigateToPrevious = () => navigate(prevResult);
43
45
  return {
44
46
  canNavigatePrevious,
45
47
  canNavigateNext,
46
48
  navigateToPrevious,
47
49
  navigateToNext,
48
50
  currentChapterIndex,
49
- isLoading: chaptersLoading,
51
+ isLoading: booksLoading,
50
52
  };
51
53
  }
52
54
  //# sourceMappingURL=useChapterNavigation.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useChapterNavigation.js","sourceRoot":"","sources":["../src/useChapterNavigation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAW5C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,EAAE,cAAc,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,GAAG,gBAAgB,EAAE,CAAC;IAEvF,MAAM,cAAc,GAAG,WAAW,EAAE,EAAE,IAAI,EAAE,CAAC;IAE7C,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,eAAe,EAAE,GAAG,WAAW,CACxD,cAAc,EAAE,EAAE,IAAI,CAAC,EACvB,cAAc,EACd,EAAE,OAAO,EAAE,OAAO,CAAC,cAAc,IAAI,cAAc,EAAE,EAAE,CAAC,EAAE,CAC3D,CAAC;IAEF,MAAM,mBAAmB,GAAG,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,cAAc,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAE9F,MAAM,mBAAmB,GAAG,OAAO,CACjC,CAAC,eAAe,IAAI,QAAQ,EAAE,IAAI,IAAI,mBAAmB,KAAK,CAAC,CAAC,IAAI,mBAAmB,GAAG,CAAC,CAC5F,CAAC;IAEF,MAAM,eAAe,GAAG,OAAO,CAC7B,CAAC,eAAe;QACd,QAAQ,EAAE,IAAI;QACd,mBAAmB,KAAK,CAAC,CAAC;QAC1B,mBAAmB,GAAG,QAAQ,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CACjD,CAAC;IAEF,MAAM,kBAAkB,GAAG,GAAG,EAAE;QAC9B,IAAI,mBAAmB,IAAI,QAAQ,EAAE,IAAI,EAAE,CAAC;YAC1C,MAAM,eAAe,GAAG,QAAQ,CAAC,IAAI,CAAC,mBAAmB,GAAG,CAAC,CAAC,CAAC;YAC/D,IAAI,eAAe,EAAE,CAAC;gBACpB,iEAAiE;gBACjE,UAAU,CAAC,eAAe,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,MAAM,cAAc,GAAG,GAAG,EAAE;QAC1B,IAAI,eAAe,IAAI,QAAQ,EAAE,IAAI,EAAE,CAAC;YACtC,MAAM,WAAW,GAAG,QAAQ,CAAC,IAAI,CAAC,mBAAmB,GAAG,CAAC,CAAC,CAAC;YAC3D,IAAI,WAAW,EAAE,CAAC;gBAChB,iEAAiE;gBACjE,UAAU,CAAC,WAAW,CAAC,CAAC;YAC1B,CAAC;QACH,CAAC;IACH,CAAC,CAAC;IAEF,OAAO;QACL,mBAAmB;QACnB,eAAe;QACf,kBAAkB;QAClB,cAAc;QACd,mBAAmB;QACnB,SAAS,EAAE,eAAe;KAC3B,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"useChapterNavigation.js","sourceRoot":"","sources":["../src/useChapterNavigation.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAWtC;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,EAAE,cAAc,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,gBAAgB,EAAE,CAAC;IAEhG,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,QAAQ,CAAC,cAAc,EAAE,EAAE,IAAI,CAAC,EAAE;QACzE,OAAO,EAAE,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC;KACrC,CAAC,CAAC;IAEH,MAAM,SAAS,GAAG,KAAK,EAAE,IAAI,IAAI,EAAE,CAAC;IACpC,MAAM,MAAM,GAAG,WAAW,EAAE,EAAE,IAAI,EAAE,CAAC;IACrC,MAAM,SAAS,GAAG,cAAc,EAAE,EAAE,IAAI,EAAE,CAAC;IAE3C,MAAM,UAAU,GAAG,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IAC5E,MAAM,UAAU,GAAG,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;IAEhF,MAAM,eAAe,GAAG,CAAC,YAAY,IAAI,UAAU,KAAK,IAAI,CAAC;IAC7D,MAAM,mBAAmB,GAAG,CAAC,YAAY,IAAI,UAAU,KAAK,IAAI,CAAC;IAEjE,MAAM,mBAAmB,GACvB,WAAW,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,cAAc,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAE7E,MAAM,QAAQ,GAAG,CAAC,MAAoD,EAAE,EAAE;QACxE,IAAI,CAAC,MAAM,IAAI,CAAC,SAAS,CAAC,MAAM;YAAE,OAAO;QAEzC,MAAM,UAAU,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC;QACjE,IAAI,CAAC,UAAU;YAAE,OAAO;QAExB,MAAM,aAAa,GACjB,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,SAAS,CAAC;YAC3D,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,KAAK,MAAM,CAAC,SAAS;gBACxC,CAAC,CAAC;oBACE,EAAE,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE;oBACvB,UAAU,EAAE,UAAU,CAAC,KAAK,CAAC,UAAU;oBACvC,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC,KAAK;iBAC9B;gBACH,CAAC,CAAC,SAAS,CAAC,CAAC;QACjB,IAAI,CAAC,aAAa;YAAE,OAAO;QAE3B,IAAI,UAAU,CAAC,EAAE,KAAK,WAAW,EAAE,EAAE,EAAE,CAAC;YACtC,OAAO,CAAC,UAAU,CAAC,CAAC;QACtB,CAAC;QACD,UAAU,CAAC,aAAa,CAAC,CAAC;IAC5B,CAAC,CAAC;IAEF,MAAM,cAAc,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAClD,MAAM,kBAAkB,GAAG,GAAG,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;IAEtD,OAAO;QACL,mBAAmB;QACnB,eAAe;QACf,kBAAkB;QAClB,cAAc;QACd,mBAAmB;QACnB,SAAS,EAAE,YAAY;KACxB,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youversion/platform-react-hooks",
3
- "version": "1.16.0",
3
+ "version": "1.18.0",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -22,7 +22,7 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@youversion/platform-core": "1.16.0"
25
+ "@youversion/platform-core": "1.18.0"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "react": ">=19.1.0 <20.0.0"
@@ -37,8 +37,8 @@
37
37
  "jsdom": "27.0.1",
38
38
  "typescript": "5.9.3",
39
39
  "vitest": "4.0.4",
40
- "@internal/eslint-config": "0.0.0",
41
- "@internal/tsconfig": "0.0.0"
40
+ "@internal/tsconfig": "0.0.0",
41
+ "@internal/eslint-config": "0.0.0"
42
42
  },
43
43
  "scripts": {
44
44
  "dev": "tsc --watch",
@@ -0,0 +1,160 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { renderHook, act } from '@testing-library/react';
3
+ import React from 'react';
4
+ import { useChapterNavigation } from './useChapterNavigation';
5
+ import { useReaderContext } from './context/ReaderContext';
6
+ import { ReaderProvider } from './context/ReaderProvider';
7
+ import { createMockBook, createMockVersion } from './__tests__/mocks/bibles';
8
+ import type { BibleBook, BibleChapter } from '@youversion/platform-core';
9
+
10
+ function makeChapters(bookId: string, count: number): BibleChapter[] {
11
+ return Array.from({ length: count }, (_, i) => ({
12
+ id: (i + 1).toString(),
13
+ passage_id: `${bookId}.${i + 1}`,
14
+ title: (i + 1).toString(),
15
+ }));
16
+ }
17
+
18
+ const genChapters = makeChapters('GEN', 50);
19
+ const exoChapters = makeChapters('EXO', 40);
20
+ const revChapters = makeChapters('REV', 22);
21
+
22
+ const mockBooks: BibleBook[] = [
23
+ createMockBook({
24
+ id: 'GEN',
25
+ title: 'Genesis',
26
+ chapters: genChapters,
27
+ intro: { id: 'INTRO', passage_id: 'GEN.INTRO', title: 'Intro' },
28
+ }),
29
+ createMockBook({
30
+ id: 'EXO',
31
+ title: 'Exodus',
32
+ canon: 'old_testament',
33
+ chapters: exoChapters,
34
+ }),
35
+ createMockBook({
36
+ id: 'REV',
37
+ title: 'Revelation',
38
+ canon: 'new_testament',
39
+ chapters: revChapters,
40
+ }),
41
+ ];
42
+
43
+ const mockUseBooks = vi.fn();
44
+ vi.mock('./useBooks', () => ({
45
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
46
+ useBooks: (...args: unknown[]) => mockUseBooks(...args),
47
+ }));
48
+
49
+ function useNavWithContext() {
50
+ const nav = useChapterNavigation();
51
+ const ctx = useReaderContext();
52
+ return { nav, ctx };
53
+ }
54
+
55
+ function wrapper(book: BibleBook, chapter: BibleChapter) {
56
+ return ({ children }: { children: React.ReactNode }) => (
57
+ <ReaderProvider
58
+ currentVersion={createMockVersion()}
59
+ currentBook={book}
60
+ currentChapter={chapter}
61
+ currentVerse={null}
62
+ >
63
+ {children}
64
+ </ReaderProvider>
65
+ );
66
+ }
67
+
68
+ const genBook = mockBooks[0]!;
69
+ const exoBook = mockBooks[1]!;
70
+ const revBook = mockBooks[2]!;
71
+
72
+ describe('useChapterNavigation', () => {
73
+ beforeEach(() => {
74
+ mockUseBooks.mockReturnValue({
75
+ books: { data: mockBooks },
76
+ loading: false,
77
+ error: null,
78
+ refetch: vi.fn(),
79
+ });
80
+ });
81
+
82
+ it('navigateToNext within same book updates chapter, keeps book', () => {
83
+ const { result } = renderHook(useNavWithContext, {
84
+ wrapper: wrapper(genBook, genChapters[0]!),
85
+ });
86
+
87
+ act(() => result.current.nav.navigateToNext());
88
+
89
+ expect(result.current.ctx.currentChapter.id).toBe('2');
90
+ expect(result.current.ctx.currentBook.id).toBe('GEN');
91
+ });
92
+
93
+ it('navigateToNext cross-book updates both book and chapter', () => {
94
+ const { result } = renderHook(useNavWithContext, {
95
+ wrapper: wrapper(genBook, genChapters.at(-1)!),
96
+ });
97
+
98
+ act(() => result.current.nav.navigateToNext());
99
+
100
+ expect(result.current.ctx.currentBook.id).toBe('EXO');
101
+ expect(result.current.ctx.currentChapter.id).toBe('1');
102
+ });
103
+
104
+ it('navigateToPrevious to intro sets chapter to INTRO, keeps book', () => {
105
+ const { result } = renderHook(useNavWithContext, {
106
+ wrapper: wrapper(genBook, genChapters[0]!),
107
+ });
108
+
109
+ act(() => result.current.nav.navigateToPrevious());
110
+
111
+ expect(result.current.ctx.currentChapter.id).toBe('INTRO');
112
+ expect(result.current.ctx.currentBook.id).toBe('GEN');
113
+ });
114
+
115
+ it('navigateToPrevious cross-book updates both book and chapter', () => {
116
+ const { result } = renderHook(useNavWithContext, {
117
+ wrapper: wrapper(exoBook, exoChapters[0]!),
118
+ });
119
+
120
+ act(() => result.current.nav.navigateToPrevious());
121
+
122
+ expect(result.current.ctx.currentBook.id).toBe('GEN');
123
+ expect(result.current.ctx.currentChapter.id).toBe('50');
124
+ });
125
+
126
+ it('canNavigatePrevious is false at Bible start', () => {
127
+ const introChapter: BibleChapter = { id: 'INTRO', passage_id: 'GEN.INTRO', title: 'Intro' };
128
+
129
+ const { result } = renderHook(useNavWithContext, {
130
+ wrapper: wrapper(genBook, introChapter),
131
+ });
132
+
133
+ expect(result.current.nav.canNavigatePrevious).toBe(false);
134
+ });
135
+
136
+ it('canNavigateNext is false at Bible end', () => {
137
+ const { result } = renderHook(useNavWithContext, {
138
+ wrapper: wrapper(revBook, revChapters[21]!),
139
+ });
140
+
141
+ expect(result.current.nav.canNavigateNext).toBe(false);
142
+ });
143
+
144
+ it('both disabled while loading', () => {
145
+ mockUseBooks.mockReturnValue({
146
+ books: null,
147
+ loading: true,
148
+ error: null,
149
+ refetch: vi.fn(),
150
+ });
151
+
152
+ const { result } = renderHook(useNavWithContext, {
153
+ wrapper: wrapper(genBook, genChapters[0]!),
154
+ });
155
+
156
+ expect(result.current.nav.canNavigateNext).toBe(false);
157
+ expect(result.current.nav.canNavigatePrevious).toBe(false);
158
+ expect(result.current.nav.isLoading).toBe(true);
159
+ });
160
+ });
@@ -1,5 +1,8 @@
1
+ 'use client';
2
+
3
+ import { getAdjacentChapter } from '@youversion/platform-core';
1
4
  import { useReaderContext } from './context/ReaderContext';
2
- import { useChapters } from './useChapters';
5
+ import { useBooks } from './useBooks';
3
6
 
4
7
  interface UseChapterNavigationResult {
5
8
  canNavigatePrevious: boolean;
@@ -11,67 +14,61 @@ interface UseChapterNavigationResult {
11
14
  }
12
15
 
13
16
  /**
14
- * Provides navigation functionality for chapters within a book, allowing the user to move
15
- * to the previous or next chapter, as well as access additional chapter navigation metadata.
16
- *
17
- * @return {UseChapterNavigationResult} An object containing properties and methods for chapter navigation:
18
- * - `canNavigatePrevious` (boolean): Indicates whether navigating to the previous chapter is possible.
19
- * - `canNavigateNext` (boolean): Indicates whether navigating to the next chapter is possible.
20
- * - `navigateToPrevious` (function): Moves to the previous chapter if possible.
21
- * - `navigateToNext` (function): Moves to the next chapter if possible.
22
- * - `currentChapterIndex` (number): The index of the current chapter within the list of chapters.
23
- * - `isLoading` (boolean): Whether the chapter data is still loading.
17
+ * Provides navigation functionality for chapters across book boundaries,
18
+ * including intro chapter support.
24
19
  */
25
20
  export function useChapterNavigation(): UseChapterNavigationResult {
26
- const { currentChapter, currentVersion, currentBook, setChapter } = useReaderContext();
21
+ const { currentChapter, currentVersion, currentBook, setChapter, setBook } = useReaderContext();
27
22
 
28
- const bookIdentifier = currentBook?.id ?? '';
23
+ const { books, loading: booksLoading } = useBooks(currentVersion?.id ?? 0, {
24
+ enabled: Boolean(currentVersion?.id),
25
+ });
29
26
 
30
- const { chapters, loading: chaptersLoading } = useChapters(
31
- currentVersion?.id ?? 0,
32
- bookIdentifier,
33
- { enabled: Boolean(bookIdentifier && currentVersion?.id) },
34
- );
27
+ const booksData = books?.data ?? [];
28
+ const bookId = currentBook?.id ?? '';
29
+ const chapterId = currentChapter?.id ?? '';
35
30
 
36
- const currentChapterIndex = chapters?.data.findIndex((c) => c.id === currentChapter.id) ?? -1;
31
+ const nextResult = getAdjacentChapter(booksData, bookId, chapterId, 'next');
32
+ const prevResult = getAdjacentChapter(booksData, bookId, chapterId, 'previous');
37
33
 
38
- const canNavigatePrevious = Boolean(
39
- !chaptersLoading && chapters?.data && currentChapterIndex !== -1 && currentChapterIndex > 0,
40
- );
34
+ const canNavigateNext = !booksLoading && nextResult !== null;
35
+ const canNavigatePrevious = !booksLoading && prevResult !== null;
41
36
 
42
- const canNavigateNext = Boolean(
43
- !chaptersLoading &&
44
- chapters?.data &&
45
- currentChapterIndex !== -1 &&
46
- currentChapterIndex < chapters.data.length - 1,
47
- );
37
+ const currentChapterIndex =
38
+ currentBook?.chapters?.findIndex((c) => c.id === currentChapter?.id) ?? -1;
48
39
 
49
- const navigateToPrevious = () => {
50
- if (canNavigatePrevious && chapters?.data) {
51
- const previousChapter = chapters.data[currentChapterIndex - 1];
52
- if (previousChapter) {
53
- // Use the chapter as-is since it already has all required fields
54
- setChapter(previousChapter);
55
- }
56
- }
57
- };
40
+ const navigate = (result: { bookId: string; chapterId: string } | null) => {
41
+ if (!result || !booksData.length) return;
58
42
 
59
- const navigateToNext = () => {
60
- if (canNavigateNext && chapters?.data) {
61
- const nextChapter = chapters.data[currentChapterIndex + 1];
62
- if (nextChapter) {
63
- // Use the chapter as-is since it already has all required fields
64
- setChapter(nextChapter);
65
- }
43
+ const targetBook = booksData.find((b) => b.id === result.bookId);
44
+ if (!targetBook) return;
45
+
46
+ const targetChapter =
47
+ targetBook.chapters?.find((c) => c.id === result.chapterId) ??
48
+ (targetBook.intro?.id === result.chapterId
49
+ ? {
50
+ id: targetBook.intro.id,
51
+ passage_id: targetBook.intro.passage_id,
52
+ title: targetBook.intro.title,
53
+ }
54
+ : undefined);
55
+ if (!targetChapter) return;
56
+
57
+ if (targetBook.id !== currentBook?.id) {
58
+ setBook(targetBook);
66
59
  }
60
+ setChapter(targetChapter);
67
61
  };
68
62
 
63
+ const navigateToNext = () => navigate(nextResult);
64
+ const navigateToPrevious = () => navigate(prevResult);
65
+
69
66
  return {
70
67
  canNavigatePrevious,
71
68
  canNavigateNext,
72
69
  navigateToPrevious,
73
70
  navigateToNext,
74
71
  currentChapterIndex,
75
- isLoading: chaptersLoading,
72
+ isLoading: booksLoading,
76
73
  };
77
74
  }