From 02c0535781768196573c73717bd4ed77c5e233d8 Mon Sep 17 00:00:00 2001 From: Dmytro Vovk Date: Sun, 17 Sep 2023 19:42:57 +0300 Subject: [PATCH] Solution --- src/App.tsx | 125 ++++++++++++++---- src/components/NewCommentForm.tsx | 175 +++++++++++++++++++++----- src/components/PostDetails.tsx | 203 ++++++++++++++++++------------ src/components/PostsList.tsx | 112 ++++++++--------- src/components/UserSelector.tsx | 91 ++++++++++++-- src/components/api/comments.ts | 21 ++++ src/components/api/posts.ts | 6 + src/components/api/users.ts | 6 + src/utils/fetchClient.ts | 1 + 9 files changed, 536 insertions(+), 204 deletions(-) create mode 100644 src/components/api/comments.ts create mode 100644 src/components/api/posts.ts create mode 100644 src/components/api/users.ts diff --git a/src/App.tsx b/src/App.tsx index db56b44b0..bb661fa20 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,93 @@ 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 { getUsers } from './components/api/users'; +import { Post } from './types/Post'; +import { getPosts } from './components/api/posts'; export const App: React.FC = () => { + const [usersFroServer, setUsersFromServer] = useState([]); + const [ + selectedUser, setSelectedUser, + ] = useState(null); + const [userPosts, setUserPosts] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + const [showPostsList, setShowPostsList] = useState(false); + const [selectedPost, setSelectedPost] = useState(null); + + useEffect(() => { + getUsers().then((data) => { + setUsersFromServer(data); + }).catch((error) => { + // eslint-disable-next-line no-console + console.error('Error loading users:', error); + setIsError(true); + }); + }, []); + + useEffect(() => { + if (selectedUser) { + setShowPostsList(false); + setIsLoading(true); + getPosts(selectedUser.id).then((data) => { + setUserPosts(data); + }).catch((error) => { + // eslint-disable-next-line no-console + console.error('Something went wrong!', error); + setIsError(true); + }).finally(() => { + setIsLoading(false); + setShowPostsList(true); + }); + } + }, [selectedUser]); + + const renderContent = () => { + let content = null; + + if (!selectedUser) { + content = ( +

+ No user selected +

+ ); + } + + if (isLoading) { + content = ( + + ); + } + + if (isError) { + content = ( +
+ Something went wrong! +
+ ); + } else if (showPostsList) { + if (userPosts.length === 0) { + content = ( +
+ No posts yet +
+ ); + } else { + content = ( + + ); + } + } + + return content; + }; + return (
@@ -17,28 +102,16 @@ export const App: React.FC = () => {
- +
-

- No user selected -

- - - -
- Something went wrong! -
- -
- No posts yet -
- - + {renderContent()}
@@ -50,12 +123,16 @@ export const App: React.FC = () => { 'is-parent', 'is-8-desktop', 'Sidebar', - 'Sidebar--open', + { 'Sidebar--open': selectedPost }, )} > -
- -
+ {selectedPost && ( +
+ +
+ )}
diff --git a/src/components/NewCommentForm.tsx b/src/components/NewCommentForm.tsx index 73a8a0b45..67d30621d 100644 --- a/src/components/NewCommentForm.tsx +++ b/src/components/NewCommentForm.tsx @@ -1,8 +1,84 @@ -import React from 'react'; +import classNames from 'classnames'; + +import React, { useEffect, useState } from 'react'; +import { CommentData } from '../types/Comment'; + +type Props = { + addComment: (inputValue: CommentData) => void, + newPostLoading: boolean, +}; + +export const NewCommentForm: React.FC = ({ + addComment, + newPostLoading, +}) => { + const [isError, setIsError] = useState({ + nameError: false, + emailError: false, + textariaError: false, + }); + const [inputValue, setInputValue] = useState({ + name: '', + email: '', + body: '', + }); + + const handleResetComment = () => { + setInputValue({ + name: '', + email: '', + body: '', + }); + + setIsError({ + nameError: false, + emailError: false, + textariaError: false, + }); + }; + + const handleAddComment = (e: React.FormEvent) => { + e.preventDefault(); + + const errors = { + nameError: !inputValue.name.trim(), + emailError: !inputValue.email.trim(), + textariaError: !inputValue.body.trim(), + }; + + setIsError(errors); + + setIsError((prevErrors) => { + const hasErrors = Object.values(prevErrors).some((error) => error); + + if (!hasErrors) { + addComment(inputValue); + setInputValue({ + ...inputValue, + body: '', + }); + } + + return prevErrors; + }); + }; + + useEffect(() => { + if (inputValue.name) { + setIsError({ ...isError, nameError: false }); + } + + if (inputValue.email) { + setIsError({ ...isError, emailError: false }); + } + + if (inputValue.body) { + setIsError({ ...isError, textariaError: false }); + } + }, [inputValue.name, inputValue.email, inputValue.body]); -export const NewCommentForm: React.FC = () => { return ( -
+
-

- Name is required -

+ {isError.nameError && ( +

+ Name is required +

+ )}
@@ -45,24 +132,35 @@ export const NewCommentForm: React.FC = () => { name="email" id="comment-author-email" placeholder="email@test.com" - className="input is-danger" + className={classNames( + 'input', + { 'is-danger': isError.emailError }, + )} + value={inputValue.email} + onChange={(e) => setInputValue( + { ...inputValue, email: e.target.value }, + )} /> - - - + {isError.emailError && ( + + + + )}
-

- Email is required -

+ {isError.emailError && ( +

+ Email is required +

+ )}
@@ -75,25 +173,44 @@ export const NewCommentForm: React.FC = () => { id="comment-body" name="body" placeholder="Type comment here" - className="textarea is-danger" + className={classNames( + 'textarea', + { 'is-danger': isError.textariaError }, + )} + value={inputValue.body} + onChange={(e) => setInputValue( + { ...inputValue, body: e.target.value }, + )} />
-

- Enter some text -

+ {isError.textariaError && ( +

+ 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..7d4b33377 100644 --- a/src/components/PostDetails.tsx +++ b/src/components/PostDetails.tsx @@ -1,117 +1,162 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Loader } from './Loader'; import { NewCommentForm } from './NewCommentForm'; +import { Post } from '../types/Post'; +import * as commentsService from './api/comments'; +import { Comment, CommentData } from '../types/Comment'; -export const PostDetails: React.FC = () => { - return ( -
-
-
-

- #18: voluptate et itaque vero tempora molestiae -

+type Props = { + selectedPost: Post | null; +}; -

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

-
+export const PostDetails: React.FC = ({ selectedPost }) => { + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + const [showNewCommentForm, setShowNewCommentForm] = useState(false); + const [postComments, setPostComments] = useState([]); + const [newPostLoading, setNewPostLoading] = useState(false); -
- + useEffect(() => { + if (selectedPost) { + setIsLoading(true); + commentsService.getComments(selectedPost.id) + .then((data) => { + setPostComments(data); + }) + .catch((error) => { + // eslint-disable-next-line no-console + console.error('Something went wrong!', error); + setIsError(true); + }) + .finally(() => { + setIsLoading(false); + }); + } + }, [selectedPost]); -
- Something went wrong -
+ useEffect(() => { + setShowNewCommentForm(false); + }, [selectedPost]); -

- No comments yet -

+ const handleAddCommentButtonClick = () => { + setShowNewCommentForm(true); + }; -

Comments:

+ const handleDeleteComment = (commentId: number) => { + setPostComments(postComments.filter(comment => comment.id !== commentId)); + commentsService.deleteComments(commentId) + .catch((error) => { + // eslint-disable-next-line no-console + console.error('Error deleting comment:', error); + setIsError(true); + }); + }; -
-
- - Misha Hrynko - - -
+ const addComment = (newCommentValue: CommentData) => { + if (selectedPost) { + setNewPostLoading(true); + const newComment = { + id: 0, + postId: selectedPost.id, + name: newCommentValue.name, + email: newCommentValue.email, + body: newCommentValue.body, + }; -
- Some comment -
-
+ commentsService.createComment(newComment) + .then(() => { + setPostComments([...postComments, newComment]); + }) + .catch((error) => { + // eslint-disable-next-line no-console + console.error('Something went wrong!', error); + setIsError(true); + }) + .finally(() => { + setNewPostLoading(false); + }); + } + }; -
-
- - Misha Hrynko - + const renderContent = () => { + if (isLoading) { + return ; + } - -
-
- One more comment -
-
+ if (isError) { + return ( +
+ Something went wrong +
+ ); + } -
+ if (postComments.length === 0) { + return ( +

+ No comments yet +

+ ); + } + + return ( + <> +

Comments:

+ {postComments.map((comment) => ( + + ))} + + ); + }; + return ( +
+
+

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

+

{selectedPost?.body}

+
+
+ {renderContent()} + + {!showNewCommentForm ? ( -
- - + ) : ( + + )}
); diff --git a/src/components/PostsList.tsx b/src/components/PostsList.tsx index 6e6efc0f3..20699bdd2 100644 --- a/src/components/PostsList.tsx +++ b/src/components/PostsList.tsx @@ -1,6 +1,18 @@ -import React from 'react'; +import classNames from 'classnames'; -export const PostsList: React.FC = () => ( +import { Post } from '../types/Post'; + +type Props = { + userPosts: Post[] | []; + selectedPost: Post | null; + setSelectedPost: (selectedPost: Post | null) => void; +}; + +export const PostsList: React.FC = ({ + userPosts, + selectedPost, + setSelectedPost, +}) => (

Posts:

@@ -14,71 +26,45 @@ export const PostsList: React.FC = () => ( - - 17 - - - fugit voluptas sed molestias voluptatem provident - + {userPosts?.map(post => { + const handleButtonClick = () => { + if (selectedPost === null || selectedPost !== post) { + setSelectedPost(post); + } else { + setSelectedPost(null); + } + }; - - - - - - - 18 + const postIsSelect = selectedPost === post; - - voluptate et itaque vero tempora molestiae - + return ( + + {post.id} - - - - + + {post.title} + - - 19 - adipisci placeat illum aut reiciendis qui - - - - - - - - 20 - doloribus ad provident suscipit at - - - - - + + + + + ); + })}
diff --git a/src/components/UserSelector.tsx b/src/components/UserSelector.tsx index cb83a8f68..39d952856 100644 --- a/src/components/UserSelector.tsx +++ b/src/components/UserSelector.tsx @@ -1,10 +1,52 @@ -import React from 'react'; +import classNames from 'classnames'; + +import React, { useEffect, useRef, useState } from 'react'; +import { User } from '../types/User'; + +type Props = { + users: User[]; + selectedUser: User | null; + setSelectedUser: (selectedUser: User) => void; + setSelectedPost: (SelectedPost: null) => void; +}; + +export const UserSelector: React.FC = ({ + users, + selectedUser, + setSelectedUser, + setSelectedPost, +}) => { + const [showMenu, setShowMenu] = useState(false); + const menuRef = useRef(null); + + const handleButtonMenuClick = () => { + setShowMenu(!showMenu); + }; + + useEffect(() => { + const handleDocumentClick = (event: MouseEvent) => { + if ( + menuRef.current && !menuRef.current.contains(event.target as Node) + ) { + setShowMenu(false); + } + }; + + document.addEventListener('click', handleDocumentClick); + + return () => { + document.removeEventListener('click', handleDocumentClick); + }; + }, []); -export const UserSelector: React.FC = () => { return (