feat: comment with replies component, convertCommentsToTree helper method, comments section component, add comment input component, useAutoResizableText area, update mocks with comments data

This commit is contained in:
MTG2000
2022-04-26 10:41:03 +03:00
parent 2b0d2c174c
commit d7b2499c13
14 changed files with 201 additions and 16 deletions

View File

@@ -0,0 +1,33 @@
import { FormEvent } from "react";
import Button from "src/Components/Button/Button";
import Avatar from "src/features/Profiles/Components/Avatar/Avatar";
import { useAutoResizableTextArea } from "src/utils/hooks";
export default function AddComment() {
const textAreaRef = useAutoResizableTextArea();
const submitComment = (e: FormEvent) => {
e.preventDefault();
alert('submitted')
}
return (
<form onSubmit={submitComment} className="border border-gray-200 rounded-10 p-24">
<div className="flex gap-16 items-start pb-24 border-b border-gray-200 focus-within:border-primary-500">
<Avatar width={48} src='https://i.pravatar.cc/150?img=1' />
<textarea
rows={2}
className="w-full border-0 text-gray-500 font-medium focus:!ring-0 resize-none"
placeholder='Leave a comment...'
ref={textAreaRef}
/>
</div>
<div className="flex mt-24">
<Button type='submit' color="primary" className="ml-auto">Submit</Button>
</div>
</form>
)
}

View File

