diff --git a/src/App.tsx b/src/App.tsx index 017957182..7916fa029 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import classNames from 'classnames'; import 'bulma/css/bulma.css'; @@ -8,53 +9,151 @@ 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 { getUserPosts } from './api/ClientFunctions'; +import { Post } from './types/Post'; -export const App = () => ( -
-
-
-
-
-
- -
+export const App = () => { + // #region states -
-

No user selected

+ const [currUser, setCurrUser] = useState(null); + const [isDdActive, setIsDdActive] = useState(false); + const [userPosts, setUserPosts] = useState([]); + const [loadingPostsError, setLoadingPostsError] = useState(false); + const [arePostsLoading, setArePostsLoading] = useState(false); + const [isNoPosts, setIsNoPosts] = useState(false); + const [openedPost, setOpenedPost] = useState(null); + const [prevUser, setPrevUser] = useState(null); + const [doesFormExist, setDoesFormExist] = useState(false); - + // #endregion + // #region handlers -
- Something went wrong! -
+ const sectionOnClickHandler = () => { + if (isDdActive) { + setIsDdActive(false); + } + }; + + // #endregion + // #region useEffects + + useEffect(() => { + if (currUser) { + const { id } = currUser; + + setArePostsLoading(true); + + getUserPosts(id) + .then(posts => { + setUserPosts(posts); + + if (loadingPostsError) { + setLoadingPostsError(false); + } + }) + .catch(() => setLoadingPostsError(true)) + .finally(() => setArePostsLoading(false)); + } + }, [currUser]); + + useEffect(() => { + if (userPosts.length === 0 && currUser) { + setIsNoPosts(true); + } else if (userPosts.length > 0 && isNoPosts) { + setIsNoPosts(false); + } + }, [currUser, isNoPosts, userPosts]); + + useEffect(() => { + if (prevUser?.id !== currUser?.id) { + setOpenedPost(null); + } + }, [prevUser, currUser]); + + useEffect(() => { + if (doesFormExist) { + setDoesFormExist(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [openedPost]); -
- No posts yet + // #endregion + // #region markups + + const loadingPostsErrorMarkup = ( +
+ Something went wrong! +
+ ); + const noPostsMarkup = ( +
+ No posts yet +
+ ); + const postsList = ( + + ); + + // #endregion + + return ( +
+
+
+
+
+
+
- +
+ {!currUser &&

No user selected

} + + {arePostsLoading ? ( + + ) : ( + (loadingPostsError && loadingPostsErrorMarkup) || + (isNoPosts && noPostsMarkup) || + (userPosts.length > 0 && postsList) + )} +
-
-
-
- +
+
+ {openedPost && ( + + )} +
-
-
-); +
+ ); +}; diff --git a/src/api/ClientFunctions.ts b/src/api/ClientFunctions.ts new file mode 100644 index 000000000..9f6ec4250 --- /dev/null +++ b/src/api/ClientFunctions.ts @@ -0,0 +1,29 @@ +import { client } from '../utils/fetchClient'; +import { User } from '../types/User'; +import { Post } from '../types/Post'; +import { Comment } from '../types/Comment'; + +export const getUsers = () => { + return client.get('/users'); +}; + +export const getUserPosts = (userId: number) => { + return client.get(`/posts?userId=${userId}`); +}; + +export const getPostComments = (postId: number) => { + return client.get(`/comments?postId=${postId}`); +}; + +export const deleteComment = (id: number) => { + return client.delete(`/comments/${id}`); +}; + +export const postComment = ({ + postId, + name, + email, + body, +}: Omit) => { + return client.post('/comments', { postId, name, email, body }); +}; diff --git a/src/components/NewCommentForm.tsx b/src/components/NewCommentForm.tsx index 73a8a0b45..60f69fe55 100644 --- a/src/components/NewCommentForm.tsx +++ b/src/components/NewCommentForm.tsx @@ -1,8 +1,117 @@ -import React from 'react'; +/* eslint-disable react-hooks/exhaustive-deps */ +import classNames from 'classnames'; +import React, { useEffect, useState } from 'react'; +import { postComment } from '../api/ClientFunctions'; +import { Comment } from '../types/Comment'; + +interface Props { + postId: number; + commentsError: boolean; + setCommentsError: (value: boolean) => void; + setDoesFormExist: (value: boolean) => void; + setComments: React.Dispatch>; +} + +export const NewCommentForm: React.FC = ({ + postId, + commentsError, + setCommentsError, + setDoesFormExist, + setComments, +}) => { + // #region states + + const [isNameValid, setIsNameValid] = useState(true); + const [isEmailValid, setIsEmailValid] = useState(true); + const [isCommentValid, setIsCommentValid] = useState(true); + const [nameValue, setNameValue] = useState(''); + const [emailValue, setEmailValue] = useState(''); + const [commentValue, setCommentValue] = useState(''); + const [isSubmitLoading, setIsSubmitLoading] = useState(false); + + // #endregion + // #region handlers + + const clearHandler = () => { + setNameValue(''); + setEmailValue(''); + setCommentValue(''); + setIsNameValid(true); + setIsEmailValid(true); + setIsCommentValid(true); + }; + + const onSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const IS_VALID = + nameValue.length > 0 && emailValue.length > 0 && commentValue.length > 0; + + if (nameValue.length === 0) { + setIsNameValid(false); + } + + if (emailValue.length === 0) { + setIsEmailValid(false); + } + + if (commentValue.length === 0) { + setIsCommentValid(false); + } + + if (!IS_VALID) { + return; + } + + setIsSubmitLoading(true); + + const comment = { + postId: postId, + name: nameValue, + email: emailValue, + body: commentValue, + }; + + if (commentsError) { + setCommentsError(false); + } + + postComment(comment) + .then(_comment => { + setComments(currentComments => [...currentComments, _comment]); + setCommentValue(''); + }) + .catch(() => { + setCommentsError(true); + setDoesFormExist(false); + }) + .finally(() => setIsSubmitLoading(false)); + }; + + // #endregion + // #region useEffects + + useEffect(() => { + if (!isNameValid) { + setIsNameValid(true); + } + }, [nameValue]); + + useEffect(() => { + if (!isEmailValid) { + setIsEmailValid(true); + } + }, [emailValue]); + + useEffect(() => { + if (!isCommentValid) { + setIsCommentValid(true); + } + }, [commentValue]); + + // #endregion -export const NewCommentForm: React.FC = () => { return ( -
+
-

- Name is required -

+ {!isNameValid && ( +

+ Name is required +

+ )}
@@ -45,24 +162,32 @@ export const NewCommentForm: React.FC = () => { name="email" id="comment-author-email" placeholder="email@test.com" - className="input is-danger" + value={emailValue} + onChange={event => setEmailValue(event.target.value)} + className={classNames('input', { + 'is-danger': !isEmailValid, + })} /> - - - + {!isEmailValid && ( + + + + )}
-

- Email is required -

+ {!isEmailValid && ( +

+ Email is required +

+ )}
@@ -75,25 +200,39 @@ export const NewCommentForm: React.FC = () => { id="comment-body" name="body" placeholder="Type comment here" - className="textarea is-danger" + value={commentValue} + onChange={event => setCommentValue(event.target.value)} + className={classNames('textarea', { + 'is-danger': !isCommentValid, + })} />
-

- Enter some text -

+ {!isCommentValid && ( +

+ 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..bb03aded0 100644 --- a/src/components/PostDetails.tsx +++ b/src/components/PostDetails.tsx @@ -1,107 +1,154 @@ -import React from 'react'; +/* eslint-disable react-hooks/exhaustive-deps */ +import React, { useEffect, useState } from 'react'; import { Loader } from './Loader'; import { NewCommentForm } from './NewCommentForm'; +import { Post } from '../types/Post'; +import { Comment } from '../types/Comment'; +import { deleteComment, getPostComments } from '../api/ClientFunctions'; -export const PostDetails: React.FC = () => { - return ( -
-
-
-

- #18: voluptate et itaque vero tempora molestiae -

- -

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

-
+interface Props { + openedPost: Post; + doesFormExist: boolean; + setDoesFormExist: (value: boolean) => void; +} + +export const PostDetails: React.FC = React.memo( + ({ openedPost, doesFormExist, setDoesFormExist }) => { + // #region states + + const [comments, setComments] = useState([]); + const [commentsError, setCommentsError] = useState(false); + const [areCommLoading, setAreCommLoading] = useState(false); + + // #endregion + // #region variables + + const { id, title, body } = openedPost; + const conditionForButton = + !commentsError && !areCommLoading && !doesFormExist; + + // #endregion + // #region useEffects + + useEffect(() => { + setAreCommLoading(true); + + getPostComments(id) + .then(_comments => { + setComments(_comments); + + if (commentsError) { + setCommentsError(false); + } + }) + .catch(() => setCommentsError(true)) + .finally(() => setAreCommLoading(false)); + }, [id]); + + // #endregion + // #region handlers + + const deleteHandler = (_id: number) => { + const updatedComments = comments.filter(comment => comment.id !== _id); + + setComments(updatedComments); + + deleteComment(_id); + }; + + // #endregion + // #region markups -
- + const errorMarkup = ( +
+ Something went wrong +
+ ); + const noCommentsMarkup = ( +

+ No comments yet +

+ ); + const commentsTitleMarkup =

Comments:

; + const commentsMarkup = comments.map(comment => { + // eslint-disable-next-line @typescript-eslint/no-shadow + const { id, name, body, email } = comment; -
- Something went wrong + return ( +
+
+ + {name} + +
-

- No comments yet -

+
+ {body} +
+
+ ); + }); -

Comments:

+ // #endregion -
-
- - Misha Hrynko - - -
+ return ( +
+
+
+

+ {`#${id}`}: {title} +

-
- Some comment -
-
+

{body}

+
-
-
- - Misha Hrynko - +
+ {areCommLoading ? ( + + ) : ( + (commentsError && errorMarkup) || + (comments.length === 0 && noCommentsMarkup) || + (commentsTitleMarkup && commentsMarkup) + )} + {conditionForButton && ( -
-
- One more comment -
-
- -
- -
- {'Multi\nline\ncomment'} -
- - - + {doesFormExist && ( + + )}
- -
-
- ); -}; + ); + }, +); + +PostDetails.displayName = 'PostDetails'; diff --git a/src/components/PostsList.tsx b/src/components/PostsList.tsx index cf90f04b0..c885ef834 100644 --- a/src/components/PostsList.tsx +++ b/src/components/PostsList.tsx @@ -1,86 +1,69 @@ import React from 'react'; +import { Post } from '../types/Post'; +import classNames from 'classnames'; -export const PostsList: React.FC = () => ( -
-

Posts:

+interface Props { + userPosts: Post[]; + openedPost: Post | null; + setOpenedPost: (post: Post | null) => void; +} - - - - - - {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} - - - +export const PostsList: React.FC = ({ + userPosts, + openedPost, + setOpenedPost, +}) => { + const clickHandler = (post: Post) => { + if (openedPost?.id === post.id) { + setOpenedPost(null); - - - + return; + } - + setOpenedPost(post); + }; - - + return ( +
+

Posts:

-
- +
#Title
17 - fugit voluptas sed molestias voluptatem provident - - -
18
+ + + + + + + - + + {userPosts?.map(post => { + const { id, title } = post; + const isNotOpen = openedPost?.id !== id; - - + return ( + + - - - + - - - - - - - - - - -
#Title
- voluptate et itaque vero tempora molestiae -
- -
{id}
19adipisci placeat illum aut reiciendis qui{title} - -
20doloribus ad provident suscipit at - -
-
-); + + + + + ); + })} + + + + ); +}; diff --git a/src/components/UserSelector.tsx b/src/components/UserSelector.tsx index c89442841..d73b9a0ef 100644 --- a/src/components/UserSelector.tsx +++ b/src/components/UserSelector.tsx @@ -1,16 +1,62 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { getUsers } from '../api/ClientFunctions'; +import { User } from '../types/User'; +import classNames from 'classnames'; + +interface Props { + currUser: User | null; + isDdActive: boolean; + setPrevUser: (user: User | null) => void; + setCurrUser: (user: User | null) => void; + setIsDdActive: (value: boolean) => void; +} + +export const UserSelector: React.FC = ({ + currUser, + isDdActive, + setPrevUser, + setCurrUser, + setIsDdActive, +}) => { + const [users, setUsers] = useState([]); + + // #region handlers + + const buttonOnClickHandler = () => { + if (isDdActive) { + setIsDdActive(false); + + return; + } + + setIsDdActive(true); + }; + + const selectUserHandler = (user: User) => { + setPrevUser(currUser); + setCurrUser(user); + }; + + // #endregion + + useEffect(() => { + getUsers().then(_users => setUsers(_users)); + }, []); -export const UserSelector: React.FC = () => { return ( -
+