diff options
| author | Armand Philippot <git@armandphilippot.com> | 2022-05-13 15:39:55 +0200 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2022-05-13 15:46:05 +0200 |
| commit | dab72bb270ee2ee47a0b472d5e9e240cba7cbf0f (patch) | |
| tree | a64a49a1048eeab1204a9b04923135edd1f259e1 /src/components | |
| parent | c5b516e2c933e77b2550fe6becebacb3fbdd30eb (diff) | |
chore: handle blog pagination
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/atoms/loaders/progress-bar.module.scss | 2 | ||||
| -rw-r--r-- | src/components/organisms/layout/posts-list.stories.tsx | 44 | ||||
| -rw-r--r-- | src/components/organisms/layout/posts-list.test.tsx | 15 | ||||
| -rw-r--r-- | src/components/organisms/layout/posts-list.tsx | 73 | ||||
| -rw-r--r-- | src/components/organisms/layout/summary.module.scss | 34 | ||||
| -rw-r--r-- | src/components/organisms/layout/summary.tsx | 11 |
6 files changed, 152 insertions, 27 deletions
diff --git a/src/components/atoms/loaders/progress-bar.module.scss b/src/components/atoms/loaders/progress-bar.module.scss index 166b7c4..878010a 100644 --- a/src/components/atoms/loaders/progress-bar.module.scss +++ b/src/components/atoms/loaders/progress-bar.module.scss @@ -1,7 +1,6 @@ @use "@styles/abstracts/functions" as fun; .progress { - width: max-content; margin: var(--spacing-sm) auto var(--spacing-md); text-align: center; @@ -15,6 +14,7 @@ width: clamp(25ch, 20vw, 30ch); max-width: 100%; height: fun.convert-px(13); + margin: auto; appearance: none; background: var(--color-bg-tertiary); border: fun.convert-px(1) solid var(--color-primary-darker); diff --git a/src/components/organisms/layout/posts-list.stories.tsx b/src/components/organisms/layout/posts-list.stories.tsx index de0478f..77318f4 100644 --- a/src/components/organisms/layout/posts-list.stories.tsx +++ b/src/components/organisms/layout/posts-list.stories.tsx @@ -9,6 +9,9 @@ export default { component: PostsList, args: { byYear: false, + isLoading: false, + showLoadMoreBtn: false, + titleLevel: 2, }, argTypes: { byYear: { @@ -25,6 +28,33 @@ export default { 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, + }, + }, posts: { description: 'The posts data.', type: { @@ -33,6 +63,20 @@ export default { 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, + }, + }, titleLevel: { control: { type: 'number', diff --git a/src/components/organisms/layout/posts-list.test.tsx b/src/components/organisms/layout/posts-list.test.tsx index 9b226ac..7429cbd 100644 --- a/src/components/organisms/layout/posts-list.test.tsx +++ b/src/components/organisms/layout/posts-list.test.tsx @@ -71,4 +71,19 @@ describe('PostsList', () => { render(<PostsList posts={posts} total={posts.length} />); expect(screen.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(screen.getByText(info)).toBeInTheDocument(); + }); + + it('renders a load more button', () => { + render( + <PostsList posts={posts} total={posts.length} showLoadMoreBtn={true} /> + ); + expect( + screen.getByRole('button', { name: /Load more/i }) + ).toBeInTheDocument(); + }); }); diff --git a/src/components/organisms/layout/posts-list.tsx b/src/components/organisms/layout/posts-list.tsx index daf4491..4d77d20 100644 --- a/src/components/organisms/layout/posts-list.tsx +++ b/src/components/organisms/layout/posts-list.tsx @@ -1,10 +1,11 @@ +import Button from '@components/atoms/buttons/button'; import Heading, { type HeadingLevel } from '@components/atoms/headings/heading'; -import { FC } from 'react'; +import ProgressBar from '@components/atoms/loaders/progress-bar'; +import Spinner from '@components/atoms/loaders/spinner'; +import { FC, Fragment, useRef } from 'react'; import { useIntl } from 'react-intl'; -import Summary, { type SummaryProps } from './summary'; import styles from './posts-list.module.scss'; -import ProgressBar from '@components/atoms/loaders/progress-bar'; -import Button from '@components/atoms/buttons/button'; +import Summary, { type SummaryProps } from './summary'; export type Post = SummaryProps & { /** @@ -23,10 +24,22 @@ export type PostsListProps = { */ byYear?: boolean; /** + * Determine if the data is loading. + */ + isLoading?: boolean; + /** + * Load more button handler. + */ + loadMore?: () => void; + /** * The posts data. */ posts: Post[]; /** + * Determine if the load more button should be visible. + */ + showLoadMoreBtn?: boolean; + /** * The posts heading level (hn). */ titleLevel?: HeadingLevel; @@ -62,29 +75,42 @@ const sortPostsByYear = (data: Post[]): YearCollection => { */ const PostsList: FC<PostsListProps> = ({ byYear = false, + isLoading = false, + loadMore, posts, + showLoadMoreBtn = false, titleLevel, total, }) => { const intl = useIntl(); + const lastPostRef = useRef<HTMLSpanElement>(null); /** * Retrieve the list of posts. * - * @param {Posts[]} data - A collection fo posts. + * @param {Posts[]} allPosts - A collection fo posts. * @param {HeadingLevel} [headingLevel] - The posts heading level (hn). * @returns {JSX.Element} The list of posts. */ const getList = ( - data: Post[], + allPosts: Post[], headingLevel: HeadingLevel = 2 ): JSX.Element => { + const lastPostId = allPosts[allPosts.length - 1].id; + return ( <ol className={styles.list}> - {data.map(({ id, ...post }) => ( - <li key={id} className={styles.item}> - <Summary {...post} titleLevel={headingLevel} /> - </li> + {allPosts.map(({ id, ...post }) => ( + <Fragment key={id}> + <li className={styles.item}> + <Summary {...post} titleLevel={headingLevel} /> + </li> + {id === lastPostId && ( + <li> + <span ref={lastPostRef} tabIndex={-1} /> + </li> + )} + </Fragment> ))} </ol> ); @@ -93,7 +119,7 @@ const PostsList: FC<PostsListProps> = ({ /** * Retrieve the list of posts. * - * @returns {JSX.Element | JSX.Element[]} - The posts list. + * @returns {JSX.Element | JSX.Element[]} The posts list. */ const getPosts = (): JSX.Element | JSX.Element[] => { if (!byYear) return getList(posts); @@ -123,12 +149,23 @@ const PostsList: FC<PostsListProps> = ({ { articlesCount: posts.length, total: total } ); - const loadMore = intl.formatMessage({ + const loadMoreBody = intl.formatMessage({ defaultMessage: 'Load more articles?', id: 'uaqd5F', description: 'PostsList: load more button', }); + /** + * Load more posts handler. + */ + const loadMorePosts = () => { + if (lastPostRef.current) { + lastPostRef.current.focus(); + } + + loadMore && loadMore(); + }; + return posts.length === 0 ? ( <p> {intl.formatMessage({ @@ -140,13 +177,23 @@ const PostsList: FC<PostsListProps> = ({ ) : ( <> {getPosts()} + {isLoading && <Spinner />} <ProgressBar min={1} max={total} current={posts.length} info={progressInfo} /> - <Button className={styles.btn}>{loadMore}</Button> + {showLoadMoreBtn && ( + <Button + kind="tertiary" + onClick={loadMorePosts} + disabled={isLoading} + className={styles.btn} + > + {loadMoreBody} + </Button> + )} </> ); }; diff --git a/src/components/organisms/layout/summary.module.scss b/src/components/organisms/layout/summary.module.scss index 6d19853..5f22fbb 100644 --- a/src/components/organisms/layout/summary.module.scss +++ b/src/components/organisms/layout/summary.module.scss @@ -2,6 +2,10 @@ @use "@styles/abstracts/mixins" as mix; .wrapper { + display: grid; + grid-template-columns: minmax(0, 1fr); + column-gap: var(--spacing-md); + row-gap: var(--spacing-sm); padding: var(--spacing-2xs) 0 var(--spacing-lg); @include mix.media("screen") { @@ -18,19 +22,26 @@ } @include mix.dimensions("sm") { - display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 1fr); grid-template-rows: repeat(3, max-content); - column-gap: var(--spacing-md); + } + } + + &:hover { + .icon { + transform: scaleX(1.4); + transform-origin: left; } } } .cover { + display: inline-flex; + flex-flow: column nowrap; + justify-content: center; width: auto; - max-height: fun.convert-px(100); + height: fun.convert-px(100); max-width: 100%; - margin-bottom: var(--spacing-sm); border: fun.convert-px(1) solid var(--color-border); @include mix.media("screen") { @@ -70,7 +81,9 @@ } .title { + margin: 0; background: none; + color: inherit; text-shadow: none; } @@ -79,18 +92,17 @@ flex-flow: row nowrap; column-gap: var(--spacing-xs); width: max-content; - margin: var(--spacing-sm) 0; + margin: var(--spacing-sm) 0 0; } .meta { - display: grid; - grid-template-columns: repeat( - auto-fit, - min(100vw, calc(50% - var(--spacing-lg))) - ); - margin-top: var(--spacing-lg); + flex-flow: row wrap; font-size: var(--font-size-sm); + &__item { + flex: 1 0 min(calc(100vw - 2 * var(--spacing-md)), 14ch); + } + @include mix.media("screen") { @include mix.dimensions("sm") { display: flex; diff --git a/src/components/organisms/layout/summary.tsx b/src/components/organisms/layout/summary.tsx index 1c4a38b..078f9ee 100644 --- a/src/components/organisms/layout/summary.tsx +++ b/src/components/organisms/layout/summary.tsx @@ -141,12 +141,19 @@ const Summary: FC<SummaryProps> = ({ <ButtonLink target={url} className={styles['read-more']}> <> {readMore} - <Arrow direction="right" /> + <Arrow direction="right" className={styles.icon} /> </> </ButtonLink> </div> <footer className={styles.footer}> - <Meta data={getMeta(meta)} layout="column" className={styles.meta} /> + <Meta + data={getMeta(meta)} + layout="column" + itemsLayout="stacked" + withSeparator={false} + className={styles.meta} + groupClassName={styles.meta__item} + /> </footer> </article> ); |
