From e616e1ccd89b660254939807e53b796dcdb4f7cc Mon Sep 17 00:00:00 2001 From: Absolution26 Date: Fri, 12 Jan 2024 01:22:18 +0200 Subject: [PATCH 01/10] solution --- src/App.tsx | 160 +++++++++++++++++++++++---- src/api/comments.ts | 14 +++ src/api/posts.ts | 6 ++ src/api/users.ts | 6 ++ src/components/NewCommentForm.tsx | 163 ++++++++++++++++++++++------ src/components/PostDetails.tsx | 172 +++++++++++++++--------------- src/components/PostsList.tsx | 134 ++++++++++------------- src/components/UserSelector.tsx | 50 +++++++-- 8 files changed, 482 insertions(+), 223 deletions(-) create mode 100644 src/api/comments.ts create mode 100644 src/api/posts.ts create mode 100644 src/api/users.ts diff --git a/src/App.tsx b/src/App.tsx index db56b44b0..6cd3bbfa0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,94 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import 'bulma/bulma.sass'; import '@fortawesome/fontawesome-free/css/all.css'; import './App.scss'; +import cn from 'classnames'; -import classNames from 'classnames'; import { PostsList } from './components/PostsList'; import { PostDetails } from './components/PostDetails'; import { UserSelector } from './components/UserSelector'; import { Loader } from './components/Loader'; +import * as fetchUsers from './api/users'; +import * as fetchPosts from './api/posts'; +import * as fetchComments from './api/comments'; +import { User } from './types/User'; +import { Post } from './types/Post'; +import { Comment } from './types/Comment'; +import { client } from './utils/fetchClient'; export const App: React.FC = () => { + const [users, setUsers] = useState([]); + const [posts, setPosts] = useState([]); + const [comments, setComments] = useState([]); + const [selectedUser, setSelectedUser] = useState(null); + const [selectedPost, setSelectedPost] = useState(null); + const [isLoadingPosts, setIsLoadingPosts] = useState(false); + const [isLoadingComments, setIsLoadingComments] = useState(false); + const [isShowingComments, setIsShowingComments] = useState(false); + const [openPostId, setOpenPostId] = useState(0); + const [isShowingForm, setIsShowingForm] = useState(false); + const [errorPosts, setErrorPosts] = useState(false); + const [errorComments, setErrorComments] = useState(false); + + useEffect(() => { + fetchUsers.getUsers('/users') + .then(res => setUsers(res)); + }, []); + + useEffect(() => { + if (selectedUser) { + setIsLoadingPosts(true); + setErrorPosts(false); + + fetchPosts.getPosts(`/posts?userId=${selectedUser.id}`) + .then(res => setPosts(res)) + .catch(() => setErrorPosts(true)) + .finally(() => setIsLoadingPosts(false)); + } + }, [selectedUser]); + + const showPostDetails = (post: Post) => { + setIsLoadingComments(true); + if (post.id === openPostId) { + setOpenPostId(0); + setIsShowingComments(!isShowingComments); + } + + if (!openPostId || openPostId !== post.id) { + setIsShowingComments(true); + setOpenPostId(post.id); + setErrorComments(false); + + fetchComments.getComments(`/comments?postId=${post.id}`) + .then(res => { + setComments(res); + setIsLoadingComments(false); + }) + .catch(() => setErrorComments(true)); + } + + setSelectedPost(post); + setIsShowingForm(false); + }; + + const handleDeleteComment = (commentId: number) => { + client.delete(`/comments/${commentId}`) + .then(() => { + const showingComments = comments + .filter(comment => comment.id !== commentId); + + setComments(showingComments); + }); + }; + + const createNewComment = (newComment: Comment) => { + setComments([...comments, newComment]); + }; + + const handleChangeErrorState = (el: boolean) => { + setErrorComments(el); + }; + return (
@@ -17,44 +96,83 @@ export const App: React.FC = () => {
- +
-

- No user selected -

+ {!errorPosts && isLoadingPosts + && } - + {!errorPosts && !selectedUser && !isLoadingPosts && ( +

+ No user selected +

+ )} -
- Something went wrong! -
+ {(!isLoadingPosts + && posts.length > 0 + && selectedUser + && !errorPosts) + && ( + + )} -
- No posts yet -
+ {!isLoadingPosts + && !posts.length + && selectedUser + && !errorPosts + && ( +
+ No posts yet +
+ )} - + {errorPosts && ( +
+ Something went wrong! +
+ )}
-
- +
+
diff --git a/src/api/comments.ts b/src/api/comments.ts new file mode 100644 index 000000000..7c4691ab4 --- /dev/null +++ b/src/api/comments.ts @@ -0,0 +1,14 @@ +import { Comment } from '../types/Comment'; +import { client } from '../utils/fetchClient'; + +export const getComments = (url: string) => { + return client.get(url); +}; + +export const addComment = (url: string, newComment: Comment) => { + return client.post(url, newComment); +}; + +export const deleteComment = (url: string) => { + return client.delete(url); +}; diff --git a/src/api/posts.ts b/src/api/posts.ts new file mode 100644 index 000000000..9f911316f --- /dev/null +++ b/src/api/posts.ts @@ -0,0 +1,6 @@ +import { Post } from '../types/Post'; +import { client } from '../utils/fetchClient'; + +export const getPosts = (url: string) => { + return client.get(url); +}; diff --git a/src/api/users.ts b/src/api/users.ts new file mode 100644 index 000000000..8210b7fa5 --- /dev/null +++ b/src/api/users.ts @@ -0,0 +1,6 @@ +import { User } from '../types/User'; +import { client } from '../utils/fetchClient'; + +export const getUsers = (url: string) => { + return client.get(url); +}; diff --git a/src/components/NewCommentForm.tsx b/src/components/NewCommentForm.tsx index 73a8a0b45..5a5072633 100644 --- a/src/components/NewCommentForm.tsx +++ b/src/components/NewCommentForm.tsx @@ -1,8 +1,86 @@ -import React from 'react'; +import React, { useState } from 'react'; +import cn from 'classnames'; + +import { Comment } from '../types/Comment'; +import { addComment } from '../api/comments'; + +interface Props { + postId: number, + addNewComment: (el: Comment) => void, + changeErrorState: (el: boolean) => void, +} + +export const NewCommentForm: React.FC = ({ + postId, + addNewComment, + changeErrorState, +}) => { + const [inputName, setInputName] = useState(''); + const [inputMail, setInputMail] = useState(''); + const [textarea, setTextarea] = useState(''); + const [hasNameError, setHasNameError] = useState(false); + const [hasMailError, setHasMailError] = useState(false); + const [hasTextareaError, setHasTextareaError] = useState(false); + const [isAdding, setIsAdding] = useState(false); + const handleReset = () => { + setInputName(''); + setInputMail(''); + setTextarea(''); + }; + + const handleSumbit = (event: React.FormEvent) => { + event.preventDefault(); + changeErrorState(false); + const newComment = { + id: 0, + postId, + name: inputName, + email: inputMail, + body: textarea, + }; + + if (inputName.trim() && inputMail.trim() && textarea.trim()) { + setIsAdding(true); + addComment('/comments', newComment) + .then(() => { + setTextarea(''); + addNewComment(newComment); + setIsAdding(false); + }) + .catch(() => changeErrorState(true)); + } + + if (!inputName.trim()) { + setHasNameError(true); + } + + if (!inputMail.trim()) { + setHasMailError(true); + } + + if (!textarea.trim()) { + setHasTextareaError(true); + } + }; + + const handleChangeName = (event: React.ChangeEvent) => { + setInputName(event.target.value); + setHasNameError(false); + }; + + const handleChangeMail = (event: React.ChangeEvent) => { + setInputMail(event.target.value); + setHasMailError(false); + }; + + // eslint-disable-next-line max-len + const handleChangeTextarea = (event: React.ChangeEvent) => { + setTextarea(event.target.value); + setHasTextareaError(false); + }; -export const NewCommentForm: React.FC = () => { return ( -
+
-

- Name is required -

+ {hasNameError && ( +

+ Name is required +

+ )}
@@ -41,28 +126,35 @@ export const NewCommentForm: React.FC = () => {
handleChangeMail(event)} + required /> - - - + {hasMailError && ( + + + + )}
-

- Email is required -

+ {hasMailError && ( +

+ Email is required +

+ )}
@@ -75,25 +167,36 @@ export const NewCommentForm: React.FC = () => { id="comment-body" name="body" placeholder="Type comment here" - className="textarea is-danger" + className={cn('textarea', { 'is-danger': hasTextareaError })} + value={textarea} + onChange={event => handleChangeTextarea(event)} />
-

- Enter some text -

+ {hasTextareaError && ( +

+ 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..7b6a33071 100644 --- a/src/components/PostDetails.tsx +++ b/src/components/PostDetails.tsx @@ -1,117 +1,119 @@ import React from 'react'; import { Loader } from './Loader'; import { NewCommentForm } from './NewCommentForm'; +import { Comment } from '../types/Comment'; +import { Post } from '../types/Post'; -export const PostDetails: React.FC = () => { +interface Props { + comments: Comment[], + selectedPost: Post | null, + isLoadingComments: boolean, + openPostId: number, + addComment: (el: Comment) => void, + isShowingForm: boolean, + changeIsShowingForm: (el: boolean) => void, + onDelete: (id: number) => void, + error: boolean, + changeErrorState: (el: boolean) => void +} + +export const PostDetails: React.FC = ({ + comments, + selectedPost, + isLoadingComments, + openPostId, + addComment, + isShowingForm, + changeIsShowingForm, + onDelete, + error, + changeErrorState, +}) => { return (
+

- #18: voluptate et itaque vero tempora molestiae + {`#${selectedPost?.id}: ${selectedPost?.title}`}

- eveniet quo quis - laborum totam consequatur non dolor - ut et est repudiandae - est voluptatem vel debitis et magnam + {selectedPost?.body}

- - -
- Something went wrong -
- -

- No comments yet -

- -

Comments:

+ {!error && isLoadingComments && ( + )} -
-
- - Misha Hrynko - - + {error && ( +
+ Something went wrong
+ )} -
- Some comment -
-
+ {!error && !comments.length && comments && !isLoadingComments && ( +

+ No comments yet +

+ )} -
-
- - Misha Hrynko - + {!error + && Boolean(comments.length) + && comments + && !isLoadingComments + && ( +

Comments:

)} - -
-
- One more comment -
-
+
+ + {comment.name} + + +
- + ))} + {!error && !isLoadingComments && (isShowingForm + ? ( + + ) + : ( -
- -
- {'Multi\nline\ncomment'} -
- - - + ))}
- -
); diff --git a/src/components/PostsList.tsx b/src/components/PostsList.tsx index cf90f04b0..77948bdcf 100644 --- a/src/components/PostsList.tsx +++ b/src/components/PostsList.tsx @@ -1,86 +1,64 @@ import React from 'react'; +import cn from 'classnames'; +import { Post } from '../types/Post'; -export const PostsList: React.FC = () => ( -
-

Posts:

+interface Props { + posts: Post[], + showPostDetails: (post: Post) => void, + openPostId: number, +} - - - - - - {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} - - - +export const PostsList: React.FC = ({ + posts, + showPostDetails, + openPostId, +}) => { + return ( +
+

Posts:

-
- - +
#Title
17
+ + + + + {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} + + + - - - - - - - - - - - - Close - - - + - - - + - - - - - - - - - - -
#Title
- fugit voluptas sed molestias voluptatem provident - - -
18 - voluptate et itaque vero tempora molestiae - -
{post.id}
19adipisci placeat illum aut reiciendis qui + {post.title} + - -
20doloribus ad provident suscipit at - -
-
-); + + + + + ))} + + + + ); +}; diff --git a/src/components/UserSelector.tsx b/src/components/UserSelector.tsx index cb83a8f68..83692f795 100644 --- a/src/components/UserSelector.tsx +++ b/src/components/UserSelector.tsx @@ -1,10 +1,30 @@ -import React from 'react'; +import React, { useState } from 'react'; +import cn from 'classnames'; + +import { User } from '../types/User'; + +interface Props { + users: User[], + selectedUser: User | null, + updateSelectedUser: (el: User | null) => void, +} + +export const UserSelector: React.FC = ({ + users, + selectedUser, + updateSelectedUser, +}) => { + const [isUserSelected, setIsUserSelected] = useState(false); + + const updateUserAndPosts = (currUser: User) => { + updateSelectedUser(currUser); + setIsUserSelected(false); + }; -export const UserSelector: React.FC = () => { return (