Skip to content

Commit

Permalink
DAO-745 Implemented nft holders table
Browse files Browse the repository at this point in the history
  • Loading branch information
Freshenext committed Oct 28, 2024
1 parent d61b5b0 commit f9c990c
Show file tree
Hide file tree
Showing 7 changed files with 211 additions and 1 deletion.
Binary file added public/images/holders-square.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
153 changes: 153 additions & 0 deletions src/app/communities/NftHoldersSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { Address } from 'viem'
import { useFetchNftHolders } from '@/shared/hooks/useFetchNftHolders'
import { HeaderTitle, Paragraph, Span } from '@/components/Typography'
import { Table } from '@/components/Table'
import { LoadingSpinner } from '@/components/LoadingSpinner'
import { EXPLORER_URL } from '@/lib/constants'
import { RxExternalLink, RxViewGrid } from 'react-icons/rx'
import Image from 'next/image'
import { truncateMiddle } from '@/lib/utils'
import { useState } from 'react'
import { FaTable } from 'react-icons/fa'

interface HolderColumnProps {
address: string
rns?: string
image?: string
}
const HolderColumn = ({ address, rns, image }: HolderColumnProps) => {
return (
<a
href={`${EXPLORER_URL}/address/${address}`}
target="_blank"
className="flex items-center gap-1.5 text-white"
>
<img src={image || '/images/treasury/holders.png'} width={24} height={24} alt="Holders Image" />
<Span className="underline text-left overflow-hidden whitespace-nowrap text-[14px]">
{rns || address}
</Span>
<RxExternalLink size={18} />
</a>
)
}

interface IdNumberColumnProps {
id: string
image?: string
}
const IdNumberColumn = ({ id, image }: IdNumberColumnProps) => {
return (
<div className="flex items-center gap-1.5">
<img src={image || '/images/holders-square.png'} width={24} height={24} alt="Holders Image Square" />
<span className="tracking-widest">#{id}</span>
</div>
)
}

interface HoldersSectionProps {
address: Address
}

const CardHolderParagraph = ({ address }: { address: string }) => (
<a
href={`${EXPLORER_URL}/address/${address}`}
target="_blank"
className="flex gap-1.5 text-white items-center"
>
<Paragraph fontFamily="kk-topo" size="large" className="pt-[6px]">
HOLDER
</Paragraph>
<img src="/images/treasury/holders.png" width={24} height={24} alt="Holders Image" />
<Span className="underline text-left overflow-hidden whitespace-nowrap text-[14px]">
{truncateMiddle(address, 5, 5)}
</Span>
<RxExternalLink size={18} />
</a>
)

interface CardProps {
image: string
id: string
holderAddress: string
}

const Card = ({ image, id, holderAddress }: CardProps) => {
return (
<div className="w-[272px] bg-foreground">
<img src={image} width={272} alt="NFT" />
<div className="px-[8px] py-[16px]">
<Paragraph fontFamily="kk-topo" size="large">
ID# {id}
</Paragraph>
<CardHolderParagraph address={holderAddress} />
</div>
</div>
)
}

interface CardViewProps {
nfts: { image_url: string; id: string; owner: string }[]
}

const CardView = ({ nfts }: CardViewProps) => (
<div className="grid grid-cols-4">
{nfts.map(({ image_url, id, owner }) => (
<Card key={id} image={image_url} id={id} holderAddress={owner} />
))}
</div>
)

type ViewState = 'images' | 'table'

const ViewIconHandler = ({
view,
onChangeView,
}: {
view: ViewState
onChangeView: (view: ViewState) => void
}) => (
<span className="absolute right-0 top-0 flex">
<div
className={`w-[46px] h-[46px] flex items-center justify-center ${view === 'table' ? 'bg-white' : ''}`}
onClick={() => onChangeView('table')}
>
<FaTable size={24} color={view === 'table' ? 'black' : 'white'} />
</div>
<div
className={`w-[46px] h-[46px] flex items-center justify-center ${view === 'images' ? 'bg-white' : ''}`}
onClick={() => onChangeView('images')}
>
<RxViewGrid size={24} color={view === 'images' ? 'black' : 'white'} />
</div>
</span>
)

