Skip to content

Commit

Permalink
[W.I.P.] Start to build token dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
csillag committed Jun 29, 2023
1 parent 6f2e33d commit 6ae0fd3
Show file tree
Hide file tree
Showing 12 changed files with 422 additions and 15 deletions.
1 change: 1 addition & 0 deletions .changelog/623.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add token dashboard
71 changes: 71 additions & 0 deletions src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { FC } from 'react'
import Card from '@mui/material/Card'
import { useRequiredScopeParam } from '../../hooks/useScopeParam'
import { useLoaderData } from 'react-router-dom'
import { useTokenInfo } from './hook'
import { useAccount } from '../AccountDetailsPage/hook'
import { TextSkeleton } from '../../components/Skeleton'
import { StyledDescriptionList } from '../../components/StyledDescriptionList'
import { useScreenSize } from '../../hooks/useScreensize'
import { useTranslation } from 'react-i18next'
import { AccountLink } from '../../components/Account/AccountLink'
import { CopyToClipboard } from '../../components/CopyToClipboard'
import { ContractVerificationIcon } from '../../components/ContractVerificationIcon'
import { getTokenTypeName } from './TokenTypeCard'
import { getNameForTicker, Ticker } from '../../../types/ticker'

export const TokenDetailsCard: FC = () => {
const { t } = useTranslation()
const scope = useRequiredScopeParam()
const address = useLoaderData() as string
const { isMobile } = useScreenSize()

const { token, isLoading: tokenIsLoading } = useTokenInfo(scope, address)
const { account, isLoading: accountIsLoading } = useAccount(scope, address)
const isLoading = tokenIsLoading || accountIsLoading

const contract = account?.evm_contract

const transactionsLabel = account ? account.stats.num_txns.toLocaleString() : ''
const balance = account?.balances[0]?.balance || '0'
const nativeToken = account?.ticker || Ticker.ROSE
const tickerName = getNameForTicker(t, nativeToken)

return (
<Card>
{isLoading && <TextSkeleton numberOfRows={8} />}
{account && token && contract && (
<StyledDescriptionList titleWidth={isMobile ? '100px' : '200px'}>
<dt>{t('common.token')}</dt>
<dd>{token.name}</dd>

<dt>{t(isMobile ? 'common.smartContract_short' : 'common.smartContract')}</dt>
<dd>
<AccountLink scope={account} address={address!} />
<CopyToClipboard value={address!} />
</dd>

<dt>{t('contract.verification.title')}</dt>
<dd>
<ContractVerificationIcon
verified={!!account?.evm_contract?.verification}
address_eth={account.address_eth!}
/>
</dd>

<dt>{t('common.type')} </dt>
<dd>{getTokenTypeName(t, token.type)} </dd>

<dt>{t('contract.creator')}</dt>
<dd> {contract.creation_tx ? contract.creation_tx : t('common.missing')}</dd>

<dt>{t('common.balance')} </dt>
<dd>{t('common.valueInToken', { value: balance, ticker: tickerName })}</dd>

<dt>{t('common.transactions')}</dt>
<dd>{transactionsLabel}</dd>
</StyledDescriptionList>
)}
</Card>
)
}
40 changes: 40 additions & 0 deletions src/app/pages/TokenDashboardPage/TokenHoldersCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
import { SnapshotCard } from '../../components/Snapshots/SnapshotCard'
import { COLORS } from '../../../styles/theme/colors'
import { useRequiredScopeParam } from '../../hooks/useScopeParam'
import { useTokenInfo } from './hook'
import { useLoaderData } from 'react-router-dom'

