Skip to content

Commit

Permalink
Paginate NFT collection tab and lazy load NFT info. (#1296)
Browse files Browse the repository at this point in the history
  • Loading branch information
NoahSaso committed Jun 29, 2023
1 parent c280a37 commit 63ddbdc
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 142 deletions.
73 changes: 72 additions & 1 deletion packages/stateful/components/NftCard.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { ComponentProps } from 'react'
import { useTranslation } from 'react-i18next'

import { NftCardProps, NftCard as StatelessNftCard } from '@dao-dao/stateless'
import {
NftCardProps,
NftCard as StatelessNftCard,
useCachedLoadingWithError,
} from '@dao-dao/stateless'
import { WithChainId } from '@dao-dao/types'
import { CHAIN_ID } from '@dao-dao/utils'

import {
nftCardInfoSelector,
nftStakerOrOwnerSelector,
} from '../recoil/selectors/nft'
import { EntityDisplay } from './EntityDisplay'

export const NftCard = (props: Omit<NftCardProps, 'EntityDisplay'>) => (
Expand All @@ -17,3 +27,64 @@ export const StakedNftCard = (props: ComponentProps<typeof NftCard>) => {
const { t } = useTranslation()
return <NftCard hideCollection ownerLabel={t('title.staker')} {...props} />
}

export type LazyNftCardProps = WithChainId<{
collectionAddress: string
tokenId: string
// If passed and the NFT is staked, get staker info from this contract.
stakingContractAddress?: string
}>

export const LazyNftCard = ({
collectionAddress,
tokenId,
stakingContractAddress,
chainId,
}: LazyNftCardProps) => {
const info = useCachedLoadingWithError(
nftCardInfoSelector({
collection: collectionAddress,
tokenId,
chainId,
})
)

const stakerOrOwner = useCachedLoadingWithError(
nftStakerOrOwnerSelector({
collectionAddress,
tokenId,
stakingContractAddress,
chainId,
})
)

const staked =
!stakerOrOwner.loading &&
!stakerOrOwner.errored &&
stakerOrOwner.data.staked

const NftCardToUse = staked ? StakedNftCard : NftCardNoCollection

return info.loading || info.errored ? (
<NftCardToUse
chainId={chainId || CHAIN_ID}
className="animate-pulse"
collection={{
address: collectionAddress,
name: 'Loading...',
}}
description={undefined}
name="Loading..."
tokenId={tokenId}
/>
) : (
<NftCardToUse
owner={
stakerOrOwner.loading || stakerOrOwner.errored
? undefined
: stakerOrOwner.data.address
}
{...info.data}
/>
)
}
43 changes: 43 additions & 0 deletions packages/stateful/recoil/selectors/nft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
refreshWalletBalancesIdAtom,
refreshWalletStargazeNftsAtom,
} from '@dao-dao/state'
import { stakerForNftSelector } from '@dao-dao/state/recoil/selectors/contracts/DaoVotingCw721Staked'
import { NftCardInfo, WithChainId } from '@dao-dao/types'
import { StargazeNft } from '@dao-dao/types/nft'
import {
Expand Down Expand Up @@ -309,3 +310,45 @@ export const walletStakedNftCardInfosSelector = selectorFamily<
}))
},
})

// Get owner of NFT, or staker if NFT is staked with the given staking contract.
export const nftStakerOrOwnerSelector = selectorFamily<
{
staked: boolean
address: string
},
WithChainId<{
collectionAddress: string
tokenId: string
stakingContractAddress?: string
}>
>({
key: 'nftStakerOrOwner',
get:
({ collectionAddress, tokenId, stakingContractAddress, chainId }) =>
async ({ get }) => {
const { owner } = get(
Cw721BaseSelectors.ownerOfSelector({
contractAddress: collectionAddress,
params: [{ tokenId }],
chainId,
})
)

const staker =
stakingContractAddress && owner === stakingContractAddress
? get(
stakerForNftSelector({
contractAddress: stakingContractAddress,
tokenId,
chainId,
})
)
: undefined

return {
staked: staker !== undefined,
address: staker || owner,
}
},
})
Original file line number Diff line number Diff line change
@@ -1,145 +1,38 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { constSelector, waitForAll } from 'recoil'

import { Cw721BaseSelectors } from '@dao-dao/state/recoil'
import { stakerForNftSelector } from '@dao-dao/state/recoil/selectors/contracts/DaoVotingCw721Staked'
import {
NftsTab,
useCachedLoadable,
useCachedLoading,
} from '@dao-dao/stateless'
import { NftsTab, useCachedLoading } from '@dao-dao/stateless'

import { NftCardNoCollection, StakedNftCard } from '../../../../components'
import { nftCardInfoSelector } from '../../../../recoil/selectors/nft'
import { LazyNftCard } from '../../../../components'
import { useGovernanceCollectionInfo } from '../hooks'

enum Filter {
All = 'all',
Staked = 'staked',
Unstaked = 'unstaked',
}

export const NftCollectionTab = () => {
const { t } = useTranslation()
const { collectionAddress, stakingContractAddress } =
useGovernanceCollectionInfo()

const allTokens = useCachedLoadable(
const allTokens = useCachedLoading(
Cw721BaseSelectors.allTokensSelector({
contractAddress: collectionAddress,
})
)

const nftCardInfosLoading = useCachedLoading(
allTokens.state === 'hasValue'
? waitForAll(
allTokens.contents.map((tokenId) =>
nftCardInfoSelector({
collection: collectionAddress,
tokenId,
})
)
)
: undefined,
}),
[]
)

