Add Load More button to profile sidebar

This commit is contained in:
esvyridov
2021-12-23 17:44:45 -06:00
committed by Umputun
parent 53f02ab4cd
commit cd391a8b0e
19 changed files with 118 additions and 15 deletions
+4 -2
View File
@@ -11,8 +11,10 @@ export const getPostComments = (sort: Sorting) => apiFetcher.get<Tree>('/find',
export const getComment = (id: Comment['id']): Promise<Comment> => apiFetcher.get(`/id/${id}`, { url });
export const getUserComments = (userId: User['id']): Promise<{ comments: Comment[]; count: number }> =>
apiFetcher.get('/comments', { user: userId, limit: 10 });
export const getUserComments = (
userId: User['id'],
config: { skip?: number; limit?: number } = { skip: 0, limit: 10 }
): Promise<{ comments: Comment[]; count: number }> => apiFetcher.get('/comments', { user: userId, ...config });
export const putCommentVote = ({ id, value }: { id: Comment['id']; value: number }): Promise<void> =>
apiFetcher.put(`/vote/${id}`, { url, vote: value });
@@ -7,6 +7,7 @@ import * as pq from 'utils/parse-query';
import type { Comment, User } from 'common/types';
import { Profile } from './profile';
import { fireEvent } from '@testing-library/dom';
const userParamsStub = {
id: '1',
@@ -49,11 +50,12 @@ describe('<Profile />', () => {
expect(queryByRole('heading', { name: /my comments/i })).not.toBeInTheDocument();
expect(queryByRole('heading', { name: /comments/i })).not.toBeInTheDocument();
expect(queryByTestId('comments-counter')).not.toBeInTheDocument();
expect(queryByRole('button', { name: /load more/i })).not.toBeInTheDocument();
});
it('should render error', async () => {
jest.spyOn(pq, 'parseQuery').mockImplementation(() => ({ ...userParamsStub }));
jest.spyOn(api, 'getUserComments').mockImplementation(async () => {
jest.spyOn(api, 'getUserComments').mockImplementation(() => {
throw new Error('error');
});
const { queryByLabelText, queryByRole, findByRole, queryByTestId } = render(<Profile />);
@@ -63,6 +65,7 @@ describe('<Profile />', () => {
expect(queryByRole('heading', { name: /my comments/i })).not.toBeInTheDocument();
expect(queryByRole('heading', { name: /comments/i })).not.toBeInTheDocument();
expect(queryByTestId('comments-counter')).not.toBeInTheDocument();
expect(queryByRole('button', { name: /load more/i })).not.toBeInTheDocument();
});
it('should render user without comments', async () => {
@@ -73,14 +76,15 @@ describe('<Profile />', () => {
const { findByText, queryByLabelText, queryByRole, queryByTestId } = render(<Profile />);
expect(getUserComments).toHaveBeenCalledWith('1');
expect(getUserComments).toHaveBeenCalledWith('1', { limit: 10, skip: 0 });
expect(await findByText("Don't have comments yet")).toBeInTheDocument();
expect(queryByTestId('comments-counter')).not.toBeInTheDocument();
expect(queryByLabelText('Loading...')).not.toBeInTheDocument();
expect(queryByRole('button', { name: /retry/i })).not.toBeInTheDocument();
expect(queryByRole('button', { name: /load more/i })).not.toBeInTheDocument();
});
it('should render user with comments', async () => {
it('should render user with comments without load more button', async () => {
jest.spyOn(pq, 'parseQuery').mockImplementation(() => userParamsStub);
jest
.spyOn(api, 'getUserComments')
@@ -92,32 +96,63 @@ describe('<Profile />', () => {
expect(queryByTestId('comments-counter')).toHaveTextContent(commentsStub.length.toString());
expect(queryByLabelText('Loading...')).not.toBeInTheDocument();
expect(queryByRole('button', { name: /retry/i })).not.toBeInTheDocument();
expect(queryByRole('button', { name: /load more/i })).not.toBeInTheDocument();
});
it('should render user with comments with load more button', async () => {
const comments = new Array(15).fill(commentStub);
jest.spyOn(pq, 'parseQuery').mockImplementation(() => userParamsStub);
jest.spyOn(api, 'getUserComments').mockImplementation(async () => ({ comments, count: comments.length }));
const { findByText, queryByLabelText, queryByRole, queryByTestId } = render(<Profile />);
expect(await findByText('Comments')).toBeInTheDocument();
expect(queryByTestId('comments-counter')).toHaveTextContent(comments.length.toString());
expect(queryByLabelText('Loading...')).not.toBeInTheDocument();
expect(queryByRole('button', { name: /retry/i })).not.toBeInTheDocument();
expect(queryByRole('button', { name: /load more/i })).toBeInTheDocument();
});
it('should render current user without comments', async () => {
jest.spyOn(pq, 'parseQuery').mockImplementation(() => ({ ...userParamsStub, current: '1' }));
jest.spyOn(api, 'getUserComments').mockImplementation(async () => ({ comments: [], count: 0 }));
const { getByText, getByTitle, findByText, queryByTestId } = render(<Profile />);
const { getByText, getByTitle, findByText, queryByTestId, queryByRole } = render(<Profile />);
expect(getByTitle('Sign Out')).toBeInTheDocument();
expect(getByText('Request my data removal')).toBeInTheDocument();
expect(await findByText("Don't have comments yet")).toBeInTheDocument();
expect(queryByTestId('comments-counter')).not.toBeInTheDocument();
expect(queryByRole('button', { name: /load more/i })).not.toBeInTheDocument();
});
it('should render current user with comments', async () => {
it('should render current user with comments without load more button', async () => {
jest.spyOn(pq, 'parseQuery').mockImplementation(() => ({ ...userParamsStub, current: '1' }));
jest
.spyOn(api, 'getUserComments')
.mockImplementation(async () => ({ comments: commentsStub, count: commentsStub.length }));
const { findByText, queryByTitle, queryByText, queryByTestId } = render(<Profile />);
const { findByText, queryByTitle, queryByText, queryByTestId, queryByRole } = render(<Profile />);
expect(await findByText('My comments')).toBeInTheDocument();
expect(queryByTestId('comments-counter')).toHaveTextContent(commentsStub.length.toString());
expect(queryByTitle('Sign Out')).toBeInTheDocument();
expect(queryByText('Request my data removal')).toBeInTheDocument();
expect(queryByRole('button', { name: /load more/i })).not.toBeInTheDocument();
});
it('should render current user with comments with load more button', async () => {
const comments = Array(15).fill(commentStub);
jest.spyOn(pq, 'parseQuery').mockImplementation(() => ({ ...userParamsStub, current: '1' }));
jest.spyOn(api, 'getUserComments').mockImplementation(async () => ({ comments, count: comments.length }));
const { findByText, queryByTitle, queryByText, queryByTestId, queryByRole } = render(<Profile />);
expect(await findByText('My comments')).toBeInTheDocument();
expect(queryByTestId('comments-counter')).toHaveTextContent(comments.length.toString());
expect(queryByTitle('Sign Out')).toBeInTheDocument();
expect(queryByText('Request my data removal')).toBeInTheDocument();
expect(queryByRole('button', { name: /load more/i })).toBeInTheDocument();
});
it('should render user without footer', async () => {
@@ -130,4 +165,15 @@ describe('<Profile />', () => {
expect(container.querySelector('profile-footer')).not.toBeInTheDocument();
});
it('load more button should dissapear if there no more comments to fetch', async () => {
const comments = Array(20).fill(commentStub);
jest.spyOn(api, 'getUserComments').mockImplementation(async () => ({ comments, count: comments.length }));
const { findByRole, queryByRole } = render(<Profile />);
expect(await findByRole('button', { name: /load more/i })).toBeInTheDocument();
fireEvent.click(await findByRole('button', { name: /load more/i }));
expect(queryByRole('button', { name: /load more/i })).not.toBeInTheDocument();
});
});
+46 -7
View File
@@ -1,6 +1,6 @@
import clsx from 'clsx';
import { h, Fragment } from 'preact';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useCallback, useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { useIntl, FormattedMessage } from 'react-intl';
import { getUserComments } from 'common/api';
@@ -32,21 +32,32 @@ async function signout() {
export function Profile() {
const intl = useIntl();
const rootRef = useRef<HTMLDivElement>(null);
const commentsLimit = useMemo(() => 10, []);
const user = useMemo(() => parseQuery(), []);
const [isCommentsLoading, setIsCommentsLoading] = useState(false);
const [error, setError] = useState(false);
const [comments, setComments] = useState<CommentType[] | null>(null);
const [commentsAmount, setCommentsAmount] = useState<number | null>(null);
const [commentsSkipCounts, setCommentsSkipCounts] = useState<number>(0);
const [isSigningOut, setSigningOut] = useState(false);
const isLoadMoreVisible = commentsAmount && commentsAmount > commentsSkipCounts + commentsLimit;
async function fetchUserComments(userId: string) {
const fetchUserComments = useCallback(
async (skip: number = 0) => {
const { comments, count } = await getUserComments(user.id, { skip, limit: commentsLimit });
return { comments, count };
},
[user.id, commentsLimit]
);
const fetchUserCommentsOnMount = useCallback(async () => {
setIsCommentsLoading(true);
setError(false);
setComments(null);
setCommentsAmount(null);
try {
const { comments, count } = await getUserComments(userId);
const { comments, count } = await fetchUserComments();
setComments(comments);
setCommentsAmount(count);
@@ -55,7 +66,23 @@ export function Profile() {
} finally {
setIsCommentsLoading(false);
}
}
}, [fetchUserComments]);
const fetchMoreUserComments = useCallback(
async (skipCounts: number) => {
setError(false);
try {
const { comments: nextComments, count } = await fetchUserComments(skipCounts);
setComments([...(comments || []), ...nextComments]);
setCommentsAmount(count);
} catch (err) {
setError(true);
}
},
[comments, fetchUserComments]
);
function handleClickClose() {
const rootElement = rootRef.current;
@@ -79,13 +106,20 @@ export function Profile() {
await signout();
}
function handleLoadMore() {
const nextSkipCounts = commentsSkipCounts + commentsLimit;
setCommentsSkipCounts(nextSkipCounts);
fetchMoreUserComments(nextSkipCounts);
}
function handleClickRetryCommentsRequest() {
fetchUserComments(user.id);
fetchMoreUserComments(commentsSkipCounts);
}
useEffect(() => {
fetchUserComments(user.id);
}, [user.id]);
fetchUserCommentsOnMount();
}, [fetchUserCommentsOnMount]);
useEffect(() => {
const styles = { height: '100%', padding: 0 };
@@ -145,6 +179,11 @@ export function Profile() {
theme={(user.theme as Theme) || 'light'}
/>
))}
{isLoadMoreVisible && (
<Button kind="link" size="sm" onClick={handleLoadMore}>
<FormattedMessage id="user.load-more" defaultMessage="Load more" />
</Button>
)}
</>
) : (
<p className={clsx('profile-emptyState', styles.emptyState)}>
+1
View File
@@ -159,6 +159,7 @@
"toolbar.unordered-list": "Ненумараваны спіс",
"user.my-comments": "My comments",
"user.comments": "Comments",
"user.load-more": "Load more",
"vote.anonymous": "Ананімныя карыстальнікі не могуць галасаваць",
"vote.deleted": "Нельга галасаваць за выдалены каментар",
"vote.guest": "Увайдзіце ў сістэму, каб галасаваць",
+1
View File
@@ -159,6 +159,7 @@
"toolbar.unordered-list": "Добави обозначен списък",
"user.my-comments": "My comments",
"user.comments": "Comments",
"user.load-more": "Load more",
"vote.anonymous": "Анонимни потребители не могат да гласуват",
"vote.deleted": "Не може да гласъвате за изтрит коментарт",
"vote.guest": "Влезте за да гласувате",
+1
View File
@@ -159,6 +159,7 @@
"toolbar.unordered-list": "Adicionar lista com marcadores",
"user.my-comments": "My comments",
"user.comments": "Comments",
"user.load-more": "Load more",
"vote.anonymous": "Usuários anônimos não podem votar",
"vote.deleted": "Não posso votar em um comentário excluído",
"vote.guest": "Faça login para votar",
+1
View File
@@ -159,6 +159,7 @@
"toolbar.unordered-list": "Aufzählungsliste einfügen",
"user.my-comments": "My comments",
"user.comments": "Comments",
"user.load-more": "Load more",
"vote.anonymous": "Anonyme Benutzer können leider nicht abstimmen",
"vote.deleted": "Sie können nicht für einen gelöschten Kommentar abstimmen",
"vote.guest": "Melden Sie sich an, um abzustimmen",
+1
View File
@@ -159,6 +159,7 @@
"toolbar.unordered-list": "Add a bulleted list",
"user.my-comments": "My comments",
"user.comments": "Comments",
"user.load-more": "Load more",
"vote.anonymous": "Anonymous users can't vote",
"vote.deleted": "Can't vote for deleted comment",
"vote.guest": "Sign in to vote",
+1
View File
@@ -159,6 +159,7 @@
"toolbar.unordered-list": "Agrega una lista sin numerar",
"user.my-comments": "My comments",
"user.comments": "Comments",
"user.load-more": "Load more",
"vote.anonymous": "Los usuarios anónimos no puede votar",
"vote.deleted": "No se puede votar un comentario eliminado",
"vote.guest": "Accede para votar",
+1
View File
@@ -159,6 +159,7 @@
"toolbar.unordered-list": "Järjestämätön luettelo",
"user.my-comments": "My comments",
"user.comments": "Comments",
"user.load-more": "Load more",
"vote.anonymous": "Anonyymit käyttäjät eivät voi äänestää",
"vote.deleted": "Poistettua kommenttia ei voi äänestää",
"vote.guest": "Kirjaudu sisään äänestääksesi",
+1
View File
@@ -159,6 +159,7 @@
"toolbar.unordered-list": "Ajouter une liste à puces",
"user.my-comments": "My comments",
"user.comments": "Comments",
"user.load-more": "Load more",
"vote.anonymous": "Les utilisateurs anonymes ne peuvent pas voter",
"vote.deleted": "Vous ne pouvez pas voter pour un commentaire supprimé",
"vote.guest": "Connectez-vous pour voter",
+1
View File
@@ -159,6 +159,7 @@
"toolbar.unordered-list": "記号付きリストの追加",
"user.my-comments": "My comments",
"user.comments": "Comments",
"user.load-more": "Load more",
"vote.anonymous": "匿名ユーザーは投稿できません",
"vote.deleted": "削除済みコメントには投票できません",
"vote.guest": "投票するにはサインインしてください",
+1
View File
@@ -159,6 +159,7 @@
"toolbar.unordered-list": "글머리 기호가 지정된 목록 추가",
"user.my-comments": "My comments",
"user.comments": "Comments",
"user.load-more": "Load more",
"vote.anonymous": "익명의 사용자는 투표할 수 없습니다",
"vote.deleted": "삭제된 댓글에는 투표할 수 없습니다",
"vote.guest": "투표하려면 로그인하세요",
+1
View File
@@ -159,6 +159,7 @@
"toolbar.unordered-list": "Dodaj listę punktowaną",
"user.my-comments": "Moje komentarze",
"user.comments": "Komentarze",
"user.load-more": "Load more",
"vote.anonymous": "Anonimowi użytkownicy nie mogą głosować",
"vote.deleted": "Nie można głosować na usunięte komentarze",
"vote.guest": "Zaloguj się żeby głosować",
+1
View File
@@ -159,6 +159,7 @@
"toolbar.unordered-list": "Добавить маркированный список",
"user.my-comments": "Мои комментарии",
"user.comments": "Комментарии",
"user.load-more": "Load more",
"vote.anonymous": "Анонимные пользователи не могут голосовать",
"vote.deleted": "Нельзя голосовать за удаленный комментарий",
"vote.guest": "Войдите, чтобы проголосовать",
+1
View File
@@ -159,6 +159,7 @@
"toolbar.unordered-list": "Liste ekle",
"user.my-comments": "My comments",
"user.comments": "Comments",
"user.load-more": "Load more",
"vote.anonymous": "Anonim kullanıcılar oy kullanamaz",
"vote.deleted": "Silinmiş yorum oylanamaz",
"vote.guest": "Oy vermek için giriş yapın",
+1
View File
@@ -159,6 +159,7 @@
"toolbar.unordered-list": "Неупорядкованний список",
"user.my-comments": "Мої коментарі",
"user.comments": "Коментарі",
"user.load-more": "Load more",
"vote.anonymous": "Не можна голосувати анонімному користувачу",
"vote.deleted": "Не можна голосувати за видалений коментар",
"vote.guest": "Увійдіть в систему для голосування",
+1
View File
@@ -159,6 +159,7 @@
"toolbar.unordered-list": "Thêm danh sách",
"user.my-comments": "My comments",
"user.comments": "Comments",
"user.load-more": "Load more",
"vote.anonymous": "Người dùng ẩn danh không thể vote",
"vote.deleted": "Không thể vote bình luận đã xoá",
"vote.guest": "Đăng nhập để vote",
+1
View File
@@ -159,6 +159,7 @@
"toolbar.unordered-list": "添加项目符号列表",
"user.my-comments": "My comments",
"user.comments": "Comments",
"user.load-more": "Load more",
"vote.anonymous": "匿名用户无法投票",
"vote.deleted": "无法为已删除的评论投票",
"vote.guest": "登录以投票",