From e8c689e1d43574b3a6097986c8ea9fdbad7bb78a Mon Sep 17 00:00:00 2001 From: Charles Bachmeier Date: Wed, 1 Mar 2023 16:18:17 -0800 Subject: [PATCH] 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 --- src/components/NavBar/SearchBarDropdown.tsx | 44 +++++---- src/featureFlags/flags/nftlGraphql.ts | 4 + .../data/__generated__/types-and-hooks.ts | 90 +++++++++++++++++- src/graphql/data/nft/TrendingCollections.ts | 95 +++++++++++++++++++ src/nft/components/explore/Banner.tsx | 18 +++- src/nft/components/explore/CarouselCard.tsx | 19 ++-- src/nft/components/explore/Cells/Cells.tsx | 6 +- .../components/explore/CollectionTable.tsx | 6 +- .../explore/TrendingCollections.tsx | 32 ++++++- src/nft/types/discover/discover.ts | 54 +++++------ 10 files changed, 301 insertions(+), 67 deletions(-) create mode 100644 src/graphql/data/nft/TrendingCollections.ts diff --git a/src/components/NavBar/SearchBarDropdown.tsx b/src/components/NavBar/SearchBarDropdown.tsx index 7e5049de53..25689568e0 100644 --- a/src/components/NavBar/SearchBarDropdown.tsx +++ b/src/components/NavBar/SearchBarDropdown.tsx @@ -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 = ({ {suggestions.map((suggestion, index) => - isLoading ? ( + isLoading || !suggestion ? ( ) : isCollection(suggestion) ? ( () @@ -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(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(isNFTPage ? 3 : 2)] + }, [gqlData, isNFTPage, isNftGraphqlEnabled, loading, trendingCollectionResults]) const { data: trendingTokenData } = useTrendingTokens(useWeb3React().chainId) diff --git a/src/featureFlags/flags/nftlGraphql.ts b/src/featureFlags/flags/nftlGraphql.ts index 0c043f6f91..764219699f 100644 --- a/src/featureFlags/flags/nftlGraphql.ts +++ b/src/featureFlags/flags/nftlGraphql.ts @@ -4,4 +4,8 @@ export function useNftGraphqlFlag(): BaseVariant { return useBaseFlag(FeatureFlag.nftGraphql) } +export function useNftGraphqlEnabled(): boolean { + return useNftGraphqlFlag() === BaseVariant.Enabled +} + export { BaseVariant as NftGraphqlVariant } diff --git a/src/graphql/data/__generated__/types-and-hooks.ts b/src/graphql/data/__generated__/types-and-hooks.ts index 6bc61fde7d..7f268c98f4 100644 --- a/src/graphql/data/__generated__/types-and-hooks.ts +++ b/src/graphql/data/__generated__/types-and-hooks.ts @@ -675,9 +675,11 @@ export type QueryNftAssetsArgs = { asc?: InputMaybe; before?: InputMaybe; chain?: InputMaybe; + cursor?: InputMaybe; filter?: InputMaybe; first?: InputMaybe; last?: InputMaybe; + limit?: InputMaybe; orderBy?: InputMaybe; }; @@ -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; + timePeriod?: InputMaybe; +}>; + + +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; export type NftRouteLazyQueryHookResult = ReturnType; -export type NftRouteQueryResult = Apollo.QueryResult; \ No newline at end of file +export type NftRouteQueryResult = Apollo.QueryResult; +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) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(TrendingCollectionsDocument, options); + } +export function useTrendingCollectionsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(TrendingCollectionsDocument, options); + } +export type TrendingCollectionsQueryHookResult = ReturnType; +export type TrendingCollectionsLazyQueryHookResult = ReturnType; +export type TrendingCollectionsQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/src/graphql/data/nft/TrendingCollections.ts b/src/graphql/data/nft/TrendingCollections.ts new file mode 100644 index 0000000000..6c297f5580 --- /dev/null +++ b/src/graphql/data/nft/TrendingCollections.ts @@ -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, + } +} diff --git a/src/nft/components/explore/Banner.tsx b/src/nft/components/explore/Banner.tsx index 3e9f71230f..ba69eb168b 100644 --- a/src/nft/components/explore/Banner.tsx +++ b/src/nft/components/explore/Banner.tsx @@ -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) => { diff --git a/src/nft/components/explore/CarouselCard.tsx b/src/nft/components/explore/CarouselCard.tsx index 120e7dd3cf..d5d8745c32 100644 --- a/src/nft/components/explore/CarouselCard.tsx +++ b/src/nft/components/explore/CarouselCard.tsx @@ -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 @@ -252,9 +254,14 @@ export const CarouselCard = ({ collection, onClick }: CarouselCardProps) => { - - {formatWeiToDecimal(collection.floor.toString())} ETH Floor - + {collection.floor && ( + + {isNftGraphqlEnabled + ? ethNumberStandardFormatter(collection.floor) + : formatWeiToDecimal(collection.floor.toString())}{' '} + ETH Floor + + )} @@ -304,7 +311,7 @@ const CollectionName = styled(ThemedText.MediumHeader)` const CarouselCardHeader = ({ collection }: { collection: TrendingCollection }) => { return ( - + diff --git a/src/nft/components/explore/Cells/Cells.tsx b/src/nft/components/explore/Cells/Cells.tsx index 7589d8c582..6633ed92da 100644 --- a/src/nft/components/explore/Cells/Cells.tsx +++ b/src/nft/components/explore/Cells/Cells.tsx @@ -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) : '-' diff --git a/src/nft/components/explore/CollectionTable.tsx b/src/nft/components/explore/CollectionTable.tsx index 042f63316b..e33ac74998 100644 --- a/src/nft/components/explore/CollectionTable.tsx +++ b/src/nft/components/explore/CollectionTable.tsx @@ -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 ? ( - ) : change >= VOLUME_CHANGE_MAX_VALUE ? ( + ) : change && change >= VOLUME_CHANGE_MAX_VALUE ? ( {`>${VOLUME_CHANGE_MAX_VALUE}`}% ) : ( diff --git a/src/nft/components/explore/TrendingCollections.tsx b/src/nft/components/explore/TrendingCollections.tsx index 89d369fc3d..1237a31ec0 100644 --- a/src/nft/components/explore/TrendingCollections.tsx +++ b/src/nft/components/explore/TrendingCollections.tsx @@ -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.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 ( diff --git a/src/nft/types/discover/discover.ts b/src/nft/types/discover/discover.ts index a3d62ab06b..98cbe2103d 100644 --- a/src/nft/types/discover/discover.ts +++ b/src/nft/types/discover/discover.ts @@ -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 }