From 504dcde2d958617dbf3eb5f7b115a31060432c19 Mon Sep 17 00:00:00 2001 From: maxmodrr Date: Tue, 24 Dec 2024 03:27:00 +0200 Subject: [PATCH 1/2] results --- src/App.tsx | 137 ++++++++++++++++------- src/api/api.ts | 36 ++++++ src/components/NewCommentForm.tsx | 163 ++++++++++++++++++++++----- src/components/PostDetails.tsx | 177 ++++++++++++++++-------------- src/components/PostsList.tsx | 106 ++++++++++++------ src/components/UserSelector.tsx | 89 +++++++++++---- src/utils/methods.ts | 1 + 7 files changed, 510 insertions(+), 199 deletions(-) create mode 100644 src/api/api.ts create mode 100644 src/utils/methods.ts diff --git a/src/App.tsx b/src/App.tsx index 017957182..abfe101b5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,53 +8,116 @@ 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 { getPosts, getUsers } from './api/api'; +import { Post } from './types/Post'; -export const App = () => ( -
-
-
-
-
-
- -
+export const App = () => { + const [users, setUsers] = useState([]); + const [posts, setPosts] = useState([]); + const [isError, setIsError] = useState(false); + const [loading, setLoading] = useState(false); + const [activeUser, setActiveUser] = useState(null); + const [activePost, setActivePost] = useState(null); + const [showSideBar, setShowSideBar] = useState(false); -
-

No user selected

+ useEffect(() => { + getUsers() + .then(setUsers) + .catch(() => setIsError(true)); + }, []); - + useEffect(() => { + if (activeUser) { + setLoading(true); -
- Something went wrong! -
+ getPosts(activeUser) + .then(setPosts) + .catch(() => setIsError(true)) + .finally(() => setLoading(false)); + } + }, [activeUser]); + + const isSideBar = + activePost && showSideBar && activeUser?.id === activePost.userId; -
- No posts yet + return ( +
+
+
+
+
+
+
- +
+ {loading ? ( + + ) : ( + <> + {isError && ( +
+ Something went wrong! +
+ )} + + {!isError && !activeUser && ( +

No user selected

+ )} + + {!isError && activeUser && posts.length === 0 && ( +
+ No posts yet +
+ )} + + {!isError && activeUser && posts.length > 0 && ( + + )} + + )} +
-
-
-
- +
+
+ {activePost && ( + + )} +
-
-
-); +
+ ); +}; diff --git a/src/api/api.ts b/src/api/api.ts new file mode 100644 index 000000000..f34507f76 --- /dev/null +++ b/src/api/api.ts @@ -0,0 +1,36 @@ +import { Comment } from '../types/Comment'; +import { Post } from '../types/Post'; +import { User } from '../types/User'; +import { client } from '../utils/fetchClient'; + +export const getUsers = () => { + return client.get('/users'); +}; + +export const getPosts = ({ id }: Pick) => { + return client.get(`/posts?userId=${id}`); +}; + +export const getComments = ({ id }: Pick) => { + return client.get(`/comments?postId=${id}`); +}; + +export const addComment = ({ + postId, + name, + email, + body, +}: Omit) => { + const data = { + postId, + name, + email, + body, + }; + + return client.post('/comments', data); +}; + +export const deleteComment = ({ id }: Pick) => { + return client.delete(`/comments/${id}`); +}; diff --git a/src/components/NewCommentForm.tsx b/src/components/NewCommentForm.tsx index 73a8a0b45..1af782f94 100644 --- a/src/components/NewCommentForm.tsx +++ b/src/components/NewCommentForm.tsx @@ -1,8 +1,86 @@ -import React from 'react'; +import cl from 'classnames'; +import React, { FormEvent, useState } from 'react'; +import { addComment } from '../api/api'; +import { pause } from '../utils/methods'; +import { Comment } from '../types/Comment'; + +interface Props { + postId: number; + onAddComment: (v: Comment) => void; +} + +export const NewCommentForm: React.FC = ({ postId, onAddComment }) => { + const [nameError, setNameError] = useState(false); + const [emailError, setEmailError] = useState(false); + const [bodyError, setBodyError] = useState(false); + + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [body, setBody] = useState(''); + + const [isLoading, setIsLoading] = useState(false); + + const handleAddChange = async () => { + setNameError(!name); + setEmailError(!email); + setBodyError(!body); + + if (!name || !email || !body) { + return; + } + + const newComment = { + postId, + name, + email, + body, + }; + + setIsLoading(true); + try { + await pause(); + + const res = await addComment(newComment); + + onAddComment(res); + setBody(''); + } catch { + } finally { + setIsLoading(false); + } + }; + + const handleAuthorChange = (e: React.ChangeEvent) => { + setName(e.target.value); + setNameError(false); + }; + + const handleEmailChange = (e: React.ChangeEvent) => { + setEmail(e.target.value); + setEmailError(false); + }; + + const handleCommentChange = (e: React.ChangeEvent) => { + setBody(e.target.value); + setBodyError(false); + }; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + }; + + const handleClear = () => { + setName(''); + setEmail(''); + setBody(''); + + setNameError(false); + setEmailError(false); + setBodyError(false); + }; -export const NewCommentForm: React.FC = () => { return ( -
+
-

- Name is required -

+ {nameError && ( +

+ Name is required +

+ )}
@@ -45,24 +129,30 @@ export const NewCommentForm: React.FC = () => { name="email" id="comment-author-email" placeholder="email@test.com" - className="input is-danger" + className={cl('input', { 'is-danger': emailError })} + value={email} + onChange={handleEmailChange} /> - - - + {emailError && ( + + + + )}
-

- Email is required -

+ {emailError && ( +

+ Email is required +

+ )}
@@ -75,25 +165,40 @@ export const NewCommentForm: React.FC = () => { id="comment-body" name="body" placeholder="Type comment here" - className="textarea is-danger" + className={cl('textarea', { 'is-danger': bodyError })} + value={body} + onChange={handleCommentChange} />
-

- Enter some text -

+ {bodyError && ( +

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

- #18: voluptate et itaque vero tempora molestiae -

+export interface Props { + post: Post; +} -

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

-
+export const PostDetails: React.FC = ({ post }) => { + const [comments, setComments] = useState([]); + const [isError, setIsError] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [showForm, setShowForm] = useState(false); -
- + const title = `#${post.id}: ${post.title}`; + const showButton = !isLoading && !showForm && !isError; -
- Something went wrong -
+ useEffect(() => { + setIsLoading(true); + getComments(post) + .then(setComments) + .catch(() => setIsError(true)) + .finally(() => setIsLoading(false)); + }, []); -

- No comments yet -

+ const handleChangeButton = () => { + setShowForm(true); + }; -

Comments:

+ const handleAddComment = (v: Comment) => { + setComments([...comments, v]); + }; -
-
- - Misha Hrynko - - -
+ const handleDeleteComment = async (com: Comment) => { + setComments(prev => prev.filter(e => e.id !== com.id)); + deleteComment(com); + }; -
- Some comment -
-
- -
-
- - Misha Hrynko - + return ( +
+
+

{title}

- -
-
- One more comment -
-
+

{post.body}

+
-
-
- - Misha Hrynko - +
+ {isLoading ? ( + + ) : ( + <> + {isError ? ( +
+ Something went wrong +
+ ) : ( + <> + {comments.length === 0 ? ( +

+ No comments yet +

+ ) : ( + <> +

Comments:

- -
+ {comments.map(comment => ( +
+
+ + {comment.name} + + +
-
- {'Multi\nline\ncomment'} -
-
+
+ {comment.body} +
+
+ ))} + + )} + + )} + + )} + {showButton && ( -
- - + )}
+ + {showForm && ( + + )}
); }; diff --git a/src/components/PostsList.tsx b/src/components/PostsList.tsx index cf90f04b0..cdd8e3bf5 100644 --- a/src/components/PostsList.tsx +++ b/src/components/PostsList.tsx @@ -1,36 +1,77 @@ -import React from 'react'; - -export const PostsList: React.FC = () => ( -
-

Posts:

- - - - - - - {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} - - - +import React, { useState } from 'react'; +import { Post } from '../types/Post'; +import cl from 'classnames'; - - +interface Props { + posts: Post[]; + onActivePost: (v: Post | null) => void; + onShowSideBar: (v: boolean) => void; +} + +export const PostsList: React.FC = ({ + posts, + onActivePost, + onShowSideBar, +}) => { + const [activePost, setActivePost] = useState(null); + const [showClose, setShowClose] = useState(false); + + const handleChangeButton = (post: Post) => { + if (post.id === activePost?.id) { + setShowClose(!showClose); + onShowSideBar(showClose ? false : true); + + return; + } + + onActivePost(post); + onShowSideBar(true); + + setShowClose(true); + setActivePost(post); + }; + + return ( +
+

Posts:

+ +
#Title
+ + + + + {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} + + + + + + {posts.map(post => ( + + + + + + + + ))} + {/* - - @@ -79,8 +120,9 @@ export const PostsList: React.FC = () => ( Open - - -
#Title
{post.id}{post.title} + +
17 fugit voluptas sed molestias voluptatem provident - -
-
-); + */} + + + + ); +}; diff --git a/src/components/UserSelector.tsx b/src/components/UserSelector.tsx index c89442841..57cc7beab 100644 --- a/src/components/UserSelector.tsx +++ b/src/components/UserSelector.tsx @@ -1,16 +1,68 @@ -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; +import { User } from '../types/User'; +import cl from 'classnames'; +import { pause } from '../utils/methods'; +import { Post } from '../types/Post'; + +interface Props { + users: User[]; + onActiveUser: (v: User) => void; + activeUser: User | null; + onShowSideBar: (v: boolean) => void; + onActivePost: (v: Post | null) => void; +} + +export const UserSelector: React.FC = ({ + users, + onActiveUser, + activeUser, + onShowSideBar, + onActivePost, +}) => { + const [showDropDown, setShowDropDown] = useState(false); + const divRef = useRef(null); + + useEffect(() => { + const handleClickedOutside = (e: MouseEvent) => { + if (divRef.current && !divRef.current.contains(e.target as Node)) { + setShowDropDown(false); + } + }; + + document.addEventListener('click', handleClickedOutside); + + return () => { + document.removeEventListener('click', handleClickedOutside); + }; + }, []); + + const handleSelectUser = async (user: User) => { + onActiveUser(user); + setShowDropDown(false); + onShowSideBar(false); + onActivePost(null); + await pause(); + }; -export const UserSelector: React.FC = () => { return ( -
-
+
+
setShowDropDown(!showDropDown)} + >