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-window-infinite-loader": "^1.0.6",
|
||||
"@uniswap/analytics": "1.5.0",
|
||||
"@uniswap/analytics-events": "^2.24.0",
|
||||
"@uniswap/analytics-events": "^2.25.0",
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
"@uniswap/merkle-distributor": "^1.0.1",
|
||||
|
@ -3,6 +3,7 @@ import { TraceEvent } from 'analytics'
|
||||
import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper'
|
||||
import Row from 'components/Row'
|
||||
import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta'
|
||||
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||
import { TokenBalance } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
@ -76,10 +77,12 @@ function TokenRow({ token, quantity, denominatedValue, tokenProjectMarket }: Tok
|
||||
|
||||
const navigate = useNavigate()
|
||||
const toggleWalletDrawer = useToggleAccountDrawer()
|
||||
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||
|
||||
const navigateToTokenDetails = useCallback(async () => {
|
||||
navigate(getTokenDetailsURL(token))
|
||||
navigate(getTokenDetailsURL({ ...token, isInfoExplorePageEnabled }))
|
||||
toggleWalletDrawer()
|
||||
}, [navigate, token, toggleWalletDrawer])
|
||||
}, [navigate, token, isInfoExplorePageEnabled, toggleWalletDrawer])
|
||||
const { formatNumber } = useFormatter()
|
||||
|
||||
const currency = gqlToCurrency(token)
|
||||
|
@ -4,6 +4,7 @@ import clsx from 'clsx'
|
||||
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
|
||||
import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon'
|
||||
import { checkSearchTokenWarning } from 'constants/tokenSafety'
|
||||
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||
import { Chain, TokenStandard } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { SearchToken } from 'graphql/data/SearchTokens'
|
||||
import { getTokenDetailsURL } from 'graphql/data/util'
|
||||
@ -138,7 +139,9 @@ export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index,
|
||||
sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties })
|
||||
}, [addRecentlySearchedAsset, token, toggleOpen, eventProperties])
|
||||
|
||||
const tokenDetailsPath = getTokenDetailsURL(token)
|
||||
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||
|
||||
const tokenDetailsPath = getTokenDetailsURL({ ...token, isInfoExplorePageEnabled })
|
||||
// Close the modal on escape
|
||||
useEffect(() => {
|
||||
const keyDownHandler = (event: KeyboardEvent) => {
|
||||
|
@ -2,6 +2,7 @@ import { Trans } from '@lingui/macro'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import { useAccountDrawer } from 'components/AccountDrawer'
|
||||
import Web3Status from 'components/Web3Status'
|
||||
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||
import { chainIdToBackendName } from 'graphql/data/util'
|
||||
import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes'
|
||||
import { useIsNftPage } from 'hooks/useIsNftPage'
|
||||
@ -61,15 +62,22 @@ export const PageTabs = () => {
|
||||
const isNftPage = useIsNftPage()
|
||||
|
||||
const shouldDisableNFTRoutes = useDisableNFTRoutes()
|
||||
const infoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem href="/swap" isActive={pathname.startsWith('/swap')}>
|
||||
<Trans>Swap</Trans>
|
||||
</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')}>
|
||||
<Trans>Tokens</Trans>
|
||||
</MenuItem>
|
||||
)}
|
||||
{!shouldDisableNFTRoutes && (
|
||||
<MenuItem dataTestId="nft-nav" href="/nfts" isActive={isNftPage}>
|
||||
<Trans>NFTs</Trans>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { SwapSkeleton } from 'components/swap/SwapSkeleton'
|
||||
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||
import { ArrowLeft } from 'react-feather'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import styled, { useTheme } from 'styled-components'
|
||||
@ -220,9 +221,12 @@ function LoadingStats() {
|
||||
/* Loading State: row component with loading bubbles */
|
||||
export default function TokenDetailsSkeleton() {
|
||||
const { chainName } = useParams<{ chainName?: string }>()
|
||||
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||
return (
|
||||
<LeftPanel>
|
||||
<BreadcrumbNavLink to={chainName ? `/tokens/${chainName}` : `/explore`}>
|
||||
<BreadcrumbNavLink
|
||||
to={(isInfoExplorePageEnabled ? '/explore' : '') + (chainName ? `/tokens/${chainName}` : `/tokens`)}
|
||||
>
|
||||
<ArrowLeft size={14} /> Tokens
|
||||
</BreadcrumbNavLink>
|
||||
<TokenInfoContainer>
|
||||
|
@ -23,6 +23,7 @@ import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage'
|
||||
import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal'
|
||||
import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens'
|
||||
import { checkWarning } from 'constants/tokenSafety'
|
||||
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||
import { useInfoTDPEnabled } from 'featureFlags/flags/infoTDP'
|
||||
import { TokenPriceQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||
import { Chain, TokenQuery, TokenQueryData } from 'graphql/data/Token'
|
||||
@ -137,6 +138,8 @@ export default function TokenDetails({
|
||||
const isBlockedToken = tokenWarning?.canProceed === false
|
||||
const navigate = useNavigate()
|
||||
|
||||
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||
|
||||
// Wrapping navigate in a transition prevents Suspense from unnecessarily showing fallbacks again.
|
||||
const [isPending, startTokenTransition] = useTransition()
|
||||
const navigateToTokenForChain = useCallback(
|
||||
@ -144,12 +147,20 @@ export default function TokenDetails({
|
||||
if (!address) return
|
||||
const bridgedAddress = crossChainMap[update]
|
||||
if (bridgedAddress) {
|
||||
startTokenTransition(() => navigate(getTokenDetailsURL({ address: bridgedAddress, chain: update })))
|
||||
startTokenTransition(() =>
|
||||
navigate(
|
||||
getTokenDetailsURL({
|
||||
address: bridgedAddress,
|
||||
chain: update,
|
||||
isInfoExplorePageEnabled,
|
||||
})
|
||||
)
|
||||
)
|
||||
} 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)
|
||||
|
||||
@ -175,11 +186,12 @@ export default function TokenDetails({
|
||||
tokens[Field.INPUT] && tokens[Field.INPUT]?.currencyId !== newDefaultTokenID
|
||||
? tokens[Field.INPUT]?.currencyId
|
||||
: null,
|
||||
isInfoExplorePageEnabled,
|
||||
})
|
||||
)
|
||||
)
|
||||
},
|
||||
[address, chain, navigate]
|
||||
[address, chain, isInfoExplorePageEnabled, navigate]
|
||||
)
|
||||
|
||||
const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike<boolean>) => void }>()
|
||||
@ -207,7 +219,7 @@ export default function TokenDetails({
|
||||
<TokenDetailsLayout>
|
||||
{detailedToken && !isPending ? (
|
||||
<LeftPanel>
|
||||
<BreadcrumbNavLink to={`/tokens/${chain.toLowerCase()}`}>
|
||||
<BreadcrumbNavLink to={`${isInfoExplorePageEnabled ? '/explore' : ''}/tokens/${chain.toLowerCase()}`}>
|
||||
<ArrowLeft data-testid="token-details-return-button" size={14} /> Tokens
|
||||
</BreadcrumbNavLink>
|
||||
<TokenInfoContainer data-testid="token-info-container">
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Badge from 'components/Badge'
|
||||
import { ChainLogo } from 'components/Logo/ChainLogo'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||
import {
|
||||
BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS,
|
||||
BACKEND_SUPPORTED_CHAINS,
|
||||
@ -8,6 +9,7 @@ import {
|
||||
validateUrlChainParam,
|
||||
} from 'graphql/data/util'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { useExploreParams } from 'pages/Explore/redirects'
|
||||
import { useRef } from 'react'
|
||||
import { Check, ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
@ -48,8 +50,8 @@ const InternalLinkMenuItem = styled(InternalMenuItem)<{ disabled?: boolean }>`
|
||||
pointer-events: none;
|
||||
`}
|
||||
`
|
||||
const MenuTimeFlyout = styled.span`
|
||||
min-width: 240px;
|
||||
const MenuTimeFlyout = styled.span<{ isInfoExplorePageEnabled: boolean }>`
|
||||
min-width: ${({ isInfoExplorePageEnabled }) => (isInfoExplorePageEnabled ? '150px' : '240px')};
|
||||
max-height: 350px;
|
||||
overflow: auto;
|
||||
background-color: ${({ theme }) => theme.surface1};
|
||||
@ -63,7 +65,18 @@ const MenuTimeFlyout = styled.span`
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
z-index: 100;
|
||||
|
||||
${({ isInfoExplorePageEnabled }) =>
|
||||
isInfoExplorePageEnabled
|
||||
? css`
|
||||
right: 0px;
|
||||
@media screen and (max-width: ${({ theme }) => `${theme.breakpoint.lg}px`}) {
|
||||
left: 0px;
|
||||
}
|
||||
`
|
||||
: css`
|
||||
left: 0px;
|
||||
`}
|
||||
`
|
||||
const StyledMenu = styled.div`
|
||||
display: flex;
|
||||
@ -96,8 +109,8 @@ const CheckContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: flex-end;
|
||||
`
|
||||
const NetworkFilterOption = styled(FilterOption)`
|
||||
min-width: 156px;
|
||||
const NetworkFilterOption = styled(FilterOption)<{ isInfoExplorePageEnabled: boolean }>`
|
||||
${({ isInfoExplorePageEnabled }) => !isInfoExplorePageEnabled && 'min-width: 156px;'}
|
||||
`
|
||||
const Tag = styled(Badge)`
|
||||
background-color: ${({ theme }) => theme.surface2};
|
||||
@ -114,6 +127,9 @@ export default function NetworkFilter() {
|
||||
const toggleMenu = useToggleModal(ApplicationModal.NETWORK_FILTER)
|
||||
useOnClickOutside(node, open ? toggleMenu : undefined)
|
||||
const navigate = useNavigate()
|
||||
const { tab } = useExploreParams()
|
||||
|
||||
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||
|
||||
const currentChainName = validateUrlChainParam(useParams().chainName)
|
||||
const chainId = supportedChainIdFromGQLChain(currentChainName)
|
||||
@ -123,6 +139,7 @@ export default function NetworkFilter() {
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<NetworkFilterOption
|
||||
isInfoExplorePageEnabled={isInfoExplorePageEnabled}
|
||||
onClick={toggleMenu}
|
||||
aria-label="networkFilter"
|
||||
active={open}
|
||||
@ -130,7 +147,7 @@ export default function NetworkFilter() {
|
||||
>
|
||||
<StyledMenuContent>
|
||||
<NetworkLabel>
|
||||
<ChainLogo chainId={chainId} size={20} /> {chainInfo.label}
|
||||
<ChainLogo chainId={chainId} size={20} /> {!isInfoExplorePageEnabled && chainInfo.label}
|
||||
</NetworkLabel>
|
||||
<Chevron open={open}>
|
||||
{open ? (
|
||||
@ -142,7 +159,7 @@ export default function NetworkFilter() {
|
||||
</StyledMenuContent>
|
||||
</NetworkFilterOption>
|
||||
{open && (
|
||||
<MenuTimeFlyout>
|
||||
<MenuTimeFlyout isInfoExplorePageEnabled={isInfoExplorePageEnabled}>
|
||||
{BACKEND_SUPPORTED_CHAINS.map((network) => {
|
||||
const chainId = supportedChainIdFromGQLChain(network)
|
||||
const chainInfo = getChainInfo(chainId)
|
||||
@ -151,7 +168,9 @@ export default function NetworkFilter() {
|
||||
key={network}
|
||||
data-testid={`tokens-network-filter-option-${network.toLowerCase()}`}
|
||||
onClick={() => {
|
||||
navigate(`/tokens/${network.toLowerCase()}`)
|
||||
isInfoExplorePageEnabled
|
||||
? navigate(`/explore/${tab}/${network.toLowerCase()}`)
|
||||
: navigate(`/tokens/${network.toLowerCase()}`)
|
||||
toggleMenu()
|
||||
}}
|
||||
>
|
||||
|
@ -3,6 +3,7 @@ import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap
|
||||
import { TraceEvent } from 'analytics'
|
||||
import searchIcon from 'assets/svg/search.svg'
|
||||
import xIcon from 'assets/svg/x.svg'
|
||||
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||
import useDebounce from 'hooks/useDebounce'
|
||||
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||
import { useEffect, useState } from 'react'
|
||||
@ -12,11 +13,12 @@ import { MEDIUM_MEDIA_BREAKPOINT } from '../constants'
|
||||
import { filterStringAtom } from '../state'
|
||||
const ICON_SIZE = '20px'
|
||||
|
||||
const SearchBarContainer = styled.div`
|
||||
const SearchBarContainer = styled.div<{ isInfoExplorePageEnabled: boolean }>`
|
||||
display: flex;
|
||||
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-image: url(${searchIcon});
|
||||
background-size: 20px 20px;
|
||||
@ -25,12 +27,14 @@ const SearchInput = styled.input`
|
||||
border-radius: 12px;
|
||||
border: 1px solid ${({ theme }) => theme.surface3};
|
||||
height: 100%;
|
||||
width: min(200px, 100%);
|
||||
width: ${({ isInfoExplorePageEnabled, isOpen }) =>
|
||||
isInfoExplorePageEnabled ? (isOpen ? '200px' : '0') : 'min(200px, 100%)'};
|
||||
font-size: 16px;
|
||||
font-weight: 485;
|
||||
padding-left: 40px;
|
||||
color: ${({ theme }) => theme.neutral2};
|
||||
transition-duration: ${({ theme }) => theme.transition.duration.fast};
|
||||
${(isInfoExplorePageEnabled) => isInfoExplorePageEnabled && 'text-overflow: ellipsis;'}
|
||||
|
||||
:hover {
|
||||
background-color: ${({ theme }) => theme.surface1};
|
||||
@ -58,15 +62,18 @@ const SearchInput = styled.input`
|
||||
}
|
||||
|
||||
@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 [localFilterString, setLocalFilterString] = useState(currentString)
|
||||
const setFilterString = useUpdateAtom(filterStringAtom)
|
||||
const debouncedLocalFilterString = useDebounce(localFilterString, 300)
|
||||
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setLocalFilterString(currentString)
|
||||
@ -76,8 +83,14 @@ export default function SearchBar() {
|
||||
setFilterString(debouncedLocalFilterString)
|
||||
}, [debouncedLocalFilterString, setFilterString])
|
||||
|
||||
const handleFocus = () => setIsOpen(true)
|
||||
|
||||
const handleBlur = () => {
|
||||
if (localFilterString === '') setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchBarContainer>
|
||||
<SearchBarContainer isInfoExplorePageEnabled={isInfoExplorePageEnabled}>
|
||||
<Trans
|
||||
render={({ translation }) => (
|
||||
<TraceEvent
|
||||
@ -86,6 +99,7 @@ export default function SearchBar() {
|
||||
element={InterfaceElementName.EXPLORE_SEARCH_INPUT}
|
||||
>
|
||||
<SearchInput
|
||||
isInfoExplorePageEnabled={isInfoExplorePageEnabled}
|
||||
data-cy="explore-tokens-search-input"
|
||||
type="search"
|
||||
placeholder={`${translation}`}
|
||||
@ -93,11 +107,14 @@ export default function SearchBar() {
|
||||
autoComplete="off"
|
||||
value={localFilterString}
|
||||
onChange={({ target: { value } }) => setLocalFilterString(value)}
|
||||
isOpen={isOpen}
|
||||
onFocus={isInfoExplorePageEnabled ? handleFocus : undefined}
|
||||
onBlur={isInfoExplorePageEnabled ? handleBlur : undefined}
|
||||
/>
|
||||
</TraceEvent>
|
||||
)}
|
||||
>
|
||||
Filter tokens
|
||||
{isInfoExplorePageEnabled ? (tab === 'tokens' ? 'Search tokens' : 'Search pools') : 'Filter tokens'}
|
||||
</Trans>
|
||||
</SearchBarContainer>
|
||||
)
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Trans } from '@lingui/macro'
|
||||
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||
import { TimePeriod } from 'graphql/data/util'
|
||||
import { useOnClickOutside } from 'hooks/useOnClickOutside'
|
||||
import { useAtom } from 'jotai'
|
||||
@ -5,7 +7,7 @@ import { useRef } from 'react'
|
||||
import { Check, ChevronDown, ChevronUp } from 'react-feather'
|
||||
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
|
||||
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 { filterTimeAtom } from '../state'
|
||||
@ -52,8 +54,8 @@ const InternalLinkMenuItem = styled(InternalMenuItem)`
|
||||
text-decoration: none;
|
||||
}
|
||||
`
|
||||
const MenuTimeFlyout = styled.span`
|
||||
min-width: 240px;
|
||||
const MenuTimeFlyout = styled.span<{ isInfoExplorePageEnabled: boolean }>`
|
||||
min-width: ${({ isInfoExplorePageEnabled }) => (isInfoExplorePageEnabled ? '150px' : '240px')};
|
||||
max-height: 300px;
|
||||
overflow: auto;
|
||||
background-color: ${({ theme }) => theme.surface1};
|
||||
@ -69,12 +71,16 @@ const MenuTimeFlyout = styled.span`
|
||||
z-index: 100;
|
||||
left: 0px;
|
||||
|
||||
${({ isInfoExplorePageEnabled }) =>
|
||||
!isInfoExplorePageEnabled &&
|
||||
css`
|
||||
@media only screen and (max-width: ${SMALL_MEDIA_BREAKPOINT}) {
|
||||
right: 0px;
|
||||
left: unset;
|
||||
right: 0px;
|
||||
}
|
||||
`}
|
||||
`
|
||||
const StyledMenu = styled.div`
|
||||
const StyledMenu = styled.div<{ isInfoExplorePageEnabled: boolean }>`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -82,11 +88,15 @@ const StyledMenu = styled.div`
|
||||
border: none;
|
||||
text-align: left;
|
||||
|
||||
${({ isInfoExplorePageEnabled }) =>
|
||||
!isInfoExplorePageEnabled &&
|
||||
css`
|
||||
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
|
||||
width: 72px;
|
||||
}
|
||||
`}
|
||||
`
|
||||
const StyledMenuContent = styled.div`
|
||||
const StyledMenuContent = styled.div<{ isInfoExplorePageEnabled: boolean }>`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
@ -94,6 +104,7 @@ const StyledMenuContent = styled.div`
|
||||
border: none;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
${({ isInfoExplorePageEnabled }) => isInfoExplorePageEnabled && 'white-space: nowrap;'}
|
||||
`
|
||||
const Chevron = styled.span<{ open: boolean }>`
|
||||
padding-top: 1px;
|
||||
@ -109,11 +120,19 @@ export default function TimeSelector() {
|
||||
useOnClickOutside(node, open ? toggleMenu : undefined)
|
||||
const [activeTime, setTime] = useAtom(filterTimeAtom)
|
||||
|
||||
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||
|
||||
return (
|
||||
<StyledMenu ref={node}>
|
||||
<StyledMenu ref={node} isInfoExplorePageEnabled={isInfoExplorePageEnabled}>
|
||||
<FilterOption onClick={toggleMenu} aria-label="timeSelector" active={open} data-testid="time-selector">
|
||||
<StyledMenuContent>
|
||||
{DISPLAYS[activeTime]}
|
||||
<StyledMenuContent isInfoExplorePageEnabled={isInfoExplorePageEnabled}>
|
||||
{isInfoExplorePageEnabled ? (
|
||||
<>
|
||||
{DISPLAYS[activeTime]} <Trans>volume</Trans>
|
||||
</>
|
||||
) : (
|
||||
DISPLAYS[activeTime]
|
||||
)}
|
||||
<Chevron open={open}>
|
||||
{open ? (
|
||||
<ChevronUp width={20} height={15} viewBox="0 0 24 20" />
|
||||
@ -124,7 +143,7 @@ export default function TimeSelector() {
|
||||
</StyledMenuContent>
|
||||
</FilterOption>
|
||||
{open && (
|
||||
<MenuTimeFlyout>
|
||||
<MenuTimeFlyout isInfoExplorePageEnabled={isInfoExplorePageEnabled}>
|
||||
{ORDERED_TIMES.map((time) => (
|
||||
<InternalLinkMenuItem
|
||||
key={DISPLAYS[time]}
|
||||
@ -134,7 +153,13 @@ export default function TimeSelector() {
|
||||
toggleMenu()
|
||||
}}
|
||||
>
|
||||
{isInfoExplorePageEnabled ? (
|
||||
<div>
|
||||
{DISPLAYS[time]} <Trans>volume</Trans>
|
||||
</div>
|
||||
) : (
|
||||
<div>{DISPLAYS[time]}</div>
|
||||
)}
|
||||
{time === activeTime && <Check color={theme.accent1} size={16} />}
|
||||
</InternalLinkMenuItem>
|
||||
))}
|
||||
|
@ -8,6 +8,7 @@ import { ArrowChangeUp } from 'components/Icons/ArrowChangeUp'
|
||||
import { Info } from 'components/Icons/Info'
|
||||
import QueryTokenLogo from 'components/Logo/QueryTokenLogo'
|
||||
import { MouseoverTooltip } from 'components/Tooltip'
|
||||
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
|
||||
import { SparklineMap, TopToken } from 'graphql/data/TopTokens'
|
||||
import { getTokenDetailsURL, supportedChainIdFromGQLChain, validateUrlChainParam } from 'graphql/data/util'
|
||||
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
|
||||
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)
|
||||
return (
|
||||
<div ref={ref} data-testid={`token-table-row-${token.address}`}>
|
||||
<StyledLink
|
||||
to={getTokenDetailsURL(token)}
|
||||
to={getTokenDetailsURL({ ...token, isInfoExplorePageEnabled })}
|
||||
onClick={() =>
|
||||
sendAnalyticsEvent(InterfaceEventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ChainId } from '@uniswap/sdk-core'
|
||||
|
||||
// Breakpoints specifically for the token pages
|
||||
export const MAX_WIDTH_MEDIA_BREAKPOINT = '1200px'
|
||||
export const XLARGE_MEDIA_BREAKPOINT = '960px'
|
||||
export const LARGE_MEDIA_BREAKPOINT = '840px'
|
||||
|
@ -3,3 +3,7 @@ import { BaseVariant, FeatureFlag, useBaseFlag } from '../index'
|
||||
export function useInfoExploreFlag(): BaseVariant {
|
||||
return useBaseFlag(FeatureFlag.infoExplore)
|
||||
}
|
||||
|
||||
export function useInfoExplorePageEnabled(): boolean {
|
||||
return useInfoExploreFlag() === BaseVariant.Enabled
|
||||
}
|
||||
|
@ -207,15 +207,17 @@ export function getTokenDetailsURL({
|
||||
address,
|
||||
chain,
|
||||
inputAddress,
|
||||
isInfoExplorePageEnabled,
|
||||
}: {
|
||||
address?: string | null
|
||||
chain: Chain
|
||||
inputAddress?: string | null
|
||||
isInfoExplorePageEnabled: boolean
|
||||
}) {
|
||||
const chainName = chain.toLowerCase()
|
||||
const tokenAddress = address ?? NATIVE_CHAIN_ID
|
||||
const inputAddressSuffix = inputAddress ? `?inputCurrency=${inputAddress}` : ''
|
||||
return `/tokens/${chainName}/${tokenAddress}${inputAddressSuffix}`
|
||||
return (isInfoExplorePageEnabled ? '/explore' : '') + `/tokens/${chainName}/${tokenAddress}${inputAddressSuffix}`
|
||||
}
|
||||
|
||||
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 { useAtom } from 'jotai'
|
||||
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 Asset = lazy(() => import('nft/pages/asset/Asset'))
|
||||
const AddLiquidity = lazy(() => import('pages/AddLiquidity'))
|
||||
const Explore = lazy(() => import('pages/Explore'))
|
||||
const RedirectDuplicateTokenIds = lazy(() => import('pages/AddLiquidity/redirects'))
|
||||
const RedirectDuplicateTokenIdsV2 = lazy(() => import('pages/AddLiquidityV2/redirects'))
|
||||
const RedirectExplore = lazy(() => import('pages/Explore/redirects'))
|
||||
const MigrateV2 = lazy(() => import('pages/MigrateV2'))
|
||||
const MigrateV2Pair = lazy(() => import('pages/MigrateV2/MigrateV2Pair'))
|
||||
const NotFound = lazy(() => import('pages/NotFound'))
|
||||
@ -28,7 +31,6 @@ const PoolFinder = lazy(() => import('pages/PoolFinder'))
|
||||
const RemoveLiquidity = lazy(() => import('pages/RemoveLiquidity'))
|
||||
const RemoveLiquidityV3 = lazy(() => import('pages/RemoveLiquidity/V3'))
|
||||
const TokenDetails = lazy(() => import('pages/TokenDetails'))
|
||||
const Tokens = lazy(() => import('pages/Tokens'))
|
||||
const Vote = lazy(() => import('pages/Vote'))
|
||||
|
||||
// this is the same svg defined in assets/images/blue-loader.svg
|
||||
@ -48,6 +50,7 @@ const LazyLoadSpinner = () => (
|
||||
interface RouterConfig {
|
||||
browserRouterEnabled?: boolean
|
||||
hash?: string
|
||||
infoExplorePageEnabled?: boolean
|
||||
infoPoolPageEnabled?: boolean
|
||||
shouldDisableNFTRoutes?: boolean
|
||||
}
|
||||
@ -59,15 +62,17 @@ export function useRouterConfig(): RouterConfig {
|
||||
const browserRouterEnabled = isBrowserRouterEnabled()
|
||||
const { hash } = useLocation()
|
||||
const infoPoolPageEnabled = useInfoPoolPageEnabled()
|
||||
const infoExplorePageEnabled = useInfoExplorePageEnabled()
|
||||
const [shouldDisableNFTRoutes] = useAtom(shouldDisableNFTRoutesAtom)
|
||||
return useMemo(
|
||||
() => ({
|
||||
browserRouterEnabled,
|
||||
hash,
|
||||
infoExplorePageEnabled,
|
||||
infoPoolPageEnabled,
|
||||
shouldDisableNFTRoutes: Boolean(shouldDisableNFTRoutes),
|
||||
}),
|
||||
[browserRouterEnabled, hash, infoPoolPageEnabled, shouldDisableNFTRoutes]
|
||||
[browserRouterEnabled, hash, infoExplorePageEnabled, infoPoolPageEnabled, shouldDisableNFTRoutes]
|
||||
)
|
||||
}
|
||||
|
||||
@ -98,15 +103,44 @@ export const routes: RouteDefinition[] = [
|
||||
},
|
||||
}),
|
||||
createRouteDefinition({
|
||||
path: '/tokens',
|
||||
nestedPaths: [':chainName'],
|
||||
getElement: () => <Tokens />,
|
||||
path: '/explore',
|
||||
nestedPaths: [':tab', ':chainName'],
|
||||
getElement: () => <RedirectExplore />,
|
||||
enabled: (args) => Boolean(args.infoExplorePageEnabled),
|
||||
}),
|
||||
createRouteDefinition({ path: '/tokens/:chainName/:tokenAddress', getElement: () => <TokenDetails /> }),
|
||||
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 />,
|
||||
enabled: (args) => Boolean(args.infoPoolPageEnabled),
|
||||
enabled: (args) => Boolean(args.infoExplorePageEnabled && args.infoPoolPageEnabled),
|
||||
}),
|
||||
createRouteDefinition({
|
||||
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],
|
||||
"getElement": [Function],
|
||||
"nestedPaths": Array [
|
||||
":tab",
|
||||
":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",
|
||||
},
|
||||
Object {
|
||||
"enabled": [Function],
|
||||
"getElement": [Function],
|
||||
"nestedPaths": Array [],
|
||||
"path": "/tokens/:chainName",
|
||||
},
|
||||
Object {
|
||||
"enabled": [Function],
|
||||
"getElement": [Function],
|
||||
@ -26,7 +53,7 @@ Array [
|
||||
"enabled": [Function],
|
||||
"getElement": [Function],
|
||||
"nestedPaths": Array [],
|
||||
"path": "/pools/:chainName/:poolAddress",
|
||||
"path": "explore/pools/:chainName/:poolAddress",
|
||||
},
|
||||
Object {
|
||||
"enabled": [Function],
|
||||
|
@ -6059,10 +6059,10 @@
|
||||
"@typescript-eslint/types" "5.59.1"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@uniswap/analytics-events@^2.24.0":
|
||||
version "2.24.0"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/analytics-events/-/analytics-events-2.24.0.tgz#c81d0c24da4f052b7f6b2663ff42bfa787be91b5"
|
||||
integrity sha512-MhX9L95Y7i28a3KxRFJnpmNxNHAgownBVPyhT+mu4PnCXiPEuSovml+uJr277tysKSqxRYqLnCeAw4LocTBIfg==
|
||||
"@uniswap/analytics-events@^2.25.0":
|
||||
version "2.25.0"
|
||||
resolved "https://registry.yarnpkg.com/@uniswap/analytics-events/-/analytics-events-2.25.0.tgz#06f2d81342b2e4dc516bdfa1222ddaa7c274ac04"
|
||||
integrity sha512-0syw7gZtoHXSCVb+zV464L+Zgy1ICnGDOrbK2xoVtpiQ8rBjUXPWvcKuaiNPfTsS9tIZNtqOmEyZEjWwvFSLUw==
|
||||
|
||||
"@uniswap/analytics@1.5.0":
|
||||
version "1.5.0"
|
||||
|
Loading…
Reference in New Issue
Block a user