diff --git a/src/Api/Api.ts b/src/Api/Api.ts new file mode 100644 index 000000000..2e7770911 --- /dev/null +++ b/src/Api/Api.ts @@ -0,0 +1,39 @@ +import { Post } from '../types/Post'; +import { Comment } from '../types/Comment'; +import { client } from '../utils/fetchClient'; + +export const getUsers = () => { + return client.get('/users'); +}; + +export const getPosts = (id?: number) => { + return client.get(`/posts?userId=${id}`); +}; + +export const getPost = (id?: number) => { + return client.get(`/posts/${id}`); +}; + +export const getComments = (id?: number) => { + return client.get(`/comments?postId=${id}`); +}; + +export const postData = (url: string, data: Post) => { + return client.post(url, data); +}; + +export const updateComments = (data: Comment[]) => { + return client.patch('/comments', data); +}; + +export const deleteData = (url: string) => { + return client.delete(url); +}; + +export const deleteComment = (id: number) => { + return client.delete(`/comments/${id}`); +}; + +export const addComment = (postId: number, data: Comment) => { + return client.post(`/comments?postId=${postId}`, data); +}; diff --git a/src/App.tsx b/src/App.tsx index db56b44b0..267ec986a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import 'bulma/bulma.sass'; import '@fortawesome/fontawesome-free/css/all.css'; import './App.scss'; @@ -8,8 +8,65 @@ import { PostsList } from './components/PostsList'; import { PostDetails } from './components/PostDetails'; import { UserSelector } from './components/UserSelector'; import { Loader } from './components/Loader'; +import { Post } from './types/Post'; +import { User } from './types/User'; +import { getPosts, getUsers } from './Api/Api'; export const App: React.FC = () => { + const [posts, setPosts] = useState([]); + const [isShowPostDetails, setIsShowPostDetails] = useState(false); + const [isError, setIsError] = useState(false); + + const [users, setUsers] = useState([]); + + const [selectedUserId, setSelectedUserId] = useState(); + const [selectPostId, setSelectPostId] = useState(); + + const [isLoading, setIsLoading] = useState(false); + + const getUsersFromServer = () => { + getUsers() + .then(data => { + const user = data as User[]; + + setUsers(user); + }) + .catch(() => { + setIsError(true); + }); + }; + + const selectUser = users.find(person => person.id === selectedUserId); + + const getPostsFromSelectedUser = () => { + setIsLoading(true); + if (selectUser) { + getPosts(selectedUserId) + .then(data => { + const selectedPosts = data as Post[]; + + setPosts(selectedPosts); + setIsLoading(false); + }) + .catch(error => { + // eslint-disable-next-line no-console + console.error('Error:', error); + setIsError(true); + setIsLoading(false); + }); + } + }; + + useEffect(() => { + getUsersFromServer(); + }, []); + + useEffect(() => { + if (selectedUserId) { + getPostsFromSelectedUser(); + } + }, [selectUser]); + return (
@@ -17,29 +74,57 @@ export const App: React.FC = () => {
- +
-
-

- No user selected -

- - +
+ {isError && ( + <> +
+ Something went wrong! +
+ + )} -
- Something went wrong! -
+ {isLoading && } -
- No posts yet -
+ {!selectedUserId && !isLoading + ? ( +

+ No user selected +

+ ) + : (posts.length > 0 + && ( + + ) + )} + {(selectedUserId && posts.length === 0 && !isLoading) + && ( +
+ No posts yet +
+ )} -
+
@@ -50,12 +135,18 @@ export const App: React.FC = () => { 'is-parent', 'is-8-desktop', 'Sidebar', - 'Sidebar--open', + { 'Sidebar--open': isShowPostDetails }, )} > -
- -
+ {isShowPostDetails && ( +
+ +
+ )}
diff --git a/src/components/NewCommentForm.tsx b/src/components/NewCommentForm.tsx index 73a8a0b45..cd53f74a4 100644 --- a/src/components/NewCommentForm.tsx +++ b/src/components/NewCommentForm.tsx @@ -1,8 +1,115 @@ -import React from 'react'; +import React, { useState } from 'react'; +import classNames from 'classnames'; +import { addComment } from '../Api/Api'; +import { Comment } from '../types/Comment'; + +type Props = { + selectedPostId: number | undefined + setComments: React.Dispatch> +}; + +export const NewCommentForm: React.FC = ({ + selectedPostId, + setComments, +}) => { + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [text, setText] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const [isErrorName, setIsErrorName] = useState(false); + const [isErrorEmail, setIsErrorEmail] = useState(false); + const [isErrorText, setIsErrorText] = useState(false); + + const handleChangeName = ( + event: React.ChangeEvent, + ) => { + event.preventDefault(); + setIsErrorName(false); + const newValue: string = event.target.value; + + setName(newValue); + }; + + const handleChangeEmail = ( + event: React.ChangeEvent, + ) => { + event.preventDefault(); + setIsErrorEmail(false); + const newValue: string = event.target.value; + + setEmail(newValue); + }; + + const handleChangeText = ( + event: React.ChangeEvent, + ) => { + event.preventDefault(); + setIsErrorText(false); + const newValue: string = event.target.value; + + setText(newValue); + }; + + const addNewComment = async () => { + if (!name.trim() || !email.trim() || !text.trim()) { + return; + } + + setIsLoading(true); + + const id = selectedPostId as number; + + const newComment = { + name, + email, + body: text, + postId: id, + }; + + try { + if (selectedPostId) { + const addedComment = await addComment(selectedPostId, newComment); + + setComments( + (currentComments: Comment[]) => [...currentComments, addedComment], + ); + setText(''); + setIsLoading(false); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error adding comment:', error); + } + }; + + const errorInput = () => { + if (name.trim().length === 0) { + setIsErrorName(true); + } + + if (email.trim().length === 0) { + setIsErrorEmail(true); + } + + if (text.trim().length === 0) { + setIsErrorText(true); + } + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + errorInput(); + addNewComment(); + }; + + const clearAllInput = () => { + setText(''); + }; -export const NewCommentForm: React.FC = () => { return ( -
+
-

- Name is required -

+ {isErrorName + && ( +

+ Name is required +

+ )}
@@ -41,28 +159,38 @@ export const NewCommentForm: React.FC = () => {
- - - + {isErrorEmail + && ( + + + + )}
- -

- Email is required -

+ {isErrorEmail + && ( +

+ Email is required +

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

- Enter some text -

+ {isErrorText + && ( +

+ 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..d92f610c5 100644 --- a/src/components/PostDetails.tsx +++ b/src/components/PostDetails.tsx @@ -1,117 +1,154 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Loader } from './Loader'; import { NewCommentForm } from './NewCommentForm'; +import { getComments, getPost, deleteComment } from '../Api/Api'; +import { Post } from '../types/Post'; +import { Comment } from '../types/Comment'; -export const PostDetails: React.FC = () => { - return ( -
-
-
-

- #18: voluptate et itaque vero tempora molestiae -

+type Props = { + selectedPostId: number | undefined + isError: boolean + setIsError: (a: boolean) => void +}; -

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

-
+export const PostDetails: React.FC = ({ + selectedPostId, + isError, + setIsError, +}) => { + const [post, setPosts] = useState(); + const [comments, setComments] = useState([]); + const [isOpen, setIsOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); -
- + const thisPost = post as Post; -
- Something went wrong -
+ const handleGetComments = () => { + setIsLoading(true); + getComments(selectedPostId) + .then(data => { + const commentsArr = data as Comment[]; -

- No comments yet -

+ setIsLoading(false); + setComments(commentsArr); + }); + }; -

Comments:

+ const deleteSelectComment = (id: number | undefined) => { + if (id) { + deleteComment(id); + setComments((prevComments: Comment[]) => prevComments.filter( + comment => comment.id !== id, + )); + } + }; -
-
- - Misha Hrynko - - -
+ useEffect(() => { + setIsLoading(true); + if (selectedPostId) { + getPost(selectedPostId) + .then(data => { + const postData = data as Post; -
- Some comment -
-
+ setPosts(postData); + setIsLoading(false); + }) + .catch(error => { + // eslint-disable-next-line no-console + console.error('Error fetching post:', error); -
-
- - Misha Hrynko - + setIsError(true); + setIsLoading(false); + }); - -
-
- One more comment -
-
+ handleGetComments(); + } + }, [selectedPostId, setComments]); -
-
- - Misha Hrynko - + return ( +
+
+ {thisPost ? ( +
+

+ {`#${selectedPostId}: ${thisPost.title}`} +

+

{thisPost.body}

+
+ ) : null} - -
+ {isLoading ? ( + + ) : ( + <> + {isError && ( +
+ Something went wrong +
+ )} + {comments.length === 0 ? ( +

+ No comments yet +

+ ) : ( + <> +

Comments:

+ {comments.map(comment => { + const { + email, + name, + body, + id, + } = comment; -
- {'Multi\nline\ncomment'} -
-
+ return ( +
+
+ + {name} + + +
+
+ {body} +
+
+ ); + })} + + )} + + )} + {!isLoading && !isOpen ? ( -
+ ) : null} - + {isOpen && ( + + )}
); diff --git a/src/components/PostsList.tsx b/src/components/PostsList.tsx index 6e6efc0f3..1f7fd3df1 100644 --- a/src/components/PostsList.tsx +++ b/src/components/PostsList.tsx @@ -1,85 +1,84 @@ import React from 'react'; +import classNames from 'classnames'; +import { Post } from '../types/Post'; -export const PostsList: React.FC = () => ( -
-

Posts:

+type Props = { + posts: Post[]; + isShowPostDetails: boolean; + setIsShowPostDetails: (a: boolean) => void; + selectedPostId: number | undefined; + setSelectPostId: (n: number) => void; +}; - - - - - - - - +export const PostsList: React.FC = ({ + posts, + setIsShowPostDetails, + isShowPostDetails, + selectedPostId, + setSelectPostId, +}) => { + const handleShowDetails = (id: number) => { + if (selectedPostId === id) { + setIsShowPostDetails(!isShowPostDetails); + } else { + setIsShowPostDetails(true); + } - - - + setSelectPostId(id); + }; - + return ( +
+

Posts:

-
- +
#Title
17 - fugit voluptas sed molestias voluptatem provident - - -
+ + + + + + + - - + + {posts.map(post => { + const { id, title } = post; - + const buttonText = ((selectedPostId === id) + && isShowPostDetails) + ? 'Close' + : 'Open'; - - + return ( + + - - - + - - - - - - - - - - -
#Title
18
- 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 cb83a8f68..2e7b92a79 100644 --- a/src/components/UserSelector.tsx +++ b/src/components/UserSelector.tsx @@ -1,19 +1,50 @@ -import React from 'react'; +import React, { useState } from 'react'; +import classNames from 'classnames'; +import { User } from '../types/User'; + +type Props = { + users: User[] + selectUser: User | undefined + selectedUserId: number | undefined + setSelectedUserId: (a: number) => void +}; + +export const UserSelector: React.FC = ({ + users, + selectUser, + selectedUserId, + setSelectedUserId, +}) => { + const [isOpen, setIsOpen] = useState(false); + let text = ''; + const myUser = selectUser as User; + + if (myUser) { + text = myUser.name; + } else { + text = 'Choose a user'; + } + + const handleChangeUser = (id: number) => { + setSelectedUserId(id); + setIsOpen(false); + }; -export const UserSelector: React.FC = () => { return (
+