Add Load More button to profile sidebar
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
@@ -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": "Увайдзіце ў сістэму, каб галасаваць",
|
||||
|
||||
@@ -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": "Влезте за да гласувате",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "投票するにはサインインしてください",
|
||||
|
||||
@@ -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": "투표하려면 로그인하세요",
|
||||
|
||||
@@ -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ć",
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
"toolbar.unordered-list": "Добавить маркированный список",
|
||||
"user.my-comments": "Мои комментарии",
|
||||
"user.comments": "Комментарии",
|
||||
"user.load-more": "Load more",
|
||||
"vote.anonymous": "Анонимные пользователи не могут голосовать",
|
||||
"vote.deleted": "Нельзя голосовать за удаленный комментарий",
|
||||
"vote.guest": "Войдите, чтобы проголосовать",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
"toolbar.unordered-list": "Неупорядкованний список",
|
||||
"user.my-comments": "Мої коментарі",
|
||||
"user.comments": "Коментарі",
|
||||
"user.load-more": "Load more",
|
||||
"vote.anonymous": "Не можна голосувати анонімному користувачу",
|
||||
"vote.deleted": "Не можна голосувати за видалений коментар",
|
||||
"vote.guest": "Увійдіть в систему для голосування",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "登录以投票",
|
||||
|
||||
Reference in New Issue
Block a user