diff --git a/src/App.tsx b/src/App.tsx index 017957182..0563328aa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,53 +8,136 @@ 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 { getData } from './api/fetch'; -export const App = () => ( -
-
-
-
-
-
- -
+export const App = () => { + const [users, setUsers] = useState([]); + const [userId, setUserId] = useState(null); -
-

No user selected

+ const [isLoading, setIsLoading] = useState(false); + const [catchError, setCatchError] = useState(false); - + const [posts, setPosts] = useState([]); + const [selectedPost, setSelectedpost] = useState(null); + const [emptyPosts, setEmptyPosts] = useState(false); -
- Something went wrong! -
+ const fetchUsers = async () => { + try { + setIsLoading(true); + const url = '/users'; + const currentUsers = await getData(url); + + setUsers(currentUsers); + } catch { + setCatchError(true); + } finally { + setIsLoading(false); + setTimeout(() => { + setCatchError(false); + }, 3000); + } + }; + + useEffect(() => { + fetchUsers(); + }, []); + + useEffect(() => { + setPosts([]); + + if (users && userId) { + const fetchPosts = async () => { + try { + setIsLoading(true); + const url = `/posts?userId=${userId}`; + + if (userId) { + const currentPosts = await getData(url); + + if (currentPosts.length === 0) { + setEmptyPosts(true); + } + + setPosts(currentPosts); + } + } catch { + setCatchError(true); + } finally { + setIsLoading(false); + } + }; -
- No posts yet + fetchPosts(); + } + }, [users, userId]); + + return ( +
+
+
+
+
+
+
- +
+ {!userId &&

No user selected

} + + {isLoading && } + + {catchError && ( +
+ Something went wrong! +
+ )} + + {emptyPosts && posts.length === 0 && userId && ( +
+ No posts yet +
+ )} + + {userId && posts.length > 0 && ( + + )} +
-
-
-
- +
+
+ {selectedPost && ( + + )} +
-
-
-); +
+ ); +}; diff --git a/src/api/fetch.ts b/src/api/fetch.ts new file mode 100644 index 000000000..1cb67ca5e --- /dev/null +++ b/src/api/fetch.ts @@ -0,0 +1,11 @@ +import { client } from '../utils/fetchClient'; + +export const getData = async (url: string) => { + try { + const data = await client.get(url); + + return data; + } catch (error) { + throw new Error('Unable to load data'); + } +}; diff --git a/src/components/NewCommentForm.tsx b/src/components/NewCommentForm.tsx index 73a8a0b45..e4171f946 100644 --- a/src/components/NewCommentForm.tsx +++ b/src/components/NewCommentForm.tsx @@ -1,8 +1,149 @@ -import React from 'react'; +import React, { useState } from 'react'; +import { Comment } from '../types/Comment'; +import { Post } from '../types/Post'; +import { client } from '../utils/fetchClient'; +import classNames from 'classnames'; + +type ChangeEvent = + | React.ChangeEvent + | React.ChangeEvent; + +type InputNames = 'name' | 'mail' | 'comment'; + +interface Props { + setComments: React.Dispatch>; + selectedPost: Post; + setLoadingError: React.Dispatch>; + setShowForm: React.Dispatch>; +} + +export const NewCommentForm: React.FC = ({ + setComments, + selectedPost, + setLoadingError, + setShowForm, +}) => { + const [userName, setUserName] = useState(''); + const [userMail, setUserMail] = useState(''); + const [commentText, setCommentText] = useState(''); + + const [isLoading, setIsLoading] = useState(false); + + const [userNameError, setUserNameError] = useState(false); + const [userMailError, setUserMailError] = useState(false); + const [commentTextError, setCommentTextError] = useState(false); + + const handleFullForm = (e: ChangeEvent, inputName: InputNames) => { + switch (inputName) { + case 'name': { + setUserName(e.target.value); + setUserNameError(false); + break; + } + + case 'mail': { + setUserMail(e.target.value); + setUserMailError(false); + break; + } + + case 'comment': { + setCommentText(e.target.value); + setCommentTextError(false); + break; + } + } + }; + + const cleanForm = () => { + setUserName(''); + setUserMail(''); + setCommentText(''); + + setUserNameError(false); + setUserMailError(false); + setCommentTextError(false); + }; + + const saveInputDataOnSuccess = () => { + setUserName(userName); + setUserMail(userMail); + setCommentText(''); + }; + + const validateInput = (input: string): boolean => { + const doesEmpty = input.trim().length > 0; + + return doesEmpty; + }; + + const addCommetns = async () => { + const newData = { + postId: selectedPost.id, + name: userName, + email: userMail, + body: commentText, + }; + + setIsLoading(true); + + try { + const newComment: Comment = await client.post('/comments', newData); + + setComments(prevComment => [...prevComment, newComment]); + + saveInputDataOnSuccess(); + } catch { + setLoadingError(true); + setComments([]); + cleanForm(); + setShowForm(false); + } + + setIsLoading(false); + }; + + const submit = (e: React.FormEvent) => { + e.preventDefault(); + + const inputs = [ + { value: userName, setError: setUserNameError }, + { value: userMail, setError: setUserMailError }, + { value: commentText, setError: setCommentTextError }, + ]; + + const hasEmptyForm = + !validateInput(userName) && + !validateInput(userMail) && + !validateInput(commentText); + + if (hasEmptyForm) { + setUserNameError(true); + setUserMailError(true); + setCommentTextError(true); + + return; + } + + const isError = inputs.some(input => { + if (!validateInput(input.value)) { + input.setError(true); + + return true; + } + + return false; + }); + + if (isError) { + return; + } + + addCommetns(); + }; -export const NewCommentForm: React.FC = () => { return ( -
+
-

- Name is required -

+ {userNameError && ( +

+ Name is required +

+ )}
@@ -45,24 +194,32 @@ export const NewCommentForm: React.FC = () => { name="email" id="comment-author-email" placeholder="email@test.com" - className="input is-danger" + className={classNames('input', { 'is-danger': userMailError })} + value={userMail} + onChange={e => { + handleFullForm(e, 'mail'); + }} /> - - - + {userMailError && ( + + + + )}
-

- Email is required -

+ {userMailError && ( +

+ Email is required +

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

- Enter some text -

+ {commentTextError && ( +

+ 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..ab71b584f 100644 --- a/src/components/PostDetails.tsx +++ b/src/components/PostDetails.tsx @@ -1,106 +1,165 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Loader } from './Loader'; import { NewCommentForm } from './NewCommentForm'; +import { getData } from '../api/fetch'; +import { Comment, CommentData } from '../types/Comment'; +import { Post } from '../types/Post'; +import { client } from '../utils/fetchClient'; + +interface Props { + posts: Post[]; + selectedPost: Post | null; +} + +export const PostDetails: React.FC = ({ posts, selectedPost }) => { + const [comments, setComments] = useState([]); + const [showForm, setShowForm] = useState(false); + + const [loading, setLoading] = useState(false); + const [loadingError, setLoadingError] = useState(false); + + useEffect(() => { + if (posts && selectedPost) { + const fetchComments = async () => { + try { + const url = `/comments?postId=${selectedPost.id}`; + + setLoading(true); + + if (selectedPost) { + setLoadingError(false); + + const currentComments = await getData(url); + + setComments(currentComments); + } + } catch { + setLoadingError(true); + } finally { + setLoading(false); + } + }; + + fetchComments(); + } + }, [posts, selectedPost]); + + useEffect(() => { + setShowForm(false); + }, [selectedPost]); + + const handleShowForm = () => { + setShowForm(true); + }; + + const handleDelete = async (id: number) => { + const updatedCommnets = comments.filter(comment => comment.id !== id); + + setComments(updatedCommnets); + + try { + await client.delete(`/comments/${id}`); + } catch (error) { + const deletedComment = comments.find(comment => comment.id === id); + + if (deletedComment) { + setTimeout(() => { + setComments([...updatedCommnets, deletedComment]); + setLoadingError(true); + + setTimeout(() => { + setLoadingError(false); + }, 1000); + }, 1000); + } + } + }; -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 -

+

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

+ +

{selectedPost?.body}

- - -
- Something went wrong -
- -

- No comments yet -

- -

Comments:

- -
-
- - Misha Hrynko - - -
+ {loading && } -
- Some comment + {loadingError && ( +
+ Something went wrong
-
- -
-
- - Misha Hrynko - - - -
-
- One more comment + )} + + {false && ( +
+ Something went wrong
-
- -
-
- - Misha Hrynko - - - -
+
+ + {name} + + +
-
- {'Multi\nline\ncomment'} -
-
- - +
+ {body} +
+ + ); + })} + + {!showForm && !loading && !loadingError && ( + + )}
- + {showForm && ( + + )}
); diff --git a/src/components/PostsList.tsx b/src/components/PostsList.tsx index cf90f04b0..c0ddff889 100644 --- a/src/components/PostsList.tsx +++ b/src/components/PostsList.tsx @@ -1,86 +1,65 @@ -import React from 'react'; +import { Post } from '../types/Post'; +import classNames from 'classnames'; -export const PostsList: React.FC = () => ( -
-

Posts:

+interface Props { + posts: Post[]; + selectedPost: Post | null; + setSelectedpost: React.Dispatch>; +} - - - - - - {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} - - - +export const PostsList: React.FC = ({ + posts, + selectedPost, + setSelectedpost, +}) => { + const handleButtonSwitch = (currentPost: Post) => { + if (selectedPost && selectedPost.id === currentPost.id) { + setSelectedpost(null); + } else { + setSelectedpost(currentPost); + } + }; - - - + return ( +
+

Posts:

-
+
#Title
17 - fugit voluptas sed molestias voluptatem provident -
+ + + + + {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} + + + - - + + {posts.map(post => ( + + - - + - - - - - - - - - - - - - - - - - - - -
#Title
- -
{post.id}
18{post.title} - voluptate et itaque vero tempora molestiae - - -
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..d1493e455 100644 --- a/src/components/UserSelector.tsx +++ b/src/components/UserSelector.tsx @@ -1,16 +1,56 @@ -import React from 'react'; +import classNames from 'classnames'; +import React, { useRef, useState } from 'react'; +import { User } from '../types/User'; +import { Post } from '../types/Post'; + +interface Props { + users: User[]; + userId: number | null; + setUserId: React.Dispatch>; + setSelectedpost: React.Dispatch>; +} + +export const UserSelector: React.FC = ({ + users, + userId, + setUserId, + setSelectedpost, +}) => { + const [showList, setShowList] = useState(false); + const [userName, setUserName] = useState('Choose a user'); + + const dropdownRef = useRef(null); + + const handleBlur = (event: React.FocusEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.relatedTarget) + ) { + setShowList(false); + } + }; + + const handleToggle = () => { + setShowList(!showList); + }; -export const UserSelector: React.FC = () => { return ( -