export const TokenHoldersCard: FC = () => {
const { t } = useTranslation()
const scope = useRequiredScopeParam()

const address = useLoaderData() as string

const { token, isFetched } = useTokenInfo(scope, address)

const title = t('tokens.holders')
return (
<SnapshotCard title={title}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
{isFetched && (
<>
<Typography
component="span"
sx={{
fontSize: '48px',
fontWeight: 700,
color: COLORS.brandDark,
}}
>
{t('tokens.holdersValue', { value: token?.num_holders })}
</Typography>
</>
)}
</Box>
</SnapshotCard>
)
}
61 changes: 61 additions & 0 deletions src/app/pages/TokenDashboardPage/TokenSnapshot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { FC } from 'react'
import Box from '@mui/material/Box'
import Grid from '@mui/material/Grid'
import Typography from '@mui/material/Typography'
import { useScreenSize } from '../../hooks/useScreensize'
import { useTheme } from '@mui/material/styles'
import { styled } from '@mui/material/styles'
import { useTranslation } from 'react-i18next'
import { AppendMobileSearch } from '../../components/AppendMobileSearch'
import { useRequiredScopeParam } from '../../hooks/useScopeParam'
import { TokenSupplyCard } from './TokenSupplyCard'
import { TokenHoldersCard } from './TokenHoldersCard'
import { TokenTypeCard } from './TokenTypeCard'
import { TokenTotalTransactionsCard } from './TokenTotalTransactionsCard'

const StyledGrid = styled(Grid)(() => ({
display: 'flex',
}))

export const TokenSnapshot: FC = () => {
const { t } = useTranslation()

const scope = useRequiredScopeParam()
const theme = useTheme()
const { isMobile } = useScreenSize()

return (
<>
<Grid container sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 4 }}>
<Grid item xs={12} sx={{ px: isMobile ? 4 : 0 }}>
<AppendMobileSearch scope={scope}>
<Box sx={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', mb: 2 }}>
<Typography
variant="h3"
sx={{ color: theme.palette.layout.main, fontWeight: 700, mr: 3, mb: isMobile ? 4 : 0 }}
>
{t('tokenSnapshot.header')}
</Typography>
</Box>
</AppendMobileSearch>
</Grid>
</Grid>

<Grid container rowSpacing={1} columnSpacing={4} columns={22}>
<StyledGrid item xs={22} md={5}>
<TokenTotalTransactionsCard />
</StyledGrid>
<StyledGrid item xs={22} md={6}>
<TokenSupplyCard />
</StyledGrid>
<StyledGrid item xs={22} md={5}>
<TokenHoldersCard />
</StyledGrid>
<StyledGrid item xs={22} md={6}>
<TokenTypeCard />
</StyledGrid>
</Grid>
</>
)
}
// <TransactionsChartCard chartDuration={chartDuration} />
43 changes: 43 additions & 0 deletions src/app/pages/TokenDashboardPage/TokenSupplyCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
import { SnapshotCard } from '../../components/Snapshots/SnapshotCard'
import { COLORS } from '../../../styles/theme/colors'
import { useRequiredScopeParam } from '../../hooks/useScopeParam'
import { useTokenInfo } from './hook'
import { useLoaderData } from 'react-router-dom'

export const TokenSupplyCard: FC = () => {
const { t } = useTranslation()
const scope = useRequiredScopeParam()

const address = useLoaderData() as string

const { token, isFetched } = useTokenInfo(scope, address)

const title = t('tokens.totalSupply')
const supplyString = token?.total_supply || '0'
const supplyNumber = parseInt(supplyString)

return (
<SnapshotCard title={title} label={token?.symbol}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
{isFetched && (
<>
<Typography
component="span"
sx={{
fontSize: '48px',
fontWeight: 700,
color: COLORS.brandDark,
}}
>
{t('tokens.totalSupplyValue', { value: supplyNumber })}
</Typography>
</>
)}
</Box>
</SnapshotCard>
)
}
52 changes: 52 additions & 0 deletions src/app/pages/TokenDashboardPage/TokenTitleCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { FC } from 'react'
import { useRequiredScopeParam } from '../../hooks/useScopeParam'
import { useLoaderData } from 'react-router-dom'
import Card from '@mui/material/Card'
import CardHeader from '@mui/material/CardHeader'
import { useTokenInfo } from './hook'
import Skeleton from '@mui/material/Skeleton'
import { ContractVerificationIcon } from '../../components/ContractVerificationIcon'
import { useAccount } from '../AccountDetailsPage/hook'

const TitleSkeleton: FC = () => <Skeleton variant="text" sx={{ display: 'inline-block', width: '100%' }} />

