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

feat: [info] update token links (#7505)

* update pdp link styles

* dynamic link text

* move links to their own file

* border width case

* todo comments

* add explorer icon

* hide chain logos on other chain

* remove quotes

* clean up

* unused style
parent 0fbc8265
import { ComponentProps } from 'react'
export const ExplorerIcon = (props: ComponentProps<'svg'>) => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
id="box-search_2"
d="M6.32064 7.43328C6.4873 7.50661 6.66065 7.55329 6.83398 7.59329V13.94C6.72732 13.9133 6.62067 13.8733 6.52067 13.8266L2.52067 12.0466C1.80067 11.7266 1.33398 11.0133 1.33398 10.2199V5.77996C1.33398 5.59996 1.36064 5.41995 1.40731 5.25329L6.32064 7.43328ZM12.754 4.37328C12.5807 4.19995 12.3806 4.05328 12.1473 3.95328L8.1473 2.17329C7.6273 1.93996 7.04067 1.93996 6.52067 2.17329L2.52067 3.95328C2.28734 4.05328 2.08731 4.19995 1.91398 4.37328L6.72062 6.51328C7.10729 6.68661 7.55401 6.68661 7.94735 6.51328L12.754 4.37328ZM11.1347 7.92862C11.8227 7.76662 12.4766 7.81863 13.0646 8.02129C13.1966 8.06663 13.334 7.97729 13.334 7.83729V5.77996C13.334 5.59996 13.3073 5.41995 13.2607 5.25329L8.34733 7.43328C8.18066 7.49995 8.00732 7.55329 7.83398 7.59329V13.94C7.84598 13.948 7.84599 13.9479 7.85799 13.9559L8.98665 13.452C9.10132 13.4006 9.13533 13.262 9.066 13.158C8.584 12.4373 8.37864 11.522 8.57397 10.5586C8.83464 9.27263 9.85802 8.22929 11.1347 7.92862ZM15.0207 14.3526C14.9233 14.4499 14.7953 14.4993 14.6673 14.4993C14.5393 14.4993 14.4113 14.4506 14.314 14.3526L13.2807 13.3193C12.896 13.5713 12.4386 13.7213 11.9453 13.7213C10.5973 13.7213 9.50065 12.6246 9.50065 11.2766C9.50065 9.92862 10.5973 8.83194 11.9453 8.83194C13.2926 8.83194 14.3893 9.92862 14.3893 11.2766C14.3893 11.77 14.24 12.228 13.988 12.6119L15.0213 13.6453C15.216 13.8406 15.216 14.1573 15.0207 14.3526ZM13.3893 11.2773C13.3893 10.4806 12.7413 9.83261 11.9453 9.83261C11.1486 9.83261 10.5007 10.4806 10.5007 11.2773C10.5007 12.0739 11.1486 12.722 11.9453 12.722C12.7413 12.7213 13.3893 12.0733 13.3893 11.2773Z"
fill="white"
/>
</svg>
)
...@@ -125,7 +125,6 @@ const StyledLogoParentContainer = styled.div` ...@@ -125,7 +125,6 @@ const StyledLogoParentContainer = styled.div`
top: 0; top: 0;
left: 0; left: 0;
` `
function DoubleCurrencyAndChainLogo({ function DoubleCurrencyAndChainLogo({
chainId, chainId,
currencies, currencies,
...@@ -165,37 +164,45 @@ function SquareL2Logo({ chainId }: { chainId: ChainId }) { ...@@ -165,37 +164,45 @@ function SquareL2Logo({ chainId }: { chainId: ChainId }) {
) )
} }
function DoubleCurrencyLogo({ chainId, currencies }: { chainId: number; currencies: Array<Currency | undefined> }) { export function DoubleCurrencyLogo({
chainId,
currencies,
small,
}: {
chainId: number
currencies: Array<Currency | undefined>
small?: boolean
}) {
const [src, nextSrc] = useTokenLogoSource(currencies?.[0]?.wrapped.address, chainId, currencies?.[0]?.isNative) const [src, nextSrc] = useTokenLogoSource(currencies?.[0]?.wrapped.address, chainId, currencies?.[0]?.isNative)
const [src2, nextSrc2] = useTokenLogoSource(currencies?.[1]?.wrapped.address, chainId, currencies?.[1]?.isNative) const [src2, nextSrc2] = useTokenLogoSource(currencies?.[1]?.wrapped.address, chainId, currencies?.[1]?.isNative)
return <DoubleLogo logo1={src} onError1={nextSrc} logo2={src2} onError2={nextSrc2} /> return <DoubleLogo logo1={src} onError1={nextSrc} logo2={src2} onError2={nextSrc2} small={small} />
} }
const DoubleLogoContainer = styled.div` const DoubleLogoContainer = styled.div<{ small?: boolean }>`
display: flex; display: flex;
gap: 2px; gap: 2px;
position: relative; position: relative;
top: 0; top: 0;
left: 0; left: 0;
img { img {
width: 16px; width: ${({ small }) => (small ? '10px' : '16px')};
height: 32px; height: ${({ small }) => (small ? '20px' : '32px')};
object-fit: cover; object-fit: cover;
} }
img:first-child { img:first-child {
border-radius: 16px 0 0 16px; border-radius: ${({ small }) => (small ? '10px 0 0 10px' : '16px 0 0 16px')};
object-position: 0 0; object-position: 0 0;
} }
img:last-child { img:last-child {
border-radius: 0 16px 16px 0; border-radius: ${({ small }) => (small ? '0 10px 10px 0' : '0 16px 16px 0')};
object-position: 100% 0; object-position: 100% 0;
} }
` `
const CircleLogoImage = styled.img` const CircleLogoImage = styled.img<{ small?: boolean }>`
width: 32px; width: ${({ small }) => (small ? '10px' : '16px')};
height: 32px; height: ${({ small }) => (small ? '20px' : '32px')};
border-radius: 50%; border-radius: 50%;
` `
...@@ -204,13 +211,14 @@ interface DoubleLogoProps { ...@@ -204,13 +211,14 @@ interface DoubleLogoProps {
logo2?: string logo2?: string
onError1?: () => void onError1?: () => void
onError2?: () => void onError2?: () => void
small?: boolean
} }
function DoubleLogo({ logo1, onError1, logo2, onError2 }: DoubleLogoProps) { function DoubleLogo({ logo1, onError1, logo2, onError2, small }: DoubleLogoProps) {
return ( return (
<DoubleLogoContainer> <DoubleLogoContainer small={small}>
<CircleLogoImage src={logo1 ?? blankTokenUrl} onError={onError1} /> <CircleLogoImage src={logo1 ?? blankTokenUrl} onError={onError1} small={small} />
<CircleLogoImage src={logo2 ?? blankTokenUrl} onError={onError2} /> <CircleLogoImage src={logo2 ?? blankTokenUrl} onError={onError2} small={small} />
</DoubleLogoContainer> </DoubleLogoContainer>
) )
} }
import { ChainId } from '@uniswap/sdk-core'
import { USDC_MAINNET } from 'constants/tokens'
import { usdcWethPoolAddress, validPoolToken0, validPoolToken1 } from 'test-utils/pools/fixtures'
import { render, screen } from 'test-utils/render'
import { PoolDetailsLink } from './PoolDetailsLink'
describe('PoolDetailsHeader', () => {
it('renders link for pool address', async () => {
const { asFragment } = render(
<PoolDetailsLink
address={usdcWethPoolAddress}
chainId={ChainId.MAINNET}
tokens={[validPoolToken0, validPoolToken1]}
/>
)
expect(asFragment()).toMatchSnapshot()
expect(screen.getByText('USDC / WETH')).toBeInTheDocument()
expect(screen.getByTestId('pdp-pool-logo-USDC-WETH')).toBeInTheDocument()
expect(screen.getByTestId(`copy-address-${usdcWethPoolAddress}`)).toBeInTheDocument()
expect(screen.getByTestId(`explorer-url-https://etherscan.io/address/${usdcWethPoolAddress}`)).toBeInTheDocument()
})
it('renders link for token address', async () => {
const { asFragment } = render(
<PoolDetailsLink address={USDC_MAINNET.address} chainId={ChainId.MAINNET} tokens={[validPoolToken0]} />
)
expect(asFragment()).toMatchSnapshot()
expect(screen.getByText('USDC')).toBeInTheDocument()
expect(screen.getByTestId('pdp-token-logo-USDC')).toBeInTheDocument()
expect(screen.getByTestId(`copy-address-${USDC_MAINNET.address}`)).toBeInTheDocument()
expect(screen.getByTestId(`explorer-url-https://etherscan.io/token/${USDC_MAINNET.address}`)).toBeInTheDocument()
})
})
import { Trans } from '@lingui/macro'
import { ChainId } from '@uniswap/sdk-core'
import { EtherscanLogo } from 'components/Icons/Etherscan'
import { ExplorerIcon } from 'components/Icons/ExplorerIcon'
import CurrencyLogo from 'components/Logo/CurrencyLogo'
import Row from 'components/Row'
import { useInfoExplorePageEnabled } from 'featureFlags/flags/infoExplore'
import { chainIdToBackendName, getTokenDetailsURL } from 'graphql/data/util'
import { Token } from 'graphql/thegraph/__generated__/types-and-hooks'
import { useCurrency } from 'hooks/Tokens'
import useCopyClipboard from 'hooks/useCopyClipboard'
import { useCallback } from 'react'
import { ChevronRight, Copy } from 'react-feather'
import { useNavigate } from 'react-router-dom'
import styled, { useTheme } from 'styled-components'
import { BREAKPOINTS } from 'theme'
import { ClickableStyle, EllipsisStyle, ExternalLink, ThemedText } from 'theme/components'
import { shortenAddress } from 'utils'
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
import { DoubleCurrencyLogo } from './PoolDetailsHeader'
import { DetailBubble, SmallDetailBubble } from './shared'
const TokenName = styled(ThemedText.BodyPrimary)`
display: none;
@media (max-width: ${BREAKPOINTS.lg - 1}px) and (min-width: ${BREAKPOINTS.xs - 1}px) {
display: block;
}
${EllipsisStyle}
`
const TokenTextWrapper = styled(Row)<{ isClickable?: boolean }>`
gap: 8px;
margin-right: 12px;
${({ isClickable }) => isClickable && ClickableStyle}
`
const SymbolText = styled(ThemedText.BodyPrimary)`
flex-shrink: 0;
@media (max-width: ${BREAKPOINTS.lg - 1}px) and (min-width: ${BREAKPOINTS.xs - 1}px) {
color: ${({ theme }) => theme.neutral2};
}
${EllipsisStyle}
`
const CopyAddress = styled(Row)`
gap: 8px;
padding: 8px 12px;
border-radius: 20px;
background-color: ${({ theme }) => theme.surface3};
font-size: 14px;
font-weight: 535;
line-height: 16px;
width: max-content;
flex-shrink: 0;
${ClickableStyle}
`
const StyledCopyIcon = styled(Copy)`
width: 16px;
height: 16px;
color: ${({ theme }) => theme.neutral2};
flex-shrink: 0;
`
const ExplorerWrapper = styled.div`
padding: 8px;
border-radius: 20px;
background-color: ${({ theme }) => theme.surface3};
display: flex;
${ClickableStyle}
`
const ButtonsRow = styled(Row)`
gap: 8px;
flex-shrink: 0;
width: max-content;
`
interface PoolDetailsLinkProps {
address?: string
chainId?: number
tokens: (Token | undefined)[]
loading?: boolean
}
export function PoolDetailsLink({ address, chainId, tokens, loading }: PoolDetailsLinkProps) {
const theme = useTheme()
const currencies = [
useCurrency(tokens[0]?.id, chainId) ?? undefined,
useCurrency(tokens[1]?.id, chainId) ?? undefined,
]
const [, setCopied] = useCopyClipboard()
const copy = useCallback(() => {
address && setCopied(address)
}, [address, setCopied])
const isPool = tokens.length === 2
const explorerUrl =
address && chainId && getExplorerLink(chainId, address, isPool ? ExplorerDataType.ADDRESS : ExplorerDataType.TOKEN)
const navigate = useNavigate()
const isInfoExplorePageEnabled = useInfoExplorePageEnabled()
const chainName = chainIdToBackendName(chainId)
const handleTokenTextClick = useCallback(() => {
if (!isPool) {
navigate(getTokenDetailsURL({ address: tokens[0]?.id, chain: chainName, isInfoExplorePageEnabled }))
}
}, [navigate, tokens, isPool, chainName, isInfoExplorePageEnabled])
if (loading || !address || !chainId) {
return (
<Row gap="8px" padding="6px 0px">
<SmallDetailBubble />
<DetailBubble $width={117} />
</Row>
)
}
return (
<Row align="space-between">
<TokenTextWrapper
data-testid={
isPool ? `pdp-pool-logo-${tokens[0]?.symbol}-${tokens[1]?.symbol}` : `pdp-token-logo-${tokens[0]?.symbol}`
}
isClickable={!isPool}
onClick={handleTokenTextClick}
>
{isPool ? (
<DoubleCurrencyLogo chainId={chainId} currencies={currencies} small />
) : (
<CurrencyLogo currency={currencies[0]} size="20px" />
)}
<TokenName>{isPool ? <Trans>Pool</Trans> : tokens[0]?.name}</TokenName>
<SymbolText>
{isPool ? (
`${tokens[0]?.symbol} / ${tokens[1]?.symbol}`
) : (
<Row gap="4px">
{tokens[0]?.symbol} <ChevronRight size={16} color={theme.neutral2} />
</Row>
)}
</SymbolText>
</TokenTextWrapper>
<ButtonsRow>
<CopyAddress data-testid={`copy-address-${address}`} onClick={copy}>
{shortenAddress(address)}
<StyledCopyIcon />
</CopyAddress>
{explorerUrl && (
<ExternalLink href={explorerUrl} data-testid={`explorer-url-${explorerUrl}`}>
<ExplorerWrapper>
{chainId === ChainId.MAINNET ? (
<EtherscanLogo width="16px" height="16px" fill={theme.neutral2} />
) : (
<ExplorerIcon width="16px" height="16px" stroke={theme.darkMode ? 'none' : theme.neutral2} />
)}
</ExplorerWrapper>
</ExternalLink>
)}
</ButtonsRow>
</Row>
)
}
...@@ -287,7 +287,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = ` ...@@ -287,7 +287,7 @@ exports[`PoolDetailsHeader renders header text correctly 1`] = `
} }
.c12 { .c12 {
width: 32px; width: 16px;
height: 32px; height: 32px;
border-radius: 50%; border-radius: 50%;
} }
......
import { Trans } from '@lingui/macro'
import Column from 'components/Column' import Column from 'components/Column'
import Row from 'components/Row' import Row from 'components/Row'
import { LoadingBubble } from 'components/Tokens/loading' import { LoadingBubble } from 'components/Tokens/loading'
...@@ -7,15 +8,16 @@ import { usePoolData } from 'graphql/thegraph/PoolData' ...@@ -7,15 +8,16 @@ import { usePoolData } from 'graphql/thegraph/PoolData'
import NotFound from 'pages/NotFound' import NotFound from 'pages/NotFound'
import { useReducer } from 'react' import { useReducer } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { Text } from 'rebass'
import styled from 'styled-components' import styled from 'styled-components'
import { BREAKPOINTS } from 'theme' import { BREAKPOINTS } from 'theme'
import { isAddress } from 'utils' import { isAddress } from 'utils'
import { PoolDetailsHeader } from './PoolDetailsHeader' import { PoolDetailsHeader } from './PoolDetailsHeader'
import { PoolDetailsLink } from './PoolDetailsLink'
import { PoolDetailsStats } from './PoolDetailsStats' import { PoolDetailsStats } from './PoolDetailsStats'
import { PoolDetailsStatsButtons } from './PoolDetailsStatsButtons' import { PoolDetailsStatsButtons } from './PoolDetailsStatsButtons'
import { PoolDetailsTableSkeleton } from './PoolDetailsTableSkeleton' import { PoolDetailsTableSkeleton } from './PoolDetailsTableSkeleton'
import { DetailBubble, SmallDetailBubble } from './shared'
const PageWrapper = styled(Row)` const PageWrapper = styled(Row)`
padding: 48px; padding: 48px;
...@@ -55,11 +57,6 @@ const ChartHeaderBubble = styled(LoadingBubble)` ...@@ -55,11 +57,6 @@ const ChartHeaderBubble = styled(LoadingBubble)`
height: 32px; height: 32px;
` `
const LinkColumn = styled(Column)`
gap: 16px;
padding: 20px;
`
const RightColumn = styled(Column)` const RightColumn = styled(Column)`
gap: 24px; gap: 24px;
margin: 0 48px 0 auto; margin: 0 48px 0 auto;
...@@ -73,6 +70,33 @@ const RightColumn = styled(Column)` ...@@ -73,6 +70,33 @@ const RightColumn = styled(Column)`
} }
` `
const TokenDetailsWrapper = styled(Column)`
gap: 24px;
padding: 20px;
@media (max-width: ${BREAKPOINTS.lg - 1}px) and (min-width: ${BREAKPOINTS.sm}px) {
flex-direction: row;
flex-wrap: wrap;
padding: unset;
}
@media (max-width: ${BREAKPOINTS.sm - 1}px) {
padding: unset;
}
`
const TokenDetailsHeader = styled(Text)`
width: 100%;
font-size: 24px;
font-weight: 485;
line-height: 32px;
`
const LinksContainer = styled(Column)`
gap: 16px;
width: 100%;
`
export default function PoolDetailsPage() { export default function PoolDetailsPage() {
const { poolAddress, chainName } = useParams<{ const { poolAddress, chainName } = useParams<{
poolAddress: string poolAddress: string
...@@ -116,19 +140,16 @@ export default function PoolDetailsPage() { ...@@ -116,19 +140,16 @@ export default function PoolDetailsPage() {
loading={loading} loading={loading}
/> />
<PoolDetailsStats poolData={poolData} isReversed={isReversed} chainId={chainId} loading={loading} /> <PoolDetailsStats poolData={poolData} isReversed={isReversed} chainId={chainId} loading={loading} />
{(token0 || token1 || loading) && <TokenDetailsWrapper>
(loading ? ( <TokenDetailsHeader>
<LinkColumn data-testid="pdp-links-loading-skeleton"> <Trans>Links</Trans>
<DetailBubble $height={24} $width={116} /> </TokenDetailsHeader>
{Array.from({ length: 3 }).map((_, i) => ( <LinksContainer>
<Row gap="8px" key={`loading-link-row-${i}`}> <PoolDetailsLink address={poolAddress} chainId={chainId} tokens={[token0, token1]} loading={loading} />
<SmallDetailBubble /> <PoolDetailsLink address={token0?.id} chainId={chainId} tokens={[token0]} loading={loading} />
<DetailBubble $width={117} /> <PoolDetailsLink address={token1?.id} chainId={chainId} tokens={[token1]} loading={loading} />
</Row> </LinksContainer>
))} </TokenDetailsWrapper>
</LinkColumn>
) : null)}
{/* TODO(WEB-2985) replace with new Token Links component */}
</RightColumn> </RightColumn>
</PageWrapper> </PageWrapper>
) )
......
...@@ -89,9 +89,11 @@ export const useMultiChainPositionsReturnValue = { ...@@ -89,9 +89,11 @@ export const useMultiChainPositionsReturnValue = {
loading: false, loading: false,
} }
export const usdcWethPoolAddress = '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640'
export const validPoolDataResponse = { export const validPoolDataResponse = {
data: { data: {
id: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640', id: usdcWethPoolAddress,
feeTier: 500, feeTier: 500,
liquidity: parseFloat('26414803986874770777'), liquidity: parseFloat('26414803986874770777'),
sqrtPrice: parseFloat('1977320351696380862605029898750440'), sqrtPrice: parseFloat('1977320351696380862605029898750440'),
...@@ -108,7 +110,7 @@ export const validPoolDataResponse = { ...@@ -108,7 +110,7 @@ export const validPoolDataResponse = {
totalValueLockedToken1: '130641.89297715763283183', totalValueLockedToken1: '130641.89297715763283183',
totalValueLockedUSD: '399590762.8476702153638342035105795', totalValueLockedUSD: '399590762.8476702153638342035105795',
__typename: 'Pool', __typename: 'Pool',
address: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640', address: usdcWethPoolAddress,
volumeUSDChange: -17.753809465717136, volumeUSDChange: -17.753809465717136,
volumeUSDWeek: 1359911419.265625, volumeUSDWeek: 1359911419.265625,
tvlUSD: 223166198.4690675, tvlUSD: 223166198.4690675,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment