Skip to content

Commit

Permalink
blog-pages
Browse files Browse the repository at this point in the history
  • Loading branch information
RoyEJohnson committed Oct 29, 2024
1 parent 839e807 commit b7986ce
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 138 deletions.
2 changes: 1 addition & 1 deletion src/app/components/explore-by-subject/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type Category = {
id: string;
id: number;
name: string;
subjectIcon?: string;
};
2 changes: 1 addition & 1 deletion src/app/pages/blog/article/article.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import './article.scss';

type ArticleArgs = {
slug: string;
onLoad: (data: unknown) => void;
onLoad: (data: ArticleData) => void;
};

export function ArticleFromSlug({slug, onLoad}: ArticleArgs) {
Expand Down
106 changes: 106 additions & 0 deletions src/app/pages/blog/blog-pages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import React, {useEffect} from 'react';
import useBlogContext from './blog-context';
import { useParams} from 'react-router-dom';
import {WindowContextProvider} from '~/contexts/window';
import useDocumentHead from '~/helpers/use-document-head';
import RawHTML from '~/components/jsx-helpers/raw-html';
import ExploreBySubject from '~/components/explore-by-subject/explore-by-subject';
import ExploreByCollection from '~/components/explore-by-collection/explore-by-collection';
import PinnedArticle from './pinned-article/pinned-article';
import DisqusForm from './disqus-form/disqus-form';
import MoreStories from './more-stories/more-stories';
import SearchBar, {HeadingAndSearchBar} from '~/components/search-bar/search-bar';
import SearchResults from './search-results/search-results';
import {ArticleData, ArticleFromSlug} from './article/article';
import GatedContentDialog from './gated-content-dialog/gated-content-dialog';
import './blog.scss';

function WriteForUs({descriptionHtml, text, link}: {
descriptionHtml: string;
text: string;
link: string;
}) {
return (
<section className='boxed'>
<RawHTML Tag='h2' className="description" html={descriptionHtml} />
<a href={link} className="btn primary">{text}</a>
</section>
);
}

export function SearchResultsPage() {
const {pageDescription, searchFor} = useBlogContext();

useDocumentHead({
title: 'OpenStax Blog Search',
description: pageDescription
});

return (
<React.Fragment>
<div className="boxed left">
<SearchBar searchFor={searchFor} amongWhat='blog posts' />
</div>
<SearchResults />
</React.Fragment>
);
}

// Exported so it can be tested
// eslint-disable-next-line complexity
export function MainBlogPage() {
const {
pinnedStory, pageDescription, searchFor,
subjectSnippet: categories,
collectionSnippet: collections,
footerText, footerButtonText, footerLink
} = useBlogContext();
const writeForUsData = {
descriptionHtml: footerText || 'Interested in sharing your story?',
text: footerButtonText || 'Write for us',
link: footerLink || '/write-for-us'
};

useDocumentHead({
title: 'OpenStax News',
description: pageDescription
});

return (
<WindowContextProvider>
<div className="boxed">
<HeadingAndSearchBar searchFor={searchFor} amongWhat='blog posts'>
<h1>OpenStax Blog</h1>
</HeadingAndSearchBar>
<ExploreBySubject categories={categories} analyticsNav='Blog Subjects' />
<ExploreByCollection collections={collections} analyticsNav='Blog Collections' />
<PinnedArticle />
<MoreStories exceptSlug={pinnedStory && pinnedStory.meta.slug} />
</div>
<div className="write-for-us">
<WriteForUs {...writeForUsData} />
</div>
</WindowContextProvider>
);
}

export function ArticlePage() {
const {slug} = useParams();
const [articleData, setArticleData] = React.useState<ArticleData>();

useEffect(
() => window.scrollTo(0, 0),
[slug]
);

return (
<WindowContextProvider>
<ArticleFromSlug slug={`news/${slug}`} onLoad={setArticleData} />
<DisqusForm />
<div className="boxed">
<MoreStories exceptSlug={slug as string} />
</div>
<GatedContentDialog articleData={articleData} />
</WindowContextProvider>
);
}
110 changes: 6 additions & 104 deletions src/app/pages/blog/blog.js
Original file line number Diff line number Diff line change
@@ -1,108 +1,10 @@
import React, {useEffect} from 'react';
import useBlogContext, {BlogContextProvider} from './blog-context';
import {Routes, Route, useLocation, useParams} from 'react-router-dom';
import {WindowContextProvider} from '~/contexts/window';
import useDocumentHead, {useCanonicalLink} from '~/helpers/use-document-head';
import RawHTML from '~/components/jsx-helpers/raw-html';
import ExploreBySubject from '~/components/explore-by-subject/explore-by-subject';
import ExploreByCollection from '~/components/explore-by-collection/explore-by-collection';
import ExplorePage from './explore-page/explore-page';
import PinnedArticle from './pinned-article/pinned-article';
import DisqusForm from './disqus-form/disqus-form';
import MoreStories from './more-stories/more-stories';
import SearchBar, {HeadingAndSearchBar} from '~/components/search-bar/search-bar';
import SearchResults from './search-results/search-results';
import React from 'react';
import {Routes, Route, useLocation} from 'react-router-dom';
import LatestBlogPosts from './latest-blog-posts/latest-blog-posts';
import {ArticleFromSlug} from './article/article';
import GatedContentDialog from './gated-content-dialog/gated-content-dialog';
import './blog.scss';

function WriteForUs({descriptionHtml, text, link}) {
return (
<section className='boxed'>
<RawHTML Tag='h2' className="description" html={descriptionHtml} />
<a href={link} className="btn primary">{text}</a>
</section>
);
}

export function SearchResultsPage() {
const {pageDescription, searchFor} = useBlogContext();

useDocumentHead({
title: 'OpenStax Blog Search',
description: pageDescription
});

return (
<React.Fragment>
<div className="boxed left">
<SearchBar searchFor={searchFor} amongWhat='blog posts' />
</div>
<SearchResults />
</React.Fragment>
);
}

// Exported so it can be tested
// eslint-disable-next-line complexity
export function MainBlogPage() {
const {
pinnedStory, pageDescription, searchFor,
subjectSnippet: categories,
collectionSnippet: collections,
footerText, footerButtonText, footerLink
} = useBlogContext();
const writeForUsData = {
descriptionHtml: footerText || 'Interested in sharing your story?',
text: footerButtonText || 'Write for us',
link: footerLink || '/write-for-us'
};

useDocumentHead({
title: 'OpenStax News',
description: pageDescription
});

return (
<WindowContextProvider>
<div className="boxed">
<HeadingAndSearchBar searchFor={searchFor} amongWhat='blog posts'>
<h1>OpenStax Blog</h1>
</HeadingAndSearchBar>
<ExploreBySubject categories={categories} analyticsNav='Blog Subjects' />
<ExploreByCollection collections={collections} analyticsNav='Blog Collections' />
<PinnedArticle />
<MoreStories exceptSlug={pinnedStory && pinnedStory.meta.slug} />
</div>
<div className="write-for-us">
<WriteForUs {...writeForUsData} />
</div>
</WindowContextProvider>
);
}

// Export so it can be tested
export function ArticlePage() {
const {slug} = useParams();
const [articleData, setArticleData] = React.useState();

useEffect(
() => window.scrollTo(0, 0),
[slug]
);

return (
<WindowContextProvider>
<ArticleFromSlug slug={`news/${slug}`} onLoad={setArticleData} />
<DisqusForm />
<div className="boxed">
<MoreStories exceptSlug={slug} />
</div>
<GatedContentDialog articleData={articleData} />
</WindowContextProvider>
);
}
import {useCanonicalLink} from '~/helpers/use-document-head';
import {SearchResultsPage, MainBlogPage, ArticlePage} from './blog-pages';
import ExplorePage from './explore-page/explore-page';
import {BlogContextProvider} from './blog-context';

export default function LoadBlog() {
const location = useLocation();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type WindowWithSettings = typeof window & {
const formSubmitUrl = (window as WindowWithSettings).SETTINGS
.gatedContentEndpoint;

export default function WaitForData({articleData}: {articleData: ArticleData}) {
export default function WaitForData({articleData}: {articleData?: ArticleData}) {
return articleData?.gatedContent ? <GatedContentDialog /> : null;
}

Expand Down
2 changes: 1 addition & 1 deletion src/app/pages/blog/more-stories/more-stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function LatestBlurbs({page, pageSize, exceptSlug='', openInNewWindow}: {
export default function MoreStories({exceptSlug, subhead}:
{
exceptSlug: string;
subhead: Parameters<typeof Section>[0]['topicHeading'];
subhead?: Parameters<typeof Section>[0]['topicHeading'];
}
) {
return (
Expand Down
1 change: 1 addition & 0 deletions test/src/data/search-subject.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default [
date: '2022-05-23',
author: 'Ed',
pin_to_top: false,
heading: 'future-test',
tags: [],
collections: [
{
Expand Down
129 changes: 99 additions & 30 deletions test/src/pages/blog/blog.test.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,104 @@
import React from 'react';
import {render, screen} from '@testing-library/preact';
import {render, screen, waitFor} from '@testing-library/preact';
import {BrowserRouter, MemoryRouter, Routes, Route} from 'react-router-dom';
import {BlogContextProvider} from '~/pages/blog/blog-context';
import {MainBlogPage, ArticlePage} from '~/pages/blog/blog';
import useBlogContext, {
BlogContextProvider,
assertTType
} from '~/pages/blog/blog-context';
import {
MainBlogPage,
ArticlePage,
SearchResultsPage
} from '~/pages/blog/blog-pages';
import {MainClassContextProvider} from '~/contexts/main-class';
import {test, expect} from '@jest/globals';

test('blog default page', async () => {
render(
<BrowserRouter>
<BlogContextProvider>
<MainClassContextProvider>
<MainBlogPage />
</MainClassContextProvider>
</BlogContextProvider>
</BrowserRouter>
);
expect(await screen.findAllByText('Read more')).toHaveLength(3);
expect(screen.queryAllByRole('textbox')).toHaveLength(1);
});
import {describe, test, expect} from '@jest/globals';
import * as PDU from '~/helpers/page-data-utils';

describe('blog pages', () => {
beforeAll(() => {
const description = document.createElement('meta');

description.setAttribute('name', 'description');
document.head.appendChild(description);
});
test('Main page', async () => {
render(
<BrowserRouter>
<BlogContextProvider>
<MainClassContextProvider>
<MainBlogPage />
</MainClassContextProvider>
</BlogContextProvider>
</BrowserRouter>
);
expect(await screen.findAllByText('Read more')).toHaveLength(3);
expect(screen.queryAllByRole('textbox')).toHaveLength(1);
});

test('Article page', async () => {
window.scrollTo = jest.fn();

render(
<MemoryRouter initialEntries={['/blog/blog-article']}>
<BlogContextProvider>
<Routes>
<Route path="/blog/:slug" element={<ArticlePage />} />
</Routes>
</BlogContextProvider>
</MemoryRouter>
);
expect(await screen.findAllByText('Read more')).toHaveLength(3);
expect(screen.queryAllByRole('link')).toHaveLength(7);
expect(window.scrollTo).toHaveBeenCalledWith(0, 0);
});

test('Search Results page', async () => {
/* eslint-disable camelcase, max-len */
jest.spyOn(PDU, 'fetchFromCMS').mockResolvedValueOnce(
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((id) => ({
id,
slug: `slug-${id}`,
collections: [],
article_subjects: []
}))
);

render(
<MemoryRouter initialEntries={['/blog/?q=education']}>
<SearchResultsPage />
</MemoryRouter>
);
expect(document.head.querySelector('title')?.textContent).toBe(
'OpenStax Blog Search'
);
});

test('assertTType throws for invalid value', () => {
expect(() => assertTType('invalid')).toThrowError();
});

test('blog-context searchFor', async () => {
function Inner() {
const {searchFor} = useBlogContext();

React.useEffect(() => searchFor('education'), [searchFor]);

return null;
}

function Outer() {
return (
<MemoryRouter initialEntries={['/blog/blog-article']}>
<BlogContextProvider>
<Inner />
</BlogContextProvider>
</MemoryRouter>
);
}

window.scrollTo = jest.fn();

test('blog Article page', async () => {
render(
<MemoryRouter initialEntries={['/blog/blog-article']}>
<BlogContextProvider>
<Routes>
<Route path='/blog/:slug' element={<ArticlePage />} />
</Routes>
</BlogContextProvider>
</MemoryRouter>
);
expect(await screen.findAllByText('Read more')).toHaveLength(3);
expect(screen.queryAllByRole('link')).toHaveLength(7);
render(<Outer />);
await waitFor(() => expect(window.scrollTo).toHaveBeenCalledWith(0, 0));
});
});

0 comments on commit b7986ce

Please sign in to comment.