summaryrefslogtreecommitdiffstats
path: root/src/components/organisms/layout/posts-list.tsx
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2022-05-24 19:35:12 +0200
committerGitHub <noreply@github.com>2022-05-24 19:35:12 +0200
commitc85ab5ad43ccf52881ee224672c41ec30021cf48 (patch)
tree8058808d9bfca19383f120c46b34d99ff2f89f63 /src/components/organisms/layout/posts-list.tsx
parent52404177c07a2aab7fc894362fb3060dff2431a0 (diff)
parent11b9de44a4b2f305a6a484187805e429b2767118 (diff)
refactor: use storybook and atomic design (#16)
BREAKING CHANGE: rewrite most of the Typescript types, so the content format (the meta in particular) needs to be updated.
Diffstat (limited to 'src/components/organisms/layout/posts-list.tsx')
-rw-r--r--src/components/organisms/layout/posts-list.tsx239
1 files changed, 239 insertions, 0 deletions
diff --git a/src/components/organisms/layout/posts-list.tsx b/src/components/organisms/layout/posts-list.tsx
new file mode 100644
index 0000000..24869fd
--- /dev/null
+++ b/src/components/organisms/layout/posts-list.tsx
@@ -0,0 +1,239 @@
+import Button from '@components/atoms/buttons/button';
+import Heading, { type HeadingLevel } from '@components/atoms/headings/heading';
+import ProgressBar from '@components/atoms/loaders/progress-bar';
+import Spinner from '@components/atoms/loaders/spinner';
+import Pagination, {
+ type PaginationProps,
+} from '@components/molecules/nav/pagination';
+import useIsMounted from '@utils/hooks/use-is-mounted';
+import useSettings from '@utils/hooks/use-settings';
+import { FC, Fragment, useRef } from 'react';
+import { useIntl } from 'react-intl';
+import NoResults, { NoResultsProps } from './no-results';
+import styles from './posts-list.module.scss';
+import Summary, { type SummaryProps } from './summary';
+
+export type Post = Omit<SummaryProps, 'titleLevel'> & {
+ /**
+ * The post id.
+ */
+ id: string | number;
+};
+
+export type YearCollection = {
+ [key: string]: Post[];
+};
+
+export type PostsListProps = Pick<PaginationProps, 'baseUrl' | 'siblings'> &
+ Pick<NoResultsProps, 'searchPage'> & {
+ /**
+ * 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: Post[];
+ /**
+ * 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 {Posts[]} data - A collection of posts.
+ * @returns {YearCollection} The posts sorted by year.
+ */
+const sortPostsByYear = (data: Post[]): YearCollection => {
+ const yearCollection: YearCollection = {};
+
+ data.forEach((post) => {
+ const postYear = new Date(post.meta.dates.publication)
+ .getFullYear()
+ .toString();
+ yearCollection[postYear] = [...(yearCollection[postYear] || []), post];
+ });
+
+ return yearCollection;
+};
+
+/**
+ * PostsList component
+ *
+ * Render a list of post summaries.
+ */
+const PostsList: FC<PostsListProps> = ({
+ baseUrl,
+ byYear = false,
+ isLoading = false,
+ loadMore,
+ pageNumber = 1,
+ posts,
+ searchPage,
+ 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;
+
+ /**
+ * Retrieve the list of 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 = (
+ allPosts: Post[],
+ headingLevel: HeadingLevel = 2
+ ): JSX.Element => {
+ return (
+ <ol className={styles.list} ref={listRef}>
+ {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>
+ );
+ };
+
+ /**
+ * 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) => {
+ return (
+ <section key={year} className={styles.section}>
+ <Heading level={firstLevel} className={styles.year}>
+ {year}
+ </Heading>
+ {getList(postsPerYear[year], nextLevel)}
+ </section>
+ );
+ });
+ };
+
+ 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: total }
+ );
+
+ 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();
+ };
+
+ const getProgressBar = () => {
+ return (
+ <>
+ <ProgressBar
+ min={1}
+ max={total}
+ current={posts.length}
+ info={progressInfo}
+ />
+ {showLoadMoreBtn && (
+ <Button
+ kind="tertiary"
+ onClick={loadMorePosts}
+ disabled={isLoading}
+ className={styles.btn}
+ >
+ {loadMoreBody}
+ </Button>
+ )}
+ </>
+ );
+ };
+
+ const getPagination = () => {
+ return posts.length <= blog.postsPerPage ? (
+ <Pagination
+ baseUrl={baseUrl}
+ current={pageNumber}
+ perPage={blog.postsPerPage}
+ siblings={siblings}
+ total={total}
+ />
+ ) : (
+ <></>
+ );
+ };
+
+ if (posts.length === 0) {
+ return <NoResults searchPage={searchPage} />;
+ }
+
+ return (
+ <>
+ {getPosts()}
+ {isLoading && <Spinner />}
+ {isMounted ? getProgressBar() : getPagination()}
+ </>
+ );
+};
+
+export default PostsList;