feat: [info] add explore page (#7381)
* feat: add explore page * add explore filter options * add search bar defocus * use Tokens Tab state for filters * flag-gate /tokens * filter bar css * remove duplicate Explore page, use flag instead * fixes * create tabbednav interface * rename Tokens to Explore * simplify routing * nit * padding nit * pr review * fix menu flyouts * fix TDP nav * add analytics events + ui updates + pr review * nit * nit * add small comment * menu flyout nit * fix merge conflict * min width expand menu flyouts * fix redirects * update routing snapshot * nit css * oops * breakpoints * fix tab routing * pr review * add better tab dynamic routing * fix redirects * error handle edge urls * further fix routing * define return val for useExploreParams * Update snapshot
This commit is contained in:
parent
9d439e7f62
commit
ed6afb50de
@ -194,7 +194,7 @@
|
|||||||
"@types/react-helmet": "^6.1.7",
|
"@types/react-helmet": "^6.1.7",
|
||||||
"@types/react-window-infinite-loader": "^1.0.6",
|
"@types/react-window-infinite-loader": "^1.0.6",
|
||||||
"@uniswap/analytics": "1.5.0",
|
"@uniswap/analytics": "1.5.0",
|
||||||
"@uniswap/analytics-events": "^2.24.0",
|
"@uniswap/analytics-events": "^2.25.0",
|
||||||
"@uniswap/governance": "^1.0.2",
|
"@uniswap/governance": "^1.0.2",
|
||||||
"@uniswap/liquidity-staker": "^1.0.2",
|
"@uniswap/liquidity-staker": "^1.0.2",
|
||||||
"@uniswap/merkle-distributor": "^1.0.1",
|
"@uniswap/merkle-distributor": "^1.0.1",
|
||||||
|
@ -3,6 +3,7 @@ import { TraceEvent } from 'analytics'
|
|||||||
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
|
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
|
||||||
import Row from 'components/Row'
|
import Row from 'components/Row'
|
||||||
import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
|
import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
|
||||||
|
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||||
import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
|
import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
|
||||||
import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util'
|
import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util'
|
||||||
import { useAtomValue } from 'jotai/utils'
|
import { useAtomValue } from 'jotai/utils'
|
||||||
@ -76,10 +77,12 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
|
|||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||||
|
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||||
|
|
||||||
const navigateToTokenDetails = useCallback(async () => {
|
const navigateToTokenDetails = useCallback(async () => {
|
||||||
navigate(getTokenDetailsURL(token))
|
navigate(getTokenDetailsURL({ ...token, isInfoExplorePageEnabled }))
|
||||||
toggleWalletDrawer()
|
toggleWalletDrawer()
|
||||||
}, [navigate, token, toggleWalletDrawer])
|
}, [navigate, token, isInfoExplorePageEnabled, toggleWalletDrawer])
|
||||||
const { formatNumber } = useFormatter()
|
const { formatNumber } = useFormatter()
|
||||||
|
|
||||||
const currency = gqlToCurrency(token)
|
const currency = gqlToCurrency(token)
|
||||||
|
@ -4,6 +4,7 @@ import clsx from 'clsx'
|
|||||||
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
|
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
|
||||||
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
|
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
|
||||||
import { checkSearchTokenWarning } from 'constants/tokenSafety'
|
import { checkSearchTokenWarning } from 'constants/tokenSafety'
|
||||||
|
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||||
import { Chain, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
|
import { Chain, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||||
import { SearchToken } from 'graphql/data/SearchTokens'
|
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||||
import { getTokenDetailsURL } from 'graphql/data/util'
|
import { getTokenDetailsURL } from 'graphql/data/util'
|
||||||
@ -138,7 +139,9 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
|
|||||||
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
||||||
}, [addRecentlySearchedAsset, token, toggleOpen, eventProperties])
|
}, [addRecentlySearchedAsset, token, toggleOpen, eventProperties])
|
||||||
|
|
||||||
const tokenDetailsPath = getTokenDetailsURL(token)
|
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||||
|
|
||||||
|
const tokenDetailsPath = getTokenDetailsURL({ ...token, isInfoExplorePageEnabled })
|
||||||
// Close the modal on escape
|
// Close the modal on escape
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const keyDownHandler = (event: KeyboardEvent) => {
|
const keyDownHandler = (event: KeyboardEvent) => {
|
||||||
|
@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro'
|
|||||||
import { useWeb3React } from '@web3-react/core'
|
import { useWeb3React } from '@web3-react/core'
|
||||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||||
import Web3Status from 'components/Web3Status'
|
import Web3Status from 'components/Web3Status'
|
||||||
|
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||||
import { chainIdToBackendName } from 'graphql/data/util'
|
import { chainIdToBackendName } from 'graphql/data/util'
|
||||||
import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
|
import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
|
||||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||||
@ -61,15 +62,22 @@ export const PageTabs = () => {
|
|||||||
const isNftPage = useIsNftPage()
|
const isNftPage = useIsNftPage()
|
||||||
|
|
||||||
const shouldDisableNFTRoutes = useDisableNFTRoutes()
|
const shouldDisableNFTRoutes = useDisableNFTRoutes()
|
||||||
|
const infoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuItem href="/swap" isActive={pathname.startsWith('/swap')}>
|
<MenuItem href="/swap" isActive={pathname.startsWith('/swap')}>
|
||||||
<Trans>Swap</Trans>
|
<Trans>Swap</Trans>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{infoExplorePageEnabled ? (
|
||||||
|
<MenuItem href={`/explore/tokens/${chainName.toLowerCase()}`} isActive={pathname.startsWith('/explore')}>
|
||||||
|
<Trans>Explore</Trans>
|
||||||
|
</MenuItem>
|
||||||
|
) : (
|
||||||
<MenuItem href={`/tokens/${chainName.toLowerCase()}`} isActive={pathname.startsWith('/tokens')}>
|
<MenuItem href={`/tokens/${chainName.toLowerCase()}`} isActive={pathname.startsWith('/tokens')}>
|
||||||
<Trans>Tokens</Trans>
|
<Trans>Tokens</Trans>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
)}
|
||||||
{!shouldDisableNFTRoutes && (
|
{!shouldDisableNFTRoutes && (
|
||||||
<MenuItem dataTestId="nft-nav" href="/nfts" isActive={isNftPage}>
|
<MenuItem dataTestId="nft-nav" href="/nfts" isActive={isNftPage}>
|
||||||
<Trans>NFTs</Trans>
|
<Trans>NFTs</Trans>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { SwapSkeleton } from 'components/swap/SwapSkeleton'
|
import { SwapSkeleton } from 'components/swap/SwapSkeleton'
|
||||||
|
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||||
import { ArrowLeft } from 'react-feather'
|
import { ArrowLeft } from 'react-feather'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import styled, { useTheme } from 'styled-components'
|
import styled, { useTheme } from 'styled-components'
|
||||||
@ -220,9 +221,12 @@ function LoadingStats() {
|
|||||||
/* Loading State: row component with loading bubbles */
|
/* Loading State: row component with loading bubbles */
|
||||||
export default function TokenDetailsSkeleton() {
|
export default function TokenDetailsSkeleton() {
|
||||||
const { chainName } = useParams<{ chainName?: string }>()
|
const { chainName } = useParams<{ chainName?: string }>()
|
||||||
|
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||||
return (
|
return (
|
||||||
<LeftPanel>
|
<LeftPanel>
|
||||||
<BreadcrumbNavLink to={chainName ? `/tokens/${chainName}` : `/explore`}>
|
<BreadcrumbNavLink
|
||||||
|
to={(isInfoExplorePageEnabled ? '/explore' : '') + (chainName ? `/tokens/${chainName}` : `/tokens`)}
|
||||||
|
>
|
||||||
<ArrowLeft size={14} /> Tokens
|
<ArrowLeft size={14} /> Tokens
|
||||||
</BreadcrumbNavLink>
|
</BreadcrumbNavLink>
|
||||||
<TokenInfoContainer>
|
<TokenInfoContainer>
|
||||||
|
@ -23,6 +23,7 @@ import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
|
|||||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||||
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
|
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
|
||||||
import { checkWarning } from 'constants/tokenSafety'
|
import { checkWarning } from 'constants/tokenSafety'
|
||||||
|
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||||
import { useInfoTDPEnabled } from 'featureFlags/flags/infoTDP'
|
import { useInfoTDPEnabled } from 'featureFlags/flags/infoTDP'
|
||||||
import { TokenPriceQuery } from 'graphql/data/__generated__/types-and-hooks'
|
import { TokenPriceQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||||
import { Chain, TokenQuery, TokenQueryData } from 'graphql/data/Token'
|
import { Chain, TokenQuery, TokenQueryData } from 'graphql/data/Token'
|
||||||
@ -137,6 +138,8 @@ export default function TokenDetails({
|
|||||||
const isBlockedToken = tokenWarning?.canProceed === false
|
const isBlockedToken = tokenWarning?.canProceed === false
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||||
|
|
||||||
// Wrapping navigate in a transition prevents Suspense from unnecessarily showing fallbacks again.
|
// Wrapping navigate in a transition prevents Suspense from unnecessarily showing fallbacks again.
|
||||||
const [isPending, startTokenTransition] = useTransition()
|
const [isPending, startTokenTransition] = useTransition()
|
||||||
const navigateToTokenForChain = useCallback(
|
const navigateToTokenForChain = useCallback(
|
||||||
@ -144,12 +147,20 @@ export default function TokenDetails({
|
|||||||
if (!address) return
|
if (!address) return
|
||||||
const bridgedAddress = crossChainMap[update]
|
const bridgedAddress = crossChainMap[update]
|
||||||
if (bridgedAddress) {
|
if (bridgedAddress) {
|
||||||
startTokenTransition(() => navigate(getTokenDetailsURL({ address: bridgedAddress, chain: update })))
|
startTokenTransition(() =>
|
||||||
|
navigate(
|
||||||
|
getTokenDetailsURL({
|
||||||
|
address: bridgedAddress,
|
||||||
|
chain: update,
|
||||||
|
isInfoExplorePageEnabled,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
} else if (didFetchFromChain || detailedToken?.isNative) {
|
} else if (didFetchFromChain || detailedToken?.isNative) {
|
||||||
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain: update })))
|
startTokenTransition(() => navigate(getTokenDetailsURL({ address, chain: update, isInfoExplorePageEnabled })))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[address, crossChainMap, didFetchFromChain, navigate, detailedToken?.isNative]
|
[address, crossChainMap, didFetchFromChain, detailedToken?.isNative, navigate, isInfoExplorePageEnabled]
|
||||||
)
|
)
|
||||||
useOnGlobalChainSwitch(navigateToTokenForChain)
|
useOnGlobalChainSwitch(navigateToTokenForChain)
|
||||||
|
|
||||||
@ -175,11 +186,12 @@ export default function TokenDetails({
|
|||||||
tokens[Field.INPUT] && tokens[Field.INPUT]?.currencyId !== newDefaultTokenID
|
tokens[Field.INPUT] && tokens[Field.INPUT]?.currencyId !== newDefaultTokenID
|
||||||
? tokens[Field.INPUT]?.currencyId
|
? tokens[Field.INPUT]?.currencyId
|
||||||
: null,
|
: null,
|
||||||
|
isInfoExplorePageEnabled,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[address, chain, navigate]
|
[address, chain, isInfoExplorePageEnabled, navigate]
|
||||||
)
|
)
|
||||||
|
|
||||||
const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike<boolean>) => void }>()
|
const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike<boolean>) => void }>()
|
||||||
@ -207,7 +219,7 @@ export default function TokenDetails({
|
|||||||
<TokenDetailsLayout>
|
<TokenDetailsLayout>
|
||||||
{detailedToken && !isPending ? (
|
{detailedToken && !isPending ? (
|
||||||
<LeftPanel>
|
<LeftPanel>
|
||||||
<BreadcrumbNavLink to={`/tokens/${chain.toLowerCase()}`}>
|
<BreadcrumbNavLink to={`${isInfoExplorePageEnabled ? '/explore' : ''}/tokens/${chain.toLowerCase()}`}>
|
||||||
<ArrowLeft data-testid="token-details-return-button" size={14} /> Tokens
|
<ArrowLeft data-testid="token-details-return-button" size={14} /> Tokens
|
||||||
</BreadcrumbNavLink>
|
</BreadcrumbNavLink>
|
||||||
<TokenInfoContainer data-testid="token-info-container">
|
<TokenInfoContainer data-testid="token-info-container">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Badge from 'components/Badge'
|
import Badge from 'components/Badge'
|
||||||
import { ChainLogo } from 'components/Logo/ChainLogo'
|
import { ChainLogo } from 'components/Logo/ChainLogo'
|
||||||
import { getChainInfo } from 'constants/chainInfo'
|
import { getChainInfo } from 'constants/chainInfo'
|
||||||
|
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||||
import {
|
import {
|
||||||
BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS,
|
BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS,
|
||||||
BACKEND_SUPPORTED_CHAINS,
|
BACKEND_SUPPORTED_CHAINS,
|
||||||
@ -8,6 +9,7 @@ import {
|
|||||||
validateUrlChainParam,
|
validateUrlChainParam,
|
||||||
} from 'graphql/data/util'
|
} from 'graphql/data/util'
|
||||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||||
|
import { useExploreParams } from 'pages/Explore/redirects'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import { Check, ChevronDown, ChevronUp } from 'react-feather'
|
import { Check, ChevronDown, ChevronUp } from 'react-feather'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
@ -48,8 +50,8 @@ const InternalLinkMenuItem = styled(InternalMenuItem)<{ disabled?: boolean }>`
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
`}
|
`}
|
||||||
`
|
`
|
||||||
const MenuTimeFlyout = styled.span`
|
const MenuTimeFlyout = styled.span<{ isInfoExplorePageEnabled: boolean }>`
|
||||||
min-width: 240px;
|
min-width: ${({ isInfoExplorePageEnabled }) => (isInfoExplorePageEnabled ? '150px' : '240px')};
|
||||||
max-height: 350px;
|
max-height: 350px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background-color: ${({ theme }) => theme.surface1};
|
background-color: ${({ theme }) => theme.surface1};
|
||||||
@ -63,7 +65,18 @@ const MenuTimeFlyout = styled.span`
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
top: 48px;
|
top: 48px;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
||||||
|
${({ isInfoExplorePageEnabled }) =>
|
||||||
|
isInfoExplorePageEnabled
|
||||||
|
? css`
|
||||||
|
right: 0px;
|
||||||
|
@media screen and (max-width: ${({ theme }) => `${theme.breakpoint.lg}px`}) {
|
||||||
left: 0px;
|
left: 0px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
: css`
|
||||||
|
left: 0px;
|
||||||
|
`}
|
||||||
`
|
`
|
||||||
const StyledMenu = styled.div`
|
const StyledMenu = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -96,8 +109,8 @@ const CheckContainer = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: flex-end;
|
flex-direction: flex-end;
|
||||||
`
|
`
|
||||||
const NetworkFilterOption = styled(FilterOption)`
|
const NetworkFilterOption = styled(FilterOption)<{ isInfoExplorePageEnabled: boolean }>`
|
||||||
min-width: 156px;
|
${({ isInfoExplorePageEnabled }) => !isInfoExplorePageEnabled && 'min-width: 156px;'}
|
||||||
`
|
`
|
||||||
const Tag = styled(Badge)`
|
const Tag = styled(Badge)`
|
||||||
background-color: ${({ theme }) => theme.surface2};
|
background-color: ${({ theme }) => theme.surface2};
|
||||||
@ -114,6 +127,9 @@ export default function NetworkFilter() {
|
|||||||
const toggleMenu = useToggleModal(ApplicationModal.NETWORK_FILTER)
|
const toggleMenu = useToggleModal(ApplicationModal.NETWORK_FILTER)
|
||||||
useOnClickOutside(node, open ? toggleMenu : undefined)
|
useOnClickOutside(node, open ? toggleMenu : undefined)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { tab } = useExploreParams()
|
||||||
|
|
||||||
|
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||||
|
|
||||||
const currentChainName = validateUrlChainParam(useParams().chainName)
|
const currentChainName = validateUrlChainParam(useParams().chainName)
|
||||||
const chainId = supportedChainIdFromGQLChain(currentChainName)
|
const chainId = supportedChainIdFromGQLChain(currentChainName)
|
||||||
@ -123,6 +139,7 @@ export default function NetworkFilter() {
|
|||||||
return (
|
return (
|
||||||
<StyledMenu ref={node}>
|
<StyledMenu ref={node}>
|
||||||
<NetworkFilterOption
|
<NetworkFilterOption
|
||||||
|
isInfoExplorePageEnabled={isInfoExplorePageEnabled}
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
aria-label="networkFilter"
|
aria-label="networkFilter"
|
||||||
active={open}
|
active={open}
|
||||||
@ -130,7 +147,7 @@ export default function NetworkFilter() {
|
|||||||
>
|
>
|
||||||
<StyledMenuContent>
|
<StyledMenuContent>
|
||||||
<NetworkLabel>
|
<NetworkLabel>
|
||||||
<ChainLogo chainId={chainId} size={20} /> {chainInfo.label}
|
<ChainLogo chainId={chainId} size={20} /> {!isInfoExplorePageEnabled && chainInfo.label}
|
||||||
</NetworkLabel>
|
</NetworkLabel>
|
||||||
<Chevron open={open}>
|
<Chevron open={open}>
|
||||||
{open ? (
|
{open ? (
|
||||||
@ -142,7 +159,7 @@ export default function NetworkFilter() {
|
|||||||
</StyledMenuContent>
|
</StyledMenuContent>
|
||||||
</NetworkFilterOption>
|
</NetworkFilterOption>
|
||||||
{open && (
|
{open && (
|
||||||
<MenuTimeFlyout>
|
<MenuTimeFlyout isInfoExplorePageEnabled={isInfoExplorePageEnabled}>
|
||||||
{BACKEND_SUPPORTED_CHAINS.map((network) => {
|
{BACKEND_SUPPORTED_CHAINS.map((network) => {
|
||||||
const chainId = supportedChainIdFromGQLChain(network)
|
const chainId = supportedChainIdFromGQLChain(network)
|
||||||
const chainInfo = getChainInfo(chainId)
|
const chainInfo = getChainInfo(chainId)
|
||||||
@ -151,7 +168,9 @@ export default function NetworkFilter() {
|
|||||||
key={network}
|
key={network}
|
||||||
data-testid={`tokens-network-filter-option-${network.toLowerCase()}`}
|
data-testid={`tokens-network-filter-option-${network.toLowerCase()}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`/tokens/${network.toLowerCase()}`)
|
isInfoExplorePageEnabled
|
||||||
|
? navigate(`/explore/${tab}/${network.toLowerCase()}`)
|
||||||
|
: navigate(`/tokens/${network.toLowerCase()}`)
|
||||||
toggleMenu()
|
toggleMenu()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -3,6 +3,7 @@ import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap
|
|||||||
import { TraceEvent } from 'analytics'
|
import { TraceEvent } from 'analytics'
|
||||||
import searchIcon from 'assets/svg/search.svg'
|
import searchIcon from 'assets/svg/search.svg'
|
||||||
import xIcon from 'assets/svg/x.svg'
|
import xIcon from 'assets/svg/x.svg'
|
||||||
|
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||||
import useDebounce from 'hooks/useDebounce'
|
import useDebounce from 'hooks/useDebounce'
|
||||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@ -12,11 +13,12 @@ import { MEDIUM_MEDIA_BREAKPOINT } from '../constants'
|
|||||||
import { filterStringAtom } from '../state'
|
import { filterStringAtom } from '../state'
|
||||||
const ICON_SIZE = '20px'
|
const ICON_SIZE = '20px'
|
||||||
|
|
||||||
const SearchBarContainer = styled.div`
|
const SearchBarContainer = styled.div<{ isInfoExplorePageEnabled: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
${({ isInfoExplorePageEnabled }) => isInfoExplorePageEnabled && 'justify-content: flex-end;'}
|
||||||
`
|
`
|
||||||
const SearchInput = styled.input`
|
const SearchInput = styled.input<{ isInfoExplorePageEnabled: boolean; isOpen?: boolean }>`
|
||||||
background: no-repeat scroll 7px 7px;
|
background: no-repeat scroll 7px 7px;
|
||||||
background-image: url(${searchIcon});
|
background-image: url(${searchIcon});
|
||||||
background-size: 20px 20px;
|
background-size: 20px 20px;
|
||||||
@ -25,12 +27,14 @@ const SearchInput = styled.input`
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid ${({ theme }) => theme.surface3};
|
border: 1px solid ${({ theme }) => theme.surface3};
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: min(200px, 100%);
|
width: ${({ isInfoExplorePageEnabled, isOpen }) =>
|
||||||
|
isInfoExplorePageEnabled ? (isOpen ? '200px' : '0') : 'min(200px, 100%)'};
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 485;
|
font-weight: 485;
|
||||||
padding-left: 40px;
|
padding-left: 40px;
|
||||||
color: ${({ theme }) => theme.neutral2};
|
color: ${({ theme }) => theme.neutral2};
|
||||||
transition-duration: ${({ theme }) => theme.transition.duration.fast};
|
transition-duration: ${({ theme }) => theme.transition.duration.fast};
|
||||||
|
${(isInfoExplorePageEnabled) => isInfoExplorePageEnabled && 'text-overflow: ellipsis;'}
|
||||||
|
|
||||||
:hover {
|
:hover {
|
||||||
background-color: ${({ theme }) => theme.surface1};
|
background-color: ${({ theme }) => theme.surface1};
|
||||||
@ -58,15 +62,18 @@ const SearchInput = styled.input`
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
||||||
width: 100%;
|
width: ${({ isInfoExplorePageEnabled, isOpen }) =>
|
||||||
|
isInfoExplorePageEnabled ? (isOpen ? 'min(100%, 200px)' : '0') : '100%'};
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
export default function SearchBar() {
|
export default function SearchBar({ tab }: { tab?: string }) {
|
||||||
const currentString = useAtomValue(filterStringAtom)
|
const currentString = useAtomValue(filterStringAtom)
|
||||||
const [localFilterString, setLocalFilterString] = useState(currentString)
|
const [localFilterString, setLocalFilterString] = useState(currentString)
|
||||||
const setFilterString = useUpdateAtom(filterStringAtom)
|
const setFilterString = useUpdateAtom(filterStringAtom)
|
||||||
const debouncedLocalFilterString = useDebounce(localFilterString, 300)
|
const debouncedLocalFilterString = useDebounce(localFilterString, 300)
|
||||||
|
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalFilterString(currentString)
|
setLocalFilterString(currentString)
|
||||||
@ -76,8 +83,14 @@ export default function SearchBar() {
|
|||||||
setFilterString(debouncedLocalFilterString)
|
setFilterString(debouncedLocalFilterString)
|
||||||
}, [debouncedLocalFilterString, setFilterString])
|
}, [debouncedLocalFilterString, setFilterString])
|
||||||
|
|
||||||
|
const handleFocus = () => setIsOpen(true)
|
||||||
|
|
||||||
|
const handleBlur = () => {
|
||||||
|
if (localFilterString === '') setIsOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SearchBarContainer>
|
<SearchBarContainer isInfoExplorePageEnabled={isInfoExplorePageEnabled}>
|
||||||
<Trans
|
<Trans
|
||||||
render={({ translation }) => (
|
render={({ translation }) => (
|
||||||
<TraceEvent
|
<TraceEvent
|
||||||
@ -86,6 +99,7 @@ export default function SearchBar() {
|
|||||||
element={InterfaceElementName.EXPLORE_SEARCH_INPUT}
|
element={InterfaceElementName.EXPLORE_SEARCH_INPUT}
|
||||||
>
|
>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
|
isInfoExplorePageEnabled={isInfoExplorePageEnabled}
|
||||||
data-cy="explore-tokens-search-input"
|
data-cy="explore-tokens-search-input"
|
||||||
type="search"
|
type="search"
|
||||||
placeholder={`${translation}`}
|
placeholder={`${translation}`}
|
||||||
@ -93,11 +107,14 @@ export default function SearchBar() {
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
value={localFilterString}
|
value={localFilterString}
|
||||||
onChange={({ target: { value } }) => setLocalFilterString(value)}
|
onChange={({ target: { value } }) => setLocalFilterString(value)}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onFocus={isInfoExplorePageEnabled ? handleFocus : undefined}
|
||||||
|
onBlur={isInfoExplorePageEnabled ? handleBlur : undefined}
|
||||||
/>
|
/>
|
||||||
</TraceEvent>
|
</TraceEvent>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Filter tokens
|
{isInfoExplorePageEnabled ? (tab === 'tokens' ? 'Search tokens' : 'Search pools') : 'Filter tokens'}
|
||||||
</Trans>
|
</Trans>
|
||||||
</SearchBarContainer>
|
</SearchBarContainer>
|
||||||
)
|
)
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Trans } from '@lingui/macro'
|
||||||
|
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||||
import { TimePeriod } from 'graphql/data/util'
|
import { TimePeriod } from 'graphql/data/util'
|
||||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
@ -5,7 +7,7 @@ import { useRef } from 'react'
|
|||||||
import { Check, ChevronDown, ChevronUp } from 'react-feather'
|
import { Check, ChevronDown, ChevronUp } from 'react-feather'
|
||||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||||
import { ApplicationModal } from 'state/application/reducer'
|
import { ApplicationModal } from 'state/application/reducer'
|
||||||
import styled, { useTheme } from 'styled-components'
|
import styled, { css, useTheme } from 'styled-components'
|
||||||
|
|
||||||
import { MOBILE_MEDIA_BREAKPOINT, SMALL_MEDIA_BREAKPOINT } from '../constants'
|
import { MOBILE_MEDIA_BREAKPOINT, SMALL_MEDIA_BREAKPOINT } from '../constants'
|
||||||
import { filterTimeAtom } from '../state'
|
import { filterTimeAtom } from '../state'
|
||||||
@ -52,8 +54,8 @@ const InternalLinkMenuItem = styled(InternalMenuItem)`
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
const MenuTimeFlyout = styled.span`
|
const MenuTimeFlyout = styled.span<{ isInfoExplorePageEnabled: boolean }>`
|
||||||
min-width: 240px;
|
min-width: ${({ isInfoExplorePageEnabled }) => (isInfoExplorePageEnabled ? '150px' : '240px')};
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
background-color: ${({ theme }) => theme.surface1};
|
background-color: ${({ theme }) => theme.surface1};
|
||||||
@ -69,12 +71,16 @@ const MenuTimeFlyout = styled.span`
|
|||||||
z-index: 100;
|
z-index: 100;
|
||||||
left: 0px;
|
left: 0px;
|
||||||
|
|
||||||
|
${({ isInfoExplorePageEnabled }) =>
|
||||||
|
!isInfoExplorePageEnabled &&
|
||||||
|
css`
|
||||||
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
|
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
|
||||||
right: 0px;
|
|
||||||
left: unset;
|
left: unset;
|
||||||
|
right: 0px;
|
||||||
}
|
}
|
||||||
|
`}
|
||||||
`
|
`
|
||||||
const StyledMenu = styled.div`
|
const StyledMenu = styled.div<{ isInfoExplorePageEnabled: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -82,11 +88,15 @@ const StyledMenu = styled.div`
|
|||||||
border: none;
|
border: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
|
${({ isInfoExplorePageEnabled }) =>
|
||||||
|
!isInfoExplorePageEnabled &&
|
||||||
|
css`
|
||||||
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
||||||
width: 72px;
|
width: 72px;
|
||||||
}
|
}
|
||||||
|
`}
|
||||||
`
|
`
|
||||||
const StyledMenuContent = styled.div`
|
const StyledMenuContent = styled.div<{ isInfoExplorePageEnabled: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@ -94,6 +104,7 @@ const StyledMenuContent = styled.div`
|
|||||||
border: none;
|
border: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
${({ isInfoExplorePageEnabled }) => isInfoExplorePageEnabled && 'white-space: nowrap;'}
|
||||||
`
|
`
|
||||||
const Chevron = styled.span<{ open: boolean }>`
|
const Chevron = styled.span<{ open: boolean }>`
|
||||||
padding-top: 1px;
|
padding-top: 1px;
|
||||||
@ -109,11 +120,19 @@ export default function TimeSelector() {
|
|||||||
useOnClickOutside(node, open ? toggleMenu : undefined)
|
useOnClickOutside(node, open ? toggleMenu : undefined)
|
||||||
const [activeTime, setTime] = useAtom(filterTimeAtom)
|
const [activeTime, setTime] = useAtom(filterTimeAtom)
|
||||||
|
|
||||||
|
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledMenu ref={node}>
|
<StyledMenu ref={node} isInfoExplorePageEnabled={isInfoExplorePageEnabled}>
|
||||||
<FilterOption onClick={toggleMenu} aria-label="timeSelector" active={open} data-testid="time-selector">
|
<FilterOption onClick={toggleMenu} aria-label="timeSelector" active={open} data-testid="time-selector">
|
||||||
<StyledMenuContent>
|
<StyledMenuContent isInfoExplorePageEnabled={isInfoExplorePageEnabled}>
|
||||||
{DISPLAYS[activeTime]}
|
{isInfoExplorePageEnabled ? (
|
||||||
|
<>
|
||||||
|
{DISPLAYS[activeTime]} <Trans>volume</Trans>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
DISPLAYS[activeTime]
|
||||||
|
)}
|
||||||
<Chevron open={open}>
|
<Chevron open={open}>
|
||||||
{open ? (
|
{open ? (
|
||||||
<ChevronUp width={20} height={15} viewBox="0 0 24 20" />
|
<ChevronUp width={20} height={15} viewBox="0 0 24 20" />
|
||||||
@ -124,7 +143,7 @@ export default function TimeSelector() {
|
|||||||
</StyledMenuContent>
|
</StyledMenuContent>
|
||||||
</FilterOption>
|
</FilterOption>
|
||||||
{open && (
|
{open && (
|
||||||
<MenuTimeFlyout>
|
<MenuTimeFlyout isInfoExplorePageEnabled={isInfoExplorePageEnabled}>
|
||||||
{ORDERED_TIMES.map((time) => (
|
{ORDERED_TIMES.map((time) => (
|
||||||
<InternalLinkMenuItem
|
<InternalLinkMenuItem
|
||||||
key={DISPLAYS[time]}
|
key={DISPLAYS[time]}
|
||||||
@ -134,7 +153,13 @@ export default function TimeSelector() {
|
|||||||
toggleMenu()
|
toggleMenu()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{isInfoExplorePageEnabled ? (
|
||||||
|
<div>
|
||||||
|
{DISPLAYS[time]} <Trans>volume</Trans>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div>{DISPLAYS[time]}</div>
|
<div>{DISPLAYS[time]}</div>
|
||||||
|
)}
|
||||||
{time === activeTime && <Check color={theme.accent1} size={16} />}
|
{time === activeTime && <Check color={theme.accent1} size={16} />}
|
||||||
</InternalLinkMenuItem>
|
</InternalLinkMenuItem>
|
||||||
))}
|
))}
|
||||||
|
@ -8,6 +8,7 @@ import { ArrowChangeUp } from 'components/Icons/ArrowChangeUp'
|
|||||||
import { Info } from 'components/Icons/Info'
|
import { Info } from 'components/Icons/Info'
|
||||||
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
|
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
|
||||||
import { MouseoverTooltip } from 'components/Tooltip'
|
import { MouseoverTooltip } from 'components/Tooltip'
|
||||||
|
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||||
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
|
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
|
||||||
import { getTokenDetailsURL, supportedChainIdFromGQLChain, validateUrlChainParam } from 'graphql/data/util'
|
import { getTokenDetailsURL, supportedChainIdFromGQLChain, validateUrlChainParam } from 'graphql/data/util'
|
||||||
import { useAtomValue } from 'jotai/utils'
|
import { useAtomValue } from 'jotai/utils'
|
||||||
@ -466,11 +467,13 @@ export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HT
|
|||||||
// A simple 0 price indicates the price is not currently available from the api
|
// A simple 0 price indicates the price is not currently available from the api
|
||||||
const price = token.market?.price?.value === 0 ? '-' : formatFiatPrice({ price: token.market?.price?.value })
|
const price = token.market?.price?.value === 0 ? '-' : formatFiatPrice({ price: token.market?.price?.value })
|
||||||
|
|
||||||
|
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||||
|
|
||||||
// TODO: currency logo sizing mobile (32px) vs. desktop (24px)
|
// TODO: currency logo sizing mobile (32px) vs. desktop (24px)
|
||||||
return (
|
return (
|
||||||
<div ref={ref} data-testid={`token-table-row-${token.address}`}>
|
<div ref={ref} data-testid={`token-table-row-${token.address}`}>
|
||||||
<StyledLink
|
<StyledLink
|
||||||
to={getTokenDetailsURL(token)}
|
to={getTokenDetailsURL({ ...token, isInfoExplorePageEnabled })}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
sendAnalyticsEvent(InterfaceEventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)
|
sendAnalyticsEvent(InterfaceEventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ChainId } from '@uniswap/sdk-core'
|
import { ChainId } from '@uniswap/sdk-core'
|
||||||
|
|
||||||
|
// Breakpoints specifically for the token pages
|
||||||
export const MAX_WIDTH_MEDIA_BREAKPOINT = '1200px'
|
export const MAX_WIDTH_MEDIA_BREAKPOINT = '1200px'
|
||||||
export const XLARGE_MEDIA_BREAKPOINT = '960px'
|
export const XLARGE_MEDIA_BREAKPOINT = '960px'
|
||||||
export const LARGE_MEDIA_BREAKPOINT = '840px'
|
export const LARGE_MEDIA_BREAKPOINT = '840px'
|
||||||
|
@ -3,3 +3,7 @@ import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
|||||||
export function useInfoExploreFlag(): BaseVariant {
|
export function useInfoExploreFlag(): BaseVariant {
|
||||||
return useBaseFlag(FeatureFlag.infoExplore)
|
return useBaseFlag(FeatureFlag.infoExplore)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useInfoExplorePageEnabled(): boolean {
|
||||||
|
return useInfoExploreFlag() === BaseVariant.Enabled
|
||||||
|
}
|
||||||
|
@ -207,15 +207,17 @@ export function getTokenDetailsURL({
|
|||||||
address,
|
address,
|
||||||
chain,
|
chain,
|
||||||
inputAddress,
|
inputAddress,
|
||||||
|
isInfoExplorePageEnabled,
|
||||||
}: {
|
}: {
|
||||||
address?: string | null
|
address?: string | null
|
||||||
chain: Chain
|
chain: Chain
|
||||||
inputAddress?: string | null
|
inputAddress?: string | null
|
||||||
|
isInfoExplorePageEnabled: boolean
|
||||||
}) {
|
}) {
|
||||||
const chainName = chain.toLowerCase()
|
const chainName = chain.toLowerCase()
|
||||||
const tokenAddress = address ?? NATIVE_CHAIN_ID
|
const tokenAddress = address ?? NATIVE_CHAIN_ID
|
||||||
const inputAddressSuffix = inputAddress ? `?inputCurrency=${inputAddress}` : ''
|
const inputAddressSuffix = inputAddress ? `?inputCurrency=${inputAddress}` : ''
|
||||||
return `/tokens/${chainName}/${tokenAddress}${inputAddressSuffix}`
|
return (isInfoExplorePageEnabled ? '/explore' : '') + `/tokens/${chainName}/${tokenAddress}${inputAddressSuffix}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unwrapToken<
|
export function unwrapToken<
|
||||||
|
246
src/pages/Explore/index.tsx
Normal file
246
src/pages/Explore/index.tsx
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import { Trans } from '@lingui/macro'
|
||||||
|
import { BrowserEvent, InterfaceElementName, InterfacePageName, SharedEventName } from '@uniswap/analytics-events'
|
||||||
|
import { TraceEvent } from 'analytics'
|
||||||
|
import { Trace } from 'analytics'
|
||||||
|
import { AutoRow } from 'components/Row'
|
||||||
|
import { MAX_WIDTH_MEDIA_BREAKPOINT, MEDIUM_MEDIA_BREAKPOINT } from 'components/Tokens/constants'
|
||||||
|
import { filterStringAtom } from 'components/Tokens/state'
|
||||||
|
import NetworkFilter from 'components/Tokens/TokenTable/NetworkFilter'
|
||||||
|
import SearchBar from 'components/Tokens/TokenTable/SearchBar'
|
||||||
|
import TimeSelector from 'components/Tokens/TokenTable/TimeSelector'
|
||||||
|
import TokenTable from 'components/Tokens/TokenTable/TokenTable'
|
||||||
|
import { MouseoverTooltip } from 'components/Tooltip'
|
||||||
|
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||||
|
import { useResetAtom } from 'jotai/utils'
|
||||||
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
import styled, { css } from 'styled-components'
|
||||||
|
import { ThemedText } from 'theme/components'
|
||||||
|
|
||||||
|
import { useExploreParams } from './redirects'
|
||||||
|
|
||||||
|
const ExploreContainer = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
min-width: 320px;
|
||||||
|
padding: 68px 12px 0px;
|
||||||
|
|
||||||
|
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
||||||
|
padding-top: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const TitleContainer = styled.div`
|
||||||
|
margin-bottom: 32px;
|
||||||
|
max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT};
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
display: flex;
|
||||||
|
`
|
||||||
|
const NavWrapper = styled.div<{ isInfoExplorePageEnabled: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT};
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-bottom: ${({ isInfoExplorePageEnabled }) => (isInfoExplorePageEnabled ? '16px' : '20px')};
|
||||||
|
color: ${({ theme }) => theme.neutral3};
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
${({ isInfoExplorePageEnabled }) =>
|
||||||
|
isInfoExplorePageEnabled &&
|
||||||
|
css`
|
||||||
|
@media screen and (max-width: ${({ theme }) => `${theme.breakpoint.lg}px`}) {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
`};
|
||||||
|
`
|
||||||
|
const TabBar = styled(AutoRow)`
|
||||||
|
gap: 24px;
|
||||||
|
@media screen and (max-width: ${({ theme }) => theme.breakpoint.md}px) {
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const TabItem = styled(ThemedText.HeadlineMedium)<{ active?: boolean }>`
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme, active }) => (active ? theme.neutral1 : theme.neutral2)};
|
||||||
|
cursor: pointer;
|
||||||
|
transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} color`};
|
||||||
|
`
|
||||||
|
const FiltersContainer = styled.div<{ isInfoExplorePageEnabled: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
||||||
|
${({ isInfoExplorePageEnabled }) => !isInfoExplorePageEnabled && 'order: 2;'}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: ${({ theme }) => theme.breakpoint.md}px) {
|
||||||
|
${({ isInfoExplorePageEnabled }) => isInfoExplorePageEnabled && 'justify-content: space-between;'}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const DropdownFilterContainer = styled(FiltersContainer)<{ isInfoExplorePageEnabled: boolean }>`
|
||||||
|
${({ isInfoExplorePageEnabled }) =>
|
||||||
|
isInfoExplorePageEnabled
|
||||||
|
? css`
|
||||||
|
@media screen and (max-width: ${({ theme }) => theme.breakpoint.md}px) {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
: css`
|
||||||
|
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
`};
|
||||||
|
`
|
||||||
|
const SearchContainer = styled(FiltersContainer)<{ isInfoExplorePageEnabled: boolean }>`
|
||||||
|
${({ isInfoExplorePageEnabled }) => !isInfoExplorePageEnabled && 'margin-left: 8px;'}
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
||||||
|
${({ isInfoExplorePageEnabled }) => !isInfoExplorePageEnabled && 'order: 1; margin: 0px;'}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: ${({ theme }) => theme.breakpoint.md}px) {
|
||||||
|
${({ isInfoExplorePageEnabled }) => isInfoExplorePageEnabled && 'justify-content: flex-end;'}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
export enum ExploreTab {
|
||||||
|
Tokens = 'tokens',
|
||||||
|
Pools = 'pools',
|
||||||
|
Transactions = 'transactions',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Page {
|
||||||
|
title: React.ReactNode
|
||||||
|
key: ExploreTab
|
||||||
|
component: () => JSX.Element
|
||||||
|
loggingElementName: string
|
||||||
|
}
|
||||||
|
const Pages: Array<Page> = [
|
||||||
|
{
|
||||||
|
title: <Trans>Tokens</Trans>,
|
||||||
|
key: ExploreTab.Tokens,
|
||||||
|
component: TokenTable,
|
||||||
|
loggingElementName: InterfaceElementName.EXPLORE_TOKENS_TAB,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <Trans>Pools</Trans>,
|
||||||
|
key: ExploreTab.Pools,
|
||||||
|
component: TokenTable,
|
||||||
|
loggingElementName: InterfaceElementName.EXPLORE_POOLS_TAB,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: <Trans>Transactions</Trans>,
|
||||||
|
key: ExploreTab.Transactions,
|
||||||
|
component: TokenTable,
|
||||||
|
loggingElementName: InterfaceElementName.EXPLORE_TRANSACTIONS_TAB,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const Explore = ({ initialTab }: { initialTab?: ExploreTab }) => {
|
||||||
|
const resetFilterString = useResetAtom(filterStringAtom)
|
||||||
|
const location = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const initialKey: number = useMemo(() => {
|
||||||
|
const key = initialTab && Pages.findIndex((page) => page.key === initialTab)
|
||||||
|
if (!key || key === -1) return 0
|
||||||
|
return key
|
||||||
|
}, [initialTab])
|
||||||
|
const [currentTab, setCurrentTab] = useState(initialKey)
|
||||||
|
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||||
|
|
||||||
|
// to allow backward navigation between tabs
|
||||||
|
const { tab } = useExploreParams()
|
||||||
|
useEffect(() => {
|
||||||
|
const tabIndex = Pages.findIndex((page) => page.key === tab)
|
||||||
|
if (tabIndex !== -1) {
|
||||||
|
setCurrentTab(tabIndex)
|
||||||
|
}
|
||||||
|
}, [tab])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
resetFilterString()
|
||||||
|
}, [location, resetFilterString])
|
||||||
|
|
||||||
|
const { component: Page, key: currentKey } = Pages[currentTab]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Trace
|
||||||
|
page={isInfoExplorePageEnabled ? InterfacePageName.EXPLORE_PAGE : InterfacePageName.TOKENS_PAGE}
|
||||||
|
shouldLogImpression
|
||||||
|
>
|
||||||
|
<ExploreContainer>
|
||||||
|
{/* TODO(WEB-2749 & WEB-2750): add graphs to explore page */}
|
||||||
|
{!isInfoExplorePageEnabled && (
|
||||||
|
<TitleContainer>
|
||||||
|
<MouseoverTooltip
|
||||||
|
text={<Trans>This table contains the top tokens by Uniswap volume, sorted based on your input.</Trans>}
|
||||||
|
placement="bottom"
|
||||||
|
>
|
||||||
|
<ThemedText.LargeHeader>
|
||||||
|
<Trans>Top tokens on Uniswap</Trans>
|
||||||
|
</ThemedText.LargeHeader>
|
||||||
|
</MouseoverTooltip>
|
||||||
|
</TitleContainer>
|
||||||
|
)}
|
||||||
|
<NavWrapper isInfoExplorePageEnabled={isInfoExplorePageEnabled}>
|
||||||
|
{isInfoExplorePageEnabled && (
|
||||||
|
<TabBar data-testid="explore-navbar">
|
||||||
|
{Pages.map(({ title, loggingElementName, key }, index) => {
|
||||||
|
const handleNavItemClick = () => {
|
||||||
|
setCurrentTab(index)
|
||||||
|
navigate(`/explore/${key}`)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<TraceEvent
|
||||||
|
events={[BrowserEvent.onClick]}
|
||||||
|
name={SharedEventName.NAVBAR_CLICKED}
|
||||||
|
element={loggingElementName}
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<TabItem onClick={handleNavItemClick} active={currentTab === index} key={key}>
|
||||||
|
{title}
|
||||||
|
</TabItem>
|
||||||
|
</TraceEvent>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</TabBar>
|
||||||
|
)}
|
||||||
|
{isInfoExplorePageEnabled ? (
|
||||||
|
<FiltersContainer isInfoExplorePageEnabled>
|
||||||
|
<DropdownFilterContainer isInfoExplorePageEnabled>
|
||||||
|
<NetworkFilter />
|
||||||
|
{currentKey === ExploreTab.Tokens && <TimeSelector />}
|
||||||
|
</DropdownFilterContainer>
|
||||||
|
<SearchContainer isInfoExplorePageEnabled>
|
||||||
|
{currentKey !== ExploreTab.Transactions && <SearchBar tab={currentKey} />}
|
||||||
|
</SearchContainer>
|
||||||
|
</FiltersContainer>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FiltersContainer isInfoExplorePageEnabled={false}>
|
||||||
|
<NetworkFilter />
|
||||||
|
<TimeSelector />
|
||||||
|
</FiltersContainer>
|
||||||
|
<SearchContainer isInfoExplorePageEnabled={false}>
|
||||||
|
<SearchBar />
|
||||||
|
</SearchContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</NavWrapper>
|
||||||
|
{isInfoExplorePageEnabled ? <Page /> : <TokenTable />}
|
||||||
|
</ExploreContainer>
|
||||||
|
</Trace>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Explore
|
39
src/pages/Explore/redirects.tsx
Normal file
39
src/pages/Explore/redirects.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { Navigate, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
|
import Explore, { ExploreTab } from '.'
|
||||||
|
|
||||||
|
// useParams struggles to distinguish between /explore/:chainId and /explore/:tab
|
||||||
|
export function useExploreParams(): {
|
||||||
|
tab?: ExploreTab
|
||||||
|
chainName?: string
|
||||||
|
tokenAddress?: string
|
||||||
|
} {
|
||||||
|
const { tab, chainName, tokenAddress } = useParams<{ tab: string; chainName: string; tokenAddress: string }>()
|
||||||
|
const exploreTabs = Object.values(ExploreTab)
|
||||||
|
if (tab && !chainName && exploreTabs.includes(tab as ExploreTab)) {
|
||||||
|
// /explore/:tab
|
||||||
|
return { tab: tab as ExploreTab, chainName: undefined, tokenAddress }
|
||||||
|
} else if (tab && !chainName) {
|
||||||
|
// /explore/:chainName
|
||||||
|
return { tab: ExploreTab.Tokens, chainName: tab, tokenAddress }
|
||||||
|
} else if (!tab && !chainName) {
|
||||||
|
// legacy /tokens
|
||||||
|
return { tab: ExploreTab.Tokens, chainName: undefined, tokenAddress: undefined }
|
||||||
|
} else {
|
||||||
|
// /explore/:tab/:chainName
|
||||||
|
return { tab: tab as ExploreTab, chainName, tokenAddress }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default function RedirectExplore() {
|
||||||
|
const { tab, chainName, tokenAddress } = useExploreParams()
|
||||||
|
if (tab && chainName && tokenAddress) {
|
||||||
|
return <Navigate to={`/explore/${tab}/${chainName}/${tokenAddress}`} replace />
|
||||||
|
} else if (chainName && tokenAddress) {
|
||||||
|
return <Navigate to={`/explore/tokens/${chainName}/${tokenAddress}`} replace />
|
||||||
|
} else if (tab && chainName) {
|
||||||
|
return <Navigate to={`/explore/${tab}/${chainName}`} replace />
|
||||||
|
} else if (chainName) {
|
||||||
|
return <Navigate to={`/explore/tokens/${chainName}`} replace />
|
||||||
|
}
|
||||||
|
return <Explore initialTab={tab as ExploreTab} />
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||||
import { useInfoPoolPageEnabled } from 'featureFlags/flags/infoPoolPage'
|
import { useInfoPoolPageEnabled } from 'featureFlags/flags/infoPoolPage'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { lazy, ReactNode, Suspense, useMemo } from 'react'
|
import { lazy, ReactNode, Suspense, useMemo } from 'react'
|
||||||
@ -15,8 +16,10 @@ const Collection = lazy(() => import('nft/pages/collection'))
|
|||||||
const Profile = lazy(() => import('nft/pages/profile'))
|
const Profile = lazy(() => import('nft/pages/profile'))
|
||||||
const Asset = lazy(() => import('nft/pages/asset/Asset'))
|
const Asset = lazy(() => import('nft/pages/asset/Asset'))
|
||||||
const AddLiquidity = lazy(() => import('pages/AddLiquidity'))
|
const AddLiquidity = lazy(() => import('pages/AddLiquidity'))
|
||||||
|
const Explore = lazy(() => import('pages/Explore'))
|
||||||
const RedirectDuplicateTokenIds = lazy(() => import('pages/AddLiquidity/redirects'))
|
const RedirectDuplicateTokenIds = lazy(() => import('pages/AddLiquidity/redirects'))
|
||||||
const RedirectDuplicateTokenIdsV2 = lazy(() => import('pages/AddLiquidityV2/redirects'))
|
const RedirectDuplicateTokenIdsV2 = lazy(() => import('pages/AddLiquidityV2/redirects'))
|
||||||
|
const RedirectExplore = lazy(() => import('pages/Explore/redirects'))
|
||||||
const MigrateV2 = lazy(() => import('pages/MigrateV2'))
|
const MigrateV2 = lazy(() => import('pages/MigrateV2'))
|
||||||
const MigrateV2Pair = lazy(() => import('pages/MigrateV2/MigrateV2Pair'))
|
const MigrateV2Pair = lazy(() => import('pages/MigrateV2/MigrateV2Pair'))
|
||||||
const NotFound = lazy(() => import('pages/NotFound'))
|
const NotFound = lazy(() => import('pages/NotFound'))
|
||||||
@ -28,7 +31,6 @@ const PoolFinder = lazy(() => import('pages/PoolFinder'))
|
|||||||
const RemoveLiquidity = lazy(() => import('pages/RemoveLiquidity'))
|
const RemoveLiquidity = lazy(() => import('pages/RemoveLiquidity'))
|
||||||
const RemoveLiquidityV3 = lazy(() => import('pages/RemoveLiquidity/V3'))
|
const RemoveLiquidityV3 = lazy(() => import('pages/RemoveLiquidity/V3'))
|
||||||
const TokenDetails = lazy(() => import('pages/TokenDetails'))
|
const TokenDetails = lazy(() => import('pages/TokenDetails'))
|
||||||
const Tokens = lazy(() => import('pages/Tokens'))
|
|
||||||
const Vote = lazy(() => import('pages/Vote'))
|
const Vote = lazy(() => import('pages/Vote'))
|
||||||
|
|
||||||
// this is the same svg defined in assets/images/blue-loader.svg
|
// this is the same svg defined in assets/images/blue-loader.svg
|
||||||
@ -48,6 +50,7 @@ const LazyLoadSpinner = () => (
|
|||||||
interface RouterConfig {
|
interface RouterConfig {
|
||||||
browserRouterEnabled?: boolean
|
browserRouterEnabled?: boolean
|
||||||
hash?: string
|
hash?: string
|
||||||
|
infoExplorePageEnabled?: boolean
|
||||||
infoPoolPageEnabled?: boolean
|
infoPoolPageEnabled?: boolean
|
||||||
shouldDisableNFTRoutes?: boolean
|
shouldDisableNFTRoutes?: boolean
|
||||||
}
|
}
|
||||||
@ -59,15 +62,17 @@ export function useRouterConfig(): RouterConfig {
|
|||||||
const browserRouterEnabled = isBrowserRouterEnabled()
|
const browserRouterEnabled = isBrowserRouterEnabled()
|
||||||
const { hash } = useLocation()
|
const { hash } = useLocation()
|
||||||
const infoPoolPageEnabled = useInfoPoolPageEnabled()
|
const infoPoolPageEnabled = useInfoPoolPageEnabled()
|
||||||
|
const infoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||||
const [shouldDisableNFTRoutes] = useAtom(shouldDisableNFTRoutesAtom)
|
const [shouldDisableNFTRoutes] = useAtom(shouldDisableNFTRoutesAtom)
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
browserRouterEnabled,
|
browserRouterEnabled,
|
||||||
hash,
|
hash,
|
||||||
|
infoExplorePageEnabled,
|
||||||
infoPoolPageEnabled,
|
infoPoolPageEnabled,
|
||||||
shouldDisableNFTRoutes: Boolean(shouldDisableNFTRoutes),
|
shouldDisableNFTRoutes: Boolean(shouldDisableNFTRoutes),
|
||||||
}),
|
}),
|
||||||
[browserRouterEnabled, hash, infoPoolPageEnabled, shouldDisableNFTRoutes]
|
[browserRouterEnabled, hash, infoExplorePageEnabled, infoPoolPageEnabled, shouldDisableNFTRoutes]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,15 +103,44 @@ export const routes: RouteDefinition[] = [
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
createRouteDefinition({
|
createRouteDefinition({
|
||||||
path: '/tokens',
|
path: '/explore',
|
||||||
nestedPaths: [':chainName'],
|
nestedPaths: [':tab', ':chainName'],
|
||||||
getElement: () => <Tokens />,
|
getElement: () => <RedirectExplore />,
|
||||||
|
enabled: (args) => Boolean(args.infoExplorePageEnabled),
|
||||||
}),
|
}),
|
||||||
createRouteDefinition({ path: '/tokens/:chainName/:tokenAddress', getElement: () => <TokenDetails /> }),
|
|
||||||
createRouteDefinition({
|
createRouteDefinition({
|
||||||
path: '/pools/:chainName/:poolAddress',
|
path: '/explore',
|
||||||
|
nestedPaths: [':tab/:chainName'],
|
||||||
|
getElement: () => <Explore />,
|
||||||
|
enabled: (args) => Boolean(args.infoExplorePageEnabled),
|
||||||
|
}),
|
||||||
|
createRouteDefinition({
|
||||||
|
path: '/explore/tokens/:chainName/:tokenAddress',
|
||||||
|
getElement: () => <TokenDetails />,
|
||||||
|
enabled: (args) => Boolean(args.infoExplorePageEnabled),
|
||||||
|
}),
|
||||||
|
createRouteDefinition({
|
||||||
|
path: '/tokens',
|
||||||
|
getElement: (args) => {
|
||||||
|
return args.infoExplorePageEnabled ? <Navigate to="/explore/tokens" replace /> : <Explore />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
createRouteDefinition({
|
||||||
|
path: '/tokens/:chainName',
|
||||||
|
getElement: (args) => {
|
||||||
|
return args.infoExplorePageEnabled ? <RedirectExplore /> : <Explore />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
createRouteDefinition({
|
||||||
|
path: '/tokens/:chainName/:tokenAddress',
|
||||||
|
getElement: (args) => {
|
||||||
|
return args.infoExplorePageEnabled ? <RedirectExplore /> : <TokenDetails />
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
createRouteDefinition({
|
||||||
|
path: 'explore/pools/:chainName/:poolAddress',
|
||||||
getElement: () => <PoolDetails />,
|
getElement: () => <PoolDetails />,
|
||||||
enabled: (args) => Boolean(args.infoPoolPageEnabled),
|
enabled: (args) => Boolean(args.infoExplorePageEnabled && args.infoPoolPageEnabled),
|
||||||
}),
|
}),
|
||||||
createRouteDefinition({
|
createRouteDefinition({
|
||||||
path: '/vote/*',
|
path: '/vote/*',
|
||||||
|
@ -1,105 +0,0 @@
|
|||||||
import { Trans } from '@lingui/macro'
|
|
||||||
import { InterfacePageName } from '@uniswap/analytics-events'
|
|
||||||
import { Trace } from 'analytics'
|
|
||||||
import { MAX_WIDTH_MEDIA_BREAKPOINT, MEDIUM_MEDIA_BREAKPOINT } from 'components/Tokens/constants'
|
|
||||||
import { filterStringAtom } from 'components/Tokens/state'
|
|
||||||
import NetworkFilter from 'components/Tokens/TokenTable/NetworkFilter'
|
|
||||||
import SearchBar from 'components/Tokens/TokenTable/SearchBar'
|
|
||||||
import TimeSelector from 'components/Tokens/TokenTable/TimeSelector'
|
|
||||||
import TokenTable from 'components/Tokens/TokenTable/TokenTable'
|
|
||||||
import { MouseoverTooltip } from 'components/Tooltip'
|
|
||||||
import { useResetAtom } from 'jotai/utils'
|
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { useLocation } from 'react-router-dom'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
import { ThemedText } from 'theme/components'
|
|
||||||
|
|
||||||
const ExploreContainer = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
min-width: 320px;
|
|
||||||
padding: 68px 12px 0px;
|
|
||||||
|
|
||||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) {
|
|
||||||
padding-top: 48px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) {
|
|
||||||
padding-top: 20px;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
const TitleContainer = styled.div`
|
|
||||||
margin-bottom: 32px;
|
|
||||||
max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT};
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
display: flex;
|
|
||||||
`
|
|
||||||
const FiltersContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
height: 40px;
|
|
||||||
|
|
||||||
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
|
||||||
order: 2;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
const SearchContainer = styled(FiltersContainer)`
|
|
||||||
margin-left: 8px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
|
||||||
margin: 0px;
|
|
||||||
order: 1;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
const FiltersWrapper = styled.div`
|
|
||||||
display: flex;
|
|
||||||
max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT};
|
|
||||||
margin: 0 auto;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: ${({ theme }) => theme.neutral3};
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
@media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const Tokens = () => {
|
|
||||||
const resetFilterString = useResetAtom(filterStringAtom)
|
|
||||||
const location = useLocation()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
resetFilterString()
|
|
||||||
}, [location, resetFilterString])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Trace page={InterfacePageName.TOKENS_PAGE} shouldLogImpression>
|
|
||||||
<ExploreContainer>
|
|
||||||
<TitleContainer>
|
|
||||||
<MouseoverTooltip
|
|
||||||
text={<Trans>This table contains the top tokens by Uniswap volume, sorted based on your input.</Trans>}
|
|
||||||
placement="bottom"
|
|
||||||
>
|
|
||||||
<ThemedText.LargeHeader>
|
|
||||||
<Trans>Top tokens on Uniswap</Trans>
|
|
||||||
</ThemedText.LargeHeader>
|
|
||||||
</MouseoverTooltip>
|
|
||||||
</TitleContainer>
|
|
||||||
<FiltersWrapper>
|
|
||||||
<FiltersContainer>
|
|
||||||
<NetworkFilter />
|
|
||||||
<TimeSelector />
|
|
||||||
</FiltersContainer>
|
|
||||||
<SearchContainer>
|
|
||||||
<SearchBar />
|
|
||||||
</SearchContainer>
|
|
||||||
</FiltersWrapper>
|
|
||||||
<TokenTable />
|
|
||||||
</ExploreContainer>
|
|
||||||
</Trace>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Tokens
|
|
@ -12,10 +12,37 @@ Array [
|
|||||||
"enabled": [Function],
|
"enabled": [Function],
|
||||||
"getElement": [Function],
|
"getElement": [Function],
|
||||||
"nestedPaths": Array [
|
"nestedPaths": Array [
|
||||||
|
":tab",
|
||||||
":chainName",
|
":chainName",
|
||||||
],
|
],
|
||||||
|
"path": "/explore",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"enabled": [Function],
|
||||||
|
"getElement": [Function],
|
||||||
|
"nestedPaths": Array [
|
||||||
|
":tab/:chainName",
|
||||||
|
],
|
||||||
|
"path": "/explore",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"enabled": [Function],
|
||||||
|
"getElement": [Function],
|
||||||
|
"nestedPaths": Array [],
|
||||||
|
"path": "/explore/tokens/:chainName/:tokenAddress",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"enabled": [Function],
|
||||||
|
"getElement": [Function],
|
||||||
|
"nestedPaths": Array [],
|
||||||
"path": "/tokens",
|
"path": "/tokens",
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"enabled": [Function],
|
||||||
|
"getElement": [Function],
|
||||||
|
"nestedPaths": Array [],
|
||||||
|
"path": "/tokens/:chainName",
|
||||||
|
},
|
||||||
Object {
|
Object {
|
||||||
"enabled": [Function],
|
"enabled": [Function],
|
||||||
"getElement": [Function],
|
"getElement": [Function],
|
||||||
@ -26,7 +53,7 @@ Array [
|
|||||||
"enabled": [Function],
|
"enabled": [Function],
|
||||||
"getElement": [Function],
|
"getElement": [Function],
|
||||||
"nestedPaths": Array [],
|
"nestedPaths": Array [],
|
||||||
"path": "/pools/:chainName/:poolAddress",
|
"path": "explore/pools/:chainName/:poolAddress",
|
||||||
},
|
},
|
||||||
Object {
|
Object {
|
||||||
"enabled": [Function],
|
"enabled": [Function],
|
||||||
|
@ -6059,10 +6059,10 @@
|
|||||||
"@typescript-eslint/types" "5.59.1"
|
"@typescript-eslint/types" "5.59.1"
|
||||||
eslint-visitor-keys "^3.3.0"
|
eslint-visitor-keys "^3.3.0"
|
||||||
|
|
||||||
"@uniswap/analytics-events@^2.24.0":
|
"@uniswap/analytics-events@^2.25.0":
|
||||||
version "2.24.0"
|
version "2.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/@uniswap/analytics-events/-/analytics-events-2.24.0.tgz#c81d0c24da4f052b7f6b2663ff42bfa787be91b5"
|
resolved "https://registry.yarnpkg.com/@uniswap/analytics-events/-/analytics-events-2.25.0.tgz#06f2d81342b2e4dc516bdfa1222ddaa7c274ac04"
|
||||||
integrity sha512-MhX9L95Y7i28a3KxRFJnpmNxNHAgownBVPyhT+mu4PnCXiPEuSovml+uJr277tysKSqxRYqLnCeAw4LocTBIfg==
|
integrity sha512-0syw7gZtoHXSCVb+zV464L+Zgy1ICnGDOrbK2xoVtpiQ8rBjUXPWvcKuaiNPfTsS9tIZNtqOmEyZEjWwvFSLUw==
|
||||||
|
|
||||||
"@uniswap/analytics@1.5.0":
|
"@uniswap/analytics@1.5.0":
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user