Skip to content

Commit

Permalink
Add first version of Tokens page
Browse files Browse the repository at this point in the history
  • Loading branch information
csillag committed Jun 16, 2023
1 parent 291d339 commit 5e723c7
Show file tree
Hide file tree
Showing 8 changed files with 362 additions and 0 deletions.
1 change: 1 addition & 0 deletions .changelog/546.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add token overview page
28 changes: 28 additions & 0 deletions src/app/components/Tokens/TokenLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { FC } from 'react'
import { Link as RouterLink } from 'react-router-dom'
import Typography from '@mui/material/Typography'
import Link from '@mui/material/Link'

import { RouteUtils } from '../../utils/route-utils'
import { TrimLinkLabel } from '../TrimLinkLabel'
import { SearchScope } from '../../../types/searchScope'
import { useScreenSize } from '../../hooks/useScreensize'

export const TokenLink: FC<{ scope: SearchScope; address: string; name: string | undefined }> = ({
scope,
address,
name,
}) => {
const { isTablet } = useScreenSize()
return (
<Typography variant="mono">
{isTablet ? (
<TrimLinkLabel label={name || address} to={RouteUtils.getTokenRoute(scope, address)} />
) : (
<Link component={RouterLink} to={RouteUtils.getTokenRoute(scope, address)}>
{name || address}
</Link>
)}
</Typography>
)
}
84 changes: 84 additions & 0 deletions src/app/components/Tokens/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { useTranslation } from 'react-i18next'
import { EvmToken } from '../../../oasis-indexer/api'
import { Table, TableCellAlign, TableColProps } from '../../components/Table'
import { TablePaginationProps } from '../Table/TablePagination'
import { useScreenSize } from '../../hooks/useScreensize'
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, verbose, pagination, limit } = props

Check warning on line 19 in src/app/components/Tokens/index.tsx

View workflow job for this annotation

GitHub Actions / lint

'verbose' is assigned a value but never used
const { t } = useTranslation()
const { isLaptop } = useScreenSize()

Check warning on line 21 in src/app/components/Tokens/index.tsx

View workflow job for this annotation

GitHub Actions / lint

'isLaptop' is assigned a value but never used
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}
/>
)
}
122 changes: 122 additions & 0 deletions src/app/pages/TokenkDetailPage/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useScreenSize } from '../../hooks/useScreensize'
import { EvmToken } from '../../../oasis-indexer/api'
import { TextSkeleton } from '../../components/Skeleton'

// export const TokenDetailPage: FC = () => {
// const { t } = useTranslation()
// const scope = useRequiredScopeParam()
// if (scope.layer === Layer.consensus) {
// throw AppErrors.UnsupportedLayer
// // We should use useGetConsensusBlocksHeight()
// }
// const blockHeight = parseInt(useParams().blockHeight!, 10)
// const { isLoading, data } = useGetRuntimeBlockByHeight(
// scope.network,
// scope.layer, // This is OK, since consensus is already handled separately
// blockHeight,
// )
// if (!data && !isLoading) {
// throw AppErrors.NotFoundBlockHeight
// }
// const block = data?.data
//
// return (
// <PageLayout>
// <SubPageCard featured title={t('common.block')}>
// <BlockDetailView isLoading={isLoading} block={block} />
// </SubPageCard>
// {!!block?.num_transactions && <TransactionsCard scope={scope} blockHeight={blockHeight} />}
// </PageLayout>
// )
// }

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

Check warning on line 41 in src/app/pages/TokenkDetailPage/index.tsx

View workflow job for this annotation

GitHub Actions / lint

't' is assigned a value but never used
const { isMobile } = useScreenSize()

Check warning on line 42 in src/app/pages/TokenkDetailPage/index.tsx

View workflow job for this annotation

GitHub Actions / lint

'isMobile' is assigned a value but never used

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

return <div>Token details</div>
// const transactionsAnchor = `${RouteUtils.getBlockRoute(block, block.round)}#${transactionsContainerId}`
// const transactionLabel = block.num_transactions.toLocaleString()
// const blockGasLimit = paraTimesConfig[block.layer]?.mainnet.blockGasLimit
// if (!blockGasLimit) throw new Error('blockGasLimit is not configured')
// return (
// <StyledDescriptionList
// titleWidth={isMobile ? '100px' : '200px'}
// standalone={standalone}
// highlight={block.markAsNew}
// >
// {showLayer && (
// <>
// <dt>{t('common.paratime')}</dt>
// <dd>
// <DashboardLink scope={block} />
// </dd>
// </>
// )}
// <dt>{t('common.height')}</dt>
// <dd>
// <BlockLink scope={block} height={block.round} />
// <CopyToClipboard value={block.round.toString()} />
// </dd>
//
// <dt>{t('common.hash')}</dt>
// <dd>
// <BlockHashLink scope={block} hash={block.hash} height={block.round} />
// <CopyToClipboard value={block.hash.toString()} />
// </dd>
//
// <dt>{t('common.timestamp')}</dt>
// <dd>{formattedTime}</dd>
//
// <dt>{t('common.size')}</dt>
// <dd>
// {t('common.bytes', {
// value: block.size,
// formatParams: {
// value: {
// style: 'unit',
// unit: 'byte',
// unitDisplay: 'long',
// } satisfies Intl.NumberFormatOptions,
// },
// })}
// </dd>
//
// <dt>{t('common.transactions')}</dt>
// <dd>
// {block.num_transactions ? (
// <Link href={transactionsAnchor}>{transactionLabel}</Link>
// ) : (
// transactionLabel
// )}
// </dd>
//
// <dt>{t('common.gasUsed')}</dt>
// <dd>
// {t('block.gasUsed', {
// value: block.gas_used,
// percentage: block.gas_used / blockGasLimit,
// formatParams: {
// percentage: {
// style: 'percent',
// maximumFractionDigits: 2,
// } satisfies Intl.NumberFormatOptions,
// },
// })}
// </dd>
//
// <dt>{t('common.gasLimit')}</dt>
// <dd>{blockGasLimit.toLocaleString()}</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>
)
}
6 changes: 6 additions & 0 deletions src/app/utils/route-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,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
6 changes: 6 additions & 0 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,12 @@
"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 5e723c7

Please sign in to comment.