From 4e681011390b96dcd4ce0f2b082e627ebbdbc7da Mon Sep 17 00:00:00 2001 From: Liliya Kalinichenko Date: Thu, 28 Sep 2023 19:25:47 +0300 Subject: [PATCH 1/4] task solution was added --- src/App.tsx | 69 ++++++++---- src/CommentsContext.tsx | 63 +++++++++++ src/PostContext.tsx | 52 +++++++++ src/api/Comments.tsx | 22 ++++ src/api/Posts.tsx | 6 ++ src/api/Users.tsx | 6 ++ src/components/Loader/Loader.tsx | 3 +- src/components/NewCommentForm.tsx | 169 +++++++++++++++++++++++++----- src/components/PostDetails.tsx | 169 ++++++++++++++---------------- src/components/PostsList.tsx | 154 ++++++++++++++------------- src/components/UserSelector.tsx | 73 +++++++++++-- src/index.tsx | 8 +- 12 files changed, 566 insertions(+), 228 deletions(-) create mode 100644 src/CommentsContext.tsx create mode 100644 src/PostContext.tsx create mode 100644 src/api/Comments.tsx create mode 100644 src/api/Posts.tsx create mode 100644 src/api/Users.tsx diff --git a/src/App.tsx b/src/App.tsx index db56b44b0..91785095e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,15 +1,26 @@ -import React from 'react'; +import React, { useState, useContext } from 'react'; import 'bulma/bulma.sass'; import '@fortawesome/fontawesome-free/css/all.css'; import './App.scss'; -import classNames from 'classnames'; +import cn from 'classnames'; import { PostsList } from './components/PostsList'; import { PostDetails } from './components/PostDetails'; import { UserSelector } from './components/UserSelector'; import { Loader } from './components/Loader'; +import { User } from './types/User'; +import { PostContext } from './PostContext'; export const App: React.FC = () => { + const [selectedUser, setSelectedUser] = useState(null); + + const { + posts, + isPostLoading, + isPostLoadError, + selectedPost, + } = useContext(PostContext); + return (
@@ -17,45 +28,59 @@ export const App: React.FC = () => {
- +
-

- No user selected -

+ {!selectedUser && ( +

+ No user selected +

+ )} - + {isPostLoading && } -
- Something went wrong! -
+ {isPostLoadError && ( +
+ Something went wrong! +
+ )} -
- No posts yet -
+ {!isPostLoading + && !isPostLoadError + && posts.length === 0 + && selectedUser && ( +
+ No posts yet +
+ )} - + {selectedUser && !!posts.length && }
-
- -
+ {!isPostLoadError && ( +
+ +
+ )}
diff --git a/src/CommentsContext.tsx b/src/CommentsContext.tsx new file mode 100644 index 000000000..672ad575e --- /dev/null +++ b/src/CommentsContext.tsx @@ -0,0 +1,63 @@ +import React, { useMemo, useState } from 'react'; +import { Comment } from './types/Comment'; +import { deleteComment } from './api/Comments'; + +type CommentsState = { + comments: Comment[], + setComments: (comments: Comment[]) => void, + isCommentLoading: boolean, + setIsCommentLoading: (value: boolean) => void, + isCommentLoadError: boolean, + setIsCommentLoadError: (value: boolean) => void, + isFormShown: boolean, + setIsFormShown: (value: boolean) => void, + hadnleCommentDelete: (id: number) => void; +}; + +export const CommentsContext = React.createContext({ + comments: [], + setComments: () => {}, + isCommentLoading: false, + setIsCommentLoading: () => {}, + isCommentLoadError: false, + setIsCommentLoadError: () => {}, + isFormShown: false, + setIsFormShown: () => {}, + hadnleCommentDelete: () => {}, +}); + +interface Props { + children: React.ReactNode, +} + +export const CommentsProvider: React.FC = ({ children }) => { + const [comments, setComments] = useState([]); + const [isCommentLoading, setIsCommentLoading] = useState(false); + const [isCommentLoadError, setIsCommentLoadError] = useState(false); + const [isFormShown, setIsFormShown] = useState(false); + + const hadnleCommentDelete = (id: number) => { + deleteComment(id) + .then(() => setComments( + currentComments => currentComments.filter(comment => comment.id !== id), + )); + }; + + const value = useMemo(() => ({ + comments, + setComments, + isCommentLoading, + setIsCommentLoading, + isCommentLoadError, + setIsCommentLoadError, + isFormShown, + setIsFormShown, + hadnleCommentDelete, + }), [comments, isCommentLoading, isCommentLoadError, isFormShown]); + + return ( + + {children} + + ); +}; diff --git a/src/PostContext.tsx b/src/PostContext.tsx new file mode 100644 index 000000000..44d4c1bfe --- /dev/null +++ b/src/PostContext.tsx @@ -0,0 +1,52 @@ +import React, { useMemo, useState } from 'react'; +import { Post } from './types/Post'; + +type PostState = { + posts: Post[], + setPosts: (comments: Post[]) => void, + selectedPost: Post | null, + setSelectedPost: (post: Post | null) => void, + isPostLoading: boolean, + setIsPostLoading: (value: boolean) => void, + isPostLoadError: boolean, + setIsPostLoadError: (value: boolean) => void, +}; + +export const PostContext = React.createContext({ + posts: [], + setPosts: () => {}, + selectedPost: null, + setSelectedPost: () => {}, + isPostLoading: false, + setIsPostLoading: () => {}, + isPostLoadError: false, + setIsPostLoadError: () => {}, +}); + +interface Props { + children: React.ReactNode, +} + +export const PostProvider: React.FC = ({ children }) => { + const [posts, setPosts] = useState([]); + const [selectedPost, setSelectedPost] = useState(null); + const [isPostLoading, setIsPostLoading] = useState(false); + const [isPostLoadError, setIsPostLoadError] = useState(false); + + const value = useMemo(() => ({ + posts, + setPosts, + selectedPost, + setSelectedPost, + isPostLoading, + setIsPostLoading, + isPostLoadError, + setIsPostLoadError, + }), [posts, isPostLoading, isPostLoadError, selectedPost]); + + return ( + + {children} + + ); +}; diff --git a/src/api/Comments.tsx b/src/api/Comments.tsx new file mode 100644 index 000000000..8c1420000 --- /dev/null +++ b/src/api/Comments.tsx @@ -0,0 +1,22 @@ +import { Comment, CommentData } from '../types/Comment'; +import { client } from '../utils/fetchClient'; + +export const getComments = (postId: number) => { + return client.get(`/comments?postId=${postId}`); +}; + +export const addComment = ( + postId: number, + { name, email, body }: CommentData, +) => { + return client.post('/comments', { + postId, + name, + email, + body, + }); +}; + +export const deleteComment = (commentId: number) => { + return client.delete(`/comments/${commentId}`); +}; diff --git a/src/api/Posts.tsx b/src/api/Posts.tsx new file mode 100644 index 000000000..a6a1739ec --- /dev/null +++ b/src/api/Posts.tsx @@ -0,0 +1,6 @@ +import { Post } from '../types/Post'; +import { client } from '../utils/fetchClient'; + +export const getPosts = (userId: number) => { + return client.get(`/posts?userId=${userId}`); +}; diff --git a/src/api/Users.tsx b/src/api/Users.tsx new file mode 100644 index 000000000..816c8274b --- /dev/null +++ b/src/api/Users.tsx @@ -0,0 +1,6 @@ +import { User } from '../types/User'; +import { client } from '../utils/fetchClient'; + +export const getUsers = () => { + return client.get('/users'); +}; diff --git a/src/components/Loader/Loader.tsx b/src/components/Loader/Loader.tsx index 29d906bbd..6dc8e938a 100644 --- a/src/components/Loader/Loader.tsx +++ b/src/components/Loader/Loader.tsx @@ -1,6 +1,7 @@ +import React from 'react'; import './Loader.scss'; -export const Loader = () => ( +export const Loader: React.FC = () => (
diff --git a/src/components/NewCommentForm.tsx b/src/components/NewCommentForm.tsx index 73a8a0b45..1f9f16f9e 100644 --- a/src/components/NewCommentForm.tsx +++ b/src/components/NewCommentForm.tsx @@ -1,8 +1,79 @@ -import React from 'react'; +import cn from 'classnames'; +import React, { useContext, useState } from 'react'; +import { addComment } from '../api/Comments'; +import { PostContext } from '../PostContext'; +import { CommentsContext } from '../CommentsContext'; export const NewCommentForm: React.FC = () => { + const [name, setName] = useState(''); + const [isNameError, setIsNameError] = useState(false); + + const [email, setEmail] = useState(''); + const [isEmailError, setIsEmailError] = useState(false); + + const [body, setBody] = useState(''); + const [isBodyError, setIsBodyError] = useState(false); + + const [isSubmitting, setIsSubmitting] = useState(false); + + const { selectedPost } = useContext(PostContext); + const { + comments, + setComments, + setIsCommentLoadError, + } = useContext(CommentsContext); + + const reset = () => { + setName(''); + setIsNameError(false); + setEmail(''); + setIsEmailError(false); + setBody(''); + setIsBodyError(false); + }; + + const handleCommentSubmit = (event: React.FormEvent) => { + event.preventDefault(); + + if (!name.trim()) { + setIsNameError(true); + } + + if (!email.trim()) { + setIsEmailError(true); + } + + if (!body.trim()) { + setIsBodyError(true); + } + + if (!name.trim() || !email.trim() || !body.trim()) { + return; + } + + setIsSubmitting(true); + + if (selectedPost) { + addComment(selectedPost.id, { + name, + email, + body, + }) + .then(newComment => { + setComments([...comments, newComment]); + }) + .catch(() => { + setIsCommentLoadError(true); + }) + .finally(() => { + setIsSubmitting(false); + setBody(''); + }); + } + }; + return ( -
+
-

- Name is required -

+ {isNameError && ( +

+ Name is required +

+ )}
@@ -45,24 +127,36 @@ export const NewCommentForm: React.FC = () => { name="email" id="comment-author-email" placeholder="email@test.com" - className="input is-danger" + className={cn('input', { + 'is-danger': isEmailError, + })} + value={email} + onChange={event => { + setEmail(event.target.value); + setIsEmailError(false); + }} + /> - - - + {isEmailError && ( + + + + )}
-

- Email is required -

+ {isEmailError && ( +

+ Email is required +

+ )}
@@ -75,25 +169,44 @@ export const NewCommentForm: React.FC = () => { id="comment-body" name="body" placeholder="Type comment here" - className="textarea is-danger" + className={cn('input', { + 'is-danger': isBodyError, + })} + value={body} + onChange={event => { + setBody(event.target.value); + setIsBodyError(false); + }} + />
-

- Enter some text -

+ {isBodyError && ( +

+ 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..5b56f5324 100644 --- a/src/components/PostDetails.tsx +++ b/src/components/PostDetails.tsx @@ -1,117 +1,106 @@ -import React from 'react'; +import React, { useContext } from 'react'; import { Loader } from './Loader'; import { NewCommentForm } from './NewCommentForm'; +import { CommentsContext } from '../CommentsContext'; +import { PostContext } from '../PostContext'; export const PostDetails: React.FC = () => { + const { + comments, + isCommentLoading, + isCommentLoadError, + isFormShown, + setIsFormShown, + hadnleCommentDelete, + } = useContext(CommentsContext); + + const { selectedPost } = useContext(PostContext); + return (

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

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

- - -
- Something went wrong -
- -

- No comments yet -

- -

Comments:

- - + {isCommentLoading ? ( + + ) : ( + <> + {isCommentLoadError && ( +
+ Something went wrong +
+ )} -
-
- - Misha Hrynko - + {!comments.length && !isCommentLoadError && ( +

+ No comments yet +

+ )} - -
-
- One more comment -
-
+ {!!comments.length && !isCommentLoadError && ( + <> +

Comments:

-
-
- - Misha Hrynko - + {comments.map(comment => { + const { id, name, body } = comment; - -
+ return ( +
+
+ + {name} + + +
-
- {'Multi\nline\ncomment'} -
-
+
+ {body} +
+
+ ); + })} + + )} - + {!isCommentLoadError && !isFormShown && ( + + )} + + )}
- + {isFormShown && !isCommentLoadError && }
); diff --git a/src/components/PostsList.tsx b/src/components/PostsList.tsx index 6e6efc0f3..bbd320170 100644 --- a/src/components/PostsList.tsx +++ b/src/components/PostsList.tsx @@ -1,85 +1,89 @@ -import React from 'react'; +import React, { useContext } from 'react'; +import cn from 'classnames'; -export const PostsList: React.FC = () => ( -
-

Posts:

+import { Post } from '../types/Post'; +import { getComments } from '../api/Comments'; +import { CommentsContext } from '../CommentsContext'; +import { PostContext } from '../PostContext'; - - - - - - - - +export const PostsList: React.FC = () => { + const { + setComments, + setIsCommentLoading, + setIsCommentLoadError, + setIsFormShown, + } = useContext(CommentsContext); - - - + const { + posts, + selectedPost, + setSelectedPost, + } = useContext(PostContext); - + const handleOpenPostClick = (post: Post) => { + setIsFormShown(false); + if (selectedPost && selectedPost.id === post.id) { + setSelectedPost(null); + } else { + setSelectedPost(post); + setIsCommentLoading(true); + getComments(post.id) + .then(setComments) + .catch(() => { + setIsCommentLoading(false); + setIsCommentLoadError(true); + }) + .finally(() => { + setIsCommentLoading(false); + }); + } + }; - - + return ( +
+

Posts:

-
- +
#Title
17 - fugit voluptas sed molestias voluptatem provident - - -
18
+ + + + + + + - + + {posts.map(post => { + const { id, title } = post; + const isPostSelected = selectedPost && selectedPost.id === post.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 cb83a8f68..5d41ea673 100644 --- a/src/components/UserSelector.tsx +++ b/src/components/UserSelector.tsx @@ -1,6 +1,48 @@ -import React from 'react'; +import React, { useEffect, useState, useContext } from 'react'; +import { User } from '../types/User'; +import { getUsers } from '../api/Users'; +import { getPosts } from '../api/Posts'; +import { PostContext } from '../PostContext'; + +type Props = { + selectedUser: User | null, + setSelectedUser: (user: User | null) => void, +}; + +export const UserSelector: React.FC = ({ + selectedUser, + setSelectedUser, +}) => { + const [users, setUsers] = useState([]); + const [isSelectOpen, setIsSelectOpen] = useState(false); + + const { + setPosts, + setIsPostLoading, + setIsPostLoadError, + } = useContext(PostContext); + + useEffect(() => { + getUsers() + .then(setUsers); + }, []); + + const handleUserSelect = (user: User) => { + setSelectedUser(user); + setIsSelectOpen(false); + setIsPostLoading(true); + getPosts(user.id) + .then(setPosts) + .catch(() => { + setIsPostLoading(false); + setIsPostLoadError(true); + setPosts([]); + }) + .finally(() => { + setIsPostLoading(false); + }); + }; -export const UserSelector: React.FC = () => { return (
{ className="button" aria-haspopup="true" aria-controls="dropdown-menu" + onClick={() => setIsSelectOpen(!isSelectOpen)} + onBlur={() => setIsSelectOpen(false)} > - Choose a user + {selectedUser ? `${selectedUser.name}` : 'Choose a user'}