From 6583e471943059b29eed669c344b8abce9b8ff33 Mon Sep 17 00:00:00 2001 From: mykyta01 Date: Thu, 21 Mar 2024 21:54:22 +0100 Subject: [PATCH 1/7] develop --- README.md | 1 + package-lock.json | 164 +++++++------------- src/App.scss | 4 + src/App.tsx | 72 +++++++-- src/api/comments.ts | 14 ++ src/api/posts.ts | 6 + src/api/users.ts | 6 + src/components/CommentItem.tsx | 54 +++++++ src/components/NewCommentForm.tsx | 243 ++++++++++++++++++++++++++---- src/components/PostDetails.tsx | 152 +++++++++---------- src/components/PostsList.tsx | 140 +++++++---------- src/components/UserSelector.tsx | 88 ++++++++--- src/index.tsx | 10 +- src/providers/PostProvider.tsx | 36 +++++ src/providers/UserProvider.tsx | 36 +++++ 15 files changed, 691 insertions(+), 335 deletions(-) create mode 100644 src/api/comments.ts create mode 100644 src/api/posts.ts create mode 100644 src/api/users.ts create mode 100644 src/components/CommentItem.tsx create mode 100644 src/providers/PostProvider.tsx create mode 100644 src/providers/UserProvider.tsx diff --git a/README.md b/README.md index c33761fd7..d5e42d38b 100644 --- a/README.md +++ b/README.md @@ -35,3 +35,4 @@ Install Prettier Extention and use this [VSCode settings](https://mate-academy.g 1. Implement comment deletion - Delete the commnet immediately not waiting for the server response to improve the UX. 1. (*) Handle `Add` and `Delete` errors so the user can retry + diff --git a/package-lock.json b/package-lock.json index fdb2e350e..abdffd7c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1671,19 +1671,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==" }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -1694,36 +1681,6 @@ "strip-ansi": "^7.0.1" } }, - "string-width-cjs": { - "version": "npm:string-width@4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - } - } - }, "strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -1732,21 +1689,6 @@ "ansi-regex": "^6.0.1" } }, - "strip-ansi-cjs": { - "version": "npm:strip-ansi@6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - } - } - }, "wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -1756,54 +1698,6 @@ "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } - }, - "wrap-ansi-cjs": { - "version": "npm:wrap-ansi@7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "requires": { - "ansi-regex": "^5.0.1" - } - } - } } } }, @@ -14357,6 +14251,23 @@ } } }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "dependencies": { + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + } + } + }, "string.prototype.matchall": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", @@ -14492,6 +14403,14 @@ "ansi-regex": "^5.0.1" } }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -16324,6 +16243,39 @@ } } }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + } + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/src/App.scss b/src/App.scss index 695435da4..96c35b55a 100644 --- a/src/App.scss +++ b/src/App.scss @@ -1,3 +1,7 @@ +iframe { + display: none; +} + .Sidebar { overflow: hidden; opacity: 0; diff --git a/src/App.tsx b/src/App.tsx index cad8e12c6..81a1d0610 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import 'bulma/bulma.sass'; import '@fortawesome/fontawesome-free/css/all.css'; import './App.scss'; @@ -8,8 +8,44 @@ import { PostsList } from './components/PostsList'; import { PostDetails } from './components/PostDetails'; import { UserSelector } from './components/UserSelector'; import { Loader } from './components/Loader'; +import { SelectedUserContext } from './providers/UserProvider'; +import { SelectedPostContext } from './providers/PostProvider'; +import { getPosts } from './api/posts'; +import { Post } from './types/Post'; export const App: React.FC = () => { + const [posts, setPosts] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isErrorOnPostsLoad, setIsErrorOnPostsLoad] = useState(false); + + const { selectedUser } = useContext(SelectedUserContext); + const { selectedPost } = useContext(SelectedPostContext); + + const isSomethingWrong = !!selectedUser && isErrorOnPostsLoad && !isLoading; + + const isNoPosts = + !!selectedUser && !isErrorOnPostsLoad && !posts.length && !isLoading; + + const isPostsShow = + !!selectedUser && !isErrorOnPostsLoad && !!posts.length && !isLoading; + + useEffect(() => { + if (selectedUser) { + setIsLoading(true); + + getPosts(selectedUser.id) + .then(loadedPosts => { + setPosts(loadedPosts); + }) + .catch(() => { + setIsErrorOnPostsLoad(true); + }) + .finally(() => { + setIsLoading(false); + }); + } + }, [selectedUser]); + return (
@@ -21,22 +57,28 @@ export const App: React.FC = () => {
-

No user selected

+ {!selectedUser && ( +

No user selected

+ )} - + {!!selectedUser && isLoading && } -
- Something went wrong! -
+ {isSomethingWrong && ( +
+ Something went wrong! +
+ )} -
- No posts yet -
+ {isNoPosts && ( +
+ No posts yet +
+ )} - + {isPostsShow && }
@@ -48,11 +90,11 @@ export const App: React.FC = () => { 'is-parent', 'is-8-desktop', 'Sidebar', - 'Sidebar--open', + { 'Sidebar--open': !!selectedPost }, )} >
- + {!!selectedPost && }
diff --git a/src/api/comments.ts b/src/api/comments.ts new file mode 100644 index 000000000..5cb3159fc --- /dev/null +++ b/src/api/comments.ts @@ -0,0 +1,14 @@ +import { client } from '../utils/fetchClient'; +import { Comment } from '../types/Comment'; + +export function getComments(postId: number) { + return client.get(`/comments?postId=${postId}`); +} + +export function deleteComment(id: number) { + return client.delete(`/comments/${id}`); +} + +export function createComment(data: Omit) { + return client.post('/comments', data); +} diff --git a/src/api/posts.ts b/src/api/posts.ts new file mode 100644 index 000000000..c249c120c --- /dev/null +++ b/src/api/posts.ts @@ -0,0 +1,6 @@ +import { Post } from '../types/Post'; +import { client } from '../utils/fetchClient'; + +export function getPosts(userId: number) { + return client.get(`/posts?userId=${userId}`); +} diff --git a/src/api/users.ts b/src/api/users.ts new file mode 100644 index 000000000..4a4bdac1c --- /dev/null +++ b/src/api/users.ts @@ -0,0 +1,6 @@ +import { User } from '../types/User'; +import { client } from '../utils/fetchClient'; + +export function getUsers() { + return client.get('/users'); +} diff --git a/src/components/CommentItem.tsx b/src/components/CommentItem.tsx new file mode 100644 index 000000000..5e680051c --- /dev/null +++ b/src/components/CommentItem.tsx @@ -0,0 +1,54 @@ +import React, { useCallback } from 'react'; +import { Comment } from '../types/Comment'; +import { deleteComment } from '../api/comments'; + +type Props = { + comment: Comment; + comments: Comment[]; + setComments: React.Dispatch>; +}; + +export const CommentItem: React.FC = ({ + comment, + comments, + setComments, +}) => { + const { id, email, name, body } = comment; + + const handleDeleteComment = useCallback(() => { + const oldComments = [...comments]; + + setComments(prevComments => { + return prevComments.filter(c => c.id !== id); + }); + + deleteComment(id).catch(() => { + setComments(oldComments); + // eslint-disable-next-line no-console + console.error('Failed to delete comment'); + }); + }, [id, setComments, comments]); + + return ( + + ); +}; diff --git a/src/components/NewCommentForm.tsx b/src/components/NewCommentForm.tsx index 73a8a0b45..684f759b4 100644 --- a/src/components/NewCommentForm.tsx +++ b/src/components/NewCommentForm.tsx @@ -1,8 +1,162 @@ -import React from 'react'; +import classNames from 'classnames'; +import React, { useContext, useState } from 'react'; +import { createComment } from '../api/comments'; +import { SelectedPostContext } from '../providers/PostProvider'; +import { Comment } from '../types/Comment'; + +const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + +type Props = { + setComments: React.Dispatch>; +}; + +export const NewCommentForm: React.FC = ({ setComments }) => { + const [author, setAuthor] = useState({ + name: '', + email: '', + comment: '', + }); + + const [error, setError] = useState({ + name: '', + email: '', + comment: '', + }); + + const [isLoading, setIsLoading] = useState(false); + + const { selectedPost } = useContext(SelectedPostContext); + + const isEmailValid = EMAIL_REGEX.test(author.email.trim()); + + const handleAuthNameChange = (event: React.ChangeEvent) => { + setError(prevError => ({ + ...prevError, + name: '', + })); + setAuthor(prevAuthor => ({ + ...prevAuthor, + name: event.target.value, + })); + }; + + const handleAuthEmailChange = ( + event: React.ChangeEvent, + ) => { + setError(prevError => ({ + ...prevError, + email: '', + })); + setAuthor(prevAuthor => ({ + ...prevAuthor, + email: event.target.value, + })); + }; + + const handleAuthCommentChange = ( + event: React.ChangeEvent, + ) => { + setError(prevError => ({ + ...prevError, + commentError: '', + })); + setAuthor(prevAuthor => ({ + ...prevAuthor, + comment: event.target.value, + })); + }; + + const handleClearForm = () => { + setAuthor({ + name: '', + email: '', + comment: '', + }); + setError({ + name: '', + email: '', + comment: '', + }); + }; + + const handleAddingComment = (event: React.FormEvent) => { + event.preventDefault(); + + const normalizedAuthName = author.name.trim(); + const normalizedAuthEmail = author.email.trim(); + const normalizedAuthComment = author.comment.trim(); + + if (!normalizedAuthName) { + setError(prevError => ({ + ...prevError, + name: 'Name is required', + })); + } + + if (!normalizedAuthEmail) { + setError(prevError => ({ + ...prevError, + email: 'Email is required', + })); + } + + if (!isEmailValid && !!normalizedAuthEmail.length) { + setError(prevError => ({ + ...prevError, + email: 'Email should be valid', + })); + } + + if (!normalizedAuthComment) { + setError(prevError => ({ + ...prevError, + comment: 'Enter some text', + })); + } + + if ( + !normalizedAuthName || + !normalizedAuthEmail || + !isEmailValid || + !normalizedAuthComment + ) { + return; + } + + setIsLoading(true); + + if (!selectedPost) { + setIsLoading(false); + + return; + } + + createComment({ + name: normalizedAuthName, + email: normalizedAuthEmail, + body: normalizedAuthComment, + postId: selectedPost.id, + }) + .then(newComment => { + setAuthor(prevAuthor => ({ + ...prevAuthor, + comment: '', + })); + setComments(prevComments => { + return [...prevComments, newComment]; + }); + }) + .catch(() => { + // eslint-disable-next-line no-console + console.error('Failed to add comment'); + }) + .finally(() => { + setIsLoading(false); + }); + }; -export const NewCommentForm: React.FC = () => { return ( -
+
-

- Name is required -

+ {!!error.name.length && ( +

+ {error.name} +

+ )}
@@ -45,24 +207,32 @@ export const NewCommentForm: React.FC = () => { name="email" id="comment-author-email" placeholder="email@test.com" - className="input is-danger" + className={classNames('input', { + 'is-danger': !!error.email.length, + })} + value={author.email} + onChange={handleAuthEmailChange} /> - - - + {!!error.email.length && ( + + + + )}
-

- Email is required -

+ {!!error.email.length && ( +

+ {error.email} +

+ )}
@@ -75,25 +245,40 @@ export const NewCommentForm: React.FC = () => { id="comment-body" name="body" placeholder="Type comment here" - className="textarea is-danger" + className={classNames('textarea', { + 'is-danger': !!error.comment.length, + })} + value={author.comment} + onChange={handleAuthCommentChange} />
-

- Enter some text -

+ {!!error.comment.length && ( +

+ {error.comment} +

+ )}
-
{/* eslint-disable-next-line react/button-has-type */} -
diff --git a/src/components/PostDetails.tsx b/src/components/PostDetails.tsx index 2f82db916..ccb9c7bdb 100644 --- a/src/components/PostDetails.tsx +++ b/src/components/PostDetails.tsx @@ -1,106 +1,98 @@ -import React from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { Loader } from './Loader'; import { NewCommentForm } from './NewCommentForm'; +import { SelectedPostContext } from '../providers/PostProvider'; +import { getComments } from '../api/comments'; +import { Comment } from '../types/Comment'; +import { CommentItem } from './CommentItem'; export const PostDetails: React.FC = () => { + const [comments, setComments] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [isErrorOnCommentsLoad, setIsErrorOnCommentsLoad] = useState(false); + const [isAddingForm, setIsAddingForm] = useState(false); + + const { selectedPost } = useContext(SelectedPostContext); + + const { id, title, body } = selectedPost || {}; + + const isNoComments = !isLoading && !isErrorOnCommentsLoad && !comments.length; + + const isCommentsShow = + !isLoading && !isErrorOnCommentsLoad && !!comments.length; + + const isButtonShow = !isAddingForm && !isErrorOnCommentsLoad && !isLoading; + + useEffect(() => { + setIsAddingForm(false); + if (selectedPost) { + setIsLoading(true); + + getComments(selectedPost.id) + .then(loadedComments => { + setComments(loadedComments); + }) + .catch(() => { + setIsErrorOnCommentsLoad(true); + }) + .finally(() => { + setIsLoading(false); + }); + } + }, [selectedPost]); + return (
-

- #18: voluptate et itaque vero tempora molestiae -

- -

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

+

{`#${id}: ${title}`}

+ +

{body}

- - -
- Something went wrong -
- -

- No comments yet -

- -

Comments:

- - - - - -
-
- - Misha Hrynko - - - + {!isLoading && isErrorOnCommentsLoad && ( +
+ Something went wrong
+ )} -
- {'Multi\nline\ncomment'} -
-
+ {isNoComments && ( +

+ No comments yet +

+ )} + {isCommentsShow && ( + <> +

Comments:

+ + {comments.map(comment => ( + + ))} + + )} +
+ + {isButtonShow && ( -
+ )} - + {isAddingForm && }
); diff --git a/src/components/PostsList.tsx b/src/components/PostsList.tsx index cf90f04b0..1120607e4 100644 --- a/src/components/PostsList.tsx +++ b/src/components/PostsList.tsx @@ -1,86 +1,54 @@ -import React from 'react'; - -export const PostsList: React.FC = () => ( -
-

Posts:

- - - - - - - {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
#Title
17 - fugit voluptas sed molestias voluptatem provident - - -
18 - voluptate et itaque vero tempora molestiae - - -
19adipisci placeat illum aut reiciendis qui - -
20doloribus ad provident suscipit at - -
-
-); +import React, { useContext } from 'react'; +import classNames from 'classnames'; +import { Post } from '../types/Post'; +import { SelectedPostContext } from '../providers/PostProvider'; + +type Props = { + posts: Post[]; +}; + +export const PostsList: React.FC = ({ posts }) => { + const { selectedPost, setSelectedPost } = useContext(SelectedPostContext); + + return ( +
+

Posts:

+ + + + + + + {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */} + + + + + + {posts.map(post => ( + + + + + + + + ))} + +
#Title
{post.id}{post.title} + +
+
+ ); +}; diff --git a/src/components/UserSelector.tsx b/src/components/UserSelector.tsx index c89442841..4c51af3fe 100644 --- a/src/components/UserSelector.tsx +++ b/src/components/UserSelector.tsx @@ -1,16 +1,61 @@ -import React from 'react'; +import React, { useContext, useEffect, useRef, useState } from 'react'; +import classNames from 'classnames'; +import { User } from '../types/User'; +import { getUsers } from '../api/users'; +import { SelectedUserContext } from '../providers/UserProvider'; +import { SelectedPostContext } from '../providers/PostProvider'; export const UserSelector: React.FC = () => { + const [users, setUsers] = useState([]); + const [isVisible, setIsVisible] = useState(false); + + const dropdownRef = useRef(null); + + const { selectedUser, setSelectedUser } = useContext(SelectedUserContext); + const { setSelectedPost } = useContext(SelectedPostContext); + + useEffect(() => { + getUsers().then(setUsers); + }, []); + + useEffect(() => { + const handleClick = (e: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(e.target as Node) + ) { + setIsVisible(false); + } + }; + + document.addEventListener('click', handleClick); + + return () => { + document.removeEventListener('click', handleClick); + }; + }, []); + return ( -
+