mirror of
https://github.com/aljazceru/landscape-template.git
synced 2025-12-29 03:54:30 +01:00
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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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: [] }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
22
src/features/Posts/Components/Comments/Comment/Comment.tsx
Normal file
22
src/features/Posts/Components/Comments/Comment/Comment.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
19
src/features/Posts/Components/Comments/helpers.tsx
Normal file
19
src/features/Posts/Components/Comments/helpers.tsx
Normal 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);
|
||||
}
|
||||
1
src/features/Posts/Components/Comments/index.tsx
Normal file
1
src/features/Posts/Components/Comments/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { }
|
||||
15
src/features/Posts/Components/Comments/types.ts
Normal file
15
src/features/Posts/Components/Comments/types.ts
Normal 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[]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from "./useResizeListener";
|
||||
export * from "./usePressHolder";
|
||||
export * from "./useInfiniteQuery";
|
||||
export * from "./useReachedBottom";
|
||||
export * from "./useAutoResizableTextArea";
|
||||
|
||||
20
src/utils/hooks/useAutoResizableTextArea.ts
Normal file
20
src/utils/hooks/useAutoResizableTextArea.ts
Normal 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
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user