diff --git a/__mocks__/p-memoize.ts b/__mocks__/p-memoize.ts new file mode 100644 index 000000000..41dfd0b32 --- /dev/null +++ b/__mocks__/p-memoize.ts @@ -0,0 +1,3 @@ +export default function pMemoize(fn: T, _ = {}): T { + return fn; +} diff --git a/__mocks__/p-throttle.ts b/__mocks__/p-throttle.ts new file mode 100644 index 000000000..0eba4e525 --- /dev/null +++ b/__mocks__/p-throttle.ts @@ -0,0 +1,3 @@ +export default function pThrottle(_ = {}): (fn: T) => T { + return (fn: any) => fn; +} diff --git a/audit-ci.json b/audit-ci.json index d2bafed91..edddab1a2 100644 --- a/audit-ci.json +++ b/audit-ci.json @@ -31,6 +31,7 @@ "GHSA-c2qf-rxjj-qqgw", // Not used at runtime "GHSA-pxg6-pf52-xh8x|@lhci/cli>*", // Used by lighthouse CI, not at runtime "GHSA-pxg6-pf52-xh8x|@lhci/utils>*", // Used by lighthouse CI, not at runtime - "GHSA-pxg6-pf52-xh8x|express>cookie" // Used by lighthouse CI, not at runtime + "GHSA-pxg6-pf52-xh8x|express>cookie", // Used by lighthouse CI, not at runtime + "GHSA-7gfc-8cq8-jh5f" // automatically mitigated by Vercel ] } diff --git a/package-lock.json b/package-lock.json index c9f7aba0c..98a5944cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@silvermine/videojs-chromecast": "^1.5.0", "@tanstack/react-query": "^5.56.2", "@tanstack/react-query-devtools": "^5.56.2", + "caniuse-lite": "^1.0.30001689", "clsx": "^2.1.1", "cookie": "^0.7.0", "dayjs": "^1.11.13", @@ -4783,9 +4784,9 @@ "dev": true }, "node_modules/@graphql-tools/utils": { - "version": "10.6.2", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.6.2.tgz", - "integrity": "sha512-ABZHTpwiVR8oE2//NI/nnU3nNhbBpqMlMYyCF5cnqjLfhlyOdFfoRuhYEATEsmMfDg0ijGreULywK/SmepVGfw==", + "version": "10.6.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.6.4.tgz", + "integrity": "sha512-itCgjwVxbO+3uI/K73G9heedG8KelNFzgn368rUhPjTrkJX6NyLQwT5EMq/A8tvazMXyJYdtnN5nD+tT4DUpbQ==", "dev": true, "license": "MIT", "dependencies": { @@ -11206,9 +11207,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001686", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001686.tgz", - "integrity": "sha512-Y7deg0Aergpa24M3qLC5xjNklnKnhsmSyR/V89dLZ1n0ucJIFNs7PgR2Yfa/Zf6W79SbBicgtGxZr2juHkEUIA==", + "version": "1.0.30001689", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz", + "integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==", "funding": [ { "type": "opencollective", @@ -12162,10 +12163,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -20338,15 +20340,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -21526,10 +21529,11 @@ "license": "ISC" }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", - "dev": true + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", diff --git a/package.json b/package.json index 8b7a83da2..27559de03 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@silvermine/videojs-chromecast": "^1.5.0", "@tanstack/react-query": "^5.56.2", "@tanstack/react-query-devtools": "^5.56.2", + "caniuse-lite": "^1.0.30001689", "clsx": "^2.1.1", "cookie": "^0.7.0", "dayjs": "^1.11.13", @@ -144,6 +145,13 @@ "overrides": { "next-pwa": { "rollup": "$rollup" + }, + "cross-spawn": "^7.0.5", + "next": { + "nanoid": "^3.3.8" + }, + "express": { + "path-to-regexp": "^0.1.12" } }, "nextBundleAnalysis": { diff --git a/src/components/molecules/loadingIndicator.spec.tsx b/src/components/molecules/loadingIndicator.spec.tsx index 4394b1d55..75b58e338 100644 --- a/src/components/molecules/loadingIndicator.spec.tsx +++ b/src/components/molecules/loadingIndicator.spec.tsx @@ -5,7 +5,7 @@ import LoadingIndicator from '~components/molecules/loadingIndicator'; import renderWithProviders from '~lib/test/renderWithProviders'; import useRouterLoading from '~src/lib/hooks/useRouterLoading'; -jest.mock('~lib/useRouterLoading'); +jest.mock('~lib/hooks/useRouterLoading'); const mockUseRouterLoading = useRouterLoading as jest.Mock; diff --git a/src/components/organisms/bookSelector.tsx b/src/components/organisms/bookSelector.tsx deleted file mode 100644 index b069fd9c0..000000000 --- a/src/components/organisms/bookSelector.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -import { PassageNavigationFragment } from './__generated__/passageNavigation'; - -type Props = { - books: Array; -}; - -export default function BookSelector({ books }: Props) {} diff --git a/src/components/organisms/passageNavigation.module.scss b/src/components/organisms/passageNavigation.module.scss deleted file mode 100644 index 52bf41f30..000000000 --- a/src/components/organisms/passageNavigation.module.scss +++ /dev/null @@ -1,105 +0,0 @@ -@import '../../styles/common.scss'; - -.wrapper { - background-color: $ts-bibleVersionH; -} - -.wrapper ul { - margin: 0; - padding: 0; -} - -.wrapper button { - background: none; - border: none; - padding: 10px 24px; -} - -.wrapper .books > li:first-child button { - padding-top: 16px; -} - -.wrapper .books > li:last-child button { - padding-bottom: 16px; -} - -// .wrapper .books:global(.grid) { -// display: grid; -// grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); -// } - -.wrapper li, -.wrapper li button, -.wrapper li a { - color: $ts-lightTone; - font-weight: 700; - font-size: 16px; - line-height: 19.2px; - text-transform: uppercase; - list-style-type: none; -} - -.wrapper :global(.active) > button { - color: $ts-salmon; -} - -.switch > :global(button.active) { - color: $ts-salmon; -} - -.wrapper, -.wrapper .books, -.wrapper .chapters { - width: 100%; -} - -// .books:global(.grid) .chapters { -// grid-column: 1 / -1; -// } - -.chapters { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); - background-color: $ts-darkened-bibleH; - border-top: 1px solid $ts-salmon; - border-bottom: 1px solid $ts-salmon; -} - -.chapter a { - width: 100%; - height: 100%; - padding: 24px 0; - display: block; - text-align: center; -} - -.chapter a:hover { - text-decoration: none; - color: $ts-salmon; -} - -.books button:hover { - color: $ts-salmon; -} - -.switch { - border-bottom: 1px solid $ts-salmon; -} - -.switch > button { - color: $ts-lightTone; - font-weight: 700; - font-size: 12px; - line-height: 14.4px; - text-transform: uppercase; -} - -.chaptersWrapper { - width: 100%; - float: left; -} - -.books:global(.grid) .book { - width: 70px; - display: inline-block; -} diff --git a/src/components/organisms/passageNavigation.tsx b/src/components/organisms/passageNavigation.tsx deleted file mode 100644 index bf2f7d93f..000000000 --- a/src/components/organisms/passageNavigation.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import clsx from 'clsx'; -import React, { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; - -import { useLocalStorage } from '~src/lib/hooks/useLocalStorage'; - -import { PassageNavigationFragment } from './__generated__/passageNavigation'; -import BookGrid from './bookGrid'; -import BookList from './bookList'; -import styles from './passageNavigation.module.scss'; - -type Props = { - books: Array; -}; - -// FIXME -const OT = [ - 'genesis', - 'exodus', - 'leviticus', - 'numbers', - 'deuteronomy', - 'joshua', - 'judges', - 'ruth', - '1 samuel', - '2 samuel', - '1 kings', - '2 kings', - '1 chronicles', - '2 chronicles', - 'ezra', - 'nehemiah', - 'esther', - 'job', - 'psalms', - 'proverbs', - 'ecclesiastes', - 'song of solomon', - 'isaiah', - 'jeremiah', - 'lamentations', - 'ezekiel', - 'daniel', - 'hosea', - 'joel', - 'amos', - 'obadiah', - 'jonah', - 'micah', - 'nahum', - 'habakkuk', - 'zephaniah', - 'haggai', - 'zechariah', - 'malachi', -]; - -export default function PassageNavigation({ books }: Props): JSX.Element { - const [selectedBook, setSelectedBook] = useState( - null, - ); - const [selectedView, setSelectedView] = useLocalStorage<'grid' | 'list'>( - 'passageNavLayout', - 'grid', - ); - - return ( -
-
- - -
- - {selectedView === 'list' ? ( - - ) : ( - <> - - OT.includes(book.title.toLocaleLowerCase()), - )} - selectedBook={selectedBook} - selectBook={setSelectedBook} - /> - !OT.includes(book.title.toLocaleLowerCase()), - )} - selectedBook={selectedBook} - selectBook={setSelectedBook} - /> - - )} -
- ); -} diff --git a/src/components/organisms/bookGrid.tsx b/src/components/organisms/passageNavigation/bookGrid.tsx similarity index 82% rename from src/components/organisms/bookGrid.tsx rename to src/components/organisms/passageNavigation/bookGrid.tsx index 57352341a..3eb2c100b 100644 --- a/src/components/organisms/bookGrid.tsx +++ b/src/components/organisms/passageNavigation/bookGrid.tsx @@ -1,9 +1,9 @@ import clsx from 'clsx'; import React from 'react'; -import { PassageNavigationFragment } from './__generated__/passageNavigation'; +import { PassageNavigationFragment } from './__generated__/index'; import ChapterGrid from './chapterGrid'; -import styles from './passageNavigation.module.scss'; +import styles from './index.module.scss'; type Props = { books: Array; @@ -13,7 +13,7 @@ type Props = { export default function BookGrid({ books, selectedBook, selectBook }: Props) { return ( -
    +
      {books.map((book) => { const chapters = book.recordings.nodes; diff --git a/src/components/organisms/bookList.tsx b/src/components/organisms/passageNavigation/bookList.tsx similarity index 86% rename from src/components/organisms/bookList.tsx rename to src/components/organisms/passageNavigation/bookList.tsx index a7ab35609..82acdfca8 100644 --- a/src/components/organisms/bookList.tsx +++ b/src/components/organisms/passageNavigation/bookList.tsx @@ -1,9 +1,9 @@ import clsx from 'clsx'; import React from 'react'; -import { PassageNavigationFragment } from './__generated__/passageNavigation'; +import { PassageNavigationFragment } from './__generated__/index'; import ChapterGrid from './chapterGrid'; -import styles from './passageNavigation.module.scss'; +import styles from './index.module.scss'; type Props = { books: Array; diff --git a/src/components/organisms/chapterGrid.tsx b/src/components/organisms/passageNavigation/chapterGrid.tsx similarity index 53% rename from src/components/organisms/chapterGrid.tsx rename to src/components/organisms/passageNavigation/chapterGrid.tsx index 99d02d770..131848953 100644 --- a/src/components/organisms/chapterGrid.tsx +++ b/src/components/organisms/passageNavigation/chapterGrid.tsx @@ -1,17 +1,24 @@ import React from 'react'; import Link from '~components/atoms/linkWithoutPrefetch'; +import { useLocalStorage } from '~src/lib/hooks/useLocalStorage'; -import { PassageNavigationFragment } from './__generated__/passageNavigation'; -import styles from './passageNavigation.module.scss'; +import { PassageNavigationFragment } from './__generated__/index'; +import styles from './index.module.scss'; type Chapter = NonNullable[0]; +type ChapterId = Chapter['id']; type Props = { chapters: Array; }; export default function ChapterGrid({ chapters }: Props) { + const [selectedChapterId] = useLocalStorage( + 'selectedChapterId', + null, + ); + return (
      • @@ -19,7 +26,14 @@ export default function ChapterGrid({ chapters }: Props) { const n = Number(chapter.title.split(' ').pop()); return (
      • - {n} + + {n} +
      • ); })} diff --git a/src/components/organisms/passageNavigation.graphql b/src/components/organisms/passageNavigation/index.graphql similarity index 100% rename from src/components/organisms/passageNavigation.graphql rename to src/components/organisms/passageNavigation/index.graphql diff --git a/src/components/organisms/passageNavigation.logic.ts b/src/components/organisms/passageNavigation/index.logic.ts similarity index 100% rename from src/components/organisms/passageNavigation.logic.ts rename to src/components/organisms/passageNavigation/index.logic.ts diff --git a/src/components/organisms/passageNavigation/index.module.scss b/src/components/organisms/passageNavigation/index.module.scss new file mode 100644 index 000000000..183a55e4f --- /dev/null +++ b/src/components/organisms/passageNavigation/index.module.scss @@ -0,0 +1,159 @@ +@import '../../../styles/common.scss'; + +.base { + --bibleAccent: #{$ts-red}; + --bibleBackground: #{$ts-bibleB}; + --bibleHatTextColor: #{$ts-white}; + --bibleHatBackground: #{$ts-bibleH}; + --bibleHatHover: #{$ts-darkened-bibleH}; + --bibleTextColor: #{$ts-dark}; + --bibleTextSecondaryColor: #{$ts-midTone}; + --bibleWavesColor: #{$ts-lightTone}; +} + +.hat { + background-color: var(--bibleHatBackground); + padding: 16px 24px; + display: flex; + text-decoration: none; + color: var(--bibleHatTextColor, --bibleTextColor); + gap: 10px; + align-items: center; + + h4 { + margin: 0; + font-size: 16px; + } +} + +.historyButton { + margin-left: auto; +} + +.dropdownButton { + text-decoration: none; +} + +.content { + display: flex; + flex-direction: column; + + p { + margin-top: 8px; + margin-bottom: 16px; + } + + @media (min-width: $breakpoint-xl) { + flex-direction: row; + } +} + +.content { + background-color: $ts-bibleVersionH; + padding: 12px 24px; +} + +.content ul { + margin: 0; + padding: 0; +} + +.content button { + background: none; + border: none; + padding: 18px 0px; +} + +.content .books > li:first-child button { + padding-top: 16px; +} + +.content .books > li:last-child button { + padding-bottom: 16px; +} + +// .content .books:global(.grid) { +// display: grid; +// grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); +// } + +.content li, +.content li button, +.content li a { + color: $ts-lightTone; + font-weight: 700; + font-size: 16px; + line-height: 19.2px; + text-transform: uppercase; + list-style-type: none; +} + +.content :global(.active) > button { + color: $ts-salmon; +} + +.switch > :global(button.active) { + color: $ts-salmon; +} + +.active { + color: $ts-salmon !important; +} + +.content, +.content .books, +.content .chapters { + width: 100%; +} + +// .books:global(.grid) .chapters { +// grid-column: 1 / -1; +// } + +.chapters { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); + + border-top: 1px solid $ts-salmon; + border-bottom: 1px solid $ts-salmon; +} + +.chapter a { + width: 100%; + height: 100%; + padding: 24px 0; + display: block; + text-align: center; +} + +.chapter a:hover { + text-decoration: none; + color: $ts-salmon; +} + +.books button:hover { + color: $ts-salmon; +} + +.switch > button { + color: $ts-lightTone; + font-weight: 700; + font-size: 12px; + line-height: 14.4px; + text-transform: uppercase; + padding-right: 12px; +} + +.chaptersWrapper { + width: 100%; + float: left; +} + +.books.grid { + border-top: 1px solid $ts-dark; +} + +.books.grid .book { + width: 16%; + display: inline-block; +} diff --git a/src/components/organisms/passageNavigation/index.tsx b/src/components/organisms/passageNavigation/index.tsx new file mode 100644 index 000000000..af3e488c4 --- /dev/null +++ b/src/components/organisms/passageNavigation/index.tsx @@ -0,0 +1,244 @@ +import clsx from 'clsx'; +import React, { ReactNode, useEffect, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; + +import IconDisclosure from '~public/img/icons/icon-disclosure.svg'; +import IconSearch from '~public/img/icons/icon-search.svg'; +import BibleVersionTypeLockup from '~src/components/molecules/bibleVersionTypeLockup'; +import Button from '~src/components/molecules/button'; +import Dropdown from '~src/components/molecules/dropdown'; +import IconButton from '~src/components/molecules/iconButton'; +import { GetAudiobibleIndexDataQuery } from '~src/containers/bible/__generated__'; +import { BaseColors } from '~src/lib/constants'; +import { getBibleAcronym } from '~src/lib/getBibleAcronym'; +import { useLocalStorage } from '~src/lib/hooks/useLocalStorage'; + +import BookGrid from './bookGrid'; +import BookList from './bookList'; +import styles from './index.module.scss'; + +export type Version = NonNullable< + GetAudiobibleIndexDataQuery['collections']['nodes'] +>[0]; + +type Book = NonNullable[0]; +type Chapter = NonNullable[0]; + +type BookId = string | number | null; +type ChapterId = string | number; + +type Props = { + versions: Array; + chapterId?: ChapterId; + children?: ReactNode; +}; + +// FIXME +const OT = [ + 'genesis', + 'exodus', + 'leviticus', + 'numbers', + 'deuteronomy', + 'joshua', + 'judges', + 'ruth', + '1 samuel', + '2 samuel', + '1 kings', + '2 kings', + '1 chronicles', + '2 chronicles', + 'ezra', + 'nehemiah', + 'esther', + 'job', + 'psalms', + 'proverbs', + 'ecclesiastes', + 'song of solomon', + 'isaiah', + 'jeremiah', + 'lamentations', + 'ezekiel', + 'daniel', + 'hosea', + 'joel', + 'amos', + 'obadiah', + 'jonah', + 'micah', + 'nahum', + 'habakkuk', + 'zephaniah', + 'haggai', + 'zechariah', + 'malachi', +]; + +function getBibleData( + versions: Array, + chapterId: ChapterId, +): [Version, Book, Chapter] { + for (const version of versions) { + for (const book of version.sequences.nodes || []) { + const chapter = book.recordings.nodes?.find((r) => r.id === chapterId); + if (chapter) { + return [version, book, chapter]; + } + } + } + throw Error("Couldn't find the chapter"); +} + +function getLabelText( + versions: Array, + chapterId: ChapterId | null, +): string { + if (chapterId) { + const [_version, _book, chapter] = getBibleData(versions, chapterId); + + return `${chapter.title}`; + } + + return `Bible`; +} + +export default function PassageNavigation({ + versions, + chapterId, + children, +}: Props): ReactNode { + const [open, setOpen] = useState(!children); + + const [selectedVersion, setSelectedVersion] = useState(versions[0]); + + const books = selectedVersion.sequences.nodes || []; + + const [selectedBookId, setSelectedBookId] = useState(books[0].id); + + const [selectedChapterId, setSelectedChapterId] = + useLocalStorage('selectedChapterId', chapterId || null); + + useEffect(() => { + if (chapterId !== undefined) { + setSelectedChapterId(chapterId); + } + + if (selectedChapterId !== null) { + const [version, book] = getBibleData(versions, selectedChapterId); + setSelectedVersion(version); + setSelectedBookId(book.id); + } + }, [selectedChapterId, chapterId, setSelectedChapterId, versions]); + + const [selectedView, setSelectedView] = useLocalStorage<'grid' | 'list'>( + 'passageNavLayout', + 'grid', + ); + + return ( +
        +
        setOpen(!open)}> + + + + + ( +
        + )} + + +
        + + {open || !children ? ( +
        +
        + + +
        + + {selectedView === 'list' ? ( + + ) : ( + <> + + OT.includes(book.title.toLocaleLowerCase()), + )} + selectedBook={selectedBookId} + selectBook={setSelectedBookId} + /> + !OT.includes(book.title.toLocaleLowerCase()), + )} + selectedBook={selectedBookId} + selectBook={setSelectedBookId} + /> + + )} +
        + ) : ( + children + )} + + ); +} diff --git a/src/components/organisms/recording.tsx b/src/components/organisms/recording.tsx index 15eccc4a5..339cbc4c7 100644 --- a/src/components/organisms/recording.tsx +++ b/src/components/organisms/recording.tsx @@ -10,7 +10,6 @@ import HorizontalRule from '~components/atoms/horizontalRule'; import LineHeading from '~components/atoms/lineHeading'; import Link from '~components/atoms/linkWithoutPrefetch'; import { TeaseRecordingFragment } from '~components/molecules/__generated__/teaseRecording'; -import BibleVersionTypeLockup from '~components/molecules/bibleVersionTypeLockup'; import Button from '~components/molecules/button'; import CopyrightInfo from '~components/molecules/copyrightInfo'; import DefinitionList, { @@ -35,11 +34,13 @@ import { RecordingContentType, SequenceContentType, } from '~src/__generated__/graphql'; +import { BibleIndexProps } from '~src/containers/bible'; import useLanguageRoute from '~src/lib/hooks/useLanguageRoute'; import { analytics } from '../../lib/analytics'; import PlaylistTypeLockup from '../molecules/playlistTypeLockup'; import { RecordingFragment } from './__generated__/recording'; +import PassageNavigation from './passageNavigation'; import styles from './recording.module.scss'; interface RecordingProps { @@ -52,17 +53,103 @@ interface RecordingProps { }; } -export function Recording({ +export function Recording( + params: (RecordingProps & BibleIndexProps) | RecordingProps, +): JSX.Element { + const intl = useIntl(); + const { recording, overrideSequence } = params; + const { id, imageWithFallback, contentType, sponsor, speakers, writers } = + recording; + return ( + + + + + + name).join(','), + }, + ), + [RecordingContentType.Sermon]: intl.formatMessage( + { + id: 'sermonDetailPage__openGraphDescription_sermon', + defaultMessage: 'Teaching by {speakerName}', + }, + { + speakerName: speakers.map(({ name }) => name).join(','), + }, + ), + [RecordingContentType.Story]: intl.formatMessage( + { + id: 'sermonDetailPage__openGraphDescription_story', + defaultMessage: 'Story by {writerName}', + }, + { + writerName: writers.map(({ name }) => name).join(','), + }, + ), + }[contentType] + } + /> + + + + {'data' in params ? ( + + + + ) : ( + + )} + + ); +} + +function RecordingInner({ recording, overrideSequence, }: RecordingProps): JSX.Element { const intl = useIntl(); const { - id, contentType, collection, description, - imageWithFallback, recordingDate, sequence, sequenceIndex, @@ -254,112 +341,44 @@ export function Recording({ ); return ( - - - - - - name).join(','), - }, - ), - [RecordingContentType.Sermon]: intl.formatMessage( - { - id: 'sermonDetailPage__openGraphDescription_sermon', - defaultMessage: 'Teaching by {speakerName}', - }, - { - speakerName: speakers.map(({ name }) => name).join(','), - }, - ), - [RecordingContentType.Story]: intl.formatMessage( - { - id: 'sermonDetailPage__openGraphDescription_story', - defaultMessage: 'Story by {writerName}', - }, - { - writerName: writers.map(({ name }) => name).join(','), - }, - ), - }[contentType] - } - /> - - - {isBibleChapter && recording.collection ? ( - - - -

        {recording.sequence?.title}

        -
        - - ) : overrideSequence && overrideSequence.playlistId ? ( - makeHat( - , - startCase(overrideSequence.title), - overrideSequence.publicPlaylist - ? root - .lang(languageRoute) - .playlists.playlist(overrideSequence.playlistId) - .get() - : root - .lang(languageRoute) - .library.playlists(overrideSequence.playlistId) - .get(), - ) - ) : overrideSequence ? ( - makeHat( - , - startCase(overrideSequence.title), - root.lang(languageRoute).songs.book(overrideSequence.title).get(), - ) - ) : ( - recording.sequence && - makeHat( - , - recording.sequence.title, - recording.sequence.canonicalPath, - ) - )} + <> + {isBibleChapter && recording.collection + ? '' + : overrideSequence && overrideSequence.playlistId + ? makeHat( + , + startCase(overrideSequence.title), + overrideSequence.publicPlaylist + ? root + .lang(languageRoute) + .playlists.playlist(overrideSequence.playlistId) + .get() + : root + .lang(languageRoute) + .library.playlists(overrideSequence.playlistId) + .get(), + ) + : overrideSequence + ? makeHat( + , + startCase(overrideSequence.title), + root + .lang(languageRoute) + .songs.book(overrideSequence.title) + .get(), + ) + : recording.sequence && + makeHat( + , + recording.sequence.title, + recording.sequence.canonicalPath, + )}
        {isBibleChapter && isShowingTranscript && transcript ? ( @@ -590,6 +609,6 @@ export function Recording({
        )}
        -
        + ); } diff --git a/src/containers/bible/book.tsx b/src/containers/bible/book.tsx index 8316b6d45..853d3af96 100644 --- a/src/containers/bible/book.tsx +++ b/src/containers/bible/book.tsx @@ -8,7 +8,6 @@ import Link from '~components/atoms/linkWithoutPrefetch'; import withFailStates from '~components/HOCs/withFailStates'; import { PlayerFragment } from '~components/molecules/__generated__/player'; import { SequenceNavFragment } from '~components/molecules/__generated__/sequenceNav'; -import BibleVersionTypeLockup from '~components/molecules/bibleVersionTypeLockup'; import Button from '~components/molecules/button'; import ContentWidthLimiter from '~components/molecules/contentWidthLimiter'; import DefinitionList, { @@ -24,6 +23,7 @@ import root from '~lib/routes'; import IconBack from '~public/img/icons/icon-back-light.svg'; import IconBlog from '~public/img/icons/icon-blog-light-small.svg'; import { RecordingContentType } from '~src/__generated__/graphql'; +import BibleVersionTypeLockup from '~src/components/molecules/bibleVersionTypeLockup'; import useLanguageRoute from '~src/lib/hooks/useLanguageRoute'; import { IBibleBook, diff --git a/src/containers/bible/index.tsx b/src/containers/bible/index.tsx index 4f261afd8..773c5c686 100644 --- a/src/containers/bible/index.tsx +++ b/src/containers/bible/index.tsx @@ -1,85 +1,19 @@ import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { BaseColors } from '~lib/constants'; -import IconDisclosure from '~public/img/icons/icon-disclosure-light-small.svg'; -import IconSearch from '~public/img/icons/icon-search.svg'; import withFailStates from '~src/components/HOCs/withFailStates'; -import BibleVersionTypeLockup from '~src/components/molecules/bibleVersionTypeLockup'; -import Button from '~src/components/molecules/button'; -import Dropdown from '~src/components/molecules/dropdown'; -import IconButton from '~src/components/molecules/iconButton'; import Tease from '~src/components/molecules/tease'; -import PassageNavigation from '~src/components/organisms/passageNavigation'; -import { getBibleAcronym } from '~src/lib/getBibleAcronym'; -import { useLocalStorage } from '~src/lib/hooks/useLocalStorage'; +import PassageNavigation, { + Version, +} from '~src/components/organisms/passageNavigation'; -import { GetAudiobibleIndexDataQuery } from './__generated__'; -import styles from './index.module.scss'; - -type Version = NonNullable< - GetAudiobibleIndexDataQuery['collections']['nodes'] ->[0]; - -export type BibleIndexProps = { +export interface BibleIndexProps { data: Array; -}; +} function Bible({ data }: BibleIndexProps): JSX.Element { - const [selected, setSelected] = useLocalStorage( - 'bibleVersion', - data[0], - ); - return ( - -
        - - - - - ( -
        - )} - - - - -
        - -
        + + ); } diff --git a/src/lib/getBibleStaticProps.ts b/src/lib/getBibleStaticProps.ts new file mode 100644 index 000000000..d1c7f23bd --- /dev/null +++ b/src/lib/getBibleStaticProps.ts @@ -0,0 +1,104 @@ +import { Language } from '~src/__generated__/graphql'; +import { BibleIndexProps } from '~src/containers/bible'; +import { getAudiobibleIndexData } from '~src/containers/bible/__generated__'; +import { BOOK_ID_MAP } from '~src/services/fcbh/constants'; +import { getBibleBookChapters } from '~src/services/fcbh/getBibleBookChapters'; +import { getBibles } from '~src/services/fcbh/getBibles'; +import { IBibleBookChapter, IBibleVersion } from '~src/services/fcbh/types'; + +import root from './routes'; + +type ApiBible = BibleIndexProps['data'][0]; +type ApiBook = NonNullable[0]; +type ApiChapter = NonNullable[0]; + +// https://www.audioverse.org/en/bibles/ENGKJV2/GEN/1 + +async function transform( + languageRoute: string, + bible: IBibleVersion, +): Promise { + const books = Object.keys(BOOK_ID_MAP).map( + async (bookId, i): Promise => { + const testament = i < 39 ? 'OT' : 'NT'; + const bbChapters = await getBibleBookChapters( + bible.id, + testament, + bookId, + ); + const chapters = bbChapters.map( + (bbChapter: IBibleBookChapter): ApiChapter => { + return { + id: bbChapter.id, + title: bbChapter.title, + canonicalPath: root + .lang(languageRoute) + .bibles.versionId(bible.id) + .bookId(bookId) + .chapterNumber(bbChapter.number) + .get(), + }; + }, + ); + + const title = bbChapters[0].book_name; + + if (!title) { + console.log({ bible, testament, bookId, bbChapters }); + throw new Error( + `Could not determine book title: ${bible.id} ${testament} ${bookId}`, + ); + } + + return { + id: bookId, + title, + recordings: { + nodes: chapters, + }, + }; + }, + ); + return { + ...bible, + sequences: { + nodes: await Promise.all(books), + }, + }; +} + +export async function getFcbhBibles( + languageRoute: string, +): Promise { + try { + const response = await getBibles(); + + if (!response) { + return null; + } + + return Promise.all(response.map((b) => transform(languageRoute, b))); + } catch (e) { + console.log(e); + return null; + } +} + +export async function getApiBibles( + languageId: Language, +): Promise { + const apiData = await getAudiobibleIndexData({ + language: languageId, + }).catch(() => null); + + return apiData?.collections.nodes || null; +} + +export function concatBibles( + first: ApiBible[] | null, + second: ApiBible[] | null, +): ApiBible[] { + return [...(first || []), ...(second || [])].sort((a, b) => + a.title.localeCompare(b.title), + ); +} diff --git a/src/pages/[language]/bibles/chapters/[id]/[[...slugs]].ts b/src/pages/[language]/bibles/chapters/[id]/[[...slugs]].ts index 2a8bb2910..2a64e741e 100644 --- a/src/pages/[language]/bibles/chapters/[id]/[[...slugs]].ts +++ b/src/pages/[language]/bibles/chapters/[id]/[[...slugs]].ts @@ -14,16 +14,24 @@ import { import { REVALIDATE, REVALIDATE_FAILURE } from '~lib/constants'; import { getDetailStaticPaths } from '~lib/getDetailStaticPaths'; import { RecordingContentType } from '~src/__generated__/graphql'; +import { BibleIndexProps } from '~src/containers/bible'; +import { + concatBibles, + getApiBibles, + getFcbhBibles, +} from '~src/lib/getBibleStaticProps'; +import { getLanguageIdByRoute } from '~src/lib/getLanguageIdByRoute'; export default Recording; export async function getStaticProps({ params, -}: GetStaticPropsContext<{ id: string }>): Promise< +}: GetStaticPropsContext<{ id: string; language: string }>): Promise< GetStaticPropsResult< { recording: RecordingFragment; - } & IBaseProps + } & IBaseProps & + BibleIndexProps > > { const { recording } = await getAudiobibleBookDetailData({ @@ -39,12 +47,27 @@ export async function getStaticProps({ }; } + const languageRoute = params?.language || 'en'; + const languageId = getLanguageIdByRoute(languageRoute); + + const apiBibles = await getApiBibles(languageId); + + if (!apiBibles) { + return { + notFound: true, + revalidate: REVALIDATE_FAILURE, + }; + } + + const fcbhBibles = await getFcbhBibles(languageRoute); + return { props: { + data: concatBibles(fcbhBibles, apiBibles), recording, title: recording?.title, }, - revalidate: REVALIDATE, + revalidate: fcbhBibles ? REVALIDATE : REVALIDATE_FAILURE, }; } diff --git a/src/pages/[language]/bibles/index.tsx b/src/pages/[language]/bibles/index.tsx index cfe92d8cf..caadf2a41 100644 --- a/src/pages/[language]/bibles/index.tsx +++ b/src/pages/[language]/bibles/index.tsx @@ -7,102 +7,17 @@ import { import { IBaseProps } from '~containers/base'; import Bible, { BibleIndexProps } from '~containers/bible'; import { LANGUAGES, REVALIDATE, REVALIDATE_FAILURE } from '~lib/constants'; -import getIntl from '~lib/getIntl'; -import { getLanguageIdByRoute } from '~lib/getLanguageIdByRoute'; import root from '~lib/routes'; -import { Language } from '~src/__generated__/graphql'; -import { getAudiobibleIndexData } from '~src/containers/bible/__generated__'; -import { BOOK_ID_MAP } from '~src/services/fcbh/constants'; -import { getBibleBookChapters } from '~src/services/fcbh/getBibleBookChapters'; -import { getBibles } from '~src/services/fcbh/getBibles'; -import { IBibleBookChapter, IBibleVersion } from '~src/services/fcbh/types'; +import { + concatBibles, + getApiBibles, + getFcbhBibles, +} from '~src/lib/getBibleStaticProps'; +import getIntl from '~src/lib/getIntl'; +import { getLanguageIdByRoute } from '~src/lib/getLanguageIdByRoute'; export default Bible; -type ApiBible = BibleIndexProps['data'][0]; -type ApiBook = NonNullable[0]; -type ApiChapter = NonNullable[0]; - -// https://www.audioverse.org/en/bibles/ENGKJV2/GEN/1 - -async function transform( - languageRoute: string, - bible: IBibleVersion, -): Promise { - const books = Object.keys(BOOK_ID_MAP).map( - async (bookId, i): Promise => { - const testament = i < 39 ? 'OT' : 'NT'; - const bbChapters = await getBibleBookChapters( - bible.id, - testament, - bookId, - ); - const chapters = bbChapters.map( - (bbChapter: IBibleBookChapter): ApiChapter => { - return { - id: bbChapter.id, - title: bbChapter.title, - canonicalPath: root - .lang(languageRoute) - .bibles.versionId(bible.id) - .bookId(bookId) - .chapterNumber(bbChapter.number) - .get(), - }; - }, - ); - - const title = bbChapters[0].book_name; - - if (!title) { - console.log({ bible, testament, bookId, bbChapters }); - throw new Error( - `Could not determine book title: ${bible.id} ${testament} ${bookId}`, - ); - } - - return { - id: bookId, - title, - recordings: { - nodes: chapters, - }, - }; - }, - ); - return { - ...bible, - sequences: { - nodes: await Promise.all(books), - }, - }; -} - -async function getFcbhBibles( - languageRoute: string, -): Promise { - try { - const response = await getBibles(); - - if (!response) { - return null; - } - - return Promise.all(response.map((b) => transform(languageRoute, b))); - } catch (e) { - console.log(e); - return null; - } -} - -async function getApiBibles(languageId: Language): Promise { - const apiData = await getAudiobibleIndexData({ - language: languageId, - }).catch(() => null); - - return apiData?.collections.nodes || null; -} - export async function getStaticProps({ params, }: GetStaticPropsContext<{ language: string }>): Promise< @@ -110,6 +25,9 @@ export async function getStaticProps({ > { const languageRoute = params?.language || 'en'; const languageId = getLanguageIdByRoute(languageRoute); + + const intl = await getIntl(languageId); + const apiBibles = await getApiBibles(languageId); if (!apiBibles) { @@ -120,13 +38,10 @@ export async function getStaticProps({ } const fcbhBibles = await getFcbhBibles(languageRoute); - const intl = await getIntl(languageId); return { props: { - data: [...(fcbhBibles || []), ...apiBibles].sort((a, b) => - a.title.localeCompare(b.title), - ), + data: concatBibles(fcbhBibles, apiBibles), title: intl.formatMessage({ id: 'bible__title', defaultMessage: 'Bible',