feat: add animation to search skeleton and typing state (#4598)

* fade when loading

* fade results between searches

* trending search loading skeleton

* adjust skeleton and cleanup

* remove unused style change

* eslint

* add trendingTokens to query and remove unnecessary returns

* move array map compatibility higher in the call hierarchy

* add feature flag to isLoading param

Co-authored-by: Charlie <charlie@uniswap.org>
This commit is contained in:
Charles Bachmeier 2022-09-12 10:47:53 -07:00 committed by GitHub
parent cfee80ce3c
commit 704ad222d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 150 additions and 98 deletions

@ -15,7 +15,7 @@ import { fetchSearchTokens } from 'nft/queries/genie/SearchTokensFetcher'
import { fetchTrendingTokens } from 'nft/queries/genie/TrendingTokensFetcher'
import { FungibleToken, GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
import { formatEthPrice } from 'nft/utils/currency'
import { ChangeEvent, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { ChangeEvent, ReactNode, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { useQuery } from 'react-query'
import { useLocation } from 'react-router-dom'
@ -38,6 +38,7 @@ interface SearchBarDropdownSectionProps {
hoveredIndex: number | undefined
startingIndex: number
setHoveredIndex: (index: number | undefined) => void
isLoading?: boolean
}
export const SearchBarDropdownSection = ({
@ -48,6 +49,7 @@ export const SearchBarDropdownSection = ({
hoveredIndex,
startingIndex,
setHoveredIndex,
isLoading,
}: SearchBarDropdownSectionProps) => {
return (
<Column gap="12">
@ -56,8 +58,10 @@ export const SearchBarDropdownSection = ({
<Box>{header}</Box>
</Row>
<Column gap="12">
{suggestions?.map((suggestion, index) =>
isCollection(suggestion) ? (
{suggestions.map((suggestion, index) =>
isLoading ? (
<SkeletonRow key={index} />
) : isCollection(suggestion) ? (
<CollectionRow
key={suggestion.address}
collection={suggestion as GenieCollection}
@ -87,9 +91,10 @@ interface SearchBarDropdownProps {
tokens: FungibleToken[]
collections: GenieCollection[]
hasInput: boolean
isLoading: boolean
}
export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }: SearchBarDropdownProps) => {
export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput, isLoading }: SearchBarDropdownProps) => {
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0)
const searchHistory = useSearchHistory(
(state: { history: (FungibleToken | GenieCollection)[] }) => state.history
@ -98,6 +103,7 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
const isNFTPage = pathname.includes('/nfts')
const isTokenPage = pathname.includes('/tokens')
const phase1Flag = useNftFlag()
const [resultsState, setResultsState] = useState<ReactNode>()
const tokenSearchResults =
tokens.length > 0 ? (
@ -131,42 +137,48 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
)
) : null
const { data: trendingCollectionResults } = useQuery(['trendingCollections', 'eth', 'twenty_four_hours'], () =>
fetchTrendingCollections({ volumeType: 'eth', timePeriod: 'ONE_DAY' as TimePeriod, size: 3 })
const { data: trendingCollectionResults, isLoading: trendingCollectionsAreLoading } = useQuery(
['trendingCollections', 'eth', 'twenty_four_hours'],
() => fetchTrendingCollections({ volumeType: 'eth', timePeriod: 'ONE_DAY' as TimePeriod, size: 3 })
)
const trendingCollections = useMemo(() => {
return trendingCollectionResults
?.map((collection) => {
return {
...collection,
collectionAddress: collection.address,
floorPrice: formatEthPrice(collection.floor?.toString()),
stats: {
total_supply: collection.totalSupply,
one_day_change: collection.floorChange,
},
}
})
.slice(0, isNFTPage ? 3 : 2)
}, [isNFTPage, trendingCollectionResults])
const showTrendingCollections: boolean = useMemo(
() => (trendingCollections?.length ?? 0) > 0 && !isTokenPage && phase1Flag === NftVariant.Enabled,
[trendingCollections?.length, isTokenPage, phase1Flag]
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,
},
}))
.slice(0, isNFTPage ? 3 : 2)
: [...Array<GenieCollection>(isNFTPage ? 3 : 2)],
[isNFTPage, trendingCollectionResults]
)
const { data: trendingTokenResults } = useQuery([], () => fetchTrendingTokens(4), {
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
})
const { data: trendingTokenResults, isLoading: trendingTokensAreLoading } = useQuery(
['trendingTokens'],
() => fetchTrendingTokens(4),
{
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchOnReconnect: false,
}
)
const trendingTokensLength = phase1Flag === NftVariant.Enabled ? (isTokenPage ? 3 : 2) : 4
const trendingTokens = useMemo(() => {
return trendingTokenResults?.slice(0, trendingTokensLength)
}, [trendingTokenResults, trendingTokensLength])
const trendingTokens = useMemo(
() =>
trendingTokenResults
? trendingTokenResults.slice(0, trendingTokensLength)
: [...Array<FungibleToken>(trendingTokensLength)],
[trendingTokenResults, trendingTokensLength]
)
const totalSuggestions = hasInput
? tokens.length + collections.length
@ -201,61 +213,90 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
}
}, [toggleOpen, hoveredIndex, totalSuggestions])
useEffect(() => {
if (!isLoading) {
const currentState = () =>
hasInput ? (
// Empty or Up to 8 combined tokens and nfts
<Column gap="20">
{isNFTPage ? (
<>
{collectionSearchResults}
{tokenSearchResults}
</>
) : (
<>
{tokenSearchResults}
{collectionSearchResults}
</>
)}
</Column>
) : (
// Recent Searches, Trending Tokens, Trending Collections
<Column gap="20">
{searchHistory.length > 0 && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={0}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={searchHistory}
header={<Trans>Recent searches</Trans>}
headerIcon={<ClockIcon />}
/>
)}
{!isNFTPage && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={searchHistory.length}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={trendingTokens}
header={<Trans>Popular tokens</Trans>}
headerIcon={<TrendingArrow />}
isLoading={trendingTokensAreLoading}
/>
)}
{!isTokenPage && phase1Flag === NftVariant.Enabled && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={searchHistory.length + (isNFTPage ? 0 : trendingTokens?.length ?? 0)}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={trendingCollections as unknown as GenieCollection[]}
header={<Trans>Popular NFT collections</Trans>}
headerIcon={<TrendingArrow />}
isLoading={trendingCollectionsAreLoading}
/>
)}
</Column>
)
setResultsState(currentState)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isLoading,
tokens,
collections,
trendingCollections,
trendingCollectionsAreLoading,
trendingTokens,
trendingTokensAreLoading,
hoveredIndex,
phase1Flag,
toggleOpen,
searchHistory,
hasInput,
isNFTPage,
isTokenPage,
])
return (
<Box className={styles.searchBarDropdown}>
{hasInput ? (
// Empty or Up to 8 combined tokens and nfts
<Column gap="20">
{isNFTPage ? (
<>
{collectionSearchResults}
{tokenSearchResults}
</>
) : (
<>
{tokenSearchResults}
{collectionSearchResults}
</>
)}
</Column>
) : (
// Recent Searches, Trending Tokens, Trending Collections
<Column gap="20">
{searchHistory.length > 0 && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={0}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={searchHistory}
header={<Trans>Recent searches</Trans>}
headerIcon={<ClockIcon />}
/>
)}
{(trendingTokens?.length ?? 0) > 0 && !isNFTPage && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={searchHistory.length}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={trendingTokens ?? []}
header={<Trans>Popular tokens</Trans>}
headerIcon={<TrendingArrow />}
/>
)}
{showTrendingCollections && (
<SearchBarDropdownSection
hoveredIndex={hoveredIndex}
startingIndex={searchHistory.length + (isNFTPage ? 0 : trendingTokens?.length ?? 0)}
setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen}
suggestions={trendingCollections as unknown as GenieCollection[]}
header={<Trans>Popular NFT collections</Trans>}
headerIcon={<TrendingArrow />}
/>
)}
</Column>
)}
<Box opacity={isLoading ? '0.3' : '1'} transition="125">
{resultsState}
</Box>
</Box>
)
}
@ -380,14 +421,13 @@ export const SearchBar = () => {
/>
</Row>
<Box className={clsx(isOpen ? styles.visible : styles.hidden)}>
{debouncedSearchValue.length > 0 && (tokensAreLoading || collectionsAreLoading) ? (
<SkeletonRow />
) : (
{isOpen && (
<SearchBarDropdown
toggleOpen={toggleOpen}
tokens={reducedTokens}
collections={reducedCollections}
hasInput={debouncedSearchValue.length > 0}
isLoading={tokensAreLoading || (collectionsAreLoading && phase1Flag === NftVariant.Enabled)}
/>
)}
</Box>

@ -175,13 +175,21 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index
export const SkeletonRow = () => {
return (
<Box className={styles.searchBarDropdown}>
<Row className={styles.suggestionRow}>
<Row>
<Box className={styles.imageHolder} />
<Box borderRadius="round" height="16" width="160" background="loading" />
</Row>
<Row className={styles.suggestionRow}>
<Row width="full">
<Box className={styles.imageHolder} />
<Column gap="4" width="full">
<Row justifyContent="space-between">
<Box borderRadius="round" height="20" background="loading" style={{ width: '180px' }} />
<Box borderRadius="round" height="20" width="48" background="loading" />
</Row>
<Row justifyContent="space-between">
<Box borderRadius="round" height="16" width="120" background="loading" />
<Box borderRadius="round" height="16" width="48" background="loading" />
</Row>
</Column>
</Row>
</Box>
</Row>
)
}

@ -24,6 +24,7 @@ const themeContractValues = {
magicGradient: '',
placeholder: '',
lightGrayButton: '',
loading: '',
// Opacities of black and white
white95: '',
@ -55,6 +56,7 @@ const dimensions = {
'2': '2',
'4': '4px',
'8': '8px',
'12': '12px',
'16': '16px',
'18': '18px',
'20': '20px',
@ -142,7 +144,6 @@ export const vars = createGlobalTheme(':root', {
transculent: '#7F7F7F',
transparent: 'transparent',
none: 'none',
loading: '#7C85A24D',
// new uniswap colors:
blue400: '#4C82FB',
@ -153,6 +154,7 @@ export const vars = createGlobalTheme(':root', {
green200: '#5CFE9D',
green400: '#1A9550',
grey900: '#0E111A',
grey800: '#141B2B',
grey700: '#293249',
grey500: '#5E6887',
grey400: '#7C85A2',
@ -298,7 +300,7 @@ const layoutStyles = defineProperties({
position: ['absolute', 'fixed', 'relative', 'sticky', 'static'],
objectFit: ['contain', 'cover'],
order: [0, 1],
opacity: ['auto', '0', '1'],
opacity: ['auto', '0', '0.3', '0.5', '0.7', '1'],
} as const,
shorthands: {
paddingX: ['paddingLeft', 'paddingRight'],

@ -22,6 +22,7 @@ export const darkTheme: Theme = {
magicGradient: vars.color.blue400,
placeholder: vars.color.grey400,
lightGrayButton: vars.color.grey700,
loading: vars.color.grey800,
// Opacities of black and white
white95: '#0E111AF2',

@ -22,6 +22,7 @@ export const lightTheme: Theme = {
magicGradient: vars.color.pink400,
placeholder: vars.color.grey300,
lightGrayButton: vars.color.grey100,
loading: vars.color.grey50,
// Opacities of black and white
white95: '#EDEFF7F2',