diff options
Diffstat (limited to 'src/components/organisms/layout')
| -rw-r--r-- | src/components/organisms/layout/index.ts | 2 | ||||
| -rw-r--r-- | src/components/organisms/layout/no-results.stories.tsx | 15 | ||||
| -rw-r--r-- | src/components/organisms/layout/no-results.test.tsx | 12 | ||||
| -rw-r--r-- | src/components/organisms/layout/no-results.tsx | 55 | ||||
| -rw-r--r-- | src/components/organisms/layout/posts-list.fixture.tsx | 62 | ||||
| -rw-r--r-- | src/components/organisms/layout/posts-list.module.scss | 66 | ||||
| -rw-r--r-- | src/components/organisms/layout/posts-list.stories.tsx | 191 | ||||
| -rw-r--r-- | src/components/organisms/layout/posts-list.test.tsx | 31 | ||||
| -rw-r--r-- | src/components/organisms/layout/posts-list.tsx | 327 |
9 files changed, 0 insertions, 761 deletions
diff --git a/src/components/organisms/layout/index.ts b/src/components/organisms/layout/index.ts deleted file mode 100644 index 03fba32..0000000 --- a/src/components/organisms/layout/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './no-results'; -export * from './posts-list'; diff --git a/src/components/organisms/layout/no-results.stories.tsx b/src/components/organisms/layout/no-results.stories.tsx deleted file mode 100644 index cfcee83..0000000 --- a/src/components/organisms/layout/no-results.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import { NoResults as NoResultsComponent } from './no-results'; - -export default { - title: 'Organisms/Layout', - component: NoResultsComponent, - argTypes: {}, -} as ComponentMeta<typeof NoResultsComponent>; - -const Template: ComponentStory<typeof NoResultsComponent> = (args) => ( - <NoResultsComponent {...args} /> -); - -export const NoResults = Template.bind({}); -NoResults.args = {}; diff --git a/src/components/organisms/layout/no-results.test.tsx b/src/components/organisms/layout/no-results.test.tsx deleted file mode 100644 index fdd86f7..0000000 --- a/src/components/organisms/layout/no-results.test.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen as rtlScreen } from '../../../../tests/utils'; -import { NoResults } from './no-results'; - -describe('NoResults', () => { - it('renders a text with a form', () => { - render(<NoResults />); - - expect(rtlScreen.getByText(/No results/i)).toBeInTheDocument(); - expect(rtlScreen.getByRole('searchbox')).toBeInTheDocument(); - }); -}); diff --git a/src/components/organisms/layout/no-results.tsx b/src/components/organisms/layout/no-results.tsx deleted file mode 100644 index f760616..0000000 --- a/src/components/organisms/layout/no-results.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useRouter } from 'next/router'; -import { type FC, useCallback } from 'react'; -import { useIntl } from 'react-intl'; -import { ROUTES } from '../../../utils/constants'; -import { SearchForm, type SearchFormSubmit } from '../forms'; - -/** - * NoResults component - * - * Renders a no results text with a search form. - */ -export const NoResults: FC = () => { - const intl = useIntl(); - const router = useRouter(); - const searchSubmitHandler: SearchFormSubmit = useCallback( - ({ query }) => { - if (!query) - return { - messages: { - error: intl.formatMessage({ - defaultMessage: 'Query must be longer than one character.', - description: 'NoResults: invalid query message', - id: 'VkfO7t', - }), - }, - validator: (value) => value.query.length > 1, - }; - - router.push({ pathname: ROUTES.SEARCH, query: { s: query } }); - - return undefined; - }, - [intl, router] - ); - - return ( - <> - <p> - {intl.formatMessage({ - defaultMessage: 'No results found.', - description: 'NoResults: no results', - id: '5O2vpy', - })} - </p> - <p> - {intl.formatMessage({ - defaultMessage: 'Would you like to try a new search?', - description: 'NoResults: try a new search message', - id: 'DVBwfu', - })} - </p> - <SearchForm isLabelHidden onSubmit={searchSubmitHandler} /> - </> - ); -}; diff --git a/src/components/organisms/layout/posts-list.fixture.tsx b/src/components/organisms/layout/posts-list.fixture.tsx deleted file mode 100644 index e1f7a95..0000000 --- a/src/components/organisms/layout/posts-list.fixture.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import NextImage from 'next/image'; -import type { PostData } from './posts-list'; - -export const introPost1 = - 'Esse et voluptas sapiente modi impedit unde et. Ducimus nulla ea impedit sit placeat nihil assumenda. Rem est fugiat amet quo hic. Corrupti fuga quod animi autem dolorem ullam corrupti vel aut.'; - -export const introPost2 = - 'Illum quae asperiores quod aut necessitatibus itaque excepturi voluptas. Incidunt exercitationem ullam saepe alias consequatur sed. Quam veniam quaerat voluptatum earum quia quisquam fugiat sed perspiciatis. Et velit saepe est recusandae facilis eos eum ipsum.'; - -export const introPost3 = - 'Sunt aperiam ut rem impedit dolor id sit. Reprehenderit ipsum iusto fugiat. Quaerat laboriosam magnam facilis. Totam sint aliquam voluptatem in quis laborum sunt eum. Enim aut debitis officiis porro iure quia nihil voluptas ipsum. Praesentium quis necessitatibus cumque quia qui velit quos dolorem.'; - -export const cover = { - alt: 'cover', - height: 480, - src: 'http://picsum.photos/640/480', - width: 640, -}; - -export const posts: PostData[] = [ - { - cover: <NextImage {...cover} />, - excerpt: introPost1, - id: 'post-1', - meta: { - publicationDate: '2022-02-26', - wordsCount: introPost1.split(' ').length, - thematics: [ - { id: 1, name: 'Cat 1', url: '#' }, - { id: 2, name: 'Cat 2', url: '#' }, - ], - comments: { count: 1, postHeading: 'Ratione velit fuga' }, - }, - heading: 'Ratione velit fuga', - url: '#', - }, - { - excerpt: introPost2, - id: 'post-2', - meta: { - publicationDate: '2022-02-20', - wordsCount: introPost2.split(' ').length, - thematics: [{ id: 2, name: 'Cat 2', url: '#' }], - comments: { count: 0, postHeading: 'Debitis laudantium laudantium' }, - }, - heading: 'Debitis laudantium laudantium', - url: '#', - }, - { - cover: <NextImage {...cover} />, - excerpt: introPost3, - id: 'post-3', - meta: { - publicationDate: '2021-12-20', - wordsCount: introPost3.split(' ').length, - thematics: [{ id: 1, name: 'Cat 1', url: '#' }], - comments: { count: 3, postHeading: 'Quaerat ut corporis' }, - }, - heading: 'Quaerat ut corporis', - url: '#', - }, -]; diff --git a/src/components/organisms/layout/posts-list.module.scss b/src/components/organisms/layout/posts-list.module.scss deleted file mode 100644 index cc5acda..0000000 --- a/src/components/organisms/layout/posts-list.module.scss +++ /dev/null @@ -1,66 +0,0 @@ -@use "../../../styles/abstracts/functions" as fun; -@use "../../../styles/abstracts/mixins" as mix; -@use "../../../styles/abstracts/placeholders"; - -.section { - &:not(:last-of-type) { - margin-bottom: var(--spacing-md); - } - - @include mix.media("screen") { - @include mix.dimensions("md") { - display: grid; - grid-template-columns: fun.convert-px(150) minmax(0, 1fr); - align-items: first baseline; - margin-left: fun.convert-px(-150); - } - } -} - -.list { - .item { - border-bottom: fun.convert-px(1) solid var(--color-border); - } -} - -.year { - margin-bottom: var(--spacing-md); - padding-bottom: fun.convert-px(3); - background: linear-gradient( - to top, - var(--color-primary-dark) 0.3rem, - transparent 0.3rem - ) - 0 0 / 3rem 100% no-repeat; - font-size: var(--font-size-2xl); - font-weight: 500; - text-shadow: fun.convert-px(1) fun.convert-px(1) 0 var(--color-shadow-light); - - @include mix.media("screen") { - @include mix.dimensions("md") { - grid-column: 1; - justify-self: end; - padding-right: var(--spacing-lg); - position: sticky; - top: var(--spacing-xs); - } - - @include mix.dimensions("lg") { - padding-right: var(--spacing-xl); - } - } -} - -.btn { - display: flex; - margin: auto; -} - -.progress { - margin-block: var(--spacing-md); -} - -.pagination { - margin-inline: auto; - margin-block-end: var(--spacing-lg); -} diff --git a/src/components/organisms/layout/posts-list.stories.tsx b/src/components/organisms/layout/posts-list.stories.tsx deleted file mode 100644 index b5af1d3..0000000 --- a/src/components/organisms/layout/posts-list.stories.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import { PostsList } from './posts-list'; -import { posts } from './posts-list.fixture'; - -/** - * PostsList - Storybook Meta - */ -export default { - title: 'Organisms/Layout/PostsList', - component: PostsList, - args: { - byYear: false, - isLoading: false, - pageNumber: 1, - showLoadMoreBtn: false, - siblings: 1, - titleLevel: 2, - }, - argTypes: { - baseUrl: { - control: { - type: 'text', - }, - description: 'The pagination base url.', - table: { - category: 'Options', - defaultValue: { summary: '/page/' }, - }, - type: { - name: 'string', - required: false, - }, - }, - byYear: { - control: { - type: 'boolean', - }, - description: 'True to display the posts by year.', - table: { - category: 'Options', - defaultValue: { summary: false }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - isLoading: { - control: { - type: 'boolean', - }, - description: 'Determine if the data is loading.', - table: { - category: 'Options', - defaultValue: { summary: false }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - loadMore: { - control: { - type: null, - }, - description: 'A function to load more posts on button click.', - table: { - category: 'Events', - }, - type: { - name: 'function', - required: false, - }, - }, - pageNumber: { - control: { - type: 'number', - }, - description: 'The current page number.', - table: { - category: 'Options', - defaultValue: { summary: 1 }, - }, - type: { - name: 'number', - required: false, - }, - }, - posts: { - description: 'The posts data.', - type: { - name: 'object', - required: true, - value: {}, - }, - }, - showLoadMoreBtn: { - control: { - type: 'boolean', - }, - description: 'Determine if the load more button should be visible.', - table: { - category: 'Options', - defaultValue: { summary: false }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - siblings: { - control: { - type: 'number', - }, - description: 'The number of page siblings inside pagination.', - table: { - category: 'Options', - defaultValue: { summary: 1 }, - }, - type: { - name: 'number', - required: false, - }, - }, - titleLevel: { - control: { - type: 'number', - min: 1, - max: 6, - }, - description: 'The title level (hn).', - table: { - category: 'Options', - defaultValue: { summary: 2 }, - }, - type: { - name: 'number', - required: false, - }, - }, - total: { - control: { - type: 'number', - }, - description: 'The number of posts.', - type: { - name: 'number', - required: true, - }, - }, - }, -} as ComponentMeta<typeof PostsList>; - -const Template: ComponentStory<typeof PostsList> = (args) => ( - <PostsList {...args} /> -); - -/** - * PostsList Stories - Default - */ -export const Default = Template.bind({}); -Default.args = { - posts, - total: posts.length, -}; - -/** - * PostsList Stories - By years - */ -export const ByYears = Template.bind({}); -ByYears.args = { - posts, - byYear: true, - total: posts.length, -}; -ByYears.decorators = [ - (Story) => ( - <div style={{ marginLeft: 150 }}> - <Story /> - </div> - ), -]; - -/** - * PostsList Stories - No results - */ -export const NoResults = Template.bind({}); -NoResults.args = { - posts: [], - total: posts.length, -}; diff --git a/src/components/organisms/layout/posts-list.test.tsx b/src/components/organisms/layout/posts-list.test.tsx deleted file mode 100644 index fabf31f..0000000 --- a/src/components/organisms/layout/posts-list.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen as rtlScreen } from '../../../../tests/utils'; -import { PostsList } from './posts-list'; -import { posts } from './posts-list.fixture'; - -describe('PostsList', () => { - it('renders the correct number of posts', () => { - render(<PostsList posts={posts} total={posts.length} />); - expect(rtlScreen.getAllByRole('article')).toHaveLength(posts.length); - }); - - it('renders the number of loaded posts', () => { - render(<PostsList posts={posts} total={posts.length} />); - const info = `${posts.length} loaded articles out of a total of ${posts.length}`; - expect(rtlScreen.getByText(info)).toBeInTheDocument(); - }); - - it('renders a load more button', () => { - render( - <PostsList posts={posts} total={posts.length} showLoadMoreBtn={true} /> - ); - expect( - rtlScreen.getByRole('button', { name: /Load more/i }) - ).toBeInTheDocument(); - }); - - it('renders a search form if no results', () => { - render(<PostsList posts={[]} total={0} showLoadMoreBtn={true} />); - expect(rtlScreen.getByRole('searchbox')).toBeInTheDocument(); - }); -}); diff --git a/src/components/organisms/layout/posts-list.tsx b/src/components/organisms/layout/posts-list.tsx deleted file mode 100644 index 40306a6..0000000 --- a/src/components/organisms/layout/posts-list.tsx +++ /dev/null @@ -1,327 +0,0 @@ -/* eslint-disable max-statements */ -import { type FC, Fragment, useRef, useCallback, useId } from 'react'; -import { useIntl } from 'react-intl'; -import { useIsMounted, useSettings } from '../../../utils/hooks'; -import { - Button, - Heading, - type HeadingLevel, - ProgressBar, - Spinner, - List, - ListItem, -} from '../../atoms'; -import { - Pagination, - type PaginationProps, - type RenderPaginationItemAriaLabel, - type RenderPaginationLink, -} from '../nav'; -import { - PostPreview, - type PostPreviewMetaData, - type PostPreviewProps, -} from '../post-preview'; -import { NoResults } from './no-results'; -import styles from './posts-list.module.scss'; - -export type PostData = Pick< - PostPreviewProps, - 'cover' | 'excerpt' | 'heading' | 'url' -> & { - /** - * The post id. - */ - id: string | number; - /** - * The post meta. - */ - meta: PostPreviewMetaData & - Required<Pick<PostPreviewMetaData, 'publicationDate'>>; -}; - -export type YearCollection = Record<string, PostData[]>; - -export type PostsListProps = Pick<PaginationProps, 'siblings'> & { - /** - * The pagination base url. - */ - baseUrl?: string; - /** - * True to display the posts by year. Default: false. - */ - byYear?: boolean; - /** - * Determine if the data is loading. - */ - isLoading?: boolean; - /** - * Load more button handler. - */ - loadMore?: () => void; - /** - * The current page number. Default: 1. - */ - pageNumber?: number; - /** - * The posts data. - */ - posts: PostData[]; - /** - * Determine if the load more button should be visible. - */ - showLoadMoreBtn?: boolean; - /** - * The posts heading level (hn). - */ - titleLevel?: HeadingLevel; - /** - * The total posts number. - */ - total: number; -}; - -/** - * Create a collection of posts sorted by year. - * - * @param {PostData[]} data - A collection of posts. - * @returns {YearCollection} The posts sorted by year. - */ -const sortPostsByYear = (data: PostData[]): YearCollection => { - const yearCollection: Partial<YearCollection> = {}; - - data.forEach((post) => { - const postYear = new Date(post.meta.publicationDate) - .getFullYear() - .toString(); - yearCollection[postYear] = [...(yearCollection[postYear] ?? []), post]; - }); - - return yearCollection as YearCollection; -}; - -/** - * PostsList component - * - * Render a list of post summaries. - */ -export const PostsList: FC<PostsListProps> = ({ - baseUrl = '', - byYear = false, - isLoading = false, - loadMore, - pageNumber = 1, - posts, - showLoadMoreBtn = false, - siblings, - titleLevel, - total, -}) => { - const intl = useIntl(); - const listRef = useRef<HTMLOListElement>(null); - const lastPostRef = useRef<HTMLSpanElement>(null); - const isMounted = useIsMounted(listRef); - const { blog } = useSettings(); - const lastPostId = posts.length ? posts[posts.length - 1].id : 0; - const progressBarId = useId(); - - /** - * Retrieve the list of posts. - * - * @param {PostData[]} allPosts - A collection fo posts. - * @param {HeadingLevel} [headingLevel] - The posts heading level (hn). - * @returns {JSX.Element} The list of posts. - */ - const getList = ( - allPosts: PostData[], - headingLevel: HeadingLevel = 2 - ): JSX.Element => ( - <List - aria-busy={isLoading} - aria-describedby={progressBarId} - className={styles.list} - hideMarker - isOrdered - ref={listRef} - spacing="md" - > - {allPosts.map(({ id, ...post }) => ( - <Fragment key={id}> - <ListItem className={styles.item}> - <PostPreview {...post} headingLvl={headingLevel} /> - </ListItem> - {id === lastPostId && ( - <ListItem> - <span ref={lastPostRef} tabIndex={-1} /> - </ListItem> - )} - </Fragment> - ))} - </List> - ); - - /** - * Retrieve the list of posts. - * - * @returns {JSX.Element | JSX.Element[]} The posts list. - */ - const getPosts = (): JSX.Element | JSX.Element[] => { - const firstLevel = titleLevel ?? 2; - if (!byYear) return getList(posts, firstLevel); - - const postsPerYear = sortPostsByYear(posts); - const years = Object.keys(postsPerYear).reverse(); - const nextLevel = (firstLevel + 1) as HeadingLevel; - - return years.map((year) => ( - <section key={year} className={styles.section}> - <Heading level={firstLevel} className={styles.year}> - {year} - </Heading> - {getList(postsPerYear[year], nextLevel)} - </section> - )); - }; - - const loadedPostsCount = - pageNumber === 1 - ? posts.length - : pageNumber * blog.postsPerPage + posts.length; - const progressInfo = intl.formatMessage( - { - defaultMessage: - '{articlesCount, plural, =0 {# loaded articles} one {# loaded article} other {# loaded articles}} out of a total of {total}', - description: 'PostsList: loaded articles progress', - id: '9MeLN3', - }, - { - articlesCount: loadedPostsCount, - total, - } - ); - - const loadMoreBody = intl.formatMessage({ - defaultMessage: 'Load more articles?', - description: 'PostsList: load more button', - id: 'uaqd5F', - }); - - const loadingMoreArticles = intl.formatMessage({ - defaultMessage: 'Loading more articles...', - description: 'PostsList: loading more articles message', - id: 'xYemkP', - }); - - /** - * Load more posts handler. - */ - const loadMorePosts = useCallback(() => { - if (lastPostRef.current) { - lastPostRef.current.focus(); - } - - if (loadMore) loadMore(); - }, [loadMore]); - - const getProgressBar = () => ( - <> - <ProgressBar - aria-label={progressInfo} - className={styles.progress} - current={loadedPostsCount} - id={progressBarId} - isCentered - isLoading={isLoading} - label={progressInfo} - max={total} - /> - {showLoadMoreBtn ? ( - <Button - className={styles.btn} - isDisabled={isLoading} - // eslint-disable-next-line react/jsx-no-literals -- Kind allowed. - kind="tertiary" - onClick={loadMorePosts} - > - {loadMoreBody} - </Button> - ) : null} - </> - ); - - const paginationAriaLabel = intl.formatMessage({ - defaultMessage: 'Pagination', - description: 'PostsList: pagination accessible name', - id: 'k1aA+G', - }); - - const renderItemAriaLabel: RenderPaginationItemAriaLabel = useCallback( - ({ kind, pageNumber: page, isCurrentPage }) => { - switch (kind) { - case 'backward': - return intl.formatMessage({ - defaultMessage: 'Go to previous page', - description: 'PostsList: pagination backward link label', - id: 'PHO94k', - }); - case 'forward': - return intl.formatMessage({ - defaultMessage: 'Go to next page', - description: 'PostsList: pagination forward link label', - id: 'HaKhih', - }); - case 'number': - default: - return isCurrentPage - ? intl.formatMessage( - { - defaultMessage: 'Current page, page {number}', - description: 'PostsList: pagination current page label', - id: 'nwDGkZ', - }, - { number: page } - ) - : intl.formatMessage( - { - defaultMessage: 'Go to page {number}', - description: 'PostsList: pagination page link label', - id: 'AmHSC4', - }, - { number: page } - ); - } - }, - [intl] - ); - - const renderLink: RenderPaginationLink = useCallback( - (page) => `${baseUrl}${page}`, - [baseUrl] - ); - - const getPagination = () => { - if (total < blog.postsPerPage) return null; - - return ( - <Pagination - aria-label={paginationAriaLabel} - className={styles.pagination} - current={pageNumber} - renderItemAriaLabel={renderItemAriaLabel} - renderLink={renderLink} - siblings={siblings} - total={Math.round(total / blog.postsPerPage)} - /> - ); - }; - - if (posts.length === 0) return <NoResults />; - - return ( - <> - {getPosts()} - {isLoading ? <Spinner>{loadingMoreArticles}</Spinner> : null} - {isMounted ? getProgressBar() : getPagination()} - </> - ); -}; |
