Commit 84fb0523 authored by cartcrom's avatar cartcrom Committed by GitHub

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