Skip to content

Commit

Permalink
solution
Browse files Browse the repository at this point in the history
  • Loading branch information
illiavinkov committed Aug 2, 2023
1 parent 757d487 commit d6cd307
Show file tree
Hide file tree
Showing 10 changed files with 573 additions and 238 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,5 @@ form to add new comments.
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

[DEMO LINK](https://Kinderwagen.github.io/react_dynamic-list-of-posts/)
74 changes: 50 additions & 24 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,60 +1,86 @@
import React from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import 'bulma/bulma.sass';
import '@fortawesome/fontawesome-free/css/all.css';
import './App.scss';

import classNames from 'classnames';
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 { Post } from './types/Post';
import { getUsers } from './api/users';

export const App: React.FC = () => {
const [users, setUsers] = useState<User[]>([]);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [selectedPost, setSelectedPost] = useState<Post | null>(null);

const handleSelectUser = useCallback((user) => {
setSelectedUser(user);
}, []);

const handleSelectPost = useCallback((post) => {
setSelectedPost(post);
}, []);

useEffect(() => {
const getUsersFromServer = async () => {
try {
const usersFromServer = await getUsers();

setUsers(usersFromServer);
} catch {
throw new Error('Can\'t load users');
}
};

getUsersFromServer();
}, []);

return (
<main className="section">
<div className="container">
<div className="tile is-ancestor">
<div className="tile is-parent">
<div className="tile is-child box is-success">
<div className="block">
<UserSelector />
<UserSelector
users={users}
selectedUser={selectedUser}
selectUser={handleSelectUser}
/>
</div>

<div className="block" data-cy="MainContent">
<p data-cy="NoSelectedUser">
No user selected
</p>

<Loader />

<div
className="notification is-danger"
data-cy="PostsLoadingError"
>
Something went wrong!
</div>
{!selectedUser
? (
<p data-cy="NoSelectedUser">
No user selected
</p>
)
: (
<PostsList
selectedUserId={selectedUser.id}
selectedPost={selectedPost}
onSelectPost={handleSelectPost}
/>
)}

<div className="notification is-warning" data-cy="NoPostsYet">
No posts yet
</div>

<PostsList />
</div>
</div>
</div>

<div
data-cy="Sidebar"
className={classNames(
'tile',
'is-parent',
'is-8-desktop',
'Sidebar',
'Sidebar--open',
{ 'Sidebar--open': selectedPost },
)}
>
<div className="tile is-child box is-success ">
<PostDetails />
{selectedPost && <PostDetails post={selectedPost} />}
</div>
</div>
</div>
Expand Down
14 changes: 14 additions & 0 deletions src/api/comments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Comment, CommentData } from '../types/Comment';
import { client } from '../utils/fetchClient';

export const getComments = (postId: number) => {
return client.get<Comment[]>(`/comments?postId=${postId}`);
};

export const deleteComment = (commentId: number) => {
return client.delete(`/comments/${commentId}`);
};

export const postComment = (comment: CommentData) => {
return client.post<Comment>('/comments', comment);
};
6 changes: 6 additions & 0 deletions src/api/posts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Post } from '../types/Post';
import { client } from '../utils/fetchClient';

export const getUserPosts = (userId: number) => {
return client.get<Post[]>(`/posts?userId=${userId}`);
};
6 changes: 6 additions & 0 deletions src/api/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { User } from '../types/User';
import { client } from '../utils/fetchClient';

export const getUsers = () => {
return client.get<User[]>('/users');
};
169 changes: 169 additions & 0 deletions src/components/CommentsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Comment, CommentData } from '../types/Comment';
import { Loader } from './Loader';
import { NewCommentForm } from './NewCommentForm';
import { deleteComment, getComments, postComment } from '../api/comments';

type Props = {
postId: number;
};

export const CommentsList: React.FC<Props> = ({ postId }) => {
const [comments, setComments] = useState<Comment[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [loadError, setLoadError] = useState(false);
const [addError, setAddError] = useState(false);
const [deleteError, setDeleteError] = useState(false);
const [isFormVisible, setIsFormVisible] = useState(false);

const addNewComment = useCallback(async (comment: CommentData) => {
const newComment = { ...comment, postId };

try {
const addedComment = await postComment(newComment);

setComments(currentComment => [...currentComment, addedComment]);
} catch {
setAddError(true);
}
}, []);

const deleteSelectedComment = useCallback(async (commentId: number) => {
const tempComments = [...comments];

setComments(currentComments => (
currentComments.filter(comment => comment.id !== commentId)
));

try {
setDeleteError(false);
await deleteComment(commentId);
} catch {
setComments(tempComments);
setDeleteError(true);
}
}, []);

useEffect(() => {
const fetchPostComments = async () => {
try {
setComments([]);
setIsFormVisible(false);
setLoadError(false);
setIsLoading(true);
const commentsFromServer = await getComments(postId);

setComments(commentsFromServer);
} catch {
setLoadError(true);
} finally {
setIsLoading(false);
}
};

fetchPostComments();
}, [postId]);

useEffect(() => {
const deleteErrorTimer = setTimeout(() => {
setDeleteError(false);
}, 3000);

return () => clearTimeout(deleteErrorTimer);
}, [deleteError]);

useEffect(() => {
const addErrorTimer = setTimeout(() => {
setAddError(false);
}, 3000);

return () => clearTimeout(addErrorTimer);
}, [addError]);

if (isLoading) {
return <Loader />;
}

if (loadError) {
return (
<div className="notification is-danger" data-cy="CommentsError">
Something went wrong
</div>
);
}

return (
<>
{!comments.length
? (
<p className="title is-4" data-cy="NoCommentsMessage">
No comments yet
</p>
)
: (
<>
<p className="title is-4">Comments:</p>

{comments.map(comment => (
<article
className="message is-small"
data-cy="Comment"
key={comment.id}
>
<div className="message-header">
<a
href={`mailto:${comment.email}`}
data-cy="CommentAuthor"
>
{comment.name}
</a>
<button
data-cy="CommentDelete"
type="button"
className="delete is-small"
aria-label="delete"
onClick={() => deleteSelectedComment(comment.id)}
/>
</div>

<div className="message-body" data-cy="CommentBody">
{comment.body}
</div>
</article>
))}
</>
)}

{addError && (
<div
className="notification is-danger"
data-cy="CommentsError"
>
Can&apos;t add new comment
</div>
)}

{deleteError && (
<div
className="notification is-danger"
data-cy="CommentsError"
>
Can&apos;t delete comment
</div>
)}

{!isFormVisible
? (
<button
data-cy="WriteCommentButton"
type="button"
className="button is-link"
onClick={() => setIsFormVisible(true)}
>
Write a comment
</button>
)
: <NewCommentForm onAddComment={addNewComment} />}
</>
);
};
Loading

0 comments on commit d6cd307

Please sign in to comment.