export const NftHoldersSection = ({ address }: HoldersSectionProps) => {
const { currentResults, paginationElement, isLoading } = useFetchNftHolders(address)

const [view, setView] = useState<ViewState>('images')

const onChangeView = (selectedView: ViewState) => {
setView(selectedView)
}

const holders = currentResults.map(({ owner, ens_domain_name, id, image_url }) => ({
holder: <HolderColumn address={owner} rns={ens_domain_name || ''} image={image_url} />,
'ID Number': <IdNumberColumn id={id} image={image_url} />,
}))

return (
<div className="pl-4 relative">
<HeaderTitle className="mb-[24px]">
Holders
<ViewIconHandler view={view} onChangeView={onChangeView} />
</HeaderTitle>
{view === 'table' && holders && holders?.length > 0 && <Table data={holders} />}
{view === 'images' && currentResults && currentResults?.length > 0 && (
<CardView nfts={currentResults} />
)}
{isLoading && <LoadingSpinner />}
<div className="mt-6">{paginationElement}</div>
</div>
)
}
19 changes: 18 additions & 1 deletion src/app/user/Balances/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ import {
fetchNFTsOwnedByAddressAndNftAddress,
fetchPricesEndpoint,
fetchProposalsCreatedByGovernorAddress,
getNftHolders,
getNftInfo,
getTokenHoldersOfAddress,
} from '@/lib/endpoints'
import { tokenContracts, GovernorAddress } from '@/lib/contracts'
import { NftMeta } from '@/shared/types'
import { ipfsGateways } from '@/config'
import { NextPageParams, ServerResponseV2, TokenHoldersResponse } from '@/app/user/Balances/types'
import {
NextPageParams,
NftHolderItem,
ServerResponseV2,
TokenHoldersResponse,
} from '@/app/user/Balances/types'

export const fetchAddressTokens = (address: string, chainId = 31) =>
axiosInstance
Expand Down Expand Up @@ -126,6 +132,17 @@ export async function fetchIpfsUri(
export const fetchNftInfo = (address: string) =>
axiosInstance.get(getNftInfo.replace('{{nftAddress}}', address))

export const fetchNftHoldersOfAddress = async (address: string, params: NextPageParams | null) => {
const { data } = await axiosInstance.get<ServerResponseV2<NftHolderItem>>(
getNftHolders.replace('{{address}}', address),
{ params },
)
if (data.error) {
throw new Error(data.error)
}
return data
}

export const fetchTokenHoldersOfAddress = async (address: string, nextParams: NextPageParams | null) => {
const { data } = await axiosInstance.get<ServerResponseV2<TokenHoldersResponse>>(
getTokenHoldersOfAddress.replace('{{address}}', address),
Expand Down
16 changes: 16 additions & 0 deletions src/app/user/Balances/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,19 @@ export interface ServerResponseV2<T> {
next_page_params: NextPageParams | null
error?: string
}

export type NftHolderItem = {
owner: string
ens_domain_name: null
id: string
image_url: string
metadata: Metadata
}

export type Metadata = {
creator: string
description: string
external_url: string
image: string
name: string
}
2 changes: 2 additions & 0 deletions src/lib/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ export const getNftInfo =
process.env.NEXT_PUBLIC_API_RWS_NFT_INFO || `/nfts/{{nftAddress}}?chainId=${CHAIN_ID}`

export const getTokenHoldersOfAddress = `/address/{{address}}/holders?chainId=${CHAIN_ID}`

export const getNftHolders = `/nfts/{{address}}/holders?chainId=${CHAIN_ID}`
3 changes: 3 additions & 0 deletions src/pages/communities/nft/[address].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { useAccount } from 'wagmi'
import { useCommunity } from '@/shared/hooks/useCommunity'
import { useStRif } from '@/shared/hooks/useStRIf'
import { CopyButton } from '@/components/CopyButton'
import { NftHoldersSection } from '@/app/communities/NftHoldersSection'

/**
* Name of the local storage variable with information about whether the token was added to the wallet
Expand Down Expand Up @@ -324,6 +325,8 @@ export default function Page() {
</div>
</div>
</div>
{/* Holders list */}
<NftHoldersSection address={nftAddress} />
</MainContainer>
)
}
Expand Down
19 changes: 19 additions & 0 deletions src/shared/hooks/useFetchNftHolders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Address } from 'viem'
import { fetchNftHoldersOfAddress } from '@/app/user/Balances/actions'
import { usePagination } from '@/shared/hooks/usePagination'
import { usePaginationUi } from '@/shared/hooks/usePaginationUi'

export const useFetchNftHolders = (address: Address) => {
const query = usePagination({
queryKey: ['nft_holders'],
queryFn: ({ pageParam }) => fetchNftHoldersOfAddress(address, pageParam),
initialPageParam: null,
resultsPerTablePage: 10,
hasMorePagesProperty: 'next_page_params',
getNextPageParam: lastPage => lastPage.next_page_params,
})

const ui = usePaginationUi(query)

return { ...query, ...ui }
}

0 comments on commit f9c990c

Please sign in to comment.