Skip to content

Commit

Permalink
DAO-743 Added pagination
Browse files Browse the repository at this point in the history
  • Loading branch information
Freshenext committed Oct 28, 2024
1 parent 3574952 commit 24eaddd
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 37 deletions.
26 changes: 6 additions & 20 deletions src/app/treasury/HoldersSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import { useFetchTokenHolders } from '@/app/treasury/hooks/useFetchTokenHolders'
import { formatBalanceToHuman } from '@/app/user/Balances/balanceUtils'
import Image from 'next/image'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { Button } from '@/components/Button'
import { ReactNode } from 'react'

interface HolderColumnProps {
address: string
Expand All @@ -31,31 +29,19 @@ const HolderColumn = ({ address, rns }: HolderColumnProps) => {

export const HoldersSection = () => {
// Fetch st rif holders
const { data, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage } =
useFetchTokenHolders(STRIF_ADDRESS)
const { currentResults, paginationElement, isLoading } = useFetchTokenHolders(STRIF_ADDRESS)

const holders = data?.pages?.reduce<{ holder: ReactNode; quantity: string }[]>((prev, currentPage) => {
currentPage.items.forEach(({ address, value }) => {
prev.push({
holder: <HolderColumn address={address.hash} rns={address.ens_domain_name} />,
quantity: `${formatBalanceToHuman(value)} stRIF`,
})
})
return prev
}, [])
const holders = currentResults.map(({ address, value }) => ({
holder: <HolderColumn address={address.hash} rns={address.ens_domain_name} />,
quantity: `${formatBalanceToHuman(value)} stRIF`,
}))

return (
<div>
<HeaderTitle className="mb-4">Holders</HeaderTitle>
{holders && holders?.length > 0 && <Table data={holders} />}
{paginationElement}
{isLoading && <LoadingSpinner />}
{isFetchingNextPage ? (
<LoadingSpinner className="w-52" />
) : hasNextPage ? (
<Button variant="secondary" onClick={() => fetchNextPage()}>
Load more
</Button>
) : null}
</div>
)
}
21 changes: 13 additions & 8 deletions src/app/treasury/hooks/useFetchTokenHolders.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { fetchTokenHoldersOfAddress } from '@/app/user/Balances/actions'
import { useInfiniteQuery } from '@tanstack/react-query'
import { Address } from 'viem'
import { NextPageParams, ServerResponseV2, TokenHoldersResponse } from '@/app/user/Balances/types'
import { NextPageParams } from '@/app/user/Balances/types'
import { usePagination } from '@/shared/hooks/usePagination'
import { usePaginationUi } from '@/shared/hooks/usePaginationUi'

export const useFetchTokenHolders = (address: Address) => {
return useInfiniteQuery<ServerResponseV2<TokenHoldersResponse>, Error>({
queryKey: [`tokenHolders${address}`, address],
const query = usePagination({
queryKey: ['tokenHolders'],
queryFn: ({ pageParam }) => fetchTokenHoldersOfAddress(address, pageParam as NextPageParams),
getNextPageParam: lastPage => lastPage.next_page_params,
initialPageParam: null,
queryFn: async ({ pageParam }) => fetchTokenHoldersOfAddress(address, pageParam as NextPageParams),
getNextPageParam: lastPage => {
return lastPage.next_page_params
},
resultsPerTablePage: 10,
hasMorePagesProperty: 'next_page_params',
})

const ui = usePaginationUi(query)

return { ...query, ...ui }
}
16 changes: 10 additions & 6 deletions src/app/user/Balances/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,13 @@ export async function fetchIpfsUri(
export const fetchNftInfo = (address: string) =>
axiosInstance.get(getNftInfo.replace('{{nftAddress}}', address))

export const fetchTokenHoldersOfAddress = (address: string, nextParams: NextPageParams | null) =>
axiosInstance
.get<
ServerResponseV2<TokenHoldersResponse>
>(getTokenHoldersOfAddress.replace('{{address}}', address), { params: nextParams })
.then(res => res.data)
export const fetchTokenHoldersOfAddress = async (address: string, nextParams: NextPageParams | null) => {
const { data } = await axiosInstance.get<ServerResponseV2<TokenHoldersResponse>>(
getTokenHoldersOfAddress.replace('{{address}}', address),
{ params: nextParams },
)
if (data.error) {
throw new Error(data.error)
}
return data
}
1 change: 1 addition & 0 deletions src/app/user/Balances/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,5 @@ export interface NextPageParams {
export interface ServerResponseV2<T> {
items: T[]
next_page_params: NextPageParams | null
error?: string
}
10 changes: 8 additions & 2 deletions src/components/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import { FaSpinner } from 'react-icons/fa6'
import { Span } from '../Typography'
import { DivWithGradient } from '@/components/Button/DivWithGradient'

export const BUTTON_DEFAULT_CLASSES = 'px-[24px] py-[12px] flex gap-x-1 items-center relative'
export const BUTTON_DEFAULT_CLASSES = 'px-[24px] py-[12px] flex gap-x-1 items-center relative rounded-[6px]'

const DEFAULT_PAGINATION_CLASSES = 'w-[32px] h-[32px] p-0'

const DEFAULT_PAGINATION_ACTIVE_CLASSES = [DEFAULT_PAGINATION_CLASSES, 'bg-primary text-black'].join(' ')

interface Props {
children: string
children: ReactNode
onClick?: (e: MouseEvent<HTMLButtonElement>) => void
startIcon?: ReactNode
variant?: ButtonVariants
Expand Down Expand Up @@ -55,6 +59,8 @@ export const Button: FC<Props> = ({
'justify-start': !centerContent,
'justify-center': centerContent,
'cursor-not-allowed': disabled,
[DEFAULT_PAGINATION_CLASSES]: variant === 'pagination',
[DEFAULT_PAGINATION_ACTIVE_CLASSES]: variant === 'pagination-active',
[className]: true,
})

Expand Down
2 changes: 2 additions & 0 deletions src/components/Button/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ export type ButtonVariants =
| 'outlined'
| 'white'
| 'sidebar-active'
| 'pagination'
| 'pagination-active'
2 changes: 1 addition & 1 deletion src/lib/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ export const fetchProposalsCreatedByGovernorAddress =
export const getNftInfo =
process.env.NEXT_PUBLIC_API_RWS_NFT_INFO || `/nfts/{{nftAddress}}?chainId=${CHAIN_ID}`

export const getTokenHoldersOfAddress = '/address/{{address}}/holders'
export const getTokenHoldersOfAddress = `/address/{{address}}/holders?chainId=${CHAIN_ID}`
110 changes: 110 additions & 0 deletions src/shared/hooks/usePagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { InfiniteData, useInfiniteQuery, UseInfiniteQueryResult } from '@tanstack/react-query'
import { useState, useCallback, useMemo, useEffect } from 'react'

interface PaginatedResponse<T> {
items: T[]
[key: string]: any
}

interface UsePaginatedQueryOptions<T> {
queryKey: string[]
queryFn: (pageParam: any) => Promise<PaginatedResponse<T>>
getNextPageParam: (lastPage: PaginatedResponse<T>) => any
initialPageParam: any
resultsPerTablePage: number
hasMorePagesProperty: keyof PaginatedResponse<T>
}

export interface UsePaginatedQueryResult<T>
extends Omit<UseInfiniteQueryResult<InfiniteData<PaginatedResponse<T>>>, 'data'> {
currentResults: T[]
tablePage: number
totalPages: number
nextTablePage: () => void
previousTablePage: () => void
isFirstFetch: boolean
hasMorePages: boolean
}

export function usePagination<T>({
queryKey,
queryFn,
getNextPageParam,
initialPageParam,
resultsPerTablePage,
hasMorePagesProperty,
}: UsePaginatedQueryOptions<T>): UsePaginatedQueryResult<T> {
const [tablePage, setTablePage] = useState(0)
const [isFirstFetch, setIsFirstFetch] = useState(true)

const { data, fetchNextPage, hasNextPage, isFetchingNextPage, ...restQueryResult } = useInfiniteQuery<
PaginatedResponse<T>,
Error
>({
queryKey,
queryFn,
getNextPageParam,
initialPageParam,
refetchOnWindowFocus: false,
})

const allItems = useMemo(() => {
return data?.pages.flatMap(page => page.items) || []
}, [data])

const totalItems = allItems.length
const totalPages = Math.ceil(totalItems / resultsPerTablePage)

const currentResults = useMemo(() => {
const start = tablePage * resultsPerTablePage
const end = start + resultsPerTablePage
return allItems.slice(start, end)
}, [allItems, tablePage, resultsPerTablePage])

const nextTablePage = useCallback(() => {
if (tablePage < totalPages - 1) {
setTablePage(prev => prev + 1)
}
}, [tablePage, totalPages])

const previousTablePage = useCallback(() => {
if (tablePage > 0) {
setTablePage(prev => prev - 1)
}
}, [tablePage])

const hasMorePages = useMemo(() => {
if (!data || data.pages.length === 0) return true
const lastPage = data.pages[data.pages.length - 1]
return lastPage[hasMorePagesProperty] !== null
}, [data, hasMorePagesProperty])

useEffect(() => {
// Check if we need to fetch the next page
const currentItemIndex = (tablePage + 1) * resultsPerTablePage
if (currentItemIndex >= totalItems && hasMorePages && !isFetchingNextPage) {
fetchNextPage()
}
}, [tablePage, resultsPerTablePage, totalItems, hasMorePages, isFetchingNextPage, fetchNextPage])

useEffect(() => {
// Update isFirstFetch
if (isFirstFetch && data && data.pages.length > 0) {
setIsFirstFetch(false)
}
}, [data, isFirstFetch])

return {
currentResults,
totalPages,
tablePage,
nextTablePage,
previousTablePage,
isFirstFetch,
hasMorePages,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
...restQueryResult,
}
}
78 changes: 78 additions & 0 deletions src/shared/hooks/usePaginationUi.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { ReactNode, useMemo } from 'react'
import { UsePaginatedQueryResult } from '@/shared/hooks/usePagination'
import { Button } from '@/components/Button'
import { ChevronLeft, ChevronRight } from 'lucide-react'

interface UseSimplePaginationResult<T> {
paginationElement: ReactNode
currentResults: T[]
}

export function usePaginationUi<T>(
paginationResult: UsePaginatedQueryResult<T>,
): UseSimplePaginationResult<T> {
const { currentResults, totalPages, tablePage, nextTablePage, previousTablePage, isLoading } =
paginationResult

const paginationElement = useMemo(() => {
const getPageNumbers = () => {
const maxVisiblePages = 5
const pages = []
const start = Math.max(0, Math.min(tablePage - 2, totalPages - maxVisiblePages))
const end = Math.min(start + maxVisiblePages, totalPages)

for (let i = start; i < end; i++) {
pages.push(i)
}

return pages
}

return (
<div className="flex gap-x-1 items-center justify-center">
<PaginationButton
text={<ChevronLeft />}
onClick={previousTablePage}
disabled={tablePage === 0 || isLoading}
/>
{getPageNumbers().map(pageNumber => (
<PaginationButton
key={pageNumber}
onClick={() =>
tablePage !== pageNumber && (pageNumber > tablePage ? nextTablePage() : previousTablePage())
}
disabled={isLoading}
text={pageNumber + 1}
isActive={pageNumber === tablePage}
/>
))}
<PaginationButton
text={<ChevronRight />}
onClick={nextTablePage}
disabled={tablePage === totalPages - 1 || isLoading}
/>
</div>
)
}, [tablePage, totalPages, isLoading, nextTablePage, previousTablePage])

return {
paginationElement,
currentResults,
}
}

const PaginationButton = ({
text,
onClick,
disabled,
isActive,
}: {
text: ReactNode
onClick: () => void
disabled?: boolean
isActive?: boolean
}) => (
<Button onClick={onClick} disabled={disabled} variant={isActive ? 'pagination-active' : 'pagination'}>
{text}
</Button>
)

0 comments on commit 24eaddd

Please sign in to comment.