Skip to content

Commit

Permalink
add task solution
Browse files Browse the repository at this point in the history
  • Loading branch information
siefimov committed Oct 9, 2024
1 parent 2315cc2 commit 61dee3e
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 78 deletions.
7 changes: 6 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ export const App = () => {
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [selectedPost, setSelectedPost] = useState<Post | null>(null);

const handleUserSelect = (user: User) => {
setSelectedUser(user);
setSelectedPost(null);
};

return (
<main className="section">
<div className="container">
Expand All @@ -24,7 +29,7 @@ export const App = () => {
<div className="block">
<UserSelector
selectedUser={selectedUser}
onSelectUser={setSelectedUser}
onSelectUser={handleUserSelect}
/>
</div>

Expand Down
8 changes: 4 additions & 4 deletions src/api/comments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import { QueryParams } from '../enums/query-params.enum';
import { Comment } from '../types/Comment';
import { client } from '../utils/fetchClient';

export const getCommentsByPost = (id: number) => {
export const getCommentsByPost = (postId: number) => {
return client.get<Comment[]>(
`${ApiPath.COMMENTS}?${QueryParams.POST_ID}=${id}`,
`${ApiPath.COMMENTS}?${QueryParams.POST_ID}=${postId}`,
);
};

export const addComment = (comment: Omit<Comment, 'id'>) => {
return client.post<Comment>(ApiPath.COMMENTS, comment);
};

export const deleteComment = (id: number) => {
return client.delete(`${ApiPath.COMMENTS}/${id}`);
export const deleteComment = (commentId: number) => {
return client.delete(`${ApiPath.COMMENTS}/${commentId}`);
};
6 changes: 4 additions & 2 deletions src/api/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { client } from '../utils/fetchClient';
import { Post } from '../types/Post';
import { ApiPath, QueryParams } from '../enums/enums';

export const getPostsByUserId = (id: number) => {
return client.get<Post[]>(`${ApiPath.POSTS}?${QueryParams.USER_ID}=${id}`);
export const getPostsByUserId = (userId: number) => {
return client.get<Post[]>(
`${ApiPath.POSTS}?${QueryParams.USER_ID}=${userId}`,
);
};
6 changes: 6 additions & 0 deletions src/components/NewCommentForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ export const NewCommentForm: React.FC<Props> = ({ postId, onSubmit }) => {
email: '',
body: '',
});

setErrors({
name: false,
email: false,
body: false,
});
};

return (
Expand Down
75 changes: 42 additions & 33 deletions src/components/PostDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Loader } from './Loader';
import { NewCommentForm } from './NewCommentForm';
import { Post } from '../types/Post';
Expand All @@ -11,9 +11,7 @@ type Props = {

export const PostDetails: React.FC<Props> = ({ post }) => {
const [comments, setComments] = useState<Comment[]>([]);

const [isAddingComment, setIsAddingComment] = useState(false);

const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(false);

Expand All @@ -40,24 +38,34 @@ export const PostDetails: React.FC<Props> = ({ post }) => {
loadComments();
}, [post]);

const handleCommentAdd = async (commentData: Omit<Comment, 'id'>) => {
try {
const newComment = await addComment(commentData);
useEffect(() => {
setIsAddingComment(false);
}, [post]);

setComments([...(comments || []), newComment]);
} catch {
setError(true);
}
};

const handleCommentDelete = async (id: number) => {
setComments(comments.filter(comment => comment.id !== id));
try {
await deleteComment(id);
} catch {
setError(true);
}
};
const handleAddComment = useCallback(
async (commentData: Omit<Comment, 'id'>) => {
try {
const newComment = await addComment(commentData);

setComments([...(comments || []), newComment]);
} catch {
setError(true);
}
},
[comments],
);

const handleDeleteComment = useCallback(
async (id: number) => {
setComments(comments.filter(comment => comment.id !== id));
try {
await deleteComment(id);
} catch {
setError(true);
}
},
[comments],
);

return (
<div className="content" data-cy="PostDetails">
Expand All @@ -76,7 +84,7 @@ export const PostDetails: React.FC<Props> = ({ post }) => {
</div>
)}

{!isLoading && comments?.length === 0 && (
{!isLoading && comments?.length === 0 && !error && (
<p className="title is-4" data-cy="NoCommentsMessage">
No comments yet
</p>
Expand All @@ -93,15 +101,15 @@ export const PostDetails: React.FC<Props> = ({ post }) => {
data-cy="Comment"
>
<div className="message-header">
<a href={`${email}`} data-cy="CommentAuthor">
<a href={`mailto:${email}`} data-cy="CommentAuthor">
{name}
</a>
<button
data-cy="CommentDelete"
type="button"
className="delete is-small"
aria-label="delete"
onClick={() => handleCommentDelete(id)}
onClick={() => handleDeleteComment(id)}
></button>
</div>

Expand All @@ -112,18 +120,19 @@ export const PostDetails: React.FC<Props> = ({ post }) => {
))}
</>
)}

<button
data-cy="WriteCommentButton"
type="button"
className="button is-link"
onClick={() => setIsAddingComment(true)}
>
Write a comment
</button>
{!isLoading && !isAddingComment && !error && (
<button
data-cy="WriteCommentButton"
type="button"
className="button is-link"
onClick={() => setIsAddingComment(true)}
>
Write a comment
</button>
)}
</div>
{isAddingComment && post && (
<NewCommentForm postId={post.id} onSubmit={handleCommentAdd} />
<NewCommentForm postId={post.id} onSubmit={handleAddComment} />
)}
</div>
</div>
Expand Down
66 changes: 39 additions & 27 deletions src/components/PostsList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Post } from '../types/Post';
import cn from 'classnames';
import { getPostsByUserId } from '../api/posts';
Expand All @@ -7,19 +7,27 @@ import { Loader } from './Loader';

type Props = {
selectedUser: User | null;
onPostSelect: (post: Post) => void;
onPostSelect: (post: Post | null) => void;
};

export const PostsList: React.FC<Props> = ({ onPostSelect, selectedUser }) => {
const [posts, setPosts] = useState<Post[]>([]);
const [openPost, setOpenPost] = useState<Post | null>(null);
const [selectedPost, setSelectedPost] = useState<Post | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(false);

const handlePostOpen = (post: Post) => {
setOpenPost(prevState => (prevState === post ? null : post));
onPostSelect(post);
};
const handleOpenPost = useCallback(
(post: Post) => {
if (selectedPost === post) {
setSelectedPost(null);
onPostSelect(null);
} else {
setSelectedPost(post);
onPostSelect(post);
}
},
[selectedPost, onPostSelect],
);

useEffect(() => {
const loadPosts = async () => {
Expand Down Expand Up @@ -79,26 +87,30 @@ export const PostsList: React.FC<Props> = ({ onPostSelect, selectedUser }) => {
</thead>

<tbody>
{posts.map(post => (
<tr key={post.id} data-cy="Post">
<td data-cy="PostId">{post.id}</td>

<td data-cy="PostTitle">{post.body}</td>

<td className="has-text-right is-vcentered">
<button
type="button"
data-cy="PostButton"
className={cn('button is-link', {
'is-light': openPost?.id !== post.id,
})}
onClick={() => handlePostOpen(post)}
>
{openPost?.id === post.id ? 'Close' : 'Open'}
</button>
</td>
</tr>
))}
{posts.map(post => {
const { id, body } = post;

return (
<tr key={id} data-cy="Post">
<td data-cy="PostId">{id}</td>

<td data-cy="PostTitle">{body}</td>

<td className="has-text-right is-vcentered">
<button
type="button"
data-cy="PostButton"
className={cn('button is-link', {
'is-light': selectedPost?.id !== id,
})}
onClick={() => handleOpenPost(post)}
>
{selectedPost?.id === id ? 'Close' : 'Open'}
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
Expand Down
25 changes: 14 additions & 11 deletions src/components/UserSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { User } from '../types/User';
import cn from 'classnames';
import { getUsers } from '../api/users';
Expand Down Expand Up @@ -32,27 +32,30 @@ export const UserSelector: React.FC<Props> = ({
loadUsers();
}, []);

const handleOutsideCLick = (e: MouseEvent) => {
const handleOutsideClick = useCallback((e: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(e.target as Node)
) {
setIsDropdownOpen(false);
}
};
}, []);

const handleUserSelect = useCallback(
(user: User) => {
onSelectUser(user);
setIsDropdownOpen(false);
},
[onSelectUser],
);

useEffect(() => {
document.addEventListener('mousedown', handleOutsideCLick);
document.addEventListener('mousedown', handleOutsideClick);

return () => {
document.removeEventListener('mousedown', handleOutsideCLick);
document.removeEventListener('mousedown', handleOutsideClick);
};
}, []);

const handleUserSelect = (user: User) => {
onSelectUser(user);
setIsDropdownOpen(false);
};
}, [handleOutsideClick]);

return (
<div
Expand Down

0 comments on commit 61dee3e

Please sign in to comment.