summaryrefslogtreecommitdiffstats
path: root/src/components/organisms/layout
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-05-13 15:39:55 +0200
committerArmand Philippot <git@armandphilippot.com>2022-05-13 15:46:05 +0200
commitdab72bb270ee2ee47a0b472d5e9e240cba7cbf0f (patch)
treea64a49a1048eeab1204a9b04923135edd1f259e1 /src/components/organisms/layout
parentc5b516e2c933e77b2550fe6becebacb3fbdd30eb (diff)
chore: handle blog pagination
Diffstat (limited to 'src/components/organisms/layout')
-rw-r--r--src/components/organisms/layout/posts-list.stories.tsx44
-rw-r--r--src/components/organisms/layout/posts-list.test.tsx15
-rw-r--r--src/components/organisms/layout/posts-list.tsx73
-rw-r--r--src/components/organisms/layout/summary.module.scss34
-rw-r--r--src/components/organisms/layout/summary.tsx11
5 files changed, 151 insertions, 26 deletions
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>
);