diff --git a/src/App.tsx b/src/App.tsx index 017957182..c51b739e3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,53 +8,119 @@ 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 { userClient } from './utils/userClient'; +import { postClient } from './utils/postClient'; +import { Post } from './types/Post'; -export const App = () => ( -
-
-
-
-
-
- -
+export const App = () => { + const [users, setUsers] = useState([]); + const [userSelected, setUserSelected] = useState(null); + const [userSelPosts, setUserSelPosts] = useState([]); + const [openedPost, setOpendPost] = useState(null); + const [error, setError] = useState(false); + const [loading, setLoading] = useState(false); -
-

No user selected

+ const readyToShowPosts = userSelected && !loading; - + useEffect(() => { + setLoading(true); + userClient + .getAll() + .then(res => { + setUsers(res); + setError(false); + }) + .catch(() => setError(true)) + .finally(() => setLoading(false)); + }, []); -
- Something went wrong! -
+ useEffect(() => { + setOpendPost(null); + + if (userSelected) { + setLoading(true); + postClient + .get(userSelected.id) + .then(res => { + setUserSelPosts(() => res); + setError(false); + }) + .catch(() => setError(true)) + .finally(() => { + setLoading(false); + }); + } + }, [userSelected]); -
- No posts yet + const handleUserSelect = (user: User) => { + setUserSelected(user); + }; + + return ( +
+
+
+
+
+
+
- +
+ {!userSelected && ( +

No user selected

+ )} + + {loading && } + + {error && ( +
+ Something went wrong! +
+ )} + + {readyToShowPosts && userSelPosts.length > 0 && ( + + )} + + {readyToShowPosts && userSelPosts.length <= 0 && ( +
+ No posts yet +
+ )} +
-
-
-
- +
+
+ +
-
-
-); +
+ ); +}; diff --git a/src/components/Comment.tsx b/src/components/Comment.tsx new file mode 100644 index 000000000..3343cf5fe --- /dev/null +++ b/src/components/Comment.tsx @@ -0,0 +1,33 @@ +import { Comment } from '../types/Comment'; + +type Props = { + comment: Comment; + onDelete: (id: number) => void; +}; + +export const CommentObj: React.FC = ({ comment, onDelete }) => { + const { id, email, name, body } = comment; + + return ( +
+
+ + {name} + + +
+ +
+ {body} +
+
+ ); +}; diff --git a/src/components/CommentList.tsx b/src/components/CommentList.tsx new file mode 100644 index 000000000..5fa7cf8d0 --- /dev/null +++ b/src/components/CommentList.tsx @@ -0,0 +1,29 @@ +import { Comment } from '../types/Comment'; +import { CommentObj } from './Comment'; + +type Props = { + comments: Comment[]; + onDelete: (id: number) => void; +}; + +export const CommentList: React.FC = ({ comments, onDelete }) => { + return ( + <> + {comments.length > 0 ? ( + comments.map(comment => { + return ( + + ); + }) + ) : ( +

+ No comments yet +

+ )} + + ); +}; diff --git a/src/components/NewCommentForm.tsx b/src/components/NewCommentForm.tsx index 73a8a0b45..9006b8462 100644 --- a/src/components/NewCommentForm.tsx +++ b/src/components/NewCommentForm.tsx @@ -1,8 +1,83 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { Post } from '../types/Post'; +import classNames from 'classnames'; +import { commentClient } from '../utils/commentsClient'; + +type Props = { + post: Post; + onError: (error: boolean) => void; + onUpdate: (v: number | null) => void; +}; +export const NewCommentForm: React.FC = ({ + post, + onError, + onUpdate, +}) => { + const [errorName, setErrorName] = useState(false); + const [errorEmail, setErrorEmail] = useState(false); + const [errorbody, setErrorBody] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [body, setBody] = useState(''); + + const anyErrorCheck = () => { + const errors = []; + + setErrorName(!name.trim()); + errors.push(!name.trim()); + + setErrorEmail(!email.trim()); + errors.push(!email.trim()); + + setErrorBody(!body.trim()); + errors.push(!body.trim()); + + return errors.some(e => e === true); + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + setIsLoading(true); + + if (anyErrorCheck()) { + setIsLoading(false); + + return; + } + + commentClient + .add({ + postId: post.id, + name: name.trim(), + email: email.trim(), + body: body.trim(), + }) + .then(res => { + if (!res.id) { + throw new Error(); + } + + setBody(''); + onUpdate(Math.random()); + }) + .catch(() => onError(true)) + .finally(() => setIsLoading(false)); + }; + + const handleClear = () => { + setBody(''); + setErrorName(false); + setErrorEmail(false); + setErrorBody(false); + }; -export const NewCommentForm: React.FC = () => { return ( -
+
- -

- Name is required -

+ {errorName && ( +

+ Name is required +

+ )}
@@ -41,28 +121,32 @@ export const NewCommentForm: React.FC = () => {
setEmail(e.target.value)} /> - - - - + {errorEmail && ( + + + + )}
- -

- Email is required -

+ {errorEmail && ( +

+ Email is required +

+ )}
@@ -75,18 +159,26 @@ export const NewCommentForm: React.FC = () => { id="comment-body" name="body" placeholder="Type comment here" - className="textarea is-danger" + className={classNames('textarea', { 'is-danger': errorbody })} + value={body} + onChange={e => setBody(e.target.value)} />
- -

- Enter some text -

+ {errorbody && ( +

+ Enter some text +

+ )}
-
diff --git a/src/components/PostDetails.tsx b/src/components/PostDetails.tsx index 2f82db916..f69845ced 100644 --- a/src/components/PostDetails.tsx +++ b/src/components/PostDetails.tsx @@ -1,106 +1,95 @@ -import React from 'react'; -import { Loader } from './Loader'; -import { NewCommentForm } from './NewCommentForm'; +import React, { useCallback, useEffect, useState } from 'react'; +import { commentClient } from '../utils/commentsClient'; -export const PostDetails: React.FC = () => { - return ( -
-
-
-

- #18: voluptate et itaque vero tempora molestiae -

+import { Post } from '../types/Post'; +import { Comment } from '../types/Comment'; -

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

-
+import { Loader } from './Loader'; +import { NewCommentForm } from './NewCommentForm'; +import { CommentList } from './CommentList'; -
- +type Props = { + post: Post | null; +}; -
- Something went wrong -
+export const PostDetails: React.FC = ({ post }) => { + const [comments, setComments] = useState([]); + const [error, setError] = useState(false); + const [loading, setLoading] = useState(false); + const [commenting, setCommenting] = useState(false); + const [updateFlag, setUpdateFlag] = useState(null); -

- No comments yet -

+ const readyToComment = !error && !loading; -

Comments:

+ useEffect(() => { + setCommenting(false); + }, [post]); -
-
- - Misha Hrynko - - -
+ useEffect(() => { + if (post) { + setLoading(true); + commentClient + .get(post.id) + .then(res => { + setError(false); + setComments(() => res); + }) + .catch(() => setError(true)) + .finally(() => setLoading(false)); + } + }, [post, updateFlag]); -
- Some comment -
-
+ const handleCommentDelete = useCallback((commentId: number) => { + setComments(prevComnts => + prevComnts.filter(comnt => comnt.id !== commentId), + ); + commentClient.delete(commentId); + }, []); -
-
- - Misha Hrynko - + return ( +
+
+
+

{`#${post?.id}: ${post?.title}`}

- -
-
- One more comment -
-
+

{post?.body}

+
-
-
- - Misha Hrynko - +
+ {loading && } - + {error && ( +
+ Something went wrong
+ )} -
- {'Multi\nline\ncomment'} -
-
+ {readyToComment && comments.length > 0 && ( + <> +

Comments:

+ + + )} - + {readyToComment && ( + + )}
- + {!error && commenting && post && ( + + )}
); diff --git a/src/components/PostItem.tsx b/src/components/PostItem.tsx new file mode 100644 index 000000000..b13d3828a --- /dev/null +++ b/src/components/PostItem.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Post } from '../types/Post'; +import classNames from 'classnames'; + +type Props = { + post: Post; + isOppened: boolean; + onClick: (post: Post) => void; +}; + +export const PostItem: React.FC = ({ post, isOppened, onClick }) => { + const { id, title } = post; + + return ( + + {id} + + {title} + + + + + + ); +}; diff --git a/src/components/PostsList.tsx b/src/components/PostsList.tsx index cf90f04b0..57af6f412 100644 --- a/src/components/PostsList.tsx +++ b/src/components/PostsList.tsx @@ -1,86 +1,48 @@ -import React from 'react'; - -export const PostsList: React.FC = () => ( -
-

Posts:

- - - - - - - {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#Title
17 - fugit voluptas sed molestias voluptatem provident - - -
18 - voluptate et itaque vero tempora molestiae - - -
19adipisci placeat illum aut reiciendis qui - -
20doloribus ad provident suscipit at - -
-
-); +import { Post } from '../types/Post'; +import { PostItem } from './PostItem'; + +type Props = { + posts: Post[]; + openedPost: Post | null; + onOpen: (post: Post | null) => void; +}; + +export const PostsList: React.FC = ({ posts, openedPost, onOpen }) => { + const handleClick = (post: Post) => { + if (post.id === openedPost?.id) { + onOpen(null); + } else { + onOpen(post); + } + }; + + return ( +
+

Posts:

+ + + + + + + {} + + + + + + {posts.map(post => { + return ( + + ); + })} + +
#Title
+
+ ); +}; diff --git a/src/components/UserSelector.tsx b/src/components/UserSelector.tsx index c89442841..bde402bc6 100644 --- a/src/components/UserSelector.tsx +++ b/src/components/UserSelector.tsx @@ -1,6 +1,25 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { User } from '../types/User'; +import classNames from 'classnames'; + +type Props = { + users: User[]; + selectedUser: User | null; + onSelect: (name: User) => void; +}; + +export const UserSelector: React.FC = ({ + users, + selectedUser, + onSelect, +}) => { + const [visible, setVisible] = useState(false); + + const handleUserSelect = (user: User): void => { + setVisible(false); + onSelect(user); + }; -export const UserSelector: React.FC = () => { return (
@@ -9,8 +28,9 @@ export const UserSelector: React.FC = () => { className="button" aria-haspopup="true" aria-controls="dropdown-menu" + onClick={() => setVisible(prev => !prev)} > - Choose a user + {selectedUser ? selectedUser.name : 'Choose a user'}