mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 21:31:43 +01:00
add pagination to followers tab
This commit is contained in:
parent
aa5febcdfc
commit
14851eaafe
@ -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",
|
||||
|
137
src/components/pagination-controls.tsx
Normal file
137
src/components/pagination-controls.tsx
Normal 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>
|
||||
);
|
||||
};
|
26
src/hooks/use-paginated-list.ts
Normal file
26
src/hooks/use-paginated-list.ts
Normal 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,
|
||||
};
|
||||
}
|
@ -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 />
|
||||
|
@ -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 />
|
||||
|
Loading…
x
Reference in New Issue
Block a user