export const TokenTitleCard: FC = () => {
const scope = useRequiredScopeParam()
const address = useLoaderData() as string

const { isLoading, token } = useTokenInfo(scope, address)
const { account } = useAccount(scope, address)

const title = isLoading ? <TitleSkeleton /> : token?.name
const subTitle = isLoading ? null : ` (${token?.symbol})` || null

const addressEth = account?.address_eth

return (
<Card>
<CardHeader
disableTypography
component="h2"
title={title}
subheader={subTitle}
action={
<>
{addressEth && (
<ContractVerificationIcon
verified={!!account?.evm_contract?.verification}
address_eth={addressEth}
/>
)}
{/*<Link*/}
{/* component={RouterLink}*/}
{/* to={RouteUtils.getLatestTransactionsRoute(scope)}*/}
{/* sx={{ color: COLORS.brandExtraDark }}*/}
{/*>*/}
{/* {t('common.viewAll')}*/}
{/*</Link>*/}
</>
}
/>
</Card>
)
}
40 changes: 40 additions & 0 deletions src/app/pages/TokenDashboardPage/TokenTotalTransactionsCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
import { SnapshotCard } from '../../components/Snapshots/SnapshotCard'
import { COLORS } from '../../../styles/theme/colors'
import { useRequiredScopeParam } from '../../hooks/useScopeParam'
import { useLoaderData } from 'react-router-dom'
import { useAccount } from '../AccountDetailsPage/hook'

export const TokenTotalTransactionsCard: FC = () => {
const { t } = useTranslation()
const scope = useRequiredScopeParam()

const address = useLoaderData() as string

const { isFetched, account } = useAccount(scope, address)
const value = account?.stats.num_txns

return (
<SnapshotCard title={t('totalTransactions.header')}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
{isFetched && (
<>
<Typography
component="span"
sx={{
fontSize: '48px',
fontWeight: 700,
color: COLORS.brandDark,
}}
>
{t('totalTransactions.value', { value })}
</Typography>
</>
)}
</Box>
</SnapshotCard>
)
}
52 changes: 52 additions & 0 deletions src/app/pages/TokenDashboardPage/TokenTypeCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Box from '@mui/material/Box'
import Typography from '@mui/material/Typography'
import { SnapshotCard } from '../../components/Snapshots/SnapshotCard'
import { COLORS } from '../../../styles/theme/colors'
import { useRequiredScopeParam } from '../../hooks/useScopeParam'
import { useTokenInfo } from './hook'
import { useLoaderData } from 'react-router-dom'
import { EvmTokenType } from '../../../oasis-indexer/api'
import { TFunction } from 'i18next'
import { exhaustedTypeWarning } from '../../../types/errors'

export const getTokenTypeName = (t: TFunction, type: EvmTokenType): string => {
switch (type) {
case 'ERC20':
return t('account.ERC20')
default:
void exhaustedTypeWarning('Unknown token type', type)
return '???'
}
}

export const TokenTypeCard: FC = () => {
const { t } = useTranslation()
const scope = useRequiredScopeParam()

const address = useLoaderData() as string

const { token, isFetched } = useTokenInfo(scope, address)

return (
<SnapshotCard title={t('tokens.type')}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
{isFetched && (
<>
<Typography
component="span"
sx={{
fontSize: '48px',
fontWeight: 700,
color: COLORS.brandDark,
}}
>
{token?.type ? getTokenTypeName(t, token.type) : '-'}
</Typography>
</>
)}
</Box>
</SnapshotCard>
)
}
15 changes: 15 additions & 0 deletions src/app/pages/TokenDashboardPage/hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Layer, useGetRuntimeEvmTokensAddress } from '../../../oasis-indexer/api'
import { AppErrors } from '../../../types/errors'
import { SearchScope } from '../../../types/searchScope'

export const useTokenInfo = (scope: SearchScope, address: string) => {
const { network, layer } = scope
if (layer === Layer.consensus) {
// There can be no ERC-20 or ERC-721 tokens on consensus
throw AppErrors.UnsupportedLayer
}
const query = useGetRuntimeEvmTokensAddress(network, layer, address!)
const token = query.data?.data
const { isLoading, isError, isFetched } = query
return { token, isLoading, isError, isFetched }
}
Loading

0 comments on commit 6ae0fd3

Please sign in to comment.