diff --git a/src/App.tsx b/src/App.tsx index 017957182..999acd232 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,54 +7,61 @@ import './App.scss'; import { PostsList } from './components/PostsList'; import { PostDetails } from './components/PostDetails'; import { UserSelector } from './components/UserSelector'; -import { Loader } from './components/Loader'; - -export const App = () => ( -
-
-
-
-
-
- -
+import { useState } from 'react'; +import { Post } from './types/Post'; -
-

No user selected

+export const App = () => { + const [selectedUser, setSelectedUser] = useState(null); + const [selectedPost, setSelectedPost] = useState(null); + const [postId, setPostId] = useState(); - + const handleUserSelect = (id: number) => { + setSelectedUser(id); + }; -
- Something went wrong! -
+ const handlePostSelect = (post: Post) => { + setSelectedPost(post); + setPostId(post.id); + }; -
- No posts yet + return ( +
+
+
+
+
+
+
- +
+ {!selectedUser && ( +

No user selected

+ )} + + {selectedUser && ( + + )} +
-
-
-
- +
+
+ +
-
-
-); +
+ ); +}; diff --git a/src/components/NewCommentForm.tsx b/src/components/NewCommentForm.tsx index 73a8a0b45..c382459f8 100644 --- a/src/components/NewCommentForm.tsx +++ b/src/components/NewCommentForm.tsx @@ -1,8 +1,97 @@ -import React from 'react'; +import classNames from 'classnames'; +import React, { useState } from 'react'; +import { Comment } from '../types/Comment'; +import { client } from '../utils/fetchClient'; + +interface Props { + postId: number; + onAddComment: (newComment: Comment) => void; +} + +export const NewCommentForm: React.FC = ({ postId, onAddComment }) => { + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [comment, setComment] = useState(''); + const [nameError, setNameError] = useState(false); + const [emailError, setEmailError] = useState(false); + const [commentError, setCommentError] = useState(false); + const [loading, setLoading] = useState(false); + + function handleNameChange(event: React.ChangeEvent) { + setName(event.target.value); + setNameError(false); + } + + function handleEmailChange(event: React.ChangeEvent) { + setEmail(event.target.value); + setEmailError(false); + } + + function handleCommentChange(event: React.ChangeEvent) { + setComment(event.target.value); + setCommentError(false); + } + + const validate = (names: string, emaill: string, coment: string) => { + let isValid = true; + + if (!names.trim()) { + setNameError(true); + isValid = false; + } + + if (!emaill.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emaill)) { + setEmailError(true); + isValid = false; + } + + if (!coment.trim()) { + setCommentError(true); + isValid = false; + } + + return isValid; + }; + + const reset = () => { + setName(''); + setEmail(''); + setComment(''); + setNameError(false); + setEmailError(false); + setCommentError(false); + }; + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!validate(name, email, comment)) { + return; // Вихід, якщо валідація не пройдена + } + + const newComment: Comment = { + id: Date.now(), + postId, + name, + email, + body: comment, + }; + + try { + setLoading(true); + + const response = await client.post('/comments', newComment); + + onAddComment(response); + reset(); + } catch { + } finally { + setLoading(false); + } + }; -export const NewCommentForm: React.FC = () => { return ( -
+
-

- Name is required -

+ {nameError && ( +

+ Name is required +

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

- Email is required -

+ {emailError && ( +

+ Email is required +

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

- Enter some text -

+ {commentError && ( +

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

- #18: voluptate et itaque vero tempora molestiae -

+interface Props { + post: Post | null; + postId: number; +} -

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

-
+export const PostDetails: React.FC = ({ post, postId }) => { + const [comments, setComments] = useState([]); + const [commentsLoading, setCommentsLoading] = useState(false); + const [error, setError] = useState(false); + const [openPostId, setOpenPostId] = useState(null); -
- + const handleAddComment = (newComment: Comment) => { + setComments(prevComments => [...prevComments, newComment]); + }; -
- Something went wrong -
- -

- No comments yet -

+ useEffect(() => { + if (post === null) { + return; + } -

Comments:

+ const fetchComments = async () => { + setCommentsLoading(true); + setError(false); -
-
- - Misha Hrynko - - -
+ try { + const response = await client.get( + `/comments?postId=${post.id}`, + ); -
- Some comment -
-
+ setComments(response); + } catch { + setError(true); + } finally { + setCommentsLoading(false); + } + }; -
-
- - Misha Hrynko - + fetchComments(); + }, [post]); - -
-
- One more comment -
-
+ return ( +
+
+

{`#${post?.id}: ${post?.title}`}

+

{post?.body}

+
-
-
- - Misha Hrynko - +
+

Comments:

- -
+ {error ? ( +
+ Something went wrong +
+ ) : commentsLoading ? ( + + ) : comments.length === 0 ? ( +

+ No comments yet +

+ ) : ( + comments.map(comment => ( + + +
- -
+
+ {comment.body} +
+ + )) + )} - +
+ + {openPostId === post?.id && ( + + )}
); }; diff --git a/src/components/PostsList.tsx b/src/components/PostsList.tsx index cf90f04b0..2e5a8e5ed 100644 --- a/src/components/PostsList.tsx +++ b/src/components/PostsList.tsx @@ -1,86 +1,91 @@ -import React from 'react'; +/* eslint-disable max-len */ +import React, { useEffect, useState } from 'react'; +import { Post } from '../types/Post'; +import { client } from '../utils/fetchClient'; +import { Loader } from './Loader'; -export const PostsList: React.FC = () => ( -
-

Posts:

+interface Props { + userId: number; + onOpen: (post: Post) => void; +} - - - - - - {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} - - - +export const PostsList: React.FC = ({ userId, onOpen }) => { + const [posts, setPosts] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); - - - + useEffect(() => { + const fetchPost = async () => { + try { + setLoading(true); + const response = await client.get(`/posts?userId=${userId}`); - + setPosts(response); + } catch { + setError(true); + } finally { + setLoading(false); + } + }; - - + fetchPost(); + }, [userId]); - - + if (loading) { + return ; + } - + const handlePostSelect = (post: Post) => { + onOpen(post); + }; - - + return ( + <> + {error ? ( +
+ Something went wrong! +
+ ) : posts.length === 0 ? ( +
+ No posts yet +
+ ) : ( +
+

Posts:

-
- - +
#Title
17 - fugit voluptas sed molestias voluptatem provident - - -
18 - voluptate et itaque vero tempora molestiae - - -
19adipisci placeat illum aut reiciendis qui
+ + + + + {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} + + + - - + + {posts.map(post => ( + + - - - + - - - -
#Title
- -
{post.id}
20doloribus ad provident suscipit at{post.title} - -
-
-); + + + + + ))} + + +
+ )} + + ); +}; diff --git a/src/components/UserSelector.tsx b/src/components/UserSelector.tsx index c89442841..f048c47f5 100644 --- a/src/components/UserSelector.tsx +++ b/src/components/UserSelector.tsx @@ -1,6 +1,38 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import { User } from '../types/User'; +import { client } from '../utils/fetchClient'; + +interface Props { + onUserSelect: (id: number) => void; +} + +export const UserSelector: React.FC = ({ onUserSelect }) => { + const [users, setUser] = useState([]); + const [isOpen, setIsOpen] = useState(false); + const [selectedUserName, setSelectedUserName] = useState(null); + + useEffect(() => { + const fetchUser = async () => { + try { + const response = await client.get('/users'); + + setUser(response); + } catch {} + }; + + fetchUser(); + }, []); + + const toodleDropDown = () => { + setIsOpen(prev => !prev); + }; + + const handleUserSelect = (id: number, name: string) => { + setSelectedUserName(name); + onUserSelect(id); + setIsOpen(false); + }; -export const UserSelector: React.FC = () => { return (
@@ -9,8 +41,9 @@ export const UserSelector: React.FC = () => { className="button" aria-haspopup="true" aria-controls="dropdown-menu" + onClick={toodleDropDown} > - Choose a user + {selectedUserName || 'Choose a user'}