From 10c268dd84e6e9aa2dd757893ae5a26ed289665d Mon Sep 17 00:00:00 2001 From: Seva Podolskiy Date: Thu, 26 Sep 2024 14:30:47 -0400 Subject: [PATCH] solution --- src/App.tsx | 137 +++++++++++---- src/components/Comment.tsx | 31 ++++ src/components/CommentList.tsx | 29 ++++ src/components/NewCommentForm.tsx | 151 ++++++++++++---- src/components/PostDetails.tsx | 164 ++++++++---------- src/components/PostsList.tsx | 134 ++++++-------- src/components/UserSelector.tsx | 64 ++++--- src/utils/commentsClient.tsx | 16 ++ src/utils/{fetchClient.ts => fetchClient.tsx} | 6 +- src/utils/postClient.tsx | 8 + src/utils/userClient.tsx | 8 + 11 files changed, 491 insertions(+), 257 deletions(-) create mode 100644 src/components/Comment.tsx create mode 100644 src/components/CommentList.tsx create mode 100644 src/utils/commentsClient.tsx rename src/utils/{fetchClient.ts => fetchClient.tsx} (84%) create mode 100644 src/utils/postClient.tsx create mode 100644 src/utils/userClient.tsx diff --git a/src/App.tsx b/src/App.tsx index 017957182..98018dd08 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,53 +8,116 @@ 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 = () => { + //region States + 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); + //endregion -
-

No user selected

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

No user selected

+ )} + + {loading && } + + {error && ( +
+ Something went wrong! +
+ )} + + {userSelected && !loading ? ( + userSelPosts.length > 0 ? ( + + ) : ( +
+ No posts yet +
+ ) + ) : undefined} +
-
-
-
- +
+
+ +
-
-
-); +
+ ); +}; diff --git a/src/components/Comment.tsx b/src/components/Comment.tsx new file mode 100644 index 000000000..93e790a66 --- /dev/null +++ b/src/components/Comment.tsx @@ -0,0 +1,31 @@ +import { Comment } from '../types/Comment'; + +type Props = { + comment: Comment; + onDelete: (id: number) => void; +}; + +export const CommentObj: React.FC = ({ comment, onDelete }) => { + return ( + + ); +}; diff --git a/src/components/CommentList.tsx b/src/components/CommentList.tsx new file mode 100644 index 000000000..3c6213172 --- /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 && comments.length > 0 ? ( + comments.map(comment => { + return ( + + ); + }) + ) : ( +

+ No comments yet +

+ )} + + ); +}; diff --git a/src/components/NewCommentForm.tsx b/src/components/NewCommentForm.tsx index 73a8a0b45..c3ff05b50 100644 --- a/src/components/NewCommentForm.tsx +++ b/src/components/NewCommentForm.tsx @@ -1,8 +1,78 @@ -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); + errors.push(!name); + + setErrorEmail(!email); + errors.push(!email); + + setErrorBody(!body); + errors.push(!body); + + 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, + email: email, + body: body, + }) + .then(() => { + setBody(''); + onUpdate(Math.random()); + }) + .catch(() => onError(true)) + .finally(() => setIsLoading(false)); + }; + + const handleClear = () => { + setErrorName(false); + setErrorEmail(false); + setErrorBody(false); + }; -export const NewCommentForm: React.FC = () => { return ( -
+
- -

- Name is required -

+ {errorName && ( +

+ Name is required +

+ )}
@@ -45,24 +120,28 @@ export const NewCommentForm: React.FC = () => { name="email" id="comment-author-email" placeholder="email@test.com" - className="input is-danger" + className={classNames('input', { 'is-danger': errorEmail })} + value={email} + onChange={e => setEmail(e.target.value)} /> - - - - + {errorEmail && ( + + + + )}
- -

- Email is required -

+ {errorEmail && ( +

+ Email is required +

+ )}
@@ -75,18 +154,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..db7714235 100644 --- a/src/components/PostDetails.tsx +++ b/src/components/PostDetails.tsx @@ -1,106 +1,96 @@ -import React from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; +import { commentClient } from '../utils/commentsClient'; + +import { Post } from '../types/Post'; +import { Comment } from '../types/Comment'; + import { Loader } from './Loader'; import { NewCommentForm } from './NewCommentForm'; +import { CommentList } from './CommentList'; + +type Props = { + post: Post | null; +}; + +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); + + useEffect(() => { + setCommenting(false); + if (post) { + setLoading(true); + commentClient + .get(post.id) + .then(res => { + setError(false); + setComments(() => res); + }) + .catch(() => setError(true)) + .finally(() => setLoading(false)); + } + }, [post, updateFlag]); + + const handleCommentDelete = useCallback((commentId: number) => { + setComments(prevComnts => + prevComnts.filter(comnt => comnt.id !== commentId), + ); + commentClient.delete(commentId); + }, []); -export const PostDetails: React.FC = () => { return (
-

- #18: voluptate et itaque vero tempora molestiae -

+

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

-

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

+

{post?.body}

- - -
- Something went wrong -
- -

- No comments yet -

- -

Comments:

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

Comments:

+ + + )} - + {!commenting && ( + + )} + + )}
- + {!error && commenting && post && ( + + )}
); diff --git a/src/components/PostsList.tsx b/src/components/PostsList.tsx index cf90f04b0..83b0aa5c9 100644 --- a/src/components/PostsList.tsx +++ b/src/components/PostsList.tsx @@ -1,86 +1,66 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { Post } from '../types/Post'; +import classNames from 'classnames'; -export const PostsList: React.FC = () => ( -
-

Posts:

+type Props = { + posts: Post[]; + onOpen: (post: Post | null) => void; +}; - - - - - - {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} - - - +export const PostsList: React.FC = ({ posts, onOpen }) => { + const [openedPost, setOpendPost] = useState(null); - - - + const handleClick = (post: Post) => { + if (post.id === openedPost?.id) { + setOpendPost(null); + onOpen(null); + } else { + setOpendPost(post); + onOpen(post); + } + }; - + return ( +
+

Posts:

-
- +
#Title
17 - fugit voluptas sed molestias voluptatem provident - - -
+ + + + + {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} + + + - - + + {posts.map(post => { + const isOppened = openedPost?.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 c89442841..b7777ebda 100644 --- a/src/components/UserSelector.tsx +++ b/src/components/UserSelector.tsx @@ -1,6 +1,26 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { User } from '../types/User'; +import classNames from 'classnames'; + +type Props = { + users: User[]; + onSelect: (name: User) => void; +}; + +export const UserSelector: React.FC = ({ users, onSelect }) => { + // region States + const [userSelect, setUserSelect] = useState(null); + const [visibleList, setVisibleList] = useState(false); + //endregion + + // region Event handlers + const handleUserSelect = (user: User): void => { + setUserSelect(user); + setVisibleList(false); + onSelect(user); + }; + //endregion -export const UserSelector: React.FC = () => { return (
@@ -9,8 +29,9 @@ export const UserSelector: React.FC = () => { className="button" aria-haspopup="true" aria-controls="dropdown-menu" + onClick={() => setVisibleList(prev => !prev)} > - Choose a user + {userSelect ? userSelect.name : 'Choose a user'}