diff --git a/src/App.tsx b/src/App.tsx index db56b44b0..4af048297 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import 'bulma/bulma.sass'; import '@fortawesome/fontawesome-free/css/all.css'; + import './App.scss'; import classNames from 'classnames'; @@ -9,7 +10,31 @@ import { PostDetails } from './components/PostDetails'; import { UserSelector } from './components/UserSelector'; import { Loader } from './components/Loader'; +import { getPosts } from './api/posts'; + +import { User } from './types/User'; +import { Post } from './types/Post'; + export const App: React.FC = () => { + const [selectedUser, setSelectedUser] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + const [usersPosts, setUsersPosts] = useState(null); + const [selectedPost, setSelectedPost] = useState(null); + + useEffect(() => { + setIsError(false); + + if (selectedUser) { + setIsLoading(true); + + getPosts(selectedUser) + .then(setUsersPosts) + .catch(() => setIsError(true)) + .finally(() => setIsLoading(false)); + } + }, [selectedUser]); + return (
@@ -17,28 +42,46 @@ export const App: React.FC = () => {
- +
-

- No user selected -

- - + {isLoading && ( + + )} -
- Something went wrong! -
+ {!isLoading && !selectedUser && ( +

+ No user selected +

+ )} -
- No posts yet -
+ {!isLoading && isError && ( +
+ Something went wrong! +
+ )} - + {!isLoading && !isError && usersPosts && (!usersPosts.length ? ( +
+ No posts yet +
+ ) : ( + + ))}
@@ -50,11 +93,11 @@ export const App: React.FC = () => { 'is-parent', 'is-8-desktop', 'Sidebar', - 'Sidebar--open', + { 'Sidebar--open': selectedPost }, )} >
- + {selectedPost && ()}
diff --git a/src/api/comments.ts b/src/api/comments.ts new file mode 100644 index 000000000..b220836ba --- /dev/null +++ b/src/api/comments.ts @@ -0,0 +1,24 @@ +import { Comment } from '../types/Comment'; +import { Post } from '../types/Post'; +import { client } from '../utils/fetchClient'; + +type Parameters = { + name: string; + email: string; + body: string; + postId: number; +}; + +export const getComments = ({ id }: Post) => { + return client.get(`/comments?postId=${id}`); +}; + +export const deleteComment = (id: number) => { + return client.delete(`/comments/${id}`); +}; + +export const addComment = ( + comment: Parameters, +) => { + return client.post('/comments', comment); +}; diff --git a/src/api/posts.ts b/src/api/posts.ts new file mode 100644 index 000000000..bc8febf7f --- /dev/null +++ b/src/api/posts.ts @@ -0,0 +1,7 @@ +import { Post } from '../types/Post'; +import { User } from '../types/User'; +import { client } from '../utils/fetchClient'; + +export const getPosts = ({ id }: User) => { + return client.get(`/posts?userId=${id}`); +}; diff --git a/src/api/users.ts b/src/api/users.ts new file mode 100644 index 000000000..816c8274b --- /dev/null +++ b/src/api/users.ts @@ -0,0 +1,6 @@ +import { User } from '../types/User'; +import { client } from '../utils/fetchClient'; + +export const getUsers = () => { + return client.get('/users'); +}; diff --git a/src/components/NewCommentForm.tsx b/src/components/NewCommentForm.tsx index 73a8a0b45..7e965351b 100644 --- a/src/components/NewCommentForm.tsx +++ b/src/components/NewCommentForm.tsx @@ -1,8 +1,97 @@ -import React from 'react'; +import React, { useState } from 'react'; +import classNames from 'classnames'; + +import { addComment } from '../api/comments'; +import { Comment } from '../types/Comment'; + +type Props = { + setComments: React.Dispatch>; + postId: number; + setIsError: React.Dispatch>; +}; + +export const NewCommentForm: React.FC = ({ + setComments, + postId, + setIsError, +}) => { + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [body, setBody] = useState(''); + const [isNameError, setIsNameError] = useState(false); + const [isEmailError, setIsEmailError] = useState(false); + const [isCommentError, setIsCommentError] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const handleName = (event: React.ChangeEvent) => { + setName(event.target.value); + setIsNameError(false); + }; + + const handleEmail = (event: React.ChangeEvent) => { + setEmail(event.target.value); + setIsEmailError(false); + }; + + const handleNewComment = (event: React.ChangeEvent) => { + setBody(event.target.value); + setIsCommentError(false); + }; + + const handleOnSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + const comment = { + name: name.trim(), + email: email.trim(), + body: body.trim(), + postId, + }; + + if (!comment.name.length) { + setIsNameError(true); + } + + if (!comment.email.length) { + setIsEmailError(true); + } + + if (!comment.body.length) { + setIsCommentError(true); + } + + if (comment.name.length && comment.email.length && comment.body.length) { + setIsLoading(true); + + addComment(comment) + .then(newComment => { + setComments(comments => ( + (comments) ? [...comments, newComment] : [newComment] + )); + setIsError(false); + setBody(''); + }) + .catch(() => setIsError(true)) + .finally(() => { + setIsLoading(false); + }); + } + }; + + const reset = () => { + setName(''); + setEmail(''); + setBody(''); + setIsNameError(false); + setIsEmailError(false); + setIsCommentError(false); + }; -export const NewCommentForm: React.FC = () => { return ( -
+ handleOnSubmit(event)} + >
-

- Name is required -

+ {isNameError && ( +

+ Name is required +

+ )}
@@ -41,28 +139,37 @@ export const NewCommentForm: React.FC = () => {
- - - + {isEmailError && ( + + + + )}
-

- Email is required -

+ {isEmailError && ( +

+ Email is required +

+ )}
@@ -75,25 +182,42 @@ export const NewCommentForm: React.FC = () => { id="comment-body" name="body" placeholder="Type comment here" - className="textarea is-danger" + className={classNames( + 'textarea', + { 'is-danger': isCommentError }, + )} + value={body} + onChange={handleNewComment} />
-

- Enter some text -

+ {isCommentError && ( +

+ 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..fc5058052 100644 --- a/src/components/PostDetails.tsx +++ b/src/components/PostDetails.tsx @@ -1,117 +1,138 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; + import { Loader } from './Loader'; import { NewCommentForm } from './NewCommentForm'; -export const PostDetails: React.FC = () => { +import { Post } from '../types/Post'; +import { Comment } from '../types/Comment'; + +import * as CommentsServices from '../api/comments'; + +type Props = { + post: Post; +}; + +export const PostDetails: React.FC = ({ post }) => { + const [comments, setComments] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + const [isComment, setIsComment] = useState(false); + + useEffect(() => { + setIsLoading(true); + setIsError(false); + setIsComment(false); + + CommentsServices.getComments(post) + .then(setComments) + .catch(() => setIsError(true)) + .finally(() => setIsLoading(false)); + }, [post]); + + const { id, title, body } = post; + + const deleteComment = (commentId: number) => () => { + if (comments) { + setComments(() => comments.filter(comment => comment.id !== commentId)); + } + + CommentsServices.deleteComment(commentId) + .catch(() => { + setIsError(true); + setComments(comments); + }); + }; + + const onButtonClick = () => { + setIsComment(true); + }; + return (

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

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

- + {isLoading && ( + + )} -
- Something went wrong -
+ {isError && ( +
+ Something went wrong +
+ )} -

- No comments yet -

+ {!isLoading && !isError && comments && (!comments.length ? ( +

+ No comments yet +

+ ) : ( + <> +

Comments:

-

Comments:

- -
-
- - Misha Hrynko - - -
+ {comments.map(comment => { + const { name, email } = comment; -
- Some comment -
-
- - - -
+ +
+ {comment.body} +
+ + ); + })} + + ))} + + {!isComment && !isError && !isLoading && ( + + )}
- + {isComment && ( + + )}
); diff --git a/src/components/PostsList.tsx b/src/components/PostsList.tsx index 6e6efc0f3..d5161bb38 100644 --- a/src/components/PostsList.tsx +++ b/src/components/PostsList.tsx @@ -1,85 +1,69 @@ import React from 'react'; +import classNames from 'classnames'; -export const PostsList: React.FC = () => ( -
-

Posts:

+import { Post } from '../types/Post'; - - - - - - - - +type Props = { + posts: Post[]; + onChoose: React.Dispatch>; + selectedPost: Post | null; +}; - - - +export const PostsList: React.FC = ({ + posts, + onChoose, + selectedPost, +}) => { + const choosePost = (post: Post | null) => () => { + onChoose(post); + }; - + return ( +
+

Posts:

-
- +
#Title
17 - fugit voluptas sed molestias voluptatem provident - - -
+ + + + + + + - - + + {posts.map(post => { + const isSelected = (selectedPost?.id === post.id); - + return ( + + - - + - - - - - - - - - - - - - - -
#Title
18
- voluptate et itaque vero tempora molestiae -
{post.id} - -
+ {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 cb83a8f68..4e3eea53c 100644 --- a/src/components/UserSelector.tsx +++ b/src/components/UserSelector.tsx @@ -1,6 +1,31 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import cn from 'classnames'; + +import { User } from '../types/User'; +import { getUsers } from '../api/users'; + +type Props = { + selectedUser: User | null; + onChoose: React.Dispatch>; +}; + +export const UserSelector: React.FC = ({ selectedUser, onChoose }) => { + const [users, setUsers] = useState([]); + const [isDropDown, setIsDropDown] = useState(false); + + useEffect(() => { + getUsers() + .then(setUsers) + .catch(() => {}); + }, []); + + const handleIsDropDown = () => setIsDropDown(!isDropDown); + + const selectUser = (user: User) => () => { + handleIsDropDown(); + onChoose(user); + }; -export const UserSelector: React.FC = () => { return (
{ className="button" aria-haspopup="true" aria-controls="dropdown-menu" + onClick={handleIsDropDown} > - Choose a user + + {!selectedUser ? 'Choose a user' : selectedUser.name} +