@@ -0,0 +1,28 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { MOCK_DATA } from 'src/mocks/data';
import Comment from './Comment';
export default {
title: 'Posts/Components/Comments/Comment with Replies',
component: Comment,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof Comment>;
const Template: ComponentStory<typeof Comment> = (args) => <div className="max-w-[70ch]"><Comment {...args} ></Comment></div>
export const Default = Template.bind({});
Default.args = {
comment: {
...MOCK_DATA.generatePostComments(1)[0],
replies: [
{ ...MOCK_DATA.generatePostComments(1)[0], replies: [] },
{ ...MOCK_DATA.generatePostComments(1)[0], replies: [] }
]
}
}

View File

@@ -0,0 +1,22 @@
import CommentCard from "../CommentCard/CommentCard";
import { CommentWithReplies } from "../types";
interface Props {
comment: CommentWithReplies
}
export default function Comment({ comment }: Props) {
return (
<div >
<CommentCard comment={comment} />
{comment.replies.length > 0 && <div className="flex mt-16 gap-20">
<div className="border-l border-b border-gray-200 w-24 h-40 rounded-bl-8 flex-shrink-0 ml-8"></div>
<div className="flex flex-col gap-16">
{comment.replies.map(reply => <Comment key={reply.id} comment={reply} />)}
</div>
</div>}
</div>
)
}

View File

@@ -4,7 +4,7 @@ import { MOCK_DATA } from 'src/mocks/data';
import CommentCard from './CommentCard';
export default {
title: 'Posts/Components/CommentCard',
title: 'Posts/Components/Comments/CommentCard',
component: CommentCard,
argTypes: {
backgroundColor: { control: 'color' },
@@ -16,7 +16,9 @@ const Template: ComponentStory<typeof CommentCard> = (args) => <div className="m
export const Default = Template.bind({});
Default.args = {
comment: MOCK_DATA.posts.stories[0].comments[0]
comment: {
...MOCK_DATA.posts.stories[0].comments[0],
}
}

View File

@@ -1,15 +1,9 @@
import { BiComment } from "react-icons/bi";
import Button from "src/Components/Button/Button";
import VotesCount from "src/Components/VotesCount/VotesCount";
import { Author } from "../../types"
import Header from "../PostCard/Header/Header";
import Header from "src/features/Posts/Components/PostCard/Header/Header";
import { Comment } from "../types";
interface Comment {
author: Author
created_at: string
body: string;
votes_count: number
}
interface Props {
comment: Comment
}

View File

@@ -0,0 +1,22 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { MOCK_DATA } from 'src/mocks/data';
import CommentsSection from './CommentsSection';
export default {
title: 'Posts/Components/Comments/CommentsSection',
component: CommentsSection,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof CommentsSection>;
const Template: ComponentStory<typeof CommentsSection> = (args) => <div className="max-w-[70ch]"><CommentsSection {...args} ></CommentsSection></div>
export const Default = Template.bind({});
Default.args = {
comments: MOCK_DATA.generatePostComments(15)
}

View File

@@ -0,0 +1,27 @@
import React, { useMemo } from 'react'
import Comment from '../Comment/Comment'
import AddComment from '../AddComment/AddComment'
import { convertCommentsToTree } from '../helpers'
import { Comment as IComment } from '../types'
interface Props {
comments: IComment[]
}
export default function CommentsSection({ comments }: Props) {
const commentsTree = useMemo(() => convertCommentsToTree(comments), [comments])
return (
<div className="border border-gray-200 rounded-10 p-32 bg-white">
<h6 className="text-body2 font-bolder">Discussion ({comments.length})</h6>
<div className="mt-24">
<AddComment />
</div>
<div className="border border-gray-200 rounded-10 p-24 mt-32">
<div className='flex flex-col gap-16'>
{commentsTree.map(comment => <Comment key={comment.id} comment={comment} />)}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,19 @@
import { Comment, CommentWithReplies } from "./types";
export function convertCommentsToTree(comments: Comment[]) {
let tree: Record<Comment['id'], CommentWithReplies> = {};
for (const comment of comments)
tree[comment.id] = { ...comment, replies: [] }
for (const comment of Object.values(tree)) {
if (comment.parentId)
tree[comment.parentId].replies = [...tree[comment.parentId].replies, comment]
}
// TODO
// Sort the comments according to date
return Object.values(tree).filter(node => !node.parentId);
}

View File

@@ -0,0 +1 @@
export { }

View File

@@ -0,0 +1,15 @@
import { Author } from "src/features/Posts/types";
export interface Comment {
id: number
author: Author
created_at: string
body: string
votes_count: number
parentId: number
}
export interface CommentWithReplies extends Comment {
replies: CommentWithReplies[]
}

View File

@@ -1,9 +1,10 @@
import { posts, feed } from "./data/posts";
import { posts, feed, generatePostComments } from "./data/posts";
import { categories, projects } from "./data/projects";
export const MOCK_DATA = {
projects,
categories,
posts,
feed
feed,
generatePostComments: generatePostComments
}

View File

@@ -14,7 +14,7 @@ const getAuthor = () => ({
join_date: getDate()
})
const getPostComments = (cnt: number = 1): Story['comments'] => {
export const generatePostComments = (cnt: number = 1): Story['comments'] => {
let comments = [];
const rootCommentsIds: any[] = []
@@ -95,7 +95,7 @@ export let posts = {
{ id: 3, title: "guide" },
],
author: getAuthor(),
comments: getPostComments(),
comments: generatePostComments(),
},
] as Story[],
@@ -137,7 +137,7 @@ export let posts = {
{ id: 2, title: "webln" },
],
author: getAuthor(),
comments: getPostComments(3)
comments: generatePostComments(3)
},
] as Question[]
}

View File

@@ -3,3 +3,4 @@ export * from "./useResizeListener";
export * from "./usePressHolder";
export * from "./useInfiniteQuery";
export * from "./useReachedBottom";
export * from "./useAutoResizableTextArea";

View File

@@ -0,0 +1,20 @@
import { useEffect, useRef } from "react";
export const useAutoResizableTextArea = () => {
const ref = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
function OnInput() {
if (ref.current) {
ref.current.style.height = "auto";
ref.current.style.height = (ref.current.scrollHeight) + "px";
}
}
ref.current?.setAttribute("style", "height:" + (ref.current?.scrollHeight) + "px;overflow-y:hidden;");
ref.current?.addEventListener("input", OnInput, false);
}, [])
return ref
}