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> <Box className={styles.primaryText}>{collection.name}</Box>
{collection.isVerified && <VerifiedIcon className={styles.suggestionIcon} />} {collection.isVerified && <VerifiedIcon className={styles.suggestionIcon} />}
</Row> </Row>
<Box className={styles.secondaryText}>{putCommas(collection.stats.total_supply)} items</Box> <Box className={styles.secondaryText}>{putCommas(collection.stats?.total_supply)} items</Box>
</Column> </Column>
</Row> </Row>
{collection.floorPrice ? ( {collection.stats?.floor_price ? (
<Column className={styles.suggestionSecondaryContainer}> <Column className={styles.suggestionSecondaryContainer}>
<Row gap="4"> <Row gap="4">
<Box className={styles.primaryText}>{ethNumberStandardFormatter(collection.floorPrice)} ETH</Box> <Box className={styles.primaryText}>{ethNumberStandardFormatter(collection.stats?.floor_price)} ETH</Box>
</Row> </Row>
<Box className={styles.secondaryText}>Floor</Box> <Box className={styles.secondaryText}>Floor</Box>
</Column> </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 // 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 // This is a temporary solution before the two endpoints merge
// https://github.com/relay-tools/relay-hooks/issues/215 // 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 fetchQuery = (params: RequestParameters, variables: Variables): Promise<GraphQLResponse> => {
const isNFT = NFT_QUERIES.includes(params.name) const isNFT = NFT_QUERIES.includes(params.name)

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

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

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

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

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

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

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

@ -145,7 +145,7 @@ export const ProfilePage = () => {
if (ownerCollections?.length && collectionStats?.length) { if (ownerCollections?.length && collectionStats?.length) {
const ownerCollectionsCopy = [...ownerCollections] const ownerCollectionsCopy = [...ownerCollections]
for (const collection of ownerCollectionsCopy) { 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) collection.floorPrice = roundFloorPrice(floorPrice)
} }
setWalletCollections(ownerCollectionsCopy) setWalletCollections(ownerCollectionsCopy)
@ -156,7 +156,7 @@ export const ProfilePage = () => {
if (ownerCollections?.length && collectionStats?.length) { if (ownerCollections?.length && collectionStats?.length) {
const ownerCollectionsCopy = [...ownerCollections] const ownerCollectionsCopy = [...ownerCollections]
for (const collection of ownerCollectionsCopy) { 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 collection.floorPrice = floorPrice ? Math.round(floorPrice * 1000 + Number.EPSILON) / 1000 : 0 //round to at most 3 digits
} }
setWalletCollections(ownerCollectionsCopy) setWalletCollections(ownerCollectionsCopy)

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

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

@ -1,4 +1,5 @@
import { isAddress } from '@ethersproject/address' import { isAddress } from '@ethersproject/address'
import { groupBy } from 'nft/utils/groupBy'
import { GenieCollection } from '../../types' import { GenieCollection } from '../../types'
@ -55,5 +56,11 @@ export const CollectionStatsFetcher = async (addressOrName: string, recursive =
}) })
const data = await r.json() 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' import { SellOrder } from '../sell'
export interface OpenSeaCollection { export interface OpenSeaCollection {
@ -97,49 +99,30 @@ export interface GenieAsset {
owner: string owner: string
creator: OpenSeaUser creator: OpenSeaUser
metadataUrl: string metadataUrl: string
traits?: { traits?: Trait[]
trait_type: string
value: string
display_type?: any
max_value?: any
trait_count: number
order?: any
}[]
} }
export interface GenieCollection { export interface GenieCollection {
collectionAddress: string
address: string address: string
indexingStatus: string isVerified?: boolean
isVerified: boolean name?: string
name: string description?: string
description: string standard?: string
standard: string
bannerImageUrl?: string bannerImageUrl?: string
floorPrice: number stats?: {
stats: { num_owners?: number
num_owners: number floor_price?: number
floor_price: number one_day_volume?: number
one_day_volume: number one_day_change?: number
one_day_change: number one_day_floor_change?: number
one_day_floor_change: number banner_image_url?: string
banner_image_url: string total_supply?: number
total_supply: number total_listings?: number
total_listings: number total_volume?: number
total_volume: number
} }
symbol: string traits?: Record<string, Trait[]>
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 } }
marketplaceCount?: { marketplace: string; count: number }[] marketplaceCount?: { marketplace: string; count: number }[]
imageUrl: string imageUrl?: string
twitter?: string twitter?: string
instagram?: string instagram?: string
discordUrl?: string discordUrl?: string

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

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