aboutsummaryrefslogtreecommitdiffstats
path: root/src/components/organisms/layout
diff options
context:
space:
mode:
authorArmand Philippot <git@armandphilippot.com>2023-09-26 18:43:11 +0200
committerArmand Philippot <git@armandphilippot.com>2023-10-24 12:25:00 +0200
commit388e687857345c85ee550cd5da472675e05a6ff5 (patch)
tree0f035a3cad57a75959c028949a57227a83d480e2 /src/components/organisms/layout
parent70efcfeaa0603415dd992cb662d8efb960e6e49a (diff)
refactor(components): rewrite Button and ButtonLink components
Both: * move styles to Sass placeholders Button: * add `isPressed` prop to Button * add `isLoading` prop to Button (to differentiate state from disabled) ButtonLink: * replace `external` prop with `isExternal` prop * replace `href` prop with `to` prop
Diffstat (limited to 'src/components/organisms/layout')
-rw-r--r--src/components/organisms/layout/footer.tsx16
-rw-r--r--src/components/organisms/layout/posts-list.tsx134
-rw-r--r--src/components/organisms/layout/summary.tsx16
3 files changed, 82 insertions, 84 deletions
diff --git a/src/components/organisms/layout/footer.tsx b/src/components/organisms/layout/footer.tsx
index f1f3236..36e85a7 100644
--- a/src/components/organisms/layout/footer.tsx
+++ b/src/components/organisms/layout/footer.tsx
@@ -1,4 +1,4 @@
-import { FC } from 'react';
+import type { FC } from 'react';
import { useIntl } from 'react-intl';
import { Copyright, type CopyrightProps } from '../../atoms';
import {
@@ -50,26 +50,26 @@ export const Footer: FC<FooterProps> = ({
description: 'Footer: an accessible name for footer nav',
id: 'd4N8nD',
});
+ const footerClass = `${styles.wrapper} ${className}`;
+ const btnClass = `${styles['back-to-top']} ${backToTopClassName}`;
return (
- <footer className={`${styles.wrapper} ${className}`}>
+ <footer className={footerClass}>
<Copyright
dates={copyright.dates}
icon={copyright.icon}
owner={copyright.owner}
/>
- {navItems && (
+ {navItems ? (
<Nav
aria-label={ariaLabel}
className={styles.nav}
items={navItems}
+ // eslint-disable-next-line react/jsx-no-literals -- Hardcoded config
kind="footer"
/>
- )}
- <BackToTop
- className={`${styles['back-to-top']} ${backToTopClassName}`}
- target={topId}
- />
+ ) : null}
+ <BackToTop className={btnClass} to={topId} />
</footer>
);
};
diff --git a/src/components/organisms/layout/posts-list.tsx b/src/components/organisms/layout/posts-list.tsx
index e214ca7..f04ba74 100644
--- a/src/components/organisms/layout/posts-list.tsx
+++ b/src/components/organisms/layout/posts-list.tsx
@@ -1,4 +1,5 @@
-import { FC, Fragment, useRef } from 'react';
+/* eslint-disable max-statements */
+import { type FC, Fragment, useRef, useCallback } from 'react';
import { useIntl } from 'react-intl';
import { useIsMounted, useSettings } from '../../../utils/hooks';
import {
@@ -20,9 +21,7 @@ export type Post = Omit<SummaryProps, 'titleLevel'> & {
id: string | number;
};
-export type YearCollection = {
- [key: string]: Post[];
-};
+export type YearCollection = Record<string, Post[]>;
export type PostsListProps = Pick<PaginationProps, 'baseUrl' | 'siblings'> &
Pick<NoResultsProps, 'searchPage'> & {
@@ -67,16 +66,16 @@ export type PostsListProps = Pick<PaginationProps, 'baseUrl' | 'siblings'> &
* @returns {YearCollection} The posts sorted by year.
*/
const sortPostsByYear = (data: Post[]): YearCollection => {
- const yearCollection: YearCollection = {};
+ const yearCollection: Partial<YearCollection> = {};
data.forEach((post) => {
const postYear = new Date(post.meta.dates.publication)
.getFullYear()
.toString();
- yearCollection[postYear] = [...(yearCollection[postYear] || []), post];
+ yearCollection[postYear] = [...(yearCollection[postYear] ?? []), post];
});
- return yearCollection;
+ return yearCollection as YearCollection;
};
/**
@@ -102,7 +101,6 @@ export const PostsList: FC<PostsListProps> = ({
const lastPostRef = useRef<HTMLSpanElement>(null);
const isMounted = useIsMounted(listRef);
const { blog } = useSettings();
-
const lastPostId = posts.length ? posts[posts.length - 1].id : 0;
/**
@@ -115,24 +113,22 @@ export const PostsList: FC<PostsListProps> = ({
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} />
+ ): JSX.Element => (
+ <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>
- {id === lastPostId && (
- <li>
- <span ref={lastPostRef} tabIndex={-1} />
- </li>
- )}
- </Fragment>
- ))}
- </ol>
- );
- };
+ )}
+ </Fragment>
+ ))}
+ </ol>
+ );
/**
* Retrieve the list of posts.
@@ -140,23 +136,21 @@ export const PostsList: FC<PostsListProps> = ({
* @returns {JSX.Element | JSX.Element[]} The posts list.
*/
const getPosts = (): JSX.Element | JSX.Element[] => {
- const firstLevel = titleLevel || 2;
+ 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>
- );
- });
+ return years.map((year) => (
+ <section key={year} className={styles.section}>
+ <Heading level={firstLevel} className={styles.year}>
+ {year}
+ </Heading>
+ {getList(postsPerYear[year], nextLevel)}
+ </section>
+ ));
};
const progressInfo = intl.formatMessage(
@@ -166,7 +160,7 @@ export const PostsList: FC<PostsListProps> = ({
description: 'PostsList: loaded articles progress',
id: '9MeLN3',
},
- { articlesCount: posts.length, total: total }
+ { articlesCount: posts.length, total }
);
const loadMoreBody = intl.formatMessage({
@@ -178,41 +172,43 @@ export const PostsList: FC<PostsListProps> = ({
/**
* Load more posts handler.
*/
- const loadMorePosts = () => {
+ const loadMorePosts = useCallback(() => {
if (lastPostRef.current) {
lastPostRef.current.focus();
}
- loadMore && loadMore();
- };
+ if (loadMore) loadMore();
+ }, [loadMore]);
- const getProgressBar = () => {
- return (
- <>
- <ProgressBar
- aria-label={progressInfo}
- current={posts.length}
- id="loaded-posts"
- label={progressInfo}
- min={1}
- max={total}
- />
- {showLoadMoreBtn && (
- <Button
- kind="tertiary"
- onClick={loadMorePosts}
- disabled={isLoading}
- className={styles.btn}
- >
- {loadMoreBody}
- </Button>
- )}
- </>
- );
- };
+ const getProgressBar = () => (
+ <>
+ <ProgressBar
+ aria-label={progressInfo}
+ current={posts.length}
+ // eslint-disable-next-line react/jsx-no-literals -- Id allowed.
+ id="loaded-posts"
+ label={progressInfo}
+ min={1}
+ 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 getPagination = () => {
- return posts.length <= blog.postsPerPage ? (
+ if (posts.length < blog.postsPerPage) return null;
+
+ return (
<Pagination
baseUrl={baseUrl}
current={pageNumber}
@@ -220,19 +216,15 @@ export const PostsList: FC<PostsListProps> = ({
siblings={siblings}
total={total}
/>
- ) : (
- <></>
);
};
- if (posts.length === 0) {
- return <NoResults searchPage={searchPage} />;
- }
+ if (posts.length === 0) return <NoResults searchPage={searchPage} />;
return (
<>
{getPosts()}
- {isLoading && <Spinner />}
+ {isLoading ? <Spinner /> : null}
{isMounted ? getProgressBar() : getPagination()}
</>
);
diff --git a/src/components/organisms/layout/summary.tsx b/src/components/organisms/layout/summary.tsx
index cacd6d2..e7a5d48 100644
--- a/src/components/organisms/layout/summary.tsx
+++ b/src/components/organisms/layout/summary.tsx
@@ -1,6 +1,6 @@
-import { FC, ReactNode } from 'react';
+import type { FC, ReactNode } from 'react';
import { useIntl } from 'react-intl';
-import { type Article, type Meta as MetaType } from '../../../types';
+import type { Article, Meta as MetaType } from '../../../types';
import { useReadingTime } from '../../../utils/hooks';
import {
Arrow,
@@ -70,6 +70,7 @@ export const Summary: FC<SummaryProps> = ({
{
title,
a11y: (chunks: ReactNode) => (
+ // eslint-disable-next-line react/jsx-no-literals -- SR class allowed
<span className="screen-reader-text">{chunks}</span>
),
}
@@ -99,7 +100,7 @@ export const Summary: FC<SummaryProps> = ({
)),
comments: {
about: title,
- count: commentsCount || 0,
+ count: commentsCount ?? 0,
target: `${url}#comments`,
},
};
@@ -107,7 +108,7 @@ export const Summary: FC<SummaryProps> = ({
return (
<article className={styles.wrapper}>
- {cover && <ResponsiveImage className={styles.cover} {...cover} />}
+ {cover ? <ResponsiveImage className={styles.cover} {...cover} /> : null}
<header className={styles.header}>
<Link href={url} className={styles.link}>
<Heading level={titleLevel} className={styles.title}>
@@ -116,13 +117,16 @@ export const Summary: FC<SummaryProps> = ({
</Link>
</header>
<div className={styles.body}>
+ {/* eslint-disable-next-line react/no-danger -- Not safe but intro can
+ * contains links or formatting so we need it. */}
<div dangerouslySetInnerHTML={{ __html: intro }} />
- <ButtonLink target={url} className={styles['read-more']}>
+ <ButtonLink className={styles['read-more']} to={url}>
<>
{readMore}
<Arrow
aria-hidden={true}
className={styles.icon}
+ // eslint-disable-next-line react/jsx-no-literals -- Direction allowed
direction="right"
/>
</>
@@ -133,7 +137,9 @@ export const Summary: FC<SummaryProps> = ({
className={styles.meta}
data={getMeta()}
groupClassName={styles.meta__item}
+ // eslint-disable-next-line react/jsx-no-literals -- Layout allowed
itemsLayout="stacked"
+ // eslint-disable-next-line react/jsx-no-literals -- Layout allowed
layout="column"
withSeparator={false}
/>