diff options
| author | Armand Philippot <git@armandphilippot.com> | 2023-11-07 16:55:58 +0100 |
|---|---|---|
| committer | Armand Philippot <git@armandphilippot.com> | 2023-11-11 18:15:27 +0100 |
| commit | bd9c9ae7e2ae973969569dd434836de9f38b07d4 (patch) | |
| tree | 84905097c4f2c2db36794c20910e3893189a65e1 /src/components/organisms/comment/approved-comment | |
| parent | c9c1c90b30e243563bb4f731da15b3fe657556d2 (diff) | |
refactor(components): split Comment component into 3 components
* add ApprovedComment, PendingComment and ReplyCommentForm components
* let consumer handle reply form visibility
* move structured data into article page (each article already has the
comments data and already handle json ltd schema so I prefered to move
the schema in the final consumer instead of adding a script element
foreach comment)
Diffstat (limited to 'src/components/organisms/comment/approved-comment')
5 files changed, 464 insertions, 0 deletions
diff --git a/src/components/organisms/comment/approved-comment/approved-comment.module.scss b/src/components/organisms/comment/approved-comment/approved-comment.module.scss new file mode 100644 index 0000000..7906632 --- /dev/null +++ b/src/components/organisms/comment/approved-comment/approved-comment.module.scss @@ -0,0 +1,52 @@ +@use "../../../../styles/abstracts/placeholders"; + +.author { + color: var(--color-primary-darker); + font-family: var(--font-family-regular); + font-size: var(--font-size-md); + font-weight: 600; + text-shadow: none; +} + +.body { + overflow-wrap: break-word; + + :global { + a { + @extend %link; + + &[hreflang], + &.download, + &.external { + @extend %link-with-icon; + } + + &[hreflang] { + @extend %link-with-lang; + } + + &[hreflang]:not(.download, .external) { + --is-icon-hidden: ""; + } + + &.download { + @extend %download-link; + } + + &.external { + @extend %external-link; + } + + &.download, + &.external { + &:not([hreflang]) { + --is-lang-hidden: ""; + } + } + + &.external.download { + @extend %external-download-link; + } + } + } +} diff --git a/src/components/organisms/comment/approved-comment/approved-comment.stories.tsx b/src/components/organisms/comment/approved-comment/approved-comment.stories.tsx new file mode 100644 index 0000000..36afa6b --- /dev/null +++ b/src/components/organisms/comment/approved-comment/approved-comment.stories.tsx @@ -0,0 +1,126 @@ +import type { ComponentMeta, ComponentStory } from '@storybook/react'; +import { ApprovedComment } from './approved-comment'; + +/** + * ApprovedComment - Storybook Meta + */ +export default { + title: 'Organisms/Comment/ApprovedComment', + component: ApprovedComment, + argTypes: { + author: { + description: 'The author data.', + type: { + name: 'object', + required: true, + value: {}, + }, + }, + content: { + control: { + type: 'text', + }, + description: 'The comment body.', + type: { + name: 'string', + required: true, + }, + }, + id: { + control: { + type: 'string', + }, + description: 'The comment id.', + type: { + name: 'string', + required: true, + }, + }, + publicationDate: { + control: { + type: 'text', + }, + description: 'The publication date.', + type: { + name: 'string', + required: true, + }, + }, + replyBtn: { + control: { + type: null, + }, + description: 'Add a reply button.', + type: { + name: 'function', + required: false, + }, + }, + }, +} as ComponentMeta<typeof ApprovedComment>; + +const Template: ComponentStory<typeof ApprovedComment> = (args) => ( + <ApprovedComment {...args} /> +); + +/** + * ApprovedComment Stories - Default + */ +export const Default = Template.bind({}); +Default.args = { + author: { + name: 'Kameron.Conn', + }, + content: + 'Quia est eos deserunt qui perferendis est pariatur eaque. Deserunt omnis quis consectetur ea quam a cupiditate. Velit laboriosam rem nihil numquam quia.', + id: 1, + publicationDate: '2023-11-06', +}; + +/** + * ApprovedComment Stories - WithAvatar + */ +export const WithAvatar = Template.bind({}); +WithAvatar.args = { + author: { + avatar: { + alt: 'Kameron.Conn avatar', + src: 'https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/82.jpg', + }, + name: 'Kameron.Conn', + }, + content: + 'Quia est eos deserunt qui perferendis est pariatur eaque. Deserunt omnis quis consectetur ea quam a cupiditate. Velit laboriosam rem nihil numquam quia.', + id: 2, + publicationDate: '2023-11-06', +}; + +/** + * ApprovedComment Stories - WithWebsite + */ +export const WithWebsite = Template.bind({}); +WithWebsite.args = { + author: { + name: 'Kameron.Conn', + website: 'https://www.armandphilippot.com/', + }, + content: + 'Quia est eos deserunt qui perferendis est pariatur eaque. Deserunt omnis quis consectetur ea quam a cupiditate. Velit laboriosam rem nihil numquam quia.', + id: 3, + publicationDate: '2023-11-06', +}; + +/** + * ApprovedComment Stories - WithReplyBtn + */ +export const WithReplyBtn = Template.bind({}); +WithReplyBtn.args = { + author: { + name: 'Kameron.Conn', + }, + content: + 'Quia est eos deserunt qui perferendis est pariatur eaque. Deserunt omnis quis consectetur ea quam a cupiditate. Velit laboriosam rem nihil numquam quia.', + id: 4, + publicationDate: '2023-11-06', + replyBtn: 'Reply', +}; diff --git a/src/components/organisms/comment/approved-comment/approved-comment.test.tsx b/src/components/organisms/comment/approved-comment/approved-comment.test.tsx new file mode 100644 index 0000000..2e29b5f --- /dev/null +++ b/src/components/organisms/comment/approved-comment/approved-comment.test.tsx @@ -0,0 +1,108 @@ +import { describe, expect, it } from '@jest/globals'; +import { userEvent } from '@testing-library/user-event'; +import { render, screen as rtlScreen } from '../../../../../tests/utils'; +import { ApprovedComment, type CommentAuthor } from './approved-comment'; + +describe('ApprovedComment', () => { + const user = userEvent.setup(); + + it('renders the author, the publication date, the comment and a permalink', () => { + const author = { + name: 'Delbert_Jacobi45', + } satisfies CommentAuthor; + const content = 'Repellat ab non et.'; + const id = 1; + const publicationDate = '2023'; + + render( + <ApprovedComment + author={author} + content={content} + id={id} + publicationDate={publicationDate} + /> + ); + + expect(rtlScreen.getByText(author.name)).toBeInTheDocument(); + expect(rtlScreen.getByText(content)).toBeInTheDocument(); + expect( + rtlScreen.getByText(new RegExp(publicationDate)) + ).toBeInTheDocument(); + expect(rtlScreen.getByRole('link')).toHaveAttribute( + 'href', + `#comment-${id}` + ); + }); + + it('can render the author avatar', () => { + const author = { + avatar: { + alt: 'enim ut maiores', + src: 'https://picsum.photos/640/480', + }, + name: 'Sandra82', + } satisfies CommentAuthor; + + render( + <ApprovedComment + author={author} + content="Ab qui aliquam esse." + id={2} + publicationDate="2022-11-03" + /> + ); + + expect(rtlScreen.getByRole('img')).toHaveAccessibleName(author.avatar.alt); + }); + + it('can render a link to the author website', () => { + const author = { + name: 'Esmeralda51', + website: 'http://example.net', + } satisfies CommentAuthor; + + render( + <ApprovedComment + author={author} + content="Ab qui aliquam esse." + id={2} + publicationDate="2022-11-03" + /> + ); + + expect(rtlScreen.getByRole('link', { name: author.name })).toHaveAttribute( + 'href', + author.website + ); + }); + + it('can render a reply button', async () => { + const id = 6; + const replyBtn = 'dolore recusandae voluptas'; + const handleReply = jest.fn((_id: number) => { + // do nothing + }); + + render( + <ApprovedComment + author={{ name: 'Kurtis5' }} + content="Ab qui aliquam esse." + id={id} + onReply={handleReply} + publicationDate="2022-11-03" + replyBtn={replyBtn} + /> + ); + + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + expect.assertions(4); + + expect(rtlScreen.getByRole('button')).toHaveTextContent(replyBtn); + expect(handleReply).not.toHaveBeenCalled(); + + await user.click(rtlScreen.getByRole('button')); + + expect(handleReply).toHaveBeenCalledTimes(1); + expect(handleReply).toHaveBeenCalledWith(id); + }); +}); diff --git a/src/components/organisms/comment/approved-comment/approved-comment.tsx b/src/components/organisms/comment/approved-comment/approved-comment.tsx new file mode 100644 index 0000000..db5345b --- /dev/null +++ b/src/components/organisms/comment/approved-comment/approved-comment.tsx @@ -0,0 +1,177 @@ +import NextImage from 'next/image'; +import { type ForwardRefRenderFunction, forwardRef, useCallback } from 'react'; +import { useIntl } from 'react-intl'; +import { Button, Link, Time } from '../../../atoms'; +import { + Card, + CardBody, + CardCover, + CardHeader, + CardMeta, + type CardProps, + CardTitle, + CardFooter, + CardActions, +} from '../../../molecules'; +import styles from './approved-comment.module.scss'; + +export type CommentAuthorAvatar = { + /** + * The alternative text for the avatar. + */ + alt: string; + /** + * The avatar url. + */ + src: string; +}; + +export type CommentAuthor = { + /** + * The author avatar. + */ + avatar?: CommentAuthorAvatar; + /** + * The author name. + */ + name: string; + /** + * The author website. + */ + website?: string; +}; + +export type CommentReplyHandler = (id: number) => void | Promise<void>; + +export type ApprovedCommentProps = Omit< + CardProps<undefined>, + | 'children' + | 'content' + | 'cover' + | 'id' + | 'isCentered' + | 'linkTo' + | 'meta' + | 'variant' +> & { + /** + * The author data. + */ + author: CommentAuthor; + /** + * The comment. + */ + content: string; + /** + * The comment id. + */ + id: number; + /** + * A callback function to handle reply. + */ + onReply?: CommentReplyHandler; + /** + * The publication date of the comment. + */ + publicationDate: string; + /** + * Add a reply button to the comment by providing a label. + */ + replyBtn?: string; +}; + +const ApprovedCommentWithRef: ForwardRefRenderFunction< + HTMLDivElement, + ApprovedCommentProps +> = ( + { + author, + className = '', + content, + id, + onReply, + publicationDate, + replyBtn, + ...props + }, + ref +) => { + const intl = useIntl(); + const commentClass = `${className}`; + const commentId = `comment-${id}`; + const commentLink = `#${commentId}`; + const publicationDateLabel = intl.formatMessage({ + defaultMessage: 'Published on:', + description: 'ApprovedComment: publication date label', + id: 'NzeU3V', + }); + + const handleReply = useCallback(() => { + if (onReply) onReply(id); + }, [id, onReply]); + + return ( + <Card + {...props} + className={commentClass} + cover={ + author.avatar ? ( + <CardCover hasBorders> + <NextImage + alt={author.avatar.alt} + height={96} + src={author.avatar.src} + width={96} + /> + </CardCover> + ) : undefined + } + id={commentId} + ref={ref} + variant={2} + > + <CardHeader> + <CardTitle className={styles.author} isFake> + {author.website ? ( + <Link href={author.website}>{author.name}</Link> + ) : ( + author.name + )} + </CardTitle> + <CardMeta + hasInlinedItems + items={[ + { + id: 'publication-date', + label: publicationDateLabel, + value: ( + <Link href={commentLink}> + <Time date={publicationDate} showTime /> + </Link> + ), + }, + ]} + /> + </CardHeader> + <CardBody + className={styles.body} + dangerouslySetInnerHTML={{ __html: content }} + /> + {replyBtn ? ( + <CardFooter> + <CardActions> + <Button + // eslint-disable-next-line react/jsx-no-literals + kind="tertiary" + onClick={handleReply} + > + {replyBtn} + </Button> + </CardActions> + </CardFooter> + ) : null} + </Card> + ); +}; + +export const ApprovedComment = forwardRef(ApprovedCommentWithRef); diff --git a/src/components/organisms/comment/approved-comment/index.ts b/src/components/organisms/comment/approved-comment/index.ts new file mode 100644 index 0000000..444b0c3 --- /dev/null +++ b/src/components/organisms/comment/approved-comment/index.ts @@ -0,0 +1 @@ +export * from './approved-comment'; |
