From fa74ad8bacea6b676acb9197140c534da1749ba2 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Wed, 6 Dec 2023 10:09:48 +0100 Subject: [PATCH 01/15] Follow token pattern and throw invalid url error --- .changelog/1069.feature.md | 1 + src/app/pages/NFTInstanceDashboardPage/index.tsx | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .changelog/1069.feature.md 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/pages/NFTInstanceDashboardPage/index.tsx b/src/app/pages/NFTInstanceDashboardPage/index.tsx index 8fe0056a1..7ab710260 100644 --- a/src/app/pages/NFTInstanceDashboardPage/index.tsx +++ b/src/app/pages/NFTInstanceDashboardPage/index.tsx @@ -32,12 +32,15 @@ export const NFTInstanceDashboardPage: FC = () => { throw AppErrors.InvalidUrl } - const { data, isFetched, isLoading } = useGetRuntimeEvmTokensAddressNftsId( + const { data, isError, isFetched, isLoading } = useGetRuntimeEvmTokensAddressNftsId( scope.network, scope.layer as Runtime, address, instanceId, ) + if (isError) { + throw AppErrors.InvalidAddress + } const nft = data?.data const metadataLink = useHref('') const context: NftDashboardContext = { From a6266e93b7f4a9d900bc5cd4d48b0682b0a99df4 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Wed, 6 Dec 2023 10:32:04 +0100 Subject: [PATCH 02/15] Keep eth address in URL while navigating to instance details page --- .../NFTMetadataCard.tsx | 11 +++----- .../pages/NFTInstanceDashboardPage/index.tsx | 18 ++----------- src/app/pages/TokenDashboardPage/NFTLinks.tsx | 2 +- src/app/pages/TokenDashboardPage/hook.ts | 27 +++++++++++++++++++ 4 files changed, 33 insertions(+), 25 deletions(-) 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 7ab710260..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,25 +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, isError, isFetched, isLoading } = useGetRuntimeEvmTokensAddressNftsId( - scope.network, - scope.layer as Runtime, - address, - instanceId, - ) - if (isError) { - throw AppErrors.InvalidAddress - } - const nft = data?.data const metadataLink = useHref('') const context: NftDashboardContext = { scope, diff --git a/src/app/pages/TokenDashboardPage/NFTLinks.tsx b/src/app/pages/TokenDashboardPage/NFTLinks.tsx index cde013101..3e9d05d4f 100644 --- a/src/app/pages/TokenDashboardPage/NFTLinks.tsx +++ b/src/app/pages/TokenDashboardPage/NFTLinks.tsx @@ -36,7 +36,7 @@ export const NFTCollectionLink: FC = ({ 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 ( 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, + } +} From 7b93eb3b652bc0aee0235957dfd9226776aa5b5e Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Tue, 12 Dec 2023 09:25:28 +0100 Subject: [PATCH 03/15] Show correct num_transfers in instance detail page --- .../pages/NFTInstanceDashboardPage/InstanceDetailsCard.tsx | 4 ++-- src/oasis-nexus/generated/api.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) 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/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. */ From 3f63939cfecd9ba99ed64b69340b238ef2d433fd Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Thu, 7 Dec 2023 14:47:27 +0100 Subject: [PATCH 04/15] Improve token dashboard mobile layout --- .../Account/ContractCreatorInfo.tsx | 24 +++++++++++-------- .../components/AppendMobileSearch/index.tsx | 14 +++++++---- .../TokenDashboardPage/TokenSnapshot.tsx | 6 +---- src/app/pages/TokenDashboardPage/index.tsx | 2 +- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/app/components/Account/ContractCreatorInfo.tsx b/src/app/components/Account/ContractCreatorInfo.tsx index 5fd065436..4784a8754 100644 --- a/src/app/components/Account/ContractCreatorInfo.tsx +++ b/src/app/components/Account/ContractCreatorInfo.tsx @@ -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,22 @@ export const ContractCreatorInfo: FC<{ -   - , - }} - /> + + , + }} + /> + ) } 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/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/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 && } + Date: Thu, 7 Dec 2023 15:00:44 +0100 Subject: [PATCH 05/15] Add counter to token inventory tab --- .../CardHeaderWithCounter/index.tsx | 29 ++++++++++++ .../AccountNFTCollectionCard.tsx | 15 +++--- .../TokenDashboardPage/TokenInventoryCard.tsx | 47 +++++++++++++++++-- 3 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 src/app/components/CardHeaderWithCounter/index.tsx 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/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/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 ( <> From 7aa6afbfae8e4b5e0e01ba68304cc774f836e99b Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Thu, 7 Dec 2023 16:41:56 +0100 Subject: [PATCH 06/15] Re-style token title card --- .../TokenDashboardPage/TokenTitleCard.tsx | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) 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 )} - + ) } From 02119c65853a9beb40231bc108e5c4ea68868cdd Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Thu, 7 Dec 2023 18:48:47 +0100 Subject: [PATCH 07/15] Update tokens list table view --- src/app/components/Tokens/TokenList.tsx | 8 ++++++-- src/app/pages/TokensOverviewPage/index.tsx | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) 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/pages/TokensOverviewPage/index.tsx b/src/app/pages/TokensOverviewPage/index.tsx index 7a9166944..272ecf8d4 100644 --- a/src/app/pages/TokensOverviewPage/index.tsx +++ b/src/app/pages/TokensOverviewPage/index.tsx @@ -74,7 +74,7 @@ export const TokensPage: FC = () => { tableView === TableLayout.Vertical && } > - {!isMobile && } + {!isMobile && } } From 0373106b27c247ee803fbf4cfd30e10199443515 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 8 Dec 2023 08:20:49 +0100 Subject: [PATCH 08/15] Improve mobile token list --- src/app/components/Tokens/TokenDetails.tsx | 17 +++++++++-------- src/styles/theme/defaultTheme.ts | 2 ++ 2 files changed, 11 insertions(+), 8 deletions(-) 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/styles/theme/defaultTheme.ts b/src/styles/theme/defaultTheme.ts index a5eff0156..8b15e2bf1 100644 --- a/src/styles/theme/defaultTheme.ts +++ b/src/styles/theme/defaultTheme.ts @@ -607,7 +607,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)})`, }, }), }, From 1ea5fb042105de9e4b73b3b62ed4830d86e043c6 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 8 Dec 2023 09:02:57 +0100 Subject: [PATCH 09/15] Make nft gallery fully responsive on mobile --- .../TokenDashboardPage/ImageListItemImage.tsx | 18 ++++++++++++++---- src/styles/theme/defaultTheme.ts | 12 +++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx b/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx index 89cada5f7..5eaa1050a 100644 --- a/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx +++ b/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx @@ -6,14 +6,21 @@ 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' +const minMobileSize = '150px' +const mobileSize = '100%' const imageSize = '210px' -const StyledImage = styled('img')({ - width: imageSize, - height: imageSize, +const StyledImage = styled('img', { + shouldForwardProp: prop => prop !== 'isMobile', +})<{ isMobile: boolean }>(({ isMobile }) => ({ + minWidth: isMobile ? minMobileSize : imageSize, + minHeight: isMobile ? minMobileSize : imageSize, + width: isMobile ? mobileSize : imageSize, + height: isMobile ? mobileSize : imageSize, objectFit: 'cover', -}) +})) type ImageListItemImageProps = { instance: EvmNft @@ -21,6 +28,8 @@ type ImageListItemImageProps = { } export const ImageListItemImage: FC = ({ instance, to }) => { + const { isMobile } = useScreenSize() + return ( {isNftImageUrlValid(instance.image) ? ( @@ -28,6 +37,7 @@ export const ImageListItemImage: FC = ({ instance, to } src={processNftImageUrl(instance.image)} alt={getNftInstanceLabel(instance)} loading="lazy" + isMobile={isMobile} /> ) : ( diff --git a/src/styles/theme/defaultTheme.ts b/src/styles/theme/defaultTheme.ts index 8b15e2bf1..e63aea5d5 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, 210px))!important`, + }, + }), }, }, MuiImageListItem: { From c2c112e9cd83cc3c61e62cfd7fb2017416b4ccab Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 8 Dec 2023 10:14:10 +0100 Subject: [PATCH 10/15] Sync token holders list with mockups --- src/app/components/ProgressBar/index.ts | 18 +++++++++++++-- src/app/components/Tokens/TokenHolders.tsx | 27 ++++++++++++++++------ src/styles/theme/defaultTheme.ts | 1 - 3 files changed, 36 insertions(+), 10 deletions(-) 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/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/styles/theme/defaultTheme.ts b/src/styles/theme/defaultTheme.ts index e63aea5d5..62f07f961 100644 --- a/src/styles/theme/defaultTheme.ts +++ b/src/styles/theme/defaultTheme.ts @@ -599,7 +599,6 @@ export const defaultTheme = createTheme({ borderRadius: 5, }, bar: { - backgroundColor: COLORS.brandDark, borderRadius: 5, }, }, From b484d4c0c4c86f77c761e4e7df5a54e69d0fc7e2 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 8 Dec 2023 16:18:35 +0100 Subject: [PATCH 11/15] Include supply and holders in details token card --- .../TokenDashboardPage/TokenDetailsCard.tsx | 23 +++++++++++++++++++ src/app/utils/route-utils.ts | 6 +++++ 2 files changed, 29 insertions(+) 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/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, From c9986e55baeea158683a8d3c85c0cf05fded613b Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 8 Dec 2023 17:45:56 +0100 Subject: [PATCH 12/15] Improve list item image hover state --- .../TokenDashboardPage/ImageListItemImage.tsx | 34 ++++++++++++++++++- src/styles/theme/defaultTheme.ts | 3 +- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx b/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx index 5eaa1050a..82b639aaa 100644 --- a/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx +++ b/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx @@ -1,12 +1,16 @@ 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 minMobileSize = '150px' const mobileSize = '100%' @@ -20,6 +24,29 @@ const StyledImage = styled('img', { width: isMobile ? mobileSize : imageSize, height: isMobile ? mobileSize : imageSize, 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 = { @@ -28,10 +55,11 @@ type ImageListItemImageProps = { } export const ImageListItemImage: FC = ({ instance, to }) => { + const { t } = useTranslation() const { isMobile } = useScreenSize() return ( - + {isNftImageUrlValid(instance.image) ? ( = ({ instance, to } ) : ( )} + + + {t('common.view')} + ) } diff --git a/src/styles/theme/defaultTheme.ts b/src/styles/theme/defaultTheme.ts index 62f07f961..e88baef86 100644 --- a/src/styles/theme/defaultTheme.ts +++ b/src/styles/theme/defaultTheme.ts @@ -547,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, }, }, }, From f329e3ac64ee735cdca0dd31dcc604dd88158156 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Mon, 11 Dec 2023 09:18:43 +0100 Subject: [PATCH 13/15] Improve responsivness of desktop gallery --- src/app/components/NoPreview/index.tsx | 1 + .../TokenDashboardPage/ImageListItemImage.tsx | 14 +++++++------- src/app/pages/TokenDashboardPage/NFTLinks.tsx | 15 +++++++++++---- src/styles/theme/defaultTheme.ts | 2 +- 4 files changed, 20 insertions(+), 12 deletions(-) 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/pages/TokenDashboardPage/ImageListItemImage.tsx b/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx index 82b639aaa..3fb266cbb 100644 --- a/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx +++ b/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx @@ -13,16 +13,16 @@ import { useScreenSize } from 'app/hooks/useScreensize' import { COLORS } from 'styles/theme/colors' const minMobileSize = '150px' -const mobileSize = '100%' -const imageSize = '210px' +const minSize = '210px' const StyledImage = styled('img', { shouldForwardProp: prop => prop !== 'isMobile', })<{ isMobile: boolean }>(({ isMobile }) => ({ - minWidth: isMobile ? minMobileSize : imageSize, - minHeight: isMobile ? minMobileSize : imageSize, - width: isMobile ? mobileSize : imageSize, - height: isMobile ? mobileSize : imageSize, + 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': { @@ -68,7 +68,7 @@ export const ImageListItemImage: FC = ({ instance, to } isMobile={isMobile} /> ) : ( - + )} diff --git a/src/app/pages/TokenDashboardPage/NFTLinks.tsx b/src/app/pages/TokenDashboardPage/NFTLinks.tsx index 3e9d05d4f..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 }) => { ), }} /> - + ) } @@ -39,7 +46,7 @@ export const NFTInstanceLink: FC = ({ scope, instance }) => { const to = RouteUtils.getNFTInstanceRoute(scope, instance.token?.eth_contract_addr, instance.id) return ( - + = ({ scope, instance }) => { ), }} /> - + ) } diff --git a/src/styles/theme/defaultTheme.ts b/src/styles/theme/defaultTheme.ts index e88baef86..496250307 100644 --- a/src/styles/theme/defaultTheme.ts +++ b/src/styles/theme/defaultTheme.ts @@ -534,7 +534,7 @@ export const defaultTheme = createTheme({ [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, 210px))!important`, + gridTemplateColumns: `repeat(auto-fill, minmax(210px, auto))!important`, }, }), }, From 1d7f08d641b48d355a197a38fc233cccbee307c9 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Mon, 11 Dec 2023 12:25:35 +0100 Subject: [PATCH 14/15] Show creator data in three rows on mobile --- src/app/components/Account/ContractCreatorInfo.tsx | 13 +++---------- src/locales/en/translation.json | 2 +- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/app/components/Account/ContractCreatorInfo.tsx b/src/app/components/Account/ContractCreatorInfo.tsx index 4784a8754..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, @@ -60,15 +60,8 @@ export const ContractCreatorInfo: FC<{ }} > - - , - }} - /> - + {t('contract.createdAt')} + ) } 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", From eaf1a70e44fa41046c59d19e788cf7f047dab02a Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Tue, 12 Dec 2023 10:58:50 +0100 Subject: [PATCH 15/15] Add age column to token transfers table --- src/app/components/Tokens/TokenTransfers.tsx | 7 +++++++ 1 file changed, 7 insertions(+) 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,