diff --git a/.changelog/1069.feature.md b/.changelog/1069.feature.md new file mode 100644 index 000000000..40c1421c8 --- /dev/null +++ b/.changelog/1069.feature.md @@ -0,0 +1 @@ +Add NFT feature diff --git a/src/app/components/Account/ContractCreatorInfo.tsx b/src/app/components/Account/ContractCreatorInfo.tsx index 5fd065436..a944c8362 100644 --- a/src/app/components/Account/ContractCreatorInfo.tsx +++ b/src/app/components/Account/ContractCreatorInfo.tsx @@ -1,6 +1,6 @@ import { FC } from 'react' import { SearchScope } from '../../../types/searchScope' -import { Trans, useTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import { TransactionLink } from '../Transactions/TransactionLink' import { Layer, @@ -12,6 +12,7 @@ import { AppErrors } from '../../../types/errors' import { AccountLink } from './AccountLink' import Box from '@mui/material/Box' import Skeleton from '@mui/material/Skeleton' +import { useScreenSize } from '../../hooks/useScreensize' const TxSender: FC<{ scope: SearchScope; txHash: string }> = ({ scope, txHash }) => { const { t } = useTranslation() @@ -42,6 +43,7 @@ export const ContractCreatorInfo: FC<{ creationTxHash: string | undefined }> = ({ scope, isLoading, creationTxHash }) => { const { t } = useTranslation() + const { isMobile } = useScreenSize() return isLoading ? ( @@ -51,20 +53,15 @@ export const ContractCreatorInfo: FC<{ -   - , - }} - /> + {t('contract.createdAt')} + ) } diff --git a/src/app/components/AppendMobileSearch/index.tsx b/src/app/components/AppendMobileSearch/index.tsx index 2ea0aab38..0211fc628 100644 --- a/src/app/components/AppendMobileSearch/index.tsx +++ b/src/app/components/AppendMobileSearch/index.tsx @@ -9,12 +9,16 @@ interface AppendMobileSearchProps { action?: ReactNode } +interface AppendMobileSearchLayoutProps { + action?: ReactNode + isMobile: boolean +} + const Layout = styled(Box, { - shouldForwardProp: (prop: PropertyKey) => - !(['action'] as (keyof AppendMobileSearchProps)[]).includes(prop as keyof AppendMobileSearchProps), -})(({ action }) => ({ + shouldForwardProp: prop => prop !== 'action' && prop !== 'isMobile', +})(({ action, isMobile }) => ({ position: 'relative', - alignItems: 'flex-start', + alignItems: isMobile ? 'center' : 'flex-start', width: '100%', ...(action ? { @@ -41,7 +45,7 @@ export const AppendMobileSearch: FC & const { isMobile } = useScreenSize() return ( - + {children} {action} diff --git a/src/app/components/CardHeaderWithCounter/index.tsx b/src/app/components/CardHeaderWithCounter/index.tsx new file mode 100644 index 000000000..58b93f800 --- /dev/null +++ b/src/app/components/CardHeaderWithCounter/index.tsx @@ -0,0 +1,29 @@ +import { FC } from 'react' +import Box from '@mui/material/Box' +import Typography from '@mui/material/Typography' +import { COLORS } from 'styles/theme/colors' + +type CardHeaderWithCounterProps = { + label: string | undefined + totalCount: number | undefined + isTotalCountClipped: boolean | undefined +} + +export const CardHeaderWithCounter: FC = ({ + label, + totalCount, + isTotalCountClipped, +}) => { + return ( + + + {label} + + {!!totalCount && ( + + ({`${isTotalCountClipped ? ' > ' : ''}${totalCount}`}) + + )} + + ) +} diff --git a/src/app/components/NoPreview/index.tsx b/src/app/components/NoPreview/index.tsx index 80f6e6127..9459efcc2 100644 --- a/src/app/components/NoPreview/index.tsx +++ b/src/app/components/NoPreview/index.tsx @@ -18,6 +18,7 @@ export const NoPreview: FC = ({ placeholderSize }) => { height: placeholderSize, width: placeholderSize, display: 'flex', + flex: 1, justifyContent: 'center', flexDirection: 'column', alignItems: 'center', diff --git a/src/app/components/ProgressBar/index.ts b/src/app/components/ProgressBar/index.ts index d2ee8b69c..325237cc6 100644 --- a/src/app/components/ProgressBar/index.ts +++ b/src/app/components/ProgressBar/index.ts @@ -1,8 +1,22 @@ -import LinearProgress from '@mui/material/LinearProgress' +import MuiLinearProgress, { linearProgressClasses } from '@mui/material/LinearProgress' import { styled } from '@mui/material/styles' +import { COLORS } from 'styles/theme/colors' -export const VerticalProgressBar = styled(LinearProgress)(() => ({ +export const VerticalProgressBar = styled(MuiLinearProgress)(() => ({ + [`&.${linearProgressClasses.determinate} > .${linearProgressClasses.bar1Determinate}`]: { + backgroundColor: COLORS.brandDark, + }, height: 36, width: 36, transform: 'rotate(-90deg)', })) + +export const ProgressBar = styled(MuiLinearProgress)(() => ({ + [`&.${linearProgressClasses.determinate} > .${linearProgressClasses.bar1Determinate}`]: { + backgroundColor: COLORS.eucalyptus, + }, + borderColor: COLORS.eucalyptus, + backgroundColor: COLORS.honeydew, + height: '12px', + width: '85px', +})) diff --git a/src/app/components/Tokens/TokenDetails.tsx b/src/app/components/Tokens/TokenDetails.tsx index 400c86107..0ba7e6c27 100644 --- a/src/app/components/Tokens/TokenDetails.tsx +++ b/src/app/components/Tokens/TokenDetails.tsx @@ -12,7 +12,7 @@ import { VerificationIcon } from '../ContractVerificationIcon' import Box from '@mui/material/Box' import { COLORS } from '../../../styles/theme/colors' import { TokenTypeTag } from './TokenList' -import { getPreciseNumberFormat } from '../../../locales/getPreciseNumberFormat' +import { RoundedBalance } from '../RoundedBalance' export const TokenDetails: FC<{ isLoading?: boolean @@ -39,7 +39,9 @@ export const TokenDetails: FC<{
{t('common.name')}
- ({token.symbol}) + + ({token.symbol}) +
{t('common.type')}
@@ -64,12 +66,11 @@ export const TokenDetails: FC<{
{t('tokens.totalSupply')}
- {token.total_supply - ? t('tokens.totalSupplyValue', { - ...getPreciseNumberFormat(token.total_supply), - ticker: token.symbol, - }) - : t('common.missing')} + {token.total_supply ? ( + + ) : ( + t('common.missing') + )}
) diff --git a/src/app/components/Tokens/TokenHolders.tsx b/src/app/components/Tokens/TokenHolders.tsx index da83e66c7..77daeae62 100644 --- a/src/app/components/Tokens/TokenHolders.tsx +++ b/src/app/components/Tokens/TokenHolders.tsx @@ -1,11 +1,13 @@ import { FC } from 'react' import { useTranslation } from 'react-i18next' +import Box from '@mui/material/Box' import { Table, TableCellAlign, TableColProps } from '../Table' import { BareTokenHolder } from '../../../oasis-nexus/api' import { TablePaginationProps } from '../Table/TablePagination' import { AccountLink } from '../Account/AccountLink' import { fromBaseUnits } from '../../utils/helpers' import { RoundedBalance } from '../RoundedBalance' +import { ProgressBar } from '../ProgressBar' type TableTokenHolder = BareTokenHolder & { markAsNew?: boolean @@ -30,16 +32,14 @@ export const TokenHolders: FC = ({ }) => { const { t } = useTranslation() const tableColumns: TableColProps[] = [ - { key: 'rank', content: t('common.rank'), align: TableCellAlign.Center }, + { key: 'rank', content: t('common.rank') }, { key: 'address', content: t('common.address') }, { key: 'quantity', content: t('common.quantity'), align: TableCellAlign.Right }, { key: 'percentage', content: t('common.percentage'), align: TableCellAlign.Right }, ] - const calculateRatio = (balance: string): string => { - return totalSupply === undefined - ? t('common.missing') - : `${((100 * parseFloat(fromBaseUnits(balance, decimals))) / parseFloat(totalSupply)).toFixed(4)}%` + const calculateRatio = (balance: string, totalSupply: string, decimals: number): number => { + return (100 * parseFloat(fromBaseUnits(balance, decimals))) / parseFloat(totalSupply) } const tableRows = holders?.map((holder, index) => { @@ -49,7 +49,6 @@ export const TokenHolders: FC = ({ { key: 'rank', content: holder.rank.toLocaleString(), - align: TableCellAlign.Center, }, { key: 'address', @@ -64,7 +63,21 @@ export const TokenHolders: FC = ({ }, { key: 'percentage', - content: calculateRatio(holder.balance), + content: ( + <> + {totalSupply ? ( + + {`${calculateRatio(holder.balance, totalSupply, decimals).toFixed(4)}%`} + + + ) : ( + {t('common.missing')} + )} + + ), align: TableCellAlign.Right, }, ], diff --git a/src/app/components/Tokens/TokenList.tsx b/src/app/components/Tokens/TokenList.tsx index 813d41ed0..9519e119d 100644 --- a/src/app/components/Tokens/TokenList.tsx +++ b/src/app/components/Tokens/TokenList.tsx @@ -67,7 +67,7 @@ export const TokenList = (props: TokensProps) => { { key: 'verification', content: t('contract.verification.title') }, { key: 'holders', - content: t('tokens.holdersCount'), + content: t('tokens.holdersCount_short'), align: TableCellAlign.Right, }, { key: 'supply', content: t('tokens.totalSupply'), align: TableCellAlign.Right }, @@ -98,7 +98,11 @@ export const TokenList = (props: TokensProps) => { }, { key: 'type', - content: , + content: ( + + + + ), }, { content: ( diff --git a/src/app/components/Tokens/TokenTransfers.tsx b/src/app/components/Tokens/TokenTransfers.tsx index 702a5d20b..79c3bb232 100644 --- a/src/app/components/Tokens/TokenTransfers.tsx +++ b/src/app/components/Tokens/TokenTransfers.tsx @@ -20,6 +20,7 @@ import { TokenLink } from './TokenLink' import { PlaceholderLabel } from '../../utils/PlaceholderLabel' import { TokenTypeTag } from './TokenList' import { parseEvmEvent } from '../../utils/parseEvmEvent' +import { formatDistanceToNow } from '../../utils/dateFormatter' const iconSize = '28px' const StyledCircle = styled(Box)(({ theme }) => ({ @@ -119,6 +120,7 @@ export const TokenTransfers: FC = ({ const tableColumns: TableColProps[] = [ { key: 'hash', content: t('common.hash') }, { key: 'block', content: t('common.block') }, + { key: 'timestamp', content: t('common.age'), align: TableCellAlign.Right }, { key: 'type', content: t('common.type'), align: TableCellAlign.Center }, { key: 'from', content: t('common.from'), width: '150px' }, { key: 'to', content: t('common.to'), width: '150px' }, @@ -147,6 +149,11 @@ export const TokenTransfers: FC = ({ content: , key: 'round', }, + { + align: TableCellAlign.Right, + content: formatDistanceToNow(new Date(transfer.timestamp)), + key: 'timestamp', + }, { key: 'type', align: TableCellAlign.Center, diff --git a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx index 711a60d6d..449792103 100644 --- a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx +++ b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx @@ -18,7 +18,6 @@ import { AccountDetailsContext } from './index' import { AccountLink } from 'app/components/Account/AccountLink' import { CopyToClipboard } from 'app/components/CopyToClipboard' import { RouteUtils } from 'app/utils/route-utils' -import { COLORS } from 'styles/theme/colors' import { ImageListItemImage } from '../TokenDashboardPage/ImageListItemImage' import { CardEmptyState } from '../AccountDetailsPage/CardEmptyState' import { TablePagination } from '../../components/Table/TablePagination' @@ -26,6 +25,7 @@ import { useAccountTokenInventory } from '../TokenDashboardPage/hook' import { EvmNft } from 'oasis-nexus/api' import { SearchScope } from '../../../types/searchScope' import { NFTCollectionLink, NFTInstanceLink } from '../TokenDashboardPage/NFTLinks' +import { CardHeaderWithCounter } from 'app/components/CardHeaderWithCounter' export const accountNFTCollectionContainerId = 'nftCollection' @@ -63,14 +63,11 @@ export const AccountNFTCollectionCard: FC = ({ scope, add {isFetched && ( - - - {firstToken?.name ? inventory?.[0].token.name : t('common.collection')} - - {!!totalCount && ( - ({`${isTotalCountClipped ? ' > ' : ''}${totalCount}`}) - )} - + )} {isLoading && } diff --git a/src/app/pages/NFTInstanceDashboardPage/InstanceDetailsCard.tsx b/src/app/pages/NFTInstanceDashboardPage/InstanceDetailsCard.tsx index c583ab371..5e25bfd94 100644 --- a/src/app/pages/NFTInstanceDashboardPage/InstanceDetailsCard.tsx +++ b/src/app/pages/NFTInstanceDashboardPage/InstanceDetailsCard.tsx @@ -72,10 +72,10 @@ export const InstanceDetailsCard: FC = ({ )} - {nft?.token?.num_transfers && ( + {nft?.num_transfers && ( <>
{t('nft.transfers')}
-
{nft.token.num_transfers!.toLocaleString()}
+
{nft.num_transfers.toLocaleString()}
)}
{t(isMobile ? 'common.smartContract_short' : 'common.smartContract')}
diff --git a/src/app/pages/NFTInstanceDashboardPage/NFTMetadataCard.tsx b/src/app/pages/NFTInstanceDashboardPage/NFTMetadataCard.tsx index 1c65e3c9f..5303130da 100644 --- a/src/app/pages/NFTInstanceDashboardPage/NFTMetadataCard.tsx +++ b/src/app/pages/NFTInstanceDashboardPage/NFTMetadataCard.tsx @@ -2,23 +2,18 @@ import { FC } from 'react' import { useTranslation } from 'react-i18next' import Card from '@mui/material/Card' import CardContent from '@mui/material/CardContent' -import { Runtime, useGetRuntimeEvmTokensAddressNftsId } from '../../../oasis-nexus/api' import { LinkableDiv } from '../../components/PageLayout/LinkableDiv' import { CardEmptyState } from './../AccountDetailsPage/CardEmptyState' import { NftDashboardContext } from '../NFTInstanceDashboardPage' import { JsonCodeDisplay } from '../../components/CodeDisplay' +import { useNFTInstance } from '../TokenDashboardPage/hook' export const nftMetadataId = 'metadata' export const NFTMetadataCard: FC = ({ scope, address, instanceId }) => { const { t } = useTranslation() - const { data, isFetched } = useGetRuntimeEvmTokensAddressNftsId( - scope.network, - scope.layer as Runtime, - address, - instanceId, - ) - const metadata = data?.data?.metadata + const { isFetched, nft } = useNFTInstance(scope, address, instanceId) + const metadata = nft?.metadata return ( diff --git a/src/app/pages/NFTInstanceDashboardPage/index.tsx b/src/app/pages/NFTInstanceDashboardPage/index.tsx index 8fe0056a1..ac55d7e59 100644 --- a/src/app/pages/NFTInstanceDashboardPage/index.tsx +++ b/src/app/pages/NFTInstanceDashboardPage/index.tsx @@ -6,10 +6,10 @@ import { PageLayout } from '../../components/PageLayout' import { InstanceTitleCard } from './InstanceTitleCard' import { InstanceDetailsCard } from './InstanceDetailsCard' import { InstanceImageCard } from './InstanceImageCard' -import { Layer, Runtime, useGetRuntimeEvmTokensAddressNftsId } from '../../../oasis-nexus/api' import { AppErrors } from '../../../types/errors' import { RouterTabs } from 'app/components/RouterTabs' import { SearchScope } from '../../../types/searchScope' +import { useNFTInstance } from '../TokenDashboardPage/hook' export type NftDashboardContext = { scope: SearchScope @@ -23,22 +23,11 @@ export const NFTInstanceDashboardPage: FC = () => { const { t } = useTranslation() const scope = useRequiredScopeParam() const { address, instanceId } = useParams() - if (scope.layer === Layer.consensus) { - // There can be no ERC-20 or ERC-721 tokens on consensus - throw AppErrors.UnsupportedLayer - } - if (!address || !instanceId) { throw AppErrors.InvalidUrl } + const { isFetched, isLoading, nft } = useNFTInstance(scope, address, instanceId) - const { data, isFetched, isLoading } = useGetRuntimeEvmTokensAddressNftsId( - scope.network, - scope.layer as Runtime, - address, - instanceId, - ) - const nft = data?.data const metadataLink = useHref('') const context: NftDashboardContext = { scope, diff --git a/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx b/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx index 89cada5f7..3fb266cbb 100644 --- a/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx +++ b/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx @@ -1,19 +1,53 @@ import { FC } from 'react' +import { useTranslation } from 'react-i18next' import { Link as RouterLink } from 'react-router-dom' +import Box from '@mui/material/Box' import Link from '@mui/material/Link' +import OpenInBrowserIcon from '@mui/icons-material/OpenInBrowser' import { styled } from '@mui/material/styles' import { isNftImageUrlValid, processNftImageUrl } from 'app/utils/nft-images' import { NoPreview } from '../../components/NoPreview' import { getNftInstanceLabel } from '../../utils/nft' import { EvmNft } from '../../../oasis-nexus/api' +import { useScreenSize } from 'app/hooks/useScreensize' +import { COLORS } from 'styles/theme/colors' -const imageSize = '210px' +const minMobileSize = '150px' +const minSize = '210px' -const StyledImage = styled('img')({ - width: imageSize, - height: imageSize, +const StyledImage = styled('img', { + shouldForwardProp: prop => prop !== 'isMobile', +})<{ isMobile: boolean }>(({ isMobile }) => ({ + minWidth: isMobile ? minMobileSize : minSize, + minHeight: isMobile ? minMobileSize : minSize, + width: '100%', + height: '100%', + maxHeight: minSize, objectFit: 'cover', -}) + transition: 'opacity 250ms ease-in-out', + '&:hover, &:focus-visible': { + opacity: 0.15, + }, +})) + +const StyledBox = styled(Box)(() => ({ + position: 'absolute', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '100%', + height: '100%', + fontSize: '14px', + fontWeight: 500, + color: COLORS.white, + transition: 'opacity, background-color 250ms ease-in-out', + opacity: 0, + '&:hover, &:focus-visible': { + opacity: 1, + backgroundColor: 'rgba(0,0,0,0.8)', + }, +})) type ImageListItemImageProps = { instance: EvmNft @@ -21,17 +55,25 @@ type ImageListItemImageProps = { } export const ImageListItemImage: FC = ({ instance, to }) => { + const { t } = useTranslation() + const { isMobile } = useScreenSize() + return ( - + {isNftImageUrlValid(instance.image) ? ( ) : ( - + )} + + + {t('common.view')} + ) } diff --git a/src/app/pages/TokenDashboardPage/NFTLinks.tsx b/src/app/pages/TokenDashboardPage/NFTLinks.tsx index cde013101..6b867828f 100644 --- a/src/app/pages/TokenDashboardPage/NFTLinks.tsx +++ b/src/app/pages/TokenDashboardPage/NFTLinks.tsx @@ -4,6 +4,7 @@ import { Link as RouterLink } from 'react-router-dom' import { EvmNft } from 'oasis-nexus/api' import Link from '@mui/material/Link' import Typography from '@mui/material/Typography' +import { styled } from '@mui/material/styles' import { RouteUtils } from '../../utils/route-utils' import { SearchScope } from '../../../types/searchScope' import { trimLongString } from '../../utils/trimLongString' @@ -13,12 +14,18 @@ type NFTLinkProps = { instance: EvmNft } +const StyledTypography = styled(Typography)({ + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', +}) + export const NFTCollectionLink: FC = ({ scope, instance }) => { const { t } = useTranslation() const to = RouteUtils.getTokenRoute(scope, instance.token?.contract_addr) return ( - + = ({ scope, instance }) => { ), }} /> - + ) } export const NFTInstanceLink: FC = ({ scope, instance }) => { const { t } = useTranslation() - const to = RouteUtils.getNFTInstanceRoute(scope, instance.token?.contract_addr, instance.id) + const to = RouteUtils.getNFTInstanceRoute(scope, instance.token?.eth_contract_addr, instance.id) return ( - + = ({ scope, instance }) => { ), }} /> - + ) } diff --git a/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx b/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx index 2c652b9c5..7b3572b4d 100644 --- a/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx +++ b/src/app/pages/TokenDashboardPage/TokenDetailsCard.tsx @@ -19,6 +19,8 @@ import { SearchScope } from '../../../types/searchScope' import { getPreciseNumberFormat } from '../../../locales/getPreciseNumberFormat' import { RouteUtils } from '../../utils/route-utils' import { tokenTransfersContainerId } from '../../pages/TokenDashboardPage/TokenTransfersCard' +import { tokenHoldersContainerId } from '../../pages/TokenDashboardPage/TokenHoldersCard' +import { RoundedBalance } from 'app/components/RoundedBalance' export const TokenDetailsCard: FC<{ scope: SearchScope; address: string }> = ({ scope, address }) => { const { t } = useTranslation() @@ -75,6 +77,27 @@ export const TokenDetailsCard: FC<{ scope: SearchScope; address: string }> = ({ : t('common.valueInToken', { ...getPreciseNumberFormat(balance), ticker: tickerName })} +
{t('tokens.totalSupply')}
+
+ {token.total_supply ? ( + + ) : ( + t('common.not_defined') + )} +
+
{t('tokens.holders')}
+
+ + {t('tokens.holdersValue', { value: token?.num_holders })} + +
+ {token.num_transfers && ( <>
{t('tokens.transfers')}
diff --git a/src/app/pages/TokenDashboardPage/TokenInventoryCard.tsx b/src/app/pages/TokenDashboardPage/TokenInventoryCard.tsx index f5303a66f..a5adfff07 100644 --- a/src/app/pages/TokenDashboardPage/TokenInventoryCard.tsx +++ b/src/app/pages/TokenDashboardPage/TokenInventoryCard.tsx @@ -17,29 +17,68 @@ import { TablePagination } from '../../components/Table/TablePagination' import { useTokenInventory } from './hook' import { ImageListItemImage } from './ImageListItemImage' import { NFTInstanceLink } from './NFTLinks' +import { CardHeaderWithCounter } from 'app/components/CardHeaderWithCounter' +import { EvmNft } from 'oasis-nexus/api' +import { To } from 'react-router-dom' +import { SearchScope } from 'types/searchScope' export const tokenInventoryContainerId = 'inventory' export const TokenInventoryCard: FC = ({ scope, address }) => { const { t } = useTranslation() + const { inventory, isFetched, pagination, totalCount } = useTokenInventory(scope, address) return ( - + + } + /> - + ) } -const TokenInventoryView: FC = ({ scope, address }) => { +type TokenInventoryViewProps = { + inventory: EvmNft[] | undefined + isFetched: boolean + totalCount: number | undefined + pagination: { + isTotalCountClipped: boolean | undefined + rowsPerPage: number + selectedPage: number + linkToPage: (pageNumber: number) => To + } + scope: SearchScope +} + +const TokenInventoryView: FC = ({ + inventory, + isFetched, + pagination, + scope, + totalCount, +}) => { const { t } = useTranslation() - const { inventory, isFetched, pagination, totalCount } = useTokenInventory(scope, address) return ( <> diff --git a/src/app/pages/TokenDashboardPage/TokenSnapshot.tsx b/src/app/pages/TokenDashboardPage/TokenSnapshot.tsx index bd471d3c1..347657cc7 100644 --- a/src/app/pages/TokenDashboardPage/TokenSnapshot.tsx +++ b/src/app/pages/TokenDashboardPage/TokenSnapshot.tsx @@ -16,7 +16,6 @@ import { SearchScope } from '../../../types/searchScope' const StyledGrid = styled(Grid)(() => ({ display: 'flex', })) - export const TokenSnapshot: FC<{ scope: SearchScope; address: string }> = ({ scope, address }) => { const { t } = useTranslation() @@ -29,10 +28,7 @@ export const TokenSnapshot: FC<{ scope: SearchScope; address: string }> = ({ sco - + {t('tokenSnapshot.header')} diff --git a/src/app/pages/TokenDashboardPage/TokenTitleCard.tsx b/src/app/pages/TokenDashboardPage/TokenTitleCard.tsx index 368188a1e..06d30e6ab 100644 --- a/src/app/pages/TokenDashboardPage/TokenTitleCard.tsx +++ b/src/app/pages/TokenDashboardPage/TokenTitleCard.tsx @@ -8,19 +8,26 @@ import Skeleton from '@mui/material/Skeleton' import { VerificationIcon } from '../../components/ContractVerificationIcon' import { AccountLink } from '../../components/Account/AccountLink' import Box from '@mui/material/Box' +import { styled } from '@mui/material/styles' import { CopyToClipboard } from '../../components/CopyToClipboard' import { useTranslation } from 'react-i18next' import { SearchScope } from '../../../types/searchScope' +export const StyledCard = styled(Card)(() => ({ + '&': { + paddingTop: '24px', // custom spacing + marginBottom: '50px', + }, +})) + const TitleSkeleton: FC = () => export const TokenTitleCard: FC<{ scope: SearchScope; address: string }> = ({ scope, address }) => { const { t } = useTranslation() - const { isLoading, token } = useTokenInfo(scope, address) return ( - + {isLoading ? ( @@ -32,32 +39,29 @@ export const TokenTitleCard: FC<{ scope: SearchScope; address: string }> = ({ sc justifyContent: 'space-between', alignItems: 'center', }} + gap={3} > - - - {token?.name ?? t('common.missing')} - + {token?.name ?? t('common.missing')}   - {token?.symbol ?? t('common.missing')} + ({token?.symbol ?? t('common.missing')}) - + {token && ( = ({ sc )} - + ) } diff --git a/src/app/pages/TokenDashboardPage/hook.ts b/src/app/pages/TokenDashboardPage/hook.ts index 4e2560b53..16ee0a3c2 100644 --- a/src/app/pages/TokenDashboardPage/hook.ts +++ b/src/app/pages/TokenDashboardPage/hook.ts @@ -7,6 +7,7 @@ import { useGetRuntimeEvmTokensAddressHolders, useGetRuntimeEvmTokensAddressNfts, useGetRuntimeAccountsAddressNfts, + useGetRuntimeEvmTokensAddressNftsId, } from '../../../oasis-nexus/api' import { AppErrors } from '../../../types/errors' import { SearchScope } from '../../../types/searchScope' @@ -222,3 +223,29 @@ export const useAccountTokenInventory = (scope: SearchScope, address: string, to totalCount, } } + +export const useNFTInstance = (scope: SearchScope, address: string, id: string) => { + const { network, layer } = scope + if (layer === Layer.consensus) { + throw AppErrors.UnsupportedLayer + // There are no tokens on the consensus layer. + } + const oasisAddress = useTransformToOasisAddress(address) + const query = useGetRuntimeEvmTokensAddressNftsId(network, layer, oasisAddress!, id, { + query: { + enabled: !!oasisAddress, + }, + }) + + const { data, isError, isFetched, isLoading } = query + if (isError) { + throw AppErrors.InvalidAddress + } + const nft = data?.data + + return { + isLoading, + isFetched, + nft, + } +} diff --git a/src/app/pages/TokenDashboardPage/index.tsx b/src/app/pages/TokenDashboardPage/index.tsx index 8f043e08c..807789133 100644 --- a/src/app/pages/TokenDashboardPage/index.tsx +++ b/src/app/pages/TokenDashboardPage/index.tsx @@ -49,7 +49,7 @@ export const TokenDashboardPage: FC = () => { - {!isMobile && } + { tableView === TableLayout.Vertical && } > - {!isMobile && } + {!isMobile && } } diff --git a/src/app/utils/route-utils.ts b/src/app/utils/route-utils.ts index 2f7b8cc64..dd38bcf08 100644 --- a/src/app/utils/route-utils.ts +++ b/src/app/utils/route-utils.ts @@ -85,6 +85,12 @@ export abstract class RouteUtils { )}` } + static getTokenHoldersRoute = ({ network, layer }: SearchScope, tokenAddress: string) => { + return `/${encodeURIComponent(network)}/${encodeURIComponent(layer)}/token/${encodeURIComponent( + tokenAddress, + )}/holders` + } + static getNFTInstanceRoute = ( { network, layer }: SearchScope, contractAddress: string, diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 605d20867..01542ea1a 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -111,7 +111,7 @@ "copyButton": "Copy {{subject}}", "creationByteCode": "Creation ByteCode", "creator": "Creator", - "createdAt": "at ", + "createdAt": "at", "noCode": "There is no bytecode on record for this account. (Are you sure this is a contract?)", "title": "Contract", "runtimeByteCode": "Runtime ByteCode", diff --git a/src/oasis-nexus/generated/api.ts b/src/oasis-nexus/generated/api.ts index 5ce53efd9..dd32cb6df 100644 --- a/src/oasis-nexus/generated/api.ts +++ b/src/oasis-nexus/generated/api.ts @@ -659,6 +659,8 @@ Currently only ERC-721 is supported, where the document is an Asset Metadata fro metadata_uri?: string; /** Identifies the asset which this NFT represents */ name?: string; + /** The total number of transfers of this NFT instance. */ + num_transfers?: number; /** The Oasis address of this NFT instance's owner. */ owner?: string; /** The Ethereum address of this NFT instance's owner. */ diff --git a/src/styles/theme/defaultTheme.ts b/src/styles/theme/defaultTheme.ts index a5eff0156..496250307 100644 --- a/src/styles/theme/defaultTheme.ts +++ b/src/styles/theme/defaultTheme.ts @@ -530,11 +530,13 @@ export const defaultTheme = createTheme({ }, MuiImageList: { styleOverrides: { - root: { - // default gridTemplateColumns is set by cols prop default number via inline styles - // and cannot be overridden without !important statement - gridTemplateColumns: `repeat(auto-fill, minmax(210px, 210px))!important`, - }, + root: ({ theme }) => ({ + [theme.breakpoints.up('sm')]: { + // default gridTemplateColumns is set by cols prop default number via inline styles + // and cannot be overridden without !important statement + gridTemplateColumns: `repeat(auto-fill, minmax(210px, auto))!important`, + }, + }), }, }, MuiImageListItem: { @@ -545,9 +547,10 @@ export const defaultTheme = createTheme({ borderColor: COLORS.brandExtraDark, borderRadius: 8, overflow: 'hidden', - transition: 'box-shadow 250ms ease-in-out', + transition: 'border-color, box-shadow 250ms ease-in-out', '&:hover, &:focus-visible': { boxShadow: '0px 8px 8px 0px rgba(0, 0, 0, 0.15)', + borderColor: COLORS.brandDark, }, }, }, @@ -597,7 +600,6 @@ export const defaultTheme = createTheme({ borderRadius: 5, }, bar: { - backgroundColor: COLORS.brandDark, borderRadius: 5, }, }, @@ -607,7 +609,9 @@ export const defaultTheme = createTheme({ root: ({ theme }) => ({ [theme.breakpoints.down('sm')]: { paddingRight: theme.spacing(4), + // force scrollbar to cover the whole table horizontally marginLeft: `-${theme.spacing(4)}`, + width: `calc(100% + ${theme.spacing(4)})`, }, }), },