aboutsummaryrefslogtreecommitdiffstats
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/atoms/layout/notice.module.scss1
-rw-r--r--src/components/organisms/forms/comment-form.stories.tsx30
-rw-r--r--src/components/organisms/forms/comment-form.test.tsx7
-rw-r--r--src/components/organisms/forms/comment-form.tsx19
-rw-r--r--src/components/organisms/layout/comment.stories.tsx39
-rw-r--r--src/components/organisms/layout/comment.test.tsx6
-rw-r--r--src/components/organisms/layout/comment.tsx14
-rw-r--r--src/components/organisms/layout/comments-list.stories.tsx30
-rw-r--r--src/components/organisms/layout/comments-list.test.tsx6
-rw-r--r--src/components/organisms/layout/comments-list.tsx10
-rw-r--r--src/components/templates/page/page-layout.stories.tsx6
-rw-r--r--src/components/templates/page/page-layout.tsx95
12 files changed, 200 insertions, 63 deletions
diff --git a/src/components/atoms/layout/notice.module.scss b/src/components/atoms/layout/notice.module.scss
index 38ec7ee..7fd972c 100644
--- a/src/components/atoms/layout/notice.module.scss
+++ b/src/components/atoms/layout/notice.module.scss
@@ -1,7 +1,6 @@
@use "@styles/abstracts/functions" as fun;
.wrapper {
- width: max-content;
padding: var(--spacing-2xs) var(--spacing-xs);
border: fun.convert-px(2) solid;
font-weight: bold;
diff --git a/src/components/organisms/forms/comment-form.stories.tsx b/src/components/organisms/forms/comment-form.stories.tsx
index f66d35c..8b11df7 100644
--- a/src/components/organisms/forms/comment-form.stories.tsx
+++ b/src/components/organisms/forms/comment-form.stories.tsx
@@ -1,13 +1,19 @@
import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
import CommentForm from './comment-form';
+const saveComment = async () => {
+ /** Do nothing. */
+};
+
/**
* CommentForm - Storybook Meta
*/
export default {
title: 'Organisms/Forms',
component: CommentForm,
+ args: {
+ saveComment,
+ },
argTypes: {
className: {
control: {
@@ -35,6 +41,16 @@ export default {
required: false,
},
},
+ parentId: {
+ control: {
+ type: null,
+ },
+ description: 'The parent id if it is a reply.',
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
saveComment: {
control: {
type: null,
@@ -74,13 +90,6 @@ export default {
},
},
},
- decorators: [
- (Story) => (
- <IntlProvider locale="en">
- <Story />
- </IntlProvider>
- ),
- ],
} as ComponentMeta<typeof CommentForm>;
const Template: ComponentStory<typeof CommentForm> = (args) => (
@@ -91,8 +100,3 @@ const Template: ComponentStory<typeof CommentForm> = (args) => (
* Forms Stories - Comment
*/
export const Comment = Template.bind({});
-Comment.args = {
- saveComment: (reset: () => void) => {
- reset();
- },
-};
diff --git a/src/components/organisms/forms/comment-form.test.tsx b/src/components/organisms/forms/comment-form.test.tsx
index 0d387b5..c67ad6b 100644
--- a/src/components/organisms/forms/comment-form.test.tsx
+++ b/src/components/organisms/forms/comment-form.test.tsx
@@ -1,17 +1,20 @@
import { render, screen } from '@test-utils';
import CommentForm from './comment-form';
+const saveComment = async () => {
+ /** Do nothing. */
+};
const title = 'Cum voluptas voluptatibus';
describe('CommentForm', () => {
it('renders a form', () => {
- render(<CommentForm saveComment={() => null} />);
+ render(<CommentForm saveComment={saveComment} />);
expect(screen.getByRole('form')).toBeInTheDocument();
});
it('renders an optional title', () => {
render(
- <CommentForm saveComment={() => null} title={title} titleLevel={2} />
+ <CommentForm saveComment={saveComment} title={title} titleLevel={2} />
);
expect(
screen.getByRole('heading', { level: 2, name: title })
diff --git a/src/components/organisms/forms/comment-form.tsx b/src/components/organisms/forms/comment-form.tsx
index d7cb0f5..9e0abdf 100644
--- a/src/components/organisms/forms/comment-form.tsx
+++ b/src/components/organisms/forms/comment-form.tsx
@@ -7,6 +7,14 @@ import { FC, ReactNode, useState } from 'react';
import { useIntl } from 'react-intl';
import styles from './comment-form.module.scss';
+export type CommentFormData = {
+ comment: string;
+ email: string;
+ name: string;
+ parentId?: number;
+ website?: string;
+};
+
export type CommentFormProps = {
/**
* Set additional classnames to the form wrapper.
@@ -17,10 +25,14 @@ export type CommentFormProps = {
*/
Notice?: ReactNode;
/**
+ * The comment parent id.
+ */
+ parentId?: number;
+ /**
* A callback function to save comment. It takes a function as parameter to
* reset the form.
*/
- saveComment: (reset: () => void) => void;
+ saveComment: (data: CommentFormData, reset: () => void) => Promise<void>;
/**
* The form title.
*/
@@ -34,6 +46,7 @@ export type CommentFormProps = {
const CommentForm: FC<CommentFormProps> = ({
className = '',
Notice,
+ parentId,
saveComment,
title,
titleLevel = 2,
@@ -95,7 +108,9 @@ const CommentForm: FC<CommentFormProps> = ({
*/
const submitHandler = () => {
setIsSubmitting(true);
- saveComment(resetForm);
+ saveComment({ comment, email, name, parentId, website }, resetForm).then(
+ () => setIsSubmitting(false)
+ );
};
return (
diff --git a/src/components/organisms/layout/comment.stories.tsx b/src/components/organisms/layout/comment.stories.tsx
index 3794b06..c31b77a 100644
--- a/src/components/organisms/layout/comment.stories.tsx
+++ b/src/components/organisms/layout/comment.stories.tsx
@@ -1,13 +1,19 @@
import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
import CommentComponent from './comment';
+const saveComment = async () => {
+ /** Do nothing. */
+};
+
/**
* Comment - Storybook Meta
*/
export default {
title: 'Organisms/Layout',
component: CommentComponent,
+ args: {
+ saveComment,
+ },
argTypes: {
author: {
description: 'The author data.',
@@ -51,6 +57,29 @@ export default {
required: true,
},
},
+ Notice: {
+ control: {
+ type: null,
+ },
+ description: 'A component to display a success or error message.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'function',
+ required: false,
+ },
+ },
+ parentId: {
+ control: {
+ type: null,
+ },
+ description: 'The parent id if it is a reply.',
+ type: {
+ name: 'number',
+ required: false,
+ },
+ },
publication: {
description: 'The publication date.',
type: {
@@ -73,13 +102,6 @@ export default {
},
},
},
- decorators: [
- (Story) => (
- <IntlProvider locale="en">
- <Story />
- </IntlProvider>
- ),
- ],
} as ComponentMeta<typeof CommentComponent>;
const Template: ComponentStory<typeof CommentComponent> = (args) => (
@@ -100,7 +122,6 @@ Comment.args = {
'Harum aut cumque iure fugit neque sequi cupiditate repudiandae laudantium. Ratione aut assumenda qui illum voluptas accusamus quis officiis exercitationem. Consectetur est harum eius perspiciatis officiis nihil. Aut corporis minima debitis adipisci possimus debitis et.',
id: 2,
publication: '2021-04-03 23:04:24',
- saveComment: () => null,
// @ts-ignore - Needed because of the placeholder image.
unoptimized: true,
};
diff --git a/src/components/organisms/layout/comment.test.tsx b/src/components/organisms/layout/comment.test.tsx
index 4961722..02a51dc 100644
--- a/src/components/organisms/layout/comment.test.tsx
+++ b/src/components/organisms/layout/comment.test.tsx
@@ -11,13 +11,15 @@ const content =
'Harum aut cumque iure fugit neque sequi cupiditate repudiandae laudantium. Ratione aut assumenda qui illum voluptas accusamus quis officiis exercitationem. Consectetur est harum eius perspiciatis officiis nihil. Aut corporis minima debitis adipisci possimus debitis et.';
const publication = '2021-04-03 23:04:24';
const id = 5;
-
+const saveComment = async () => {
+ /** Do nothing. */
+};
const data = {
author,
content,
id,
publication,
- saveComment: () => null,
+ saveComment,
};
const formattedDate = getFormattedDate(publication);
diff --git a/src/components/organisms/layout/comment.tsx b/src/components/organisms/layout/comment.tsx
index 248efc2..6df393b 100644
--- a/src/components/organisms/layout/comment.tsx
+++ b/src/components/organisms/layout/comment.tsx
@@ -25,7 +25,7 @@ export type CommentAuthor = {
url?: string;
};
-export type CommentProps = {
+export type CommentProps = Pick<CommentFormProps, 'Notice' | 'saveComment'> & {
/**
* The comment author data.
*/
@@ -50,10 +50,6 @@ export type CommentProps = {
* The comment date and time separated with a space.
*/
publication: string;
- /**
- * A callback function to save comment form data.
- */
- saveComment: CommentFormProps['saveComment'];
};
/**
@@ -66,6 +62,7 @@ const Comment: FC<CommentProps> = ({
canReply = true,
content,
id,
+ Notice,
parentId,
publication,
saveComment,
@@ -169,7 +166,10 @@ const Comment: FC<CommentProps> = ({
className={styles.date}
groupClassName={styles.date__item}
/>
- <div className={styles.body}>{content}</div>
+ <div
+ className={styles.body}
+ dangerouslySetInnerHTML={{ __html: content }}
+ />
<footer className={styles.footer}>
{canReply && (
<Button kind="tertiary" onClick={() => setIsReplying(!isReplying)}>
@@ -180,6 +180,8 @@ const Comment: FC<CommentProps> = ({
</article>
{isReplying && (
<CommentForm
+ Notice={Notice}
+ parentId={id as number}
saveComment={saveComment}
title={formTitle}
className={`${styles.wrapper} ${styles['wrapper--form']}`}
diff --git a/src/components/organisms/layout/comments-list.stories.tsx b/src/components/organisms/layout/comments-list.stories.tsx
index 9edf368..4d95205 100644
--- a/src/components/organisms/layout/comments-list.stories.tsx
+++ b/src/components/organisms/layout/comments-list.stories.tsx
@@ -1,13 +1,19 @@
import { ComponentMeta, ComponentStory } from '@storybook/react';
-import { IntlProvider } from 'react-intl';
import CommentsListComponent, { Comment } from './comments-list';
+const saveComment = async () => {
+ /** Do nothing. */
+};
+
/**
* CommentsList - Storybook Meta
*/
export default {
title: 'Organisms/Layout/CommentsList',
component: CommentsListComponent,
+ args: {
+ saveComment,
+ },
argTypes: {
comments: {
control: {
@@ -30,6 +36,19 @@ export default {
required: true,
},
},
+ Notice: {
+ control: {
+ type: null,
+ },
+ description: 'A component to display a success or error message.',
+ table: {
+ category: 'Options',
+ },
+ type: {
+ name: 'function',
+ required: false,
+ },
+ },
saveComment: {
control: {
type: null,
@@ -44,13 +63,6 @@ export default {
},
},
},
- decorators: [
- (Story) => (
- <IntlProvider locale="en">
- <Story />
- </IntlProvider>
- ),
- ],
} as ComponentMeta<typeof CommentsListComponent>;
const Template: ComponentStory<typeof CommentsListComponent> = (args) => (
@@ -130,7 +142,6 @@ export const WithoutChildComments = Template.bind({});
WithoutChildComments.args = {
comments,
depth: 0,
- saveComment: () => null,
};
/**
@@ -140,5 +151,4 @@ export const WithChildComments = Template.bind({});
WithChildComments.args = {
comments,
depth: 1,
- saveComment: () => null,
};
diff --git a/src/components/organisms/layout/comments-list.test.tsx b/src/components/organisms/layout/comments-list.test.tsx
index 542b1df..e135ec9 100644
--- a/src/components/organisms/layout/comments-list.test.tsx
+++ b/src/components/organisms/layout/comments-list.test.tsx
@@ -57,10 +57,14 @@ const comments: Comment[] = [
},
];
+const saveComment = async () => {
+ /** Do nothing. */
+};
+
describe('CommentsList', () => {
it('renders a comments list', () => {
render(
- <CommentsList comments={comments} depth={1} saveComment={() => null} />
+ <CommentsList comments={comments} depth={1} saveComment={saveComment} />
);
});
});
diff --git a/src/components/organisms/layout/comments-list.tsx b/src/components/organisms/layout/comments-list.tsx
index 03f508e..f04354c 100644
--- a/src/components/organisms/layout/comments-list.tsx
+++ b/src/components/organisms/layout/comments-list.tsx
@@ -8,7 +8,7 @@ export type Comment = Omit<CommentProps, 'canReply' | 'saveComment'> & {
child?: Comment[];
};
-export type CommentsListProps = {
+export type CommentsListProps = Pick<CommentProps, 'Notice' | 'saveComment'> & {
/**
* An array of comments.
*/
@@ -17,10 +17,6 @@ export type CommentsListProps = {
* The maximum depth. Use `0` to not display nested comments.
*/
depth: 0 | 1 | 2 | 3 | 4;
- /**
- * A callback function to save comment form data.
- */
- saveComment: CommentProps['saveComment'];
};
/**
@@ -31,6 +27,7 @@ export type CommentsListProps = {
const CommentsList: FC<CommentsListProps> = ({
comments,
depth,
+ Notice,
saveComment,
}) => {
/**
@@ -48,8 +45,9 @@ const CommentsList: FC<CommentsListProps> = ({
return commentsList.map(({ child, ...comment }) => (
<li key={comment.id} className={styles.item}>
<SingleComment
- saveComment={saveComment}
canReply={!isLastLevel}
+ Notice={Notice}
+ saveComment={saveComment}
{...comment}
/>
{child && !isLastLevel && (
diff --git a/src/components/templates/page/page-layout.stories.tsx b/src/components/templates/page/page-layout.stories.tsx
index 480c76e..8e518aa 100644
--- a/src/components/templates/page/page-layout.stories.tsx
+++ b/src/components/templates/page/page-layout.stories.tsx
@@ -85,12 +85,12 @@ export default {
},
id: {
control: {
- type: 'text',
+ type: 'number',
},
description: 'The page id.',
type: {
- name: 'string',
- required: true,
+ name: 'number',
+ required: false,
},
},
intro: {
diff --git a/src/components/templates/page/page-layout.tsx b/src/components/templates/page/page-layout.tsx
index ac021ba..045b8c1 100644
--- a/src/components/templates/page/page-layout.tsx
+++ b/src/components/templates/page/page-layout.tsx
@@ -1,4 +1,5 @@
import Heading from '@components/atoms/headings/heading';
+import Notice, { type NoticeKind } from '@components/atoms/layout/notice';
import Sidebar from '@components/atoms/layout/sidebar';
import PageFooter, {
type PageFooterProps,
@@ -9,15 +10,19 @@ import PageHeader, {
import Breadcrumb, {
type BreadcrumbItem,
} from '@components/molecules/nav/breadcrumb';
-import CommentForm from '@components/organisms/forms/comment-form';
+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 { FC, ReactNode, useRef } from 'react';
+import { FC, ReactNode, useRef, useState } from 'react';
import { useIntl } from 'react-intl';
-import Layout, { LayoutProps } from '../layout/layout';
+import Layout, { type LayoutProps } from '../layout/layout';
import styles from './page-layout.module.scss';
export type PageLayoutProps = {
@@ -46,6 +51,10 @@ export type PageLayoutProps = {
*/
headerMeta?: PageHeaderProps['meta'];
/**
+ * The page id.
+ */
+ id?: number;
+ /**
* The page introduction.
*/
intro?: PageHeaderProps['intro'];
@@ -79,6 +88,7 @@ const PageLayout: FC<PageLayoutProps> = ({
comments,
footerMeta,
headerMeta,
+ id,
intro,
isHome = false,
widgets,
@@ -106,8 +116,56 @@ const PageLayout: FC<PageLayoutProps> = ({
? 'article--has-comments'
: 'article--no-comments';
- const saveComment = () => {
- return null;
+ 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);
+ }
};
return (
@@ -154,15 +212,36 @@ const PageLayout: FC<PageLayoutProps> = ({
<section className={styles.comments__section}>
<Heading level={2}>{commentsTitle}</Heading>
<CommentsList
- saveComment={saveComment}
comments={comments}
- depth={2}
+ depth={1}
+ Notice={
+ isReplyRef.current === true ? (
+ <Notice
+ kind={status}
+ message={statusMessage}
+ className={styles.notice}
+ />
+ ) : undefined
+ }
+ saveComment={saveComment}
/>
</section>
)}
{allowComments && (
<section className={styles.comments__section}>
- <CommentForm saveComment={saveComment} title={commentFormTitle} />
+ <CommentForm
+ saveComment={saveComment}
+ title={commentFormTitle}
+ Notice={
+ isReplyRef.current === false ? (
+ <Notice
+ kind={status}
+ message={statusMessage}
+ className={styles.notice}
+ />
+ ) : undefined
+ }
+ />
</section>
)}
</div>