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 {
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" />
}
}
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`
......@@ -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>
......
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
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