From 3c17a6e3a138b1fd27bc6d13b994c73c4cd8b465 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Thu, 21 Sep 2023 14:22:03 +0300 Subject: [PATCH 1/3] implented --- README.md | 1 + src/App.tsx | 175 +++++++++++++++++++++++++---- src/api/api.ts | 34 ++++++ src/components/NewCommentForm.tsx | 167 +++++++++++++++++++++++----- src/components/PostDetails.tsx | 178 +++++++++++++++--------------- src/components/PostsList.tsx | 105 +++++++----------- src/components/UserSelector.tsx | 51 +++++++-- 7 files changed, 495 insertions(+), 216 deletions(-) create mode 100644 src/api/api.ts diff --git a/README.md b/README.md index b8767524f..a309e1a22 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ be opened in the sidebar with its comments. There should delete a comment and a form to add new comments. > Here is [the working version](https://mate-academy.github.io/react_dynamic-list-of-posts/) +https://ZadorozhnyiYevhenii.github.io/react_dynamic-list-of-posts/ 1. Learn the `utils/fetchClient.ts` and use it to interact with the API (tests expect that you each API request is sent after 300 ms delay); 1. Initially the `App` shows the `UserSelector` and a paragraph `No user selected` in the main content block. diff --git a/src/App.tsx b/src/App.tsx index db56b44b0..020921c22 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import 'bulma/bulma.sass'; import '@fortawesome/fontawesome-free/css/all.css'; import './App.scss'; @@ -8,8 +8,110 @@ import { PostsList } from './components/PostsList'; import { PostDetails } from './components/PostDetails'; import { UserSelector } from './components/UserSelector'; import { Loader } from './components/Loader'; +import { User } from './types/User'; +import { + addComment, + deleteComment, + getUsers, + getUsersComments, + getUsersPosts, +} from './api/api'; +import { Post } from './types/Post'; +import { Comment } from './types/Comment'; export const App: React.FC = () => { + const [users, setUsers] = useState([]); + const [selectedUser, setSelectedUser] = useState(null); + const [userPosts, setUserPosts] = useState([]); + const [selectedPost, setSelectedPost] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + const [isCommentLoading, setIsCommentLoading] = useState(false); + const [comments, setComments] = useState([]); + const [isCommentError, setisCommentError] = useState(false); + const [canWriteComment, setCanWriteComment] = useState(false); + + useEffect(() => { + getUsers().then(setUsers); + }, []); + + const getUserPostFromServer = (userId: number) => { + setIsLoading(true); + + getUsersPosts(userId) + .then((data) => { + setUserPosts(data); + setIsError(false); + }) + .catch(() => setIsError(true)) + .finally(() => setIsLoading(false)); + }; + + const getCommentsFromServer = (postId: number) => { + setIsCommentLoading(true); + + getUsersComments(postId) + .then((data) => { + setComments(data); + setisCommentError(false); + }) + .catch(() => setisCommentError(true)) + .finally(() => { + setIsLoading(false); + setIsCommentLoading(false); + }); + }; + + const handleUserSelect = ( + event: React.MouseEvent, + user: User, + ) => { + event.preventDefault(); + + getUserPostFromServer(user.id); + setSelectedUser(user); + setSelectedPost(null); + }; + + const handleSelectPost = (post: Post) => { + setSelectedPost((currentPost) => { + if (currentPost?.id === post.id) { + return null; + } + + return post; + }); + + getCommentsFromServer(post.id); + setCanWriteComment(false); + }; + + const handleAddNewComment = async ( + postId: number, + name: string, + email: string, + body: string, + ) => { + const newComment = await addComment(postId, name, email, body); + + const filteredComments + = comments.filter((currentComment) => { + return selectedPost?.id === currentComment.postId; + }); + + setComments([...filteredComments, newComment]); + }; + + const handleDeleteComment = (commentId: number) => { + const filteredComments + = comments.filter(currentComment => { + return currentComment.id !== commentId; + }); + + setComments(filteredComments); + deleteComment(commentId); + }; + return (
@@ -17,28 +119,51 @@ export const App: React.FC = () => {
- +
-

- No user selected -

+ {!selectedUser && ( +

+ No user selected +

+ )} - + {isLoading && } -
- Something went wrong! -
+ {isError && !isLoading && ( +
+ Something went wrong! +
+ )} -
- No posts yet -
+ {!userPosts.length + && selectedUser + && !isLoading + && !isError + && ( +
+ No posts yet +
+ )} - + {userPosts.length > 0 && !isLoading && ( + + )}
@@ -49,12 +174,24 @@ export const App: React.FC = () => { 'tile', 'is-parent', 'is-8-desktop', - 'Sidebar', - 'Sidebar--open', + 'Sidebar', { + 'Sidebar--open': selectedUser, + }, )} >
- + {selectedPost && ( + + )}
diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 000000000..078ea958e --- /dev/null +++ b/src/api/api.ts @@ -0,0 +1,34 @@ +import { Comment } from '../types/Comment'; +import { Post } from '../types/Post'; +import { User } from '../types/User'; +import { client } from '../utils/fetchClient'; + +export const getUsers = () => { + return client.get('/users'); +}; + +export const getUsersPosts = (userId: number) => { + return client.get(`/posts?userId=${userId}`); +}; + +export const getUsersComments = (postId: number) => { + return client.get(`/comments?postId=${postId}`); +}; + +export const addComment = ( + postId: number, + name: string, + email: string, + body: string, +) => { + return client.post('/comments', { + postId, + name, + email, + body, + }); +}; + +export const deleteComment = (commentId: number) => { + return client.delete(`/comments/${commentId}`); +}; diff --git a/src/components/NewCommentForm.tsx b/src/components/NewCommentForm.tsx index 73a8a0b45..db04eaf64 100644 --- a/src/components/NewCommentForm.tsx +++ b/src/components/NewCommentForm.tsx @@ -1,8 +1,76 @@ -import React from 'react'; +import React, { useState } from 'react'; +import classNames from 'classnames'; + +type Props = { + selectedPostId: number; + handleAddComment: ( + postId: number, + name: string, + email: string, + body: string, + ) => void; +}; + +export const NewCommentForm: React.FC = ({ + selectedPostId, + handleAddComment, +}) => { + const [name, setName] = useState(''); + const [isNameError, setIsNameError] = useState(false); + const [email, setEmail] = useState(''); + const [isEmailError, setIsEmailError] = useState(false); + const [body, setBody] = useState(''); + const [isBodyError, setIsBodyError] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!name.trim().length) { + setIsNameError(true); + } else { + setIsNameError(false); + } + + if (!email.trim().length) { + setIsEmailError(true); + } else { + setIsEmailError(false); + } + + if (!body.length) { + setIsBodyError(true); + } else { + setIsBodyError(false); + } + + if (name.trim().length + && email.trim().length + && body.trim().length) { + try { + setIsLoading(true); + + await handleAddComment(selectedPostId, name, email, body); + setBody(''); + } catch { + throw new Error(); + } finally { + setIsLoading(false); + } + } + }; + + const handleClear = () => { + setName(''); + setIsNameError(false); + setEmail(''); + setIsEmailError(false); + setBody(''); + setIsBodyError(false); + }; -export const NewCommentForm: React.FC = () => { return ( -
+
-

- Name is required -

+ {isNameError && ( +

+ Name is required +

+ )}
@@ -41,28 +120,39 @@ export const NewCommentForm: React.FC = () => {
{ + setEmail(e.target.value); + setIsNameError(false); + }} /> - - - + {isEmailError && ( + + + + )}
-

- Email is required -

+ {isEmailError && ( +

+ Email is required +

+ )}
@@ -75,25 +165,42 @@ export const NewCommentForm: React.FC = () => { id="comment-body" name="body" placeholder="Type comment here" - className="textarea is-danger" + className={classNames('textarea', { + 'is-danger': isBodyError, + })} + onChange={(e) => { + setBody(e.target.value); + setIsBodyError(false); + }} />
-

- Enter some text -

+ {isBodyError && ( +

+ Enter some text +

+ )}
-
{/* eslint-disable-next-line react/button-has-type */} -
diff --git a/src/components/PostDetails.tsx b/src/components/PostDetails.tsx index ace945f0a..6e171e3e1 100644 --- a/src/components/PostDetails.tsx +++ b/src/components/PostDetails.tsx @@ -1,117 +1,117 @@ import React from 'react'; import { Loader } from './Loader'; import { NewCommentForm } from './NewCommentForm'; +import { Post } from '../types/Post'; +import { Comment } from '../types/Comment'; -export const PostDetails: React.FC = () => { +type Props = { + selectedPost: Post; + isCommentError: boolean; + isCommentsLoading: boolean; + comments: Comment[] | null; + handleDelete: (commentId: number) => void; + setCanWriteAComment: React.Dispatch>; + canWriteAComment: boolean; + handleAddComment: ( + postId: number, + name: string, + email: string, + body: string, + ) => void; +}; + +export const PostDetails: React.FC = ({ + selectedPost, + isCommentError, + isCommentsLoading, + comments, + handleDelete, + canWriteAComment, + setCanWriteAComment, + handleAddComment, +}) => { return (

- #18: voluptate et itaque vero tempora molestiae + {`#${selectedPost.id}: ${selectedPost.title}`}

- eveniet quo quis - laborum totam consequatur non dolor - ut et est repudiandae - est voluptatem vel debitis et magnam + {selectedPost.body}

- - -
- Something went wrong -
- -

- No comments yet -

- -

Comments:

+ {isCommentsLoading && !isCommentError && } -
-
- - Misha Hrynko - - + {isCommentError && ( +
+ Something went wrong
+ )} -
- Some comment -
-
+ {comments?.length === 0 && ( +

+ No comments yet +

+ )} -
-
- - Misha Hrynko - + {comments && !isCommentsLoading && ( + <> +

Comments:

- -
-
- One more comment -
-
+ {comments.map((comment) => ( +
+
+ + {comment.name} + + +
- + ))} + + )} -
- {'Multi\nline\ncomment'} -
-
- - + {!canWriteAComment && ( + + )}
- + {canWriteAComment && ( + + )}
); diff --git a/src/components/PostsList.tsx b/src/components/PostsList.tsx index 6e6efc0f3..da239d3b3 100644 --- a/src/components/PostsList.tsx +++ b/src/components/PostsList.tsx @@ -1,6 +1,18 @@ import React from 'react'; - -export const PostsList: React.FC = () => ( +import classNames from 'classnames'; +import { Post } from '../types/Post'; + +type Props = { + posts: Post[]; + handleSelectPost: (post: Post) => void; + selectedPost: Post | null; +}; + +export const PostsList: React.FC = ({ + posts, + handleSelectPost, + selectedPost, +}) => (

Posts:

@@ -14,71 +26,30 @@ export const PostsList: React.FC = () => ( - - 17 - - - fugit voluptas sed molestias voluptatem provident - - - - - - - - - 18 - - - voluptate et itaque vero tempora molestiae - - - - - - - - - 19 - adipisci placeat illum aut reiciendis qui - - - - - - - - 20 - doloribus ad provident suscipit at - - - - - + {posts.map((post) => ( + + + {post.id} + + + + {post.title} + + + + + + + ))}
diff --git a/src/components/UserSelector.tsx b/src/components/UserSelector.tsx index cb83a8f68..36d5ac583 100644 --- a/src/components/UserSelector.tsx +++ b/src/components/UserSelector.tsx @@ -1,6 +1,22 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { User } from '../types/User'; + +type Props = { + users: User[]; + handleSelectUser: ( + event: React.MouseEvent, + user: User, + ) => void; + selectedUser: User | null; +}; + +export const UserSelector: React.FC = ({ + users, + handleSelectUser, + selectedUser, +}) => { + const [isSelectOpened, setIsSelectOpened] = useState(false); -export const UserSelector: React.FC = () => { return (
{ className="button" aria-haspopup="true" aria-controls="dropdown-menu" + onClick={() => setIsSelectOpened(!isSelectOpened)} > - Choose a user + + {selectedUser ? selectedUser.name : 'Choose a user'} +