fix: data api loading states and repetitive calls (#4461)
* diagnosing issues * fixed loading states * addressed PR comments * fixed missing symbol issue * fixed merge conflcit * fixed uppercase token symbol issue
This commit is contained in:
parent
7500bbc0be
commit
84fb05239b
@ -16,7 +16,7 @@ import {
|
||||
TokenInfoContainer,
|
||||
TokenNameCell,
|
||||
TopArea,
|
||||
} from './TokenDetail'
|
||||
} from './TokenDetailContainers'
|
||||
|
||||
const LoadingChartContainer = styled(ChartContainer)`
|
||||
height: 336px;
|
||||
@ -34,15 +34,17 @@ const TitleLoadingBubble = styled(LoadingDetailBubble)`
|
||||
const SquareLoadingBubble = styled(LoadingDetailBubble)`
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
const PriceLoadingBubble = styled(SquareLoadingBubble)`
|
||||
height: 40px;
|
||||
`
|
||||
const LongLoadingBubble = styled(LoadingDetailBubble)`
|
||||
margin-top: 6px;
|
||||
width: 100%;
|
||||
`
|
||||
const HalfLoadingBubble = styled(LoadingDetailBubble)`
|
||||
margin-top: 6px;
|
||||
width: 50%;
|
||||
`
|
||||
const IconLoadingBubble = styled(LoadingDetailBubble)`
|
||||
|
@ -47,17 +47,26 @@ const StyledDownArrow = styled(ArrowDownRight)`
|
||||
color: ${({ theme }) => theme.accentFailure};
|
||||
`
|
||||
|
||||
export function getDelta(start: number, current: number) {
|
||||
const delta = (current / start - 1) * 100
|
||||
const isPositive = Math.sign(delta) > 0
|
||||
export function calculateDelta(start: number, current: number) {
|
||||
return (current / start - 1) * 100
|
||||
}
|
||||
|
||||
const formattedDelta = delta.toFixed(2) + '%'
|
||||
if (isPositive) {
|
||||
return ['+' + formattedDelta, <StyledUpArrow size={16} key="arrow-up" />]
|
||||
export function getDeltaArrow(delta: number) {
|
||||
if (Math.sign(delta) > 0) {
|
||||
return <StyledUpArrow size={16} key="arrow-up" />
|
||||
} else if (delta === 0) {
|
||||
return [formattedDelta, null]
|
||||
return null
|
||||
} else {
|
||||
return <StyledDownArrow size={16} key="arrow-down" />
|
||||
}
|
||||
return [formattedDelta, <StyledDownArrow size={16} key="arrow-down" />]
|
||||
}
|
||||
|
||||
export function formatDelta(delta: number) {
|
||||
let formattedDelta = delta.toFixed(2) + '%'
|
||||
if (Math.sign(delta) > 0) {
|
||||
formattedDelta = '+' + formattedDelta
|
||||
}
|
||||
return formattedDelta
|
||||
}
|
||||
|
||||
export const ChartHeader = styled.div`
|
||||
@ -165,8 +174,9 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
const [crosshair, setCrosshair] = useState<number | null>(null)
|
||||
|
||||
const graphWidth = width + crosshairDateOverhang
|
||||
const graphHeight = height - timeOptionsHeight
|
||||
const graphInnerHeight = graphHeight - margin.top - margin.bottom
|
||||
// TODO: remove this logic after suspense is properly added
|
||||
const graphHeight = height - timeOptionsHeight > 0 ? height - timeOptionsHeight : 0
|
||||
const graphInnerHeight = graphHeight - margin.top - margin.bottom > 0 ? graphHeight - margin.top - margin.bottom : 0
|
||||
|
||||
// Defining scales
|
||||
// x scale
|
||||
@ -177,7 +187,7 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
const handleHover = useCallback(
|
||||
(event: Element | EventType) => {
|
||||
const { x } = localPoint(event) || { x: 0 }
|
||||
const x0 = timeScale.invert(x) // get timestamp from the scale
|
||||
const x0 = timeScale.invert(x) // get timestamp from the scalexw
|
||||
const index = bisect(
|
||||
pricePoints.map((x) => x.timestamp),
|
||||
x0,
|
||||
@ -215,7 +225,9 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
timePeriod,
|
||||
locale
|
||||
)
|
||||
const [delta, arrow] = getDelta(startingPrice.value, displayPrice.value)
|
||||
const delta = calculateDelta(startingPrice.value, displayPrice.value)
|
||||
const formattedDelta = formatDelta(delta)
|
||||
const arrow = getDeltaArrow(delta)
|
||||
const crosshairEdgeMax = width * 0.85
|
||||
const crosshairAtEdge = !!crosshair && crosshair > crosshairEdgeMax
|
||||
|
||||
@ -224,7 +236,7 @@ export function PriceChart({ width, height, token }: PriceChartProps) {
|
||||
<ChartHeader>
|
||||
<TokenPrice>${displayPrice.value.toFixed(2)}</TokenPrice>
|
||||
<DeltaContainer>
|
||||
{delta}
|
||||
{formattedDelta}
|
||||
<ArrowCell>{arrow}</ArrowCell>
|
||||
</DeltaContainer>
|
||||
</ChartHeader>
|
||||
|
@ -10,46 +10,35 @@ import { chainIdToChainName, useTokenDetailQuery } from 'graphql/data/TokenDetai
|
||||
import { useCurrency, useIsUserAddedToken, useToken } from 'hooks/Tokens'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { darken } from 'polished'
|
||||
import { useCallback } from 'react'
|
||||
import { Suspense, useCallback } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { ArrowLeft, Heart, TrendingUp } from 'react-feather'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { ArrowLeft, Heart } from 'react-feather'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
import { ClickableStyle, CopyContractAddress } from 'theme'
|
||||
import { formatDollarAmount } from 'utils/formatDollarAmt'
|
||||
|
||||
import { favoritesAtom, filterNetworkAtom, useToggleFavorite } from '../state'
|
||||
import { ClickFavorited } from '../TokenTable/TokenRow'
|
||||
import { Wave } from './LoadingTokenDetail'
|
||||
import LoadingTokenDetail from './LoadingTokenDetail'
|
||||
import Resource from './Resource'
|
||||
import ShareButton from './ShareButton'
|
||||
import {
|
||||
AboutContainer,
|
||||
AboutHeader,
|
||||
BreadcrumbNavLink,
|
||||
ChartContainer,
|
||||
ChartHeader,
|
||||
ContractAddressSection,
|
||||
ResourcesContainer,
|
||||
Stat,
|
||||
StatPair,
|
||||
StatsSection,
|
||||
TokenInfoContainer,
|
||||
TokenNameCell,
|
||||
TopArea,
|
||||
} from './TokenDetailContainers'
|
||||
|
||||
export const AboutHeader = styled.span`
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
`
|
||||
export const BreadcrumbNavLink = styled(Link)`
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
text-decoration: none;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
}
|
||||
`
|
||||
export const ChartHeader = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
gap: 4px;
|
||||
margin-bottom: 24px;
|
||||
`
|
||||
const ContractAddress = styled.button`
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
@ -60,9 +49,6 @@ const ContractAddress = styled.button`
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
`
|
||||
export const ContractAddressSection = styled.div`
|
||||
padding: 24px 0px;
|
||||
`
|
||||
const Contract = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -70,63 +56,18 @@ const Contract = styled.div`
|
||||
font-size: 14px;
|
||||
gap: 4px;
|
||||
`
|
||||
export const ChartContainer = styled.div`
|
||||
display: flex;
|
||||
height: 436px;
|
||||
align-items: center;
|
||||
`
|
||||
export const Stat = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 14px;
|
||||
min-width: 168px;
|
||||
flex: 1;
|
||||
gap: 4px;
|
||||
padding: 24px 0px;
|
||||
`
|
||||
const StatPrice = styled.span`
|
||||
font-size: 28px;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
`
|
||||
export const StatsSection = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
export const StatPair = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
export const TokenNameCell = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
align-items: center;
|
||||
`
|
||||
const TokenActions = styled.div`
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
export const TokenInfoContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
const TokenSymbol = styled.span`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
`
|
||||
export const TopArea = styled.div`
|
||||
max-width: 832px;
|
||||
overflow: hidden;
|
||||
`
|
||||
export const ResourcesContainer = styled.div`
|
||||
display: flex;
|
||||
padding-top: 12px;
|
||||
gap: 14px;
|
||||
`
|
||||
const NetworkBadge = styled.div<{ networkColor?: string; backgroundColor?: string }>`
|
||||
border-radius: 5px;
|
||||
padding: 4px 8px;
|
||||
@ -143,38 +84,11 @@ const FavoriteIcon = styled(Heart)<{ isFavorited: boolean }>`
|
||||
color: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : theme.textSecondary)};
|
||||
fill: ${({ isFavorited, theme }) => (isFavorited ? theme.accentAction : 'transparent')};
|
||||
`
|
||||
const ChartEmpty = styled.div`
|
||||
display: flex;
|
||||
height: 400px;
|
||||
align-items: center;
|
||||
`
|
||||
const NoInfoAvailable = styled.span`
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
`
|
||||
const MissingChartData = styled.div`
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
display: flex;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.backgroundOutline};
|
||||
padding: 8px 0px;
|
||||
margin-top: -40px;
|
||||
`
|
||||
const MissingData = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
`
|
||||
|
||||
export const AboutContainer = styled.div`
|
||||
gap: 16px;
|
||||
padding: 24px 0px;
|
||||
`
|
||||
|
||||
const TokenDescriptionContainer = styled.div`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@ -184,7 +98,6 @@ const TokenDescriptionContainer = styled.div`
|
||||
line-height: 24px;
|
||||
white-space: pre-wrap;
|
||||
`
|
||||
|
||||
const TruncateDescriptionButton = styled.div`
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-weight: 400;
|
||||
@ -285,9 +198,15 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
|
||||
twitterName,
|
||||
}))(tokenDetailData)
|
||||
|
||||
// catch token error and loading state
|
||||
if (!token || !token.name || !token.symbol) {
|
||||
return (
|
||||
return <LoadingTokenDetail />
|
||||
}
|
||||
|
||||
const tokenName = tokenDetailData.name
|
||||
const tokenSymbol = tokenDetailData.tokens?.[0]?.symbol?.toUpperCase() ?? token.symbol
|
||||
|
||||
return (
|
||||
<Suspense fallback={<LoadingTokenDetail />}>
|
||||
<TopArea>
|
||||
<BreadcrumbNavLink to="/tokens">
|
||||
<ArrowLeft size={14} /> Tokens
|
||||
@ -296,8 +215,8 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
|
||||
<TokenInfoContainer>
|
||||
<TokenNameCell>
|
||||
<CurrencyLogo currency={currency} size={'32px'} />
|
||||
<Trans>{!token ? 'Name not found' : token.name}</Trans>
|
||||
<TokenSymbol>{token && token.symbol}</TokenSymbol>
|
||||
{tokenName ?? <Trans>Name not found</Trans>}
|
||||
<TokenSymbol>{tokenSymbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
|
||||
{!warning && <VerifiedIcon size="20px" />}
|
||||
{networkBadgebackgroundColor && (
|
||||
<NetworkBadge networkColor={chainInfo?.color} backgroundColor={networkBadgebackgroundColor}>
|
||||
@ -305,32 +224,58 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
|
||||
</NetworkBadge>
|
||||
)}
|
||||
</TokenNameCell>
|
||||
<TokenActions>
|
||||
{tokenName && tokenSymbol && (
|
||||
<ShareButton tokenName={tokenName} tokenSymbol={tokenSymbol} tokenAddress={address} />
|
||||
)}
|
||||
<ClickFavorited onClick={toggleFavorite}>
|
||||
<FavoriteIcon isFavorited={isFavorited} />
|
||||
</ClickFavorited>
|
||||
</TokenActions>
|
||||
</TokenInfoContainer>
|
||||
<ChartEmpty>
|
||||
<Wave />
|
||||
<Wave />
|
||||
</ChartEmpty>
|
||||
<MissingChartData>
|
||||
<TrendingUp size={12} />
|
||||
Missing chart data
|
||||
</MissingChartData>
|
||||
<ChartContainer>
|
||||
<ParentSize>{({ width, height }) => <PriceChart token={token} width={width} height={height} />}</ParentSize>
|
||||
</ChartContainer>
|
||||
</ChartHeader>
|
||||
<MissingData>
|
||||
<AboutSection address={address} tokenDetailData={relevantTokenDetailData} />
|
||||
<StatsSection>
|
||||
<NoInfoAvailable>
|
||||
<Trans>No stats available</Trans>
|
||||
</NoInfoAvailable>
|
||||
</StatsSection>
|
||||
<ContractAddressSection>
|
||||
<Contract>
|
||||
Contract address
|
||||
<ContractAddress>
|
||||
<CopyContractAddress address={address} />
|
||||
</ContractAddress>
|
||||
</Contract>
|
||||
</ContractAddressSection>
|
||||
</MissingData>
|
||||
<AboutSection address={address} tokenDetailData={relevantTokenDetailData} />
|
||||
<StatsSection>
|
||||
<StatPair>
|
||||
<Stat>
|
||||
Market cap
|
||||
<StatPrice>
|
||||
{tokenDetailData.marketCap?.value ? formatDollarAmount(tokenDetailData.marketCap?.value) : '-'}
|
||||
</StatPrice>
|
||||
</Stat>
|
||||
<Stat>
|
||||
24H volume
|
||||
<StatPrice>
|
||||
{tokenDetailData.volume24h?.value ? formatDollarAmount(tokenDetailData.volume24h?.value) : '-'}
|
||||
</StatPrice>
|
||||
</Stat>
|
||||
</StatPair>
|
||||
<StatPair>
|
||||
<Stat>
|
||||
52W low
|
||||
<StatPrice>
|
||||
{tokenDetailData.priceLow52W?.value ? formatDollarAmount(tokenDetailData.priceLow52W?.value) : '-'}
|
||||
</StatPrice>
|
||||
</Stat>
|
||||
<Stat>
|
||||
52W high
|
||||
<StatPrice>
|
||||
{tokenDetailData.priceHigh52W?.value ? formatDollarAmount(tokenDetailData.priceHigh52W?.value) : '-'}
|
||||
</StatPrice>
|
||||
</Stat>
|
||||
</StatPair>
|
||||
</StatsSection>
|
||||
<ContractAddressSection>
|
||||
<Contract>
|
||||
Contract address
|
||||
<ContractAddress>
|
||||
<CopyContractAddress address={address} />
|
||||
</ContractAddress>
|
||||
</Contract>
|
||||
</ContractAddressSection>
|
||||
<TokenSafetyModal
|
||||
isOpen={warningModalOpen}
|
||||
tokenAddress={address}
|
||||
@ -338,88 +283,6 @@ export default function LoadedTokenDetail({ address }: { address: string }) {
|
||||
onContinue={handleDismissWarning}
|
||||
/>
|
||||
</TopArea>
|
||||
)
|
||||
}
|
||||
|
||||
const tokenName = tokenDetailData.name
|
||||
const tokenSymbol = tokenDetailData.tokens?.[0].symbol?.toUpperCase()
|
||||
|
||||
return (
|
||||
<TopArea>
|
||||
<BreadcrumbNavLink to="/tokens">
|
||||
<ArrowLeft size={14} /> Tokens
|
||||
</BreadcrumbNavLink>
|
||||
<ChartHeader>
|
||||
<TokenInfoContainer>
|
||||
<TokenNameCell>
|
||||
<CurrencyLogo currency={currency} size={'32px'} />
|
||||
{tokenName ?? <Trans>Name not found</Trans>}
|
||||
<TokenSymbol>{tokenSymbol ?? <Trans>Symbol not found</Trans>}</TokenSymbol>
|
||||
{!warning && <VerifiedIcon size="20px" />}
|
||||
{networkBadgebackgroundColor && (
|
||||
<NetworkBadge networkColor={chainInfo?.color} backgroundColor={networkBadgebackgroundColor}>
|
||||
{networkLabel}
|
||||
</NetworkBadge>
|
||||
)}
|
||||
</TokenNameCell>
|
||||
<TokenActions>
|
||||
{tokenName && tokenSymbol && (
|
||||
<ShareButton tokenName={tokenName} tokenSymbol={tokenSymbol} tokenAddress={address} />
|
||||
)}
|
||||
<ClickFavorited onClick={toggleFavorite}>
|
||||
<FavoriteIcon isFavorited={isFavorited} />
|
||||
</ClickFavorited>
|
||||
</TokenActions>
|
||||
</TokenInfoContainer>
|
||||
<ChartContainer>
|
||||
<ParentSize>{({ width, height }) => <PriceChart token={token} width={width} height={height} />}</ParentSize>
|
||||
</ChartContainer>
|
||||
</ChartHeader>
|
||||
<AboutSection address={address} tokenDetailData={relevantTokenDetailData} />
|
||||
<StatsSection>
|
||||
<StatPair>
|
||||
<Stat>
|
||||
Market cap
|
||||
<StatPrice>
|
||||
{tokenDetailData.marketCap?.value ? formatDollarAmount(tokenDetailData.marketCap?.value) : '-'}
|
||||
</StatPrice>
|
||||
</Stat>
|
||||
<Stat>
|
||||
24H volume
|
||||
<StatPrice>
|
||||
{tokenDetailData.volume24h?.value ? formatDollarAmount(tokenDetailData.volume24h?.value) : '-'}
|
||||
</StatPrice>
|
||||
</Stat>
|
||||
</StatPair>
|
||||
<StatPair>
|
||||
<Stat>
|
||||
52W low
|
||||
<StatPrice>
|
||||
{tokenDetailData.priceLow52W?.value ? formatDollarAmount(tokenDetailData.priceLow52W?.value) : '-'}
|
||||
</StatPrice>
|
||||
</Stat>
|
||||
<Stat>
|
||||
52W high
|
||||
<StatPrice>
|
||||
{tokenDetailData.priceHigh52W?.value ? formatDollarAmount(tokenDetailData.priceHigh52W?.value) : '-'}
|
||||
</StatPrice>
|
||||
</Stat>
|
||||
</StatPair>
|
||||
</StatsSection>
|
||||
<ContractAddressSection>
|
||||
<Contract>
|
||||
Contract address
|
||||
<ContractAddress>
|
||||
<CopyContractAddress address={address} />
|
||||
</ContractAddress>
|
||||
</Contract>
|
||||
</ContractAddressSection>
|
||||
<TokenSafetyModal
|
||||
isOpen={warningModalOpen}
|
||||
tokenAddress={address}
|
||||
onCancel={() => navigate(-1)}
|
||||
onContinue={handleDismissWarning}
|
||||
/>
|
||||
</TopArea>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
81
src/components/Tokens/TokenDetails/TokenDetailContainers.tsx
Normal file
81
src/components/Tokens/TokenDetails/TokenDetailContainers.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
export const AboutContainer = styled.div`
|
||||
gap: 16px;
|
||||
padding: 24px 0px;
|
||||
`
|
||||
export const AboutHeader = styled.span`
|
||||
font-size: 28px;
|
||||
line-height: 36px;
|
||||
`
|
||||
export const BreadcrumbNavLink = styled(Link)`
|
||||
display: flex;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
text-decoration: none;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.textTertiary};
|
||||
}
|
||||
`
|
||||
export const ChartHeader = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.textPrimary};
|
||||
gap: 4px;
|
||||
margin-bottom: 24px;
|
||||
`
|
||||
export const ContractAddressSection = styled.div`
|
||||
padding: 24px 0px;
|
||||
`
|
||||
export const ChartContainer = styled.div`
|
||||
display: flex;
|
||||
height: 436px;
|
||||
align-items: center;
|
||||
`
|
||||
export const Stat = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: ${({ theme }) => theme.textSecondary};
|
||||
font-size: 14px;
|
||||
min-width: 168px;
|
||||
flex: 1;
|
||||
gap: 4px;
|
||||
padding: 24px 0px;
|
||||
`
|
||||
export const StatsSection = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
export const StatPair = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
`
|
||||
export const TokenNameCell = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
align-items: center;
|
||||
`
|
||||
export const TokenInfoContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
export const TopArea = styled.div`
|
||||
max-width: 832px;
|
||||
overflow: hidden;
|
||||
`
|
||||
export const ResourcesContainer = styled.div`
|
||||
display: flex;
|
||||
padding-top: 12px;
|
||||
gap: 14px;
|
||||
`
|
@ -5,7 +5,6 @@ import { EventName } from 'components/AmplitudeAnalytics/constants'
|
||||
import SparklineChart from 'components/Charts/SparklineChart'
|
||||
import CurrencyLogo from 'components/CurrencyLogo'
|
||||
import { getChainInfo } from 'constants/chainInfo'
|
||||
import { useTokenPriceQuery } from 'graphql/data/TokenPriceQuery'
|
||||
import { useCurrency, useToken } from 'hooks/Tokens'
|
||||
import { TimePeriod, TokenData } from 'hooks/useExplorePageQuery'
|
||||
import { useAtom } from 'jotai'
|
||||
@ -33,7 +32,7 @@ import {
|
||||
useSetSortCategory,
|
||||
useToggleFavorite,
|
||||
} from '../state'
|
||||
import { DATA_EMPTY, getDelta, PricePoint } from '../TokenDetails/PriceChart'
|
||||
import { formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart'
|
||||
import { Category, SortDirection } from '../types'
|
||||
import { DISPLAYS } from './TimeSelector'
|
||||
|
||||
@ -455,17 +454,9 @@ export default function LoadedRow({
|
||||
const filterString = useAtomValue(filterStringAtom)
|
||||
const filterNetwork = useAtomValue(filterNetworkAtom)
|
||||
const L2Icon = getChainInfo(filterNetwork).circleLogoUrl
|
||||
|
||||
// TODO: make delta shareable and fix based on future changes
|
||||
const pricePoints: PricePoint[] = useTokenPriceQuery(tokenAddress, timePeriod, 'ETHEREUM').filter(
|
||||
(p): p is PricePoint => Boolean(p && p.value)
|
||||
)
|
||||
const hasData = pricePoints.length !== 0
|
||||
|
||||
/* TODO: Implement API calls & cache to use here */
|
||||
const startingPrice = hasData ? pricePoints[0] : DATA_EMPTY
|
||||
const endingPrice = hasData ? pricePoints[pricePoints.length - 1] : DATA_EMPTY
|
||||
const [delta, arrow] = getDelta(startingPrice.value, endingPrice.value)
|
||||
const delta = tokenData.percentChange[timePeriod]?.value
|
||||
const arrow = delta ? getDeltaArrow(delta) : null
|
||||
const formattedDelta = delta ? formatDelta(delta) : null
|
||||
|
||||
const exploreTokenSelectedEventProperties = {
|
||||
chain_id: filterNetwork,
|
||||
@ -523,7 +514,7 @@ export default function LoadedRow({
|
||||
}
|
||||
percentChange={
|
||||
<ClickableContent>
|
||||
{delta}
|
||||
{formattedDelta}
|
||||
{arrow}
|
||||
</ClickableContent>
|
||||
}
|
||||
|
@ -7,9 +7,9 @@ import {
|
||||
sortDirectionAtom,
|
||||
} from 'components/Tokens/state'
|
||||
import { useAllTokens } from 'hooks/Tokens'
|
||||
import { TimePeriod, TokenData, UseTopTokensResult } from 'hooks/useExplorePageQuery'
|
||||
import { TimePeriod, TokenData } from 'hooks/useExplorePageQuery'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { ReactNode, useCallback, useMemo } from 'react'
|
||||
import { ReactNode, Suspense, useCallback, useMemo } from 'react'
|
||||
import { AlertTriangle } from 'react-feather'
|
||||
import styled from 'styled-components/macro'
|
||||
|
||||
@ -143,7 +143,7 @@ const LOADING_ROWS = Array.from({ length: 100 })
|
||||
.fill(0)
|
||||
.map((_item, index) => <LoadingRow key={index} />)
|
||||
|
||||
function LoadingTokenTable() {
|
||||
export function LoadingTokenTable() {
|
||||
return (
|
||||
<GridContainer>
|
||||
<HeaderRow />
|
||||
@ -152,7 +152,7 @@ function LoadingTokenTable() {
|
||||
)
|
||||
}
|
||||
|
||||
export default function TokenTable({ data, error, loading }: UseTopTokensResult) {
|
||||
export default function TokenTable({ data }: { data: Record<string, TokenData> | null }) {
|
||||
const showFavorites = useAtomValue<boolean>(showFavoritesAtom)
|
||||
const timePeriod = useAtomValue<TimePeriod>(filterTimeAtom)
|
||||
const topTokenAddresses = data ? Object.keys(data) : []
|
||||
@ -160,9 +160,7 @@ export default function TokenTable({ data, error, loading }: UseTopTokensResult)
|
||||
const filteredAndSortedTokens = useSortedTokens(filteredTokens, data)
|
||||
|
||||
/* loading and error state */
|
||||
if (loading) {
|
||||
return <LoadingTokenTable />
|
||||
} else if (error || data === null) {
|
||||
if (data === null) {
|
||||
return (
|
||||
<NoTokensState
|
||||
message={
|
||||
@ -184,20 +182,22 @@ export default function TokenTable({ data, error, loading }: UseTopTokensResult)
|
||||
}
|
||||
|
||||
return (
|
||||
<GridContainer>
|
||||
<HeaderRow />
|
||||
<TokenRowsContainer>
|
||||
{filteredAndSortedTokens.map((tokenAddress, index) => (
|
||||
<LoadedRow
|
||||
key={tokenAddress}
|
||||
tokenAddress={tokenAddress}
|
||||
tokenListIndex={index}
|
||||
tokenListLength={filteredAndSortedTokens.length}
|
||||
tokenData={data[tokenAddress]}
|
||||
timePeriod={timePeriod}
|
||||
/>
|
||||
))}
|
||||
</TokenRowsContainer>
|
||||
</GridContainer>
|
||||
<Suspense fallback={<LoadingTokenTable />}>
|
||||
<GridContainer>
|
||||
<HeaderRow />
|
||||
<TokenRowsContainer>
|
||||
{filteredAndSortedTokens.map((tokenAddress, index) => (
|
||||
<LoadedRow
|
||||
key={tokenAddress}
|
||||
tokenAddress={tokenAddress}
|
||||
tokenListIndex={index}
|
||||
tokenListLength={filteredAndSortedTokens.length}
|
||||
tokenData={data[tokenAddress]}
|
||||
timePeriod={timePeriod}
|
||||
/>
|
||||
))}
|
||||
</TokenRowsContainer>
|
||||
</GridContainer>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
@ -48,6 +48,34 @@ export function useTopTokenQuery(page: number) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
volumeAll: volume(duration: MAX) {
|
||||
value
|
||||
currency
|
||||
}
|
||||
pricePercentChange1H: pricePercentChange(duration: HOUR) {
|
||||
currency
|
||||
value
|
||||
}
|
||||
pricePercentChange24h {
|
||||
currency
|
||||
value
|
||||
}
|
||||
pricePercentChange1W: pricePercentChange(duration: WEEK) {
|
||||
currency
|
||||
value
|
||||
}
|
||||
pricePercentChange1M: pricePercentChange(duration: MONTH) {
|
||||
currency
|
||||
value
|
||||
}
|
||||
pricePercentChange1Y: pricePercentChange(duration: YEAR) {
|
||||
currency
|
||||
value
|
||||
}
|
||||
pricePercentChangeAll: pricePercentChange(duration: MAX) {
|
||||
currency
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -73,7 +101,15 @@ export function useTopTokenQuery(page: number) {
|
||||
[TimePeriod.WEEK]: token?.markets?.[0]?.volume1W,
|
||||
[TimePeriod.MONTH]: token?.markets?.[0]?.volume1M,
|
||||
[TimePeriod.YEAR]: token?.markets?.[0]?.volume1Y,
|
||||
[TimePeriod.ALL]: token?.markets?.[0]?.volume1Y, // todo: figure out all
|
||||
[TimePeriod.ALL]: token?.markets?.[0]?.volumeAll,
|
||||
},
|
||||
percentChange: {
|
||||
[TimePeriod.HOUR]: token?.markets?.[0]?.pricePercentChange1H,
|
||||
[TimePeriod.DAY]: token?.markets?.[0]?.pricePercentChange24h,
|
||||
[TimePeriod.WEEK]: token?.markets?.[0]?.pricePercentChange1W,
|
||||
[TimePeriod.MONTH]: token?.markets?.[0]?.pricePercentChange1M,
|
||||
[TimePeriod.YEAR]: token?.markets?.[0]?.pricePercentChange1Y,
|
||||
[TimePeriod.ALL]: token?.markets?.[0]?.pricePercentChangeAll,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ export type TokenData = {
|
||||
price: IAmount | null | undefined
|
||||
marketCap: IAmount | null | undefined
|
||||
volume: Record<TimePeriod, IAmount | null | undefined>
|
||||
percentChange: Record<TimePeriod, IAmount | null | undefined>
|
||||
}
|
||||
|
||||
export interface UseTopTokensResult {
|
||||
|
@ -39,7 +39,8 @@ import RemoveLiquidity from './RemoveLiquidity'
|
||||
import RemoveLiquidityV3 from './RemoveLiquidity/V3'
|
||||
import Swap from './Swap'
|
||||
import { OpenClaimAddressModalAndRedirectToSwap, RedirectPathToSwapOnly, RedirectToSwap } from './Swap/redirects'
|
||||
import Tokens from './Tokens'
|
||||
import { LoadingTokenDetails } from './TokenDetails'
|
||||
import Tokens, { LoadingTokens } from './Tokens'
|
||||
|
||||
const TokenDetails = lazy(() => import('./TokenDetails'))
|
||||
const Vote = lazy(() => import('./Vote'))
|
||||
@ -161,8 +162,22 @@ export default function App() {
|
||||
<Routes>
|
||||
{tokensFlag === TokensVariant.Enabled && (
|
||||
<>
|
||||
<Route path="/tokens" element={<Tokens />} />
|
||||
<Route path="/tokens/:tokenAddress" element={<TokenDetails />} />
|
||||
<Route
|
||||
path="/tokens"
|
||||
element={
|
||||
<Suspense fallback={<LoadingTokens />}>
|
||||
<Tokens />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/tokens/:tokenAddress"
|
||||
element={
|
||||
<Suspense fallback={<LoadingTokenDetails />}>
|
||||
<TokenDetails />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Route
|
||||
|
@ -18,7 +18,6 @@ import { checkWarning } from 'constants/tokenSafety'
|
||||
import { useToken } from 'hooks/Tokens'
|
||||
import { useActiveLocale } from 'hooks/useActiveLocale'
|
||||
import { useNetworkTokenBalances } from 'hooks/useNetworkTokenBalances'
|
||||
import useTokenDetailPageQuery from 'hooks/useTokenDetailPageQuery'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useIsDarkMode } from 'state/user/hooks'
|
||||
@ -67,7 +66,6 @@ function NetworkBalances(tokenAddress: string) {
|
||||
|
||||
export default function TokenDetails() {
|
||||
const { tokenAddress } = useParams<{ tokenAddress?: string }>()
|
||||
const { loading } = useTokenDetailPageQuery(tokenAddress)
|
||||
const tokenSymbol = useToken(tokenAddress)?.symbol
|
||||
|
||||
const darkMode = useIsDarkMode()
|
||||
@ -83,16 +81,6 @@ export default function TokenDetails() {
|
||||
console.log('onTxFail')
|
||||
}, [])
|
||||
|
||||
let tokenDetail
|
||||
if (!tokenAddress) {
|
||||
// TODO: handle no address / invalid address cases
|
||||
tokenDetail = 'invalid token address'
|
||||
} else if (loading) {
|
||||
tokenDetail = <LoadingTokenDetail />
|
||||
} else {
|
||||
tokenDetail = <TokenDetail address={tokenAddress} />
|
||||
}
|
||||
|
||||
const tokenWarning = tokenAddress ? checkWarning(tokenAddress) : null
|
||||
/* network balance handling */
|
||||
|
||||
@ -133,9 +121,9 @@ export default function TokenDetails() {
|
||||
|
||||
return (
|
||||
<TokenDetailsLayout>
|
||||
{tokenDetail}
|
||||
{tokenAddress && (
|
||||
<>
|
||||
<TokenDetail address={tokenAddress} />
|
||||
<RightPanel>
|
||||
<SwapWidget
|
||||
defaultChainId={connectedChainId}
|
||||
@ -154,21 +142,27 @@ export default function TokenDetails() {
|
||||
width={WIDGET_WIDTH}
|
||||
/>
|
||||
{tokenWarning && <TokenSafetyMessage tokenAddress={tokenAddress} warning={tokenWarning} />}
|
||||
{!loading && (
|
||||
<BalanceSummary address={tokenAddress} totalBalance={totalBalance} networkBalances={balancesByNetwork} />
|
||||
)}
|
||||
<BalanceSummary address={tokenAddress} totalBalance={totalBalance} networkBalances={balancesByNetwork} />
|
||||
</RightPanel>
|
||||
<Footer>
|
||||
{!loading && (
|
||||
<FooterBalanceSummary
|
||||
address={tokenAddress}
|
||||
totalBalance={totalBalance}
|
||||
networkBalances={balancesByNetwork}
|
||||
/>
|
||||
)}
|
||||
<FooterBalanceSummary
|
||||
address={tokenAddress}
|
||||
totalBalance={totalBalance}
|
||||
networkBalances={balancesByNetwork}
|
||||
/>
|
||||
</Footer>
|
||||
</>
|
||||
)}
|
||||
</TokenDetailsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export function LoadingTokenDetails() {
|
||||
return (
|
||||
<TokenDetailsLayout>
|
||||
<LoadingTokenDetail />
|
||||
<RightPanel></RightPanel>
|
||||
<Footer />
|
||||
</TokenDetailsLayout>
|
||||
)
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import FavoriteButton from 'components/Tokens/TokenTable/FavoriteButton'
|
||||
import NetworkFilter from 'components/Tokens/TokenTable/NetworkFilter'
|
||||
import SearchBar from 'components/Tokens/TokenTable/SearchBar'
|
||||
import TimeSelector from 'components/Tokens/TokenTable/TimeSelector'
|
||||
import TokenTable from 'components/Tokens/TokenTable/TokenTable'
|
||||
import TokenTable, { LoadingTokenTable } from 'components/Tokens/TokenTable/TokenTable'
|
||||
import { TokensNetworkFilterVariant, useTokensNetworkFilterFlag } from 'featureFlags/flags/tokensNetworkFilter'
|
||||
import { useTopTokenQuery } from 'graphql/data/TopTokenQuery'
|
||||
import { useResetAtom } from 'jotai/utils'
|
||||
@ -65,8 +65,6 @@ const Tokens = () => {
|
||||
const tokensNetworkFilterFlag = useTokensNetworkFilterFlag()
|
||||
const resetFilterString = useResetAtom(filterStringAtom)
|
||||
const location = useLocation()
|
||||
const error = null
|
||||
const loading = false
|
||||
useEffect(() => {
|
||||
resetFilterString()
|
||||
}, [location, resetFilterString])
|
||||
@ -91,11 +89,30 @@ const Tokens = () => {
|
||||
</FiltersWrapper>
|
||||
|
||||
<TokenTableContainer>
|
||||
<TokenTable data={topTokens} error={error} loading={loading} />
|
||||
<TokenTable data={topTokens} />
|
||||
</TokenTableContainer>
|
||||
</ExploreContainer>
|
||||
</Trace>
|
||||
)
|
||||
}
|
||||
|
||||
export const LoadingTokens = () => {
|
||||
return (
|
||||
<ExploreContainer>
|
||||
<TitleContainer>
|
||||
<ThemedText.LargeHeader>
|
||||
<Trans>Explore Tokens</Trans>
|
||||
</ThemedText.LargeHeader>
|
||||
</TitleContainer>
|
||||
<FiltersWrapper>
|
||||
<FiltersContainer />
|
||||
<SearchContainer />
|
||||
</FiltersWrapper>
|
||||
<TokenTableContainer>
|
||||
<LoadingTokenTable />
|
||||
</TokenTableContainer>
|
||||
</ExploreContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tokens
|
||||
|
Loading…
Reference in New Issue
Block a user