fix: chart design fixes & style updates (#4341)

* removed ticks outside of hover
* simplifying copyhelper
* finished implementing fred's feedback
* addressed PR comments
* fixed more of fred's feedback
This commit is contained in:
cartcrom 2022-08-16 19:59:02 -04:00 committed by GitHub
parent 8efc5af2bc
commit d6d0a98afe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 10936 additions and 285 deletions

3
src/assets/svg/share.svg Normal file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="#fff">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4m14-7-5-5-5 5m5-5v12"/>
</svg>

After

Width:  |  Height:  |  Size: 257 B

@ -1,64 +0,0 @@
import { Trans } from '@lingui/macro'
import useCopyClipboard from 'hooks/useCopyClipboard'
import React, { useCallback } from 'react'
import { CheckCircle, Copy } from 'react-feather'
import styled from 'styled-components/macro'
import { LinkStyledButton } from 'theme'
const CopyIcon = styled(LinkStyledButton)`
color: ${({ color, theme }) => color || theme.accentAction};
flex-shrink: 0;
display: flex;
text-decoration: none;
:hover,
:active,
:focus {
text-decoration: none;
color: ${({ color, theme }) => color || theme.accentAction};
}
`
const StyledText = styled.span`
margin-left: 0.25rem;
${({ theme }) => theme.flexRowNoWrap};
align-items: center;
`
const Copied = ({ iconSize }: { iconSize?: number }) => (
<StyledText>
<CheckCircle size={iconSize ?? '16'} />
<StyledText>
<Trans>Copied</Trans>
</StyledText>
</StyledText>
)
const Icon = ({ iconSize }: { iconSize?: number }) => (
<StyledText>
<Copy size={iconSize ?? '16'} />
</StyledText>
)
interface BaseProps {
toCopy: string
color?: string
iconSize?: number
iconPosition?: 'left' | 'right'
}
export type CopyHelperProps = BaseProps & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, keyof BaseProps>
export default function CopyHelper({ color, toCopy, children, iconSize, iconPosition }: CopyHelperProps) {
const [isCopied, setCopied] = useCopyClipboard()
const copy = useCallback(() => {
setCopied(toCopy)
}, [toCopy, setCopied])
return (
<CopyIcon onClick={copy} color={color}>
{iconPosition === 'left' ? isCopied ? <Copied iconSize={iconSize} /> : <Icon iconSize={iconSize} /> : null}
{iconPosition === 'left' && <>&nbsp;</>}
{isCopied ? '' : children}
{iconPosition === 'right' && <>&nbsp;</>}
{iconPosition === 'right' ? isCopied ? <Copied iconSize={iconSize} /> : <Icon iconSize={iconSize} /> : null}
</CopyIcon>
)
}

@ -1,6 +1,5 @@
import { Trans } from '@lingui/macro'
import { useWeb3React } from '@web3-react/core'
import CopyHelper from 'components/AccountDetails/Copy'
import { getConnection, getConnectionName, getIsCoinbaseWallet, getIsMetaMask } from 'connection/utils'
import { Context, useCallback, useContext } from 'react'
import { ExternalLink as LinkIcon } from 'react-feather'
@ -13,7 +12,7 @@ import { isMobile } from 'utils/userAgent'
import { ReactComponent as Close } from '../../assets/images/x.svg'
import { clearAllTransactions } from '../../state/transactions/reducer'
import { ExternalLink, LinkStyledButton, ThemedText } from '../../theme'
import { CopyHelper, ExternalLink, LinkStyledButton, ThemedText } from '../../theme'
import { shortenAddress } from '../../utils'
import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink'
import { ButtonSecondary } from '../Button'
@ -130,11 +129,12 @@ const AccountControl = styled.div`
`
const AddressLink = styled(ExternalLink)`
font-size: 0.825rem;
color: ${({ theme }) => theme.deprecated_text3};
margin-left: 1rem;
font-size: 0.825rem;
display: flex;
gap: 6px;
text-decoration: none !important;
:hover {
color: ${({ theme }) => theme.deprecated_text2};
}
@ -284,7 +284,7 @@ export default function AccountDetails({
<AccountControl>
<div>
{account && (
<CopyHelper toCopy={account} iconPosition="left">
<CopyHelper toCopy={account} gap={6} iconSize={16} fontSize={14}>
<Trans>Copy Address</Trans>
</CopyHelper>
)}

@ -11,7 +11,7 @@ interface LineChartProps<T> {
data: T[]
getX: (t: T) => number
getY: (t: T) => number
marginTop: number
marginTop?: number
curve?: CurveFactory
color?: Color
strokeWidth: number

@ -12,6 +12,7 @@ import { useAtom } from 'jotai'
import { useCallback, useState } from 'react'
import { ArrowDownRight, ArrowUpRight } from 'react-feather'
import styled from 'styled-components/macro'
import { OPACITY_HOVER } from 'theme'
import {
dayHourFormatter,
hourFormatter,
@ -32,7 +33,7 @@ const TIME_DISPLAYS: [TimePeriod, string][] = [
[TimePeriod.week, '1W'],
[TimePeriod.month, '1M'],
[TimePeriod.year, '1Y'],
[TimePeriod.all, 'ALL'],
[TimePeriod.all, 'All'],
]
type PricePoint = { value: number; timestamp: number }
@ -64,11 +65,6 @@ function getDelta(start: number, current: number) {
return [formattedDelta, <StyledDownArrow size={16} key="arrow-down" />]
}
export const ChartWrapper = styled.div`
position: relative;
overflow: visible;
`
export const ChartHeader = styled.div`
position: absolute;
`
@ -111,6 +107,9 @@ const TimeButton = styled.button<{ active: boolean }>`
border: none;
cursor: pointer;
color: ${({ theme, active }) => (active ? theme.textPrimary : theme.textSecondary)};
:hover {
${({ active }) => !active && `opacity: ${OPACITY_HOVER};`}
}
`
function getTicks(startTimestamp: number, endTimestamp: number, numTicks = 5) {
@ -142,7 +141,7 @@ function tickFormat(
}
}
const margin = { top: 86, bottom: 32, crosshair: 72 }
const margin = { top: 86, bottom: 48, crosshair: 72 }
const timeOptionsHeight = 44
const crosshairDateOverhang = 80
@ -208,7 +207,7 @@ export function PriceChart({ width, height }: PriceChartProps) {
const crosshairAtEdge = !!selected.xCoordinate && selected.xCoordinate > crosshairEdgeMax
return (
<ChartWrapper>
<>
<ChartHeader>
<TokenPrice>${selected.pricePoint.value.toFixed(2)}</TokenPrice>
<DeltaContainer>
@ -227,24 +226,24 @@ export function PriceChart({ width, height }: PriceChartProps) {
width={graphWidth}
height={graphHeight}
>
<AxisBottom
scale={timeScale}
stroke={theme.backgroundOutline}
tickFormat={tickFormatter}
tickStroke={theme.backgroundOutline}
tickLength={4}
tickTransform={'translate(0 -5)'}
tickValues={ticks}
top={graphHeight - 1}
tickLabelProps={() => ({
fill: theme.textSecondary,
fontSize: 12,
textAnchor: 'middle',
transform: 'translate(0 -24)',
})}
/>
{selected.xCoordinate !== null && (
{selected.xCoordinate !== null ? (
<g>
<AxisBottom
scale={timeScale}
stroke={theme.backgroundOutline}
tickFormat={tickFormatter}
tickStroke={theme.backgroundOutline}
tickLength={4}
tickTransform={'translate(0 -5)'}
tickValues={ticks}
top={graphHeight - 1}
tickLabelProps={() => ({
fill: theme.textSecondary,
fontSize: 12,
textAnchor: 'middle',
transform: 'translate(0 -24)',
})}
/>
<text
x={selected.xCoordinate + (crosshairAtEdge ? -4 : 4)}
y={margin.crosshair + 10}
@ -271,6 +270,8 @@ export function PriceChart({ width, height }: PriceChartProps) {
strokeWidth={2}
/>
</g>
) : (
<AxisBottom scale={timeScale} stroke={theme.backgroundOutline} top={graphHeight - 1} hideTicks />
)}
<rect
x={0}
@ -293,7 +294,7 @@ export function PriceChart({ width, height }: PriceChartProps) {
))}
</TimeOptionsContainer>
</TimeOptionsWrapper>
</ChartWrapper>
</>
)
}

@ -1,17 +1,18 @@
import { Trans } from '@lingui/macro'
import CopyHelper from 'components/AccountDetails/Copy'
import Column from 'components/Column'
import useTheme from 'hooks/useTheme'
import { AlertOctagon } from 'react-feather'
import styled from 'styled-components/macro'
import { ExternalLink, ThemedText } from 'theme'
import { CopyHelper } from '../../theme'
import Modal from '../Modal'
const ContentWrapper = styled(Column)`
align-items: center;
margin: 32px;
text-align: center;
font-size: 12px;
`
const WarningIcon = styled(AlertOctagon)`
min-height: 22px;
@ -49,7 +50,14 @@ export default function ConnectedAccountBlocked(props: ConnectedAccountBlockedPr
<ThemedText.DeprecatedMain fontSize={12}>
<Trans>If you believe this is an error, please send an email including your address to </Trans>{' '}
</ThemedText.DeprecatedMain>
<Copy iconSize={12} toCopy="compliance@uniswap.org" color={theme.deprecated_primary1} iconPosition="right">
<Copy
toCopy="compliance@uniswap.org"
fontSize={14}
iconSize={16}
gap={6}
color={theme.deprecated_primary1}
iconPosition="right"
>
compliance@uniswap.org
</Copy>
</ContentWrapper>

@ -1,6 +1,7 @@
import useTheme from 'hooks/useTheme'
import styled from 'styled-components/macro'
import { ChartWrapper, DeltaContainer, TokenPrice } from '../../Charts/PriceChart'
import { DeltaContainer, TokenPrice } from '../../Charts/PriceChart'
import { LoadingBubble } from '../loading'
import {
AboutHeader,
@ -18,6 +19,11 @@ import {
TopArea,
} from './TokenDetail'
const LoadingChartContainer = styled(ChartContainer)`
height: 336px;
overflow: hidden;
`
/* Loading state bubbles */
const LoadingDetailBubble = styled(LoadingBubble)`
height: 16px;
@ -71,6 +77,15 @@ const Space = styled.div<{ heightSize: number }>`
height: ${({ heightSize }) => `${heightSize}px`};
`
function Wave() {
const theme = useTheme()
return (
<svg width="416" height="160" xmlns="http://www.w3.org/2000/svg">
<path d="M 0 80 Q 104 10, 208 80 T 416 80" stroke={theme.backgroundOutline} fill="transparent" strokeWidth="2" />
</svg>
)
}
/* Loading State: row component with loading bubbles */
export default function LoadingTokenDetail() {
return (
@ -85,35 +100,23 @@ export default function LoadingTokenDetail() {
<TitleLoadingBubble />
</TokenNameCell>
</TokenInfoContainer>
<ChartContainer>
<ChartWrapper>
<ChartHeader>
<TokenPrice>
<PriceLoadingBubble />
</TokenPrice>
<DeltaContainer>
<Space heightSize={20} />
</DeltaContainer>
</ChartHeader>
<TokenPrice>
<PriceLoadingBubble />
</TokenPrice>
<DeltaContainer>
<Space heightSize={20} />
</DeltaContainer>
<LoadingChartContainer>
<div>
<ChartAnimation>
<svg width="416" height="160" xmlns="http://www.w3.org/2000/svg">
<path d="M 0 80 Q 104 10, 208 80 T 416 80" stroke="#2e3138" fill="transparent" strokeWidth="2" />
</svg>
<svg width="416" height="160" xmlns="http://www.w3.org/2000/svg">
<path d="M 0 80 Q 104 10, 208 80 T 416 80" stroke="#2e3138" fill="transparent" strokeWidth="2" />
</svg>
<svg width="416" height="160" xmlns="http://www.w3.org/2000/svg">
<path d="M 0 80 Q 104 10, 208 80 T 416 80" stroke="#2e3138" fill="transparent" strokeWidth="2" />
</svg>
<svg width="416" height="160" xmlns="http://www.w3.org/2000/svg">
<path d="M 0 80 Q 104 10, 208 80 T 416 80" stroke="#2e3138" fill="transparent" strokeWidth="2" />
</svg>
<svg width="416" height="160" xmlns="http://www.w3.org/2000/svg">
<path d="M 0 80 Q 104 10, 208 80 T 416 80" stroke="#2e3138" fill="transparent" strokeWidth="2" />
</svg>
<Wave />
<Wave />
<Wave />
<Wave />
<Wave />
</ChartAnimation>
</ChartWrapper>
</ChartContainer>
</div>
</LoadingChartContainer>
<Space heightSize={32} />
</ChartHeader>
<AboutSection>

@ -1,83 +1,64 @@
import { useOnClickOutside } from 'hooks/useOnClickOutside'
import { darken } from 'polished'
import { useRef, useState } from 'react'
import { Check, Link, Share, Twitter } from 'react-feather'
import { useRef } from 'react'
import { Twitter } from 'react-feather'
import { useModalIsOpen, useToggleModal } from 'state/application/hooks'
import { ApplicationModal } from 'state/application/reducer'
import styled, { useTheme } from 'styled-components/macro'
import { Z_INDEX } from 'theme'
import { ClickableStyle, CopyHelperRefType, OPACITY_CLICK, Z_INDEX } from 'theme'
import { colors } from 'theme/colors'
import { opacify } from 'theme/utils'
import { ReactComponent as ShareIcon } from '../../../assets/svg/share.svg'
import { CopyHelper } from '../../../theme'
const TWITTER_WIDTH = 560
const TWITTER_HEIGHT = 480
const ShareButtonDisplay = styled.div`
display: flex;
cursor: pointer;
position: relative;
z-index: ${Z_INDEX.dropdown};
&:hover {
color: ${({ theme }) => darken(0.1, theme.textSecondary)};
}
`
const Share = styled(ShareIcon)<{ open: boolean }>`
stroke: ${({ theme }) => theme.textSecondary};
height: 24px;
width: 24px;
${ClickableStyle}
${({ open }) => open && `opacity: ${OPACITY_CLICK} !important`};
`
const ShareActions = styled.div`
position: absolute;
top: 28px;
z-index: ${Z_INDEX.dropdown};
width: 240px;
top: 36px;
right: 0px;
padding: 8px 0px;
justify-content: center;
display: flex;
flex-direction: column;
width: fit-content;
overflow: auto;
padding: 8px;
background-color: ${({ theme }) => theme.backgroundSurface};
border: 1px solid ${({ theme }) => theme.backgroundOutline};
border: 0.5px solid ${({ theme }) => theme.backgroundOutline};
box-shadow: ${({ theme }) => theme.flyoutDropShadow};
border-radius: 12px;
`
const ShareAction = styled.div`
display: flex;
align-items: center;
padding: 12px 16px;
padding: 8px;
border-radius: 8px;
font-size: 16px;
gap: 8px;
width: 200px;
height: 48px;
font-weight: 400;
gap: 12px;
height: 40px;
color: ${({ theme }) => theme.textPrimary};
cursor: pointer;
&:hover {
background-color: ${({ theme }) => theme.backgroundModule};
:hover {
background-color: ${({ theme }) => opacify(10, theme.darkMode ? colors.gray200 : colors.gray300)};
}
`
const LinkCopied = styled.div<{ show: boolean }>`
display: ${({ show }) => (show ? 'flex' : 'none')};
width: 328px;
height: 72px;
color: ${({ theme }) => theme.textPrimary};
background-color: ${({ theme }) => theme.backgroundBackdrop};
justify-content: flex-start;
align-items: center;
padding: 24px 16px;
position: absolute;
right: 32px;
bottom: 32px;
font-size: 14px;
gap: 8px;
border: 1px solid rgba(153, 161, 189, 0.08);
box-shadow: ${({ theme }) => theme.flyoutDropShadow};
border-radius: 20px;
animation: floatIn 0s ease-in 3s forwards;
@keyframes floatIn {
to {
width: 0;
height: 0;
overflow: hidden;
display: none;
}
}
`
interface TokenInfo {
tokenName: string
tokenSymbol: string
@ -89,7 +70,6 @@ export default function ShareButton(tokenInfo: TokenInfo) {
const open = useModalIsOpen(ApplicationModal.SHARE)
const toggleShare = useToggleModal(ApplicationModal.SHARE)
useOnClickOutside(node, open ? toggleShare : undefined)
const [showCopied, setShowCopied] = useState(false)
const positionX = (window.screen.width - TWITTER_WIDTH) / 2
const positionY = (window.screen.height - TWITTER_HEIGHT) / 2
@ -101,41 +81,32 @@ export default function ShareButton(tokenInfo: TokenInfo) {
`left=${positionX}, top=${positionY}, width=${TWITTER_WIDTH}, height=${TWITTER_HEIGHT}`
)
}
const copyLink = () => {
navigator.clipboard.writeText(window.location.href).then(
function handleClipboardWriteSuccess() {
setShowCopied(true)
toggleShare()
setTimeout(() => setShowCopied(false), 3000)
},
function error() {
console.error('Clipboard copy failed.')
}
)
}
const copyHelperRef = useRef<CopyHelperRefType>(null)
return (
<>
<ShareButtonDisplay ref={node}>
<Share size={18} onClick={toggleShare} aria-label={`ShareOptions`} />
{open && (
<ShareActions>
<ShareAction onClick={copyLink}>
<Link color={theme.textSecondary} size={18} />
Copy link
</ShareAction>
<ShareButtonDisplay ref={node}>
<Share onClick={toggleShare} aria-label={`ShareOptions`} open={open} />
{open && (
<ShareActions>
<ShareAction onClick={() => copyHelperRef.current?.forceCopy()}>
<CopyHelper
link
color={theme.textPrimary}
iconPosition="left"
toCopy={window.location.href}
ref={copyHelperRef}
>
Copy Link
</CopyHelper>
</ShareAction>
<ShareAction onClick={shareTweet}>
<Twitter color={theme.textSecondary} size={18} />
Share to Twitter
</ShareAction>
</ShareActions>
)}
</ShareButtonDisplay>
<LinkCopied show={showCopied}>
<Check color={theme.accentSuccess} />
Link Copied
</LinkCopied>
</>
<ShareAction onClick={shareTweet}>
<Twitter color={theme.textPrimary} size={20} strokeWidth={1.5} />
Share to Twitter
</ShareAction>
</ShareActions>
)}
</ShareButtonDisplay>
)
}

@ -8,14 +8,13 @@ import { getChainInfo } from 'constants/chainInfo'
import { checkWarning } from 'constants/tokenSafety'
import { useCurrency, useIsUserAddedToken, useToken } from 'hooks/Tokens'
import { useAtomValue } from 'jotai/utils'
import { darken } from 'polished'
import { useCallback } from 'react'
import { useState } from 'react'
import { ArrowLeft, Copy, Heart } from 'react-feather'
import { ArrowLeft, Heart } from 'react-feather'
import { Link, useNavigate } from 'react-router-dom'
import styled, { useTheme } from 'styled-components/macro'
import styled from 'styled-components/macro'
import { ClickableStyle, CopyContractAddress } from 'theme'
import { MOBILE_MEDIA_BREAKPOINT } from '../constants'
import { favoritesAtom, useToggleFavorite } from '../state'
import { ClickFavorited } from '../TokenTable/TokenRow'
import Resource from './Resource'
@ -62,10 +61,6 @@ const ContractAddress = styled.button`
border: none;
padding: 0px;
cursor: pointer;
&:hover {
color: ${({ theme }) => darken(0.1, theme.textPrimary)};
}
`
export const ContractAddressSection = styled.div`
padding: 24px 0px;
@ -114,7 +109,7 @@ export const TokenNameCell = styled.div`
`
const TokenActions = styled.div`
display: flex;
gap: 24px;
gap: 16px;
color: ${({ theme }) => theme.textSecondary};
`
export const TokenInfoContainer = styled.div`
@ -132,17 +127,6 @@ export const ResourcesContainer = styled.div`
display: flex;
gap: 14px;
`
const FullAddress = styled.span`
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
display: none;
}
`
const TruncatedAddress = styled.span`
display: none;
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
display: flex;
}
`
const NetworkBadge = styled.div<{ networkColor?: string; backgroundColor?: string }>`
border-radius: 5px;
padding: 4px 8px;
@ -152,9 +136,15 @@ const NetworkBadge = styled.div<{ networkColor?: string; backgroundColor?: strin
color: ${({ theme, networkColor }) => networkColor ?? theme.textPrimary};
background-color: ${({ theme, backgroundColor }) => backgroundColor ?? theme.backgroundSurface};
`
const FavoriteIcon = styled(Heart)<{ isFavorited: boolean }>`
${ClickableStyle}
height: 22px;
width: 24px;
color: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : theme.textSecondary)};
fill: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : 'transparent')};
`
export default function LoadedTokenDetail({ address }: { address: string }) {
const theme = useTheme()
const token = useToken(address)
const currency = useCurrency(address)
const favoriteTokens = useAtomValue<string[]>(favoritesAtom)
@ -184,7 +174,6 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
'Ethereum is a decentralized computing platform that uses ETH (Ether) to pay transaction fees (gas). Developers can use Ethereum to run decentralized applications (dApps) and issue new crypto assets, known as Ethereum tokens.'
const tokenMarketCap = '23.02B'
const tokenVolume = '1.6B'
const truncatedTokenAddress = `${address.slice(0, 4)}...${address.slice(-3)}`
return (
<TopArea>
@ -196,7 +185,7 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
<TokenNameCell>
<CurrencyLogo currency={currency} size={'32px'} />
{tokenName} <TokenSymbol>{tokenSymbol}</TokenSymbol>
{!warning && <VerifiedIcon size="24px" />}
{!warning && <VerifiedIcon size="20px" />}
{networkBadgebackgroundColor && (
<NetworkBadge networkColor={chainInfo?.color} backgroundColor={networkBadgebackgroundColor}>
{networkLabel}
@ -206,11 +195,7 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
<TokenActions>
<ShareButton tokenName={tokenName} tokenSymbol={tokenSymbol} />
<ClickFavorited onClick={toggleFavorite}>
<Heart
size={15}
color={isFavorited ? theme.accentAction : theme.textSecondary}
fill={isFavorited ? theme.accentAction : 'transparent'}
/>
<FavoriteIcon isFavorited={isFavorited} />
</ClickFavorited>
</TokenActions>
</TokenInfoContainer>
@ -235,7 +220,7 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
</Stat>
<Stat>
{/* TODO: connect to chart's selected time */}
1h volume
24H volume
<StatPrice>${tokenVolume}</StatPrice>
</Stat>
</StatPair>
@ -253,10 +238,8 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
<ContractAddressSection>
<Contract>
Contract Address
<ContractAddress onClick={() => navigator.clipboard.writeText(address)}>
<FullAddress>{address}</FullAddress>
<TruncatedAddress>{truncatedTokenAddress}</TruncatedAddress>
<Copy size={13} color={theme.textSecondary} />
<ContractAddress>
<CopyContractAddress address={address} />
</ContractAddress>
</Contract>
</ContractAddressSection>

@ -0,0 +1,167 @@
/**
* @generated SignedSource<<0becdf63598262462f6fa0cabb891ad0>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest, Query } from 'relay-runtime';
export type AllV3TicksQuery$variables = {
poolAddress: string;
skip: number;
};
export type AllV3TicksQuery$data = {
readonly ticks: ReadonlyArray<{
readonly liquidityNet: any;
readonly price0: any;
readonly price1: any;
readonly tick: any;
}>;
};
export type AllV3TicksQuery = {
response: AllV3TicksQuery$data;
variables: AllV3TicksQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "poolAddress"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "skip"
}
],
v1 = [
{
"kind": "Literal",
"name": "first",
"value": 1000
},
{
"kind": "Literal",
"name": "orderBy",
"value": "tickIdx"
},
{
"kind": "Variable",
"name": "skip",
"variableName": "skip"
},
{
"fields": [
{
"kind": "Variable",
"name": "poolAddress",
"variableName": "poolAddress"
}
],
"kind": "ObjectValue",
"name": "where"
}
],
v2 = {
"alias": "tick",
"args": null,
"kind": "ScalarField",
"name": "tickIdx",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "liquidityNet",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "price0",
"storageKey": null
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "price1",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "AllV3TicksQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "Tick",
"kind": "LinkedField",
"name": "ticks",
"plural": true,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/)
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "AllV3TicksQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "Tick",
"kind": "LinkedField",
"name": "ticks",
"plural": true,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "9f2d65b1e565e3d0ecbe7b1f908ebc83",
"id": null,
"metadata": {},
"name": "AllV3TicksQuery",
"operationKind": "query",
"text": "query AllV3TicksQuery(\n $poolAddress: String!\n $skip: Int!\n) {\n ticks(first: 1000, skip: $skip, where: {poolAddress: $poolAddress}, orderBy: tickIdx) {\n tick: tickIdx\n liquidityNet\n price0\n price1\n id\n }\n}\n"
}
};
})();
(node as any).hash = "82709c11c929a8eb6caf2ab1df2b99cc";
export default node;

@ -0,0 +1,242 @@
/**
* @generated SignedSource<<5761481cf3bba524864626a7f965c0d7>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest, Query } from 'relay-runtime';
export type FeeTierDistributionQuery$variables = {
token0: string;
token1: string;
};
export type FeeTierDistributionQuery$data = {
readonly _meta: {
readonly block: {
readonly number: number;
};
} | null;
readonly asToken0: ReadonlyArray<{
readonly feeTier: any;
readonly totalValueLockedToken0: any;
readonly totalValueLockedToken1: any;
}>;
readonly asToken1: ReadonlyArray<{
readonly feeTier: any;
readonly totalValueLockedToken0: any;
readonly totalValueLockedToken1: any;
}>;
};
export type FeeTierDistributionQuery = {
response: FeeTierDistributionQuery$data;
variables: FeeTierDistributionQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "token0"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "token1"
}
],
v1 = {
"alias": null,
"args": null,
"concreteType": "_Meta_",
"kind": "LinkedField",
"name": "_meta",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "_Block_",
"kind": "LinkedField",
"name": "block",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "number",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
},
v2 = {
"kind": "Literal",
"name": "orderBy",
"value": "totalValueLockedToken0"
},
v3 = {
"kind": "Literal",
"name": "orderDirection",
"value": "desc"
},
v4 = [
(v2/*: any*/),
(v3/*: any*/),
{
"fields": [
{
"kind": "Variable",
"name": "token0",
"variableName": "token0"
},
{
"kind": "Variable",
"name": "token1",
"variableName": "token1"
}
],
"kind": "ObjectValue",
"name": "where"
}
],
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "feeTier",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalValueLockedToken0",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalValueLockedToken1",
"storageKey": null
},
v8 = [
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/)
],
v9 = [
(v2/*: any*/),
(v3/*: any*/),
{
"fields": [
{
"kind": "Variable",
"name": "token0",
"variableName": "token1"
},
{
"kind": "Variable",
"name": "token1",
"variableName": "token0"
}
],
"kind": "ObjectValue",
"name": "where"
}
],
v10 = [
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "FeeTierDistributionQuery",
"selections": [
(v1/*: any*/),
{
"alias": "asToken0",
"args": (v4/*: any*/),
"concreteType": "Pool",
"kind": "LinkedField",
"name": "pools",
"plural": true,
"selections": (v8/*: any*/),
"storageKey": null
},
{
"alias": "asToken1",
"args": (v9/*: any*/),
"concreteType": "Pool",
"kind": "LinkedField",
"name": "pools",
"plural": true,
"selections": (v8/*: any*/),
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "FeeTierDistributionQuery",
"selections": [
(v1/*: any*/),
{
"alias": "asToken0",
"args": (v4/*: any*/),
"concreteType": "Pool",
"kind": "LinkedField",
"name": "pools",
"plural": true,
"selections": (v10/*: any*/),
"storageKey": null
},
{
"alias": "asToken1",
"args": (v9/*: any*/),
"concreteType": "Pool",
"kind": "LinkedField",
"name": "pools",
"plural": true,
"selections": (v10/*: any*/),
"storageKey": null
}
]
},
"params": {
"cacheID": "d989fcfb8fc9ef13bdc6de811e574284",
"id": null,
"metadata": {},
"name": "FeeTierDistributionQuery",
"operationKind": "query",
"text": "query FeeTierDistributionQuery(\n $token0: String!\n $token1: String!\n) {\n _meta {\n block {\n number\n }\n }\n asToken0: pools(orderBy: totalValueLockedToken0, orderDirection: desc, where: {token0: $token0, token1: $token1}) {\n feeTier\n totalValueLockedToken0\n totalValueLockedToken1\n id\n }\n asToken1: pools(orderBy: totalValueLockedToken0, orderDirection: desc, where: {token0: $token1, token1: $token0}) {\n feeTier\n totalValueLockedToken0\n totalValueLockedToken1\n id\n }\n}\n"
}
};
})();
(node as any).hash = "ac9cd4bfdc24db90dbb2bf8a7508009f";
export default node;

File diff suppressed because it is too large Load Diff

@ -0,0 +1,167 @@
/**
* @generated SignedSource<<0becdf63598262462f6fa0cabb891ad0>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest, Query } from 'relay-runtime';
export type AllV3TicksQuery$variables = {
poolAddress: string;
skip: number;
};
export type AllV3TicksQuery$data = {
readonly ticks: ReadonlyArray<{
readonly liquidityNet: any;
readonly price0: any;
readonly price1: any;
readonly tick: any;
}>;
};
export type AllV3TicksQuery = {
response: AllV3TicksQuery$data;
variables: AllV3TicksQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "poolAddress"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "skip"
}
],
v1 = [
{
"kind": "Literal",
"name": "first",
"value": 1000
},
{
"kind": "Literal",
"name": "orderBy",
"value": "tickIdx"
},
{
"kind": "Variable",
"name": "skip",
"variableName": "skip"
},
{
"fields": [
{
"kind": "Variable",
"name": "poolAddress",
"variableName": "poolAddress"
}
],
"kind": "ObjectValue",
"name": "where"
}
],
v2 = {
"alias": "tick",
"args": null,
"kind": "ScalarField",
"name": "tickIdx",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "liquidityNet",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "price0",
"storageKey": null
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "price1",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "AllV3TicksQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "Tick",
"kind": "LinkedField",
"name": "ticks",
"plural": true,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/)
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "AllV3TicksQuery",
"selections": [
{
"alias": null,
"args": (v1/*: any*/),
"concreteType": "Tick",
"kind": "LinkedField",
"name": "ticks",
"plural": true,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "9f2d65b1e565e3d0ecbe7b1f908ebc83",
"id": null,
"metadata": {},
"name": "AllV3TicksQuery",
"operationKind": "query",
"text": "query AllV3TicksQuery(\n $poolAddress: String!\n $skip: Int!\n) {\n ticks(first: 1000, skip: $skip, where: {poolAddress: $poolAddress}, orderBy: tickIdx) {\n tick: tickIdx\n liquidityNet\n price0\n price1\n id\n }\n}\n"
}
};
})();
(node as any).hash = "82709c11c929a8eb6caf2ab1df2b99cc";
export default node;

@ -0,0 +1,242 @@
/**
* @generated SignedSource<<5761481cf3bba524864626a7f965c0d7>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest, Query } from 'relay-runtime';
export type FeeTierDistributionQuery$variables = {
token0: string;
token1: string;
};
export type FeeTierDistributionQuery$data = {
readonly _meta: {
readonly block: {
readonly number: number;
};
} | null;
readonly asToken0: ReadonlyArray<{
readonly feeTier: any;
readonly totalValueLockedToken0: any;
readonly totalValueLockedToken1: any;
}>;
readonly asToken1: ReadonlyArray<{
readonly feeTier: any;
readonly totalValueLockedToken0: any;
readonly totalValueLockedToken1: any;
}>;
};
export type FeeTierDistributionQuery = {
response: FeeTierDistributionQuery$data;
variables: FeeTierDistributionQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "token0"
},
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "token1"
}
],
v1 = {
"alias": null,
"args": null,
"concreteType": "_Meta_",
"kind": "LinkedField",
"name": "_meta",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "_Block_",
"kind": "LinkedField",
"name": "block",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "number",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
},
v2 = {
"kind": "Literal",
"name": "orderBy",
"value": "totalValueLockedToken0"
},
v3 = {
"kind": "Literal",
"name": "orderDirection",
"value": "desc"
},
v4 = [
(v2/*: any*/),
(v3/*: any*/),
{
"fields": [
{
"kind": "Variable",
"name": "token0",
"variableName": "token0"
},
{
"kind": "Variable",
"name": "token1",
"variableName": "token1"
}
],
"kind": "ObjectValue",
"name": "where"
}
],
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "feeTier",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalValueLockedToken0",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalValueLockedToken1",
"storageKey": null
},
v8 = [
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/)
],
v9 = [
(v2/*: any*/),
(v3/*: any*/),
{
"fields": [
{
"kind": "Variable",
"name": "token0",
"variableName": "token1"
},
{
"kind": "Variable",
"name": "token1",
"variableName": "token0"
}
],
"kind": "ObjectValue",
"name": "where"
}
],
v10 = [
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "FeeTierDistributionQuery",
"selections": [
(v1/*: any*/),
{
"alias": "asToken0",
"args": (v4/*: any*/),
"concreteType": "Pool",
"kind": "LinkedField",
"name": "pools",
"plural": true,
"selections": (v8/*: any*/),
"storageKey": null
},
{
"alias": "asToken1",
"args": (v9/*: any*/),
"concreteType": "Pool",
"kind": "LinkedField",
"name": "pools",
"plural": true,
"selections": (v8/*: any*/),
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "FeeTierDistributionQuery",
"selections": [
(v1/*: any*/),
{
"alias": "asToken0",
"args": (v4/*: any*/),
"concreteType": "Pool",
"kind": "LinkedField",
"name": "pools",
"plural": true,
"selections": (v10/*: any*/),
"storageKey": null
},
{
"alias": "asToken1",
"args": (v9/*: any*/),
"concreteType": "Pool",
"kind": "LinkedField",
"name": "pools",
"plural": true,
"selections": (v10/*: any*/),
"storageKey": null
}
]
},
"params": {
"cacheID": "d989fcfb8fc9ef13bdc6de811e574284",
"id": null,
"metadata": {},
"name": "FeeTierDistributionQuery",
"operationKind": "query",
"text": "query FeeTierDistributionQuery(\n $token0: String!\n $token1: String!\n) {\n _meta {\n block {\n number\n }\n }\n asToken0: pools(orderBy: totalValueLockedToken0, orderDirection: desc, where: {token0: $token0, token1: $token1}) {\n feeTier\n totalValueLockedToken0\n totalValueLockedToken1\n id\n }\n asToken1: pools(orderBy: totalValueLockedToken0, orderDirection: desc, where: {token0: $token1, token1: $token0}) {\n feeTier\n totalValueLockedToken0\n totalValueLockedToken1\n id\n }\n}\n"
}
};
})();
(node as any).hash = "ac9cd4bfdc24db90dbb2bf8a7508009f";
export default node;

@ -156,14 +156,7 @@ export default function App() {
{exploreFlag === ExploreVariant.Enabled && (
<>
<Route path="/explore" element={<Explore />} />
<Route
path="/tokens/:tokenAddress"
element={
<Suspense fallback={<LazyLoadSpinner />}>
<TokenDetails />
</Suspense>
}
/>
<Route path="/tokens/:tokenAddress" element={<TokenDetails />} />
</>
)}
<Route

4892
src/schema/schema.graphql Normal file

File diff suppressed because it is too large Load Diff

@ -1,30 +1,25 @@
import { Trans } from '@lingui/macro'
import { outboundLink } from 'components/analytics'
import { MOBILE_MEDIA_BREAKPOINT } from 'components/Explore/constants'
import useCopyClipboard from 'hooks/useCopyClipboard'
import React, { HTMLProps, useCallback } from 'react'
import { ArrowLeft, Copy, ExternalLink as LinkIconFeather, Trash, X } from 'react-feather'
import React, { forwardRef, HTMLProps, ReactNode, useCallback, useImperativeHandle } from 'react'
import {
ArrowLeft,
CheckCircle,
Copy,
ExternalLink as ExternalLinkIconFeather,
Link as LinkIconFeather,
Trash,
X,
} from 'react-feather'
import { Link } from 'react-router-dom'
import styled, { css, keyframes } from 'styled-components/macro'
import { ReactComponent as TooltipTriangle } from '../assets/svg/tooltip_triangle.svg'
import { anonymizeLink } from '../utils/anonymizeLink'
import { Color } from './styled'
export const ButtonText = styled.button`
outline: none;
border: none;
font-size: inherit;
padding: 0;
margin: 0;
background: none;
cursor: pointer;
:hover {
opacity: 0.7;
}
:focus {
text-decoration: underline;
}
`
// TODO: Break this file into a components folder
export const CloseIcon = styled(X)<{ onClick: () => void }>`
cursor: pointer;
@ -68,23 +63,48 @@ export const LinkStyledButton = styled.button<{ disabled?: boolean }>`
}
`
export const LinkStyle = css`
text-decoration: none;
color: ${({ theme }) => theme.accentAction};
stroke: ${({ theme }) => theme.accentAction};
export const OPACITY_HOVER = 0.6
export const OPACITY_CLICK = 0.4
export const ButtonText = styled.button`
outline: none;
border: none;
font-size: inherit;
padding: 0;
margin: 0;
background: none;
cursor: pointer;
font-weight: 500;
:hover {
opacity: 0.6;
opacity: ${OPACITY_HOVER};
}
:focus {
text-decoration: underline;
}
`
export const ClickableStyle = css`
text-decoration: none;
cursor: pointer;
:hover {
opacity: ${OPACITY_HOVER};
}
:active {
opacity: 0.4;
opacity: ${OPACITY_CLICK};
}
`
export const LinkStyle = css`
color: ${({ theme }) => theme.accentAction};
stroke: ${({ theme }) => theme.accentAction};
font-weight: 500;
`
// An internal link from the react-router-dom library that is correctly styled
export const StyledInternalLink = styled(Link)`
${ClickableStyle}
${LinkStyle}
`
@ -94,27 +114,21 @@ const LinkIconWrapper = styled.a`
display: flex;
`
const CopyIconWrapper = styled.div`
text-decoration: none;
cursor: pointer;
align-items: center;
justify-content: center;
display: flex;
`
const IconStyle = css`
height: 16px;
width: 18px;
margin-left: 10px;
`
const LinkIcon = styled(LinkIconFeather)`
const LinkIcon = styled(ExternalLinkIconFeather)`
${IconStyle}
${ClickableStyle}
${LinkStyle}
`
const CopyIcon = styled(Copy)`
${IconStyle}
${ClickableStyle}
${LinkStyle}
stroke: ${({ theme }) => theme.accentActive};
`
@ -129,7 +143,7 @@ export const TrashIcon = styled(Trash)`
display: flex;
:hover {
opacity: 0.7;
opacity: ${OPACITY_HOVER};
}
`
@ -169,6 +183,7 @@ function handleClickExternalLink(event: React.MouseEvent<HTMLAnchorElement>) {
}
const StyledLink = styled.a`
${ClickableStyle}
${LinkStyle}
`
/**
@ -227,6 +242,14 @@ function ToolTip() {
)
}
const CopyIconWrapper = styled.div`
text-decoration: none;
cursor: pointer;
align-items: center;
justify-content: center;
display: flex;
`
export function CopyLinkIcon({ toCopy }: { toCopy: string }) {
const [isCopied, setCopied] = useCopyClipboard()
const copy = useCallback(() => {
@ -240,6 +263,134 @@ export function CopyLinkIcon({ toCopy }: { toCopy: string }) {
)
}
const FullAddress = styled.span`
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
display: none;
}
`
const TruncatedAddress = styled.span`
display: none;
@media only screen and (max-width: ${MOBILE_MEDIA_BREAKPOINT}) {
display: flex;
}
`
const CopyAddressRow = styled.div<{ isClicked: boolean }>`
${ClickableStyle}
color: inherit;
stroke: inherit;
cursor: pointer;
align-items: center;
justify-content: center;
display: flex;
gap: 6px;
${({ isClicked }) => isClicked && `opacity: ` + OPACITY_CLICK + ` !important`}
`
const CopyContractAddressWrapper = styled.div`
position: relative;
align-items: center;
justify-content: center;
display: flex;
`
export function CopyContractAddress({ address }: { address: string }) {
const [isCopied, setCopied] = useCopyClipboard()
const copy = useCallback(() => {
setCopied(address)
}, [address, setCopied])
const truncated = `${address.slice(0, 4)}...${address.slice(-3)}`
return (
<CopyContractAddressWrapper onClick={copy}>
<CopyAddressRow isClicked={isCopied}>
<FullAddress>{address}</FullAddress>
<TruncatedAddress>{truncated}</TruncatedAddress>
<Copy size={14} />
</CopyAddressRow>
{isCopied && <ToolTip />}
</CopyContractAddressWrapper>
)
}
const CopyHelperContainer = styled(LinkStyledButton)<{ clicked: boolean }>`
${({ clicked }) => !clicked && ClickableStyle};
color: ${({ color, theme }) => color || theme.accentAction};
padding: 0;
flex-shrink: 0;
display: flex;
text-decoration: none;
:hover,
:active,
:focus {
text-decoration: none;
color: ${({ color, theme }) => color || theme.accentAction};
}
`
const CopyHelperText = styled.span<{ fontSize: number }>`
${({ theme }) => theme.flexRowNoWrap};
font-size: ${({ fontSize }) => fontSize + 'px'};
font-weight: 400;
align-items: center;
`
const CopiedIcon = styled(CheckCircle)`
color: ${({ theme }) => theme.accentSuccess};
stroke-width: 1.5px;
`
interface CopyHelperProps {
link?: boolean
toCopy: string
color?: Color
fontSize?: number
iconSize?: number
gap?: number
iconPosition?: 'left' | 'right'
iconColor?: Color
children: ReactNode
}
export type CopyHelperRefType = { forceCopy: () => void }
export const CopyHelper = forwardRef<CopyHelperRefType, CopyHelperProps>(
(
{
link,
toCopy,
color,
fontSize = 16,
iconSize = 20,
gap = 12,
iconPosition = 'left',
iconColor,
children,
}: CopyHelperProps,
ref
) => {
const [isCopied, setCopied] = useCopyClipboard()
const copy = useCallback(() => {
setCopied(toCopy)
}, [toCopy, setCopied])
useImperativeHandle(ref, () => ({
forceCopy() {
copy()
},
}))
const BaseIcon = isCopied ? CopiedIcon : link ? LinkIconFeather : Copy
return (
<CopyHelperContainer onClick={copy} color={color} clicked={isCopied}>
<div style={{ display: 'flex', flexDirection: 'row', gap }}>
{iconPosition === 'left' && <BaseIcon size={iconSize} strokeWidth={1.5} color={iconColor} />}
<CopyHelperText fontSize={fontSize}>{isCopied ? <Trans>Copied!</Trans> : children}</CopyHelperText>
{iconPosition === 'right' && <BaseIcon size={iconSize} strokeWidth={1.5} color={iconColor} />}
</div>
</CopyHelperContainer>
)
}
)
CopyHelper.displayName = 'CopyHelper'
const rotate = keyframes`
from {
transform: rotate(0deg);