Skip to content

Commit

Permalink
Add Tokens page and dashboard component
Browse files Browse the repository at this point in the history
  • Loading branch information
csillag committed Jun 16, 2023
1 parent dfca051 commit ece4d04
Show file tree
Hide file tree
Showing 10 changed files with 344 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .changelog/546.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add token overview page and dashboard component

18 changes: 18 additions & 0 deletions src/app/components/Tokens/TokenLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { FC } from 'react'
import { Link as RouterLink } from 'react-router-dom'
import Link from '@mui/material/Link'

import { RouteUtils } from '../../utils/route-utils'
import { SearchScope } from '../../../types/searchScope'

export const TokenLink: FC<{ scope: SearchScope; address: string; name: string | undefined }> = ({
scope,
address,
name,
}) => {
return (
<Link component={RouterLink} to={RouteUtils.getTokenRoute(scope, address)}>
{name || address}
</Link>
)
}
82 changes: 82 additions & 0 deletions src/app/components/Tokens/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useTranslation } from 'react-i18next'
import { EvmToken } from '../../../oasis-indexer/api'
import { Table, TableCellAlign, TableColProps } from '../../components/Table'
import { TablePaginationProps } from '../Table/TablePagination'
import { AccountLink } from '../Account/AccountLink'
import { TokenLink } from './TokenLink'
import { CopyToClipboard } from '../CopyToClipboard'

type TokensProps = {
tokens?: EvmToken[]
isLoading: boolean
limit: number
verbose?: boolean
pagination: false | TablePaginationProps
}

export const Tokens = (props: TokensProps) => {
const { isLoading, tokens, pagination, limit } = props
const { t } = useTranslation()
const tableColumns: TableColProps[] = [
{ content: '' },
{ content: t('common.name') },
{ content: t('common.smartContract') },
{
content: t('tokens.holdersCount'),
align: TableCellAlign.Right,
},
{ content: t('tokens.supply'), align: TableCellAlign.Right },
{ content: t('common.ticker'), align: TableCellAlign.Right },
]

const tableRows = tokens?.map((token, index) => {
return {
key: token.contract_addr,
data: [
{
content: (index + 1).toLocaleString(),
key: 'index',
},
{
content: <TokenLink scope={token} address={token.evm_contract_addr} name={token.name} />,
key: 'name',
},
{
content: (
<span>
<AccountLink scope={token} address={token.evm_contract_addr} />
<CopyToClipboard value={token.evm_contract_addr} />
</span>
),
key: 'contactAddress',
},
{
content: token.num_holders.toLocaleString(),
key: 'holdersCount',
align: TableCellAlign.Right,
},
{
content: token.total_supply,
key: 'supply',
align: TableCellAlign.Right,
},
{
content: token.symbol,
key: 'ticker',
align: TableCellAlign.Right,
},
],
}
})

return (
<Table
columns={tableColumns}
rows={tableRows}
rowsNumber={limit}
name={t('tokens.title')}
isLoading={isLoading}
pagination={pagination}
/>
)
}
55 changes: 55 additions & 0 deletions src/app/pages/DashboardPage/TopTokens.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { FC } from 'react'
import { useTranslation } from 'react-i18next'
import Card from '@mui/material/Card'
import CardHeader from '@mui/material/CardHeader'
import CardContent from '@mui/material/CardContent'
import { Link as RouterLink } from 'react-router-dom'
import Link from '@mui/material/Link'
import { Layer, useGetRuntimeEvmTokens } from '../../../oasis-indexer/api'
import { NUMBER_OF_ITEMS_ON_DASHBOARD } from '../../config'
import { COLORS } from '../../../styles/theme/colors'
import { AppErrors } from '../../../types/errors'
import { useRequiredScopeParam } from '../../hooks/useScopeParam'
import { RouteUtils } from '../../utils/route-utils'
import { Tokens } from '../../components/Tokens'

const limit = NUMBER_OF_ITEMS_ON_DASHBOARD

export const TopTokens: FC = () => {
const { t } = useTranslation()
const scope = useRequiredScopeParam()
const { network, layer } = scope
if (layer === Layer.consensus) {
throw AppErrors.UnsupportedLayer
// Listing the latest consensus transactions is not yet supported.
// We should use useGetConsensusTransactions()
}
const tokensQuery = useGetRuntimeEvmTokens(network, layer, { limit })

return (
<Card>
<CardHeader
disableTypography
component="h3"
title={t('tokens.title')}
action={
<Link
component={RouterLink}
to={RouteUtils.getTopTokensRoute(scope)}
sx={{ color: COLORS.brandExtraDark }}
>
{t('common.viewAll')}
</Link>
}
/>
<CardContent>
<Tokens
tokens={tokensQuery.data?.data.evm_tokens}
isLoading={tokensQuery.isLoading}
limit={limit}
pagination={false}
/>
</CardContent>
</Card>
)
}
2 changes: 2 additions & 0 deletions src/app/pages/DashboardPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { TransactionsStats } from './TransactionsStats'
import { TotalTransactions } from './TotalTransactions'
import { PageLayout } from '../../components/PageLayout'
import { ParaTimeSnapshot } from './ParaTimeSnapshot'
import { TopTokens } from './TopTokens'

export const DashboardPage: FC = () => {
const { isMobile } = useScreenSize()
Expand All @@ -27,6 +28,7 @@ export const DashboardPage: FC = () => {
<LatestBlocks />
</Grid>
</Grid>
<TopTokens />
<TransactionsStats />
<TotalTransactions />
<Social />
Expand Down
55 changes: 55 additions & 0 deletions src/app/pages/TokenkDetailPage/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { FC } from 'react'
import { EvmToken } from '../../../oasis-indexer/api'
import { TextSkeleton } from '../../components/Skeleton'
import { StyledDescriptionList } from '../../components/StyledDescriptionList'
import { useScreenSize } from '../../hooks/useScreensize'
import { DashboardLink } from '../DashboardPage/DashboardLink'
import { useTranslation } from 'react-i18next'
import { TokenLink } from '../../components/Tokens/TokenLink'
import { CopyToClipboard } from '../../components/CopyToClipboard'
import { AccountLink } from '../../components/Account/AccountLink'

export const TokenDetailView: FC<{
isLoading?: boolean
token: EvmToken | undefined
showLayer?: boolean
standalone?: boolean
}> = ({ isLoading, token, showLayer, standalone = false }) => {
const { t } = useTranslation()
const { isMobile } = useScreenSize()

if (isLoading) return <TextSkeleton numberOfRows={7} />
if (!token) return null

return (
<StyledDescriptionList titleWidth={isMobile ? '100px' : '200px'} standalone={standalone}>
{showLayer && (
<>
<dt>{t('common.paratime')}</dt>
<dd>
<DashboardLink scope={token} />
</dd>
</>
)}
<dt>{t('common.name')}</dt>
<dd>
<TokenLink scope={token} address={token.evm_contract_addr} name={token.name} />
</dd>

<dt>{t('common.smartContract')}</dt>
<dd>
<AccountLink scope={token} address={token.evm_contract_addr} />
<CopyToClipboard value={token.evm_contract_addr} />
</dd>

<dt>{t('tokens.holdersCount')}</dt>
<dd>{token.num_holders.toLocaleString()}</dd>

<dt>{t('tokens.supply')}</dt>
<dd>{token.total_supply}</dd>

<td>{t('common.ticker')}</td>
<dd>{token.symbol}</dd>
</StyledDescriptionList>
)
}
110 changes: 110 additions & 0 deletions src/app/pages/TokensPage/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@mui/material/Divider'
import { useScreenSize } from '../../hooks/useScreensize'
import { styled } from '@mui/material/styles'
import { PageLayout } from '../../components/PageLayout'
import { SubPageCard } from '../../components/SubPageCard'
import { Layer, useGetRuntimeEvmTokens } from '../../../oasis-indexer/api'
import { NUMBER_OF_ITEMS_ON_SEPARATE_PAGE, REFETCH_INTERVAL } from '../../config'
import { useSearchParamsPagination } from '../../components/Table/useSearchParamsPagination'
import Box from '@mui/material/Box'
import { COLORS } from '../../../styles/theme/colors'
import { AppErrors } from '../../../types/errors'
import { TableLayout, TableLayoutButton } from '../../components/TableLayoutButton'
import { LoadMoreButton } from '../../components/LoadMoreButton'
import { useRequiredScopeParam } from '../../hooks/useScopeParam'
import { Tokens } from '../../components/Tokens'
import { TokenDetailView } from '../TokenkDetailPage'

const PAGE_SIZE = NUMBER_OF_ITEMS_ON_SEPARATE_PAGE

const TokenDetails = styled(Box)(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: `0 ${theme.spacing(2)}`,
backgroundColor: COLORS.brandDark,
}))

