diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-11-09 17:18:46 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:15:27 +0100 |
| commit | f699802b837d7d9fcf150ff2bf00cd3c5475c87a (patch) | |
| tree | 6c96a140193e7386b454b6d444058a99a0e07454 /src | |
| parent | bd9c9ae7e2ae973969569dd434836de9f38b07d4 (diff) | |
refactor(components): rewrite CommentsList component
* use ApprovedCommentProps to make CommentData type
* add the author name of the parent on reply form heading
* add tests
Diffstat (limited to 'src')
17 files changed, 726 insertions, 343 deletions
diff --git a/src/components/organisms/layout/comments-list.module.scss b/src/components/organisms/comments-list/comments-list.module.scss index e690250..e690250 100644 --- a/src/components/organisms/layout/comments-list.module.scss +++ b/src/components/organisms/comments-list/comments-list.module.scss diff --git a/src/components/organisms/comments-list/comments-list.stories.tsx b/src/components/organisms/comments-list/comments-list.stories.tsx new file mode 100644 index 0000000..f6ad58e --- /dev/null +++ b/src/components/organisms/comments-list/comments-list.stories.tsx @@ -0,0 +1,177 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { type CommentData, CommentsList } from './comments-list'; + +const saveComment = () => undefined; + +/** + * CommentsList - Storybook Meta + */ +export default { + title: 'Organisms/CommentsList', + component: CommentsList, + argTypes: { + comments: { + control: { + type: null, + }, + description: 'An array of comments.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + depth: { + control: { + type: 'number', + min: 0, + max: 4, + }, + description: 'The maximum depth. Use `0` to hide replies.', + table: { + category: 'Options', + defaultValue: { summary: 0 }, + }, + type: { + name: 'number', + required: false, + }, + }, + onSubmit: { + control: { + type: null, + }, + description: 'A callback function to save the comment form data.', + table: { + category: 'Events', + }, + type: { + name: 'function', + required: false, + }, + }, + }, +} as ComponentMeta<typeof CommentsList>; + +const Template: ComponentStory<typeof CommentsList> = (args) => ( + <CommentsList {...args} /> +); + +const comments = [ + { + author: { + name: 'Milan0', + avatar: { + alt: 'Milan0 avatar', + src: 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/976.jpg', + }, + }, + content: 'Fugit veniam quas qui dolor explicabo.', + id: 1, + isApproved: true, + publicationDate: '2023-01-23', + replies: [ + { + author: { name: 'Haskell42' }, + content: 'Error quas accusamus nesciunt enim quae a.', + id: 25, + isApproved: true, + publicationDate: '2023-02-04', + }, + { + author: { name: 'Hanna49', website: 'https://www.armandphilippot.com' }, + content: 'Ut ducimus neque aliquam soluta sed totam commodi cum sit.', + id: 30, + isApproved: true, + publicationDate: '2023-03-10', + }, + ], + }, + { + author: { + name: 'Corrine9', + avatar: { + alt: 'Corrine9 avatar', + src: 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/539.jpg', + }, + }, + content: + 'Dolore hic iure voluptatum quam error minima. Quas ut aperiam sit commodi cumque consequatur. Voluptas debitis veritatis officiis in voluptas ea et laborum animi. Voluptatem qui enim neque. Et sunt quo neque assumenda iure. Non vel ut consectetur.', + id: 2, + isApproved: true, + publicationDate: '2023-04-20', + }, + { + author: { name: 'Presley12' }, + content: + 'Nulla eaque similique recusandae enim aut eligendi iure consequatur. Et aut qui. Voluptatem a voluptatem consequatur aliquid distinctio ex culpa. Adipisci animi amet reprehenderit autem quia commodi voluptatum commodi.', + id: 3, + isApproved: true, + publicationDate: '2023-05-01', + replies: [ + { + author: { + name: 'Ana_Haley33', + avatar: { + alt: 'Ana_Haley33 avatar', + src: 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/881.jpg', + }, + }, + content: 'Ab ea et fugit autem.', + id: 17, + isApproved: true, + publicationDate: '2023-05-01', + }, + { + author: { name: 'Santos.Harris17' }, + content: + 'Illo dolores voluptatem similique voluptas quasi hic aspernatur ab nisi.', + id: 18, + isApproved: false, + publicationDate: '2023-05-02', + }, + ], + }, + { + author: { name: 'Julius.Borer' }, + content: 'Ea fugit totam et voluptatum quidem laborum explicabo fuga quod.', + id: 4, + isApproved: true, + publicationDate: '2023-06-15', + }, + { + author: { name: 'Geo87' }, + content: + 'Enim consequatur deleniti aliquid adipisci. Et mollitia saepe vel rerum totam praesentium assumenda repellat fuga. Ipsum ut architecto consequatur. Ut laborum suscipit sed corporis quas aliquid. Et et omnis quo. Dolore quia ipsum ut corporis eum et corporis qui.', + id: 5, + isApproved: false, + publicationDate: '2023-06-16', + }, + { + author: { name: 'Kurt.Keeling' }, + content: 'Eligendi repellat officiis amet.', + id: 6, + isApproved: true, + publicationDate: '2023-06-17', + }, +] satisfies CommentData[]; + +/** + * Layout Stories - Without nested comments + */ +export const WithoutReplies = Template.bind({}); +WithoutReplies.args = { + comments, + depth: 0, + onSubmit: saveComment, +}; + +/** + * Layout Stories - With nested comments + */ +export const WithReplies = Template.bind({}); +WithReplies.args = { + comments, + depth: 2, + onSubmit: saveComment, +}; diff --git a/src/components/organisms/comments-list/comments-list.test.tsx b/src/components/organisms/comments-list/comments-list.test.tsx new file mode 100644 index 0000000..6706362 --- /dev/null +++ b/src/components/organisms/comments-list/comments-list.test.tsx @@ -0,0 +1,264 @@ +import { describe, expect, it } from '@jest/globals'; +import { userEvent } from '@testing-library/user-event'; +import { render, screen as rtlScreen } from '../../../../tests/utils'; +import { type CommentData, CommentsList } from './comments-list'; + +describe('CommentsList', () => { + it('renders a list of approved comments', () => { + const comments = [ + { + author: { name: 'Milan0' }, + content: 'Fugit veniam quas qui dolor explicabo.', + id: 1, + isApproved: true, + publicationDate: '2023-01-23', + }, + { + author: { name: 'Haskell42' }, + content: 'Error quas accusamus nesciunt enim quae a.', + id: 2, + isApproved: true, + publicationDate: '2023-02-04', + }, + ] satisfies CommentData[]; + + render(<CommentsList comments={comments} />); + + expect(rtlScreen.getAllByRole('listitem')).toHaveLength(comments.length); + expect(rtlScreen.getByText(comments[0].author.name)).toBeInTheDocument(); + expect(rtlScreen.getByText(comments[0].content)).toBeInTheDocument(); + expect(rtlScreen.getByText(comments[1].author.name)).toBeInTheDocument(); + expect(rtlScreen.getByText(comments[1].content)).toBeInTheDocument(); + }); + + it('renders a list of pending comments', () => { + const comments = [ + { + author: { name: 'Milan0' }, + content: 'Fugit veniam quas qui dolor explicabo.', + id: 1, + isApproved: false, + publicationDate: '2023-01-23', + }, + { + author: { name: 'Haskell42' }, + content: 'Error quas accusamus nesciunt enim quae a.', + id: 2, + isApproved: false, + publicationDate: '2023-02-04', + }, + ] satisfies CommentData[]; + + render(<CommentsList comments={comments} />); + + expect(rtlScreen.getAllByRole('listitem')).toHaveLength(comments.length); + expect( + rtlScreen.queryByText(comments[0].author.name) + ).not.toBeInTheDocument(); + expect(rtlScreen.queryByText(comments[0].content)).not.toBeInTheDocument(); + expect( + rtlScreen.queryByText(comments[1].author.name) + ).not.toBeInTheDocument(); + expect(rtlScreen.queryByText(comments[1].content)).not.toBeInTheDocument(); + }); + + it('renders a mixed list of approved and pending comments', () => { + const comments = [ + { + author: { name: 'Milan0' }, + content: 'Fugit veniam quas qui dolor explicabo.', + id: 1, + isApproved: true, + publicationDate: '2023-01-23', + }, + { + author: { name: 'Haskell42' }, + content: 'Error quas accusamus nesciunt enim quae a.', + id: 2, + isApproved: false, + publicationDate: '2023-02-04', + }, + ] satisfies CommentData[]; + + render(<CommentsList comments={comments} />); + + expect(rtlScreen.getAllByRole('listitem')).toHaveLength(comments.length); + expect(rtlScreen.getByText(comments[0].author.name)).toBeInTheDocument(); + expect(rtlScreen.getByText(comments[0].content)).toBeInTheDocument(); + expect( + rtlScreen.queryByText(comments[1].author.name) + ).not.toBeInTheDocument(); + expect(rtlScreen.queryByText(comments[1].content)).not.toBeInTheDocument(); + }); + + it('does not render the replies by default', () => { + const comments = [ + { + author: { name: 'Milan0' }, + content: 'Fugit veniam quas qui dolor explicabo.', + id: 1, + isApproved: true, + publicationDate: '2023-01-23', + replies: [ + { + author: { name: 'Haskell42' }, + content: 'Error quas accusamus nesciunt enim quae a.', + id: 2, + isApproved: true, + publicationDate: '2023-02-04', + }, + { + author: { name: 'Hanna49' }, + content: + 'Ut ducimus neque aliquam soluta sed totam commodi cum sit.', + id: 3, + isApproved: true, + publicationDate: '2023-03-10', + }, + ], + }, + ] satisfies CommentData[]; + + render(<CommentsList comments={comments} />); + + expect(rtlScreen.getAllByRole('listitem')).toHaveLength(1); + expect(rtlScreen.getByText(comments[0].author.name)).toBeInTheDocument(); + expect(rtlScreen.getByText(comments[0].content)).toBeInTheDocument(); + expect( + rtlScreen.queryByText(comments[0].replies[0].author.name) + ).not.toBeInTheDocument(); + expect( + rtlScreen.queryByText(comments[0].replies[0].content) + ).not.toBeInTheDocument(); + expect(rtlScreen.queryByText(/Reply/)).not.toBeInTheDocument(); + }); + + it('can render the replies by providing a depth', () => { + const comments = [ + { + author: { name: 'Milan0' }, + content: 'Fugit veniam quas qui dolor explicabo.', + id: 1, + isApproved: true, + publicationDate: '2023-01-23', + replies: [ + { + author: { name: 'Haskell42' }, + content: 'Error quas accusamus nesciunt enim quae a.', + id: 2, + isApproved: true, + publicationDate: '2023-02-04', + }, + { + author: { name: 'Hanna49' }, + content: + 'Ut ducimus neque aliquam soluta sed totam commodi cum sit.', + id: 3, + isApproved: true, + publicationDate: '2023-03-10', + }, + ], + }, + ] satisfies CommentData[]; + + render(<CommentsList comments={comments} depth={1} />); + + const totalComments = + comments.length + + comments.reduce( + (accumulator, currentValue) => + accumulator + currentValue.replies.length, + 0 + ); + + expect(rtlScreen.getAllByRole('listitem')).toHaveLength(totalComments); + expect(rtlScreen.getByText(comments[0].author.name)).toBeInTheDocument(); + expect(rtlScreen.getByText(comments[0].content)).toBeInTheDocument(); + expect( + rtlScreen.getByText(comments[0].replies[0].author.name) + ).toBeInTheDocument(); + expect( + rtlScreen.getByText(comments[0].replies[0].content) + ).toBeInTheDocument(); + expect(rtlScreen.getByText(/Reply/)).toBeInTheDocument(); + }); + + it('can allow replies on replies by providing a depth', () => { + const comments = [ + { + author: { name: 'Milan0' }, + content: 'Fugit veniam quas qui dolor explicabo.', + id: 1, + isApproved: true, + publicationDate: '2023-01-23', + replies: [ + { + author: { name: 'Haskell42' }, + content: 'Error quas accusamus nesciunt enim quae a.', + id: 2, + isApproved: true, + publicationDate: '2023-02-04', + }, + { + author: { name: 'Hanna49' }, + content: + 'Ut ducimus neque aliquam soluta sed totam commodi cum sit.', + id: 3, + isApproved: true, + publicationDate: '2023-03-10', + }, + ], + }, + ] satisfies CommentData[]; + + render(<CommentsList comments={comments} depth={3} />); + + const totalComments = + comments.length + + comments.reduce( + (accumulator, currentValue) => + accumulator + currentValue.replies.length, + 0 + ); + + expect(rtlScreen.getAllByRole('listitem')).toHaveLength(totalComments); + expect(rtlScreen.getAllByText(/Reply/)).toHaveLength(totalComments); + }); + + it('can render a reply form when clicking on the reply button', async () => { + const user = userEvent.setup(); + const comments = [ + { + author: { name: 'Milan0' }, + content: 'Fugit veniam quas qui dolor explicabo.', + id: 1, + isApproved: true, + publicationDate: '2023-01-23', + }, + { + author: { name: 'Haskell42' }, + content: 'Error quas accusamus nesciunt enim quae a.', + id: 2, + isApproved: true, + publicationDate: '2023-02-04', + }, + ] satisfies CommentData[]; + + render(<CommentsList comments={comments} depth={2} />); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect.assertions(3); + + const replyButtons = rtlScreen.getAllByText(/Reply/); + + expect(rtlScreen.queryByRole('form')).not.toBeInTheDocument(); + + await user.click(replyButtons[0]); + + expect(rtlScreen.getByRole('form')).toHaveAccessibleName(/Leave a reply/); + + await user.click(replyButtons[0]); + + expect(rtlScreen.queryByRole('form')).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/organisms/comments-list/comments-list.tsx b/src/components/organisms/comments-list/comments-list.tsx new file mode 100644 index 0000000..0470f99 --- /dev/null +++ b/src/components/organisms/comments-list/comments-list.tsx @@ -0,0 +1,147 @@ +import { + forwardRef, + useCallback, + useState, + type ForwardRefRenderFunction, +} from 'react'; +import { useIntl } from 'react-intl'; +import type { Nullable } from '../../../types'; +import { Heading, List, ListItem, type ListProps } from '../../atoms'; +import { + ApprovedComment, + type ApprovedCommentProps, + PendingComment, + ReplyCommentForm, + type ReplyCommentFormProps, +} from '../comment'; +import styles from './comments-list.module.scss'; + +export type CommentData = Pick< + ApprovedCommentProps, + 'author' | 'content' | 'id' | 'publicationDate' +> & { + isApproved: boolean; + replies?: CommentData[]; +}; + +export type CommentsListProps = Omit< + ListProps<true, false>, + | 'children' + | 'hideMarker' + | 'isHierarchical' + | 'isInline' + | 'isOrdered' + | 'onSubmit' + | 'spacing' +> & + Pick<ReplyCommentFormProps, 'onSubmit'> & { + /** + * The comments. + */ + comments: CommentData[]; + /** + * A positive integer. When depth is set to `0`, replies are not used. + * + * @default 0 + */ + depth?: number; + }; + +const CommentsListWithRef: ForwardRefRenderFunction< + HTMLOListElement, + CommentsListProps +> = ({ comments, depth = 0, onSubmit, ...props }, ref) => { + const [replyingTo, setReplyingTo] = useState<Nullable<number>>(null); + const intl = useIntl(); + + const toggleReply = useCallback((id: number) => { + setReplyingTo((prevId) => { + if (prevId === id) return null; + return id; + }); + }, []); + + const getComments = useCallback( + (data: CommentData[], currentDepth = 0) => { + const isLastLevel = depth === currentDepth; + + return data.map(({ isApproved, replies, ...comment }) => { + const replyFormHeading = intl.formatMessage( + { + defaultMessage: 'Leave a reply to {name}', + description: 'CommentsList: comment form title', + id: 'c1Ju/q', + }, + { name: comment.author.name } + ); + const replyBtnLabel = + replyingTo === comment.id + ? intl.formatMessage({ + defaultMessage: 'Cancel reply', + description: 'CommentsList: cancel reply button', + id: 'uZj4QI', + }) + : intl.formatMessage({ + defaultMessage: 'Reply', + description: 'CommentsList: reply button', + id: 'Qa9twM', + }); + + return ( + <ListItem key={comment.id}> + {isApproved ? ( + <> + <ApprovedComment + {...comment} + onReply={toggleReply} + replyBtn={isLastLevel ? undefined : replyBtnLabel} + /> + {replyingTo === comment.id ? ( + <ReplyCommentForm + className={styles.reply} + commentId={comment.id} + heading={<Heading level={2}>{replyFormHeading}</Heading>} + onSubmit={onSubmit} + /> + ) : null} + {replies?.length && !isLastLevel ? ( + <List + hideMarker + isOrdered + // eslint-disable-next-line react/jsx-no-literals + spacing="sm" + > + {getComments(replies, currentDepth + 1)} + </List> + ) : null} + </> + ) : ( + <PendingComment /> + )} + </ListItem> + ); + }); + }, + [depth, intl, onSubmit, replyingTo, toggleReply] + ); + + return ( + <List + {...props} + hideMarker + isOrdered + ref={ref} + // eslint-disable-next-line react/jsx-no-literals + spacing="sm" + > + {getComments(comments)} + </List> + ); +}; + +/** + * CommentsList component + * + * Render a list of comments. + */ +export const CommentsList = forwardRef(CommentsListWithRef); diff --git a/src/components/organisms/comments-list/index.ts b/src/components/organisms/comments-list/index.ts new file mode 100644 index 0000000..5d7fb9f --- /dev/null +++ b/src/components/organisms/comments-list/index.ts @@ -0,0 +1 @@ +export * from './comments-list'; diff --git a/src/components/organisms/index.ts b/src/components/organisms/index.ts index 7962603..4e62dd1 100644 --- a/src/components/organisms/index.ts +++ b/src/components/organisms/index.ts @@ -1,4 +1,5 @@ export * from './comment'; +export * from './comments-list'; export * from './forms'; export * from './layout'; export * from './nav'; diff --git a/src/components/organisms/layout/comments-list.fixture.ts b/src/components/organisms/layout/comments-list.fixture.ts deleted file mode 100644 index c6a1891..0000000 --- a/src/components/organisms/layout/comments-list.fixture.ts +++ /dev/null @@ -1,108 +0,0 @@ -import type { SingleComment } from '../../../types/app'; - -export const comments: SingleComment[] = [ - { - approved: true, - content: - 'Voluptas ducimus inventore. Libero ut et doloribus. Earum nostrum ab. Aliquam rem dolores omnis voluptate. Sunt aut ut et.', - id: 1, - meta: { - author: { - avatar: { - alt: 'Author 1 avatar', - height: 480, - src: 'http://picsum.photos/640/480', - width: 640, - }, - name: 'Author 1', - }, - date: '2021-04-03 18:04:11', - }, - parentId: 0, - replies: [], - }, - { - approved: true, - content: - 'Sit sed error quasi voluptatem velit voluptas aut. Aut debitis eveniet. Praesentium dolores quia voluptate vero quis dicta quasi vel. Aut voluptas accusantium ut aut quidem consectetur itaque laboriosam rerum.', - id: 2, - meta: { - author: { - avatar: { - alt: 'Author 2 avatar', - height: 480, - src: 'http://picsum.photos/640/480', - width: 640, - }, - name: 'Author 2', - website: '#', - }, - date: '2021-04-03 23:30:20', - }, - parentId: 0, - replies: [ - { - approved: true, - content: - 'Vel ullam in porro tempore. Maiores quos quia magnam beatae nemo libero velit numquam. Sapiente aliquid cumque. Velit neque in adipisci aut assumenda voluptates earum. Autem esse autem provident in tempore. Aut distinctio dolor qui repellat et et adipisci velit aspernatur.', - id: 4, - meta: { - author: { - avatar: { - alt: 'Author 4 avatar', - height: 480, - src: 'http://picsum.photos/640/480', - width: 640, - }, - name: 'Author 4', - }, - date: '2021-04-03 23:04:24', - }, - parentId: 2, - replies: [], - }, - { - approved: true, - content: - 'Sed non omnis. Quam porro est. Quae tempore quae. Exercitationem eos non velit voluptatem velit voluptas iusto. Sit debitis qui ipsam quo asperiores numquam veniam praesentium ut.', - id: 5, - meta: { - author: { - avatar: { - alt: 'Author 1 avatar', - height: 480, - src: 'http://picsum.photos/640/480', - width: 640, - }, - name: 'Author 1', - }, - date: '2021-04-04 08:05:14', - }, - parentId: 2, - replies: [], - }, - ], - }, - { - approved: false, - content: - 'Natus consequatur maiores aperiam dolore eius nesciunt ut qui et. Ab ea nobis est. Eaque dolor corrupti id aut. Impedit architecto autem qui neque rerum ab dicta dignissimos voluptates.', - id: 3, - meta: { - author: { - avatar: { - alt: 'Author 3', - height: 480, - src: 'http://picsum.photos/640/480', - width: 640, - }, - name: 'Author 3', - }, - date: '2021-09-13 13:24:54', - }, - parentId: 0, - replies: [], - }, -]; - -export const saveComment = () => undefined; diff --git a/src/components/organisms/layout/comments-list.stories.tsx b/src/components/organisms/layout/comments-list.stories.tsx deleted file mode 100644 index c1a262e..0000000 --- a/src/components/organisms/layout/comments-list.stories.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import { CommentsList } from './comments-list'; -import { comments } from './comments-list.fixture'; - -const saveComment = async () => { - /** Do nothing. */ -}; - -/** - * CommentsList - Storybook Meta - */ -export default { - title: 'Organisms/Layout/CommentsList', - component: CommentsList, - args: { - saveComment, - }, - argTypes: { - comments: { - control: { - type: null, - }, - description: 'An array of comments.', - type: { - name: 'object', - required: true, - value: {}, - }, - }, - depth: { - control: { - type: 'number', - min: 0, - max: 4, - }, - description: 'The maximum depth. Use `0` to not display nested comments.', - type: { - name: 'number', - required: true, - }, - }, - onSubmit: { - control: { - type: null, - }, - description: 'A callback function to save the comment form data.', - table: { - category: 'Events', - }, - type: { - name: 'function', - required: true, - }, - }, - }, -} as ComponentMeta<typeof CommentsList>; - -const Template: ComponentStory<typeof CommentsList> = (args) => ( - <CommentsList {...args} /> -); - -/** - * Layout Stories - Without child comments - */ -export const WithoutChildComments = Template.bind({}); -WithoutChildComments.args = { - comments, - depth: 0, -}; - -/** - * Layout Stories - With child comments - */ -export const WithChildComments = Template.bind({}); -WithChildComments.args = { - comments, - depth: 1, -}; diff --git a/src/components/organisms/layout/comments-list.test.tsx b/src/components/organisms/layout/comments-list.test.tsx deleted file mode 100644 index 2a05204..0000000 --- a/src/components/organisms/layout/comments-list.test.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, it } from '@jest/globals'; -import { render } from '../../../../tests/utils'; -import { CommentsList } from './comments-list'; -import { comments, saveComment } from './comments-list.fixture'; - -describe('CommentsList', () => { - it('renders a comments list', () => { - render( - <CommentsList comments={comments} depth={1} onSubmit={saveComment} /> - ); - }); -}); diff --git a/src/components/organisms/layout/comments-list.tsx b/src/components/organisms/layout/comments-list.tsx deleted file mode 100644 index 385aea9..0000000 --- a/src/components/organisms/layout/comments-list.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { useState, type FC, useCallback } from 'react'; -import { useIntl } from 'react-intl'; -import type { SingleComment } from '../../../types'; -import { Heading, List, ListItem } from '../../atoms'; -import { - ApprovedComment, - type CommentReplyHandler, - PendingComment, - ReplyCommentForm, - type ReplyCommentFormProps, -} from '../comment'; -import styles from './comments-list.module.scss'; - -// eslint-disable-next-line @typescript-eslint/no-magic-numbers -export type CommentsListDepth = 0 | 1 | 2 | 3 | 4; - -export type CommentsListProps = Pick<ReplyCommentFormProps, 'onSubmit'> & { - /** - * An array of comments. - */ - comments: SingleComment[]; - /** - * The maximum depth. Use `0` to not display nested comments. - */ - depth: CommentsListDepth; -}; - -/** - * CommentsList component - * - * Render a comments list. - */ -export const CommentsList: FC<CommentsListProps> = ({ - comments, - depth, - onSubmit, -}) => { - const [replyingTo, setReplyingTo] = useState<number | null>(null); - const intl = useIntl(); - const replyFormHeading = intl.formatMessage({ - defaultMessage: 'Leave a reply', - description: 'CommentsList: comment form title', - id: 'w8uLLF', - }); - - const handleReplyFormVisibility: CommentReplyHandler = useCallback((id) => { - setReplyingTo((prevId) => { - if (prevId === id) return null; - return id; - }); - }, []); - - /** - * Get each comment wrapped in a list item. - * - * @param {SingleComment[]} commentsList - An array of comments. - * @returns {JSX.Element[]} The list items. - */ - const getItems = ( - commentsList: SingleComment[], - startLevel: number - ): JSX.Element[] => { - const isLastLevel = startLevel === depth; - - return commentsList.map( - ({ approved, meta, replies, parentId, ...comment }) => { - const replyBtnLabel = - replyingTo === comment.id - ? intl.formatMessage({ - defaultMessage: 'Cancel reply', - description: 'CommentsList: cancel reply button', - id: 'uZj4QI', - }) - : intl.formatMessage({ - defaultMessage: 'Reply', - description: 'CommentsList: reply button', - id: 'Qa9twM', - }); - - return ( - <ListItem key={comment.id}> - {approved ? ( - <> - <ApprovedComment - {...comment} - author={meta.author} - onReply={handleReplyFormVisibility} - publicationDate={meta.date} - replyBtn={replyBtnLabel} - /> - {replyingTo === comment.id ? ( - <ReplyCommentForm - className={styles.reply} - heading={<Heading level={2}>{replyFormHeading}</Heading>} - onSubmit={onSubmit} - commentId={comment.id} - /> - ) : null} - </> - ) : ( - <PendingComment /> - )} - {replies.length && !isLastLevel ? ( - <List hideMarker isOrdered spacing="sm"> - {getItems(replies, startLevel + 1)} - </List> - ) : null} - </ListItem> - ); - } - ); - }; - - return ( - <List hideMarker isOrdered spacing="sm"> - {getItems(comments, 0)} - </List> - ); -}; diff --git a/src/components/organisms/layout/index.ts b/src/components/organisms/layout/index.ts index 55a9357..ebe48e7 100644 --- a/src/components/organisms/layout/index.ts +++ b/src/components/organisms/layout/index.ts @@ -1,4 +1,3 @@ -export * from './comments-list'; export * from './no-results'; export * from './overview'; export * from './posts-list'; diff --git a/src/components/templates/page/page-layout.stories.tsx b/src/components/templates/page/page-layout.stories.tsx index 05b47da..4086fcd 100644 --- a/src/components/templates/page/page-layout.stories.tsx +++ b/src/components/templates/page/page-layout.stories.tsx @@ -1,7 +1,6 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import { ButtonLink, Heading, Link } from '../../atoms'; import { LinksListWidget, PostsList, Sharing } from '../../organisms'; -import { comments } from '../../organisms/layout/comments-list.fixture'; import { posts } from '../../organisms/layout/posts-list.fixture'; import { LayoutBase } from '../layout/layout.stories'; import { PageLayout as PageLayoutComponent } from './page-layout'; @@ -346,7 +345,108 @@ Post.args = { />, ], withToC: true, - comments, + comments: [ + { + author: { + name: 'Milan0', + avatar: { + alt: 'Milan0 avatar', + src: 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/976.jpg', + }, + }, + content: 'Fugit veniam quas qui dolor explicabo.', + id: 1, + isApproved: true, + publicationDate: '2023-01-23', + replies: [ + { + author: { name: 'Haskell42' }, + content: 'Error quas accusamus nesciunt enim quae a.', + id: 25, + isApproved: true, + publicationDate: '2023-02-04', + }, + { + author: { + name: 'Hanna49', + website: 'https://www.armandphilippot.com', + }, + content: 'Ut ducimus neque aliquam soluta sed totam commodi cum sit.', + id: 30, + isApproved: true, + publicationDate: '2023-03-10', + }, + ], + }, + { + author: { + name: 'Corrine9', + avatar: { + alt: 'Corrine9 avatar', + src: 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/539.jpg', + }, + }, + content: + 'Dolore hic iure voluptatum quam error minima. Quas ut aperiam sit commodi cumque consequatur. Voluptas debitis veritatis officiis in voluptas ea et laborum animi. Voluptatem qui enim neque. Et sunt quo neque assumenda iure. Non vel ut consectetur.', + id: 2, + isApproved: true, + publicationDate: '2023-04-20', + }, + { + author: { name: 'Presley12' }, + content: + 'Nulla eaque similique recusandae enim aut eligendi iure consequatur. Et aut qui. Voluptatem a voluptatem consequatur aliquid distinctio ex culpa. Adipisci animi amet reprehenderit autem quia commodi voluptatum commodi.', + id: 3, + isApproved: true, + publicationDate: '2023-05-01', + replies: [ + { + author: { + name: 'Ana_Haley33', + avatar: { + alt: 'Ana_Haley33 avatar', + src: 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/881.jpg', + }, + }, + content: 'Ab ea et fugit autem.', + id: 17, + isApproved: true, + publicationDate: '2023-05-01', + }, + { + author: { name: 'Santos.Harris17' }, + content: + 'Illo dolores voluptatem similique voluptas quasi hic aspernatur ab nisi.', + id: 18, + isApproved: false, + publicationDate: '2023-05-02', + }, + ], + }, + { + author: { name: 'Julius.Borer' }, + content: + 'Ea fugit totam et voluptatum quidem laborum explicabo fuga quod.', + id: 4, + isApproved: true, + publicationDate: '2023-06-15', + }, + { + author: { name: 'Geo87' }, + content: + 'Enim consequatur deleniti aliquid adipisci. Et mollitia saepe vel rerum totam praesentium assumenda repellat fuga. Ipsum ut architecto consequatur. Ut laborum suscipit sed corporis quas aliquid. Et et omnis quo. Dolore quia ipsum ut corporis eum et corporis qui.', + id: 5, + isApproved: false, + publicationDate: '2023-06-16', + }, + { + author: { name: 'Kurt.Keeling' }, + content: 'Eligendi repellat officiis amet.', + id: 6, + isApproved: true, + publicationDate: '2023-06-17', + }, + ], allowComments: true, }; diff --git a/src/components/templates/page/page-layout.test.tsx b/src/components/templates/page/page-layout.test.tsx index 394f995..c7d7a65 100644 --- a/src/components/templates/page/page-layout.test.tsx +++ b/src/components/templates/page/page-layout.test.tsx @@ -1,7 +1,6 @@ import { describe, expect, it } from '@jest/globals'; import type { BreadcrumbList } from 'schema-dts'; import { render, screen as rtlScreen } from '../../../../tests/utils'; -import { comments } from '../../organisms/layout/comments-list.fixture'; import { PageLayout } from './page-layout'; const title = 'Incidunt ad earum'; @@ -94,7 +93,15 @@ describe('PageLayout', () => { breadcrumbSchema={breadcrumbSchema} title={title} allowComments={true} - comments={comments} + comments={[ + { + author: { name: 'Burley40' }, + content: 'Veritatis praesentium non autem ut.', + id: 1, + isApproved: true, + publicationDate: '2023-11-02', + }, + ]} > {children} </PageLayout> diff --git a/src/components/templates/page/page-layout.tsx b/src/components/templates/page/page-layout.tsx index 434b8ff..28c850b 100644 --- a/src/components/templates/page/page-layout.tsx +++ b/src/components/templates/page/page-layout.tsx @@ -9,7 +9,7 @@ import { import { useIntl } from 'react-intl'; import type { BreadcrumbList } from 'schema-dts'; import { sendComment } from '../../../services/graphql'; -import type { SendCommentInput, SingleComment } from '../../../types'; +import type { SendCommentInput } from '../../../types'; import { useIsMounted } from '../../../utils/hooks'; import { Heading, Sidebar } from '../../atoms'; import { @@ -29,16 +29,6 @@ import { } from '../../organisms'; import styles from './page-layout.module.scss'; -/** - * Check if there is at least one comment. - * - * @param {SingleComment[] | undefined} comments - The comments. - */ -const hasComments = ( - comments: SingleComment[] | undefined -): comments is SingleComment[] => - Array.isArray(comments) && comments.length > 0; - export type PageLayoutProps = { /** * True if the page accepts new comments. Default: false. @@ -262,7 +252,7 @@ export const PageLayout: FC<PageLayoutProps> = ({ <Heading className={styles.comments__title} level={2}> {commentsTitle} </Heading> - {hasComments(comments) ? ( + {comments?.length ? ( <CommentsList comments={comments} depth={2} diff --git a/src/i18n/en.json b/src/i18n/en.json index 3e6bf93..6d7af16 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -515,6 +515,10 @@ "defaultMessage": "Home", "description": "Layout: main nav - home link" }, + "c1Ju/q": { + "defaultMessage": "Leave a reply to {name}", + "description": "CommentsList: comment form title" + }, "c556Qo": { "defaultMessage": "Sidebar", "description": "PageLayout: accessible name for the sidebar" @@ -751,10 +755,6 @@ "defaultMessage": "Free", "description": "HomePage: link to free thematic" }, - "w8uLLF": { - "defaultMessage": "Leave a reply", - "description": "CommentsList: comment form title" - }, "wQrvgw": { "defaultMessage": "Updated on:", "description": "ProjectsPage: update date label" diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 9244863..27a3c8a 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -515,6 +515,10 @@ "defaultMessage": "Accueil", "description": "Layout: main nav - home link" }, + "c1Ju/q": { + "defaultMessage": "Laisser une réponse à {name}", + "description": "CommentsList: comment form title" + }, "c556Qo": { "defaultMessage": "Barre latérale", "description": "PageLayout: accessible name for the sidebar" @@ -751,10 +755,6 @@ "defaultMessage": "Libre", "description": "HomePage: link to free thematic" }, - "w8uLLF": { - "defaultMessage": "Laisser une réponse", - "description": "CommentsList: comment form title" - }, "wQrvgw": { "defaultMessage": "Mis à jour le :", "description": "ProjectsPage: update date label" diff --git a/src/pages/article/[slug].tsx b/src/pages/article/[slug].tsx index 449af8d..d35541a 100644 --- a/src/pages/article/[slug].tsx +++ b/src/pages/article/[slug].tsx @@ -17,6 +17,7 @@ import { Spinner, type MetaItemData, Time, + type CommentData, } from '../../components'; import { getAllArticlesSlugs, @@ -64,6 +65,19 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({ contentId: article?.id, fallback: comments, }); + + const getComments = (data?: SingleComment[]) => + data?.map((comment): CommentData => { + return { + author: comment.meta.author, + content: comment.content, + id: comment.id, + isApproved: comment.approved, + publicationDate: comment.meta.date, + replies: getComments(comment.replies), + }; + }); + const { items: breadcrumbItems, schema: breadcrumbSchema } = useBreadcrumb({ title: article?.title ?? '', url: `${ROUTES.ARTICLE}/${slug}`, @@ -313,7 +327,7 @@ const ArticlePage: NextPageWithLayout<ArticlePageProps> = ({ bodyClassName={styles.body} breadcrumb={breadcrumbItems} breadcrumbSchema={breadcrumbSchema} - comments={commentsData} + comments={getComments(commentsData)} footerMeta={footerMeta} headerMeta={filteredHeaderMeta} id={id as number} |
