diff options
Diffstat (limited to 'src/components/organisms/layout')
| -rw-r--r-- | src/components/organisms/layout/comment.fixture.ts | 35 | ||||
| -rw-r--r-- | src/components/organisms/layout/comment.module.scss | 78 | ||||
| -rw-r--r-- | src/components/organisms/layout/comment.stories.tsx | 115 | ||||
| -rw-r--r-- | src/components/organisms/layout/comment.test.tsx | 44 | ||||
| -rw-r--r-- | src/components/organisms/layout/comment.tsx | 189 | ||||
| -rw-r--r-- | src/components/organisms/layout/comments-list.fixture.ts | 2 | ||||
| -rw-r--r-- | src/components/organisms/layout/comments-list.module.scss | 3 | ||||
| -rw-r--r-- | src/components/organisms/layout/comments-list.test.tsx | 3 | ||||
| -rw-r--r-- | src/components/organisms/layout/comments-list.tsx | 88 | ||||
| -rw-r--r-- | src/components/organisms/layout/index.ts | 1 |
10 files changed, 80 insertions, 478 deletions
diff --git a/src/components/organisms/layout/comment.fixture.ts b/src/components/organisms/layout/comment.fixture.ts deleted file mode 100644 index 84aa20e..0000000 --- a/src/components/organisms/layout/comment.fixture.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { UserCommentProps } from './comment'; - -export const author = { - avatar: { - alt: 'Author avatar', - height: 480, - src: 'http://placeimg.com/640/480', - width: 640, - }, - name: 'Armand', - website: 'https://www.armandphilippot.com/', -}; - -export 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.'; - -export const date = '2021-04-03 23:04:24'; - -export const meta = { - author, - date, -}; - -export const id = 5; - -export const saveComment = () => undefined; - -export const data: UserCommentProps = { - approved: true, - content, - id, - meta, - parentId: 0, - onSubmit: saveComment, -}; diff --git a/src/components/organisms/layout/comment.module.scss b/src/components/organisms/layout/comment.module.scss deleted file mode 100644 index 096f4c4..0000000 --- a/src/components/organisms/layout/comment.module.scss +++ /dev/null @@ -1,78 +0,0 @@ -@use "../../../styles/abstracts/functions" as fun; -@use "../../../styles/abstracts/mixins" as mix; -@use "../../../styles/abstracts/placeholders"; - -.avatar { - img { - border-radius: fun.convert-px(3); - box-shadow: - 0 0 0 fun.convert-px(1) var(--color-shadow-light), - fun.convert-px(2) fun.convert-px(2) 0 fun.convert-px(1) - var(--color-shadow); - } -} - -.author { - color: var(--color-primary-darker); - font-family: var(--font-family-regular); - font-size: var(--font-size-md); - font-weight: 600; - text-align: center; - 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; - } - } - } -} - -.form { - &__wrapper { - margin-top: var(--spacing-sm); - } - - &__heading { - width: fit-content; - margin: 0 auto var(--spacing-md) auto; - } - - margin-inline: auto; -} diff --git a/src/components/organisms/layout/comment.stories.tsx b/src/components/organisms/layout/comment.stories.tsx deleted file mode 100644 index 7426fc3..0000000 --- a/src/components/organisms/layout/comment.stories.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import { UserComment } from './comment'; -import { data } from './comment.fixture'; - -const saveComment = async () => { - /** Do nothing. */ -}; - -/** - * Comment - Storybook Meta - */ -export default { - title: 'Organisms/Layout/Comment', - component: UserComment, - args: { - canReply: true, - onSubmit: saveComment, - }, - argTypes: { - author: { - description: 'The author data.', - type: { - name: 'object', - required: true, - value: {}, - }, - }, - canReply: { - control: { - type: 'boolean', - }, - description: 'Enable or disable the reply button.', - table: { - category: 'Options', - defaultValue: { summary: true }, - }, - type: { - name: 'boolean', - required: false, - }, - }, - content: { - control: { - type: 'text', - }, - description: 'The comment body.', - type: { - name: 'string', - required: true, - }, - }, - id: { - control: { - type: 'number', - }, - description: 'The comment id.', - type: { - name: 'number', - required: true, - }, - }, - parentId: { - control: { - type: null, - }, - description: 'The parent id if it is a reply.', - type: { - name: 'number', - required: false, - }, - }, - publication: { - description: 'The publication date.', - type: { - name: 'object', - required: true, - value: {}, - }, - }, - 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 UserComment>; - -const Template: ComponentStory<typeof UserComment> = (args) => ( - <UserComment {...args} /> -); - -/** - * Layout Stories - Approved - */ -export const Approved = Template.bind({}); -Approved.args = { - ...data, -}; - -/** - * Layout Stories - Unapproved - */ -export const Unapproved = Template.bind({}); -Unapproved.args = { - ...data, - approved: false, -}; diff --git a/src/components/organisms/layout/comment.test.tsx b/src/components/organisms/layout/comment.test.tsx deleted file mode 100644 index 0e0ea3a..0000000 --- a/src/components/organisms/layout/comment.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { render, screen as rtlScreen } from '../../../../tests/utils'; -import { UserComment } from './comment'; -import { author, data, id } from './comment.fixture'; - -describe('UserComment', () => { - it('renders an avatar', () => { - render(<UserComment canReply={true} {...data} />); - expect( - rtlScreen.getByRole('img', { name: author.avatar.alt }) - ).toBeInTheDocument(); - }); - - it('renders the author website url', () => { - render(<UserComment canReply={true} {...data} />); - expect(rtlScreen.getByRole('link', { name: author.name })).toHaveAttribute( - 'href', - author.website - ); - }); - - it('renders a permalink to the comment', () => { - render(<UserComment canReply={true} {...data} />); - expect( - rtlScreen.getByRole('link', { - name: /\sat\s/, - }) - ).toHaveAttribute('href', `#comment-${id}`); - }); - - it('renders a reply button', () => { - render(<UserComment canReply={true} {...data} />); - expect( - rtlScreen.getByRole('button', { name: 'Reply' }) - ).toBeInTheDocument(); - }); - - it('does not render a reply button', () => { - render(<UserComment canReply={false} {...data} />); - expect( - rtlScreen.queryByRole('button', { name: 'Reply' }) - ).not.toBeInTheDocument(); - }); -}); diff --git a/src/components/organisms/layout/comment.tsx b/src/components/organisms/layout/comment.tsx deleted file mode 100644 index b55bb3d..0000000 --- a/src/components/organisms/layout/comment.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import NextImage from 'next/image'; -import Script from 'next/script'; -import type { FC } from 'react'; -import { useIntl } from 'react-intl'; -import type { Comment as CommentSchema, WithContext } from 'schema-dts'; -import type { SingleComment } from '../../../types'; -import { useSettings, useToggle } from '../../../utils/hooks'; -import { Button, Heading, Link, Time } from '../../atoms'; -import { - Card, - CardActions, - CardBody, - CardCover, - CardFooter, - CardHeader, - CardMeta, - CardTitle, -} from '../../molecules'; -import { CommentForm, type CommentFormProps } from '../forms'; -import styles from './comment.module.scss'; - -export type UserCommentProps = Pick< - SingleComment, - 'approved' | 'content' | 'id' | 'meta' | 'parentId' -> & - Pick<CommentFormProps, 'onSubmit'> & { - /** - * Enable or disable the reply button. Default: true. - */ - canReply?: boolean; - }; - -/** - * UserComment component - * - * Render a single comment. - */ -export const UserComment: FC<UserCommentProps> = ({ - approved, - canReply = true, - content, - id, - meta, - onSubmit, - parentId, - ...props -}) => { - const intl = useIntl(); - const { website } = useSettings(); - const [isReplying, toggleIsReplying] = useToggle(false); - - if (!approved) { - return ( - <div className={styles.wrapper}> - {intl.formatMessage({ - defaultMessage: 'This comment is awaiting moderation...', - description: 'Comment: awaiting moderation', - id: '6a1Uo6', - })} - </div> - ); - } - - const { author, date } = meta; - - const buttonLabel = isReplying - ? intl.formatMessage({ - defaultMessage: 'Cancel reply', - description: 'Comment: cancel reply button', - id: 'LCorTC', - }) - : intl.formatMessage({ - defaultMessage: 'Reply', - description: 'Comment: reply button', - id: 'hzHuCc', - }); - const formTitle = intl.formatMessage({ - defaultMessage: 'Leave a reply', - description: 'Comment: comment form title', - id: '2fD5CI', - }); - - const commentSchema: WithContext<CommentSchema> = { - '@context': 'https://schema.org', - '@id': `${website.url}/#comment-${id}`, - '@type': 'Comment', - parentItem: parentId - ? { '@id': `${website.url}/#comment-${parentId}` } - : undefined, - about: { '@type': 'Article', '@id': `${website.url}/#article` }, - author: { - '@type': 'Person', - name: author.name, - image: author.avatar?.src, - url: author.website, - }, - creator: { - '@type': 'Person', - name: author.name, - image: author.avatar?.src, - url: author.website, - }, - dateCreated: date, - datePublished: date, - text: content, - }; - - return ( - <> - <Script - dangerouslySetInnerHTML={{ __html: JSON.stringify(commentSchema) }} - id="schema-comments" - type="application/ld+json" - /> - <Card - cover={ - author.avatar ? ( - <CardCover className={styles.avatar}> - <NextImage - {...props} - alt={author.avatar.alt} - fill - src={author.avatar.src} - style={{ objectFit: 'cover' }} - /> - </CardCover> - ) : undefined - } - id={`comment-${id}`} - 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: intl.formatMessage({ - defaultMessage: 'Published on:', - description: 'Comment: publication date label', - id: 'soj7do', - }), - value: ( - <Link href={`#comment-${id}`}> - <Time date={date} showTime /> - </Link> - ), - }, - ]} - /> - </CardHeader> - <CardBody - className={styles.body} - dangerouslySetInnerHTML={{ __html: content }} - /> - {canReply ? ( - <CardFooter> - <CardActions> - <Button kind="tertiary" onClick={toggleIsReplying}> - {buttonLabel} - </Button> - </CardActions> - </CardFooter> - ) : null} - </Card> - {isReplying ? ( - <Card className={styles.form__wrapper} variant={2}> - <CardBody> - <Heading className={styles.form__heading} level={2}> - {formTitle} - </Heading> - <CommentForm - className={styles.form} - onSubmit={onSubmit} - parentId={id} - /> - </CardBody> - </Card> - ) : null} - </> - ); -}; diff --git a/src/components/organisms/layout/comments-list.fixture.ts b/src/components/organisms/layout/comments-list.fixture.ts index 30a4f11..c6a1891 100644 --- a/src/components/organisms/layout/comments-list.fixture.ts +++ b/src/components/organisms/layout/comments-list.fixture.ts @@ -104,3 +104,5 @@ export const comments: SingleComment[] = [ replies: [], }, ]; + +export const saveComment = () => undefined; diff --git a/src/components/organisms/layout/comments-list.module.scss b/src/components/organisms/layout/comments-list.module.scss new file mode 100644 index 0000000..e690250 --- /dev/null +++ b/src/components/organisms/layout/comments-list.module.scss @@ -0,0 +1,3 @@ +.reply { + margin-top: var(--spacing-sm); +} diff --git a/src/components/organisms/layout/comments-list.test.tsx b/src/components/organisms/layout/comments-list.test.tsx index d7e170c..2a05204 100644 --- a/src/components/organisms/layout/comments-list.test.tsx +++ b/src/components/organisms/layout/comments-list.test.tsx @@ -1,8 +1,7 @@ import { describe, it } from '@jest/globals'; import { render } from '../../../../tests/utils'; -import { saveComment } from './comment.fixture'; import { CommentsList } from './comments-list'; -import { comments } from './comments-list.fixture'; +import { comments, saveComment } from './comments-list.fixture'; describe('CommentsList', () => { it('renders a comments list', () => { diff --git a/src/components/organisms/layout/comments-list.tsx b/src/components/organisms/layout/comments-list.tsx index 2d43583..385aea9 100644 --- a/src/components/organisms/layout/comments-list.tsx +++ b/src/components/organisms/layout/comments-list.tsx @@ -1,12 +1,20 @@ -import type { FC } from 'react'; +import { useState, type FC, useCallback } from 'react'; +import { useIntl } from 'react-intl'; import type { SingleComment } from '../../../types'; -import { List, ListItem } from '../../atoms'; -import { UserComment, type UserCommentProps } from './comment'; +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<UserCommentProps, 'onSubmit'> & { +export type CommentsListProps = Pick<ReplyCommentFormProps, 'onSubmit'> & { /** * An array of comments. */ @@ -27,6 +35,21 @@ export const CommentsList: FC<CommentsListProps> = ({ 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. * @@ -39,16 +62,53 @@ export const CommentsList: FC<CommentsListProps> = ({ ): JSX.Element[] => { const isLastLevel = startLevel === depth; - return commentsList.map(({ replies, ...comment }) => ( - <ListItem key={comment.id}> - <UserComment canReply={!isLastLevel} onSubmit={onSubmit} {...comment} /> - {replies.length && !isLastLevel ? ( - <List hideMarker isOrdered spacing="sm"> - {getItems(replies, startLevel + 1)} - </List> - ) : null} - </ListItem> - )); + 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 ( diff --git a/src/components/organisms/layout/index.ts b/src/components/organisms/layout/index.ts index 552ed27..55a9357 100644 --- a/src/components/organisms/layout/index.ts +++ b/src/components/organisms/layout/index.ts @@ -1,4 +1,3 @@ -export * from './comment'; export * from './comments-list'; export * from './no-results'; export * from './overview'; |
