From 9ddae878f3a06dd53347bacf9fb7883c7c6a4a0a Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Mon, 14 Oct 2024 16:46:37 -0500 Subject: [PATCH 1/7] Context, blog-posts, carousel-section --- src/app/components/carousel/carousel.js | 5 +- .../{blog-posts.js => blog-posts.tsx} | 19 +- ...rousel-section.js => carousel-section.tsx} | 13 +- .../pages/subjects/new/specific/context.d.ts | 68 -- .../pages/subjects/new/specific/context.js | 44 -- .../pages/subjects/new/specific/context.ts | 107 ++++ .../new/specific/import-blog-posts.js | 3 + .../pages/subjects/new/specific/specific.tsx | 2 +- .../pages/subjects/new/specific/webinars.tsx | 16 +- test/src/data/business-blog-blurbs.js | 28 + test/src/data/business-books.js | 594 ++++++++++++++++++ test/src/pages/subjects/blog-posts.test.tsx | 68 ++ test/src/pages/subjects/specific.test.tsx | 2 +- 13 files changed, 836 insertions(+), 133 deletions(-) rename src/app/pages/subjects/new/specific/{blog-posts.js => blog-posts.tsx} (75%) rename src/app/pages/subjects/new/specific/components/{carousel-section.js => carousel-section.tsx} (75%) delete mode 100644 src/app/pages/subjects/new/specific/context.d.ts delete mode 100644 src/app/pages/subjects/new/specific/context.js create mode 100644 src/app/pages/subjects/new/specific/context.ts create mode 100644 src/app/pages/subjects/new/specific/import-blog-posts.js create mode 100644 test/src/data/business-blog-blurbs.js create mode 100644 test/src/data/business-books.js create mode 100644 test/src/pages/subjects/blog-posts.test.tsx 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/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/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..ed83c3913 --- /dev/null +++ b/src/app/pages/subjects/new/specific/context.ts @@ -0,0 +1,107 @@ +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 {InfoBox} from '../info-boxes'; +import {LocaleEntry} from '~/components/language-selector/language-selector'; + +// 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 BlogSectionInfo = { + content: SectionContent & { + blogDescription: string; + }; +}; + +type SpecificSubjectPageData = { + translations?: [LocaleEntry[]]; + title?: string; + subjects?: SubjectEntry; + tutorAd: SectionInfo; + aboutOs: SectionInfo; + webinarHeader: WebinarSectionInfo; + infoBoxes: InfoBox; + blogHeader: BlogSectionInfo; +}; + +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/specific.tsx b/src/app/pages/subjects/new/specific/specific.tsx index 08215c702..f937c5a57 100644 --- a/src/app/pages/subjects/new/specific/specific.tsx +++ b/src/app/pages/subjects/new/specific/specific.tsx @@ -24,7 +24,7 @@ import './specific.scss'; const importPhilanthropicSupport = () => import('../import-philanthropic-support.js'); const importLearnMore = () => 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/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/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/specific.test.tsx b/test/src/pages/subjects/specific.test.tsx index 11a61af04..42ae53955 100644 --- a/test/src/pages/subjects/specific.test.tsx +++ b/test/src/pages/subjects/specific.test.tsx @@ -52,7 +52,7 @@ 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/import-blog-posts.js', () => jest.fn()); jest.mock('~/pages/subjects/new/specific/learn-more.js', () => jest.fn()); describe('specific subject page', () => { From 5830f553d7dc4e4a5d6567a4b364ee8ea1f9115e Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Mon, 14 Oct 2024 17:25:06 -0500 Subject: [PATCH 2/7] Learn More section --- .../accordion-group/accordion-group.js | 2 +- .../pages/subjects/new/specific/context.ts | 3 ++ .../new/specific/import-learn-more.js | 3 ++ .../{learn-more.js => learn-more.tsx} | 12 +++-- .../pages/subjects/new/specific/specific.tsx | 2 +- test/src/pages/subjects/learn-more.test.tsx | 48 +++++++++++++++++++ test/src/pages/subjects/specific.test.tsx | 2 +- 7 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 src/app/pages/subjects/new/specific/import-learn-more.js rename src/app/pages/subjects/new/specific/{learn-more.js => learn-more.tsx} (77%) create mode 100644 test/src/pages/subjects/learn-more.test.tsx 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/pages/subjects/new/specific/context.ts b/src/app/pages/subjects/new/specific/context.ts index ed83c3913..f23a82fe3 100644 --- a/src/app/pages/subjects/new/specific/context.ts +++ b/src/app/pages/subjects/new/specific/context.ts @@ -5,6 +5,7 @@ import {setPageTitleAndDescriptionFromBookData} from '~/helpers/use-document-hea import {ImageData} from '../context'; import type {InfoBox} from '../info-boxes'; 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 = { @@ -67,6 +68,8 @@ type SpecificSubjectPageData = { webinarHeader: WebinarSectionInfo; infoBoxes: InfoBox; blogHeader: BlogSectionInfo; + osTextbookHeading: string; + osTextbookCategories: [OsTextbookCategory[]]; }; const preserveWrapping = false; 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/specific.tsx b/src/app/pages/subjects/new/specific/specific.tsx index f937c5a57..449874ef8 100644 --- a/src/app/pages/subjects/new/specific/specific.tsx +++ b/src/app/pages/subjects/new/specific/specific.tsx @@ -22,7 +22,7 @@ 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('./import-blog-posts.js'); 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/specific.test.tsx b/test/src/pages/subjects/specific.test.tsx index 42ae53955..cc9af1fef 100644 --- a/test/src/pages/subjects/specific.test.tsx +++ b/test/src/pages/subjects/specific.test.tsx @@ -53,7 +53,7 @@ jest.mock('~/pages/subjects/new/specific/translation-selector', () => ); jest.mock('react-lazyload', () => jest.fn()); jest.mock('~/pages/subjects/new/specific/import-blog-posts.js', () => jest.fn()); -jest.mock('~/pages/subjects/new/specific/learn-more.js', () => jest.fn()); +jest.mock('~/pages/subjects/new/specific/import-learn-more.js', () => jest.fn()); describe('specific subject page', () => { it('renders something', () => { From e21ec45daf33b6e86e6cb7edb7f453ad11b0f7b8 Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Mon, 14 Oct 2024 17:46:34 -0500 Subject: [PATCH 3/7] Subject-intro --- src/app/pages/subjects/new/specific/context.ts | 1 + .../{subject-intro.js => subject-intro.tsx} | 16 ++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) rename src/app/pages/subjects/new/specific/{subject-intro.js => subject-intro.tsx} (64%) diff --git a/src/app/pages/subjects/new/specific/context.ts b/src/app/pages/subjects/new/specific/context.ts index f23a82fe3..cc0a97b83 100644 --- a/src/app/pages/subjects/new/specific/context.ts +++ b/src/app/pages/subjects/new/specific/context.ts @@ -70,6 +70,7 @@ type SpecificSubjectPageData = { blogHeader: BlogSectionInfo; osTextbookHeading: string; osTextbookCategories: [OsTextbookCategory[]]; + pageDescription: string; }; const preserveWrapping = false; 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 (
From f87d90184738e7158bd0f5f73fdd6dae2261c322 Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Tue, 15 Oct 2024 10:00:47 -0500 Subject: [PATCH 4/7] BookViewer --- .../{book-viewer.js => book-viewer.tsx} | 25 ++++---- .../pages/subjects/new/specific/context.ts | 16 +++-- test/src/pages/subjects/book-viewer.test.tsx | 60 +++++++++++++++++++ 3 files changed, 82 insertions(+), 19 deletions(-) rename src/app/pages/subjects/new/specific/{book-viewer.js => book-viewer.tsx} (74%) create mode 100644 test/src/pages/subjects/book-viewer.test.tsx 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/context.ts b/src/app/pages/subjects/new/specific/context.ts index cc0a97b83..222e825b5 100644 --- a/src/app/pages/subjects/new/specific/context.ts +++ b/src/app/pages/subjects/new/specific/context.ts @@ -19,18 +19,16 @@ export type Book = { coverUrl: string; }; -type Category = [ - string, - { - books: { - [title: string]: [Book]; - }; - } -]; +export type CategoryData = { + books: { + [title: string]: [Book]; + }; + categoryDescription: string; +}; type SubjectEntry = { [title: string]: { - categories: Category[]; + categories: CategoryData[]; }; }; 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(); + }); +}); From 39579052e52973d5ab3aeef1fa53ada332ab5789 Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Tue, 15 Oct 2024 17:25:58 -0500 Subject: [PATCH 5/7] Specific-subjects and navigator --- src/app/contexts/subject-category.ts | 2 +- .../pages/subjects/new/specific/context.ts | 3 + .../new/specific/navigator-context.js | 87 ----------- .../new/specific/navigator-context.ts | 65 ++++++++ .../specific/{navigator.js => navigator.tsx} | 141 ++++++++++-------- test/src/pages/subjects/navigator.test.tsx | 53 +++++++ test/src/pages/subjects/specific.test.tsx | 117 ++++++++------- 7 files changed, 266 insertions(+), 202 deletions(-) delete mode 100644 src/app/pages/subjects/new/specific/navigator-context.js create mode 100644 src/app/pages/subjects/new/specific/navigator-context.ts rename src/app/pages/subjects/new/specific/{navigator.js => navigator.tsx} (67%) create mode 100644 test/src/pages/subjects/navigator.test.tsx 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/specific/context.ts b/src/app/pages/subjects/new/specific/context.ts index 222e825b5..fc3be4506 100644 --- a/src/app/pages/subjects/new/specific/context.ts +++ b/src/app/pages/subjects/new/specific/context.ts @@ -69,6 +69,9 @@ type SpecificSubjectPageData = { osTextbookHeading: string; osTextbookCategories: [OsTextbookCategory[]]; pageDescription: string; + learnMoreAboutBooks: string; + learnMoreBlogPosts: string; + learnMoreWebinars: string; }; const preserveWrapping = false; 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/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 cc9af1fef..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/import-blog-posts.js', () => jest.fn()); -jest.mock('~/pages/subjects/new/specific/import-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 }); }); From 98c8060adbe1245cce03bf576277bfc0f5a55980 Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Wed, 16 Oct 2024 07:22:22 -0500 Subject: [PATCH 6/7] About OpenStax and some prettier --- .../{about-openstax.js => about-openstax.tsx} | 32 +++++++++++++----- src/app/pages/subjects/new/context.tsx | 2 ++ src/app/pages/subjects/new/hero.tsx | 4 ++- .../pages/subjects/new/specific/context.ts | 3 +- .../pages/subjects/new/subjects-listing.tsx | 2 +- src/app/pages/subjects/new/subjects.tsx | 33 ++++++++++--------- 6 files changed, 49 insertions(+), 27 deletions(-) rename src/app/pages/subjects/new/{about-openstax.js => about-openstax.tsx} (55%) 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.tsx index c10fe80e2..e63c05ec6 100644 --- a/src/app/pages/subjects/new/context.tsx +++ b/src/app/pages/subjects/new/context.tsx @@ -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/context.ts b/src/app/pages/subjects/new/specific/context.ts index fc3be4506..08b41aa1d 100644 --- a/src/app/pages/subjects/new/specific/context.ts +++ b/src/app/pages/subjects/new/specific/context.ts @@ -4,6 +4,7 @@ import buildContext from '~/components/jsx-helpers/build-context'; import {setPageTitleAndDescriptionFromBookData} from '~/helpers/use-document-head'; import {ImageData} from '../context'; import type {InfoBox} from '../info-boxes'; +import type { AboutOsData } from '../about-openstax'; import {LocaleEntry} from '~/components/language-selector/language-selector'; import type {OsTextbookCategory} from './learn-more'; @@ -62,7 +63,7 @@ type SpecificSubjectPageData = { title?: string; subjects?: SubjectEntry; tutorAd: SectionInfo; - aboutOs: SectionInfo; + aboutOs: {content: AboutOsData}; webinarHeader: WebinarSectionInfo; infoBoxes: InfoBox; blogHeader: BlogSectionInfo; 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={} /> - } /> + } /> ); From 516fc132085b10537f7905445f88d3d61ffcbfb0 Mon Sep 17 00:00:00 2001 From: Roy Johnson Date: Wed, 30 Oct 2024 09:41:50 -0500 Subject: [PATCH 7/7] Rebase; move context to .ts --- src/app/pages/subjects/new/{context.tsx => context.ts} | 0 src/app/pages/subjects/new/specific/context.ts | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/app/pages/subjects/new/{context.tsx => context.ts} (100%) diff --git a/src/app/pages/subjects/new/context.tsx b/src/app/pages/subjects/new/context.ts similarity index 100% rename from src/app/pages/subjects/new/context.tsx rename to src/app/pages/subjects/new/context.ts diff --git a/src/app/pages/subjects/new/specific/context.ts b/src/app/pages/subjects/new/specific/context.ts index 08b41aa1d..6f3dcb5a0 100644 --- a/src/app/pages/subjects/new/specific/context.ts +++ b/src/app/pages/subjects/new/specific/context.ts @@ -3,7 +3,7 @@ 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 {InfoBox} from '../info-boxes'; +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'; @@ -65,7 +65,7 @@ type SpecificSubjectPageData = { tutorAd: SectionInfo; aboutOs: {content: AboutOsData}; webinarHeader: WebinarSectionInfo; - infoBoxes: InfoBox; + infoBoxes: InfoBoxValues; blogHeader: BlogSectionInfo; osTextbookHeading: string; osTextbookCategories: [OsTextbookCategory[]];