aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/organisms/posts-list/posts-list.tsx
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-11-13 17:45:59 +0100
committerArmand Philippot <git@armandphilippot.com>2023-11-13 17:45:59 +0100
commit56878f647ea0f1066fa3e222d7aa0d83057f496d (patch)
tree26f673a062741414bfa7db5d37990936ce115f49 /src/components/organisms/posts-list/posts-list.tsx
parent599b70cd2390d08ce26ee44174b3f39c6587110c (diff)
refactor(components): rewrite PostsList component
* remove NoResults component and move logic to Search page * add a usePostsList hook * remove Pagination from PostsList (it is only used if javascript is disabled and not on every posts list) * replace `byYear` prop with `sortByYear` * replace `loadMore` prop with `onLoadMore` * remove `showLoadMoreBtn` (we can use `loadMore` prop instead to determine if we need to display the button) * replace `titleLevel` prop with `headingLvl` * add `firstNewResult` prop to handle focus on the new results when loading more article (we should not focus a useless span but the item directly)
Diffstat (limited to 'src/components/organisms/posts-list/posts-list.tsx')
-rw-r--r--src/components/organisms/posts-list/posts-list.tsx236
1 files changed, 236 insertions, 0 deletions
diff --git a/src/components/organisms/posts-list/posts-list.tsx b/src/components/organisms/posts-list/posts-list.tsx
new file mode 100644
index 0000000..783bc4e
--- /dev/null
+++ b/src/components/organisms/posts-list/posts-list.tsx
@@ -0,0 +1,236 @@
+import {
+ type ForwardRefRenderFunction,
+ type HTMLAttributes,
+ forwardRef,
+ useCallback,
+ useRef,
+ type RefCallback,
+} from 'react';
+import { useIntl } from 'react-intl';
+import { mergeRefs } from '../../../utils/helpers';
+import {
+ Heading,
+ type HeadingLevel,
+ List,
+ ListItem,
+ Button,
+ ProgressBar,
+} from '../../atoms';
+import {
+ PostPreview,
+ type PostPreviewMetaData,
+ type PostPreviewProps,
+} from '../post-preview';
+import styles from './posts-list.module.scss';
+
+const MAX_HEADING_LVL = 6;
+
+export type PostData = Pick<
+ PostPreviewProps,
+ 'cover' | 'excerpt' | 'heading' | 'url'
+> & {
+ /**
+ * The post id.
+ */
+ id: string | number;
+ /**
+ * The post meta.
+ */
+ meta: PostPreviewMetaData &
+ Required<Pick<PostPreviewMetaData, 'publicationDate'>>;
+};
+
+const getPostsByYear = (posts: PostData[]) => {
+ const yearCollection = new Map<string, PostData[]>();
+
+ for (const post of posts) {
+ const currentPostYear = new Date(post.meta.publicationDate)
+ .getFullYear()
+ .toString();
+
+ const yearPosts = yearCollection.get(currentPostYear) ?? [];
+
+ yearCollection.set(currentPostYear, [...yearPosts, post]);
+ }
+
+ return yearCollection;
+};
+
+type GetPostsListOptions = {
+ headingLvl: HeadingLevel;
+ isOrdered?: boolean;
+};
+
+export type PostsListProps = Omit<
+ HTMLAttributes<HTMLDivElement>,
+ 'children'
+> & {
+ /**
+ * The first new result index. It will be use to make the load more button
+ * accessible for keyboard users.
+ */
+ firstNewResult?: number;
+ /**
+ * The heading level to use on posts titles.
+ *
+ * @default 2
+ */
+ headingLvl?: HeadingLevel;
+ /**
+ * Should we indicate that new posts are loading?
+ *
+ * @default false
+ */
+ isLoading?: boolean;
+ /**
+ * A callback function to handle loading more posts.
+ */
+ onLoadMore?: () => void;
+ /**
+ * The posts.
+ */
+ posts: PostData[];
+ /**
+ * Should we use a different section by year?
+ */
+ sortByYear?: boolean;
+ /**
+ * The total posts number.
+ */
+ total?: number;
+};
+
+const PostsListWithRef: ForwardRefRenderFunction<
+ HTMLDivElement,
+ PostsListProps
+> = (
+ {
+ firstNewResult,
+ headingLvl = 2,
+ isLoading = false,
+ onLoadMore,
+ posts,
+ sortByYear = false,
+ total,
+ ...props
+ },
+ ref
+) => {
+ const wrapperRef = useRef<HTMLDivElement | null>(null);
+ const firstNewResultRef: RefCallback<HTMLLIElement> = useCallback((el) => {
+ el?.focus();
+ }, []);
+ const intl = useIntl();
+ 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: posts.length,
+ total,
+ }
+ );
+ const loadMoreBtn = intl.formatMessage({
+ defaultMessage: 'Load more posts?',
+ description: 'PostsList: load more button',
+ id: 'hGvQpI',
+ });
+
+ const getPostsList = useCallback(
+ (
+ data: PostData[],
+ { headingLvl: lvl, isOrdered }: GetPostsListOptions,
+ indexAcc = 0
+ ) => (
+ <List
+ hideMarker
+ isOrdered={isOrdered}
+ // eslint-disable-next-line react/jsx-no-literals
+ spacing="md"
+ >
+ {data.map(({ id, ...post }, index) => {
+ const isFirstNewResult = firstNewResult === indexAcc + index;
+
+ return (
+ <ListItem
+ key={id}
+ ref={isFirstNewResult ? firstNewResultRef : undefined}
+ tabIndex={isFirstNewResult ? -1 : undefined}
+ >
+ <PostPreview {...post} headingLvl={lvl} />
+ </ListItem>
+ );
+ })}
+ </List>
+ ),
+ [firstNewResult, firstNewResultRef]
+ );
+
+ const getSortedPostsList = useCallback(
+ (data: PostData[]) => {
+ const postsByYear = Array.from(getPostsByYear(data));
+ const postsLvl =
+ headingLvl < MAX_HEADING_LVL
+ ? ((headingLvl + 1) as HeadingLevel)
+ : headingLvl;
+ let indexAcc = 0;
+
+ return postsByYear.map(([year, sortedPosts], index) => {
+ indexAcc +=
+ index > 0 ? postsByYear[index - 1][1].length : sortedPosts.length;
+
+ return (
+ <section className={styles.section} key={year}>
+ <Heading className={styles.year} level={headingLvl}>
+ {year}
+ </Heading>
+ {getPostsList(
+ sortedPosts,
+ {
+ headingLvl: postsLvl,
+ isOrdered: true,
+ },
+ indexAcc
+ )}
+ </section>
+ );
+ });
+ },
+ [getPostsList, headingLvl]
+ );
+
+ return (
+ <div {...props} ref={mergeRefs([wrapperRef, ref])}>
+ {sortByYear
+ ? getSortedPostsList(posts)
+ : getPostsList(posts, { headingLvl })}
+ {total ? (
+ <ProgressBar
+ aria-label={progressInfo}
+ className={styles.progress}
+ current={posts.length}
+ isCentered
+ isLoading={isLoading}
+ label={progressInfo}
+ max={total}
+ />
+ ) : null}
+ {onLoadMore ? (
+ <Button
+ className={styles.btn}
+ isLoading={isLoading}
+ // eslint-disable-next-line react/jsx-no-literals
+ kind="tertiary"
+ onClick={onLoadMore}
+ >
+ {loadMoreBtn}
+ </Button>
+ ) : null}
+ </div>
+ );
+};
+
+export const PostsList = forwardRef(PostsListWithRef);