feat: [info] Add Token Description Component (#7400)
* working token details section * update decription styling * different chain explorers * remove wrap check for color extraction * move token description to its own component, add copy, make simple project query * rename styled components and add tests * remove old comment * await test fragment * fix: update description truncation from TokenDescription (#7413) * fix: update description truncation from TokenDescription * fix: use better name * fix: test if description is hidden or not (#7422) --------- Co-authored-by: Charles Bachmeier <charles@bachmeier.io> * make darker or lighter * showCopy default false * update test * remove unused styles --------- Co-authored-by: eddie <66155195+just-toby@users.noreply.github.com>
This commit is contained in:
parent
1882b14690
commit
82a194987a
22
src/components/Icons/Etherscan.tsx
Normal file
22
src/components/Icons/Etherscan.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentProps } from 'react'
|
||||||
|
|
||||||
|
export const EtherscanLogo = (props: ComponentProps<'svg'>) => (
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
stroke="transparent"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5.08042 8.66148C5.08043 8.58693 5.09517 8.51313 5.12378 8.44429C5.1524 8.37546 5.19432 8.31297 5.24716 8.26038C5.30001 8.2078 5.3627 8.16617 5.43167 8.13788C5.50064 8.1096 5.57452 8.09522 5.64907 8.09557L6.59187 8.09865C6.74218 8.09865 6.88635 8.15836 6.99263 8.26465C7.09893 8.37094 7.15865 8.5151 7.15865 8.66543V12.2303C7.26478 12.1988 7.4011 12.1652 7.55026 12.1301C7.65387 12.1058 7.74621 12.0471 7.8123 11.9637C7.87839 11.8803 7.91434 11.777 7.91432 11.6705V7.24848C7.91432 7.09814 7.97403 6.95397 8.08032 6.84766C8.1866 6.74135 8.33077 6.68162 8.4811 6.68158H9.42577C9.57609 6.68162 9.72026 6.74135 9.82655 6.84766C9.93284 6.95397 9.99255 7.09814 9.99255 7.24848V11.3526C9.99255 11.3526 10.2291 11.2569 10.4595 11.1596C10.545 11.1234 10.6181 11.0629 10.6694 10.9854C10.7208 10.908 10.7482 10.8172 10.7483 10.7242V5.83152C10.7483 5.68122 10.808 5.53707 10.9143 5.43078C11.0206 5.32449 11.1647 5.26478 11.315 5.26474H12.2597C12.41 5.26474 12.5542 5.32445 12.6604 5.43075C12.7667 5.53704 12.8265 5.6812 12.8265 5.83152V9.86056C13.6455 9.267 14.4754 8.55315 15.1341 7.69474C15.2297 7.57015 15.2929 7.42383 15.3181 7.26887C15.3434 7.1139 15.3299 6.95509 15.2788 6.8066C14.9739 5.9294 14.4894 5.12551 13.856 4.44636C13.2226 3.76722 12.4544 3.22777 11.6005 2.86256C10.7467 2.49734 9.82602 2.31439 8.89742 2.32542C7.96882 2.33645 7.05275 2.54121 6.20783 2.9266C5.36291 3.31199 4.60774 3.86952 3.99066 4.56352C3.37358 5.25751 2.90817 6.07269 2.62422 6.95689C2.34027 7.84107 2.24403 8.7748 2.34166 9.69832C2.43929 10.6218 2.72863 11.5148 3.19118 12.3201C3.27176 12.459 3.39031 12.572 3.53289 12.6459C3.67548 12.7198 3.83618 12.7514 3.99614 12.7372C4.17482 12.7215 4.3973 12.6992 4.66181 12.6681C4.77695 12.655 4.88326 12.6001 4.96048 12.5137C5.0377 12.4273 5.08043 12.3155 5.08053 12.1996L5.08042 8.66148Z"
|
||||||
|
fill={props.fill ?? '#607BEE'}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M5.05957 14.3792C6.05531 15.1036 7.23206 15.5384 8.45961 15.6356C9.68716 15.7326 10.9176 15.4883 12.0149 14.9294C13.1122 14.3705 14.0334 13.519 14.6768 12.4691C15.3201 11.4191 15.6605 10.2116 15.6601 8.98024C15.6601 8.82658 15.653 8.67457 15.6428 8.52344C13.2041 12.1605 8.70139 13.8609 5.05978 14.3786"
|
||||||
|
fill={props.fill ?? '#607BEE'}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
18
src/components/Icons/Globe.tsx
Normal file
18
src/components/Icons/Globe.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { ComponentProps } from 'react'
|
||||||
|
|
||||||
|
export const Globe = (props: ComponentProps<'svg'>) => (
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
stroke="transparent"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5.12245 9.5625C5.23495 11.8725 6.01495 14.2275 7.37245 16.32C4.19245 15.615 1.76996 12.8925 1.52246 9.5625H5.12245ZM7.37245 1.67999C4.19245 2.38499 1.76996 5.1075 1.52246 8.4375H5.12245C5.23495 6.1275 6.01495 3.77249 7.37245 1.67999ZM9.14997 1.5H8.84995L8.62496 1.82249C7.19996 3.84749 6.36745 6.1725 6.24745 8.4375H11.7525C11.6325 6.1725 10.8 3.84749 9.37496 1.82249L9.14997 1.5ZM6.24745 9.5625C6.36745 11.8275 7.19996 14.1525 8.62496 16.1775L8.84995 16.5H9.14997L9.37496 16.1775C10.8 14.1525 11.6325 11.8275 11.7525 9.5625H6.24745ZM12.8775 9.5625C12.765 11.8725 11.985 14.2275 10.6275 16.32C13.8075 15.615 16.23 12.8925 16.4775 9.5625H12.8775ZM16.4775 8.4375C16.23 5.1075 13.8075 2.38499 10.6275 1.67999C11.985 3.77249 12.765 6.1275 12.8775 8.4375H16.4775Z"
|
||||||
|
fill={props.fill ?? '#607BEE'}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
18
src/components/Icons/TwitterX.tsx
Normal file
18
src/components/Icons/TwitterX.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { ComponentProps } from 'react'
|
||||||
|
|
||||||
|
export const TwitterXLogo = (props: ComponentProps<'svg'>) => (
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
stroke="transparent"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12.8761 3H14.9451L10.4251 8.16609L15.7425 15.196H11.579L8.31797 10.9324L4.58662 15.196H2.51644L7.35104 9.67026L2.25 3H6.51922L9.46689 6.89708L12.8761 3ZM12.15 13.9576H13.2964L5.89628 4.17332H4.66605L12.15 13.9576Z"
|
||||||
|
fill={props.fill ?? '#607BEE'}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
@ -1,19 +1,14 @@
|
|||||||
import { Trans } from '@lingui/macro'
|
import { Trans } from '@lingui/macro'
|
||||||
import { ChainId } from '@uniswap/sdk-core'
|
import { ChainId } from '@uniswap/sdk-core'
|
||||||
import { getChainInfo } from 'constants/chainInfo'
|
import { getChainInfo } from 'constants/chainInfo'
|
||||||
import { darken } from 'polished'
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { ThemedText } from 'theme/components'
|
import { ThemedText } from 'theme/components'
|
||||||
import { textFadeIn } from 'theme/styles'
|
import { textFadeIn } from 'theme/styles'
|
||||||
|
|
||||||
import Resource from './Resource'
|
import Resource from './Resource'
|
||||||
|
import { NoInfoAvailable, TRUNCATE_CHARACTER_COUNT, truncateDescription, TruncateDescriptionButton } from './shared'
|
||||||
|
|
||||||
const NoInfoAvailable = styled.span`
|
|
||||||
color: ${({ theme }) => theme.neutral3};
|
|
||||||
font-weight: 485;
|
|
||||||
font-size: 16px;
|
|
||||||
`
|
|
||||||
const TokenDescriptionContainer = styled.div`
|
const TokenDescriptionContainer = styled.div`
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@ -24,32 +19,6 @@ const TokenDescriptionContainer = styled.div`
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
`
|
`
|
||||||
|
|
||||||
const TruncateDescriptionButton = styled.div`
|
|
||||||
color: ${({ theme }) => theme.neutral2};
|
|
||||||
font-weight: 485;
|
|
||||||
font-size: 0.85em;
|
|
||||||
padding-top: 0.5em;
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&:focus {
|
|
||||||
color: ${({ theme }) => darken(0.1, theme.neutral2)};
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const truncateDescription = (desc: string) => {
|
|
||||||
//trim the string to the maximum length
|
|
||||||
let tokenDescriptionTruncated = desc.slice(0, TRUNCATE_CHARACTER_COUNT)
|
|
||||||
//re-trim if we are in the middle of a word
|
|
||||||
tokenDescriptionTruncated = `${tokenDescriptionTruncated.slice(
|
|
||||||
0,
|
|
||||||
Math.min(tokenDescriptionTruncated.length, tokenDescriptionTruncated.lastIndexOf(' '))
|
|
||||||
)}...`
|
|
||||||
return tokenDescriptionTruncated
|
|
||||||
}
|
|
||||||
|
|
||||||
const TRUNCATE_CHARACTER_COUNT = 400
|
|
||||||
|
|
||||||
export const AboutContainer = styled.div`
|
export const AboutContainer = styled.div`
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 24px 0px;
|
padding: 24px 0px;
|
||||||
|
85
src/components/Tokens/TokenDetails/TokenDescription.test.tsx
Normal file
85
src/components/Tokens/TokenDetails/TokenDescription.test.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { QueryResult } from '@apollo/client'
|
||||||
|
import userEvent from '@testing-library/user-event'
|
||||||
|
import { USDC_MAINNET } from 'constants/tokens'
|
||||||
|
import { Chain, Exact, TokenProjectQuery, useTokenProjectQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||||
|
import { useCurrency } from 'hooks/Tokens'
|
||||||
|
import { mocked } from 'test-utils/mocked'
|
||||||
|
import { validTokenProjectResponse, validUSDCCurrency } from 'test-utils/pools/fixtures'
|
||||||
|
import { act, render, screen } from 'test-utils/render'
|
||||||
|
|
||||||
|
import { TokenDescription } from './TokenDescription'
|
||||||
|
|
||||||
|
jest.mock('graphql/data/__generated__/types-and-hooks', () => {
|
||||||
|
const originalModule = jest.requireActual('graphql/data/__generated__/types-and-hooks')
|
||||||
|
return {
|
||||||
|
...originalModule,
|
||||||
|
useTokenProjectQuery: jest.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
jest.mock('hooks/Tokens', () => {
|
||||||
|
const originalModule = jest.requireActual('hooks/Tokens')
|
||||||
|
return {
|
||||||
|
...originalModule,
|
||||||
|
useCurrency: jest.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const tokenAddress = USDC_MAINNET.address
|
||||||
|
|
||||||
|
describe('TokenDescription', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocked(useTokenProjectQuery).mockReturnValue(validTokenProjectResponse)
|
||||||
|
mocked(useCurrency).mockReturnValue(validUSDCCurrency)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders token information correctly with defaults', () => {
|
||||||
|
const { asFragment } = render(<TokenDescription tokenAddress={tokenAddress} showCopy />)
|
||||||
|
expect(asFragment()).toMatchSnapshot()
|
||||||
|
|
||||||
|
expect(screen.getByText('USDC')).toBeVisible()
|
||||||
|
expect(screen.getByText('USDCoin')).toBeVisible()
|
||||||
|
expect(screen.getByText('Website')).toBeVisible()
|
||||||
|
expect(screen.getByText('Twitter')).toBeVisible()
|
||||||
|
expect(screen.getByText('Etherscan')).toBeVisible()
|
||||||
|
expect(screen.getByText('0xA0b8...eB48')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('truncates description and shows more', async () => {
|
||||||
|
const { asFragment } = render(<TokenDescription tokenAddress={tokenAddress} showCopy />)
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot()
|
||||||
|
const truncatedDescription = screen.getByTestId('token-description-truncated')
|
||||||
|
const fullDescription = screen.getByTestId('token-description-full')
|
||||||
|
|
||||||
|
expect(truncatedDescription).toHaveStyleRule('display', 'inline')
|
||||||
|
expect(fullDescription).toHaveStyleRule('display', 'none')
|
||||||
|
|
||||||
|
await act(() => userEvent.click(screen.getByText('Show more')))
|
||||||
|
expect(truncatedDescription).toHaveStyleRule('display', 'none')
|
||||||
|
expect(fullDescription).toHaveStyleRule('display', 'inline')
|
||||||
|
expect(screen.getByText('Hide')).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('copy address button hidden when flagged', async () => {
|
||||||
|
const { asFragment } = render(<TokenDescription tokenAddress={tokenAddress} />)
|
||||||
|
expect(asFragment()).toMatchSnapshot()
|
||||||
|
|
||||||
|
expect(screen.queryByText('0xA0b8...eB48')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('no description or social buttons shown when not available', async () => {
|
||||||
|
mocked(useTokenProjectQuery).mockReturnValue({ data: undefined } as unknown as QueryResult<
|
||||||
|
TokenProjectQuery,
|
||||||
|
Exact<{ chain: Chain; address?: string }>
|
||||||
|
>)
|
||||||
|
const { asFragment } = render(<TokenDescription tokenAddress={tokenAddress} showCopy />)
|
||||||
|
expect(asFragment()).toMatchSnapshot()
|
||||||
|
|
||||||
|
expect(screen.getByText('No token information available')).toBeVisible()
|
||||||
|
expect(screen.queryByText('Website')).toBeNull()
|
||||||
|
expect(screen.queryByText('Twitter')).toBeNull()
|
||||||
|
expect(screen.getByText('Etherscan')).toBeVisible()
|
||||||
|
expect(screen.getByText('0xA0b8...eB48')).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
170
src/components/Tokens/TokenDetails/TokenDescription.tsx
Normal file
170
src/components/Tokens/TokenDetails/TokenDescription.tsx
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import { Trans } from '@lingui/macro'
|
||||||
|
import { ChainId } from '@uniswap/sdk-core'
|
||||||
|
import Column from 'components/Column'
|
||||||
|
import { EtherscanLogo } from 'components/Icons/Etherscan'
|
||||||
|
import { Globe } from 'components/Icons/Globe'
|
||||||
|
import { TwitterXLogo } from 'components/Icons/TwitterX'
|
||||||
|
import CurrencyLogo from 'components/Logo/CurrencyLogo'
|
||||||
|
import Row from 'components/Row'
|
||||||
|
import { NoInfoAvailable, truncateDescription, TruncateDescriptionButton } from 'components/Tokens/TokenDetails/shared'
|
||||||
|
import { useTokenProjectQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||||
|
import { chainIdToBackendName } from 'graphql/data/util'
|
||||||
|
import { useCurrency } from 'hooks/Tokens'
|
||||||
|
import { useColor } from 'hooks/useColor'
|
||||||
|
import useCopyClipboard from 'hooks/useCopyClipboard'
|
||||||
|
import { useCallback, useReducer } from 'react'
|
||||||
|
import { Copy } from 'react-feather'
|
||||||
|
import styled, { useTheme } from 'styled-components'
|
||||||
|
import { BREAKPOINTS } from 'theme'
|
||||||
|
import { ClickableStyle, EllipsisStyle, ExternalLink, ThemedText } from 'theme/components'
|
||||||
|
import { opacify } from 'theme/utils'
|
||||||
|
import { shortenAddress } from 'utils'
|
||||||
|
import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink'
|
||||||
|
|
||||||
|
const TokenInfoSection = styled(Column)`
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@media (max-width: ${BREAKPOINTS.lg - 1}px) and (min-width: ${BREAKPOINTS.sm}px) {
|
||||||
|
max-width: 45%;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TokenNameRow = styled(Row)`
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const TokenName = styled(ThemedText.BodyPrimary)`
|
||||||
|
${EllipsisStyle}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TokenButtonRow = styled(TokenNameRow)`
|
||||||
|
flex-wrap: wrap;
|
||||||
|
`
|
||||||
|
|
||||||
|
const TokenInfoButton = styled(Row)<{ tokenColor: string }>`
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: ${({ tokenColor }) => tokenColor};
|
||||||
|
background-color: ${({ tokenColor }) => opacify(12, tokenColor)};
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 535;
|
||||||
|
line-height: 16px;
|
||||||
|
width: max-content;
|
||||||
|
${ClickableStyle}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TokenDescriptionContainer = styled(ThemedText.BodyPrimary)`
|
||||||
|
${EllipsisStyle}
|
||||||
|
max-width: 100%;
|
||||||
|
// max-height: fit-content;
|
||||||
|
line-height: 24px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
`
|
||||||
|
|
||||||
|
const DescriptionVisibilityWrapper = styled.span<{ $visible: boolean }>`
|
||||||
|
display: ${({ $visible }) => ($visible ? 'inline' : 'none')};
|
||||||
|
`
|
||||||
|
|
||||||
|
const TRUNCATE_CHARACTER_COUNT = 75
|
||||||
|
|
||||||
|
export function TokenDescription({
|
||||||
|
tokenAddress,
|
||||||
|
chainId = ChainId.MAINNET,
|
||||||
|
showCopy = false,
|
||||||
|
}: {
|
||||||
|
tokenAddress: string
|
||||||
|
chainId?: number
|
||||||
|
showCopy?: boolean
|
||||||
|
}) {
|
||||||
|
const currency = useCurrency(tokenAddress, chainId)
|
||||||
|
const theme = useTheme()
|
||||||
|
const color = useColor(currency?.wrapped, theme.surface1, theme.darkMode)
|
||||||
|
const { data: tokenQuery } = useTokenProjectQuery({
|
||||||
|
variables: {
|
||||||
|
address: tokenAddress,
|
||||||
|
chain: chainIdToBackendName(chainId),
|
||||||
|
},
|
||||||
|
errorPolicy: 'all',
|
||||||
|
})
|
||||||
|
const tokenProject = tokenQuery?.token?.project
|
||||||
|
const description = tokenProject?.description
|
||||||
|
const explorerUrl = getExplorerLink(chainId, tokenAddress, ExplorerDataType.TOKEN)
|
||||||
|
|
||||||
|
const [, setCopied] = useCopyClipboard()
|
||||||
|
const copy = useCallback(() => {
|
||||||
|
setCopied(tokenAddress)
|
||||||
|
}, [tokenAddress, setCopied])
|
||||||
|
|
||||||
|
const [isDescriptionTruncated, toggleIsDescriptionTruncated] = useReducer((x) => !x, true)
|
||||||
|
const truncatedDescription = truncateDescription(description ?? '', TRUNCATE_CHARACTER_COUNT)
|
||||||
|
const shouldTruncate = !!description && description.length > TRUNCATE_CHARACTER_COUNT
|
||||||
|
const showTruncatedDescription = shouldTruncate && isDescriptionTruncated
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TokenInfoSection>
|
||||||
|
<TokenNameRow>
|
||||||
|
<CurrencyLogo currency={currency} size="20px" />
|
||||||
|
<TokenName>{currency?.name}</TokenName>
|
||||||
|
<ThemedText.BodySecondary>{currency?.symbol}</ThemedText.BodySecondary>
|
||||||
|
</TokenNameRow>
|
||||||
|
<TokenButtonRow>
|
||||||
|
{showCopy && (
|
||||||
|
<TokenInfoButton tokenColor={color} onClick={copy}>
|
||||||
|
<Copy width="18px" height="18px" color={color} />
|
||||||
|
{shortenAddress(tokenAddress)}
|
||||||
|
</TokenInfoButton>
|
||||||
|
)}
|
||||||
|
<ExternalLink href={explorerUrl}>
|
||||||
|
<TokenInfoButton tokenColor={color}>
|
||||||
|
<EtherscanLogo width="18px" height="18px" fill={color} />
|
||||||
|
{chainId === ChainId.MAINNET ? <Trans>Etherscan</Trans> : <Trans>Explorer</Trans>}
|
||||||
|
</TokenInfoButton>
|
||||||
|
</ExternalLink>
|
||||||
|
{!!tokenProject?.homepageUrl && (
|
||||||
|
<ExternalLink href={tokenProject.homepageUrl}>
|
||||||
|
<TokenInfoButton tokenColor={color}>
|
||||||
|
<Globe width="18px" height="18px" fill={color} />
|
||||||
|
<Trans>Website</Trans>
|
||||||
|
</TokenInfoButton>
|
||||||
|
</ExternalLink>
|
||||||
|
)}
|
||||||
|
{!!tokenProject?.twitterName && (
|
||||||
|
<ExternalLink href={`https://x.com/${tokenProject.twitterName}`}>
|
||||||
|
<TokenInfoButton tokenColor={color}>
|
||||||
|
<TwitterXLogo width="18px" height="18px" fill={color} />
|
||||||
|
<Trans>Twitter</Trans>
|
||||||
|
</TokenInfoButton>
|
||||||
|
</ExternalLink>
|
||||||
|
)}
|
||||||
|
</TokenButtonRow>
|
||||||
|
<TokenDescriptionContainer>
|
||||||
|
{!description && (
|
||||||
|
<NoInfoAvailable>
|
||||||
|
<Trans>No token information available</Trans>
|
||||||
|
</NoInfoAvailable>
|
||||||
|
)}
|
||||||
|
{description && (
|
||||||
|
<>
|
||||||
|
<DescriptionVisibilityWrapper data-testid="token-description-full" $visible={!showTruncatedDescription}>
|
||||||
|
{description}
|
||||||
|
</DescriptionVisibilityWrapper>
|
||||||
|
<DescriptionVisibilityWrapper data-testid="token-description-truncated" $visible={showTruncatedDescription}>
|
||||||
|
{truncatedDescription}
|
||||||
|
</DescriptionVisibilityWrapper>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{shouldTruncate && (
|
||||||
|
<TruncateDescriptionButton
|
||||||
|
onClick={toggleIsDescriptionTruncated}
|
||||||
|
data-testid="token-description-show-more-button"
|
||||||
|
>
|
||||||
|
{isDescriptionTruncated ? <Trans>Show more</Trans> : <Trans>Hide</Trans>}
|
||||||
|
</TruncateDescriptionButton>
|
||||||
|
)}
|
||||||
|
</TokenDescriptionContainer>
|
||||||
|
</TokenInfoSection>
|
||||||
|
)
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
34
src/components/Tokens/TokenDetails/shared.ts
Normal file
34
src/components/Tokens/TokenDetails/shared.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { darken } from 'polished'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
export const NoInfoAvailable = styled.span`
|
||||||
|
color: ${({ theme }) => theme.neutral3};
|
||||||
|
font-weight: 485;
|
||||||
|
font-size: 16px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const TruncateDescriptionButton = styled.div`
|
||||||
|
color: ${({ theme }) => theme.neutral2};
|
||||||
|
font-weight: 485;
|
||||||
|
font-size: 0.85em;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: ${({ theme }) => darken(0.1, theme.neutral2)};
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const truncateDescription = (desc: string, maxCharacterCount = TRUNCATE_CHARACTER_COUNT) => {
|
||||||
|
//trim the string to the maximum length
|
||||||
|
let tokenDescriptionTruncated = desc.slice(0, maxCharacterCount)
|
||||||
|
//re-trim if we are in the middle of a word
|
||||||
|
tokenDescriptionTruncated = `${tokenDescriptionTruncated.slice(
|
||||||
|
0,
|
||||||
|
Math.min(tokenDescriptionTruncated.length, tokenDescriptionTruncated.lastIndexOf(' '))
|
||||||
|
)}...`
|
||||||
|
return tokenDescriptionTruncated
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TRUNCATE_CHARACTER_COUNT = 400
|
@ -63,6 +63,32 @@ gql`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
gql`
|
||||||
|
query TokenProject($chain: Chain!, $address: String = null) {
|
||||||
|
token(chain: $chain, address: $address) {
|
||||||
|
id
|
||||||
|
decimals
|
||||||
|
name
|
||||||
|
chain
|
||||||
|
address
|
||||||
|
symbol
|
||||||
|
standard
|
||||||
|
project {
|
||||||
|
id
|
||||||
|
description
|
||||||
|
homepageUrl
|
||||||
|
twitterName
|
||||||
|
logoUrl
|
||||||
|
tokens {
|
||||||
|
id
|
||||||
|
chain
|
||||||
|
address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
export type { Chain, TokenQuery } from './__generated__/types-and-hooks'
|
export type { Chain, TokenQuery } from './__generated__/types-and-hooks'
|
||||||
|
|
||||||
export type TokenQueryData = TokenQuery['token']
|
export type TokenQueryData = TokenQuery['token']
|
||||||
|
@ -1,11 +1,26 @@
|
|||||||
import { ChainId, Token } from '@uniswap/sdk-core'
|
import { ChainId, Token } from '@uniswap/sdk-core'
|
||||||
import { DEFAULT_COLOR } from 'constants/tokenColors'
|
import { DEFAULT_COLOR } from 'constants/tokenColors'
|
||||||
import useTokenLogoSource from 'hooks/useAssetLogoSource'
|
import useTokenLogoSource from 'hooks/useAssetLogoSource'
|
||||||
import { rgb } from 'polished'
|
import { darken, lighten, rgb } from 'polished'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
|
import { WrappedTokenInfo } from 'state/lists/wrappedTokenInfo'
|
||||||
import { useTheme } from 'styled-components'
|
import { useTheme } from 'styled-components'
|
||||||
import { getColor } from 'utils/getColor'
|
import { getColor } from 'utils/getColor'
|
||||||
|
import { hex } from 'wcag-contrast'
|
||||||
|
|
||||||
|
// The WCAG AA standard color contrast threshold
|
||||||
|
const MIN_COLOR_CONTRAST_THRESHOLD = 3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compares a given color against the background color to determine if it passes the minimum contrast threshold.
|
||||||
|
* @param color The hex value of the extracted color
|
||||||
|
* @param backgroundColor The hex value of the background color to check contrast against
|
||||||
|
* @returns either 'sporeWhite' or 'sporeBlack'
|
||||||
|
*/
|
||||||
|
function passesContrast(color: string, backgroundColor: string): boolean {
|
||||||
|
const contrast = hex(color, backgroundColor)
|
||||||
|
return contrast >= MIN_COLOR_CONTRAST_THRESHOLD
|
||||||
|
}
|
||||||
|
|
||||||
function URIForEthToken(address: string) {
|
function URIForEthToken(address: string) {
|
||||||
return `https://raw.githubusercontent.com/uniswap/assets/master/blockchains/ethereum/assets/${address}/logo.png`
|
return `https://raw.githubusercontent.com/uniswap/assets/master/blockchains/ethereum/assets/${address}/logo.png`
|
||||||
@ -20,10 +35,6 @@ function URIForEthToken(address: string) {
|
|||||||
* @returns {Promise< | null>} A promise that resolves to a color string or null if color cannot be determined.
|
* @returns {Promise< | null>} A promise that resolves to a color string or null if color cannot be determined.
|
||||||
*/
|
*/
|
||||||
async function getColorFromToken(token: Token, primarySrc?: string): Promise<string | null> {
|
async function getColorFromToken(token: Token, primarySrc?: string): Promise<string | null> {
|
||||||
if (!(token instanceof WrappedTokenInfo)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrappedToken = token as WrappedTokenInfo
|
const wrappedToken = token as WrappedTokenInfo
|
||||||
let color: string | null = null
|
let color: string | null = null
|
||||||
|
|
||||||
@ -33,7 +44,7 @@ async function getColorFromToken(token: Token, primarySrc?: string): Promise<str
|
|||||||
color = colorArray === DEFAULT_COLOR ? null : convertColorArrayToString(colorArray)
|
color = colorArray === DEFAULT_COLOR ? null : convertColorArrayToString(colorArray)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!color && wrappedToken.logoURI) {
|
if (!color && wrappedToken?.logoURI) {
|
||||||
const colorArray = await getColor(wrappedToken.logoURI)
|
const colorArray = await getColor(wrappedToken.logoURI)
|
||||||
color = colorArray === DEFAULT_COLOR ? null : convertColorArrayToString(colorArray)
|
color = colorArray === DEFAULT_COLOR ? null : convertColorArrayToString(colorArray)
|
||||||
}
|
}
|
||||||
@ -54,7 +65,7 @@ function convertColorArrayToString([red, green, blue]: number[]): string {
|
|||||||
return rgb({ red, green, blue })
|
return rgb({ red, green, blue })
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useColor(token?: Token) {
|
export function useColor(token?: Token, backgroundColor?: string, makeLighter?: boolean) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const [color, setColor] = useState(theme.accent1)
|
const [color, setColor] = useState(theme.accent1)
|
||||||
const [src] = useTokenLogoSource(token?.address, token?.chainId, token?.isNative)
|
const [src] = useTokenLogoSource(token?.address, token?.chainId, token?.isNative)
|
||||||
@ -65,6 +76,13 @@ export function useColor(token?: Token) {
|
|||||||
if (token) {
|
if (token) {
|
||||||
getColorFromToken(token, src).then((tokenColor) => {
|
getColorFromToken(token, src).then((tokenColor) => {
|
||||||
if (!stale && tokenColor !== null) {
|
if (!stale && tokenColor !== null) {
|
||||||
|
if (backgroundColor) {
|
||||||
|
let increment = 0.1
|
||||||
|
while (!passesContrast(tokenColor, backgroundColor)) {
|
||||||
|
tokenColor = makeLighter ? lighten(increment, tokenColor) : darken(increment, tokenColor)
|
||||||
|
increment += 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
setColor(tokenColor)
|
setColor(tokenColor)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -74,7 +92,7 @@ export function useColor(token?: Token) {
|
|||||||
stale = true
|
stale = true
|
||||||
setColor(theme.accent1)
|
setColor(theme.accent1)
|
||||||
}
|
}
|
||||||
}, [src, theme.accent1, token])
|
}, [backgroundColor, makeLighter, src, theme.accent1, token])
|
||||||
|
|
||||||
return color
|
return color
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
|
import { enableNetConnect } from 'nock'
|
||||||
import { validPoolDataResponse } from 'test-utils/pools/fixtures'
|
import { validPoolDataResponse } from 'test-utils/pools/fixtures'
|
||||||
import { render, screen } from 'test-utils/render'
|
import { act, render, screen } from 'test-utils/render'
|
||||||
import { BREAKPOINTS } from 'theme'
|
import { BREAKPOINTS } from 'theme'
|
||||||
|
|
||||||
import { PoolDetailsStats } from './PoolDetailsStats'
|
import { PoolDetailsStats } from './PoolDetailsStats'
|
||||||
@ -11,8 +12,18 @@ describe('PoolDetailsStats', () => {
|
|||||||
chainId: 1,
|
chainId: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
it('renders stats text correctly', () => {
|
beforeEach(() => {
|
||||||
|
// Enable network connections for retrieving token logos
|
||||||
|
enableNetConnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders stats text correctly', async () => {
|
||||||
const { asFragment } = render(<PoolDetailsStats {...mockProps} />)
|
const { asFragment } = render(<PoolDetailsStats {...mockProps} />)
|
||||||
|
// After the first render, the extracted color is updated to an a11y compliant color
|
||||||
|
// This is why we need to wrap the fragment in act(...)
|
||||||
|
await act(async () => {
|
||||||
|
await asFragment
|
||||||
|
})
|
||||||
expect(asFragment()).toMatchSnapshot()
|
expect(asFragment()).toMatchSnapshot()
|
||||||
|
|
||||||
expect(screen.getByText(/Stats/i)).toBeInTheDocument()
|
expect(screen.getByText(/Stats/i)).toBeInTheDocument()
|
||||||
@ -23,13 +34,16 @@ describe('PoolDetailsStats', () => {
|
|||||||
expect(screen.getByTestId('pool-balance-chart')).toBeInTheDocument()
|
expect(screen.getByTestId('pool-balance-chart')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('pool balance chart not visible on mobile', () => {
|
it('pool balance chart not visible on mobile', async () => {
|
||||||
Object.defineProperty(window, 'innerWidth', {
|
Object.defineProperty(window, 'innerWidth', {
|
||||||
writable: true,
|
writable: true,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: BREAKPOINTS.md,
|
value: BREAKPOINTS.md,
|
||||||
})
|
})
|
||||||
const { asFragment } = render(<PoolDetailsStats {...mockProps} />)
|
const { asFragment } = render(<PoolDetailsStats {...mockProps} />)
|
||||||
|
await act(async () => {
|
||||||
|
await asFragment
|
||||||
|
})
|
||||||
expect(asFragment()).toMatchSnapshot()
|
expect(asFragment()).toMatchSnapshot()
|
||||||
|
|
||||||
expect(screen.queryByTestId('pool-balance-chart')).toBeNull()
|
expect(screen.queryByTestId('pool-balance-chart')).toBeNull()
|
||||||
|
@ -105,8 +105,8 @@ export function PoolDetailsStats({ poolData, isReversed, chainId }: PoolDetailsS
|
|||||||
const currency0 = useCurrency(poolData?.token0?.id, chainId) ?? undefined
|
const currency0 = useCurrency(poolData?.token0?.id, chainId) ?? undefined
|
||||||
const currency1 = useCurrency(poolData?.token1?.id, chainId) ?? undefined
|
const currency1 = useCurrency(poolData?.token1?.id, chainId) ?? undefined
|
||||||
|
|
||||||
const color0 = useColor(currency0?.wrapped)
|
const color0 = useColor(currency0?.wrapped, theme.surface2, theme.darkMode)
|
||||||
let color1 = useColor(currency1?.wrapped)
|
let color1 = useColor(currency1?.wrapped, theme.surface2, theme.darkMode)
|
||||||
if (color0 === color1 && color0 === theme.accent1) {
|
if (color0 === color1 && color0 === theme.accent1) {
|
||||||
color1 = colors.blue400
|
color1 = colors.blue400
|
||||||
}
|
}
|
||||||
|
@ -506,7 +506,7 @@ exports[`PoolDetailsStats renders stats text correctly 1`] = `
|
|||||||
.c9 {
|
.c9 {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
width: 40.698463777008904%;
|
width: 40.698463777008904%;
|
||||||
background: #FC72FF;
|
background: #0066d9;
|
||||||
border-top-left-radius: 5px;
|
border-top-left-radius: 5px;
|
||||||
border-bottom-left-radius: 5px;
|
border-bottom-left-radius: 5px;
|
||||||
border-right: 1px solid #F9F9F9;
|
border-right: 1px solid #F9F9F9;
|
||||||
@ -515,7 +515,7 @@ exports[`PoolDetailsStats renders stats text correctly 1`] = `
|
|||||||
.c10 {
|
.c10 {
|
||||||
height: 8px;
|
height: 8px;
|
||||||
width: 59.3015362229911%;
|
width: 59.3015362229911%;
|
||||||
background: #4C82FB;
|
background: #FC72FF;
|
||||||
border-top-right-radius: 5px;
|
border-top-right-radius: 5px;
|
||||||
border-bottom-right-radius: 5px;
|
border-bottom-right-radius: 5px;
|
||||||
border-left: 1px solid #F9F9F9;
|
border-left: 1px solid #F9F9F9;
|
||||||
|
@ -110,6 +110,25 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the
|
|||||||
color: #222222;
|
color: #222222;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c43 {
|
||||||
|
-webkit-text-decoration: none;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-transition-duration: 125ms;
|
||||||
|
transition-duration: 125ms;
|
||||||
|
color: #FC72FF;
|
||||||
|
stroke: #FC72FF;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c43:hover {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c43:active {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
.c3 {
|
.c3 {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
display: -webkit-flex;
|
display: -webkit-flex;
|
||||||
@ -124,6 +143,97 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c40 {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity 250ms ease-in;
|
||||||
|
transition: opacity 250ms ease-in;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c39 {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: #22222212;
|
||||||
|
-webkit-transition: background-color 250ms ease-in;
|
||||||
|
transition: background-color 250ms ease-in;
|
||||||
|
box-shadow: 0 0 1px white;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c38 {
|
||||||
|
position: relative;
|
||||||
|
display: -webkit-box;
|
||||||
|
display: -webkit-flex;
|
||||||
|
display: -ms-flexbox;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c46 {
|
||||||
|
color: #CECECE;
|
||||||
|
font-weight: 485;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c36 {
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c37 {
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c41 {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c42 {
|
||||||
|
-webkit-flex-wrap: wrap;
|
||||||
|
-ms-flex-wrap: wrap;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c44 {
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: #FC72FF;
|
||||||
|
background-color: #FC72FF1f;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 535;
|
||||||
|
line-height: 16px;
|
||||||
|
width: -webkit-max-content;
|
||||||
|
width: -moz-max-content;
|
||||||
|
width: max-content;
|
||||||
|
-webkit-text-decoration: none;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-transition-duration: 125ms;
|
||||||
|
transition-duration: 125ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c44:hover {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c44:active {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c45 {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
|
line-height: 24px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.c21 {
|
.c21 {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
@ -381,6 +491,24 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the
|
|||||||
min-width: 360px;
|
min-width: 360px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.c34 {
|
||||||
|
gap: 24px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.c35 {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 485;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:1023px) and (min-width:640px) {
|
||||||
|
.c36 {
|
||||||
|
max-width: 45%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width:1023px) {
|
@media (max-width:1023px) {
|
||||||
.c23 {
|
.c23 {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -474,6 +602,24 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width:1023px) and (min-width:640px) {
|
||||||
|
.c34 {
|
||||||
|
-webkit-flex-direction: row;
|
||||||
|
-ms-flex-direction: row;
|
||||||
|
flex-direction: row;
|
||||||
|
-webkit-flex-wrap: wrap;
|
||||||
|
-ms-flex-wrap: wrap;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width:639px) {
|
||||||
|
.c34 {
|
||||||
|
padding: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="c0 c1 c2"
|
class="c0 c1 c2"
|
||||||
>
|
>
|
||||||
@ -741,6 +887,163 @@ exports[`PoolDetailsPage pool header is displayed when data is received from the
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="c3 c34"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c35 css-vurnku"
|
||||||
|
>
|
||||||
|
Info
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="c3 c36"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c0 c1 c37"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c38"
|
||||||
|
style="height: 20px; width: 20px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c39"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="UNKNOWN logo"
|
||||||
|
class="c40"
|
||||||
|
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="c7 c41 css-1urox24"
|
||||||
|
>
|
||||||
|
Unknown Token
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="c6 css-1urox24"
|
||||||
|
>
|
||||||
|
UNKNOWN
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="c0 c1 c37 c42"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="c43"
|
||||||
|
href="https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c0 c1 c44"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="#FC72FF"
|
||||||
|
height="18px"
|
||||||
|
stroke="transparent"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
width="18px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5.08042 8.66148C5.08043 8.58693 5.09517 8.51313 5.12378 8.44429C5.1524 8.37546 5.19432 8.31297 5.24716 8.26038C5.30001 8.2078 5.3627 8.16617 5.43167 8.13788C5.50064 8.1096 5.57452 8.09522 5.64907 8.09557L6.59187 8.09865C6.74218 8.09865 6.88635 8.15836 6.99263 8.26465C7.09893 8.37094 7.15865 8.5151 7.15865 8.66543V12.2303C7.26478 12.1988 7.4011 12.1652 7.55026 12.1301C7.65387 12.1058 7.74621 12.0471 7.8123 11.9637C7.87839 11.8803 7.91434 11.777 7.91432 11.6705V7.24848C7.91432 7.09814 7.97403 6.95397 8.08032 6.84766C8.1866 6.74135 8.33077 6.68162 8.4811 6.68158H9.42577C9.57609 6.68162 9.72026 6.74135 9.82655 6.84766C9.93284 6.95397 9.99255 7.09814 9.99255 7.24848V11.3526C9.99255 11.3526 10.2291 11.2569 10.4595 11.1596C10.545 11.1234 10.6181 11.0629 10.6694 10.9854C10.7208 10.908 10.7482 10.8172 10.7483 10.7242V5.83152C10.7483 5.68122 10.808 5.53707 10.9143 5.43078C11.0206 5.32449 11.1647 5.26478 11.315 5.26474H12.2597C12.41 5.26474 12.5542 5.32445 12.6604 5.43075C12.7667 5.53704 12.8265 5.6812 12.8265 5.83152V9.86056C13.6455 9.267 14.4754 8.55315 15.1341 7.69474C15.2297 7.57015 15.2929 7.42383 15.3181 7.26887C15.3434 7.1139 15.3299 6.95509 15.2788 6.8066C14.9739 5.9294 14.4894 5.12551 13.856 4.44636C13.2226 3.76722 12.4544 3.22777 11.6005 2.86256C10.7467 2.49734 9.82602 2.31439 8.89742 2.32542C7.96882 2.33645 7.05275 2.54121 6.20783 2.9266C5.36291 3.31199 4.60774 3.86952 3.99066 4.56352C3.37358 5.25751 2.90817 6.07269 2.62422 6.95689C2.34027 7.84107 2.24403 8.7748 2.34166 9.69832C2.43929 10.6218 2.72863 11.5148 3.19118 12.3201C3.27176 12.459 3.39031 12.572 3.53289 12.6459C3.67548 12.7198 3.83618 12.7514 3.99614 12.7372C4.17482 12.7215 4.3973 12.6992 4.66181 12.6681C4.77695 12.655 4.88326 12.6001 4.96048 12.5137C5.0377 12.4273 5.08043 12.3155 5.08053 12.1996L5.08042 8.66148Z"
|
||||||
|
fill="#FC72FF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M5.05957 14.3792C6.05531 15.1036 7.23206 15.5384 8.45961 15.6356C9.68716 15.7326 10.9176 15.4883 12.0149 14.9294C13.1122 14.3705 14.0334 13.519 14.6768 12.4691C15.3201 11.4191 15.6605 10.2116 15.6601 8.98024C15.6601 8.82658 15.653 8.67457 15.6428 8.52344C13.2041 12.1605 8.70139 13.8609 5.05978 14.3786"
|
||||||
|
fill="#FC72FF"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Etherscan
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="c7 c45 css-1urox24"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="c46"
|
||||||
|
>
|
||||||
|
No token information available
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="c3 c36"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c0 c1 c37"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c38"
|
||||||
|
style="height: 20px; width: 20px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c39"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="WETH logo"
|
||||||
|
class="c40"
|
||||||
|
src="https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="c7 c41 css-1urox24"
|
||||||
|
>
|
||||||
|
Wrapped Ether
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="c6 css-1urox24"
|
||||||
|
>
|
||||||
|
WETH
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="c0 c1 c37 c42"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="c43"
|
||||||
|
href="https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="c0 c1 c44"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
fill="#FC72FF"
|
||||||
|
height="18px"
|
||||||
|
stroke="transparent"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
width="18px"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M5.08042 8.66148C5.08043 8.58693 5.09517 8.51313 5.12378 8.44429C5.1524 8.37546 5.19432 8.31297 5.24716 8.26038C5.30001 8.2078 5.3627 8.16617 5.43167 8.13788C5.50064 8.1096 5.57452 8.09522 5.64907 8.09557L6.59187 8.09865C6.74218 8.09865 6.88635 8.15836 6.99263 8.26465C7.09893 8.37094 7.15865 8.5151 7.15865 8.66543V12.2303C7.26478 12.1988 7.4011 12.1652 7.55026 12.1301C7.65387 12.1058 7.74621 12.0471 7.8123 11.9637C7.87839 11.8803 7.91434 11.777 7.91432 11.6705V7.24848C7.91432 7.09814 7.97403 6.95397 8.08032 6.84766C8.1866 6.74135 8.33077 6.68162 8.4811 6.68158H9.42577C9.57609 6.68162 9.72026 6.74135 9.82655 6.84766C9.93284 6.95397 9.99255 7.09814 9.99255 7.24848V11.3526C9.99255 11.3526 10.2291 11.2569 10.4595 11.1596C10.545 11.1234 10.6181 11.0629 10.6694 10.9854C10.7208 10.908 10.7482 10.8172 10.7483 10.7242V5.83152C10.7483 5.68122 10.808 5.53707 10.9143 5.43078C11.0206 5.32449 11.1647 5.26478 11.315 5.26474H12.2597C12.41 5.26474 12.5542 5.32445 12.6604 5.43075C12.7667 5.53704 12.8265 5.6812 12.8265 5.83152V9.86056C13.6455 9.267 14.4754 8.55315 15.1341 7.69474C15.2297 7.57015 15.2929 7.42383 15.3181 7.26887C15.3434 7.1139 15.3299 6.95509 15.2788 6.8066C14.9739 5.9294 14.4894 5.12551 13.856 4.44636C13.2226 3.76722 12.4544 3.22777 11.6005 2.86256C10.7467 2.49734 9.82602 2.31439 8.89742 2.32542C7.96882 2.33645 7.05275 2.54121 6.20783 2.9266C5.36291 3.31199 4.60774 3.86952 3.99066 4.56352C3.37358 5.25751 2.90817 6.07269 2.62422 6.95689C2.34027 7.84107 2.24403 8.7748 2.34166 9.69832C2.43929 10.6218 2.72863 11.5148 3.19118 12.3201C3.27176 12.459 3.39031 12.572 3.53289 12.6459C3.67548 12.7198 3.83618 12.7514 3.99614 12.7372C4.17482 12.7215 4.3973 12.6992 4.66181 12.6681C4.77695 12.655 4.88326 12.6001 4.96048 12.5137C5.0377 12.4273 5.08043 12.3155 5.08053 12.1996L5.08042 8.66148Z"
|
||||||
|
fill="#FC72FF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M5.05957 14.3792C6.05531 15.1036 7.23206 15.5384 8.45961 15.6356C9.68716 15.7326 10.9176 15.4883 12.0149 14.9294C13.1122 14.3705 14.0334 13.519 14.6768 12.4691C15.3201 11.4191 15.6605 10.2116 15.6601 8.98024C15.6601 8.82658 15.653 8.67457 15.6428 8.52344C13.2041 12.1605 8.70139 13.8609 5.05978 14.3786"
|
||||||
|
fill="#FC72FF"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Etherscan
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="c7 c45 css-1urox24"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="c46"
|
||||||
|
>
|
||||||
|
No token information available
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DocumentFragment>
|
</DocumentFragment>
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
|
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 { TokenDescription } from 'components/Tokens/TokenDetails/TokenDescription'
|
||||||
import { getValidUrlChainName, supportedChainIdFromGQLChain } from 'graphql/data/util'
|
import { getValidUrlChainName, supportedChainIdFromGQLChain } from 'graphql/data/util'
|
||||||
import { usePoolData } from 'graphql/thegraph/PoolData'
|
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'
|
||||||
@ -40,6 +43,28 @@ 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;
|
||||||
|
`
|
||||||
|
|
||||||
export default function PoolDetailsPage() {
|
export default function PoolDetailsPage() {
|
||||||
const { poolAddress, chainName } = useParams<{
|
const { poolAddress, chainName } = useParams<{
|
||||||
poolAddress: string
|
poolAddress: string
|
||||||
@ -70,6 +95,15 @@ export default function PoolDetailsPage() {
|
|||||||
<RightColumn>
|
<RightColumn>
|
||||||
<PoolDetailsStatsButtons chainId={chainId} token0={token0} token1={token1} feeTier={poolData?.feeTier} />
|
<PoolDetailsStatsButtons chainId={chainId} token0={token0} token1={token1} feeTier={poolData?.feeTier} />
|
||||||
{poolData && <PoolDetailsStats poolData={poolData} isReversed={isReversed} chainId={chainId} />}
|
{poolData && <PoolDetailsStats poolData={poolData} isReversed={isReversed} chainId={chainId} />}
|
||||||
|
{(token0 || token1) && (
|
||||||
|
<TokenDetailsWrapper>
|
||||||
|
<TokenDetailsHeader>
|
||||||
|
<Trans>Info</Trans>
|
||||||
|
</TokenDetailsHeader>
|
||||||
|
{token0 && <TokenDescription tokenAddress={token0.id} chainId={chainId} />}
|
||||||
|
{token1 && <TokenDescription tokenAddress={token1.id} chainId={chainId} />}
|
||||||
|
</TokenDetailsWrapper>
|
||||||
|
)}
|
||||||
</RightColumn>
|
</RightColumn>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
)
|
)
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import { QueryResult } from '@apollo/client'
|
||||||
import { BigNumber } from '@ethersproject/bignumber'
|
import { BigNumber } from '@ethersproject/bignumber'
|
||||||
import { ChainId, WETH9 } from '@uniswap/sdk-core'
|
import { ChainId, Currency, WETH9 } from '@uniswap/sdk-core'
|
||||||
import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk'
|
import { FeeAmount, Pool, Position } from '@uniswap/v3-sdk'
|
||||||
import { USDC_MAINNET } from 'constants/tokens'
|
import { USDC_MAINNET } from 'constants/tokens'
|
||||||
|
import { Chain, Exact, TokenProjectQuery } from 'graphql/data/__generated__/types-and-hooks'
|
||||||
import { Token } from 'graphql/thegraph/__generated__/types-and-hooks'
|
import { Token } from 'graphql/thegraph/__generated__/types-and-hooks'
|
||||||
|
import { PoolData } from 'graphql/thegraph/PoolData'
|
||||||
|
|
||||||
export const validParams = { poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640', chainName: 'ethereum' }
|
export const validParams = { poolAddress: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640', chainName: 'ethereum' }
|
||||||
|
|
||||||
@ -15,6 +18,21 @@ export const validPoolToken0 = {
|
|||||||
__typename: 'Token',
|
__typename: 'Token',
|
||||||
} as Token
|
} as Token
|
||||||
|
|
||||||
|
export const validUSDCCurrency = {
|
||||||
|
isNative: false,
|
||||||
|
isToken: true,
|
||||||
|
name: 'USDCoin',
|
||||||
|
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
||||||
|
symbol: 'USDC',
|
||||||
|
decimals: 6,
|
||||||
|
chainId: 1,
|
||||||
|
logoURI:
|
||||||
|
'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png',
|
||||||
|
_checksummedAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
||||||
|
_tags: null,
|
||||||
|
wrapped: validPoolToken0,
|
||||||
|
} as unknown as Currency
|
||||||
|
|
||||||
export const validPoolToken1 = {
|
export const validPoolToken1 = {
|
||||||
id: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
|
id: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
|
||||||
symbol: 'WETH',
|
symbol: 'WETH',
|
||||||
@ -97,7 +115,32 @@ export const validPoolDataResponse = {
|
|||||||
tvlUSDChange: -0.3657085465786977,
|
tvlUSDChange: -0.3657085465786977,
|
||||||
tvlToken0: 90930713.7356909,
|
tvlToken0: 90930713.7356909,
|
||||||
tvlToken1: 82526.48678530742,
|
tvlToken1: 82526.48678530742,
|
||||||
},
|
} as PoolData,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: false,
|
error: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const validTokenProjectResponse = {
|
||||||
|
data: {
|
||||||
|
token: {
|
||||||
|
id: 'VG9rZW46RVRIRVJFVU1fMHhBMGI4Njk5MWM2MjE4YjM2YzFkMTlENGEyZTlFYjBjRTM2MDZlQjQ4',
|
||||||
|
decimals: 6,
|
||||||
|
name: 'USD Coin',
|
||||||
|
chain: 'ETHEREUM',
|
||||||
|
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
|
||||||
|
symbol: 'USDC',
|
||||||
|
standard: 'ERC20',
|
||||||
|
project: {
|
||||||
|
id: 'VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YTBiODY5OTFjNjIxOGIzNmMxZDE5ZDRhMmU5ZWIwY2UzNjA2ZWI0OF9VU0RD',
|
||||||
|
description:
|
||||||
|
'USDC is a fully collateralized US dollar stablecoin. USDC is the bridge between dollars and trading on cryptocurrency exchanges. The technology behind CENTRE makes it possible to exchange value between people, businesses and financial institutions just like email between mail services and texts between SMS providers. We believe by removing artificial economic borders, we can create a more inclusive global economy.',
|
||||||
|
homepageUrl: 'https://www.circle.com/en/usdc',
|
||||||
|
twitterName: 'circle',
|
||||||
|
logoUrl:
|
||||||
|
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png',
|
||||||
|
__typename: 'TokenProject',
|
||||||
|
},
|
||||||
|
__typename: 'Token',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as QueryResult<TokenProjectQuery, Exact<{ chain: Chain; address?: string }>>
|
||||||
|
Loading…
Reference in New Issue
Block a user