const tokenOwners = useCachedLoading(
allTokens.state === 'hasValue'
? waitForAll(
allTokens.contents.map((tokenId) =>
Cw721BaseSelectors.ownerOfSelector({
contractAddress: collectionAddress,
params: [
{
tokenId,
},
],
})
)
)
: undefined,
[]
)

// Show the owner by checking if owner is staking contract and using the
// staker instead if so. If not staked with staking contract, use owner.
const stakerOrOwnerForTokens = useCachedLoading(
allTokens.state === 'hasValue' && !tokenOwners.loading
? waitForAll(
allTokens.contents.map((tokenId, index) =>
tokenOwners.data[index].owner === stakingContractAddress
? stakerForNftSelector({
contractAddress: stakingContractAddress,
tokenId,
})
: constSelector(tokenOwners.data[index].owner)
)
)
: undefined,
[]
)

const [filter, setFilter] = useState(Filter.All)

return (
<NftsTab
NftCard={NftCardNoCollection}
description={t('info.nftCollectionExplanation', { context: filter })}
filterDropdownProps={{
onSelect: (value) => setFilter(value),
options: [
{
label: t('title.allNfts'),
value: Filter.All,
},
{
label: t('title.stakedNfts'),
value: Filter.Staked,
},
{
label: t('title.unstakedNfts'),
value: Filter.Unstaked,
},
],
selected: filter,
}}
NftCard={LazyNftCard}
description={t('info.nftCollectionExplanation', { context: 'all' })}
nfts={
nftCardInfosLoading.loading ||
tokenOwners.loading ||
stakerOrOwnerForTokens.loading
allTokens.loading
? { loading: true }
: {
loading: false,
data: nftCardInfosLoading.data
.map((nft, index) => ({
...nft,
owner: stakerOrOwnerForTokens.data[index],
// If staked, show staked card instead of default.
OverrideNftCard:
tokenOwners.data[index].owner === stakingContractAddress
? StakedNftCard
: undefined,
}))
// Filter by selected filter.
.filter((nft) =>
filter === Filter.All
? true
: filter === Filter.Staked
? nft.OverrideNftCard
: !nft.OverrideNftCard
)
// Sort staked NFTs first.
.sort((a, b) =>
a.OverrideNftCard && b.OverrideNftCard
? a.name.localeCompare(b.name)
: a.OverrideNftCard
? -1
: b.OverrideNftCard
? 1
: 0
),
data: allTokens.data.map((tokenId) => ({
collectionAddress,
tokenId,
stakingContractAddress,
key: collectionAddress + tokenId,
})),
}
}
/>
Expand Down
2 changes: 1 addition & 1 deletion packages/stateless/components/dao/tabs/NftsTab.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Default.args = {
makeNftCardProps(),
makeNftCardProps(),
makeNftCardProps(),
],
].map((props) => ({ ...props, key: props.tokenId })),
},
NftCard,
description: 'This is the NFTs tab.',
Expand Down
47 changes: 27 additions & 20 deletions packages/stateless/components/dao/tabs/NftsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
import { Image } from '@mui/icons-material'
import { ComponentType } from 'react'
import { ComponentType, useState } from 'react'
import { useTranslation } from 'react-i18next'

import { LoadingData, NftCardInfo } from '@dao-dao/types'
import { LoadingData } from '@dao-dao/types'

import { GridCardContainer } from '../../GridCardContainer'
import { Dropdown, DropdownProps } from '../../inputs/Dropdown'
import { Loader } from '../../logo/Loader'
import { NoContent } from '../../NoContent'
import { PAGINATION_MIN_PAGE, Pagination } from '../../Pagination'

export interface NftsTabProps<N extends NftCardInfo> {
nfts: LoadingData<(N & { OverrideNftCard?: ComponentType<N> })[]>
export interface NftsTabProps<N = any> {
nfts: LoadingData<(N & { key: string })[]>
NftCard: ComponentType<N>
description?: string
// If present, will show a dropdown to filter the NFTs.
filterDropdownProps?: DropdownProps<any>
}

export const NftsTab = <N extends NftCardInfo>({
const NFTS_PER_PAGE = 30

export const NftsTab = <N extends object>({
nfts,
NftCard,
description,
filterDropdownProps,
}: NftsTabProps<N>) => {
const { t } = useTranslation()

const [page, setPage] = useState(PAGINATION_MIN_PAGE)

return nfts.loading || nfts.data.length > 0 ? (
<>
<div className="flex min-h-[3.5rem] flex-col gap-y-4 gap-x-16 pb-6 sm:flex-row sm:items-center sm:justify-between">
Expand Down Expand Up @@ -52,21 +57,23 @@ export const NftsTab = <N extends NftCardInfo>({
{nfts.loading ? (
<Loader fill={false} />
) : (
<GridCardContainer className="pb-6">
{nfts.data.map((props) =>
props.OverrideNftCard ? (
<props.OverrideNftCard
{...(props as N)}
key={props.collection.address + props.tokenId}
/>
) : (
<NftCard
{...(props as N)}
key={props.collection.address + props.tokenId}
/>
)
)}
</GridCardContainer>
<>
<GridCardContainer className="pb-6">
{nfts.data
.slice((page - 1) * NFTS_PER_PAGE, page * NFTS_PER_PAGE)
.map((props) => (
<NftCard {...(props as N)} key={props.key} />
))}
</GridCardContainer>

<Pagination
className="mx-auto mt-12"
page={page}
pageSize={NFTS_PER_PAGE}
setPage={setPage}
total={nfts.data.length}
/>
</>
)}
</>
) : (
Expand Down

2 comments on commit 63ddbdc

@vercel
Copy link

@vercel vercel bot commented on 63ddbdc Jun 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 63ddbdc Jun 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.