refactor: Add Collection Stats GraphQL query (#5022)

* add demo Asset Fetcher

* new file

* update fetcher

* update query name

* beginning integration type

* uncomment

* working mutant apes

* comment out debug logging

* pass in inputs to query

* update collections to handle inf scroll

* paginated query first attempt

* wrapped assetQuery

* building pagination, needs spread

* working pagination

* working sort

* use cacheconfig

* change query source in Collection page

* passed in filters

* fetch schema from main endpoint

* delete unused relayenv

* rename token_url

* easy GenieAsset refactoring

* add rarity

* update price info

* remove logging

* remove redundancy

* refactor usd price fetching for assets

* update standard and address

* remove unused cacheconfig

* add gql collection query

* dont repeat ethprice calc

* unmemo bools

* reduce duplicated usd price logic

* cleanup imports

* useUsd price hook

* restructure Traits datatype

* working traits

* add new markets

* resolve merge conflict

* totalVolume workaround

* update comment

* fix for totalVolume bug

* add sudoswap icon

* deprecate unused vars in GenieCollection

* interim rarity verified

* cleanup

* use forEach

* add comment

* undefined division

* cleanup

* usememo marketplace select

* update % formatting

* use updated prod schema

* useLazyLoad

* remove any cast

* re-add null checks

* respond to comments

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
This commit is contained in:
Charles Bachmeier 2022-11-02 13:33:08 -07:00 committed by GitHub
parent c8f365ca31
commit d74c05008b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 257 additions and 117 deletions

@ -0,0 +1,6 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 14.4C0 6.4471 6.4471 0 14.4 0H33.6C41.5529 0 48 6.4471 48 14.4V33.6C48 41.5529 41.5529 48 33.6 48H14.4C6.4471 48 0 41.5529 0 33.6V14.4Z" fill="#B9B9FF"/>
<path d="M9.29999 15H12L15.3 24L12 33H9.29999L12.6 24L9.29999 15Z" fill="#121212"/>
<path d="M23.5727 33.192C22.1807 33.192 21.0367 32.872 20.1407 32.232C19.2607 31.592 18.8207 30.72 18.8207 29.616H21.2447C21.2447 30.112 21.4607 30.496 21.8927 30.768C22.3407 31.024 22.9167 31.152 23.6207 31.152H24.6287C25.4607 31.152 26.0767 30.992 26.4767 30.672C26.8767 30.336 27.0767 29.896 27.0767 29.352C27.0767 28.808 26.8847 28.384 26.5007 28.08C26.1167 27.776 25.5727 27.56 24.8687 27.432L23.0687 27.168C20.4447 26.752 19.1327 25.48 19.1327 23.352C19.1327 22.136 19.5327 21.208 20.3327 20.568C21.1327 19.928 22.2767 19.608 23.7647 19.608H24.6767C26.0527 19.608 27.1567 19.92 27.9887 20.544C28.8207 21.152 29.2367 21.96 29.2367 22.968H26.8127C26.8127 22.568 26.6127 22.248 26.2127 22.008C25.8287 21.768 25.3007 21.648 24.6287 21.648H23.7167C22.9807 21.648 22.4207 21.8 22.0367 22.104C21.6527 22.392 21.4607 22.808 21.4607 23.352C21.4607 24.28 22.1167 24.848 23.4287 25.056L25.2287 25.344C26.6687 25.568 27.7247 25.992 28.3967 26.616C29.0687 27.224 29.4047 28.104 29.4047 29.256C29.4047 30.488 28.9967 31.456 28.1807 32.16C27.3807 32.848 26.1807 33.192 24.5807 33.192H23.5727Z" fill="#121212"/>
<path d="M38.7 15H36L32.7 24L36 33H38.7L35.4 24L38.7 15Z" fill="#121212"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -93,13 +93,13 @@ export const CollectionRow = ({
<Box className={styles.primaryText}>{collection.name}</Box>
{collection.isVerified && <VerifiedIcon className={styles.suggestionIcon} />}
</Row>
<Box className={styles.secondaryText}>{putCommas(collection.stats.total_supply)} items</Box>
<Box className={styles.secondaryText}>{putCommas(collection.stats?.total_supply)} items</Box>
</Column>
</Row>
{collection.floorPrice ? (
{collection.stats?.floor_price ? (
<Column className={styles.suggestionSecondaryContainer}>
<Row gap="4">
<Box className={styles.primaryText}>{ethNumberStandardFormatter(collection.floorPrice)} ETH</Box>
<Box className={styles.primaryText}>{ethNumberStandardFormatter(collection.stats?.floor_price)} ETH</Box>
</Row>
<Box className={styles.secondaryText}>Floor</Box>
</Column>

@ -19,7 +19,7 @@ const nftHeaders = {
// The issue below prevented using a custom var in metadata to gate which queries are for the nft endpoint vs base endpoint
// This is a temporary solution before the two endpoints merge
// https://github.com/relay-tools/relay-hooks/issues/215
const NFT_QUERIES = ['AssetQuery', 'AssetPaginationQuery']
const NFT_QUERIES = ['AssetQuery', 'AssetPaginationQuery', 'CollectionQuery']
const fetchQuery = (params: RequestParameters, variables: Variables): Promise<GraphQLResponse> => {
const isNFT = NFT_QUERIES.includes(params.name)

@ -1,9 +1,8 @@
import graphql from 'babel-plugin-relay/macro'
import { parseEther } from 'ethers/lib/utils'
import { GenieAsset, Rarity } from 'nft/types'
import { loadQuery, usePaginationFragment, usePreloadedQuery } from 'react-relay'
import { useLazyLoadQuery, usePaginationFragment } from 'react-relay'
import RelayEnvironment from '../RelayEnvironment'
import { AssetPaginationQuery } from './__generated__/AssetPaginationQuery.graphql'
import { AssetQuery, NftAssetsFilterInput, NftAssetSortableField } from './__generated__/AssetQuery.graphql'
@ -117,7 +116,7 @@ export function useAssetsQuery(
last?: number,
before?: string
) {
const assetsQueryReference = loadQuery<AssetQuery>(RelayEnvironment, assetQuery, {
const queryData = useLazyLoadQuery<AssetQuery>(assetQuery, {
address,
orderBy,
asc,
@ -127,7 +126,6 @@ export function useAssetsQuery(
last,
before,
})
const queryData = usePreloadedQuery<AssetQuery>(assetQuery, assetsQueryReference)
const { data, hasNext, loadNext, isLoadingNext } = usePaginationFragment<AssetPaginationQuery, any>(
assetPaginationQuery,
queryData

@ -0,0 +1,131 @@
import graphql from 'babel-plugin-relay/macro'
import { Trait } from 'nft/hooks/useCollectionFilters'
import { GenieCollection } from 'nft/types'
import { useLazyLoadQuery } from 'react-relay'
import { CollectionQuery } from './__generated__/CollectionQuery.graphql'
const collectionQuery = graphql`
query CollectionQuery($address: String!) {
nftCollections(filter: { addresses: [$address] }) {
edges {
cursor
node {
bannerImage {
url
}
collectionId
description
discordUrl
homepageUrl
image {
url
}
instagramName
isVerified
name
numAssets
twitterName
nftContracts {
address
chain
name
standard
symbol
totalSupply
}
traits {
name
values
stats {
name
value
assets
listings
}
}
markets(currencies: ETH) {
floorPrice {
currency
value
}
owners
totalVolume {
value
currency
}
listings {
value
}
volume(duration: DAY) {
value
currency
}
volumePercentChange(duration: DAY) {
value
currency
}
floorPricePercentChange(duration: DAY) {
value
currency
}
}
}
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
}
`
export function useCollectionQuery(address: string): GenieCollection | undefined {
const queryData = useLazyLoadQuery<CollectionQuery>(collectionQuery, { address })
const queryCollection = queryData.nftCollections?.edges[0]?.node
const market = queryCollection?.markets && queryCollection?.markets[0]
const traits = {} as Record<string, Trait[]>
if (queryCollection?.traits) {
queryCollection?.traits.forEach((trait) => {
trait.values?.map((value) => {
return {
trait_type: trait.name,
trait_value: value,
} as Trait
})
})
}
return {
address,
isVerified: queryCollection?.isVerified ?? undefined,
name: queryCollection?.name ?? undefined,
description: queryCollection?.description ?? undefined,
standard: queryCollection?.nftContracts ? queryCollection?.nftContracts[0]?.standard ?? undefined : undefined,
bannerImageUrl: queryCollection?.bannerImage?.url,
stats: queryCollection?.markets
? {
num_owners: market?.owners ?? undefined,
floor_price: market?.floorPrice?.value ?? undefined,
one_day_volume: market?.volume?.value ?? undefined,
one_day_change: market?.volumePercentChange?.value ?? undefined,
one_day_floor_change: market?.floorPricePercentChange?.value ?? undefined,
banner_image_url: queryCollection?.bannerImage?.url ?? undefined,
total_supply: queryCollection?.numAssets ?? undefined,
total_listings: market?.listings?.value ?? undefined,
total_volume: market?.totalVolume?.value ?? undefined,
}
: {},
traits,
// marketplaceCount: { marketplace: string; count: number }[], // TODO add when backend supports
imageUrl: queryCollection?.image?.url,
twitter: queryCollection?.twitterName ?? undefined,
instagram: queryCollection?.instagramName ?? undefined,
discordUrl: queryCollection?.discordUrl ?? undefined,
externalUrl: queryCollection?.homepageUrl ?? undefined,
rarityVerified: false, // TODO update when backend supports
// isFoundation: boolean, // TODO ask backend to add
}
}

@ -169,7 +169,7 @@ const PriceTooltip = ({ price }: { price: string }) => (
)
export const PriceCell = ({ marketplace, price }: { marketplace?: Markets; price?: string }) => {
const formattedPrice = useMemo(() => (price ? putCommas(formatEthPrice(price)).toString() : null), [price])
const formattedPrice = useMemo(() => (price ? putCommas(formatEthPrice(price))?.toString() : null), [price])
return (
<Row display={{ sm: 'none', md: 'flex' }} gap="8">

@ -44,7 +44,7 @@ import styled, { useTheme } from 'styled-components/macro'
import { ThemedText } from 'theme'
import { CollectionAssetLoading } from './CollectionAssetLoading'
import { marketPlaceItems } from './MarketplaceSelect'
import { MARKETPLACE_ITEMS } from './MarketplaceSelect'
import { Sweep } from './Sweep'
import { TraitChip } from './TraitChip'
@ -409,9 +409,9 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
}, [collectionStats, location])
useEffect(() => {
if (collectionStats && collectionStats.floorPrice) {
const lowValue = collectionStats.floorPrice
const maxValue = 10 * collectionStats.floorPrice
if (collectionStats && collectionStats.stats?.floor_price) {
const lowValue = collectionStats.stats?.floor_price
const maxValue = 10 * collectionStats.stats?.floor_price
if (priceRangeLow === '') {
setPriceRangeLow(lowValue?.toFixed(2))
@ -491,7 +491,7 @@ export const CollectionNfts = ({ contractAddress, collectionStats, rarityVerifie
{markets.map((market) => (
<TraitChip
key={market}
value={marketPlaceItems[market as keyof typeof marketPlaceItems]}
value={MARKETPLACE_ITEMS[market as keyof typeof MARKETPLACE_ITEMS]}
onClick={() => {
scrollToTop()
removeMarket(market)

@ -1,5 +1,6 @@
import clsx from 'clsx'
import { getDeltaArrow } from 'components/Tokens/TokenDetails/PriceChart'
import { NftGraphQlVariant, useNftGraphQlFlag } from 'featureFlags/flags/nftGraphQl'
import { Box, BoxProps } from 'nft/components/Box'
import { Column, Row } from 'nft/components/Flex'
import { Marquee } from 'nft/components/layout/Marquee'
@ -264,21 +265,27 @@ const statsLoadingSkeleton = (isMobile: boolean) =>
)
const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMobile?: boolean } & BoxProps) => {
const uniqueOwnersPercentage = stats.stats
? roundWholePercentage((stats.stats.num_owners / stats.stats.total_supply) * 100)
: 0
const totalSupplyStr = stats.stats ? quantityFormatter(stats.stats.total_supply) : 0
const isNftGraphQl = useNftGraphQlFlag() === NftGraphQlVariant.Enabled
const uniqueOwnersPercentage =
stats.stats && stats.stats.total_supply
? roundWholePercentage(((stats.stats.num_owners ?? 0) / stats.stats.total_supply) * 100)
: 0
const totalSupplyStr = stats.stats ? quantityFormatter(stats.stats.total_supply ?? 0) : 0
const listedPercentageStr =
stats.stats && stats.stats.total_listings > 0
? roundWholePercentage((stats.stats.total_listings / stats.stats.total_supply) * 100)
stats.stats && stats.stats.total_supply
? roundWholePercentage(((stats.stats.total_listings ?? 0) / stats.stats.total_supply) * 100)
: 0
const isCollectionStatsLoading = useIsCollectionLoading((state) => state.isCollectionStatsLoading)
// round daily volume & floorPrice to 3 decimals or less
const totalVolumeStr = volumeFormatter(stats.stats?.total_volume)
const floorPriceStr = floorFormatter(stats.floorPrice)
const totalVolumeStr = volumeFormatter(stats.stats?.total_volume ?? 0)
const floorPriceStr = floorFormatter(stats.stats?.floor_price ?? 0)
// graphQL formatted %age values out of 100, whereas v3 endpoint did a decimal between 0 & 1
// TODO: remove feature flag gated logic when graphql migration is complete
const floorChangeStr =
stats.stats && stats.stats.one_day_floor_change ? Math.round(Math.abs(stats.stats.one_day_floor_change) * 100) : 0
stats.stats && stats.stats.one_day_floor_change
? Math.round(Math.abs(stats.stats.one_day_floor_change) * (isNftGraphQl ? 1 : 100))
: 0
const arrow = stats.stats && stats.stats.one_day_change ? getDeltaArrow(stats.stats.one_day_floor_change) : null
return (
@ -287,23 +294,18 @@ const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMob
statsLoadingSkeleton(isMobile ?? false)
) : (
<>
{stats.floorPrice ? (
{stats.stats?.floor_price ? (
<StatsItem label="Global floor" isMobile={isMobile ?? false}>
{floorPriceStr} ETH
</StatsItem>
) : null}
{stats.stats?.one_day_floor_change ? (
<StatsItem label="24-Hour floor" isMobile={isMobile ?? false}>
<StatsItem label="24-Hour Floor" isMobile={isMobile ?? false}>
<PercentChange>
{floorChangeStr}% {arrow}
</PercentChange>
</StatsItem>
) : null}
{stats.stats?.total_volume ? (
<StatsItem label="Total volume" isMobile={isMobile ?? false}>
{totalVolumeStr} ETH
</StatsItem>
) : null}
{totalSupplyStr ? (
<StatsItem label="Items" isMobile={isMobile ?? false}>
{totalSupplyStr}
@ -314,6 +316,11 @@ const StatsRow = ({ stats, isMobile, ...props }: { stats: GenieCollection; isMob
{uniqueOwnersPercentage}%
</StatsItem>
) : null}
{stats.stats?.total_volume ? (
<StatsItem label="Total Volume" isMobile={isMobile ?? false}>
{totalVolumeStr} ETH
</StatsItem>
) : null}
{stats.stats?.total_listings && listedPercentageStr > 0 ? (
<StatsItem label="Listed" isMobile={isMobile ?? false}>
{listedPercentageStr}%
@ -381,8 +388,8 @@ export const CollectionStats = ({ stats, isMobile }: { stats: GenieCollection; i
<Box className={styles.statsText}>
<CollectionName
collectionStats={stats}
name={stats.name}
isVerified={stats.isVerified}
name={stats.name ?? ''}
isVerified={stats.isVerified ?? false}
isMobile={isMobile}
collectionSocialsIsOpen={collectionSocialsIsOpen}
toggleCollectionSocials={toggleCollectionSocials}
@ -390,7 +397,7 @@ export const CollectionStats = ({ stats, isMobile }: { stats: GenieCollection; i
{!isMobile && (
<>
{(stats.description || isCollectionStatsLoading) && (
<CollectionDescription description={stats.description} />
<CollectionDescription description={stats.description ?? ''} />
)}
<StatsRow stats={stats} marginTop="20" />
</>

@ -8,18 +8,15 @@ import { subhead } from 'nft/css/common.css'
import { useCollectionFilters } from 'nft/hooks'
import { Trait } from 'nft/hooks/useCollectionFilters'
import { TraitPosition } from 'nft/hooks/useTraitsOpen'
import { groupBy } from 'nft/utils/groupBy'
import { useMemo } from 'react'
import { useReducer } from 'react'
import { TraitSelect } from './TraitSelect'
export const Filters = ({ traits }: { traits: Trait[] }) => {
export const Filters = ({ traitsByGroup }: { traitsByGroup: Record<string, Trait[]> }) => {
const { buyNow, setBuyNow } = useCollectionFilters((state) => ({
buyNow: state.buyNow,
setBuyNow: state.setBuyNow,
}))
const traitsByGroup: Record<string, Trait[]> = useMemo(() => (traits ? groupBy(traits, 'trait_type') : {}), [traits])
const [buyNowHovered, toggleBuyNowHover] = useReducer((state) => !state, false)
const handleBuyNowToggle = () => {

@ -1,6 +1,7 @@
import { sendAnalyticsEvent } from 'analytics'
import { EventName, FilterTypes } from 'analytics/constants'
import clsx from 'clsx'
import { NftGraphQlVariant, useNftGraphQlFlag } from 'featureFlags/flags/nftGraphQl'
import { Box } from 'nft/components/Box'
import * as styles from 'nft/components/collection/Filters.css'
import { Column, Row } from 'nft/components/Flex'
@ -9,16 +10,18 @@ import { subheadSmall } from 'nft/css/common.css'
import { useCollectionFilters } from 'nft/hooks/useCollectionFilters'
import { useTraitsOpen } from 'nft/hooks/useTraitsOpen'
import { TraitPosition } from 'nft/hooks/useTraitsOpen'
import { FormEvent, useEffect, useReducer, useState } from 'react'
import { FormEvent, useEffect, useMemo, useReducer, useState } from 'react'
import { Checkbox } from '../layout/Checkbox'
export const marketPlaceItems = {
export const MARKETPLACE_ITEMS = {
looksrare: 'LooksRare',
nft20: 'NFT20',
nftx: 'NFTX',
opensea: 'OpenSea',
x2y2: 'X2Y2',
cryptopunks: 'LarvaLabs',
sudoswap: 'SudoSwap',
}
const MarketplaceItem = ({
@ -84,6 +87,8 @@ const MarketplaceItem = ({
)
}
const GRAPHQL_MARKETS = ['cryptopunks', 'sudoswap']
export const MarketplaceSelect = () => {
const {
addMarket,
@ -99,6 +104,23 @@ export const MarketplaceSelect = () => {
const [isOpen, setOpen] = useState(!!selectedMarkets.length)
const setTraitsOpen = useTraitsOpen((state) => state.setTraitsOpen)
const isNftGraphQl = useNftGraphQlFlag() === NftGraphQlVariant.Enabled
const MarketplaceItems = useMemo(
() =>
Object.entries(MARKETPLACE_ITEMS)
.filter(([value]) => isNftGraphQl || !GRAPHQL_MARKETS.includes(value))
.map(([value, title]) => (
<MarketplaceItem
key={value}
title={title}
value={value}
count={marketCount?.[value] || 0}
{...{ addMarket, removeMarket, isMarketSelected: selectedMarkets.includes(value) }}
/>
)),
[addMarket, isNftGraphQl, marketCount, removeMarket, selectedMarkets]
)
return (
<>
@ -141,15 +163,7 @@ export const MarketplaceSelect = () => {
</Box>
</Box>
<Column className={styles.filterDropDowns} paddingBottom="8" paddingLeft="0">
{Object.entries(marketPlaceItems).map(([value, title]) => (
<MarketplaceItem
key={value}
title={title}
value={value}
count={marketCount?.[value] || 0}
{...{ addMarket, removeMarket, isMarketSelected: selectedMarkets.includes(value) }}
/>
))}
{MarketplaceItems}
</Column>
</Box>
</>

@ -1,3 +1,4 @@
import { Trait } from 'nft/hooks'
import qs from 'query-string'
import { badge } from '../../css/common.css'
@ -5,12 +6,7 @@ import { Box } from '../Box'
import { Column } from '../Flex'
import * as styles from './Traits.css'
interface TraitProps {
label: string
value: string
}
const Trait: React.FC<TraitProps> = ({ label, value }: TraitProps) => (
const TraitRow: React.FC<Trait> = ({ trait_type, trait_value }: Trait) => (
<Column backgroundColor="backgroundSurface" padding="16" gap="4" borderRadius="12">
<Box
as="span"
@ -22,7 +18,7 @@ const Trait: React.FC<TraitProps> = ({ label, value }: TraitProps) => (
style={{ textTransform: 'uppercase' }}
maxWidth={{ sm: '120', md: '160' }}
>
{label}
{trait_type}
</Box>
<Box
@ -35,27 +31,18 @@ const Trait: React.FC<TraitProps> = ({ label, value }: TraitProps) => (
textOverflow="ellipsis"
maxWidth={{ sm: '120', md: '160' }}
>
{value}
{trait_value}
</Box>
</Column>
)
export const Traits = ({
traits,
collectionAddress,
}: {
traits: {
value: string
trait_type: string
}[]
collectionAddress: string
}) => (
export const Traits = ({ traits, collectionAddress }: { traits: Trait[]; collectionAddress: string }) => (
<div className={styles.grid}>
{traits.length === 0
? 'No traits'
: traits.map((item) => {
const params = qs.stringify(
{ traits: [`("${item.trait_type}","${item.value}")`] },
{ traits: [`("${item.trait_type}","${item.trait_value}")`] },
{
arrayFormat: 'comma',
}
@ -63,11 +50,11 @@ export const Traits = ({
return (
<a
key={`${item.trait_type}-${item.value}`}
key={`${item.trait_type}-${item.trait_value}`}
href={`#/nfts/collection/${collectionAddress}?${params}`}
style={{ textDecoration: 'none' }}
>
<Trait label={item.trait_type} value={item.value} />
<TraitRow trait_type={item.trait_type} trait_value={item.trait_value} />
</a>
)
})}

@ -66,7 +66,7 @@ const ActivityRow = ({ event, index, current }: { event: ActivityEvent; index: n
const navigate = useNavigate()
const formattedPrice = useMemo(
() => (event.price ? putCommas(formatEthPrice(event.price)).toString() : null),
() => (event.price ? putCommas(formatEthPrice(event.price))?.toString() : null),
[event.price]
)

@ -145,7 +145,7 @@ export const ProfilePage = () => {
if (ownerCollections?.length && collectionStats?.length) {
const ownerCollectionsCopy = [...ownerCollections]
for (const collection of ownerCollectionsCopy) {
const floorPrice = collectionStats.find((stat) => stat.address === collection.address)?.floorPrice
const floorPrice = collectionStats.find((stat) => stat.address === collection.address)?.stats?.floor_price
collection.floorPrice = roundFloorPrice(floorPrice)
}
setWalletCollections(ownerCollectionsCopy)
@ -156,7 +156,7 @@ export const ProfilePage = () => {
if (ownerCollections?.length && collectionStats?.length) {
const ownerCollectionsCopy = [...ownerCollections]
for (const collection of ownerCollectionsCopy) {
const floorPrice = collectionStats.find((stat) => stat.address === collection.address)?.floorPrice
const floorPrice = collectionStats.find((stat) => stat.address === collection.address)?.stats?.floor_price //TODO update when changing walletStats endpoint to gql
collection.floorPrice = floorPrice ? Math.round(floorPrice * 1000 + Number.EPSILON) / 1000 : 0 //round to at most 3 digits
}
setWalletCollections(ownerCollectionsCopy)

@ -29,7 +29,7 @@ export const SortByQueries = {
export type Trait = {
trait_type: string
trait_value: string
trait_count: number
trait_count?: number
floorPrice?: number
}

@ -1,6 +1,8 @@
import { useWeb3React } from '@web3-react/core'
import { PageName } from 'analytics/constants'
import { Trace } from 'analytics/Trace'
import { NftGraphQlVariant, useNftGraphQlFlag } from 'featureFlags/flags/nftGraphQl'
import { useCollectionQuery } from 'graphql/data/nft/Collection'
import { MobileHoverBag } from 'nft/components/bag/MobileHoverBag'
import { AnimatedBox, Box } from 'nft/components/Box'
import { Activity, ActivitySwitcher, CollectionNfts, CollectionStats, Filters } from 'nft/components/collection'
@ -10,7 +12,7 @@ import { useBag, useCollectionFilters, useFiltersExpanded, useIsCollectionLoadin
import * as styles from 'nft/pages/collection/index.css'
import { CollectionStatsFetcher } from 'nft/queries'
import { GenieCollection } from 'nft/types'
import { Suspense, useEffect } from 'react'
import { Suspense, useEffect, useMemo } from 'react'
import { useQuery } from 'react-query'
import { useLocation, useNavigate, useParams } from 'react-router-dom'
import { useSpring } from 'react-spring'
@ -42,12 +44,20 @@ const Collection = () => {
const isActivityToggled = pathname.includes('/activity')
const setMarketCount = useCollectionFilters((state) => state.setMarketCount)
const isBagExpanded = useBag((state) => state.bagExpanded)
const isNftGraphQl = useNftGraphQlFlag() === NftGraphQlVariant.Enabled
const { chainId } = useWeb3React()
const { data: collectionStats, isLoading } = useQuery(['collectionStats', contractAddress], () =>
const { data: queryCollection, isLoading } = useQuery(['collectionStats', contractAddress], () =>
CollectionStatsFetcher(contractAddress as string)
)
const gqlCollection = useCollectionQuery(contractAddress as string)
const collectionStats = useMemo(
() => (isNftGraphQl ? gqlCollection : queryCollection),
[isNftGraphQl, gqlCollection, queryCollection]
)
useEffect(() => {
setIsCollectionStatsLoading(isLoading)
}, [isLoading, setIsCollectionStatsLoading])
@ -119,7 +129,7 @@ const Collection = () => {
</CollectionDescriptionSection>
<CollectionDisplaySection>
<Box position="sticky" top="72" width="0">
{isFiltersExpanded && <Filters traits={collectionStats?.traits ?? []} />}
{isFiltersExpanded && <Filters traitsByGroup={collectionStats?.traits ?? {}} />}
</Box>
{/* @ts-ignore: https://github.com/microsoft/TypeScript/issues/34933 */}

@ -1,4 +1,5 @@
import { isAddress } from '@ethersproject/address'
import { groupBy } from 'nft/utils/groupBy'
import { GenieCollection } from '../../types'
@ -55,5 +56,11 @@ export const CollectionStatsFetcher = async (addressOrName: string, recursive =
})
const data = await r.json()
return data?.data ? data.data[0] : {}
const collections = data?.data.map((collection: Record<string, unknown>) => {
return {
...collection,
traits: collection.traits && groupBy(collection.traits as unknown[], 'trait_type'),
}
})
return collections[0]
}

@ -1,3 +1,5 @@
import { Trait } from 'nft/hooks/useCollectionFilters'
import { SellOrder } from '../sell'
export interface OpenSeaCollection {
@ -97,49 +99,30 @@ export interface GenieAsset {
owner: string
creator: OpenSeaUser
metadataUrl: string
traits?: {
trait_type: string
value: string
display_type?: any
max_value?: any
trait_count: number
order?: any
}[]
traits?: Trait[]
}
export interface GenieCollection {
collectionAddress: string
address: string
indexingStatus: string
isVerified: boolean
name: string
description: string
standard: string
isVerified?: boolean
name?: string
description?: string
standard?: string
bannerImageUrl?: string
floorPrice: number
stats: {
num_owners: number
floor_price: number
one_day_volume: number
one_day_change: number
one_day_floor_change: number
banner_image_url: string
total_supply: number
total_listings: number
total_volume: number
stats?: {
num_owners?: number
floor_price?: number
one_day_volume?: number
one_day_change?: number
one_day_floor_change?: number
banner_image_url?: string
total_supply?: number
total_listings?: number
total_volume?: number
}
symbol: string
traits: {
trait_type: string
trait_value: string
trait_count: number
floorSellOrder: PriceInfo
floorPrice: number
}[]
numTraitsByAmount: { traitCount: number; numWithTrait: number }[]
indexingStats: { openSea: { successfulExecutionDate: string; lastRequestedAt: string } }
traits?: Record<string, Trait[]>
marketplaceCount?: { marketplace: string; count: number }[]
imageUrl: string
imageUrl?: string
twitter?: string
instagram?: string
discordUrl?: string

@ -1,4 +1,4 @@
export const putCommas = (value: number) => {
export const putCommas = (value?: number) => {
try {
if (!value) return value
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')

@ -100,9 +100,9 @@ const urlParamsUtils = {
clonedQuery['traits'] = clonedQuery['traits'].map((queryTrait: string) => {
const modifiedTrait = trimTraitStr(queryTrait.replace(/(")/g, ''))
const [trait_type, trait_value] = modifiedTrait.split(',')
const traitInStats = collectionStats.traits.find(
(item) => item.trait_type === trait_type && item.trait_value === trait_value
)
const traitInStats =
collectionStats.traits &&
collectionStats.traits[trait_type].find((trait) => trait.trait_value === trait_value)
/*
For most cases, `traitInStats` is assigned. In case the trait