Commit e069ba11 authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

feat: NFT Collection Search via GraphQL (#6081)

* add new search query and formatting helper fn

* working search

* skip gql search if not enabled

* minimize query

---------
Co-authored-by: default avatarCharles Bachmeier <charlie@genie.xyz>
parent 4ebc467c
......@@ -4,6 +4,8 @@ import { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from '@uniswap/analyt
import { BrowserEvent, InterfaceElementName, InterfaceEventName, InterfaceSectionName } from '@uniswap/analytics-events'
import { useWeb3React } from '@web3-react/core'
import clsx from 'clsx'
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
import { useCollectionSearch } from 'graphql/data/nft/CollectionSearch'
import { useSearchTokens } from 'graphql/data/SearchTokens'
import useDebounce from 'hooks/useDebounce'
import { useIsNftPage } from 'hooks/useIsNftPage'
......@@ -49,12 +51,13 @@ export const SearchBar = () => {
const { pathname } = useLocation()
const isMobile = useIsMobile()
const isTablet = useIsTablet()
const isNftGraphqlEnabled = useNftGraphqlEnabled()
useOnClickOutside(searchRef, () => {
isOpen && toggleOpen()
})
const { data: collections, isLoading: collectionsAreLoading } = useQuery(
const { data: queryCollections, isLoading: queryCollectionsAreLoading } = useQuery(
['searchCollections', debouncedSearchValue],
() => fetchSearchCollections(debouncedSearchValue),
{
......@@ -65,12 +68,26 @@ export const SearchBar = () => {
}
)
const { data: gqlCollections, loading: gqlCollectionsAreLoading } = useCollectionSearch(debouncedSearchValue)
const { gatedCollections, gatedCollectionsAreLoading } = useMemo(() => {
return isNftGraphqlEnabled
? {
gatedCollections: gqlCollections,
gatedCollectionsAreLoading: gqlCollectionsAreLoading,
}
: {
gatedCollections: queryCollections,
gatedCollectionsAreLoading: queryCollectionsAreLoading,
}
}, [gqlCollections, gqlCollectionsAreLoading, isNftGraphqlEnabled, queryCollections, queryCollectionsAreLoading])
const { chainId } = useWeb3React()
const { data: tokens, loading: tokensAreLoading } = useSearchTokens(debouncedSearchValue, chainId ?? 1)
const isNFTPage = useIsNftPage()
const [reducedTokens, reducedCollections] = organizeSearchResults(isNFTPage, tokens ?? [], collections ?? [])
const [reducedTokens, reducedCollections] = organizeSearchResults(isNFTPage, tokens ?? [], gatedCollections ?? [])
// close dropdown on escape
useEffect(() => {
......@@ -86,7 +103,7 @@ export const SearchBar = () => {
return () => {
document.removeEventListener('keydown', escapeKeyDownHandler)
}
}, [isOpen, toggleOpen, collections])
}, [isOpen, toggleOpen, gatedCollections])
// clear searchbar when changing pages
useEffect(() => {
......@@ -208,7 +225,7 @@ export const SearchBar = () => {
collections={reducedCollections}
queryText={debouncedSearchValue}
hasInput={debouncedSearchValue.length > 0}
isLoading={tokensAreLoading || collectionsAreLoading}
isLoading={tokensAreLoading || gatedCollectionsAreLoading}
/>
)}
</Box>
......
......@@ -1091,6 +1091,13 @@ export type CollectionQueryVariables = Exact<{
export type CollectionQuery = { __typename?: 'Query', nftCollections?: { __typename?: 'NftCollectionConnection', edges: Array<{ __typename?: 'NftCollectionEdge', cursor: string, node: { __typename?: 'NftCollection', collectionId: string, description?: string, discordUrl?: string, homepageUrl?: string, instagramName?: string, isVerified?: boolean, name?: string, numAssets?: number, twitterName?: string, bannerImage?: { __typename?: 'Image', url: string }, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string, chain: Chain, name?: string, standard?: NftStandard, symbol?: string, totalSupply?: number }>, traits?: Array<{ __typename?: 'NftCollectionTrait', name?: string, values?: Array<string>, stats?: Array<{ __typename?: 'NftCollectionTraitStats', name?: string, value?: string, assets?: number, listings?: number }> }>, markets?: Array<{ __typename?: 'NftCollectionMarket', owners?: number, floorPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, value: number }, totalVolume?: { __typename?: 'TimestampedAmount', value: number, currency?: Currency }, listings?: { __typename?: 'TimestampedAmount', value: number }, volume?: { __typename?: 'TimestampedAmount', value: number, currency?: Currency }, volumePercentChange?: { __typename?: 'TimestampedAmount', value: number, currency?: Currency }, floorPricePercentChange?: { __typename?: 'TimestampedAmount', value: number, currency?: Currency }, marketplaces?: Array<{ __typename?: 'NftCollectionMarketplace', marketplace?: NftMarketplace, listings?: number, floorPrice?: number }> }> } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, startCursor?: string } } };
export type CollectionSearchQueryVariables = Exact<{
query: Scalars['String'];
}>;
export type CollectionSearchQuery = { __typename?: 'Query', nftCollections?: { __typename?: 'NftCollectionConnection', edges: Array<{ __typename?: 'NftCollectionEdge', cursor: string, node: { __typename?: 'NftCollection', isVerified?: boolean, name?: string, numAssets?: number, image?: { __typename?: 'Image', url: string }, nftContracts?: Array<{ __typename?: 'NftContract', address: string, chain: Chain, name?: string, symbol?: string, totalSupply?: number }>, markets?: Array<{ __typename?: 'NftCollectionMarket', floorPrice?: { __typename?: 'TimestampedAmount', currency?: Currency, value: number } }> } }>, pageInfo: { __typename?: 'PageInfo', endCursor?: string, hasNextPage?: boolean, hasPreviousPage?: boolean, startCursor?: string } } };
export type DetailsQueryVariables = Exact<{
address: Scalars['String'];
tokenId: Scalars['String'];
......@@ -1824,6 +1831,70 @@ export function useCollectionLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions
export type CollectionQueryHookResult = ReturnType<typeof useCollectionQuery>;
export type CollectionLazyQueryHookResult = ReturnType<typeof useCollectionLazyQuery>;
export type CollectionQueryResult = Apollo.QueryResult<CollectionQuery, CollectionQueryVariables>;
export const CollectionSearchDocument = gql`
query CollectionSearch($query: String!) {
nftCollections(filter: {nameQuery: $query}) {
edges {
cursor
node {
image {
url
}
isVerified
name
numAssets
nftContracts {
address
chain
name
symbol
totalSupply
}
markets(currencies: ETH) {
floorPrice {
currency
value
}
}
}
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
}
`;
/**
* __useCollectionSearchQuery__
*
* To run a query within a React component, call `useCollectionSearchQuery` and pass it any options that fit your needs.
* When your component renders, `useCollectionSearchQuery` 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 } = useCollectionSearchQuery({
* variables: {
* query: // value for 'query'
* },
* });
*/
export function useCollectionSearchQuery(baseOptions: Apollo.QueryHookOptions<CollectionSearchQuery, CollectionSearchQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<CollectionSearchQuery, CollectionSearchQueryVariables>(CollectionSearchDocument, options);
}
export function useCollectionSearchLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<CollectionSearchQuery, CollectionSearchQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<CollectionSearchQuery, CollectionSearchQueryVariables>(CollectionSearchDocument, options);
}
export type CollectionSearchQueryHookResult = ReturnType<typeof useCollectionSearchQuery>;
export type CollectionSearchLazyQueryHookResult = ReturnType<typeof useCollectionSearchLazyQuery>;
export type CollectionSearchQueryResult = Apollo.QueryResult<CollectionSearchQuery, CollectionSearchQueryVariables>;
export const DetailsDocument = gql`
query Details($address: String!, $tokenId: String!) {
nftAssets(address: $address, filter: {listed: false, tokenIds: [$tokenId]}) {
......
......@@ -86,23 +86,13 @@ gql`
}
`
interface useCollectionReturnProps {
data: GenieCollection
loading: boolean
}
export function useCollection(address: string): useCollectionReturnProps {
const { data: queryData, loading } = useCollectionQuery({
variables: {
addresses: address,
},
})
const queryCollection = queryData?.nftCollections?.edges?.[0]?.node as NonNullable<NftCollection>
export function formatCollectionQueryData(
queryCollection: NonNullable<NftCollection>,
address?: string
): GenieCollection {
const market = queryCollection?.markets?.[0]
const traits = useMemo(() => {
return {} as Record<string, Trait[]>
}, [])
if (!address && !queryCollection?.nftContracts?.[0]?.address) return {} as GenieCollection
const traits = {} as Record<string, Trait[]>
if (queryCollection?.traits) {
queryCollection?.traits.forEach((trait) => {
if (trait.name && trait.stats) {
......@@ -116,43 +106,60 @@ export function useCollection(address: string): useCollectionReturnProps {
}
})
}
return {
address: address ?? queryCollection?.nftContracts?.[0]?.address ?? '',
isVerified: queryCollection?.isVerified,
name: queryCollection?.name,
description: queryCollection?.description,
standard: queryCollection?.nftContracts?.[0]?.standard,
bannerImageUrl: queryCollection?.bannerImage?.url,
stats: {
num_owners: market?.owners,
floor_price: market?.floorPrice?.value,
one_day_volume: market?.volume?.value,
one_day_change: market?.volumePercentChange?.value,
one_day_floor_change: market?.floorPricePercentChange?.value,
banner_image_url: queryCollection?.bannerImage?.url,
total_supply: queryCollection?.numAssets,
total_listings: market?.listings?.value,
total_volume: market?.totalVolume?.value,
},
traits,
marketplaceCount: market?.marketplaces?.map((market) => {
return {
marketplace: market.marketplace?.toLowerCase() ?? '',
count: market.listings ?? 0,
floorPrice: market.floorPrice ?? 0,
}
}),
imageUrl: queryCollection?.image?.url ?? '',
twitterUrl: queryCollection?.twitterName,
instagram: queryCollection?.instagramName,
discordUrl: queryCollection?.discordUrl,
externalUrl: queryCollection?.homepageUrl,
rarityVerified: false, // TODO update when backend supports
// isFoundation: boolean, // TODO ask backend to add
}
}
interface useCollectionReturnProps {
data: GenieCollection
loading: boolean
}
export function useCollection(address: string, skip?: boolean): useCollectionReturnProps {
const { data: queryData, loading } = useCollectionQuery({
variables: {
addresses: address,
},
skip,
})
const queryCollection = queryData?.nftCollections?.edges?.[0]?.node as NonNullable<NftCollection>
return useMemo(() => {
return {
data: {
address,
isVerified: queryCollection?.isVerified,
name: queryCollection?.name,
description: queryCollection?.description,
standard: queryCollection?.nftContracts?.[0]?.standard,
bannerImageUrl: queryCollection?.bannerImage?.url,
stats: {
num_owners: market?.owners,
floor_price: market?.floorPrice?.value,
one_day_volume: market?.volume?.value,
one_day_change: market?.volumePercentChange?.value,
one_day_floor_change: market?.floorPricePercentChange?.value,
banner_image_url: queryCollection?.bannerImage?.url,
total_supply: queryCollection?.numAssets,
total_listings: market?.listings?.value,
total_volume: market?.totalVolume?.value,
},
traits,
marketplaceCount: market?.marketplaces?.map((market) => {
return {
marketplace: market.marketplace?.toLowerCase() ?? '',
count: market.listings ?? 0,
floorPrice: market.floorPrice ?? 0,
}
}),
imageUrl: queryCollection?.image?.url ?? '',
twitterUrl: queryCollection?.twitterName,
instagram: queryCollection?.instagramName,
discordUrl: queryCollection?.discordUrl,
externalUrl: queryCollection?.homepageUrl,
rarityVerified: false, // TODO update when backend supports
// isFoundation: boolean, // TODO ask backend to add
},
data: formatCollectionQueryData(queryCollection, address),
loading,
}
}, [address, loading, market, queryCollection, traits])
}, [address, loading, queryCollection])
}
import { isAddress } from '@ethersproject/address'
import { useNftGraphqlEnabled } from 'featureFlags/flags/nftlGraphql'
import gql from 'graphql-tag'
import { GenieCollection } from 'nft/types'
import { blocklistedCollections } from 'nft/utils'
import { useMemo } from 'react'
import { NftCollection, useCollectionSearchQuery } from '../__generated__/types-and-hooks'
import { formatCollectionQueryData, useCollection } from './Collection'
const MAX_SEARCH_RESULTS = 6
gql`
query CollectionSearch($query: String!) {
nftCollections(filter: { nameQuery: $query }) {
edges {
cursor
node {
image {
url
}
isVerified
name
numAssets
nftContracts {
address
chain
name
symbol
totalSupply
}
markets(currencies: ETH) {
floorPrice {
currency
value
}
}
}
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
startCursor
}
}
}
`
interface useCollectionSearchReturnProps {
data: GenieCollection[]
loading: boolean
}
function useCollectionQuerySearch(query: string, skip?: boolean): useCollectionSearchReturnProps {
const { data: queryData, loading } = useCollectionSearchQuery({
variables: {
query,
},
skip,
})
return useMemo(() => {
return {
data:
queryData?.nftCollections?.edges
?.filter(
(collectionEdge) =>
collectionEdge.node.nftContracts?.[0]?.address &&
!blocklistedCollections.includes(collectionEdge.node.nftContracts?.[0]?.address)
)
.slice(0, MAX_SEARCH_RESULTS)
.map((collectionEdge) => {
const queryCollection = collectionEdge.node as NonNullable<NftCollection>
return formatCollectionQueryData(queryCollection)
}) ?? [],
loading,
}
}, [loading, queryData])
}
export function useCollectionSearch(queryOrAddress: string): useCollectionSearchReturnProps {
const isNftGraphqlEnabled = useNftGraphqlEnabled()
const isName = !isAddress(queryOrAddress.toLowerCase())
const queryResult = useCollectionQuerySearch(queryOrAddress, isNftGraphqlEnabled ? !isName : true)
const addressResult = useCollection(queryOrAddress, isNftGraphqlEnabled ? isName : true)
return isName ? queryResult : { data: [addressResult.data], loading: addressResult.loading }
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment