@youversion/platform-react-hooks 0.4.1
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.
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +9 -0
- package/LICENSE +201 -0
- package/dist/context/BibleSDKContext.d.ts +6 -0
- package/dist/context/BibleSDKContext.d.ts.map +1 -0
- package/dist/context/BibleSDKContext.js +4 -0
- package/dist/context/BibleSDKContext.js.map +1 -0
- package/dist/context/BibleSDKProvider.d.ts +8 -0
- package/dist/context/BibleSDKProvider.d.ts.map +1 -0
- package/dist/context/BibleSDKProvider.js +7 -0
- package/dist/context/BibleSDKProvider.js.map +1 -0
- package/dist/context/ReaderContext.d.ts +15 -0
- package/dist/context/ReaderContext.d.ts.map +1 -0
- package/dist/context/ReaderContext.js +11 -0
- package/dist/context/ReaderContext.js.map +1 -0
- package/dist/context/ReaderProvider.d.ts +11 -0
- package/dist/context/ReaderProvider.d.ts.map +1 -0
- package/dist/context/ReaderProvider.js +21 -0
- package/dist/context/ReaderProvider.js.map +1 -0
- package/dist/context/VerseSelectionContext.d.ts +9 -0
- package/dist/context/VerseSelectionContext.d.ts.map +1 -0
- package/dist/context/VerseSelectionContext.js +3 -0
- package/dist/context/VerseSelectionContext.js.map +1 -0
- package/dist/context/VerseSelectionProvider.d.ts +3 -0
- package/dist/context/VerseSelectionProvider.d.ts.map +1 -0
- package/dist/context/VerseSelectionProvider.js +33 -0
- package/dist/context/VerseSelectionProvider.js.map +1 -0
- package/dist/context/index.d.ts +7 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +7 -0
- package/dist/context/index.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/test/setup.d.ts +2 -0
- package/dist/test/setup.d.ts.map +1 -0
- package/dist/test/setup.js +9 -0
- package/dist/test/setup.js.map +1 -0
- package/dist/useApiData.d.ts +12 -0
- package/dist/useApiData.d.ts.map +1 -0
- package/dist/useApiData.js +46 -0
- package/dist/useApiData.js.map +1 -0
- package/dist/useBibleClient.d.ts +3 -0
- package/dist/useBibleClient.d.ts.map +1 -0
- package/dist/useBibleClient.js +17 -0
- package/dist/useBibleClient.js.map +1 -0
- package/dist/useBook.d.ts +9 -0
- package/dist/useBook.d.ts.map +1 -0
- package/dist/useBook.js +11 -0
- package/dist/useBook.js.map +1 -0
- package/dist/useBooks.d.ts +9 -0
- package/dist/useBooks.d.ts.map +1 -0
- package/dist/useBooks.js +11 -0
- package/dist/useBooks.js.map +1 -0
- package/dist/useChapter.d.ts +9 -0
- package/dist/useChapter.d.ts.map +1 -0
- package/dist/useChapter.js +11 -0
- package/dist/useChapter.js.map +1 -0
- package/dist/useChapterNavigation.d.ts +23 -0
- package/dist/useChapterNavigation.d.ts.map +1 -0
- package/dist/useChapterNavigation.js +52 -0
- package/dist/useChapterNavigation.js.map +1 -0
- package/dist/useChapters.d.ts +9 -0
- package/dist/useChapters.d.ts.map +1 -0
- package/dist/useChapters.js +13 -0
- package/dist/useChapters.js.map +1 -0
- package/dist/useFilteredVersions.d.ts +6 -0
- package/dist/useFilteredVersions.d.ts.map +1 -0
- package/dist/useFilteredVersions.js +24 -0
- package/dist/useFilteredVersions.js.map +1 -0
- package/dist/useHighlights.d.ts +11 -0
- package/dist/useHighlights.d.ts.map +1 -0
- package/dist/useHighlights.js +40 -0
- package/dist/useHighlights.js.map +1 -0
- package/dist/useInitData.d.ts +23 -0
- package/dist/useInitData.d.ts.map +1 -0
- package/dist/useInitData.js +24 -0
- package/dist/useInitData.js.map +1 -0
- package/dist/useLanguages.d.ts +9 -0
- package/dist/useLanguages.d.ts.map +1 -0
- package/dist/useLanguages.js +29 -0
- package/dist/useLanguages.js.map +1 -0
- package/dist/usePassage.d.ts +18 -0
- package/dist/usePassage.d.ts.map +1 -0
- package/dist/usePassage.js +11 -0
- package/dist/usePassage.js.map +1 -0
- package/dist/useVOTD.d.ts +9 -0
- package/dist/useVOTD.d.ts.map +1 -0
- package/dist/useVOTD.js +9 -0
- package/dist/useVOTD.js.map +1 -0
- package/dist/useVerse.d.ts +9 -0
- package/dist/useVerse.d.ts.map +1 -0
- package/dist/useVerse.js +11 -0
- package/dist/useVerse.js.map +1 -0
- package/dist/useVerseSelection.d.ts +3 -0
- package/dist/useVerseSelection.d.ts.map +1 -0
- package/dist/useVerseSelection.js +10 -0
- package/dist/useVerseSelection.js.map +1 -0
- package/dist/useVerses.d.ts +9 -0
- package/dist/useVerses.d.ts.map +1 -0
- package/dist/useVerses.js +11 -0
- package/dist/useVerses.js.map +1 -0
- package/dist/useVersion.d.ts +9 -0
- package/dist/useVersion.d.ts.map +1 -0
- package/dist/useVersion.js +11 -0
- package/dist/useVersion.js.map +1 -0
- package/dist/useVersions.d.ts +9 -0
- package/dist/useVersions.d.ts.map +1 -0
- package/dist/useVersions.js +11 -0
- package/dist/useVersions.js.map +1 -0
- package/dist/utility/extractTextFromHTML.d.ts +9 -0
- package/dist/utility/extractTextFromHTML.d.ts.map +1 -0
- package/dist/utility/extractTextFromHTML.js +21 -0
- package/dist/utility/extractTextFromHTML.js.map +1 -0
- package/dist/utility/extractVersesFromHTML.d.ts +9 -0
- package/dist/utility/extractVersesFromHTML.d.ts.map +1 -0
- package/dist/utility/extractVersesFromHTML.js +26 -0
- package/dist/utility/extractVersesFromHTML.js.map +1 -0
- package/dist/utility/getDayOfYear.d.ts +2 -0
- package/dist/utility/getDayOfYear.d.ts.map +1 -0
- package/dist/utility/getDayOfYear.js +5 -0
- package/dist/utility/getDayOfYear.js.map +1 -0
- package/dist/utility/index.d.ts +6 -0
- package/dist/utility/index.d.ts.map +1 -0
- package/dist/utility/index.js +6 -0
- package/dist/utility/index.js.map +1 -0
- package/dist/utility/useDebounce.d.ts +14 -0
- package/dist/utility/useDebounce.d.ts.map +1 -0
- package/dist/utility/useDebounce.js +24 -0
- package/dist/utility/useDebounce.js.map +1 -0
- package/dist/utility/version.d.ts +3 -0
- package/dist/utility/version.d.ts.map +1 -0
- package/dist/utility/version.js +4 -0
- package/dist/utility/version.js.map +1 -0
- package/package.json +50 -0
- package/src/context/BibleSDKContext.tsx +9 -0
- package/src/context/BibleSDKProvider.tsx +16 -0
- package/src/context/ReaderContext.tsx +27 -0
- package/src/context/ReaderProvider.tsx +36 -0
- package/src/context/VerseSelectionContext.tsx +11 -0
- package/src/context/VerseSelectionProvider.tsx +39 -0
- package/src/context/index.ts +6 -0
- package/src/index.ts +20 -0
- package/src/test/setup.ts +9 -0
- package/src/useApiData.ts +71 -0
- package/src/useBibleClient.test.tsx +151 -0
- package/src/useBibleClient.ts +22 -0
- package/src/useBook.ts +28 -0
- package/src/useBooks.ts +31 -0
- package/src/useChapter.ts +33 -0
- package/src/useChapterNavigation.ts +77 -0
- package/src/useChapters.ts +36 -0
- package/src/useFilteredVersions.test.tsx +248 -0
- package/src/useFilteredVersions.ts +38 -0
- package/src/useHighlights.test.tsx +448 -0
- package/src/useHighlights.ts +80 -0
- package/src/useInitData.ts +54 -0
- package/src/useLanguages.test.tsx +296 -0
- package/src/useLanguages.ts +57 -0
- package/src/usePassage.ts +48 -0
- package/src/useVOTD.test.tsx +269 -0
- package/src/useVOTD.ts +19 -0
- package/src/useVerse.ts +35 -0
- package/src/useVerseSelection.ts +13 -0
- package/src/useVerses.ts +34 -0
- package/src/useVersion.ts +28 -0
- package/src/useVersions.ts +33 -0
- package/src/utility/extractTextFromHTML.test.ts +112 -0
- package/src/utility/extractTextFromHTML.ts +22 -0
- package/src/utility/extractVersesFromHTML.test.ts +186 -0
- package/src/utility/extractVersesFromHTML.ts +31 -0
- package/src/utility/getDayOfYear.ts +6 -0
- package/src/utility/index.ts +5 -0
- package/src/utility/useDebounce.test.tsx +95 -0
- package/src/utility/useDebounce.ts +27 -0
- package/src/utility/version.ts +5 -0
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +13 -0
- package/vitest.config.ts +11 -0
- package/vitest.setup.ts +1 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { useBibleClient } from './useBibleClient';
|
|
5
|
+
import { BibleSDKContext } from './context';
|
|
6
|
+
import { BibleClient, ApiClient } from '@youversion/platform-core';
|
|
7
|
+
|
|
8
|
+
vi.mock('@youversion/platform-core', async () => {
|
|
9
|
+
const actual = await vi.importActual('@youversion/platform-core');
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
BibleClient: vi.fn(function () {
|
|
13
|
+
return {
|
|
14
|
+
isBibleClient: true,
|
|
15
|
+
};
|
|
16
|
+
}),
|
|
17
|
+
ApiClient: vi.fn(function () {
|
|
18
|
+
return {
|
|
19
|
+
isApiClient: true,
|
|
20
|
+
};
|
|
21
|
+
}),
|
|
22
|
+
YouVersionPlatformConfiguration: {
|
|
23
|
+
get installationId() {
|
|
24
|
+
return 'auto-generated-uuid';
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('useBibleClient', () => {
|
|
31
|
+
const mockAppId = 'test-app-id';
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should create and return a BibleClient instance when context is valid', () => {
|
|
38
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
39
|
+
<BibleSDKContext.Provider
|
|
40
|
+
value={{
|
|
41
|
+
appId: mockAppId,
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
</BibleSDKContext.Provider>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const { result } = renderHook(() => useBibleClient(), { wrapper });
|
|
49
|
+
|
|
50
|
+
expect(ApiClient).toHaveBeenCalledWith({
|
|
51
|
+
appId: mockAppId,
|
|
52
|
+
installationId: 'auto-generated-uuid',
|
|
53
|
+
});
|
|
54
|
+
expect(BibleClient).toHaveBeenCalledWith(expect.objectContaining({ isApiClient: true }));
|
|
55
|
+
expect(result.current).toEqual(expect.objectContaining({ isBibleClient: true }));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should throw error when context is not provided', () => {
|
|
59
|
+
expect(() => renderHook(() => useBibleClient())).toThrow(
|
|
60
|
+
'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should throw error when appId is missing', () => {
|
|
65
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
66
|
+
<BibleSDKContext.Provider
|
|
67
|
+
value={{
|
|
68
|
+
appId: '',
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{children}
|
|
72
|
+
</BibleSDKContext.Provider>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(() => renderHook(() => useBibleClient(), { wrapper })).toThrow(
|
|
76
|
+
'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should memoize the BibleClient instance', () => {
|
|
81
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
82
|
+
<BibleSDKContext.Provider
|
|
83
|
+
value={{
|
|
84
|
+
appId: mockAppId,
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{children}
|
|
88
|
+
</BibleSDKContext.Provider>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const { result, rerender } = renderHook(() => useBibleClient(), { wrapper });
|
|
92
|
+
const firstClient = result.current;
|
|
93
|
+
|
|
94
|
+
rerender();
|
|
95
|
+
const secondClient = result.current;
|
|
96
|
+
|
|
97
|
+
expect(firstClient).toBe(secondClient);
|
|
98
|
+
expect(BibleClient).toHaveBeenCalledTimes(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should create new BibleClient when appId changes', () => {
|
|
102
|
+
let currentAppId = mockAppId;
|
|
103
|
+
|
|
104
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
105
|
+
<BibleSDKContext.Provider
|
|
106
|
+
value={{
|
|
107
|
+
appId: currentAppId,
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{children}
|
|
111
|
+
</BibleSDKContext.Provider>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const { result, rerender } = renderHook(() => useBibleClient(), { wrapper });
|
|
115
|
+
|
|
116
|
+
const firstClient = result.current;
|
|
117
|
+
expect(BibleClient).toHaveBeenCalledTimes(1);
|
|
118
|
+
expect(ApiClient).toHaveBeenCalledWith({
|
|
119
|
+
appId: mockAppId,
|
|
120
|
+
installationId: 'auto-generated-uuid',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
currentAppId = 'new-app-id';
|
|
124
|
+
rerender();
|
|
125
|
+
|
|
126
|
+
const secondClient = result.current;
|
|
127
|
+
expect(BibleClient).toHaveBeenCalledTimes(2);
|
|
128
|
+
expect(firstClient).not.toBe(secondClient);
|
|
129
|
+
|
|
130
|
+
expect(ApiClient).toHaveBeenLastCalledWith({
|
|
131
|
+
appId: 'new-app-id',
|
|
132
|
+
installationId: 'auto-generated-uuid',
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should throw error when appId is null', () => {
|
|
137
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
138
|
+
<BibleSDKContext.Provider
|
|
139
|
+
value={{
|
|
140
|
+
appId: null as unknown as string,
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
{children}
|
|
144
|
+
</BibleSDKContext.Provider>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(() => renderHook(() => useBibleClient(), { wrapper })).toThrow(
|
|
148
|
+
'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useContext, useMemo } from 'react';
|
|
4
|
+
import { BibleSDKContext } from './context';
|
|
5
|
+
import { BibleClient, ApiClient, YouVersionPlatformConfiguration } from '@youversion/platform-core';
|
|
6
|
+
|
|
7
|
+
export function useBibleClient(): BibleClient {
|
|
8
|
+
const context = useContext(BibleSDKContext);
|
|
9
|
+
return useMemo(() => {
|
|
10
|
+
if (!context?.appId) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
'BibleSDK context not found. Make sure your component is wrapped with BibleSDKProvider and an API key is provided.',
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return new BibleClient(
|
|
16
|
+
new ApiClient({
|
|
17
|
+
appId: context.appId,
|
|
18
|
+
installationId: YouVersionPlatformConfiguration.installationId,
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
}, [context?.appId]);
|
|
22
|
+
}
|
package/src/useBook.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useBibleClient } from './useBibleClient';
|
|
4
|
+
import { useApiData, type UseApiDataOptions } from './useApiData';
|
|
5
|
+
import type { BibleBook } from '@youversion/platform-core';
|
|
6
|
+
|
|
7
|
+
export function useBook(
|
|
8
|
+
versionId: number,
|
|
9
|
+
book: string,
|
|
10
|
+
options?: UseApiDataOptions,
|
|
11
|
+
): {
|
|
12
|
+
book: BibleBook | null;
|
|
13
|
+
loading: boolean;
|
|
14
|
+
error: Error | null;
|
|
15
|
+
refetch: () => void;
|
|
16
|
+
} {
|
|
17
|
+
const bibleClient = useBibleClient();
|
|
18
|
+
|
|
19
|
+
const { data, loading, error, refetch } = useApiData<BibleBook>(
|
|
20
|
+
() => bibleClient.getBook(versionId, book),
|
|
21
|
+
[bibleClient, versionId, book],
|
|
22
|
+
{
|
|
23
|
+
enabled: options?.enabled !== false,
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return { book: data, loading, error, refetch };
|
|
28
|
+
}
|
package/src/useBooks.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useBibleClient } from './useBibleClient';
|
|
3
|
+
import { useApiData, type UseApiDataOptions } from './useApiData';
|
|
4
|
+
import type { BibleBook, Collection } from '@youversion/platform-core';
|
|
5
|
+
|
|
6
|
+
export function useBooks(
|
|
7
|
+
versionId: number,
|
|
8
|
+
options?: UseApiDataOptions,
|
|
9
|
+
): {
|
|
10
|
+
books: Collection<BibleBook> | null;
|
|
11
|
+
loading: boolean;
|
|
12
|
+
error: Error | null;
|
|
13
|
+
refetch: () => void;
|
|
14
|
+
} {
|
|
15
|
+
const bibleClient = useBibleClient();
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
data: books,
|
|
19
|
+
loading,
|
|
20
|
+
error,
|
|
21
|
+
refetch,
|
|
22
|
+
} = useApiData<Collection<BibleBook>>(
|
|
23
|
+
() => bibleClient.getBooks(versionId),
|
|
24
|
+
[bibleClient, versionId],
|
|
25
|
+
{
|
|
26
|
+
enabled: options?.enabled !== false,
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return { books, loading, error, refetch };
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useBibleClient } from './useBibleClient';
|
|
3
|
+
import { useApiData, type UseApiDataOptions } from './useApiData';
|
|
4
|
+
import type { BibleChapter } from '@youversion/platform-core';
|
|
5
|
+
|
|
6
|
+
export function useChapter(
|
|
7
|
+
versionId: number,
|
|
8
|
+
book: string,
|
|
9
|
+
chapter: number,
|
|
10
|
+
options?: UseApiDataOptions,
|
|
11
|
+
): {
|
|
12
|
+
chapter: BibleChapter | null;
|
|
13
|
+
loading: boolean;
|
|
14
|
+
error: Error | null;
|
|
15
|
+
refetch: () => void;
|
|
16
|
+
} {
|
|
17
|
+
const bibleClient = useBibleClient();
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
data: chapterData,
|
|
21
|
+
loading,
|
|
22
|
+
error,
|
|
23
|
+
refetch,
|
|
24
|
+
} = useApiData<BibleChapter>(
|
|
25
|
+
() => bibleClient.getChapter(versionId, book, chapter),
|
|
26
|
+
[bibleClient, versionId, book, chapter],
|
|
27
|
+
{
|
|
28
|
+
enabled: options?.enabled !== false,
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return { chapter: chapterData, loading, error, refetch };
|
|
33
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useReaderContext } from './context/ReaderContext';
|
|
2
|
+
import { useChapters } from './useChapters';
|
|
3
|
+
|
|
4
|
+
interface UseChapterNavigationResult {
|
|
5
|
+
canNavigatePrevious: boolean;
|
|
6
|
+
canNavigateNext: boolean;
|
|
7
|
+
navigateToPrevious: () => void;
|
|
8
|
+
navigateToNext: () => void;
|
|
9
|
+
currentChapterIndex: number;
|
|
10
|
+
isLoading: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
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.
|
|
24
|
+
*/
|
|
25
|
+
export function useChapterNavigation(): UseChapterNavigationResult {
|
|
26
|
+
const { currentChapter, currentVersion, currentBook, setChapter } = useReaderContext();
|
|
27
|
+
|
|
28
|
+
const bookIdentifier = currentBook?.id ?? '';
|
|
29
|
+
|
|
30
|
+
const { chapters, loading: chaptersLoading } = useChapters(
|
|
31
|
+
currentVersion?.id ?? 0,
|
|
32
|
+
bookIdentifier,
|
|
33
|
+
{ enabled: Boolean(bookIdentifier && currentVersion?.id) },
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const currentChapterIndex = chapters?.data.findIndex((c) => c.id === currentChapter.id) ?? -1;
|
|
37
|
+
|
|
38
|
+
const canNavigatePrevious = Boolean(
|
|
39
|
+
!chaptersLoading && chapters?.data && currentChapterIndex !== -1 && currentChapterIndex > 0,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const canNavigateNext = Boolean(
|
|
43
|
+
!chaptersLoading &&
|
|
44
|
+
chapters?.data &&
|
|
45
|
+
currentChapterIndex !== -1 &&
|
|
46
|
+
currentChapterIndex < chapters.data.length - 1,
|
|
47
|
+
);
|
|
48
|
+
|
|
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
|
+
};
|
|
58
|
+
|
|
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
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
canNavigatePrevious,
|
|
71
|
+
canNavigateNext,
|
|
72
|
+
navigateToPrevious,
|
|
73
|
+
navigateToNext,
|
|
74
|
+
currentChapterIndex,
|
|
75
|
+
isLoading: chaptersLoading,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useBibleClient } from './useBibleClient';
|
|
4
|
+
import { useApiData, type UseApiDataOptions } from './useApiData';
|
|
5
|
+
import type { BibleChapter, Collection } from '@youversion/platform-core';
|
|
6
|
+
|
|
7
|
+
export function useChapters(
|
|
8
|
+
versionId: number,
|
|
9
|
+
book: string,
|
|
10
|
+
options?: UseApiDataOptions,
|
|
11
|
+
): {
|
|
12
|
+
chapters: Collection<BibleChapter> | null;
|
|
13
|
+
loading: boolean;
|
|
14
|
+
error: Error | null;
|
|
15
|
+
refetch: () => void;
|
|
16
|
+
} {
|
|
17
|
+
const bibleClient = useBibleClient();
|
|
18
|
+
|
|
19
|
+
// Don't attempt to fetch if book is invalid
|
|
20
|
+
const isValidBook = Boolean(book) && book !== 'undefined' && book !== 'null';
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
data: chapters,
|
|
24
|
+
loading,
|
|
25
|
+
error,
|
|
26
|
+
refetch,
|
|
27
|
+
} = useApiData<Collection<BibleChapter>>(
|
|
28
|
+
() => bibleClient.getChapters(versionId, book),
|
|
29
|
+
[bibleClient, versionId, book],
|
|
30
|
+
{
|
|
31
|
+
enabled: options?.enabled !== false && isValidBook,
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return { chapters, loading, error, refetch };
|
|
36
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import type { BibleVersion } from '@youversion/platform-core';
|
|
4
|
+
import { useFilteredVersions } from './useFilteredVersions';
|
|
5
|
+
|
|
6
|
+
const mockVersions: BibleVersion[] = [
|
|
7
|
+
{
|
|
8
|
+
id: 1,
|
|
9
|
+
title: 'King James Version',
|
|
10
|
+
abbreviation: 'KJV',
|
|
11
|
+
copyright_short: '',
|
|
12
|
+
copyright_long: '',
|
|
13
|
+
info: null,
|
|
14
|
+
publisher_url: null,
|
|
15
|
+
language_tag: 'en',
|
|
16
|
+
local_abbreviation: 'KJV',
|
|
17
|
+
local_title: 'King James Version',
|
|
18
|
+
books: [],
|
|
19
|
+
youversion_deep_link: 'https://www.bible.com/versions/1',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 2,
|
|
23
|
+
title: 'New International Version',
|
|
24
|
+
abbreviation: 'NIV',
|
|
25
|
+
copyright_short: '',
|
|
26
|
+
copyright_long: '',
|
|
27
|
+
info: null,
|
|
28
|
+
publisher_url: null,
|
|
29
|
+
language_tag: 'en',
|
|
30
|
+
local_abbreviation: 'NIV',
|
|
31
|
+
local_title: 'New International Version',
|
|
32
|
+
books: [],
|
|
33
|
+
youversion_deep_link: 'https://www.bible.com/versions/2',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 3,
|
|
37
|
+
title: 'Reina-Valera 1960',
|
|
38
|
+
abbreviation: 'RVR1960',
|
|
39
|
+
copyright_short: '',
|
|
40
|
+
copyright_long: '',
|
|
41
|
+
info: null,
|
|
42
|
+
publisher_url: null,
|
|
43
|
+
language_tag: 'es',
|
|
44
|
+
local_abbreviation: 'RVR1960',
|
|
45
|
+
local_title: 'Reina-Valera 1960',
|
|
46
|
+
books: [],
|
|
47
|
+
youversion_deep_link: 'https://www.bible.com/versions/3',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 4,
|
|
51
|
+
title: 'Nueva Versión Internacional',
|
|
52
|
+
abbreviation: 'NVI',
|
|
53
|
+
copyright_short: '',
|
|
54
|
+
copyright_long: '',
|
|
55
|
+
info: null,
|
|
56
|
+
publisher_url: null,
|
|
57
|
+
language_tag: 'es',
|
|
58
|
+
local_abbreviation: 'NVI',
|
|
59
|
+
local_title: 'Nueva Versión Internacional',
|
|
60
|
+
books: [],
|
|
61
|
+
youversion_deep_link: 'https://www.bible.com/versions/4',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 5,
|
|
65
|
+
title: 'La Bible du Semeur',
|
|
66
|
+
abbreviation: 'BDS',
|
|
67
|
+
copyright_short: '',
|
|
68
|
+
copyright_long: '',
|
|
69
|
+
info: null,
|
|
70
|
+
publisher_url: null,
|
|
71
|
+
language_tag: 'fr',
|
|
72
|
+
local_abbreviation: 'BDS',
|
|
73
|
+
local_title: 'La Bible du Semeur',
|
|
74
|
+
books: [],
|
|
75
|
+
youversion_deep_link: 'https://www.bible.com/versions/5',
|
|
76
|
+
},
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
describe('useFilteredVersions', () => {
|
|
80
|
+
describe('language filtering', () => {
|
|
81
|
+
it('should return all versions when language is "*"', () => {
|
|
82
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, '', '*'));
|
|
83
|
+
|
|
84
|
+
expect(result.current).toEqual(mockVersions);
|
|
85
|
+
expect(result.current).toHaveLength(5);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should filter versions by language_tag', () => {
|
|
89
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, '', 'en'));
|
|
90
|
+
|
|
91
|
+
expect(result.current).toHaveLength(2);
|
|
92
|
+
expect(result.current.every((v) => v.language_tag === 'en')).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should filter versions by language_tag (Spanish)', () => {
|
|
96
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, '', 'es'));
|
|
97
|
+
|
|
98
|
+
expect(result.current).toHaveLength(2);
|
|
99
|
+
expect(result.current.every((v) => v.language_tag === 'es')).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should handle case-insensitive language matching', () => {
|
|
103
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, '', 'EN'));
|
|
104
|
+
|
|
105
|
+
expect(result.current).toHaveLength(2);
|
|
106
|
+
expect(result.current.every((v) => v.language_tag === 'en')).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should return empty array when language matches no versions', () => {
|
|
110
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, '', 'de'));
|
|
111
|
+
|
|
112
|
+
expect(result.current).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('search term filtering', () => {
|
|
117
|
+
it('should return all versions when search term is empty', () => {
|
|
118
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, '', '*'));
|
|
119
|
+
|
|
120
|
+
expect(result.current).toEqual(mockVersions);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('should return all versions when search term is whitespace only', () => {
|
|
124
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, ' ', '*'));
|
|
125
|
+
|
|
126
|
+
expect(result.current).toEqual(mockVersions);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should filter by title', () => {
|
|
130
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, 'King', '*'));
|
|
131
|
+
|
|
132
|
+
expect(result.current).toHaveLength(1);
|
|
133
|
+
expect(result.current[0]?.title).toBe('King James Version');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should filter by abbreviation', () => {
|
|
137
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, 'NIV', '*'));
|
|
138
|
+
|
|
139
|
+
expect(result.current).toHaveLength(1);
|
|
140
|
+
expect(result.current[0]?.abbreviation).toBe('NIV');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should filter by language_tag', () => {
|
|
144
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, 'fr', '*'));
|
|
145
|
+
|
|
146
|
+
expect(result.current).toHaveLength(1);
|
|
147
|
+
expect(result.current[0]?.language_tag).toBe('fr');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should be case-insensitive', () => {
|
|
151
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, 'KING', '*'));
|
|
152
|
+
|
|
153
|
+
expect(result.current).toHaveLength(1);
|
|
154
|
+
expect(result.current[0]?.title).toBe('King James Version');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should find partial matches', () => {
|
|
158
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, 'Version', '*'));
|
|
159
|
+
|
|
160
|
+
expect(result.current).toHaveLength(2);
|
|
161
|
+
expect(result.current[0]?.title).toBe('King James Version');
|
|
162
|
+
expect(result.current[1]?.title).toBe('New International Version');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should return empty array when no matches found', () => {
|
|
166
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, 'xyz', '*'));
|
|
167
|
+
|
|
168
|
+
expect(result.current).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('combined filtering', () => {
|
|
173
|
+
it('should filter by both language and search term', () => {
|
|
174
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, 'International', 'en'));
|
|
175
|
+
|
|
176
|
+
expect(result.current).toHaveLength(1);
|
|
177
|
+
expect(result.current[0]?.abbreviation).toBe('NIV');
|
|
178
|
+
expect(result.current[0]?.language_tag).toBe('en');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should return empty array when language matches but search term does not', () => {
|
|
182
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, 'Reina', 'en'));
|
|
183
|
+
|
|
184
|
+
expect(result.current).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should return empty array when search term matches but language does not', () => {
|
|
188
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, 'King', 'es'));
|
|
189
|
+
|
|
190
|
+
expect(result.current).toEqual([]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should filter Spanish versions by search term', () => {
|
|
194
|
+
const { result } = renderHook(() => useFilteredVersions(mockVersions, 'Reina', 'es'));
|
|
195
|
+
|
|
196
|
+
expect(result.current).toHaveLength(1);
|
|
197
|
+
expect(result.current[0]?.abbreviation).toBe('RVR1960');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('memoization', () => {
|
|
202
|
+
it('should return the same reference when inputs do not change', () => {
|
|
203
|
+
const { result, rerender } = renderHook(
|
|
204
|
+
({ versions, searchTerm, language }) => useFilteredVersions(versions, searchTerm, language),
|
|
205
|
+
{
|
|
206
|
+
initialProps: {
|
|
207
|
+
versions: mockVersions,
|
|
208
|
+
searchTerm: 'King',
|
|
209
|
+
language: 'en',
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const firstResult = result.current;
|
|
215
|
+
|
|
216
|
+
rerender({
|
|
217
|
+
versions: mockVersions,
|
|
218
|
+
searchTerm: 'King',
|
|
219
|
+
language: 'en',
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
expect(result.current).toBe(firstResult);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should return a new reference when inputs change', () => {
|
|
226
|
+
const { result, rerender } = renderHook(
|
|
227
|
+
({ versions, searchTerm, language }) => useFilteredVersions(versions, searchTerm, language),
|
|
228
|
+
{
|
|
229
|
+
initialProps: {
|
|
230
|
+
versions: mockVersions,
|
|
231
|
+
searchTerm: 'King',
|
|
232
|
+
language: 'en',
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const firstResult = result.current;
|
|
238
|
+
|
|
239
|
+
rerender({
|
|
240
|
+
versions: mockVersions,
|
|
241
|
+
searchTerm: 'International',
|
|
242
|
+
language: 'en',
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
expect(result.current).not.toBe(firstResult);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { BibleVersion } from '@youversion/platform-core';
|
|
4
|
+
import { useMemo } from 'react';
|
|
5
|
+
import { getISOFromVersion } from './utility/version';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Custom hook to filter versions based on search term
|
|
9
|
+
*/
|
|
10
|
+
export function useFilteredVersions(
|
|
11
|
+
versions: BibleVersion[],
|
|
12
|
+
searchTerm: string,
|
|
13
|
+
selectedLanguage: string,
|
|
14
|
+
): BibleVersion[] {
|
|
15
|
+
return useMemo(() => {
|
|
16
|
+
let result = [...versions];
|
|
17
|
+
|
|
18
|
+
// Language filter
|
|
19
|
+
if (selectedLanguage && selectedLanguage !== '*') {
|
|
20
|
+
result = result.filter(
|
|
21
|
+
(version) => getISOFromVersion(version).toLowerCase() === selectedLanguage.toLowerCase(),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Search filter
|
|
26
|
+
if (searchTerm.trim()) {
|
|
27
|
+
const searchLower = searchTerm.toLowerCase();
|
|
28
|
+
result = result.filter(
|
|
29
|
+
(version) =>
|
|
30
|
+
version.title.toLowerCase().includes(searchLower) ||
|
|
31
|
+
version.abbreviation.toLowerCase().includes(searchLower) ||
|
|
32
|
+
getISOFromVersion(version).toLowerCase().includes(searchLower),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return result;
|
|
37
|
+
}, [versions, searchTerm, selectedLanguage]);
|
|
38
|
+
}
|