diff --git a/src/App.tsx b/src/App.tsx index db56b44b0..ffd2131c7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import 'bulma/bulma.sass'; import '@fortawesome/fontawesome-free/css/all.css'; import './App.scss'; @@ -8,8 +8,56 @@ import { PostsList } from './components/PostsList'; import { PostDetails } from './components/PostDetails'; import { UserSelector } from './components/UserSelector'; import { Loader } from './components/Loader'; +import { client } from './utils/fetchClient'; +import { User } from './types/User'; +import { Post } from './types/Post'; export const App: React.FC = () => { + const [users, setUsers] = useState([]); + const [errorMessage, setErrorMessage] = useState(''); + const [isDropDownActive, setIsDropDownActive] = useState(false); + const [selectedUser, setSelectedUser] = useState(null); + const [usersPosts, setUsersPosts] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [selectedPost, setSelectedPost] = useState(null); + const [isCommentButtonClicked, setIsCommentButtonClicked] = useState(false); + + const getAllUsers = () => { + return client.get('/users') + .catch(() => { + setErrorMessage('Something went wrong!'); + }); + }; + + useEffect(() => { + getAllUsers() + .then(usersFromServer => { + if (usersFromServer) { + setUsers(usersFromServer); + } + }); + }, []); + + const getUsersPosts = () => { + return client.get(`/posts?userId=${selectedUser?.id}`) + .catch(() => { + setErrorMessage('Something went wrong!'); + }); + }; + + useEffect(() => { + if (selectedUser) { + setIsLoading(true); + getUsersPosts() + .then(postsFromServer => { + if (postsFromServer) { + setUsersPosts(postsFromServer); + } + }) + .finally(() => setIsLoading(false)); + } + }, [selectedUser]); + return (
@@ -17,28 +65,59 @@ export const App: React.FC = () => {
- +
-

- No user selected -

+ {!selectedUser && ( +

+ No user selected +

+ )} - + {isLoading && } -
- Something went wrong! -
+ {errorMessage && ( +
+ {errorMessage} +
+ )} -
- No posts yet -
+ {(!usersPosts.length + && selectedUser + && !errorMessage + && !isLoading + && !isDropDownActive + ) && ( +
+ No posts yet +
+ )} - + {(usersPosts.length > 0 + && selectedUser + && !isLoading + ) && ( + + )}
@@ -50,12 +129,18 @@ export const App: React.FC = () => { 'is-parent', 'is-8-desktop', 'Sidebar', - 'Sidebar--open', + { 'Sidebar--open': selectedPost !== null }, )} > -
- -
+ {selectedPost !== null && ( +
+ +
+ )}
diff --git a/src/components/NewCommentForm.tsx b/src/components/NewCommentForm.tsx index 73a8a0b45..6d931c4d4 100644 --- a/src/components/NewCommentForm.tsx +++ b/src/components/NewCommentForm.tsx @@ -1,8 +1,131 @@ -import React from 'react'; +import React, { useState } from 'react'; +import classNames from 'classnames'; +import { Post } from '../types/Post'; +import { client } from '../utils/fetchClient'; +import { Comment } from '../types/Comment'; + +type Props = { + selectedPost: Post | null, + setPostsComments: React.Dispatch>, + setCommentErrorMessage: (value: string) => void, +}; + +export const NewCommentForm: React.FC = ({ + selectedPost, + setPostsComments, + setCommentErrorMessage, +}) => { + const [isFormLoading, setIsFormLoading] = useState(false); + const [commentName, setCommentName] = useState(''); + const [commentEmail, setCommentEmail] = useState(''); + const [commentText, setCommentText] = useState(''); + const [formError, setFormError] = useState({ + name: '', + email: '', + text: '', + }); + + const isEmptyField = !commentText.length + || !commentEmail.length + || !commentName.length; + + const handleNameChange = (event: React.ChangeEvent) => { + setCommentName(event.target.value); + setFormError({ + ...formError, + name: '', + }); + }; + + const handleEmailChange = (event: React.ChangeEvent) => { + setCommentEmail(event.target.value); + setFormError({ + ...formError, + email: '', + }); + }; + + const handleTextChange = (event: React.ChangeEvent) => { + setCommentText(event.target.value); + setFormError({ + ...formError, + text: '', + }); + }; + + const createComment = ({ + postId, + name, + email, + body, + }: Omit) => { + return client.post('/comments', { + postId, name, email, body, + }); + }; + + const addComment = (event: React.FormEvent) => { + event.preventDefault(); + if (!commentName) { + setFormError(rest => ({ + ...rest, + name: 'Name is required', + })); + } + + if (!commentEmail) { + setFormError(rest => ({ + ...rest, + email: 'Email is required', + })); + } + + if (!commentText) { + setFormError(rest => ({ + ...rest, + text: 'Enter some text', + })); + } + + if (selectedPost?.id && !isEmptyField) { + setIsFormLoading(true); + + createComment({ + postId: selectedPost?.id, + name: commentName.trim(), + email: commentEmail.trim(), + body: commentText.trim(), + }) + .then(newComment => { + setPostsComments(currentComments => [...currentComments, newComment]); + }) + .catch(() => { + setCommentErrorMessage('Can not to load comments'); + }) + .finally(() => { + setCommentText(''); + setIsFormLoading(false); + }); + } + }; + + const onClear = () => { + setFormError({ + name: '', + email: '', + text: '', + }); + + setCommentEmail(''); + setCommentName(''); + setCommentText(''); + }; -export const NewCommentForm: React.FC = () => { return ( -
+ addComment(event)} + >
-

- Name is required -

+ {formError.name && ( +

+ {formError.name} +

+ )}
@@ -41,28 +170,34 @@ export const NewCommentForm: React.FC = () => {
- - - + {formError.email && ( + + + + )}
-

- Email is required -

+ {formError.email && ( +

+ {formError.email} +

+ )}
@@ -75,25 +210,38 @@ export const NewCommentForm: React.FC = () => { id="comment-body" name="body" placeholder="Type comment here" - className="textarea is-danger" + className={classNames('textarea', { 'is-danger': formError.text })} + value={commentText} + onChange={handleTextChange} />
-

- Enter some text -

+ {formError.text && ( +

+ {formError.text} +

+ )}
-
{/* eslint-disable-next-line react/button-has-type */} -
diff --git a/src/components/PostDetails.tsx b/src/components/PostDetails.tsx index ace945f0a..64b9f5fc4 100644 --- a/src/components/PostDetails.tsx +++ b/src/components/PostDetails.tsx @@ -1,117 +1,149 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Loader } from './Loader'; import { NewCommentForm } from './NewCommentForm'; +import { Comment } from '../types/Comment'; +import { Post } from '../types/Post'; +import { client } from '../utils/fetchClient'; + +type Props = { + selectedPost: Post | null, + isCommentButtonClicked: boolean, + setIsCommentButtonClicked: (value: boolean) => void, +}; + +export const PostDetails: React.FC = ({ + selectedPost, + isCommentButtonClicked, + setIsCommentButtonClicked, +}) => { + const [isCommentsLoading, setIsCommentsLoading] = useState(false); + const [commentErrorMessage, setCommentErrorMessage] = useState(''); + const [postsComments, setPostsComments] = useState([]); + + const commentButtonHandler = () => { + setIsCommentButtonClicked(true); + }; + + const getPostsComments = () => { + return client.get(`/comments?postId=${selectedPost?.id}`) + .catch(() => { + setCommentErrorMessage('Something went wrong!'); + }); + }; + + useEffect(() => { + setIsCommentsLoading(true); + getPostsComments() + .then(comentsFromServer => { + if (comentsFromServer) { + setPostsComments(comentsFromServer); + } + }) + .finally(() => setIsCommentsLoading(false)); + }, [selectedPost]); + + const removeComment = (id: number) => { + return client.delete(`/comments/${id}`); + }; + + const deleteCommentHandler = (id: number) => { + setPostsComments(prew => prew.filter(curentComment => ( + id !== curentComment.id + ))); + + removeComment(id) + .catch(() => { + setCommentErrorMessage('delete error'); + }); + }; -export const PostDetails: React.FC = () => { 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 -

+ {isCommentsLoading && } -

Comments:

- -
-
- - Misha Hrynko - - + {commentErrorMessage && ( +
+ Something went wrong
+ )} -
- Some comment -
-
+ {(!postsComments.length + && !commentErrorMessage + && !isCommentsLoading + ) && ( +

+ No comments yet +

+ )} + {(postsComments.length > 0 + && !commentErrorMessage + && !isCommentsLoading + ) && ( +

Comments:

+ )} -
-
- - Misha Hrynko - - - -
-
- One more comment -
-
+
+ + {comment.name} + + +
- + )))} -
- {'Multi\nline\ncomment'} -
- - - + {(!isCommentButtonClicked + && !commentErrorMessage + && !isCommentsLoading + ) && ( + + )}
- + {(isCommentButtonClicked && !isCommentsLoading) && ( + + )}
); diff --git a/src/components/PostsList.tsx b/src/components/PostsList.tsx index 6e6efc0f3..5e6e506b3 100644 --- a/src/components/PostsList.tsx +++ b/src/components/PostsList.tsx @@ -1,85 +1,69 @@ import React from 'react'; +import classNames from 'classnames'; +import { Post } from '../types/Post'; -export const PostsList: React.FC = () => ( -
-

Posts:

+type Props = { + usersPosts: Post[], + setSelectedPost: (value: Post | null) => void, + selectedPost: Post | null, + setIsCommentButtonClicked: (value: boolean) => void, +}; - - - - - - - - +export const PostsList: React.FC = ({ + usersPosts, + setSelectedPost, + selectedPost, + setIsCommentButtonClicked, +}) => { + const selectPostHandler = (post: Post) => { + if (selectedPost !== post) { + setSelectedPost(post); + setIsCommentButtonClicked(false); + } else { + setSelectedPost(null); + } + }; - - - + return ( +
+

Posts:

-
+
#Title
17 - fugit voluptas sed molestias voluptatem provident -
+ + + + + + + - - + + {usersPosts.map(post => ( + + - - + - - - - - - - - - - - - - - - - - - - -
#Title
- -
{post.id}
18 + {post.title} + - voluptate et itaque vero tempora molestiae - - -
19adipisci placeat illum aut reiciendis qui - -
20doloribus ad provident suscipit at - -
-
-); + + + + + ))} + + +
+ ); +}; diff --git a/src/components/UserSelector.tsx b/src/components/UserSelector.tsx index cb83a8f68..dc08bb18b 100644 --- a/src/components/UserSelector.tsx +++ b/src/components/UserSelector.tsx @@ -1,19 +1,72 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; +import classNames from 'classnames'; +import { User } from '../types/User'; +import { Post } from '../types/Post'; + +type Props = { + users: User[], + setIsDropDownActive: (value: boolean) => void, + isDropDownActive: boolean, + setSelectedUser: (value: User) => void, + selectedUser: User | null, + setSelectedPost: (value: Post | null) => void, +}; + +export const UserSelector: React.FC = ({ + users, + setIsDropDownActive, + isDropDownActive, + setSelectedUser, + selectedUser, + setSelectedPost, +}) => { + const dropDownHandler = () => setIsDropDownActive(!isDropDownActive); + + const dropdown = useRef(null); + + useEffect(() => { + const closeDropdown = (e: MouseEvent) => { + if (dropdown.current + && isDropDownActive + && !dropdown.current.contains(e.target as Node)) { + setIsDropDownActive(false); + } + }; + + document.addEventListener('click', closeDropdown); + + return () => { + document.removeEventListener('click', closeDropdown); + }; + }, [isDropDownActive]); + + const selectUserHandler = (user: User) => { + if (selectedUser !== user) { + setSelectedUser(user); + setIsDropDownActive(false); + setSelectedPost(null); + } + }; -export const UserSelector: React.FC = () => { return (
-
+