diff --git a/src/app/components/accordion-group/accordion-group.js b/src/app/components/accordion-group/accordion-group.js index b22acb216..6c3e6d5e8 100644 --- a/src/app/components/accordion-group/accordion-group.js +++ b/src/app/components/accordion-group/accordion-group.js @@ -109,7 +109,7 @@ export default function AccordionGroup({ items, accordionProps={allowZeroExpanded: true}, noScroll=false, - forwardOnChange, + forwardOnChange=false, preExpanded=[], ...props }) { diff --git a/src/app/components/carousel/carousel.js b/src/app/components/carousel/carousel.js index fbece06f8..9a3bd6010 100644 --- a/src/app/components/carousel/carousel.js +++ b/src/app/components/carousel/carousel.js @@ -41,7 +41,8 @@ function HoverText({which, thing}) { export default function Carousel({ atATime = 1, children, - hoverTextThing + hoverTextThing, + ref }) { const slides = React.useMemo( () => { @@ -52,7 +53,7 @@ export default function Carousel({ ); return ( - + {slides} diff --git a/src/app/contexts/subject-category.ts b/src/app/contexts/subject-category.ts index eb8fd52fa..d7871c8d4 100644 --- a/src/app/contexts/subject-category.ts +++ b/src/app/contexts/subject-category.ts @@ -11,7 +11,7 @@ type InputItem = { subject_color: string; }; -type Category = { +export type Category = { value: string; cms: string; html: string; diff --git a/src/app/pages/subjects/new/about-openstax.js b/src/app/pages/subjects/new/about-openstax.tsx similarity index 55% rename from src/app/pages/subjects/new/about-openstax.js rename to src/app/pages/subjects/new/about-openstax.tsx index 29516175f..654be9fc2 100644 --- a/src/app/pages/subjects/new/about-openstax.js +++ b/src/app/pages/subjects/new/about-openstax.tsx @@ -1,16 +1,32 @@ import React from 'react'; -import useSubjectsContext from './context'; +import useSubjectsContext, {ImageData} from './context'; import RawHTML from '~/components/jsx-helpers/raw-html'; import useOptimizedImage from '~/helpers/use-optimized-image'; import './about-openstax.scss'; +export type AboutOsData = { + heading: string; + osText: string; + linkText: string; + linkHref: string; + image: ImageData; +}; + export default function AboutOpenStax({ - forceButtonUrl='', - forceButtonText='', + forceButtonUrl = '', + forceButtonText = '', aboutOs +}: { + forceButtonUrl?: string; + forceButtonText?: string; + aboutOs: AboutOsData; }) { const { - heading, osText: paragraph, linkText: buttonText, linkHref: buttonUrl, image: {file: imgSrc} + heading, + osText: paragraph, + linkText: buttonText, + linkHref: buttonUrl, + image: {file: imgSrc} } = aboutOs; const url = forceButtonUrl || buttonUrl; const text = forceButtonText || buttonText; @@ -21,7 +37,9 @@ export default function AboutOpenStax({

{heading}

- {text} + + {text} +
@@ -31,7 +49,5 @@ export default function AboutOpenStax({ export function AllSubjectsAboutOpenStax() { const {aboutOs} = useSubjectsContext(); - return ( - - ); + return ; } diff --git a/src/app/pages/subjects/new/context.tsx b/src/app/pages/subjects/new/context.ts similarity index 97% rename from src/app/pages/subjects/new/context.tsx rename to src/app/pages/subjects/new/context.ts index c10fe80e2..e63c05ec6 100644 --- a/src/app/pages/subjects/new/context.tsx +++ b/src/app/pages/subjects/new/context.ts @@ -5,6 +5,7 @@ import useLanguageContext from '~/contexts/language'; import type {LocaleEntry} from '~/components/language-selector/language-selector'; import type {InfoBoxValues} from './info-boxes'; import type {TutorValue} from './tutor-ad'; +import type {AboutOsData} from './about-openstax'; import {toNumber} from 'lodash'; type DevStandardKeys = 'devStandard1Heading'; @@ -41,6 +42,7 @@ type SubjectsPageData = { } ]; philanthropicSupport: string; + aboutOs: [{value: AboutOsData}]; }; // The Page data before DevStandardPair is translated to aboutBlurbs diff --git a/src/app/pages/subjects/new/hero.tsx b/src/app/pages/subjects/new/hero.tsx index 793e3f273..003f42f5a 100644 --- a/src/app/pages/subjects/new/hero.tsx +++ b/src/app/pages/subjects/new/hero.tsx @@ -17,7 +17,9 @@ export default function Hero() {

{heading}

diff --git a/src/app/pages/subjects/new/specific/blog-posts.js b/src/app/pages/subjects/new/specific/blog-posts.tsx similarity index 75% rename from src/app/pages/subjects/new/specific/blog-posts.js rename to src/app/pages/subjects/new/specific/blog-posts.tsx index e5218f4d1..a9d7ec0fc 100644 --- a/src/app/pages/subjects/new/specific/blog-posts.js +++ b/src/app/pages/subjects/new/specific/blog-posts.tsx @@ -6,8 +6,16 @@ import useOptimizedImage from '~/helpers/use-optimized-image'; import useEnglishSubject from './use-english-subject'; import {useIntl} from 'react-intl'; import './blog-posts.scss'; +import { assertDefined } from '~/helpers/data'; -function Card({article_image: image, title: linkText, slug}) { +type Blurb = { + id: number; + article_image: string; + title: string; + slug: string; +} + +function Card({article_image: image, title: linkText, slug}: Omit) { const link = `/blog/${slug}`; const optimizedImage = useOptimizedImage(image, 400); @@ -21,23 +29,22 @@ function Card({article_image: image, title: linkText, slug}) { function BlogPosts() { const { - blogHeader: {content: {heading, blogDescription, linkText, linkHref}} - } = useSpecificSubjectContext(); + content: {heading, blogDescription, linkText, linkHref} + } = assertDefined(useSpecificSubjectContext().blogHeader); const cms = useEnglishSubject(); - const blurbs = useDataFromSlug(`search/?subjects=${cms}`) || []; + const blurbs: Blurb[] = useDataFromSlug(`search/?subjects=${cms}`) || []; const intl = useIntl(); return ( blurbs.length ? - {blurbs.map((blurb) => )} + {blurbs.map((blurb) => )} :

{intl.formatMessage({id: 'subject.noBlog'})}

); diff --git a/src/app/pages/subjects/new/specific/book-viewer.js b/src/app/pages/subjects/new/specific/book-viewer.tsx similarity index 74% rename from src/app/pages/subjects/new/specific/book-viewer.js rename to src/app/pages/subjects/new/specific/book-viewer.tsx index e7bded069..949b7607e 100644 --- a/src/app/pages/subjects/new/specific/book-viewer.js +++ b/src/app/pages/subjects/new/specific/book-viewer.tsx @@ -1,26 +1,31 @@ import React from 'react'; -import useSpecificSubjectContext from './context'; +import useSpecificSubjectContext, {CategoryData} from './context'; import BookTile from '~/components/book-tile/book-tile'; import {ActiveElementContextProvider} from '~/contexts/active-element'; import {useLocation} from 'react-router-dom'; import throttle from 'lodash/throttle'; import './book-viewer.scss'; -function Category({category: [heading, categoryData]}) { +function Category({ + category: [heading, categoryData] +}: { + category: [string, CategoryData]; +}) { const booksObj = categoryData.books; const books = Object.values(booksObj); const text = categoryData.categoryDescription; const {hash} = useLocation(); - const ref = React.useRef(); + const ref = React.useRef(null); // Image loads screw up the scroll-to location, so we listen for them // and re-scroll when images complete loading // throttled to reduce possible jitter React.useEffect(() => { - const isInitialScrollTarget = window.decodeURIComponent(hash.substring(1)) === heading; + const isInitialScrollTarget = + window.decodeURIComponent(hash.substring(1)) === heading; if (!isInitialScrollTarget) { - return null; + return undefined; } const throttleGoTo = throttle((el) => { el.scrollIntoView({block: 'center', behavior: 'smooth'}); @@ -36,12 +41,12 @@ function Category({category: [heading, categoryData]}) { }, [heading, hash]); return ( -
+

{heading}

{text}
-
+
{books.map((b) => ( - + ))}
@@ -53,8 +58,8 @@ export default function BookViewer() { return ( -
-
+
+
{categories.map((c) => ( ))} diff --git a/src/app/pages/subjects/new/specific/components/carousel-section.js b/src/app/pages/subjects/new/specific/components/carousel-section.tsx similarity index 75% rename from src/app/pages/subjects/new/specific/components/carousel-section.js rename to src/app/pages/subjects/new/specific/components/carousel-section.tsx index 1d155bfcc..5a4da5665 100644 --- a/src/app/pages/subjects/new/specific/components/carousel-section.js +++ b/src/app/pages/subjects/new/specific/components/carousel-section.tsx @@ -5,14 +5,21 @@ import './carousel-section.scss'; export default function CarouselSection({ heading, description, linkUrl, linkText, children, thing, minWidth -}) { +}: React.PropsWithChildren<{ + heading: string; + description: string; + linkUrl: string; + linkText: string; + thing: string; + minWidth: number; +}>) { const {innerWidth} = useWindowContext(); - const ref = React.useRef(); + const ref = React.useRef<{base: HTMLDivElement}>(); const [atATime, setAtATime] = React.useState(1); React.useEffect( () => { - const carouselWidth = ref.current.base?.getBoundingClientRect().width ?? 0; + const carouselWidth = ref.current?.base?.getBoundingClientRect().width ?? 0; setAtATime(Math.max(1, Math.floor(carouselWidth / minWidth))); }, diff --git a/src/app/pages/subjects/new/specific/context.d.ts b/src/app/pages/subjects/new/specific/context.d.ts deleted file mode 100644 index d5210e0f9..000000000 --- a/src/app/pages/subjects/new/specific/context.d.ts +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import { LocaleEntry } from '~/components/language-selector/language-selector'; -import { ImageData } from '../context'; -import type { InfoBoxValues } from '../info-boxes'; - -// There will be more, but this is what I need for now -export type Book = { - id: number; - slug: string; - title: string; - webviewRexLink: string; - webviewLink: string; - highResolutionPdfUrl: string; - lowResolutionPdfUrl: string; - coverUrl: string; -}; - -type Category = [ - string, { - books: { - [title: string]: [Book] - } - } -]; - -type SubjectEntry = { - [title: string]: { - categories: Category[]; - } -}; - -type SectionContent = { - heading: string; - linkHref: string; - linkText: string; -}; - -type SectionInfo = { - content: SectionContent & { - image: ImageData; - adHtml: string; - } -}; - -type WebinarSectionInfo = { - content: SectionContent & { - webinarDescription: string; - } -}; - -type SpecificSubjectPageData = { - translations?: [LocaleEntry[]]; - title?: string; - subjects?: SubjectEntry; - tutorAd: SectionInfo; - aboutOs: SectionInfo; - webinarHeader: WebinarSectionInfo; - infoBoxes: InfoBoxValues; -}; - -export default function (): SpecificSubjectPageData; - -type ProviderArgs = { - contextValueParameters: string; -}; -export function SpecificSubjectContextProvider( - args: React.PropsWithChildren -): React.ReactNode; diff --git a/src/app/pages/subjects/new/specific/context.js b/src/app/pages/subjects/new/specific/context.js deleted file mode 100644 index f64c36c52..000000000 --- a/src/app/pages/subjects/new/specific/context.js +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import usePageData from '~/helpers/use-page-data'; -import buildContext from '~/components/jsx-helpers/build-context'; -import {setPageTitleAndDescriptionFromBookData} from '~/helpers/use-document-head'; - -const preserveWrapping = false; - -function useContextValue(slug) { - const data = usePageData(`pages/${slug}-books?type=pages.Subject`, preserveWrapping); - const categories = React.useMemo( - () => { - if (!data) { - return []; - } - - const {subjects, title} = data; - - if (subjects && title && subjects[title]) { - return Object.entries(subjects[title].categories); - } - console.warn('Specific subjects and title need to be defined'); - return []; - }, - [data] - ); - - React.useEffect( - () => { - if (data) { - setPageTitleAndDescriptionFromBookData(data); - } - }, - [data] - ); - - return {...data, categories}; -} - -const {useContext, ContextProvider} = buildContext({useContextValue}); - -export { - useContext as default, - ContextProvider as SpecificSubjectContextProvider -}; diff --git a/src/app/pages/subjects/new/specific/context.ts b/src/app/pages/subjects/new/specific/context.ts new file mode 100644 index 000000000..6f3dcb5a0 --- /dev/null +++ b/src/app/pages/subjects/new/specific/context.ts @@ -0,0 +1,113 @@ +import React from 'react'; +import usePageData from '~/helpers/use-page-data'; +import buildContext from '~/components/jsx-helpers/build-context'; +import {setPageTitleAndDescriptionFromBookData} from '~/helpers/use-document-head'; +import {ImageData} from '../context'; +import type {InfoBoxValues} from '../info-boxes'; +import type { AboutOsData } from '../about-openstax'; +import {LocaleEntry} from '~/components/language-selector/language-selector'; +import type {OsTextbookCategory} from './learn-more'; + +// There will be more, but this is what I need for now +export type Book = { + id: number; + slug: string; + title: string; + webviewRexLink: string; + webviewLink: string; + highResolutionPdfUrl: string; + lowResolutionPdfUrl: string; + coverUrl: string; +}; + +export type CategoryData = { + books: { + [title: string]: [Book]; + }; + categoryDescription: string; +}; + +type SubjectEntry = { + [title: string]: { + categories: CategoryData[]; + }; +}; + +type SectionContent = { + heading: string; + linkHref: string; + linkText: string; +}; + +type SectionInfo = { + content: SectionContent & { + image: ImageData; + adHtml: string; + }; +}; + +type WebinarSectionInfo = { + content: SectionContent & { + webinarDescription: string; + }; +}; + +type BlogSectionInfo = { + content: SectionContent & { + blogDescription: string; + }; +}; + +type SpecificSubjectPageData = { + translations?: [LocaleEntry[]]; + title?: string; + subjects?: SubjectEntry; + tutorAd: SectionInfo; + aboutOs: {content: AboutOsData}; + webinarHeader: WebinarSectionInfo; + infoBoxes: InfoBoxValues; + blogHeader: BlogSectionInfo; + osTextbookHeading: string; + osTextbookCategories: [OsTextbookCategory[]]; + pageDescription: string; + learnMoreAboutBooks: string; + learnMoreBlogPosts: string; + learnMoreWebinars: string; +}; + +const preserveWrapping = false; + +function useContextValue(slug: string) { + const data = usePageData( + `pages/${slug}-books?type=pages.Subject`, + preserveWrapping + ); + const categories = React.useMemo(() => { + if (!data) { + return []; + } + + const {subjects, title} = data; + + if (subjects && title && subjects[title]) { + return Object.entries(subjects[title].categories); + } + console.warn('Specific subjects and title need to be defined'); + return []; + }, [data]); + + React.useEffect(() => { + if (data) { + setPageTitleAndDescriptionFromBookData(data); + } + }, [data]); + + return {...data, categories}; +} + +const {useContext, ContextProvider} = buildContext({useContextValue}); + +export { + useContext as default, + ContextProvider as SpecificSubjectContextProvider +}; diff --git a/src/app/pages/subjects/new/specific/import-blog-posts.js b/src/app/pages/subjects/new/specific/import-blog-posts.js new file mode 100644 index 000000000..c09928179 --- /dev/null +++ b/src/app/pages/subjects/new/specific/import-blog-posts.js @@ -0,0 +1,3 @@ +import BlogPosts from './blog-posts'; + +export default BlogPosts; diff --git a/src/app/pages/subjects/new/specific/import-learn-more.js b/src/app/pages/subjects/new/specific/import-learn-more.js new file mode 100644 index 000000000..268284615 --- /dev/null +++ b/src/app/pages/subjects/new/specific/import-learn-more.js @@ -0,0 +1,3 @@ +import LearnMore from './learn-more'; + +export default LearnMore; diff --git a/src/app/pages/subjects/new/specific/learn-more.js b/src/app/pages/subjects/new/specific/learn-more.tsx similarity index 77% rename from src/app/pages/subjects/new/specific/learn-more.js rename to src/app/pages/subjects/new/specific/learn-more.tsx index 305ef5a7c..f848e4a20 100644 --- a/src/app/pages/subjects/new/specific/learn-more.js +++ b/src/app/pages/subjects/new/specific/learn-more.tsx @@ -2,9 +2,15 @@ import React from 'react'; import useSpecificSubjectContext from './context'; import AccordionGroup from '~/components/accordion-group/accordion-group'; import RawHTML from '~/components/jsx-helpers/raw-html'; +import {assertDefined} from '~/helpers/data'; import './learn-more.scss'; -function learnMoreDataToAccordionItem({heading: title, text: html}) { +export type OsTextbookCategory = { + heading: string; + text: string; +} + +function learnMoreDataToAccordionItem({heading: title, text: html}: OsTextbookCategory) { return { title, contentComponent: @@ -14,7 +20,7 @@ function learnMoreDataToAccordionItem({heading: title, text: html}) { function LearnMore() { const {osTextbookHeading, osTextbookCategories} = useSpecificSubjectContext(); const accordionItems = React.useMemo( - () => osTextbookCategories[0].map(learnMoreDataToAccordionItem), + () => assertDefined(osTextbookCategories)[0].map(learnMoreDataToAccordionItem), [osTextbookCategories] ); @@ -32,5 +38,5 @@ export default function MaybeLearnMore() { if (!ctx?.osTextbookHeading) { return null; } - return (); + return ; } diff --git a/src/app/pages/subjects/new/specific/navigator-context.js b/src/app/pages/subjects/new/specific/navigator-context.js deleted file mode 100644 index 628a1e945..000000000 --- a/src/app/pages/subjects/new/specific/navigator-context.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - Specs - 1. Navigator listens to scroll and resize events to determine which of its - linked elements is closest to the middle of the viewport - 2. Context includes "goTo" that scrolls the indicated linked element to the - middle of the viewport - 3. Navigator has state variable for the ID of the linked element closest to - the middle of the viewport -*/ -import React from 'react'; -import buildContext from '~/components/jsx-helpers/build-context'; -import useWindowContext from '~/contexts/window'; - -function idListReducer(state, action) { - switch (action.type) { - case 'register': - if (!state.includes(action.id)) { - return [...state, action.id]; - } - break; - case 'unregister': - if (state.includes(action.id)) { - return state.filter((i) => i !== action.id); - } - break; - default: break; - } - return []; -} - -function useContextValue() { - const [idList, dispatch] = React.useReducer(idListReducer, []); - const registerId = React.useCallback( - (id) => { - dispatch({type: 'register', id}); - }, - [] - ); - const unregisterId = React.useCallback( - (id) => { - dispatch({type: 'unregister', id}); - }, - [] - ); - const goTo = React.useCallback( - (id) => { - const target = document.getElementById(id); - - if (target) { - target.scrollIntoView({block: 'center', behavior: 'smooth'}); - } else { - console.warn('Target not found', id); - } - }, - [] - ); - const wCtx = useWindowContext(); - const currentId = React.useMemo( - () => { - const midY = wCtx.innerHeight / 2; - - return idList.find( - (id) => { - const el = document.getElementById(id); - - if (! el) { - console.info('Did not find', id); - return null; - } - const {top, bottom} = el.getBoundingClientRect(); - - return top < midY && bottom > midY; - } - ); - }, - [wCtx, idList] - ); - - return {registerId, goTo, currentId, unregisterId}; -} - -const {useContext, ContextProvider} = buildContext({useContextValue}); - -export { - useContext as default, - ContextProvider as NavigatorContextProvider -}; diff --git a/src/app/pages/subjects/new/specific/navigator-context.ts b/src/app/pages/subjects/new/specific/navigator-context.ts new file mode 100644 index 000000000..9b768f1de --- /dev/null +++ b/src/app/pages/subjects/new/specific/navigator-context.ts @@ -0,0 +1,65 @@ +/* + Specs + 1. Navigator listens to scroll and resize events to determine which of its + linked elements is closest to the middle of the viewport + 2. Context includes "goTo" that scrolls the indicated linked element to the + middle of the viewport + 3. Navigator has state variable for the ID of the linked element closest to + the middle of the viewport +*/ +import React from 'react'; +import buildContext from '~/components/jsx-helpers/build-context'; +import useWindowContext from '~/contexts/window'; + +type ActionType = 'register' | 'unregister'; +type Action = { + type: ActionType; + id: string; +}; + +type State = string[]; + +function idListReducer(state: State, action: Action) { + if (action.type === 'register' && !state.includes(action.id)) { + return [...state, action.id]; + } + // It's really the only thing left: unregister + return state.filter((i) => i !== action.id); +} + +function useContextValue() { + const [idList, dispatch] = React.useReducer(idListReducer, []); + const registerId = React.useCallback((id: string) => { + dispatch({type: 'register', id}); + }, []); + const unregisterId = React.useCallback((id: string) => { + dispatch({type: 'unregister', id}); + }, []); + const goTo = React.useCallback((id: string) => { + const target = document.getElementById(id); + + target?.scrollIntoView({block: 'center', behavior: 'smooth'}); + }, []); + const wCtx = useWindowContext(); + const currentId = React.useMemo(() => { + const midY = wCtx.innerHeight / 2; + + return idList.find((id) => { + const el = document.getElementById(id); + + if (!el) { + console.info('Did not find', id); + return null; + } + const {top, bottom} = el.getBoundingClientRect(); + + return top < midY && bottom > midY; + }); + }, [wCtx, idList]); + + return {registerId, goTo, currentId, unregisterId}; +} + +const {useContext, ContextProvider} = buildContext({useContextValue}); + +export {useContext as default, ContextProvider as NavigatorContextProvider}; diff --git a/src/app/pages/subjects/new/specific/navigator.js b/src/app/pages/subjects/new/specific/navigator.tsx similarity index 67% rename from src/app/pages/subjects/new/specific/navigator.js rename to src/app/pages/subjects/new/specific/navigator.tsx index f58644fa0..6cceab489 100644 --- a/src/app/pages/subjects/new/specific/navigator.js +++ b/src/app/pages/subjects/new/specific/navigator.tsx @@ -1,33 +1,77 @@ import React from 'react'; import useSpecificSubjectContext from './context'; +import useNavigatorContext from './navigator-context'; import {IfToggleIsOpen} from '~/components/toggle/toggle'; import ToggleControlBar from '~/components/toggle/toggle-control-bar'; import ArrowToggle from '~/components/toggle/arrow-toggle'; import AccordionGroup from '~/components/accordion-group/accordion-group'; -import useNavigatorContext from './navigator-context'; import {FormattedMessage, useIntl} from 'react-intl'; import {Link} from 'react-router-dom'; +import type {Category} from '~/contexts/subject-category'; +import {assertDefined} from '~/helpers/data'; import './navigator.scss'; const LEARN_MORE_IDS = ['blog-posts', 'webinars', 'learn']; -function SectionLink({id, text}) { +export default function Navigator({subject}: {subject: Category}) { + return ( +
+
+ + +
+
+ ); +} + +function CategorySectionLinks() { + const {categories} = useSpecificSubjectContext(); + + return ( + + {categories.map(([c]) => ( + + ))} + + ); +} + +function CategoryLink({category}: {category: string}) { + return ; +} + +function SectionLink({id, text}: {id: string; text: string}) { const {currentId, registerId, unregisterId, goTo} = useNavigatorContext(); const onClick = React.useCallback( - (e) => { + (e: React.MouseEvent) => { e.preventDefault(); goTo(id); }, [id, goTo] ); - React.useEffect( - () => { - registerId(id); - return () => unregisterId(id); - }, - [registerId, unregisterId, id] - ); + React.useEffect(() => { + registerId(id); + return () => unregisterId(id); + }, [registerId, unregisterId, id]); return ( {text} - ); -} - -function CategoryLink({category}) { - return ( - - ); -} - -function CategorySectionLinks() { - const {categories} = useSpecificSubjectContext(); - - return ( - - {categories.map(([c]) => )} - + > + {text} + ); } function OtherSectionLinks() { - const {learnMoreAboutBooks, learnMoreBlogPosts, learnMoreWebinars} = useSpecificSubjectContext(); + const {learnMoreAboutBooks, learnMoreBlogPosts, learnMoreWebinars} = + useSpecificSubjectContext(); + // If one is defined, they all are + if (!learnMoreAboutBooks) { + return null; + } return ( - - - + + + ); } -function useAccordionItems(subjectName) { +function useAccordionItems(subjectName: string) { const intl = useIntl(); return [ @@ -85,44 +126,18 @@ function useAccordionItems(subjectName) { ]; } -export function JumpToSection({subjectName}) { +export function JumpToSection({subjectName}: {subjectName: string}) { return (
Jump to section
- +
); } - -export default function Navigator({subject}) { - return ( -
-
- - -
-
- ); -} diff --git a/src/app/pages/subjects/new/specific/specific.tsx b/src/app/pages/subjects/new/specific/specific.tsx index 08215c702..449874ef8 100644 --- a/src/app/pages/subjects/new/specific/specific.tsx +++ b/src/app/pages/subjects/new/specific/specific.tsx @@ -22,9 +22,9 @@ import cn from 'classnames'; import './specific.scss'; const importPhilanthropicSupport = () => import('../import-philanthropic-support.js'); -const importLearnMore = () => import('./learn-more.js'); +const importLearnMore = () => import('./import-learn-more.js'); const importWebinars = () => import('./import-webinars.js'); -const importBlogPosts = () => import('./blog-posts.js'); +const importBlogPosts = () => import('./import-blog-posts.js'); // Had to make this layer to use the context function Translations() { diff --git a/src/app/pages/subjects/new/specific/subject-intro.js b/src/app/pages/subjects/new/specific/subject-intro.tsx similarity index 64% rename from src/app/pages/subjects/new/specific/subject-intro.js rename to src/app/pages/subjects/new/specific/subject-intro.tsx index ab7a4cec6..10c09f5c2 100644 --- a/src/app/pages/subjects/new/specific/subject-intro.js +++ b/src/app/pages/subjects/new/specific/subject-intro.tsx @@ -2,20 +2,24 @@ import React from 'react'; import RawHTML from '~/components/jsx-helpers/raw-html'; import useSpecificSubjectContext from './context'; import {JumpToSection} from './navigator'; -import useToggleContext, {ToggleContextProvider} from '~/components/toggle/toggle-context'; +import useToggleContext, { + ToggleContextProvider +} from '~/components/toggle/toggle-context'; import {FormattedMessage} from 'react-intl'; import cn from 'classnames'; import './subject-intro.scss'; - -function IntroContent({subjectName}) { - const {pageDescription: introHtml} = useSpecificSubjectContext(); +function IntroContent({subjectName}: {subjectName: string}) { + const introHtml = useSpecificSubjectContext().pageDescription ?? ''; const {isOpen} = useToggleContext(); return (
- +

{subjectName}

@@ -24,7 +28,7 @@ function IntroContent({subjectName}) { ); } -export default function SubjectIntro({subjectName}) { +export default function SubjectIntro({subjectName}: {subjectName: string}) { return (
diff --git a/src/app/pages/subjects/new/specific/webinars.tsx b/src/app/pages/subjects/new/specific/webinars.tsx index 36d4e381f..7f3e44e03 100644 --- a/src/app/pages/subjects/new/specific/webinars.tsx +++ b/src/app/pages/subjects/new/specific/webinars.tsx @@ -8,6 +8,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faChevronRight} from '@fortawesome/free-solid-svg-icons/faChevronRight'; import {useIntl} from 'react-intl'; import './webinars.scss'; +import {assertDefined} from '~/helpers/data'; export default function MaybeWebinars() { const ctx = useSpecificSubjectContext(); @@ -24,18 +25,17 @@ type WebinarCardData = { registration_url: string; registration_link_text: string; link: string; -} +}; const importCarouselSection = () => import('./components/carousel-section'); function Webinars() { const { - webinarHeader: { - content: {heading, webinarDescription, linkHref, linkText} - } - } = useSpecificSubjectContext(); + content: {heading, webinarDescription, linkHref, linkText} + } = assertDefined(useSpecificSubjectContext().webinarHeader); const cms = useEnglishSubject(); - const blurbs: WebinarCardData[] = useDataFromSlug(`webinars/?subject=${cms}`) || []; + const blurbs: WebinarCardData[] = + useDataFromSlug(`webinars/?subject=${cms}`) || []; const intl = useIntl(); return blurbs.length ? ( @@ -45,7 +45,7 @@ function Webinars() { description={webinarDescription} linkUrl={linkHref} linkText={linkText} - thing='webinars' + thing="webinars" minWidth={290} > {blurbs.map((blurb) => ( @@ -64,7 +64,7 @@ function Card({ registration_link_text: watchText }: WebinarCardData) { return ( -
+

{title}

diff --git a/src/app/pages/subjects/new/subjects-listing.tsx b/src/app/pages/subjects/new/subjects-listing.tsx index 3ecdfebc1..cc0c6bf5f 100644 --- a/src/app/pages/subjects/new/subjects-listing.tsx +++ b/src/app/pages/subjects/new/subjects-listing.tsx @@ -8,7 +8,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faArrowRight} from '@fortawesome/free-solid-svg-icons/faArrowRight'; import {useLocation} from 'react-router-dom'; import './subjects-listing.scss'; -import { assertDefined } from '~/helpers/data'; +import {assertDefined} from '~/helpers/data'; export default function SubjectsListing() { const {subjects} = assertDefined(useSubjectsContext()); diff --git a/src/app/pages/subjects/new/subjects.tsx b/src/app/pages/subjects/new/subjects.tsx index 0f1972a39..60df2e39b 100644 --- a/src/app/pages/subjects/new/subjects.tsx +++ b/src/app/pages/subjects/new/subjects.tsx @@ -13,7 +13,8 @@ const importLanguageSelector = () => import('./language-selector-section.js'); const importSubjectsListing = () => import('./import-subjects-listing.js'); const importTutorAd = () => import('./import-tutor-ad.js'); const importInfoBoxes = () => import('./import-info-boxes.js'); -const importPhilanthropicSupport = () => import('./import-philanthropic-support.js'); +const importPhilanthropicSupport = () => + import('./import-philanthropic-support.js'); function SEOSetup() { const {title, pageDescription} = assertDefined(useSubjectsContext()); @@ -29,20 +30,20 @@ function SEOSetup() { export function SubjectsPage() { const {translations} = assertDefined(useSubjectsContext()); - const otherLocales = translations?.length ? - translations[0].value.map((t) => t.locale) : - []; + const otherLocales = translations?.length + ? translations[0].value.map((t) => t.locale) + : []; return ( -
+
- } /> + } /> } + path="view-all" + element={} /> } + path="ap" + element={} /> - } /> + } /> ); diff --git a/test/src/data/business-blog-blurbs.js b/test/src/data/business-blog-blurbs.js new file mode 100644 index 000000000..19e89936d --- /dev/null +++ b/test/src/data/business-blog-blurbs.js @@ -0,0 +1,28 @@ +/* eslint-disable camelcase, max-len */ +export default [ + { + id: 264, + title: 'Meet our student blogger Kharl', + subheading: null, + body_blurb: + '

This post is part of our For Students, Forever series. OpenStax is working with three students to share the student perspective on affordability and access within the context of higher education and open materials. Stay tuned for their posts from the Open Education Conference in Niagara Falls!

', + article_image: + 'https://assets.openstax.org/oscms-dev/media/original_images/Kharl_Chicago_Bean.jpg', + article_image_alt: null, + date: '2018-10-08', + author: 'Kharl Reynado', + pin_to_top: false, + tags: [], + collections: [], + article_subjects: [ + { + name: 'Business', + featured: false + } + ], + content_types: [], + slug: 'meet-our-student-blogger-kharl', + seo_title: '', + search_description: '' + } +]; diff --git a/test/src/data/business-books.js b/test/src/data/business-books.js new file mode 100644 index 000000000..c07886df1 --- /dev/null +++ b/test/src/data/business-books.js @@ -0,0 +1,594 @@ +/* eslint-disable max-len */ +export default { + id: 414, + meta: { + seoTitle: '', + searchDescription: '', + type: 'pages.Subject', + detailUrl: 'https://dev.openstax.org/apps/cms/api/v2/pages/414/', + htmlUrl: 'https://dev.openstax.org/subjects/business', + slug: 'business-books', + showInMenus: false, + firstPublishedAt: '2022-02-08T13:50:26.265667-06:00', + aliasOf: null, + parent: { + id: 413, + meta: { + type: 'pages.Subjects', + detailUrl: + 'https://dev.openstax.org/apps/cms/api/v2/pages/413/', + htmlUrl: 'https://dev.openstax.org/new-subjects/' + }, + title: 'New Subjects' + }, + locale: 'en' + }, + title: 'Business', + pageDescription: + 'Simple to use. Easy to adopt. Our Business textbooks are designed to meet the standard scope and sequence of several business courses - and are 100% free.', + tutorAd: { + content: { + heading: 'Instructors, take your course online', + image: { + id: 743, + file: 'https://assets.openstax.org/oscms-dev/media/original_images/Books_Devices_Mockup_Final_agR6ob9.webp', + title: 'Books Devices Mockup_Final.webp', + height: 805, + width: 1462, + createdAt: '2021-05-06T16:13:16.043056-05:00' + }, + adHtml: 'Assign homework and readings synced with OpenStax textbooks', + linkText: 'Learn more', + linkHref: 'https://dev.openstax.org/openstax-tutor' + } + }, + blogHeader: { + content: { + heading: 'Read our blogs about Business Textbooks', + blogDescription: 'Read our blog', + linkText: 'View all blog posts', + linkHref: 'https://dev.openstax.org/blog' + } + }, + webinarHeader: { + content: { + heading: 'Webinars about OpenStax textbooks', + webinarDescription: + 'Hear about the making of OpenStax textbooks from experts. Tips and Tricks on using out textbooks', + linkText: 'View all webinars', + linkHref: 'https://dev.openstax.org/webinars' + } + }, + osTextbookHeading: 'Learn more about OpenStax Business textbooks', + osTextbookCategories: [ + [ + { + heading: 'General Business', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' + }, + { + heading: 'Accounting', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' + }, + { + heading: 'Statistics', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' + }, + { + heading: 'Management', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' + }, + { + heading: 'Entrepreneurship', + text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' + } + ] + ], + aboutOs: { + content: { + heading: 'About Openstax textbooks', + image: { + id: 736, + file: 'https://assets.openstax.org/oscms-dev/media/original_images/Student_Final.webp', + title: 'Student_Final.webp', + height: 646, + width: 704, + createdAt: '2021-05-04T17:15:10.250786-05:00' + }, + osText: 'OpenStax is part of Rice University, which is a 501(c)(3) nonprofit charitable corporation. Our mission is to improve educational access and learning for everyone. We do this by publishing openly licensed books, developing and improving research-based courseware, establishing partnerships with educational resource companies, and more.', + linkText: 'View all subjects', + linkHref: 'https://dev.openstax.org/subjects' + } + }, + infoBoxes: [ + [ + { + image: { + id: 719, + file: 'https://assets.openstax.org/oscms-dev/media/original_images/school-icon2x.png', + title: 'school-icon@2x.png', + height: 62, + width: 62, + createdAt: '2021-01-27T12:05:30.529584-06:00' + }, + heading: 'Expert Authors', + text: 'Our open source textbooks are written by professional content developers who are experts in their fields' + }, + { + image: { + id: 719, + file: 'https://assets.openstax.org/oscms-dev/media/original_images/school-icon2x.png', + title: 'school-icon@2x.png', + height: 62, + width: 62, + createdAt: '2021-01-27T12:05:30.529584-06:00' + }, + heading: 'Standard Scope and Sequence', + text: 'All textbooks meet standard scope and sequence requirements, making them seamlessly adaptable into existing courses.' + }, + { + image: { + id: 719, + file: 'https://assets.openstax.org/oscms-dev/media/original_images/school-icon2x.png', + title: 'school-icon@2x.png', + height: 62, + width: 62, + createdAt: '2021-01-27T12:05:30.529584-06:00' + }, + heading: 'Peer Reviewed', + text: 'OpenStax textbooks undergo a rigorous peer review process. You can view the list of contributors when you click on each book.' + } + ] + ], + bookCategoriesHeading: 'Business book categories', + learnMoreHeading: 'Learn More', + learnMoreBlogPosts: 'Business blog posts', + learnMoreWebinars: 'Business webinars', + learnMoreAboutBooks: 'Learn more about our books', + philanthropicSupport: + "With philanthropic support, our books have been used in 38,160 classrooms, saving students $1,747,190,405 since 2012.
Learn more about our impact and how you can help.", + subjects: { + Business: { + icon: 'https://assets.openstax.org/oscms-dev/media/original_images/noun_presentation_3480624_1.png', + categories: { + Accounting: { + categoryDescription: + 'Description placeholder until we have real data', + books: { + 'Principles of Accounting, Volume 1: Financial Accounting': + [ + { + id: 313, + slug: 'books/principles-financial-accounting', + bookState: 'live', + title: 'Principles of Accounting, Volume 1: Financial Accounting', + subjects: ['Business'], + subjectCategories: ['Accounting'], + k12subject: [], + isAp: false, + coverUrl: + 'https://assets.openstax.org/oscms-dev/media/documents/principles_of_acounting_volume_1_web_card.svg', + coverColor: 'blue', + highResolutionPdfUrl: + 'https://assets.openstax.org/oscms-dev/media/documents/FinancialAccounting-OP_7jk73M0.pdf', + lowResolutionPdfUrl: null, + ibookLink: '', + ibookLinkVolume2: '', + webviewLink: + 'https://dev.cnx.org/contents/9ab4ba6d-1e48-486d-a2de-38ae1617ca84', + webviewRexLink: + 'https://staging.openstax.org/books/principles-financial-accounting/pages/1-why-it-matters', + bookshareLink: '', + kindleLink: + 'https://www.amazon.com/dp/B07T1DLTD9/ref=cm_sw_em_r_mt_dp_U_C6DiEbK2J0MS2', + amazonComingSoon: false, + amazonLink: + 'https://www.amazon.com/dp/1947172689', + bookstoreComingSoon: true, + compCopyAvailable: false, + salesforceAbbreviation: + 'Financial Accounting', + salesforceName: 'Financial Accounting', + urls: [], + lastUpdatedPdf: null + } + ], + 'Principles of Accounting, Volume 2: Managerial Accounting': + [ + { + id: 335, + slug: 'books/principles-managerial-accounting', + bookState: 'live', + title: 'Principles of Accounting, Volume 2: Managerial Accounting', + subjects: ['Business'], + subjectCategories: ['Accounting'], + k12subject: [], + isAp: false, + coverUrl: + 'https://assets.openstax.org/oscms-dev/media/documents/principles_of_acounting_volume_2_book_card_Vu9ykSz.svg', + coverColor: 'blue', + highResolutionPdfUrl: + 'https://assets.openstax.org/oscms-dev/media/documents/ManagerialAccounting-OP_os574CR.pdf', + lowResolutionPdfUrl: null, + ibookLink: '', + ibookLinkVolume2: '', + webviewLink: + 'https://dev.cnx.org/contents/920d1c8a-606c-4888-bfd4-d1ee27ce1795', + webviewRexLink: + 'https://staging.openstax.org/books/principles-managerial-accounting/pages/1-why-it-matters', + bookshareLink: '', + kindleLink: + 'https://www.amazon.com/dp/B07SZ93NJK/ref=cm_sw_em_r_mt_dp_U_D6DiEbMDEC61A', + amazonComingSoon: false, + amazonLink: + 'https://www.amazon.com/dp/1947172603', + bookstoreComingSoon: true, + compCopyAvailable: false, + salesforceAbbreviation: + 'Managerial Accounting', + salesforceName: 'Managerial Accounting', + urls: [], + lastUpdatedPdf: null + } + ] + } + }, + Entrepreneurship: { + categoryDescription: + 'Description placeholder until we have real data', + books: {} + }, + 'General Business': { + categoryDescription: + 'Description placeholder until we have real data', + books: { + 'Introduction to Business': [ + { + id: 259, + slug: 'books/introduction-business', + bookState: 'live', + title: 'Introduction to Business', + subjects: ['Business'], + subjectCategories: ['General Business'], + k12subject: [], + isAp: false, + coverUrl: + 'https://assets.openstax.org/oscms-dev/media/documents/introduction_to_business.svg', + coverColor: 'blue', + highResolutionPdfUrl: + 'https://assets.openstax.org/oscms-dev/media/documents/IntroductionToBusiness-OP.pdf', + lowResolutionPdfUrl: null, + ibookLink: '', + ibookLinkVolume2: '', + webviewLink: + 'https://dev.cnx.org/contents/4e09771f-a8aa-40ce-9063-aa58cc24e77f', + webviewRexLink: + 'https://staging.openstax.org/books/introduction-business/pages/1-introduction', + bookshareLink: '', + kindleLink: + 'https://www.amazon.com/dp/B07VT9HT7J/ref=cm_sw_em_r_mt_dp_U_44DiEbG5BKXEK', + amazonComingSoon: false, + amazonLink: + 'https://www.amazon.com/dp/1947172549', + bookstoreComingSoon: false, + compCopyAvailable: false, + salesforceAbbreviation: + 'Introduction to Business', + salesforceName: 'Introduction to Business', + urls: [], + lastUpdatedPdf: null + } + ], + 'Makroekonomia – podstawy': [ + { + id: 516, + slug: 'books/makroekonomia-podstawy', + bookState: 'live', + title: 'Makroekonomia – podstawy', + subjects: ['Social Sciences', 'Business'], + subjectCategories: ['General Business'], + k12subject: [], + isAp: false, + coverUrl: + 'https://assets.openstax.org/oscms-dev/media/documents/makroekonomia_square_tgUFDNh.svg', + coverColor: 'gray', + highResolutionPdfUrl: null, + lowResolutionPdfUrl: null, + ibookLink: '', + ibookLinkVolume2: '', + webviewLink: + 'https://dev.cnx.org/contents/823ae3e1-57c4-44c5-b54a-310091040cf6', + webviewRexLink: '', + bookshareLink: '', + kindleLink: '', + amazonComingSoon: false, + amazonLink: '', + bookstoreComingSoon: false, + compCopyAvailable: true, + salesforceAbbreviation: null, + salesforceName: null, + urls: [], + lastUpdatedPdf: null + } + ] + } + }, + Management: { + categoryDescription: + 'Description placeholder until we have real data', + books: {} + }, + Statistics: { + categoryDescription: + 'Description placeholder until we have real data', + books: { + 'Introductory Business Statistics 2e': [ + { + id: 519, + slug: 'books/introductory-business-statistics-2e', + bookState: 'live', + title: 'Introductory Business Statistics 2e', + subjects: ['Math', 'Business'], + subjectCategories: ['Statistics', 'Statistics'], + k12subject: [], + isAp: false, + coverUrl: + 'https://assets.openstax.org/oscms-dev/media/documents/introductory_business_statistics_2e_web_card.svg', + coverColor: 'blue', + highResolutionPdfUrl: null, + lowResolutionPdfUrl: null, + ibookLink: '', + ibookLinkVolume2: '', + webviewLink: + 'https://dev.cnx.org/contents/547026cb-a330-4809-94bf-126be5f62381', + webviewRexLink: + 'https://dev.openstax.org/books/introductory-business-statistics-2e/pages/1-introduction', + bookshareLink: '', + kindleLink: '', + amazonComingSoon: false, + amazonLink: + 'https://he.kendallhunt.com/sites/default/files/uploadedFiles/Kendall_Hunt/OPENSTAX_PRICE_LIST_and_ORDER_FORM.pdf', + bookstoreComingSoon: false, + compCopyAvailable: false, + salesforceAbbreviation: null, + salesforceName: null, + urls: [], + lastUpdatedPdf: null + } + ] + } + } + } + } + }, + translations: [ + [ + { + locale: 'es', + slug: 'empresarial-books' + } + ] + ], + promoteImage: null, + slug: 'pages/business-books?type=pages.Subject', + categories: [ + [ + 'Accounting', + { + categoryDescription: + 'Description placeholder until we have real data', + books: { + 'Principles of Accounting, Volume 1: Financial Accounting': + [ + { + id: 313, + slug: 'books/principles-financial-accounting', + bookState: 'live', + title: 'Principles of Accounting, Volume 1: Financial Accounting', + subjects: ['Business'], + subjectCategories: ['Accounting'], + k12subject: [], + isAp: false, + coverUrl: + 'https://assets.openstax.org/oscms-dev/media/documents/principles_of_acounting_volume_1_web_card.svg', + coverColor: 'blue', + highResolutionPdfUrl: + 'https://assets.openstax.org/oscms-dev/media/documents/FinancialAccounting-OP_7jk73M0.pdf', + lowResolutionPdfUrl: null, + ibookLink: '', + ibookLinkVolume2: '', + webviewLink: + 'https://dev.cnx.org/contents/9ab4ba6d-1e48-486d-a2de-38ae1617ca84', + webviewRexLink: + 'https://staging.openstax.org/books/principles-financial-accounting/pages/1-why-it-matters', + bookshareLink: '', + kindleLink: + 'https://www.amazon.com/dp/B07T1DLTD9/ref=cm_sw_em_r_mt_dp_U_C6DiEbK2J0MS2', + amazonComingSoon: false, + amazonLink: + 'https://www.amazon.com/dp/1947172689', + bookstoreComingSoon: true, + compCopyAvailable: false, + salesforceAbbreviation: 'Financial Accounting', + salesforceName: 'Financial Accounting', + urls: [], + lastUpdatedPdf: null + } + ], + 'Principles of Accounting, Volume 2: Managerial Accounting': + [ + { + id: 335, + slug: 'books/principles-managerial-accounting', + bookState: 'live', + title: 'Principles of Accounting, Volume 2: Managerial Accounting', + subjects: ['Business'], + subjectCategories: ['Accounting'], + k12subject: [], + isAp: false, + coverUrl: + 'https://assets.openstax.org/oscms-dev/media/documents/principles_of_acounting_volume_2_book_card_Vu9ykSz.svg', + coverColor: 'blue', + highResolutionPdfUrl: + 'https://assets.openstax.org/oscms-dev/media/documents/ManagerialAccounting-OP_os574CR.pdf', + lowResolutionPdfUrl: null, + ibookLink: '', + ibookLinkVolume2: '', + webviewLink: + 'https://dev.cnx.org/contents/920d1c8a-606c-4888-bfd4-d1ee27ce1795', + webviewRexLink: + 'https://staging.openstax.org/books/principles-managerial-accounting/pages/1-why-it-matters', + bookshareLink: '', + kindleLink: + 'https://www.amazon.com/dp/B07SZ93NJK/ref=cm_sw_em_r_mt_dp_U_D6DiEbMDEC61A', + amazonComingSoon: false, + amazonLink: + 'https://www.amazon.com/dp/1947172603', + bookstoreComingSoon: true, + compCopyAvailable: false, + salesforceAbbreviation: 'Managerial Accounting', + salesforceName: 'Managerial Accounting', + urls: [], + lastUpdatedPdf: null + } + ] + } + } + ], + [ + 'Entrepreneurship', + { + categoryDescription: + 'Description placeholder until we have real data', + books: {} + } + ], + [ + 'General Business', + { + categoryDescription: + 'Description placeholder until we have real data', + books: { + 'Introduction to Business': [ + { + id: 259, + slug: 'books/introduction-business', + bookState: 'live', + title: 'Introduction to Business', + subjects: ['Business'], + subjectCategories: ['General Business'], + k12subject: [], + isAp: false, + coverUrl: + 'https://assets.openstax.org/oscms-dev/media/documents/introduction_to_business.svg', + coverColor: 'blue', + highResolutionPdfUrl: + 'https://assets.openstax.org/oscms-dev/media/documents/IntroductionToBusiness-OP.pdf', + lowResolutionPdfUrl: null, + ibookLink: '', + ibookLinkVolume2: '', + webviewLink: + 'https://dev.cnx.org/contents/4e09771f-a8aa-40ce-9063-aa58cc24e77f', + webviewRexLink: + 'https://staging.openstax.org/books/introduction-business/pages/1-introduction', + bookshareLink: '', + kindleLink: + 'https://www.amazon.com/dp/B07VT9HT7J/ref=cm_sw_em_r_mt_dp_U_44DiEbG5BKXEK', + amazonComingSoon: false, + amazonLink: 'https://www.amazon.com/dp/1947172549', + bookstoreComingSoon: false, + compCopyAvailable: false, + salesforceAbbreviation: 'Introduction to Business', + salesforceName: 'Introduction to Business', + urls: [], + lastUpdatedPdf: null + } + ], + 'Makroekonomia – podstawy': [ + { + id: 516, + slug: 'books/makroekonomia-podstawy', + bookState: 'live', + title: 'Makroekonomia – podstawy', + subjects: ['Social Sciences', 'Business'], + subjectCategories: ['General Business'], + k12subject: [], + isAp: false, + coverUrl: + 'https://assets.openstax.org/oscms-dev/media/documents/makroekonomia_square_tgUFDNh.svg', + coverColor: 'gray', + highResolutionPdfUrl: null, + lowResolutionPdfUrl: null, + ibookLink: '', + ibookLinkVolume2: '', + webviewLink: + 'https://dev.cnx.org/contents/823ae3e1-57c4-44c5-b54a-310091040cf6', + webviewRexLink: '', + bookshareLink: '', + kindleLink: '', + amazonComingSoon: false, + amazonLink: '', + bookstoreComingSoon: false, + compCopyAvailable: true, + salesforceAbbreviation: null, + salesforceName: null, + urls: [], + lastUpdatedPdf: null + } + ] + } + } + ], + [ + 'Management', + { + categoryDescription: + 'Description placeholder until we have real data', + books: {} + } + ], + [ + 'Statistics', + { + categoryDescription: + 'Description placeholder until we have real data', + books: { + 'Introductory Business Statistics 2e': [ + { + id: 519, + slug: 'books/introductory-business-statistics-2e', + bookState: 'live', + title: 'Introductory Business Statistics 2e', + subjects: ['Math', 'Business'], + subjectCategories: ['Statistics', 'Statistics'], + k12subject: [], + isAp: false, + coverUrl: + 'https://assets.openstax.org/oscms-dev/media/documents/introductory_business_statistics_2e_web_card.svg', + coverColor: 'blue', + highResolutionPdfUrl: null, + lowResolutionPdfUrl: null, + ibookLink: '', + ibookLinkVolume2: '', + webviewLink: + 'https://dev.cnx.org/contents/547026cb-a330-4809-94bf-126be5f62381', + webviewRexLink: + 'https://dev.openstax.org/books/introductory-business-statistics-2e/pages/1-introduction', + bookshareLink: '', + kindleLink: '', + amazonComingSoon: false, + amazonLink: + 'https://he.kendallhunt.com/sites/default/files/uploadedFiles/Kendall_Hunt/OPENSTAX_PRICE_LIST_and_ORDER_FORM.pdf', + bookstoreComingSoon: false, + compCopyAvailable: false, + salesforceAbbreviation: null, + salesforceName: null, + urls: [], + lastUpdatedPdf: null + } + ] + } + } + ] + ] +}; diff --git a/test/src/pages/subjects/blog-posts.test.tsx b/test/src/pages/subjects/blog-posts.test.tsx new file mode 100644 index 000000000..8463721fd --- /dev/null +++ b/test/src/pages/subjects/blog-posts.test.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import {describe, expect, it} from '@jest/globals'; +import {render, screen} from '@testing-library/preact'; +import MaybeBlogPosts from '~/pages/subjects/new/specific/blog-posts'; +import ShellContextProvider from '~/../../test/helpers/shell-context'; +import { SpecificSubjectContextProvider } from '~/pages/subjects/new/specific/context'; +import businessBooksData from '~/../../test/src/data/business-books'; +import businessBlogBlurbs from '~/../../test/src/data/business-blog-blurbs'; + +const mockUsePageData = jest.fn(); +const mockUseDataFromSlug = jest.fn(); + +jest.mock('~/helpers/use-page-data', () => ({ + __esModule: true, + default: () => mockUsePageData() +})); +jest.mock('~/helpers/page-data-utils', () => ({ + __esModule: true, + ...jest.requireActual('~/helpers/page-data-utils'), + useDataFromSlug: () => mockUseDataFromSlug() +})); + +const mockCarouselSection = jest.fn(); + +jest.mock('~/pages/subjects/new/specific/components/carousel-section', () => ({ + __esModule: true, + CarouselSection: () => mockCarouselSection() +})); + +function Component() { + return ( + + + + + + ); +} + +describe('subjects/blog-posts section', () => { + it('returns null if context is empty', () => { + mockUsePageData.mockReturnValue(undefined); + mockUseDataFromSlug.mockReturnValue(undefined); + const {container} = render(); + + expect(container.innerHTML).toBe(''); + }); + it('returns blog posts', async () => { + mockUsePageData.mockReturnValue(businessBooksData); + mockUseDataFromSlug.mockReturnValue(businessBlogBlurbs); + mockCarouselSection.mockImplementation(({children}) => ( + + )); + render(); + await screen.findByText('Meet our student blogger Kharl'); + }); + it('returns no-blogs message if none found', async () => { + const dataNoSubject = {...businessBooksData}; + + dataNoSubject.title = ''; + mockUsePageData.mockReturnValue(dataNoSubject); + mockUseDataFromSlug.mockReturnValue(undefined); + render(); + await screen.findByText('No blog entries found (yet)'); + }); +}); diff --git a/test/src/pages/subjects/book-viewer.test.tsx b/test/src/pages/subjects/book-viewer.test.tsx new file mode 100644 index 000000000..00c116dee --- /dev/null +++ b/test/src/pages/subjects/book-viewer.test.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import {describe, expect, it} from '@jest/globals'; +import {render, screen} from '@testing-library/preact'; +import BookViewer from '~/pages/subjects/new/specific/book-viewer'; +import ShellContextProvider from '~/../../test/helpers/shell-context'; +import {SpecificSubjectContextProvider} from '~/pages/subjects/new/specific/context'; +import businessBooksData from '~/../../test/src/data/business-books'; +import {MemoryRouter} from 'react-router-dom'; + +const mockUsePageData = jest.fn(); + +jest.mock('~/helpers/use-page-data', () => ({ + __esModule: true, + default: () => mockUsePageData() +})); + +mockUsePageData.mockReturnValue(businessBooksData); + +const mockBookTile = jest.fn(); + +jest.mock('~/components/book-tile/book-tile', () => ({ + __esModule: true, + default: () => mockBookTile() +})); + +mockBookTile.mockImplementation(() =>
); + +function Component({ + pathAndHash = '/subjects/business' +}: { + pathAndHash?: string; +}) { + return ( + + + + + + + + ); +} + +describe('subjects/book-viewer', () => { + afterEach(() => jest.clearAllMocks()); + it('renders', async () => { + const eventSpy = jest.spyOn(document.body, 'addEventListener'); + + render(); + expect(await screen.findAllByRole('heading')).toHaveLength(5); + expect(eventSpy).not.toHaveBeenCalled(); + }); + it('scrolls to hash', async () => { + const eventSpy = jest.spyOn(document.body, 'addEventListener'); + + render(); + expect(await screen.findAllByRole('heading')).toHaveLength(5); + expect(eventSpy).toHaveBeenCalled(); + }); +}); diff --git a/test/src/pages/subjects/learn-more.test.tsx b/test/src/pages/subjects/learn-more.test.tsx new file mode 100644 index 000000000..e3f7026fe --- /dev/null +++ b/test/src/pages/subjects/learn-more.test.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import {describe, expect, it} from '@jest/globals'; +import {render, screen} from '@testing-library/preact'; +import ShellContextProvider from '~/../../test/helpers/shell-context'; +import {SpecificSubjectContextProvider} from '~/pages/subjects/new/specific/context'; +import LearnMore from '~/pages/subjects/new/specific/learn-more'; +import businessBooksData from '~/../../test/src/data/business-books'; +import businessBlogBlurbs from '~/../../test/src/data/business-blog-blurbs'; + +const mockUsePageData = jest.fn(); +const mockUseDataFromSlug = jest.fn(); + +jest.mock('~/helpers/use-page-data', () => ({ + __esModule: true, + default: () => mockUsePageData() +})); +jest.mock('~/helpers/page-data-utils', () => ({ + __esModule: true, + ...jest.requireActual('~/helpers/page-data-utils'), + useDataFromSlug: () => mockUseDataFromSlug() +})); + +function Component() { + return ( + + + + + + ); +} + +describe('subjects/learn-more section', () => { + it('returns null if context is empty', () => { + mockUsePageData.mockReturnValue(undefined); + mockUseDataFromSlug.mockReturnValue(undefined); + const {container} = render(); + + expect(container.innerHTML).toBe(''); + }); + it('renders the section', async () => { + mockUsePageData.mockReturnValue(businessBooksData); + mockUseDataFromSlug.mockReturnValue(businessBlogBlurbs); + + render(); + await screen.findByText('Learn more about OpenStax Business textbooks'); + }); +}); diff --git a/test/src/pages/subjects/navigator.test.tsx b/test/src/pages/subjects/navigator.test.tsx new file mode 100644 index 000000000..734716659 --- /dev/null +++ b/test/src/pages/subjects/navigator.test.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import {describe, expect, it} from '@jest/globals'; +import {render} from '@testing-library/preact'; +import Navigator from '~/pages/subjects/new/specific/navigator'; +import ShellContextProvider from '~/../../test/helpers/shell-context'; +import {SpecificSubjectContextProvider} from '~/pages/subjects/new/specific/context'; +import businessBooksData from '~/../../test/src/data/business-books'; +import {MemoryRouter} from 'react-router-dom'; +import {NavigatorContextProvider} from '~/pages/subjects/new/specific/navigator-context'; + +const mockUsePageData = jest.fn(); + +jest.mock('~/helpers/use-page-data', () => ({ + __esModule: true, + default: () => mockUsePageData() +})); + +mockUsePageData.mockReturnValue(businessBooksData); +const subject = { + value: 'business', + cms: 'business', + html: 'Business', + title: 'business', + icon: '?', + color: '?' +}; + +function Component() { + return ( + + + + + + + + + + ); +} + +describe('subjects/navigator-context', () => { + jest.spyOn(console, 'info').mockImplementation(() => null); + + it('handles clicks for links whose target is not there', async () => { + render(); + + expect(console.info).toHaveBeenCalledWith('Did not find', 'Accounting'); + }); +}); diff --git a/test/src/pages/subjects/specific.test.tsx b/test/src/pages/subjects/specific.test.tsx index 11a61af04..09b85d69f 100644 --- a/test/src/pages/subjects/specific.test.tsx +++ b/test/src/pages/subjects/specific.test.tsx @@ -1,68 +1,70 @@ import React from 'react'; import {describe, expect, it} from '@jest/globals'; import {render, screen} from '@testing-library/preact'; +import userEvent from '@testing-library/user-event'; import {MemoryRouter, Routes, Route} from 'react-router-dom'; import ShellContextProvider from '~/../../test/helpers/shell-context'; import LoadSubject from '~/pages/subjects/new/specific/specific'; -import useSpecificSubjectContext from '~/pages/subjects/new/specific/context'; import useDebounceTest from '~/helpers/use-debounce-test'; import FindTranslation from '~/pages/subjects/new/specific/find-translation'; -import { - mathSubjectContext, - tutorAd, - infoBoxes, - aboutOs -} from '../../data/specific-subject'; import LazyLoad from 'react-lazyload'; +import businessBooksData from '~/../../test/src/data/business-books'; -function Component({subject = 'math'}) { +const adText = 'Instructors, take your course online'; +const infoboxText = 'Expert Authors'; +const aboutText = 'About Openstax textbooks'; // sic + +function Component({subject = 'business'}) { return ( - } /> + } /> ); } -const mathCategories = { - value: 'math', - cms: 'math', - html: 'Math', - title: 'math', - icon: 'image', - color: 'red' -}; +const mockUsePageData = jest.fn(); + +jest.mock('~/helpers/use-page-data', () => ({ + __esModule: true, + default: () => mockUsePageData() +})); -jest.mock('~/contexts/subject-category', () => jest.fn(() => [mathCategories])); -jest.mock('~/pages/subjects/new/specific/context', () => - jest.fn(() => ({categories: []})) -); jest.mock('~/components/promote-badge/promote-badge', () => jest.fn()); jest.mock('~/helpers/use-debounce-test', () => jest.fn(() => false)); jest.mock('~/pages/subjects/new/specific/find-translation', () => jest.fn()); -jest.mock('~/pages/subjects/new/specific/navigator', () => jest.fn()); -jest.mock('~/pages/subjects/new/specific/book-viewer', () => jest.fn()); +jest.mock('~/components/book-tile/book-tile', () => jest.fn()); jest.mock('~/pages/subjects/new/specific/translation-selector', () => jest.fn() ); jest.mock('react-lazyload', () => jest.fn()); -jest.mock('~/pages/subjects/new/specific/blog-posts.js', () => jest.fn()); -jest.mock('~/pages/subjects/new/specific/learn-more.js', () => jest.fn()); +jest.mock('~/pages/subjects/new/specific/import-blog-posts.js', () => + jest.fn() +); +jest.mock('~/pages/subjects/new/specific/import-learn-more.js', () => + jest.fn() +); +jest.mock('~/pages/subjects/new/specific/webinars', () => jest.fn()); + +const mockLazyLoad = LazyLoad as jest.Mock; describe('specific subject page', () => { - it('renders something', () => { + beforeEach(() => mockUsePageData.mockReturnValue(businessBooksData)); + + const user = userEvent.setup(); + + it('renders something', async () => { render(); - expect(screen.queryAllByText('Open textbooks')).toHaveLength(1); - expect(screen.queryAllByText('Math')).toHaveLength(1); + await screen.findByText('Business Book Categories'); }); it('handles subject not found', () => { - render(); + render(); expect( screen.queryAllByText('Categories', {exact: false}) ).toHaveLength(0); @@ -70,38 +72,51 @@ describe('specific subject page', () => { }); it('handles subject not found after timeout', () => { (useDebounceTest as jest.Mock).mockReturnValueOnce(true); - render(); + render(); expect( screen.queryAllByText('Categories', {exact: false}) ).toHaveLength(0); expect(FindTranslation).toHaveBeenCalled(); }); - it('omits infoboxes, tutor ad, about OS when not supplied', () => { - (LazyLoad as jest.Mock).mockImplementation(({children}) => ( + it('omits infoboxes, tutor ad, about OS, translations, learn-more when not supplied', async () => { + mockLazyLoad.mockImplementation(({children}) => ( {children} )); - (useSpecificSubjectContext as jest.Mock).mockReturnValue( - mathSubjectContext - ); + // Remove them from page data + mockUsePageData.mockReturnValueOnce({ + ...businessBooksData, + aboutOs: undefined, + infoBoxes: undefined, + tutorAd: undefined, + translations: undefined, + learnMoreAboutBooks: undefined, + pageDescription: undefined // just substitutes empty string + }); render(); - expect(screen.queryAllByText('Open textbooks')).toHaveLength(1); - expect(screen.queryAllByText(tutorAd.content.heading)).toHaveLength(0); - expect(screen.queryAllByText(infoBoxes[0][0].heading)).toHaveLength(0); - expect(screen.queryAllByText(aboutOs.content.heading)).toHaveLength(0); + await screen.findAllByText('Open textbooks'); + expect(screen.queryAllByText(adText)).toHaveLength(0); + expect(screen.queryAllByText(infoboxText)).toHaveLength(0); + expect(screen.queryAllByText(aboutText)).toHaveLength(0); }); - it('does infoboxes, tutor ad, and about OS when present', () => { - (LazyLoad as jest.Mock).mockImplementation(({children}) => ( + it('does infoboxes, tutor ad, and about OS when present', async () => { + mockLazyLoad.mockImplementation(({children}) => ( + {children} + )); + render(); + await screen.findByText(adText); + screen.getByText(infoboxText); + screen.getByText(aboutText); + }); + it('has working navigator links', async () => { + mockLazyLoad.mockImplementation(({children}) => ( {children} )); - (useSpecificSubjectContext as jest.Mock).mockReturnValue({ - ...mathSubjectContext, - tutorAd, - infoBoxes, - aboutOs - }); render(); - expect(screen.queryAllByText(tutorAd.content.heading)).toHaveLength(1); - expect(screen.queryAllByText(infoBoxes[0][0].heading)).toHaveLength(1); - expect(screen.queryAllByText(aboutOs.content.heading)).toHaveLength(1); + const [accountingLink, ...links] = + await screen.findAllByRole('link'); + + expect(links).toHaveLength(9); + await user.click(accountingLink); + // This is just a scroll action; no test }); });