Commit 704ad222 authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

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: default avatarCharlie <charlie@uniswap.org>
parent cfee80ce
...@@ -15,7 +15,7 @@ import { fetchSearchTokens } from 'nft/queries/genie/SearchTokensFetcher' ...@@ -15,7 +15,7 @@ import { fetchSearchTokens } from 'nft/queries/genie/SearchTokensFetcher'
import { fetchTrendingTokens } from 'nft/queries/genie/TrendingTokensFetcher' import { fetchTrendingTokens } from 'nft/queries/genie/TrendingTokensFetcher'
import { FungibleToken, GenieCollection, TimePeriod, TrendingCollection } from 'nft/types' import { FungibleToken, GenieCollection, TimePeriod, TrendingCollection } from 'nft/types'
import { formatEthPrice } from 'nft/utils/currency' 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 { useQuery } from 'react-query'
import { useLocation } from 'react-router-dom' import { useLocation } from 'react-router-dom'
...@@ -38,6 +38,7 @@ interface SearchBarDropdownSectionProps { ...@@ -38,6 +38,7 @@ interface SearchBarDropdownSectionProps {
hoveredIndex: number | undefined hoveredIndex: number | undefined
startingIndex: number startingIndex: number
setHoveredIndex: (index: number | undefined) => void setHoveredIndex: (index: number | undefined) => void
isLoading?: boolean
} }
export const SearchBarDropdownSection = ({ export const SearchBarDropdownSection = ({
...@@ -48,6 +49,7 @@ export const SearchBarDropdownSection = ({ ...@@ -48,6 +49,7 @@ export const SearchBarDropdownSection = ({
hoveredIndex, hoveredIndex,
startingIndex, startingIndex,
setHoveredIndex, setHoveredIndex,
isLoading,
}: SearchBarDropdownSectionProps) => { }: SearchBarDropdownSectionProps) => {
return ( return (
<Column gap="12"> <Column gap="12">
...@@ -56,8 +58,10 @@ export const SearchBarDropdownSection = ({ ...@@ -56,8 +58,10 @@ export const SearchBarDropdownSection = ({
<Box>{header}</Box> <Box>{header}</Box>
</Row> </Row>
<Column gap="12"> <Column gap="12">
{suggestions?.map((suggestion, index) => {suggestions.map((suggestion, index) =>
isCollection(suggestion) ? ( isLoading ? (
<SkeletonRow key={index} />
) : isCollection(suggestion) ? (
<CollectionRow <CollectionRow
key={suggestion.address} key={suggestion.address}
collection={suggestion as GenieCollection} collection={suggestion as GenieCollection}
...@@ -87,9 +91,10 @@ interface SearchBarDropdownProps { ...@@ -87,9 +91,10 @@ interface SearchBarDropdownProps {
tokens: FungibleToken[] tokens: FungibleToken[]
collections: GenieCollection[] collections: GenieCollection[]
hasInput: boolean 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 [hoveredIndex, setHoveredIndex] = useState<number | undefined>(0)
const searchHistory = useSearchHistory( const searchHistory = useSearchHistory(
(state: { history: (FungibleToken | GenieCollection)[] }) => state.history (state: { history: (FungibleToken | GenieCollection)[] }) => state.history
...@@ -98,6 +103,7 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }: ...@@ -98,6 +103,7 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
const isNFTPage = pathname.includes('/nfts') const isNFTPage = pathname.includes('/nfts')
const isTokenPage = pathname.includes('/tokens') const isTokenPage = pathname.includes('/tokens')
const phase1Flag = useNftFlag() const phase1Flag = useNftFlag()
const [resultsState, setResultsState] = useState<ReactNode>()
const tokenSearchResults = const tokenSearchResults =
tokens.length > 0 ? ( tokens.length > 0 ? (
...@@ -131,14 +137,16 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }: ...@@ -131,14 +137,16 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
) )
) : null ) : null
const { data: trendingCollectionResults } = useQuery(['trendingCollections', 'eth', 'twenty_four_hours'], () => const { data: trendingCollectionResults, isLoading: trendingCollectionsAreLoading } = useQuery(
fetchTrendingCollections({ volumeType: 'eth', timePeriod: 'ONE_DAY' as TimePeriod, size: 3 }) ['trendingCollections', 'eth', 'twenty_four_hours'],
() => fetchTrendingCollections({ volumeType: 'eth', timePeriod: 'ONE_DAY' as TimePeriod, size: 3 })
) )
const trendingCollections = useMemo(() => { const trendingCollections = useMemo(
return trendingCollectionResults () =>
?.map((collection) => { trendingCollectionResults
return { ? trendingCollectionResults
.map((collection) => ({
...collection, ...collection,
collectionAddress: collection.address, collectionAddress: collection.address,
floorPrice: formatEthPrice(collection.floor?.toString()), floorPrice: formatEthPrice(collection.floor?.toString()),
...@@ -146,27 +154,31 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }: ...@@ -146,27 +154,31 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
total_supply: collection.totalSupply, total_supply: collection.totalSupply,
one_day_change: collection.floorChange, one_day_change: collection.floorChange,
}, },
} }))
})
.slice(0, isNFTPage ? 3 : 2) .slice(0, isNFTPage ? 3 : 2)
}, [isNFTPage, trendingCollectionResults]) : [...Array<GenieCollection>(isNFTPage ? 3 : 2)],
[isNFTPage, trendingCollectionResults]
const showTrendingCollections: boolean = useMemo(
() => (trendingCollections?.length ?? 0) > 0 && !isTokenPage && phase1Flag === NftVariant.Enabled,
[trendingCollections?.length, isTokenPage, phase1Flag]
) )
const { data: trendingTokenResults } = useQuery([], () => fetchTrendingTokens(4), { const { data: trendingTokenResults, isLoading: trendingTokensAreLoading } = useQuery(
['trendingTokens'],
() => fetchTrendingTokens(4),
{
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
refetchOnMount: false, refetchOnMount: false,
refetchOnReconnect: false, refetchOnReconnect: false,
}) }
)
const trendingTokensLength = phase1Flag === NftVariant.Enabled ? (isTokenPage ? 3 : 2) : 4 const trendingTokensLength = phase1Flag === NftVariant.Enabled ? (isTokenPage ? 3 : 2) : 4
const trendingTokens = useMemo(() => { const trendingTokens = useMemo(
return trendingTokenResults?.slice(0, trendingTokensLength) () =>
}, [trendingTokenResults, trendingTokensLength]) trendingTokenResults
? trendingTokenResults.slice(0, trendingTokensLength)
: [...Array<FungibleToken>(trendingTokensLength)],
[trendingTokenResults, trendingTokensLength]
)
const totalSuggestions = hasInput const totalSuggestions = hasInput
? tokens.length + collections.length ? tokens.length + collections.length
...@@ -201,9 +213,10 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }: ...@@ -201,9 +213,10 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
} }
}, [toggleOpen, hoveredIndex, totalSuggestions]) }, [toggleOpen, hoveredIndex, totalSuggestions])
return ( useEffect(() => {
<Box className={styles.searchBarDropdown}> if (!isLoading) {
{hasInput ? ( const currentState = () =>
hasInput ? (
// Empty or Up to 8 combined tokens and nfts // Empty or Up to 8 combined tokens and nfts
<Column gap="20"> <Column gap="20">
{isNFTPage ? ( {isNFTPage ? (
...@@ -232,18 +245,19 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }: ...@@ -232,18 +245,19 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
headerIcon={<ClockIcon />} headerIcon={<ClockIcon />}
/> />
)} )}
{(trendingTokens?.length ?? 0) > 0 && !isNFTPage && ( {!isNFTPage && (
<SearchBarDropdownSection <SearchBarDropdownSection
hoveredIndex={hoveredIndex} hoveredIndex={hoveredIndex}
startingIndex={searchHistory.length} startingIndex={searchHistory.length}
setHoveredIndex={setHoveredIndex} setHoveredIndex={setHoveredIndex}
toggleOpen={toggleOpen} toggleOpen={toggleOpen}
suggestions={trendingTokens ?? []} suggestions={trendingTokens}
header={<Trans>Popular tokens</Trans>} header={<Trans>Popular tokens</Trans>}
headerIcon={<TrendingArrow />} headerIcon={<TrendingArrow />}
isLoading={trendingTokensAreLoading}
/> />
)} )}
{showTrendingCollections && ( {!isTokenPage && phase1Flag === NftVariant.Enabled && (
<SearchBarDropdownSection <SearchBarDropdownSection
hoveredIndex={hoveredIndex} hoveredIndex={hoveredIndex}
startingIndex={searchHistory.length + (isNFTPage ? 0 : trendingTokens?.length ?? 0)} startingIndex={searchHistory.length + (isNFTPage ? 0 : trendingTokens?.length ?? 0)}
...@@ -252,10 +266,37 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }: ...@@ -252,10 +266,37 @@ export const SearchBarDropdown = ({ toggleOpen, tokens, collections, hasInput }:
suggestions={trendingCollections as unknown as GenieCollection[]} suggestions={trendingCollections as unknown as GenieCollection[]}
header={<Trans>Popular NFT collections</Trans>} header={<Trans>Popular NFT collections</Trans>}
headerIcon={<TrendingArrow />} headerIcon={<TrendingArrow />}
isLoading={trendingCollectionsAreLoading}
/> />
)} )}
</Column> </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}>
<Box opacity={isLoading ? '0.3' : '1'} transition="125">
{resultsState}
</Box>
</Box> </Box>
) )
} }
...@@ -380,14 +421,13 @@ export const SearchBar = () => { ...@@ -380,14 +421,13 @@ export const SearchBar = () => {
/> />
</Row> </Row>
<Box className={clsx(isOpen ? styles.visible : styles.hidden)}> <Box className={clsx(isOpen ? styles.visible : styles.hidden)}>
{debouncedSearchValue.length > 0 && (tokensAreLoading || collectionsAreLoading) ? ( {isOpen && (
<SkeletonRow />
) : (
<SearchBarDropdown <SearchBarDropdown
toggleOpen={toggleOpen} toggleOpen={toggleOpen}
tokens={reducedTokens} tokens={reducedTokens}
collections={reducedCollections} collections={reducedCollections}
hasInput={debouncedSearchValue.length > 0} hasInput={debouncedSearchValue.length > 0}
isLoading={tokensAreLoading || (collectionsAreLoading && phase1Flag === NftVariant.Enabled)}
/> />
)} )}
</Box> </Box>
......
...@@ -175,13 +175,21 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index ...@@ -175,13 +175,21 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index
export const SkeletonRow = () => { export const SkeletonRow = () => {
return ( return (
<Box className={styles.searchBarDropdown}>
<Row className={styles.suggestionRow}> <Row className={styles.suggestionRow}>
<Row> <Row width="full">
<Box className={styles.imageHolder} /> <Box className={styles.imageHolder} />
<Box borderRadius="round" height="16" width="160" background="loading" /> <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> </Row>
</Row> </Row>
</Box>
) )
} }
...@@ -24,6 +24,7 @@ const themeContractValues = { ...@@ -24,6 +24,7 @@ const themeContractValues = {
magicGradient: '', magicGradient: '',
placeholder: '', placeholder: '',
lightGrayButton: '', lightGrayButton: '',
loading: '',
// Opacities of black and white // Opacities of black and white
white95: '', white95: '',
...@@ -55,6 +56,7 @@ const dimensions = { ...@@ -55,6 +56,7 @@ const dimensions = {
'2': '2', '2': '2',
'4': '4px', '4': '4px',
'8': '8px', '8': '8px',
'12': '12px',
'16': '16px', '16': '16px',
'18': '18px', '18': '18px',
'20': '20px', '20': '20px',
...@@ -142,7 +144,6 @@ export const vars = createGlobalTheme(':root', { ...@@ -142,7 +144,6 @@ export const vars = createGlobalTheme(':root', {
transculent: '#7F7F7F', transculent: '#7F7F7F',
transparent: 'transparent', transparent: 'transparent',
none: 'none', none: 'none',
loading: '#7C85A24D',
// new uniswap colors: // new uniswap colors:
blue400: '#4C82FB', blue400: '#4C82FB',
...@@ -153,6 +154,7 @@ export const vars = createGlobalTheme(':root', { ...@@ -153,6 +154,7 @@ export const vars = createGlobalTheme(':root', {
green200: '#5CFE9D', green200: '#5CFE9D',
green400: '#1A9550', green400: '#1A9550',
grey900: '#0E111A', grey900: '#0E111A',
grey800: '#141B2B',
grey700: '#293249', grey700: '#293249',
grey500: '#5E6887', grey500: '#5E6887',
grey400: '#7C85A2', grey400: '#7C85A2',
...@@ -298,7 +300,7 @@ const layoutStyles = defineProperties({ ...@@ -298,7 +300,7 @@ const layoutStyles = defineProperties({
position: ['absolute', 'fixed', 'relative', 'sticky', 'static'], position: ['absolute', 'fixed', 'relative', 'sticky', 'static'],
objectFit: ['contain', 'cover'], objectFit: ['contain', 'cover'],
order: [0, 1], order: [0, 1],
opacity: ['auto', '0', '1'], opacity: ['auto', '0', '0.3', '0.5', '0.7', '1'],
} as const, } as const,
shorthands: { shorthands: {
paddingX: ['paddingLeft', 'paddingRight'], paddingX: ['paddingLeft', 'paddingRight'],
......
...@@ -22,6 +22,7 @@ export const darkTheme: Theme = { ...@@ -22,6 +22,7 @@ export const darkTheme: Theme = {
magicGradient: vars.color.blue400, magicGradient: vars.color.blue400,
placeholder: vars.color.grey400, placeholder: vars.color.grey400,
lightGrayButton: vars.color.grey700, lightGrayButton: vars.color.grey700,
loading: vars.color.grey800,
// Opacities of black and white // Opacities of black and white
white95: '#0E111AF2', white95: '#0E111AF2',
......
...@@ -22,6 +22,7 @@ export const lightTheme: Theme = { ...@@ -22,6 +22,7 @@ export const lightTheme: Theme = {
magicGradient: vars.color.pink400, magicGradient: vars.color.pink400,
placeholder: vars.color.grey300, placeholder: vars.color.grey300,
lightGrayButton: vars.color.grey100, lightGrayButton: vars.color.grey100,
loading: vars.color.grey50,
// Opacities of black and white // Opacities of black and white
white95: '#EDEFF7F2', white95: '#EDEFF7F2',
......
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