summaryrefslogtreecommitdiffstats
path: root/src/components/templates/page/page-layout.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/templates/page/page-layout.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/templates/page/page-layout.tsx')
-rw-r--r--src/components/templates/page/page-layout.tsx297
1 files changed, 297 insertions, 0 deletions
diff --git a/src/components/templates/page/page-layout.tsx b/src/components/templates/page/page-layout.tsx
new file mode 100644
index 0000000..f96666e
--- /dev/null
+++ b/src/components/templates/page/page-layout.tsx
@@ -0,0 +1,297 @@
+import Heading from '@components/atoms/headings/heading';
+import Notice, { type NoticeKind } from '@components/atoms/layout/notice';
+import Sidebar from '@components/atoms/layout/sidebar';
+import { MetaData } from '@components/molecules/layout/meta';
+import PageFooter, {
+ type PageFooterProps,
+} from '@components/molecules/layout/page-footer';
+import PageHeader, {
+ type PageHeaderProps,
+} from '@components/molecules/layout/page-header';
+import Breadcrumb, {
+ type BreadcrumbItem,
+} from '@components/molecules/nav/breadcrumb';
+import CommentForm, {
+ type CommentFormProps,
+} from '@components/organisms/forms/comment-form';
+import CommentsList, {
+ type CommentsListProps,
+} from '@components/organisms/layout/comments-list';
+import TableOfContents from '@components/organisms/widgets/table-of-contents';
+import { type SendCommentVars } from '@services/graphql/api';
+import { sendComment } from '@services/graphql/comments';
+import useIsMounted from '@utils/hooks/use-is-mounted';
+import Script from 'next/script';
+import { FC, HTMLAttributes, ReactNode, useRef, useState } from 'react';
+import { useIntl } from 'react-intl';
+import { BreadcrumbList } from 'schema-dts';
+import styles from './page-layout.module.scss';
+
+export type PageLayoutProps = {
+ /**
+ * True if the page accepts new comments. Default: false.
+ */
+ allowComments?: boolean;
+ /**
+ * Set attributes to the page body.
+ */
+ bodyAttributes?: HTMLAttributes<HTMLDivElement>;
+ /**
+ * Set additional classnames to the body wrapper.
+ */
+ bodyClassName?: string;
+ /**
+ * The breadcrumb items.
+ */
+ breadcrumb: BreadcrumbItem[];
+ /**
+ * The breadcrumb JSON schema.
+ */
+ breadcrumbSchema: BreadcrumbList['itemListElement'][];
+ /**
+ * The main content of the page.
+ */
+ children: ReactNode;
+ /**
+ * The page comments
+ */
+ comments?: CommentsListProps['comments'];
+ /**
+ * The footer metadata.
+ */
+ footerMeta?: PageFooterProps['meta'];
+ /**
+ * The header metadata.
+ */
+ headerMeta?: PageHeaderProps['meta'];
+ /**
+ * The page id.
+ */
+ id?: number;
+ /**
+ * The page introduction.
+ */
+ intro?: PageHeaderProps['intro'];
+ /**
+ * The page title.
+ */
+ title: PageHeaderProps['title'];
+ /**
+ * An array of widgets to put in the last sidebar.
+ */
+ widgets?: ReactNode[];
+ /**
+ * Show the table of contents. Default: false.
+ */
+ withToC?: boolean;
+};
+
+/**
+ * PageLayout component
+ *
+ * Render the pages layout.
+ */
+const PageLayout: FC<PageLayoutProps> = ({
+ children,
+ allowComments = false,
+ bodyAttributes,
+ bodyClassName = '',
+ breadcrumb,
+ breadcrumbSchema,
+ comments,
+ footerMeta,
+ headerMeta,
+ id,
+ intro,
+ title,
+ widgets,
+ withToC = false,
+}) => {
+ const intl = useIntl();
+ const commentsTitle = intl.formatMessage({
+ defaultMessage: 'Comments',
+ description: 'PageLayout: comments title',
+ id: '+dJU3e',
+ });
+ const commentFormTitle = intl.formatMessage({
+ defaultMessage: 'Leave a comment',
+ description: 'PageLayout: comment form title',
+ id: 'kzIYoQ',
+ });
+
+ const bodyRef = useRef<HTMLDivElement>(null);
+ const isMounted = useIsMounted(bodyRef);
+ const hasComments = Array.isArray(comments) && comments.length > 0;
+ const [status, setStatus] = useState<NoticeKind>('info');
+ const [statusMessage, setStatusMessage] = useState<string>('');
+ const isReplyRef = useRef<boolean>(false);
+
+ const saveComment: CommentFormProps['saveComment'] = async (data, reset) => {
+ if (!id) throw new Error('Page id missing. Cannot save comment.');
+
+ const { comment: commentBody, email, name, parentId, website } = data;
+ const commentData: SendCommentVars = {
+ author: name,
+ authorEmail: email,
+ authorUrl: website || '',
+ clientMutationId: 'contact',
+ commentOn: id,
+ content: commentBody,
+ parent: parentId,
+ };
+ const { comment, success } = await sendComment(commentData);
+
+ isReplyRef.current = !!parentId;
+
+ if (success) {
+ setStatus('success');
+ const successPrefix = intl.formatMessage({
+ defaultMessage: 'Thanks, your comment was successfully sent.',
+ description: 'PageLayout: comment form success message',
+ id: 'B290Ph',
+ });
+ const successMessage = comment?.approved
+ ? intl.formatMessage({
+ defaultMessage: 'It has been approved.',
+ id: 'g3+Ahv',
+ description: 'PageLayout: comment approved.',
+ })
+ : intl.formatMessage({
+ defaultMessage: 'It is now awaiting moderation.',
+ id: 'Vmj5cw',
+ description: 'PageLayout: comment awaiting moderation',
+ });
+ setStatusMessage(`${successPrefix} ${successMessage}`);
+ reset();
+ } else {
+ const error = intl.formatMessage({
+ defaultMessage: 'An error occurred:',
+ description: 'PageLayout: comment form error message',
+ id: 'fkcTGp',
+ });
+ setStatus('error');
+ setStatusMessage(error);
+ }
+ };
+
+ /**
+ * Check if meta properties are defined.
+ *
+ * @param {MetaData} meta - The metadata.
+ */
+ const hasMeta = (meta: MetaData) => {
+ return Object.values(meta).every((value) => value);
+ };
+
+ return (
+ <>
+ <Script
+ id="schema-breadcrumb"
+ type="application/ld+json"
+ dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumbSchema) }}
+ />
+ <Breadcrumb
+ items={breadcrumb}
+ className={styles.breadcrumb}
+ itemClassName={styles.breadcrumb__items}
+ />
+ <PageHeader
+ title={title}
+ intro={intro}
+ meta={headerMeta}
+ className={styles.header}
+ />
+ {withToC && (
+ <Sidebar
+ className={`${styles.sidebar} ${styles['sidebar--first']}`}
+ aria-label={intl.formatMessage({
+ defaultMessage: 'Table of contents sidebar',
+ id: 'Q+1GbT',
+ description: 'PageLayout: accessible name for ToC sidebar',
+ })}
+ >
+ {isMounted && bodyRef.current && (
+ <TableOfContents wrapper={bodyRef.current} />
+ )}
+ </Sidebar>
+ )}
+ {typeof children === 'string' ? (
+ <div
+ ref={bodyRef}
+ className={`${styles.body} ${bodyClassName}`}
+ dangerouslySetInnerHTML={{ __html: children }}
+ {...bodyAttributes}
+ />
+ ) : (
+ <div ref={bodyRef} className={`${styles.body} ${bodyClassName}`}>
+ {children}
+ </div>
+ )}
+ {footerMeta && hasMeta(footerMeta) && (
+ <PageFooter meta={footerMeta} className={styles.footer} />
+ )}
+ <Sidebar
+ className={`${styles.sidebar} ${styles['sidebar--last']}`}
+ aria-label={intl.formatMessage({
+ defaultMessage: 'Sidebar',
+ id: 'c556Qo',
+ description: 'PageLayout: accessible name for the sidebar',
+ })}
+ >
+ {widgets}
+ </Sidebar>
+ {allowComments && (
+ <div className={styles.comments} id="comments">
+ <section className={styles.comments__section}>
+ <Heading level={2} alignment="center">
+ {commentsTitle}
+ </Heading>
+ {hasComments ? (
+ <CommentsList
+ comments={comments}
+ depth={2}
+ Notice={
+ isReplyRef.current === true && (
+ <Notice
+ kind={status}
+ message={statusMessage}
+ className={styles.notice}
+ />
+ )
+ }
+ saveComment={saveComment}
+ />
+ ) : (
+ <p className={styles['comments__no-comments']}>
+ {intl.formatMessage({
+ defaultMessage: 'No comments.',
+ id: 'sBwfCy',
+ description: 'PageLayout: no comments text',
+ })}
+ </p>
+ )}
+ </section>
+ <section className={styles.comments__section}>
+ <CommentForm
+ className={styles.comments__form}
+ saveComment={saveComment}
+ title={commentFormTitle}
+ titleAlignment="center"
+ Notice={
+ isReplyRef.current === false && (
+ <Notice
+ kind={status}
+ message={statusMessage}
+ className={styles.notice}
+ />
+ )
+ }
+ />
+ </section>
+ </div>
+ )}
+ </>
+ );
+};
+
+export default PageLayout;