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:
cartcrom 2022-08-24 14:13:21 -04:00 committed by GitHub
parent 7500bbc0be
commit 84fb05239b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 311 additions and 299 deletions

@ -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>
)
}

@ -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