feat: Migrate trending nfts endpoint to graphql (#6049)

* defined useTrendingCollections hook and made fields optional

* working trending collections table

* add gql file

* add gql to search suggestions and handle floor wei vs eth

* gql carousel

* fontWeight typo and skip if flag disabled

---------

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
This commit is contained in:
Charles Bachmeier 2023-03-01 16:18:17 -08:00 committed by GitHub
parent 70cd7272a1
commit e8c689e1d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 301 additions and 67 deletions

@ -2,7 +2,9 @@ import { Trans } from '@lingui/macro'
import { useTrace } from '@uniswap/analytics'
import { InterfaceSectionName, NavBarSearchTypes } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import { SafetyLevel } from 'graphql/data/__generated__/types-and-hooks'
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
import { HistoryDuration, SafetyLevel } from 'graphql/data/__generated__/types-and-hooks'
import { useTrendingCollections } from 'graphql/data/nft/TrendingCollections'
import { SearchToken } from 'graphql/data/SearchTokens'
import useTrendingTokens from 'graphql/data/TrendingTokens'
import { useIsNftPage } from 'hooks/useIsNftPage'
@ -56,7 +58,7 @@ const SearchBarDropdownSection = ({
</Row>
<Column gap="12">
{suggestions.map((suggestion, index) =>
isLoading ? (
isLoading || !suggestion ? (
<SkeletonRow key={index} />
) : isCollection(suggestion) ? (
<CollectionRow
@ -123,6 +125,7 @@ export const SearchBarDropdown = ({
const { pathname } = useLocation()
const isNFTPage = useIsNftPage()
const isNftGraphqlEnabled = useNftGraphqlEnabled()
const isTokenPage = pathname.includes('/tokens')
const [resultsState, setResultsState] = useState<ReactNode>()
@ -131,24 +134,25 @@ export const SearchBarDropdown = ({
() => fetchTrendingCollections({ volumeType: 'eth', timePeriod: 'ONE_DAY' as TimePeriod, size: 3 })
)
const trendingCollections = useMemo(
() =>
trendingCollectionResults
? trendingCollectionResults
.map((collection) => ({
...collection,
collectionAddress: collection.address,
floorPrice: formatEthPrice(collection.floor?.toString()),
stats: {
total_supply: collection.totalSupply,
one_day_change: collection.floorChange,
floor_price: formatEthPrice(collection.floor?.toString()),
},
}))
.slice(0, isNFTPage ? 3 : 2)
: [...Array<GenieCollection>(isNFTPage ? 3 : 2)],
[isNFTPage, trendingCollectionResults]
)
const { data: gqlData, loading } = useTrendingCollections(3, HistoryDuration.Day)
const trendingCollections = useMemo(() => {
const gatedTrendingCollections = isNftGraphqlEnabled ? gqlData : trendingCollectionResults
return gatedTrendingCollections && (!isNftGraphqlEnabled || !loading)
? gatedTrendingCollections
.map((collection) => ({
...collection,
collectionAddress: collection.address,
floorPrice: isNftGraphqlEnabled ? collection.floor : formatEthPrice(collection.floor?.toString()),
stats: {
total_supply: collection.totalSupply,
one_day_change: collection.floorChange,
floor_price: isNftGraphqlEnabled ? collection.floor : formatEthPrice(collection.floor?.toString()),
},
}))
.slice(0, isNFTPage ? 3 : 2)
: [...Array<GenieCollection>(isNFTPage ? 3 : 2)]
}, [gqlData, isNFTPage, isNftGraphqlEnabled, loading, trendingCollectionResults])
const { data: trendingTokenData } = useTrendingTokens(useWeb3React().chainId)

@ -4,4 +4,8 @@ export function useNftGraphqlFlag(): BaseVariant {
return useBaseFlag(FeatureFlag.nftGraphql)
}
export function useNftGraphqlEnabled(): boolean {
return useNftGraphqlFlag() === BaseVariant.Enabled
}
export { BaseVariant as NftGraphqlVariant }

@ -675,9 +675,11 @@ export type QueryNftAssetsArgs = {
asc?: InputMaybe<Scalars['Boolean']>;
before?: InputMaybe<Scalars['String']>;
chain?: InputMaybe<Chain>;
cursor?: InputMaybe<Scalars['String']>;
filter?: InputMaybe<NftAssetsFilterInput>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
limit?: InputMaybe<Scalars['Int']>;
orderBy?: InputMaybe<NftAssetSortableField>;
};
@ -1119,6 +1121,14 @@ export type NftRouteQueryVariables = Exact<{
export type NftRouteQuery = { __typename?: 'Query', nftRoute?: { __typename?: 'NftRouteResponse', id: string, calldata: string, toAddress: string, route?: Array<{ __typename?: 'NftTrade', amount: number, contractAddress: string, id: string, marketplace: NftMarketplace, tokenId: string, tokenType: NftStandard, price: { __typename?: 'TokenAmount', id: string, currency: Currency, value: string }, quotePrice?: { __typename?: 'TokenAmount', id: string, currency: Currency, value: string } }>, sendAmount: { __typename?: 'TokenAmount', id: string, currency: Currency, value: string } } };
export type TrendingCollectionsQueryVariables = Exact<{
size?: InputMaybe<Scalars['Int']>;
timePeriod?: InputMaybe<HistoryDuration>;
}>;
export type TrendingCollectionsQuery = { __typename?: 'Query', topCollections?: { __typename?: 'NftCollectionConnection', edges: Array<{ __typename?: 'NftCollectionEdge', node: { __typename?: 'NftCollection', name?: string, isVerified?: boolean, nftContracts?: Array<{ __typename?: 'NftContract', address: string, totalSupply?: number }>, image?: { __typename?: 'Image', url: string }, bannerImage?: { __typename?: 'Image', url: string }, markets?: Array<{ __typename?: 'NftCollectionMarket', owners?: number, floorPrice?: { __typename?: 'TimestampedAmount', value: number }, totalVolume?: { __typename?: 'TimestampedAmount', value: number }, volume?: { __typename?: 'TimestampedAmount', value: number }, volumePercentChange?: { __typename?: 'TimestampedAmount', value: number }, floorPricePercentChange?: { __typename?: 'TimestampedAmount', value: number }, sales?: { __typename?: 'TimestampedAmount', value: number }, listings?: { __typename?: 'TimestampedAmount', value: number } }> } }> } };
export const RecentlySearchedAssetsDocument = gql`
query RecentlySearchedAssets($collectionAddresses: [String!]!, $contracts: [ContractInput!]!) {
@ -2117,4 +2127,82 @@ export function useNftRouteLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<N
}
export type NftRouteQueryHookResult = ReturnType<typeof useNftRouteQuery>;
export type NftRouteLazyQueryHookResult = ReturnType<typeof useNftRouteLazyQuery>;
export type NftRouteQueryResult = Apollo.QueryResult<NftRouteQuery, NftRouteQueryVariables>;
export type NftRouteQueryResult = Apollo.QueryResult<NftRouteQuery, NftRouteQueryVariables>;
export const TrendingCollectionsDocument = gql`
query TrendingCollections($size: Int, $timePeriod: HistoryDuration) {
topCollections(limit: $size, duration: $timePeriod) {
edges {
node {
name
nftContracts {
address
totalSupply
}
image {
url
}
bannerImage {
url
}
isVerified
markets(currencies: ETH) {
floorPrice {
value
}
owners
totalVolume {
value
}
volume(duration: $timePeriod) {
value
}
volumePercentChange(duration: $timePeriod) {
value
}
floorPricePercentChange(duration: $timePeriod) {
value
}
sales {
value
}
totalVolume {
value
}
listings {
value
}
}
}
}
}
}
`;
/**
* __useTrendingCollectionsQuery__
*
* To run a query within a React component, call `useTrendingCollectionsQuery` and pass it any options that fit your needs.
* When your component renders, `useTrendingCollectionsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useTrendingCollectionsQuery({
* variables: {
* size: // value for 'size'
* timePeriod: // value for 'timePeriod'
* },
* });
*/
export function useTrendingCollectionsQuery(baseOptions?: Apollo.QueryHookOptions<TrendingCollectionsQuery, TrendingCollectionsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<TrendingCollectionsQuery, TrendingCollectionsQueryVariables>(TrendingCollectionsDocument, options);
}
export function useTrendingCollectionsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TrendingCollectionsQuery, TrendingCollectionsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<TrendingCollectionsQuery, TrendingCollectionsQueryVariables>(TrendingCollectionsDocument, options);
}
export type TrendingCollectionsQueryHookResult = ReturnType<typeof useTrendingCollectionsQuery>;
export type TrendingCollectionsLazyQueryHookResult = ReturnType<typeof useTrendingCollectionsLazyQuery>;
export type TrendingCollectionsQueryResult = Apollo.QueryResult<TrendingCollectionsQuery, TrendingCollectionsQueryVariables>;

@ -0,0 +1,95 @@
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
import gql from 'graphql-tag'
import { TrendingCollection } from 'nft/types'
import { useMemo } from 'react'
import { HistoryDuration, useTrendingCollectionsQuery } from '../__generated__/types-and-hooks'
gql`
query TrendingCollections($size: Int, $timePeriod: HistoryDuration) {
topCollections(limit: $size, duration: $timePeriod) {
edges {
node {
name
nftContracts {
address
totalSupply
}
image {
url
}
bannerImage {
url
}
isVerified
markets(currencies: ETH) {
floorPrice {
value
}
owners
totalVolume {
value
}
volume(duration: $timePeriod) {
value
}
volumePercentChange(duration: $timePeriod) {
value
}
floorPricePercentChange(duration: $timePeriod) {
value
}
sales {
value
}
listings {
value
}
}
}
}
}
}
`
export function useTrendingCollections(size: number, timePeriod: HistoryDuration) {
const isNftGraphqlEnabled = useNftGraphqlEnabled()
const { data, loading, error } = useTrendingCollectionsQuery({
variables: {
size,
timePeriod,
},
skip: !isNftGraphqlEnabled,
})
const trendingCollections: TrendingCollection[] | undefined = useMemo(
() =>
data?.topCollections?.edges?.map((edge) => {
const collection = edge?.node
return {
name: collection.name,
address: collection.nftContracts?.[0].address,
imageUrl: collection.image?.url,
bannerImageUrl: collection.bannerImage?.url,
isVerified: collection.isVerified,
volume: collection.markets?.[0].volume?.value,
volumeChange: collection.markets?.[0].volumePercentChange?.value,
floor: collection.markets?.[0].floorPrice?.value,
floorChange: collection.markets?.[0].floorPricePercentChange?.value,
marketCap: collection.markets?.[0].totalVolume?.value,
percentListed:
(collection.markets?.[0].listings?.value ?? 0) / (collection.nftContracts?.[0].totalSupply ?? 1),
owners: collection.markets?.[0].owners,
sales: collection.markets?.[0].sales?.value,
totalSupply: collection.nftContracts?.[0].totalSupply,
}
}),
[data?.topCollections?.edges]
)
return {
data: trendingCollections,
loading,
error,
}
}

@ -1,3 +1,6 @@
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
import { HistoryDuration } from 'graphql/data/__generated__/types-and-hooks'
import { useTrendingCollections } from 'graphql/data/nft/TrendingCollections'
import { fetchTrendingCollections } from 'nft/queries'
import { TimePeriod } from 'nft/types'
import { calculateCardIndex } from 'nft/utils'
@ -114,6 +117,7 @@ const TRENDING_COLLECTION_SIZE = 5
const Banner = () => {
const navigate = useNavigate()
const isNftGraphqlEnabled = useNftGraphqlEnabled()
const { data } = useQuery(
['trendingCollections'],
@ -130,12 +134,18 @@ const Banner = () => {
refetchOnMount: false,
}
)
const collections = useMemo(
() => data?.filter((collection) => !EXCLUDED_COLLECTIONS.includes(collection.address)).slice(0, 5),
[data]
const { data: gqlData } = useTrendingCollections(
TRENDING_COLLECTION_SIZE + EXCLUDED_COLLECTIONS.length,
HistoryDuration.Day
)
const collections = useMemo(() => {
const gatedData = isNftGraphqlEnabled ? gqlData : data
return gatedData
?.filter((collection) => collection.address && !EXCLUDED_COLLECTIONS.includes(collection.address))
.slice(0, 5)
}, [data, gqlData, isNftGraphqlEnabled])
const [activeCollectionIdx, setActiveCollectionIdx] = useState(0)
const onToggleNextSlide = useCallback(
(direction: number) => {

@ -1,10 +1,11 @@
import { formatNumberOrString, NumberType } from '@uniswap/conedison/format'
import { loadingAnimation } from 'components/Loader/styled'
import { LoadingBubble } from 'components/Tokens/loading'
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
import { useCollection } from 'graphql/data/nft/Collection'
import { VerifiedIcon } from 'nft/components/icons'
import { Markets, TrendingCollection } from 'nft/types'
import { formatWeiToDecimal } from 'nft/utils'
import { ethNumberStandardFormatter, formatWeiToDecimal } from 'nft/utils'
import styled from 'styled-components/macro'
import { ThemedText } from 'theme/components/text'
@ -235,7 +236,8 @@ const MARKETS_ENUM_TO_NAME = {
}
export const CarouselCard = ({ collection, onClick }: CarouselCardProps) => {
const { data: gqlCollection, loading } = useCollection(collection.address)
const { data: gqlCollection, loading } = useCollection(collection.address ?? '')
const isNftGraphqlEnabled = useNftGraphqlEnabled()
if (loading) return <LoadingCarouselCard />
@ -252,9 +254,14 @@ export const CarouselCard = ({ collection, onClick }: CarouselCardProps) => {
</FirstColumnTextWrapper>
</TableElement>
<TableElement>
<ThemedText.SubHeaderSmall color="userThemeColor">
{formatWeiToDecimal(collection.floor.toString())} ETH Floor
</ThemedText.SubHeaderSmall>
{collection.floor && (
<ThemedText.SubHeaderSmall color="userThemeColor">
{isNftGraphqlEnabled
? ethNumberStandardFormatter(collection.floor)
: formatWeiToDecimal(collection.floor.toString())}{' '}
ETH Floor
</ThemedText.SubHeaderSmall>
)}
</TableElement>
<TableElement>
<ThemedText.SubHeaderSmall color="userThemeColor">
@ -304,7 +311,7 @@ const CollectionName = styled(ThemedText.MediumHeader)`
const CarouselCardHeader = ({ collection }: { collection: TrendingCollection }) => {
return (
<CardHeaderContainer src={collection.bannerImageUrl}>
<CardHeaderContainer src={collection.bannerImageUrl ?? ''}>
<CardHeaderColumn>
<CollectionImage src={collection.imageUrl} />
<CollectionNameContainer>

@ -1,4 +1,5 @@
import { formatEther } from '@ethersproject/units'
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
import { SquareArrowDownIcon, SquareArrowUpIcon, VerifiedIcon } from 'nft/components/icons'
import { useIsMobile } from 'nft/hooks'
import { Denomination } from 'nft/types'
@ -114,9 +115,12 @@ export const EthCell = ({
usdPrice?: number
}) => {
const denominatedValue = getDenominatedValue(denomination, true, value, usdPrice)
const isNftGraphqlEnabled = useNftGraphqlEnabled()
const formattedValue = denominatedValue
? denomination === Denomination.ETH
? formatWeiToDecimal(denominatedValue.toString(), true) + ' ETH'
? isNftGraphqlEnabled
? ethNumberStandardFormatter(denominatedValue.toString(), false, true, false) + ' ETH'
: formatWeiToDecimal(denominatedValue.toString(), true) + ' ETH'
: ethNumberStandardFormatter(denominatedValue, true, false, true)
: '-'

@ -19,7 +19,9 @@ export enum ColumnHeaders {
const VOLUME_CHANGE_MAX_VALUE = 9999
const compareFloats = (a: number, b: number): 1 | -1 => {
const compareFloats = (a?: number, b?: number): 1 | -1 => {
if (!a) return -1
if (!b) return 1
return Math.round(a * 100000) >= Math.round(b * 100000) ? 1 : -1
}
@ -123,7 +125,7 @@ const CollectionTable = ({ data, timePeriod }: { data: CollectionTableColumn[];
const { change } = cell.row.original.volume
return timePeriod === TimePeriod.AllTime ? (
<TextCell value="-" />
) : change >= VOLUME_CHANGE_MAX_VALUE ? (
) : change && change >= VOLUME_CHANGE_MAX_VALUE ? (
<ChangeCell change={change}>{`>${VOLUME_CHANGE_MAX_VALUE}`}%</ChangeCell>
) : (
<ChangeCell change={change} />

@ -1,4 +1,7 @@
import { OpacityHoverState } from 'components/Common'
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
import { HistoryDuration } from 'graphql/data/__generated__/types-and-hooks'
import { useTrendingCollections } from 'graphql/data/nft/TrendingCollections'
import ms from 'ms.macro'
import { CollectionTableColumn, Denomination, TimePeriod, VolumeType } from 'nft/types'
import { fetchPrice } from 'nft/utils'
@ -29,7 +32,7 @@ const StyledHeader = styled.div`
color: ${({ theme }) => theme.textPrimary};
font-size: 36px;
line-height: 44px;
weight: 500;
font-weight: 500;
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
font-size: 20px;
@ -69,9 +72,25 @@ const StyledSelectorText = styled(ThemedText.SubHeader)<{ active: boolean }>`
color: ${({ theme, active }) => (active ? theme.textPrimary : theme.textSecondary)};
`
function convertTimePeriodToHistoryDuration(timePeriod: TimePeriod): HistoryDuration {
switch (timePeriod) {
case TimePeriod.OneDay:
return HistoryDuration.Day
case TimePeriod.SevenDays:
return HistoryDuration.Week
case TimePeriod.ThirtyDays:
return HistoryDuration.Month
case TimePeriod.AllTime:
return HistoryDuration.Max
default:
return HistoryDuration.Day
}
}
const TrendingCollections = () => {
const [timePeriod, setTimePeriod] = useState<TimePeriod>(TimePeriod.OneDay)
const [isEthToggled, setEthToggled] = useState(true)
const isNftGraphqlEnabled = useNftGraphqlEnabled()
const { isSuccess, data } = useQuery(
['trendingCollections', timePeriod],
@ -86,6 +105,8 @@ const TrendingCollections = () => {
}
)
const { data: gqlData, loading } = useTrendingCollections(100, convertTimePeriodToHistoryDuration(timePeriod))
const { data: usdPrice } = useQuery(['fetchPrice', {}], () => fetchPrice(), {
refetchOnReconnect: false,
refetchOnWindowFocus: false,
@ -94,8 +115,10 @@ const TrendingCollections = () => {
})
const trendingCollections = useMemo(() => {
if (isSuccess && data) {
return data.map((d) => ({
const gatedData = isNftGraphqlEnabled ? gqlData : data
const dataLoaded = isNftGraphqlEnabled ? !loading : isSuccess
if (dataLoaded && gatedData) {
return gatedData.map((d) => ({
...d,
collection: {
name: d.name,
@ -114,7 +137,6 @@ const TrendingCollections = () => {
},
owners: {
value: d.owners,
change: d.ownersChange,
},
sales: d.sales,
totalSupply: d.totalSupply,
@ -122,7 +144,7 @@ const TrendingCollections = () => {
usdPrice,
}))
} else return [] as CollectionTableColumn[]
}, [data, isSuccess, isEthToggled, usdPrice])
}, [isNftGraphqlEnabled, gqlData, data, loading, isSuccess, isEthToggled, usdPrice])
return (
<ExploreContainer>

@ -35,21 +35,20 @@ export interface TransactionsResponse {
}
export interface TrendingCollection {
name: string
address: string
imageUrl: string
bannerImageUrl: string
isVerified: boolean
volume: number
volumeChange: number
floor: number
floorChange: number
marketCap: number
percentListed: number
owners: number
ownersChange: number
totalSupply: number
sales: number
name?: string
address?: string
imageUrl?: string
bannerImageUrl?: string
isVerified?: boolean
volume?: number
volumeChange?: number
floor?: number
floorChange?: number
marketCap?: number
percentListed?: number
owners?: number
totalSupply?: number
sales?: number
}
export enum Denomination {
@ -59,26 +58,25 @@ export enum Denomination {
export interface CollectionTableColumn {
collection: {
name: string
address: string
logo: string
isVerified: boolean
name?: string
address?: string
logo?: string
isVerified?: boolean
}
volume: {
value: number
change: number
type: VolumeType
value?: number
change?: number
type?: VolumeType
}
floor: {
value: number
change: number
value?: number
change?: number
}
owners: {
value: number
change: number
value?: number
}
sales: number
totalSupply: number
sales?: number
totalSupply?: number
denomination: Denomination
usdPrice?: number
}