Skip to content

Commit c7b0304

Browse files
committed
refactor: comment display to the component library
1 parent 39fe0a1 commit c7b0304

28 files changed

+688
-594
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { fakeCommentSB } from '../utils'
2+
import { CommentDisplay } from './CommentDisplay'
3+
4+
import type { Meta, StoryFn } from '@storybook/react'
5+
6+
export default {
7+
title: 'Commenting/CommentDisplay',
8+
component: CommentDisplay,
9+
} as Meta<typeof CommentDisplay>
10+
11+
const itemType = 'CommentItem'
12+
const isEditable = false
13+
const setShowDeleteModal = () => {}
14+
const setShowEditModal = () => {}
15+
16+
export const Default: StoryFn<typeof CommentDisplay> = () => {
17+
const comment = fakeCommentSB()
18+
19+
return (
20+
<CommentDisplay
21+
comment={comment}
22+
itemType={itemType}
23+
isEditable={isEditable}
24+
setShowDeleteModal={setShowDeleteModal}
25+
setShowEditModal={setShowEditModal}
26+
/>
27+
)
28+
}
29+
30+
export const Editable: StoryFn<typeof CommentDisplay> = () => {
31+
const comment = fakeCommentSB()
32+
33+
return (
34+
<CommentDisplay
35+
comment={comment}
36+
itemType={itemType}
37+
isEditable={true}
38+
setShowDeleteModal={setShowDeleteModal}
39+
setShowEditModal={setShowEditModal}
40+
/>
41+
)
42+
}
43+
44+
export const Edited: StoryFn<typeof CommentDisplay> = () => {
45+
const comment = fakeCommentSB({ modifiedAt: new Date() })
46+
47+
return (
48+
<CommentDisplay
49+
comment={comment}
50+
itemType={itemType}
51+
isEditable={isEditable}
52+
setShowDeleteModal={setShowDeleteModal}
53+
setShowEditModal={setShowEditModal}
54+
/>
55+
)
56+
}
57+
58+
export const LongText: StoryFn<typeof CommentDisplay> = () => {
59+
const commentText = `Ut dignissim, odio a cursus pretium, erat ex dictum quam, a eleifend augue mauris vel metus. Suspendisse pellentesque, elit efficitur rutrum maximus, arcu enim congue ipsum, vel aliquam ipsum urna quis tellus. Mauris at imperdiet nisi. Integer at neque ex. Nullam vel ipsum sodales, porttitor nulla vitae, tincidunt est. Pellentesque vitae lectus arcu. Integer dapibus rutrum facilisis. Nullam tincidunt quam at arcu interdum, vitae egestas libero vehicula. Morbi metus tortor, dapibus id finibus ac, egestas quis leo. Phasellus scelerisque suscipit mauris sed rhoncus. In quis ultricies ipsum. Integer vitae iaculis risus, sit amet elementum augue. Pellentesque vitae sagittis erat, eget consectetur lorem.\n\nUt pharetra molestie quam id dictum. In molestie, arcu sit amet faucibus pulvinar, eros erat egestas leo, at molestie nunc velit a arcu. Aliquam erat volutpat. Vivamus vehicula mi sit amet nibh auctor efficitur. Duis fermentum sem et nibh facilisis, ut tincidunt sem commodo. Nullam ornare ex a elementum accumsan. Etiam a neque ut lacus suscipit blandit. Maecenas id tortor velit.\n\nInterdum et malesuada fames ac ante ipsum primis in faucibus. Nam ut commodo tellus. Maecenas at leo metus. Vivamus ullamcorper ex purus, volutpat auctor nunc lobortis a. Integer sit amet ornare nisi, sed ultrices enim. Pellentesque ut aliquam urna, eu fringilla ante. Nullam dui nibh, feugiat id vestibulum nec, efficitur a lorem. In vitae pellentesque tellus. Pellentesque sed odio iaculis, imperdiet turpis at, aliquam ex. Praesent iaculis bibendum nibh, vel egestas turpis ultrices ac. Praesent tincidunt libero sed gravida ornare. Aliquam vehicula risus ut molestie suscipit. Nunc erat odio, venenatis nec posuere in, placerat eget massa. Sed in ultrices ex, vel egestas quam. Integer lectus magna, ornare at nisl sed, convallis euismod enim. Cras pretium commodo arcu non bibendum.\n\nNullam dictum lectus felis. Duis vitae lacus vitae nisl aliquet faucibus. Integer neque lacus, dignissim sed mi et, dignissim luctus metus. Cras sollicitudin vestibulum leo, ac ultrices sapien bibendum ac. Phasellus lobortis aliquam libero eu volutpat. Donec vitae rutrum tellus. Fusce vel ante ipsum. Suspendisse mollis tempus porta. Sed a orci tempor, rhoncus tortor eu, sodales justo.`
60+
const comment = fakeCommentSB({ comment: commentText })
61+
62+
return (
63+
<CommentDisplay
64+
comment={comment}
65+
itemType={itemType}
66+
isEditable={isEditable}
67+
setShowDeleteModal={setShowDeleteModal}
68+
setShowEditModal={setShowEditModal}
69+
/>
70+
)
71+
}
72+
73+
export const ShortTextWithLink: StoryFn<typeof CommentDisplay> = () => {
74+
const comment = fakeCommentSB({
75+
comment: `Ut dignissim, odio a cursus pretium. https://example.com`,
76+
})
77+
78+
return (
79+
<CommentDisplay
80+
comment={comment}
81+
itemType={itemType}
82+
isEditable={isEditable}
83+
setShowDeleteModal={setShowDeleteModal}
84+
setShowEditModal={setShowEditModal}
85+
/>
86+
)
87+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { createRef, useEffect, useState } from 'react'
2+
import { compareDesc } from 'date-fns'
3+
import { Box, Flex, Text } from 'theme-ui'
4+
5+
import { Button } from '../Button/Button'
6+
import { CommentAvatar } from '../CommentAvatar/CommentAvatar'
7+
import { DisplayDate } from '../DisplayDate/DisplayDate'
8+
import { LinkifyText } from '../LinkifyText/LinkifyText'
9+
import { Username } from '../Username/Username'
10+
11+
import type { Comment } from 'oa-shared'
12+
13+
export interface IProps {
14+
comment: Comment
15+
isEditable: boolean | undefined
16+
itemType: 'ReplyItem' | 'CommentItem'
17+
setShowDeleteModal: (arg: boolean) => void
18+
setShowEditModal: (arg: boolean) => void
19+
}
20+
21+
const DELETED_COMMENT = 'The original comment got deleted'
22+
const SHORT_COMMENT = 129
23+
24+
export const CommentDisplay = (props: IProps) => {
25+
const {
26+
comment,
27+
isEditable,
28+
itemType,
29+
setShowDeleteModal,
30+
setShowEditModal,
31+
} = props
32+
const textRef = createRef<any>()
33+
34+
const [textHeight, setTextHeight] = useState(0)
35+
const [isShowMore, setShowMore] = useState(false)
36+
37+
const maxHeight = isShowMore ? 'max-content' : '128px'
38+
39+
const showMore = () => {
40+
setShowMore((prev) => !prev)
41+
}
42+
43+
useEffect(() => {
44+
if (textRef.current) {
45+
setTextHeight(textRef.current.scrollHeight)
46+
}
47+
}, [textRef])
48+
49+
if (comment.deleted) {
50+
return (
51+
<Box
52+
sx={{
53+
marginBottom: 2,
54+
border: `${comment.highlighted ? '2px dashed black' : 'none'}`,
55+
}}
56+
data-cy="deletedComment"
57+
>
58+
<Text sx={{ color: 'grey' }}>[{DELETED_COMMENT}]</Text>
59+
</Box>
60+
)
61+
}
62+
63+
if (!comment.deleted) {
64+
return (
65+
<Flex
66+
sx={{
67+
gap: 2,
68+
flexGrow: 1,
69+
border: `${comment.highlighted ? '2px dashed black' : 'none'}`,
70+
}}
71+
>
72+
<Box data-cy="commentAvatar" data-testid="commentAvatar">
73+
<CommentAvatar
74+
name={comment.createdBy?.name}
75+
photoUrl={comment.createdBy?.photoUrl}
76+
/>
77+
</Box>
78+
79+
<Flex
80+
sx={{
81+
flexDirection: 'column',
82+
flex: 1,
83+
}}
84+
>
85+
<Flex
86+
sx={{
87+
flexWrap: 'wrap',
88+
justifyContent: 'space-between',
89+
flexDirection: ['column', 'row'],
90+
gap: 2,
91+
}}
92+
>
93+
<Flex
94+
sx={{
95+
alignItems: 'baseline',
96+
gap: 2,
97+
flexDirection: 'row',
98+
}}
99+
>
100+
<Username
101+
user={{
102+
userName: comment.createdBy?.username || '',
103+
countryCode: comment.createdBy?.country,
104+
isVerified: comment.createdBy?.isVerified,
105+
// TODO: isSupporter
106+
}}
107+
/>
108+
<Text sx={{ fontSize: 1, color: 'darkGrey' }}>
109+
{comment.modifiedAt &&
110+
compareDesc(comment.createdAt, comment.modifiedAt) > 0 &&
111+
'Edited '}
112+
<DisplayDate date={comment.modifiedAt || comment.createdAt} />
113+
</Text>
114+
</Flex>
115+
116+
{isEditable && (
117+
<Flex
118+
sx={{
119+
alignItems: 'flex-end',
120+
gap: 2,
121+
paddingBottom: 2,
122+
}}
123+
>
124+
<Button
125+
type="button"
126+
data-cy={`${itemType}: edit button`}
127+
variant="subtle"
128+
small={true}
129+
icon="edit"
130+
onClick={() => setShowEditModal(true)}
131+
>
132+
edit
133+
</Button>
134+
<Button
135+
type="button"
136+
data-cy={`${itemType}: delete button`}
137+
variant="subtle"
138+
small={true}
139+
icon="delete"
140+
onClick={() => setShowDeleteModal(true)}
141+
>
142+
delete
143+
</Button>
144+
</Flex>
145+
)}
146+
</Flex>
147+
<Text
148+
data-cy="comment-text"
149+
data-testid="commentText"
150+
sx={{
151+
fontFamily: 'body',
152+
lineHeight: 1.3,
153+
maxHeight,
154+
overflow: 'hidden',
155+
whiteSpace: 'pre-wrap',
156+
wordBreak: 'break-word',
157+
marginTop: 1,
158+
marginBottom: 2,
159+
}}
160+
ref={textRef}
161+
>
162+
<LinkifyText>{comment.comment}</LinkifyText>
163+
</Text>
164+
{textHeight > SHORT_COMMENT && (
165+
<a
166+
onClick={showMore}
167+
style={{
168+
color: 'gray',
169+
cursor: 'pointer',
170+
}}
171+
>
172+
{isShowMore ? 'Show less' : 'Show more'}
173+
</a>
174+
)}
175+
</Flex>
176+
</Flex>
177+
)
178+
}
179+
}

packages/components/src/CommentItem/CommentItem.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { CommentItem } from './CommentItem'
44
import type { Meta, StoryFn } from '@storybook/react'
55

66
export default {
7-
title: 'Discussions/CommentItem',
7+
title: 'Commenting/CommentItem',
88
component: CommentItem,
99
} as Meta<typeof CommentItem>
1010

packages/components/src/CommentList/CommentList.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { CommentList } from './CommentList'
44
import type { Meta, StoryFn } from '@storybook/react'
55

66
export default {
7-
title: 'Discussions/CommentList',
7+
title: 'Commenting/CommentList',
88
component: CommentList,
99
} as Meta<typeof CommentList>
1010

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { CommentsTitle } from './CommentsTitle'
2+
3+
import type { Meta, StoryFn } from '@storybook/react'
4+
import type { Comment } from 'oa-shared'
5+
6+
export default {
7+
title: 'Commenting/CommentsTitle',
8+
component: CommentsTitle,
9+
} as Meta<typeof CommentsTitle>
10+
11+
export const NoComments: StoryFn<typeof CommentsTitle> = () => (
12+
<CommentsTitle comments={[]} />
13+
)
14+
15+
export const OneComment: StoryFn<typeof CommentsTitle> = () => {
16+
const comment = {} as Comment
17+
18+
return <CommentsTitle comments={[comment]} />
19+
}
20+
21+
export const MultipleComments: StoryFn<typeof CommentsTitle> = () => {
22+
const comment = {} as Comment
23+
return <CommentsTitle comments={[comment, comment, comment]} />
24+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import '@testing-library/jest-dom/vitest'
2+
3+
import { describe, expect, it } from 'vitest'
4+
5+
import { render } from '../test/utils'
6+
import { COMMENTS, NO_COMMENTS, ONE_COMMENT } from './CommentsTitle'
7+
import {
8+
MultipleComments,
9+
NoComments,
10+
OneComment,
11+
} from './CommentsTitle.stories'
12+
13+
import type { IProps } from './CommentsTitle'
14+
15+
describe('CommentsTitle', () => {
16+
it('renders correctly when there are zero comments', () => {
17+
const { getByText } = render(
18+
<NoComments {...(NoComments.args as IProps)} />,
19+
)
20+
21+
expect(getByText(NO_COMMENTS)).toBeInTheDocument()
22+
})
23+
24+
it('renders correctly when there is one comment', () => {
25+
const { getByText } = render(
26+
<OneComment {...(OneComment.args as IProps)} />,
27+
)
28+
29+
expect(getByText(ONE_COMMENT)).toBeInTheDocument()
30+
})
31+
32+
it('renders correctly when there are multiple comments', () => {
33+
const { getByText } = render(
34+
<MultipleComments {...(MultipleComments.args as IProps)} />,
35+
)
36+
37+
expect(getByText(`3 ${COMMENTS}`)).toBeInTheDocument()
38+
})
39+
})
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { useMemo } from 'react'
2+
import { Heading } from 'theme-ui'
3+
4+
import type { Comment } from 'oa-shared'
5+
6+
export const NO_COMMENTS = 'Start the discussion'
7+
export const ONE_COMMENT = '1 Comment'
8+
export const COMMENTS = 'Comments'
9+
10+
export interface IProps {
11+
comments: Comment[]
12+
}
13+
14+
export const CommentsTitle = ({ comments }: IProps) => {
15+
const commentCount = useMemo(
16+
() =>
17+
comments.filter((x) => !x.deleted).length +
18+
comments.flatMap((x) => x.replies).filter((x) => !!x).length,
19+
[comments],
20+
)
21+
const setTitle = () => {
22+
if (commentCount === 0) {
23+
return NO_COMMENTS
24+
}
25+
if (commentCount === 1) {
26+
return ONE_COMMENT
27+
}
28+
29+
return `${commentCount} ${COMMENTS}`
30+
}
31+
32+
const title = setTitle()
33+
34+
return (
35+
<Heading as="h3" data-cy="DiscussionTitle">
36+
{title}
37+
</Heading>
38+
)
39+
}

packages/components/src/CreateComment/CreateComment.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { CreateComment } from './CreateComment'
55
import type { Meta, StoryFn } from '@storybook/react'
66

77
export default {
8-
title: 'Discussions/CreateComment',
8+
title: 'Commenting/CreateComment',
99
component: CreateComment,
1010
} as Meta<typeof CreateComment>
1111

0 commit comments

Comments
 (0)