export const TokensPage: FC = () => {
const [tableView, setTableView] = useState<TableLayout>(TableLayout.Horizontal)
const { isMobile } = useScreenSize()
const { t } = useTranslation()
const pagination = useSearchParamsPagination('page')
const offset = (pagination.selectedPage - 1) * PAGE_SIZE
const scope = useRequiredScopeParam()
// Consensus is not yet enabled in ENABLED_LAYERS, just some preparation
if (scope.layer === Layer.consensus) {
throw AppErrors.UnsupportedLayer
// Listing the latest consensus blocks is not yet implemented.
// we should call useGetConsensusBlocks()
}

useEffect(() => {
if (!isMobile) {
setTableView(TableLayout.Horizontal)
}
}, [isMobile, setTableView])

const tokensQuery = useGetRuntimeEvmTokens(
scope.network,
scope.layer, // This is OK, since consensus is already handled separately
{
limit: tableView === TableLayout.Vertical ? offset + PAGE_SIZE : PAGE_SIZE,
offset: tableView === TableLayout.Vertical ? 0 : offset,
},
{
query: {
refetchInterval: REFETCH_INTERVAL,
// Keep showing data while loading more
keepPreviousData: tableView === TableLayout.Vertical,
},
},
)

return (
<PageLayout
mobileFooterAction={
tableView === TableLayout.Vertical && (
<LoadMoreButton pagination={pagination} isLoading={tokensQuery.isLoading} />
)
}
>
{!isMobile && <Divider variant="layout" />}
<SubPageCard
title={t('tokens.title')}
action={isMobile && <TableLayoutButton tableView={tableView} setTableView={setTableView} />}
noPadding={tableView === TableLayout.Vertical}
>
{tableView === TableLayout.Horizontal && (
<Tokens
isLoading={tokensQuery.isLoading}
tokens={tokensQuery.data?.data.evm_tokens}
limit={PAGE_SIZE}
verbose={true}
pagination={{
selectedPage: pagination.selectedPage,
linkToPage: pagination.linkToPage,
totalCount: tokensQuery.data?.data.total_count,
isTotalCountClipped: tokensQuery.data?.data.is_total_count_clipped,
rowsPerPage: NUMBER_OF_ITEMS_ON_SEPARATE_PAGE,
}}
/>
)}
{tableView === TableLayout.Vertical && (
<TokenDetails>
{tokensQuery.isLoading &&
[...Array(PAGE_SIZE).keys()].map(key => (
<TokenDetailView key={key} isLoading={true} token={undefined} standalone />
))}

{!tokensQuery.isLoading &&
tokensQuery.data?.data.evm_tokens.map(token => (
<TokenDetailView key={token.contract_addr} token={token} standalone />
))}
</TokenDetails>
)}
</SubPageCard>
</PageLayout>
)
}
10 changes: 10 additions & 0 deletions src/app/utils/route-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export abstract class RouteUtils {
return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/tx`
}

static getTopTokensRoute = ({ network, layer }: SearchScope) => {
return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/token`
}

static getLatestBlocksRoute = ({ network, layer }: SearchScope) => {
return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/block`
}
Expand Down Expand Up @@ -68,6 +72,12 @@ export abstract class RouteUtils {
: `/search?q=${encodeURIComponent(searchTerm)}`
}

static getTokenRoute = ({ network, layer }: SearchScope, tokenAddress: string) => {
return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/token/${encodeURIComponent(
tokenAddress,
)}`
}

static getEnabledLayersForNetwork(network: Network): Layer[] {
return RouteUtils.ENABLED_LAYERS_FOR_NETWORK[network] || []
}
Expand Down
5 changes: 5 additions & 0 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@
"tableLayoutButton": {
"changeView": "Change view"
},
"tokens": {
"holdersCount": "Holders count",
"supply": "Supply",
"title": "Tokens"
},
"totalTransactions": {
"header": "Total Transactions",
"tooltip": "{{value, number}} total transactions"
Expand Down
5 changes: 5 additions & 0 deletions src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { searchParamLoader } from './app/components/Search/search-utils'
import { RoutingErrorPage } from './app/pages/RoutingErrorPage'
import { ThemeByNetwork, withDefaultTheme } from './app/components/ThemeByNetwork'
import { useRequiredScopeParam } from './app/hooks/useScopeParam'
import { TokensPage } from './app/pages/TokensPage'

const NetworkSpecificPart = () => (
<ThemeByNetwork network={useRequiredScopeParam().network}>
Expand Down Expand Up @@ -95,6 +96,10 @@ export const routes: RouteObject[] = [
element: <TransactionDetailPage />,
loader: transactionParamLoader,
},
{
path: `token`,
element: <TokensPage />,
},
{
path: `token/:address`, // This is a temporal workaround, until we have the required dedicated functionality for tokens
element: <AccountDetailsPage />,
Expand Down

0 comments on commit ece4d04

Please sign in to comment.