add pagination to followers tab

This commit is contained in:
hzrd149 2023-02-07 17:04:18 -06:00
parent aa5febcdfc
commit 14851eaafe
5 changed files with 187 additions and 3 deletions

View File

@ -80,6 +80,18 @@ export const ArrowDownSIcon = createIcon({
defaultProps,
});
export const ArrowLeftSIcon = createIcon({
displayName: "arrow-left-s",
d: "M10.828 12l4.95 4.95-1.414 1.414L8 12l6.364-6.364 1.414 1.414z",
defaultProps,
});
export const ArrowRightSIcon = createIcon({
displayName: "arrow-right-s",
d: "M13.172 12l-4.95-4.95 1.414-1.414L16 12l-6.364 6.364-1.414-1.414z",
defaultProps,
});
export const LinkItem = createIcon({
displayName: "ri-link",
d: "M18.364 15.536L16.95 14.12l1.414-1.414a5 5 0 1 0-7.071-7.071L9.879 7.05 8.464 5.636 9.88 4.222a7 7 0 0 1 9.9 9.9l-1.415 1.414zm-2.828 2.828l-1.415 1.414a7 7 0 0 1-9.9-9.9l1.415-1.414L7.05 9.88l-1.414 1.414a5 5 0 1 0 7.071 7.071l1.414-1.414 1.415 1.414zm-.708-10.607l1.415 1.415-7.071 7.07-1.415-1.414 7.071-7.07z",

View File

@ -0,0 +1,137 @@
import { Button, ButtonGroup, ButtonProps, IconButton } from "@chakra-ui/react";
import { useMemo } from "react";
import { usePaginatedList } from "../hooks/use-paginated-list";
import { ArrowLeftSIcon, ArrowRightSIcon } from "./icons";
const range = (start: number, end: number) => {
let length = end - start + 1;
return Array.from({ length }, (_, idx) => idx + start);
};
export type PaginationControlsProps = ReturnType<typeof usePaginatedList> & {
buttonSize?: ButtonProps["size"];
siblingCount?: number;
};
export const PaginationControls = ({
pageCount,
currentPage,
setPage,
next,
previous,
buttonSize,
siblingCount = 1,
}: PaginationControlsProps) => {
const renderPageButton = (pageNumber: number) => (
<Button
key={pageNumber}
variant={pageNumber - 1 === currentPage ? "solid" : "link"}
title={`page ${pageNumber}`}
size={buttonSize}
onClick={() => setPage(pageNumber - 1)}
>
{pageNumber}
</Button>
);
// copied from https://www.freecodecamp.org/news/build-a-custom-pagination-component-in-react/
const renderPageButtons = () => {
// Pages count is determined as siblingCount + firstPage + lastPage + currentPage + 2*DOTS
const totalPageNumbers = siblingCount + 5;
/*
Case 1:
If the number of pages is less than the page numbers we want to show in our
paginationComponent, we return the range [1..pageCount]
*/
if (totalPageNumbers >= pageCount) {
return range(1, pageCount).map(renderPageButton);
}
/*
Calculate left and right sibling index and make sure they are within range 1 and pageCount
*/
const leftSiblingIndex = Math.max(currentPage + 1 - siblingCount, 1);
const rightSiblingIndex = Math.min(currentPage + 1 + siblingCount, pageCount);
/*
We do not show dots just when there is just one page number to be inserted between the extremes of sibling and the page limits i.e 1 and pageCount. Hence we are using leftSiblingIndex > 2 and rightSiblingIndex < pageCount - 2
*/
const shouldShowLeftDots = leftSiblingIndex > 3;
const shouldShowRightDots = rightSiblingIndex < pageCount - 2;
const firstPageIndex = 1;
const lastPageIndex = pageCount;
/*
Case 2: No left dots to show, but rights dots to be shown
*/
if (!shouldShowLeftDots && shouldShowRightDots) {
let leftItemCount = 3 + 2 * siblingCount;
let leftRange = range(1, leftItemCount);
return [
...leftRange.map(renderPageButton),
<Button key="left-dots" size={buttonSize} variant="link">
...
</Button>,
renderPageButton(lastPageIndex),
];
}
/*
Case 3: No right dots to show, but left dots to be shown
*/
if (shouldShowLeftDots && !shouldShowRightDots) {
let rightItemCount = 3 + 2 * siblingCount;
let rightRange = range(pageCount - rightItemCount + 1, pageCount);
return [
renderPageButton(firstPageIndex),
<Button key="right-dots" size={buttonSize} variant="link">
...
</Button>,
...rightRange.map(renderPageButton),
];
}
/*
Case 4: Both left and right dots to be shown
*/
if (shouldShowLeftDots && shouldShowRightDots) {
let middleRange = range(leftSiblingIndex, rightSiblingIndex);
return [
renderPageButton(firstPageIndex),
<Button key="left-dots" size={buttonSize} variant="link">
...
</Button>,
...middleRange.map(renderPageButton),
<Button key="right-dots" size={buttonSize} variant="link">
...
</Button>,
renderPageButton(lastPageIndex),
];
}
};
return (
<ButtonGroup>
<IconButton
icon={<ArrowLeftSIcon />}
aria-label="previous"
title="previous"
size={buttonSize}
onClick={previous}
isDisabled={currentPage === 0}
/>
{renderPageButtons()}
<IconButton
icon={<ArrowRightSIcon />}
aria-label="next"
title="next"
size={buttonSize}
onClick={next}
isDisabled={currentPage === pageCount - 1}
/>
</ButtonGroup>
);
};

View File

@ -0,0 +1,26 @@
import { useCallback, useMemo, useState } from "react";
type Options = {
pageSize?: number;
};
export function usePaginatedList<T extends unknown>(list: T[], opts?: Options) {
const [currentPage, setPage] = useState(0);
const pageSize = opts?.pageSize ?? 20;
const pageCount = Math.ceil(list.length / pageSize);
const next = useCallback(() => setPage((v) => Math.min(v + 1, pageCount - 1)), [setPage, pageCount]);
const previous = useCallback(() => setPage((v) => Math.max(v - 1, 0)), [setPage]);
const pageItems = useMemo(
() => list.slice(pageSize * currentPage, pageSize * currentPage + pageSize),
[list, currentPage, pageSize]
);
return {
currentPage,
setPage,
pageItems,
pageCount,
previous,
next,
};
}

View File

@ -3,12 +3,16 @@ import { Flex, FormControl, FormLabel, Grid, SkeletonText, Switch, useDisclosure
import { UserCard } from "./components/user-card";
import { useUserFollowers } from "../../hooks/use-user-followers";
import { useOutletContext } from "react-router-dom";
import { usePaginatedList } from "../../hooks/use-paginated-list";
import { PaginationControls } from "../../components/pagination-controls";
const UserFollowersTab = () => {
const { pubkey } = useOutletContext() as { pubkey: string };
const { isOpen, onToggle } = useDisclosure();
const followers = useUserFollowers(pubkey, [], isOpen);
const pagination = usePaginatedList(followers ?? [], { pageSize: 3 * 10 });
return (
<Flex gap="2" direction="column">
<FormControl display="flex" alignItems="center">
@ -20,10 +24,11 @@ const UserFollowersTab = () => {
{followers ? (
<>
<Grid templateColumns={{ base: "1fr", xl: "repeat(2, 1fr)", "2xl": "repeat(3, 1fr)" }} gap="2">
{Array.from(followers).map((pubkey) => (
{pagination.pageItems.map((pubkey) => (
<UserCard key={pubkey} pubkey={pubkey} />
))}
</Grid>
<PaginationControls {...pagination} buttonSize="sm" />
</>
) : (
<SkeletonText />

View File

@ -4,21 +4,25 @@ import { Flex, Grid, SkeletonText, Text } from "@chakra-ui/react";
import { UserCard } from "./components/user-card";
import { useUserContacts } from "../../hooks/use-user-contacts";
import { useOutletContext } from "react-router-dom";
import { usePaginatedList } from "../../hooks/use-paginated-list";
import { PaginationControls } from "../../components/pagination-controls";
const UserFollowingTab = () => {
const { pubkey } = useOutletContext() as { pubkey: string };
const contacts = useUserContacts(pubkey, [], true);
const pagination = usePaginatedList(contacts?.contacts ?? [], { pageSize: 3 * 10 });
return (
<Flex gap="2" direction="column">
{contacts ? (
<>
<Grid templateColumns={{ base: "1fr", xl: "repeat(2, 1fr)", "2xl": "repeat(3, 1fr)" }} gap="2">
{contacts.contacts.map((pubkey, i) => (
{pagination.pageItems.map((pubkey, i) => (
<UserCard key={pubkey + i} pubkey={pubkey} />
))}
</Grid>
<Text>{`Updated ${moment(contacts?.created_at * 1000).fromNow()}`}</Text>
<PaginationControls {...pagination} buttonSize="sm" />
</>
) : (
<SkeletonText />