diff --git a/src/App.tsx b/src/App.tsx index 017957182..f0bc7e6cf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,53 +8,117 @@ import { PostsList } from './components/PostsList'; import { PostDetails } from './components/PostDetails'; import { UserSelector } from './components/UserSelector'; import { Loader } from './components/Loader'; +import { useEffect, useState } from 'react'; +import { User } from './types/User'; +import { Post } from './types/Post'; +import { getUserPosts, getUsers } from './utils/fetchClient'; -export const App = () => ( -
-
-
-
-
-
- -
+export const App = () => { + const [users, setUsers] = useState([]); -
-

No user selected

+ const [selectedUser, setSelectedUser] = useState(null); + const [userPosts, setUserPosts] = useState([]); - + const [selectedPost, setSelectedPost] = useState(null); -
- Something went wrong! -
+ const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const [showForm, setShowForm] = useState(false); + + useEffect(() => { + getUsers() + .then(setUsers) + .catch(newError => { + throw newError; + }); + }, []); -
- No posts yet + useEffect(() => { + if (selectedUser) { + setLoading(true); + + getUserPosts(selectedUser.id) + .then(setUserPosts) + .catch(() => setError('Something went wrong!')) + .finally(() => setLoading(false)); + } + }, [selectedUser]); + + return ( +
+
+
+
+
+
+
- +
+ {!selectedUser && ( +

No user selected

+ )} + + {!loading && + !error && + selectedUser && + (userPosts.length > 0 ? ( + + ) : ( +
+ No posts yet +
+ ))} + + {loading && } + + {error && ( +
+ {error} +
+ )} +
-
- -
-
- +
+
+ {selectedPost && ( + + )} +
-
-
-); +
+ ); +}; diff --git a/src/components/NewCommentForm.tsx b/src/components/NewCommentForm.tsx index 73a8a0b45..21877ce2e 100644 --- a/src/components/NewCommentForm.tsx +++ b/src/components/NewCommentForm.tsx @@ -1,8 +1,81 @@ -import React from 'react'; +import classNames from 'classnames'; +import React, { useState } from 'react'; +import { CommentData } from '../types/Comment'; + +type Props = { + buttonLoader: boolean; + addFunction: (newComment: CommentData) => Promise; +}; + +export const NewCommentForm: React.FC = ({ + buttonLoader, + addFunction, +}) => { + const [name, setName] = useState(''); + const [nameError, setNameError] = useState(false); + const [email, setEmail] = useState(''); + const [emailError, setEmailError] = useState(false); + const [text, setText] = useState(''); + const [textError, setTextError] = useState(false); + + const handleNameChange = (event: React.ChangeEvent) => { + setName(event.target.value); + setNameError(false); + }; + + const handleEmailChange = (event: React.ChangeEvent) => { + setEmail(event.target.value); + setEmailError(false); + }; + + const handleTextChange = (event: React.ChangeEvent) => { + setText(event.target.value); + setTextError(false); + }; + + const checkQuery = ( + query: string, + setQuery: (query: string) => void, + error: (value: boolean) => void, + ) => { + const normalizedQuery = query.trim(); + const isError = normalizedQuery.length === 0 || !query; + + error(isError); + setQuery(normalizedQuery); + + return isError; + }; + + const onSubmit = (event: React.FormEvent | React.MouseEvent) => { + event.preventDefault(); + + const isNameError = checkQuery(name, setName, setNameError); + const isEmailError = checkQuery(email, setEmail, setEmailError); + const isTextError = checkQuery(text, setText, setTextError); + + if (isNameError || isEmailError || isTextError) { + return; + } + + const newComment = addFunction({ name, email, body: text }); + + if (newComment instanceof Promise) { + newComment.then(() => setText('')); + } + }; + + const clearFunction = () => { + setEmail(''); + setEmailError(false); + setName(''); + setNameError(false); + setText(''); + setTextError(false); + }; -export const NewCommentForm: React.FC = () => { return ( -
+ onSubmit(event)}>
- -

- Name is required -

@@ -41,28 +119,34 @@ export const NewCommentForm: React.FC = () => {
- - - -
+ {emailError && ( + <> + + + -

- Email is required -

+

+ Email is required +

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

- Enter some text -

+ {textError && ( +

+ Enter some text +

+ )}
-
- {/* eslint-disable-next-line react/button-has-type */} -
diff --git a/src/components/PostDetails.tsx b/src/components/PostDetails.tsx index 2f82db916..33e31a3f0 100644 --- a/src/components/PostDetails.tsx +++ b/src/components/PostDetails.tsx @@ -1,106 +1,141 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Loader } from './Loader'; import { NewCommentForm } from './NewCommentForm'; +import { Post } from '../types/Post'; +import { addComment, deleteComment, getComments } from '../utils/fetchClient'; +import { Comment, CommentData } from '../types/Comment'; + +type Props = { + selectedPost: Post | null; + showForm: boolean; + setShowForm: (value: boolean) => void; +}; + +export const PostDetails: React.FC = ({ + selectedPost, + showForm, + setShowForm, +}) => { + const [comments, setComments] = useState([]); + + const [buttonLoader, setButtonLoader] = useState(false); + const [commentLoader, setCommentLoader] = useState(false); + const [commentError, setCommentError] = useState(''); + + const errorMessage = 'Something went wrong'; + + useEffect(() => { + if (selectedPost) { + setCommentLoader(true); + + getComments(selectedPost.id) + .then(setComments) + .catch(() => setCommentError(errorMessage)) + .finally(() => setCommentLoader(false)); + } + }, [selectedPost, setComments]); + + const addFunction = ({ name, email, body }: CommentData) => { + setButtonLoader(true); + + const postId = selectedPost?.id || 0; + + return addComment({ postId, name, email, body }) + .then(newComment => setComments([...comments, newComment])) + .catch(() => { + setCommentError(errorMessage); + }) + .finally(() => setButtonLoader(false)); + }; + + const deleteFunction = (commentId: number) => { + setComments(currentCommenst => { + return currentCommenst.filter(comment => comment.id !== commentId); + }); + + deleteComment(commentId); + }; -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 -

- -

Comments:

- - - - - -
-
- - Misha Hrynko - - - + {commentError && ( +
+ {errorMessage}
+ )} -
- {'Multi\nline\ncomment'} -
-
- - -
+ {!commentLoader && !commentError && ( + <> + {comments.length > 0 ? ( + <> +

Comments:

+ + {comments.map(comment => ( +
+
+ + {comment.name} + + +
- +
+ {comment.body} +
+
+ ))} + + ) : ( +

+ No comments yet +

+ )} + + {showForm ? ( + + ) : ( + + )} + + )} +
); diff --git a/src/components/PostsList.tsx b/src/components/PostsList.tsx index cf90f04b0..92c0ab2c0 100644 --- a/src/components/PostsList.tsx +++ b/src/components/PostsList.tsx @@ -1,86 +1,74 @@ import React from 'react'; +import { Post } from '../types/Post'; +import classNames from 'classnames'; -export const PostsList: React.FC = () => ( -
-

Posts:

+type Props = { + userPosts: Post[]; + selectedPost: Post | null; + setShowForm: (value: boolean) => void; + setSelectedPost: (postId: Post | null) => void; +}; - - - - - - {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} - - - +export const PostsList: React.FC = ({ + userPosts, + selectedPost, + setShowForm, + setSelectedPost, +}) => { + const openPostFunction = (postToSelect: Post) => { + if (selectedPost && selectedPost.id === postToSelect.id) { + setSelectedPost(null); + } else { + setSelectedPost(postToSelect); + setShowForm(false); + } + }; - - - + const isOpenFunction = (currentPost: Post) => { + if (selectedPost && selectedPost.id === currentPost.id) { + return true; + } else { + return false; + } + }; - + return ( +
+

Posts:

-
- +
#Title
17 - fugit voluptas sed molestias voluptatem provident - - -
+ + + + + {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} + + + - - + + {userPosts.map(post => ( + + - + - - - - - - - - - - - - - - - - - -
#Title
18
{post.id} - voluptate et itaque vero tempora molestiae - {post.title} - -
19adipisci placeat illum aut reiciendis qui - -
20doloribus ad provident suscipit at - -
-
-); + + + + + ))} + + + + ); +}; diff --git a/src/components/UserSelector.tsx b/src/components/UserSelector.tsx index c89442841..429cfc047 100644 --- a/src/components/UserSelector.tsx +++ b/src/components/UserSelector.tsx @@ -1,16 +1,74 @@ -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import { User } from '../types/User'; +import classNames from 'classnames'; +import { Post } from '../types/Post'; + +type Props = { + users: User[]; + selectedUser: User | null; + setSelectedUser: (userToSelect: User) => void; + setSelectedPost: (postToSelect: Post | null) => void; +}; + +export const UserSelector: React.FC = ({ + users, + selectedUser, + setSelectedUser, + setSelectedPost, +}) => { + const [menuVisibility, setMenuVisibility] = useState(false); + + const selectUserFunction = ( + event: React.MouseEvent, + user: User, + ) => { + event.preventDefault(); + + setSelectedUser(user); + setSelectedPost(null); + setMenuVisibility(false); + }; + + const dropdownRef = useRef(null); + + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setMenuVisibility(false); + } + }; + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); -export const UserSelector: React.FC = () => { return ( -
+
0 && menuVisibility, + })} + >