From dd44681e345f1b1d6fb2832b55c1930c8c27de1b Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Thu, 30 Nov 2023 09:18:03 +0100 Subject: [PATCH 01/12] Add account nft collection route --- .changelog/1052.feature.md | 1 + .../AccountNFTCollectionCard.tsx | 31 +++++++++++++++++++ src/locales/en/translation.json | 1 + src/routes.tsx | 5 +++ 4 files changed, 38 insertions(+) create mode 100644 .changelog/1052.feature.md create mode 100644 src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx diff --git a/.changelog/1052.feature.md b/.changelog/1052.feature.md new file mode 100644 index 000000000..40c1421c8 --- /dev/null +++ b/.changelog/1052.feature.md @@ -0,0 +1 @@ +Add NFT feature diff --git a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx new file mode 100644 index 000000000..a5945d4de --- /dev/null +++ b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx @@ -0,0 +1,31 @@ +import { 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 { ErrorBoundary } from '../../components/ErrorBoundary' +import { LinkableDiv } from '../../components/PageLayout/LinkableDiv' +import { AccountDetailsContext } from './index' +import { accountTokenTransfersContainerId } from './AccountTokenTransfersCard' + +export const accountTokenContainerId = 'nftCollection' + +export const AccountNFTCollectionCard: FC = ({ scope, address }) => { + const { t } = useTranslation() + return ( + + + + + + + + + + + ) +} + +const AccountNFTCollection: FC = ({ scope, address }) => { + return <>sub view +} diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 4cc94eac9..967c7b95e 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -122,6 +122,7 @@ } }, "nft": { + "accountCollection": "ERC-721 Tokens", "collection": "Collection", "instanceIdLink": "ID: ", "instanceTokenId": "Token ID", diff --git a/src/routes.tsx b/src/routes.tsx index ea48d5987..6bd28291a 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -23,6 +23,7 @@ import { TokensPage } from './app/pages/TokensOverviewPage' import { ContractCodeCard } from './app/pages/AccountDetailsPage/ContractCodeCard' import { TokenDashboardPage, useTokenDashboardProps } from './app/pages/TokenDashboardPage' import { AccountTokenTransfersCard } from './app/pages/AccountDetailsPage/AccountTokenTransfersCard' +import { AccountNFTCollectionCard } from './app/pages/AccountDetailsPage/AccountNFTCollectionCard' import { TokenTransfersCard } from './app/pages/TokenDashboardPage/TokenTransfersCard' import { TokenHoldersCard } from './app/pages/TokenDashboardPage/TokenHoldersCard' import { TokenInventoryCard } from './app/pages/TokenDashboardPage/TokenInventoryCard' @@ -103,6 +104,10 @@ export const routes: RouteObject[] = [ path: '', Component: () => , }, + { + path: ':contractAddress', + Component: () => , + }, ], }, { From 215cd9cf9940fd59e41171076c42e6e95403ad37 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Thu, 30 Nov 2023 09:45:58 +0100 Subject: [PATCH 02/12] Link to account nft collection child route --- .../AccountDetailsPage/AccountTokensCard.tsx | 25 ++++++++++++++++--- .../InstanceDetailsCard.tsx | 2 +- src/locales/en/translation.json | 3 ++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/app/pages/AccountDetailsPage/AccountTokensCard.tsx b/src/app/pages/AccountDetailsPage/AccountTokensCard.tsx index b5adb6237..e9db59539 100644 --- a/src/app/pages/AccountDetailsPage/AccountTokensCard.tsx +++ b/src/app/pages/AccountDetailsPage/AccountTokensCard.tsx @@ -1,10 +1,11 @@ import { FC } from 'react' import { useTranslation } from 'react-i18next' -import { useLocation } from 'react-router-dom' +import { useLocation, Link as RouterLink } from 'react-router-dom' import Box from '@mui/material/Box' import Card from '@mui/material/Card' import CardHeader from '@mui/material/CardHeader' import CardContent from '@mui/material/CardContent' +import Link from '@mui/material/Link' import { CardEmptyState } from './CardEmptyState' import { Table, TableCellAlign, TableColProps } from '../../components/Table' import { CopyToClipboard } from '../../components/CopyToClipboard' @@ -43,11 +44,20 @@ export const AccountTokensCard: FC = ({ scope, address, const { t } = useTranslation() const locationHash = useLocation().hash.replace('#', '') const tokenListLabel = getTokenTypePluralName(t, type) + const isERC721 = type === EvmTokenType.ERC721 const tableColumns: TableColProps[] = [ - { key: 'name', content: t('common.name') }, + { key: 'name', content: t(isERC721 ? 'common.collection' : 'common.name') }, { key: 'contract', content: t('common.smartContract') }, - { key: 'balance', align: TableCellAlign.Right, content: t('common.balance') }, + { key: 'balance', align: TableCellAlign.Right, content: t(isERC721 ? 'common.owned' : 'common.balance') }, { key: 'ticker', align: TableCellAlign.Right, content: t('common.ticker') }, + ...(isERC721 + ? [ + { + key: 'link', + content: '', + }, + ] + : []), ] const { layer } = scope if (layer === Layer.consensus) { @@ -86,6 +96,15 @@ export const AccountTokensCard: FC = ({ scope, address, content: item.token_symbol || t('common.missing'), key: 'ticker', }, + { + align: TableCellAlign.Right, + key: 'link', + content: ( + + {t('common.viewAll')} + + ), + }, ], highlight: item.token_contract_addr_eth === locationHash || item.token_contract_addr === locationHash, })) diff --git a/src/app/pages/NFTInstanceDashboardPage/InstanceDetailsCard.tsx b/src/app/pages/NFTInstanceDashboardPage/InstanceDetailsCard.tsx index f17313e74..c583ab371 100644 --- a/src/app/pages/NFTInstanceDashboardPage/InstanceDetailsCard.tsx +++ b/src/app/pages/NFTInstanceDashboardPage/InstanceDetailsCard.tsx @@ -55,7 +55,7 @@ export const InstanceDetailsCard: FC = ({ )}
{t('nft.instanceTokenId')}
{nft.id}
-
{t('nft.collection')}
+
{t('common.collection')}
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 967c7b95e..649bc4ca7 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -40,6 +40,7 @@ "block": "Block", "bytes": "{{value, number}}", "cancel": "Cancel", + "collection": "Collection", "copy": "Copy", "data": "Data", "emerald": "Emerald", @@ -67,6 +68,7 @@ "nfts": "NFTs", "not_defined": "Not defined", "oasis": "Oasis", + "owned": "Owned", "paratime": "Paratime", "parentheses": "({{subject}})", "percentage": "Percentage", @@ -123,7 +125,6 @@ }, "nft": { "accountCollection": "ERC-721 Tokens", - "collection": "Collection", "instanceIdLink": "ID: ", "instanceTokenId": "Token ID", "instanceTitleSuffix": "(NFT Instance)", From c0a3016ef3e7671cbcd3779f7f7d3347b9201a95 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Thu, 30 Nov 2023 10:16:21 +0100 Subject: [PATCH 03/12] Update AccountNFTCollectionCard header --- .../AccountNFTCollectionCard.tsx | 44 ++++++++++++++++++- src/styles/theme/defaultTheme.ts | 13 ++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx index a5945d4de..bc10c7b42 100644 --- a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx +++ b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx @@ -1,21 +1,63 @@ import { FC } from 'react' import { useTranslation } from 'react-i18next' +import { useParams, Link as RouterLink } from 'react-router-dom' +import Box from '@mui/material/Box' +import Breadcrumbs from '@mui/material/Breadcrumbs' import Card from '@mui/material/Card' import CardHeader from '@mui/material/CardHeader' import CardContent from '@mui/material/CardContent' +import Link from '@mui/material/Link' +import Typography from '@mui/material/Typography' import { ErrorBoundary } from '../../components/ErrorBoundary' import { LinkableDiv } from '../../components/PageLayout/LinkableDiv' import { AccountDetailsContext } from './index' import { accountTokenTransfersContainerId } from './AccountTokenTransfersCard' +import { AccountLink } from 'app/components/Account/AccountLink' +import { CopyToClipboard } from 'app/components/CopyToClipboard' +import { AppErrors } from 'types/errors' +import { RouteUtils } from 'app/utils/route-utils' +import { COLORS } from 'styles/theme/colors' export const accountTokenContainerId = 'nftCollection' export const AccountNFTCollectionCard: FC = ({ scope, address }) => { const { t } = useTranslation() + const { contractAddress } = useParams() + + if (!contractAddress) { + throw AppErrors.InvalidAddress + } + return ( - + + + + + } + disableTypography + title={ + + + + {t('nft.accountCollection')} + + + + + {t('common.collection')} + + (3) + + + } + /> diff --git a/src/styles/theme/defaultTheme.ts b/src/styles/theme/defaultTheme.ts index 0cec866b9..a5eff0156 100644 --- a/src/styles/theme/defaultTheme.ts +++ b/src/styles/theme/defaultTheme.ts @@ -398,6 +398,19 @@ export const defaultTheme = createTheme({ }, }, }, + MuiBreadcrumbs: { + styleOverrides: { + li: { + fontSize: '24px', + }, + separator: { + color: COLORS.brandDark, + fontSize: '24px', + paddingRight: 3, + paddingLeft: 3, + }, + }, + }, MuiCardHeader: { styleOverrides: { root: ({ theme }) => ({ From 6e01c235b57b7224b68bb4d1c18f79384af18c45 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Thu, 30 Nov 2023 12:40:18 +0100 Subject: [PATCH 04/12] Show collection in account ERC721 tab --- .../AccountNFTCollectionCard.tsx | 64 +++++++++++++++---- src/app/pages/TokenDashboardPage/hook.ts | 37 +++++++++++ src/app/utils/route-utils.ts | 7 ++ src/routes.tsx | 2 + 4 files changed, 98 insertions(+), 12 deletions(-) diff --git a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx index bc10c7b42..d42b95142 100644 --- a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx +++ b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx @@ -1,6 +1,6 @@ import { FC } from 'react' import { useTranslation } from 'react-i18next' -import { useParams, Link as RouterLink } from 'react-router-dom' +import { useLoaderData, Link as RouterLink } from 'react-router-dom' import Box from '@mui/material/Box' import Breadcrumbs from '@mui/material/Breadcrumbs' import Card from '@mui/material/Card' @@ -14,18 +14,23 @@ import { AccountDetailsContext } from './index' import { accountTokenTransfersContainerId } from './AccountTokenTransfersCard' import { AccountLink } from 'app/components/Account/AccountLink' import { CopyToClipboard } from 'app/components/CopyToClipboard' -import { AppErrors } from 'types/errors' import { RouteUtils } from 'app/utils/route-utils' import { COLORS } from 'styles/theme/colors' +import { ImageListItemImage } from '../TokenDashboardPage/ImageListItemImage' +import ImageList from '@mui/material/ImageList' +import ImageListItem from '@mui/material/ImageListItem' +import ImageListItemBar from '@mui/material/ImageListItemBar' +import { CardEmptyState } from '../AccountDetailsPage/CardEmptyState' +import { TablePagination } from '../../components/Table/TablePagination' +import { useAccountTokenInventory } from '../TokenDashboardPage/hook' export const accountTokenContainerId = 'nftCollection' export const AccountNFTCollectionCard: FC = ({ scope, address }) => { const { t } = useTranslation() - const { contractAddress } = useParams() - - if (!contractAddress) { - throw AppErrors.InvalidAddress + const { ethContractAddress, oasisContractAddress } = useLoaderData() as { + ethContractAddress: string + oasisContractAddress: string } return ( @@ -34,8 +39,8 @@ export const AccountNFTCollectionCard: FC = ({ scope, add - - + + } disableTypography @@ -44,7 +49,7 @@ export const AccountNFTCollectionCard: FC = ({ scope, add {t('nft.accountCollection')} @@ -61,13 +66,48 @@ export const AccountNFTCollectionCard: FC = ({ scope, add - + ) } -const AccountNFTCollection: FC = ({ scope, address }) => { - return <>sub view +type AccountNFTCollectionProps = { + tokenAddress: string +} & AccountDetailsContext + +const AccountNFTCollection: FC = ({ address, scope, tokenAddress }) => { + const { t } = useTranslation() + const { inventory, isFetched, pagination, totalCount } = useAccountTokenInventory( + scope, + address, + tokenAddress, + ) + + return ( + <> + {isFetched && !totalCount && } + {!!inventory?.length && ( + <> + + {inventory?.map(instance => { + const to = RouteUtils.getNFTInstanceRoute(scope, instance.token?.contract_addr, instance.id) + return ( + + + + + ) + })} + + {pagination && ( + + + + )} + + )} + + ) } diff --git a/src/app/pages/TokenDashboardPage/hook.ts b/src/app/pages/TokenDashboardPage/hook.ts index 0f0972ea3..6afd0cbcf 100644 --- a/src/app/pages/TokenDashboardPage/hook.ts +++ b/src/app/pages/TokenDashboardPage/hook.ts @@ -6,6 +6,7 @@ import { useGetRuntimeEvmTokensAddress, useGetRuntimeEvmTokensAddressHolders, useGetRuntimeEvmTokensAddressNfts, + useGetRuntimeAccountsAddressNfts, } from '../../../oasis-nexus/api' import { AppErrors } from '../../../types/errors' import { SearchScope } from '../../../types/searchScope' @@ -141,3 +142,39 @@ export const useTokenInventory = (scope: SearchScope, address: string) => { totalCount, } } + +export const useAccountTokenInventory = (scope: SearchScope, address: string, tokenAddress: string) => { + const { network, layer } = scope + const pagination = useSearchParamsPagination('page') + const offset = (pagination.selectedPage - 1) * NUMBER_OF_INVENTORY_ITEMS + if (layer === Layer.consensus) { + throw AppErrors.UnsupportedLayer + // There are no tokens on the consensus layer. + } + const query = useGetRuntimeAccountsAddressNfts(network, layer, address, { + limit: NUMBER_OF_INVENTORY_ITEMS, + offset: offset, + token_address: tokenAddress, + }) + const { isFetched, isLoading, data } = query + const inventory = data?.data.evm_nfts + + if (isFetched && pagination.selectedPage > 1 && !inventory?.length) { + throw AppErrors.PageDoesNotExist + } + + const totalCount = data?.data.total_count + const isTotalCountClipped = data?.data.is_total_count_clipped + + return { + isLoading, + isFetched, + inventory, + pagination: { + ...pagination, + isTotalCountClipped, + rowsPerPage: NUMBER_OF_INVENTORY_ITEMS, + }, + totalCount, + } +} diff --git a/src/app/utils/route-utils.ts b/src/app/utils/route-utils.ts index 60f732526..646960fcd 100644 --- a/src/app/utils/route-utils.ts +++ b/src/app/utils/route-utils.ts @@ -148,6 +148,13 @@ export const addressParamLoader = async ({ params }: LoaderFunctionArgs) => { return address } +export const contractAddressParamLoader = async ({ params }: LoaderFunctionArgs) => { + validateAddressParam(params.contractAddress!) + // TODO: remove conversion when API supports querying by EVM address + const oasisContractAddress = await getOasisAddress(params.contractAddress!) + return { ethContractAddress: params.contractAddress, oasisContractAddress } +} + export const blockHeightParamLoader = async ({ params }: LoaderFunctionArgs) => { return validateBlockHeightParam(params.blockHeight!) } diff --git a/src/routes.tsx b/src/routes.tsx index 6bd28291a..bff15af7f 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -12,6 +12,7 @@ import { SearchResultsPage } from './app/pages/SearchResultsPage' import { addressParamLoader, blockHeightParamLoader, + contractAddressParamLoader, transactionParamLoader, scopeLoader, } from './app/utils/route-utils' @@ -107,6 +108,7 @@ export const routes: RouteObject[] = [ { path: ':contractAddress', Component: () => , + loader: contractAddressParamLoader, }, ], }, From f94818a0c7bbf3b8427b03cda222e0a2b8226cd9 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 1 Dec 2023 08:49:40 +0100 Subject: [PATCH 05/12] Sync account ERC721 collection header with API --- .../AccountNFTCollectionCard.tsx | 92 +++++++++++++------ src/app/pages/TokenDashboardPage/hook.ts | 2 +- 2 files changed, 64 insertions(+), 30 deletions(-) diff --git a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx index d42b95142..d7513aa60 100644 --- a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx +++ b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx @@ -1,6 +1,6 @@ import { FC } from 'react' import { useTranslation } from 'react-i18next' -import { useLoaderData, Link as RouterLink } from 'react-router-dom' +import { useLoaderData, Link as RouterLink, To } from 'react-router-dom' import Box from '@mui/material/Box' import Breadcrumbs from '@mui/material/Breadcrumbs' import Card from '@mui/material/Card' @@ -8,6 +8,10 @@ import CardHeader from '@mui/material/CardHeader' import CardContent from '@mui/material/CardContent' import Link from '@mui/material/Link' import Typography from '@mui/material/Typography' +import ImageList from '@mui/material/ImageList' +import ImageListItem from '@mui/material/ImageListItem' +import ImageListItemBar from '@mui/material/ImageListItemBar' +import Skeleton from '@mui/material/Skeleton' import { ErrorBoundary } from '../../components/ErrorBoundary' import { LinkableDiv } from '../../components/PageLayout/LinkableDiv' import { AccountDetailsContext } from './index' @@ -17,12 +21,11 @@ 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 ImageList from '@mui/material/ImageList' -import ImageListItem from '@mui/material/ImageListItem' -import ImageListItemBar from '@mui/material/ImageListItemBar' import { CardEmptyState } from '../AccountDetailsPage/CardEmptyState' import { TablePagination } from '../../components/Table/TablePagination' import { useAccountTokenInventory } from '../TokenDashboardPage/hook' +import { EvmNft } from 'oasis-nexus/api' +import { SearchScope } from '../../../types/searchScope' export const accountTokenContainerId = 'nftCollection' @@ -32,6 +35,8 @@ export const AccountNFTCollectionCard: FC = ({ scope, add ethContractAddress: string oasisContractAddress: string } + const { inventory, isFetched, isLoading, isTotalCountClipped, pagination, totalCount } = + useAccountTokenInventory(scope, address, oasisContractAddress) return ( @@ -45,28 +50,42 @@ export const AccountNFTCollectionCard: FC = ({ scope, add } disableTypography title={ - - - - {t('nft.accountCollection')} - - - - - {t('common.collection')} + + + + + {t('nft.accountCollection')} + - (3) - - + {isFetched && ( + + + {inventory?.[0].token.name ? inventory?.[0].token.name : t('common.collection')} + + {totalCount && ( + ({`${isTotalCountClipped ? ' > ' : ''}${totalCount}`}) + )} + + )} + + {isLoading && } + } /> - + @@ -74,16 +93,27 @@ export const AccountNFTCollectionCard: FC = ({ scope, add } type AccountNFTCollectionProps = { - tokenAddress: string -} & AccountDetailsContext + inventory: EvmNft[] | undefined + isFetched: boolean + isTotalCountClipped: boolean | undefined + totalCount: number | undefined + pagination: { + rowsPerPage: number + selectedPage: number + linkToPage: (pageNumber: number) => To + } + scope: SearchScope +} -const AccountNFTCollection: FC = ({ address, scope, tokenAddress }) => { +const AccountNFTCollection: FC = ({ + inventory, + isFetched, + isTotalCountClipped, + pagination, + scope, + totalCount, +}) => { const { t } = useTranslation() - const { inventory, isFetched, pagination, totalCount } = useAccountTokenInventory( - scope, - address, - tokenAddress, - ) return ( <> @@ -103,7 +133,11 @@ const AccountNFTCollection: FC = ({ address, scope, t {pagination && ( - + )} diff --git a/src/app/pages/TokenDashboardPage/hook.ts b/src/app/pages/TokenDashboardPage/hook.ts index 6afd0cbcf..91064af01 100644 --- a/src/app/pages/TokenDashboardPage/hook.ts +++ b/src/app/pages/TokenDashboardPage/hook.ts @@ -172,9 +172,9 @@ export const useAccountTokenInventory = (scope: SearchScope, address: string, to inventory, pagination: { ...pagination, - isTotalCountClipped, rowsPerPage: NUMBER_OF_INVENTORY_ITEMS, }, + isTotalCountClipped, totalCount, } } From c20aa58dce7960335eba2892ffc5698e7772581c Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 1 Dec 2023 10:20:47 +0100 Subject: [PATCH 06/12] Create NFTInstanceLink --- .../AccountNFTCollectionCard.tsx | 7 +++- src/app/pages/TokenDashboardPage/NFTLinks.tsx | 34 +++++++++++++++++++ .../TokenDashboardPage/TokenInventoryCard.tsx | 19 ++--------- 3 files changed, 43 insertions(+), 17 deletions(-) create mode 100644 src/app/pages/TokenDashboardPage/NFTLinks.tsx diff --git a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx index d7513aa60..431d353c8 100644 --- a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx +++ b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx @@ -26,6 +26,7 @@ import { TablePagination } from '../../components/Table/TablePagination' import { useAccountTokenInventory } from '../TokenDashboardPage/hook' import { EvmNft } from 'oasis-nexus/api' import { SearchScope } from '../../../types/searchScope' +import { NFTInstanceLink } from '../TokenDashboardPage/NFTLinks' export const accountTokenContainerId = 'nftCollection' @@ -126,7 +127,11 @@ const AccountNFTCollection: FC = ({ return ( - + } + position="below" + /> ) })} diff --git a/src/app/pages/TokenDashboardPage/NFTLinks.tsx b/src/app/pages/TokenDashboardPage/NFTLinks.tsx new file mode 100644 index 000000000..ed522ff75 --- /dev/null +++ b/src/app/pages/TokenDashboardPage/NFTLinks.tsx @@ -0,0 +1,34 @@ +import { FC } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { Link as RouterLink } from 'react-router-dom' +import Link from '@mui/material/Link' +import Typography from '@mui/material/Typography' +import { RouteUtils } from '../../utils/route-utils' +import { EvmNft } from 'oasis-nexus/api' +import { SearchScope } from 'types/searchScope' + +type NFTInstanceLinkProps = { + scope: SearchScope + instance: EvmNft +} + +export const NFTInstanceLink: FC = ({ scope, instance }) => { + const { t } = useTranslation() + const to = RouteUtils.getNFTInstanceRoute(scope, instance.token?.contract_addr, instance.id) + + return ( + + + {instance.id} + + ), + }} + /> + + ) +} diff --git a/src/app/pages/TokenDashboardPage/TokenInventoryCard.tsx b/src/app/pages/TokenDashboardPage/TokenInventoryCard.tsx index ae6fde10a..f5303a66f 100644 --- a/src/app/pages/TokenDashboardPage/TokenInventoryCard.tsx +++ b/src/app/pages/TokenDashboardPage/TokenInventoryCard.tsx @@ -1,6 +1,5 @@ import { FC } from 'react' -import { Trans, useTranslation } from 'react-i18next' -import { Link as RouterLink } from 'react-router-dom' +import { useTranslation } from 'react-i18next' import Box from '@mui/material/Box' import Card from '@mui/material/Card' import CardHeader from '@mui/material/CardHeader' @@ -8,7 +7,6 @@ import CardContent from '@mui/material/CardContent' import ImageList from '@mui/material/ImageList' import ImageListItem from '@mui/material/ImageListItem' import ImageListItemBar from '@mui/material/ImageListItemBar' -import Link from '@mui/material/Link' import { ErrorBoundary } from '../../components/ErrorBoundary' import { LinkableDiv } from '../../components/PageLayout/LinkableDiv' import { CardEmptyState } from '../AccountDetailsPage/CardEmptyState' @@ -18,6 +16,7 @@ import { RouteUtils } from '../../utils/route-utils' import { TablePagination } from '../../components/Table/TablePagination' import { useTokenInventory } from './hook' import { ImageListItemImage } from './ImageListItemImage' +import { NFTInstanceLink } from './NFTLinks' export const tokenInventoryContainerId = 'inventory' @@ -55,19 +54,7 @@ const TokenInventoryView: FC = ({ scope, address }) => { - #{instance.id} - - ), - }} - /> - } + title={} subtitle={ owner ? : undefined } From 1e19d6e5e93f9d0ccf4c925b38c61b0f2a5a47e3 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 1 Dec 2023 10:18:40 +0100 Subject: [PATCH 07/12] Create NFTCollectionLink --- .../AccountNFTCollectionCard.tsx | 4 +-- src/app/pages/TokenDashboardPage/NFTLinks.tsx | 30 ++++++++++++++++--- src/locales/en/translation.json | 1 + 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx index 431d353c8..dcd40de98 100644 --- a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx +++ b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx @@ -26,7 +26,7 @@ import { TablePagination } from '../../components/Table/TablePagination' import { useAccountTokenInventory } from '../TokenDashboardPage/hook' import { EvmNft } from 'oasis-nexus/api' import { SearchScope } from '../../../types/searchScope' -import { NFTInstanceLink } from '../TokenDashboardPage/NFTLinks' +import { NFTCollectionLink, NFTInstanceLink } from '../TokenDashboardPage/NFTLinks' export const accountTokenContainerId = 'nftCollection' @@ -128,7 +128,7 @@ const AccountNFTCollection: FC = ({ } subtitle={} position="below" /> diff --git a/src/app/pages/TokenDashboardPage/NFTLinks.tsx b/src/app/pages/TokenDashboardPage/NFTLinks.tsx index ed522ff75..cde013101 100644 --- a/src/app/pages/TokenDashboardPage/NFTLinks.tsx +++ b/src/app/pages/TokenDashboardPage/NFTLinks.tsx @@ -1,18 +1,40 @@ import { FC } from 'react' import { Trans, useTranslation } from 'react-i18next' 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 { RouteUtils } from '../../utils/route-utils' -import { EvmNft } from 'oasis-nexus/api' -import { SearchScope } from 'types/searchScope' +import { SearchScope } from '../../../types/searchScope' +import { trimLongString } from '../../utils/trimLongString' -type NFTInstanceLinkProps = { +type NFTLinkProps = { scope: SearchScope instance: EvmNft } -export const NFTInstanceLink: FC = ({ scope, instance }) => { +export const NFTCollectionLink: FC = ({ scope, instance }) => { + const { t } = useTranslation() + const to = RouteUtils.getTokenRoute(scope, instance.token?.contract_addr) + + return ( + + + {instance.token?.name ?? trimLongString(instance.token?.eth_contract_addr, 5, 5)} + + ), + }} + /> + + ) +} + +export const NFTInstanceLink: FC = ({ scope, instance }) => { const { t } = useTranslation() const to = RouteUtils.getNFTInstanceRoute(scope, instance.token?.contract_addr, instance.id) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 649bc4ca7..b52eafed0 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -125,6 +125,7 @@ }, "nft": { "accountCollection": "ERC-721 Tokens", + "collectionLink": "Collection: ", "instanceIdLink": "ID: ", "instanceTokenId": "Token ID", "instanceTitleSuffix": "(NFT Instance)", From 101a6fb1c48bbb2745ed1525e6f15d96fc9d0ef1 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 1 Dec 2023 10:26:23 +0100 Subject: [PATCH 08/12] Do not crash when nft is missing ownership data --- .../pages/AccountDetailsPage/AccountNFTCollectionCard.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx index dcd40de98..ba692fe90 100644 --- a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx +++ b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx @@ -64,9 +64,11 @@ export const AccountNFTCollectionCard: FC = ({ scope, add {isFetched && ( - {inventory?.[0].token.name ? inventory?.[0].token.name : t('common.collection')} + {inventory?.length && inventory?.[0].token.name + ? inventory?.[0].token.name + : t('common.collection')} - {totalCount && ( + {!!totalCount && ( ({`${isTotalCountClipped ? ' > ' : ''}${totalCount}`}) )} From f504c3a3c5a187fabdb4f22f7e7b498406a82391 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 1 Dec 2023 10:30:32 +0100 Subject: [PATCH 09/12] Add loading state to account collection card --- src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx index ba692fe90..b8c1ba6d7 100644 --- a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx +++ b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx @@ -84,6 +84,7 @@ export const AccountNFTCollectionCard: FC = ({ scope, add = ({ scope, add type AccountNFTCollectionProps = { inventory: EvmNft[] | undefined + isLoading: boolean isFetched: boolean isTotalCountClipped: boolean | undefined totalCount: number | undefined @@ -110,6 +112,7 @@ type AccountNFTCollectionProps = { const AccountNFTCollection: FC = ({ inventory, + isLoading, isFetched, isTotalCountClipped, pagination, @@ -120,6 +123,7 @@ const AccountNFTCollection: FC = ({ return ( <> + {isLoading && } {isFetched && !totalCount && } {!!inventory?.length && ( <> From a1ebbfb54822ad508e6310d133989a8343d9fd3d Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 1 Dec 2023 11:08:36 +0100 Subject: [PATCH 10/12] Prevent changing scroll position during navigation between collections --- .../pages/AccountDetailsPage/AccountNFTCollectionCard.tsx | 8 ++++---- src/app/pages/AccountDetailsPage/AccountTokensCard.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx index b8c1ba6d7..b19577d1c 100644 --- a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx +++ b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx @@ -15,7 +15,6 @@ import Skeleton from '@mui/material/Skeleton' import { ErrorBoundary } from '../../components/ErrorBoundary' import { LinkableDiv } from '../../components/PageLayout/LinkableDiv' import { AccountDetailsContext } from './index' -import { accountTokenTransfersContainerId } from './AccountTokenTransfersCard' import { AccountLink } from 'app/components/Account/AccountLink' import { CopyToClipboard } from 'app/components/CopyToClipboard' import { RouteUtils } from 'app/utils/route-utils' @@ -28,7 +27,7 @@ import { EvmNft } from 'oasis-nexus/api' import { SearchScope } from '../../../types/searchScope' import { NFTCollectionLink, NFTInstanceLink } from '../TokenDashboardPage/NFTLinks' -export const accountTokenContainerId = 'nftCollection' +export const accountNFTCollectionContainerId = 'nftCollection' export const AccountNFTCollectionCard: FC = ({ scope, address }) => { const { t } = useTranslation() @@ -41,7 +40,7 @@ export const AccountNFTCollectionCard: FC = ({ scope, add return ( - + @@ -55,8 +54,9 @@ export const AccountNFTCollectionCard: FC = ({ scope, add {t('nft.accountCollection')} diff --git a/src/app/pages/AccountDetailsPage/AccountTokensCard.tsx b/src/app/pages/AccountDetailsPage/AccountTokensCard.tsx index e9db59539..6c336b814 100644 --- a/src/app/pages/AccountDetailsPage/AccountTokensCard.tsx +++ b/src/app/pages/AccountDetailsPage/AccountTokensCard.tsx @@ -100,7 +100,7 @@ export const AccountTokensCard: FC = ({ scope, address, align: TableCellAlign.Right, key: 'link', content: ( - + {t('common.viewAll')} ), From d3ee8627c4811ee7cf4535251977b23b7b9c2327 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Fri, 1 Dec 2023 15:39:10 +0100 Subject: [PATCH 11/12] Maintain ImageListItemImage aspect ratio --- src/app/pages/TokenDashboardPage/ImageListItemImage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx b/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx index 6f1b16f45..89cada5f7 100644 --- a/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx +++ b/src/app/pages/TokenDashboardPage/ImageListItemImage.tsx @@ -12,6 +12,7 @@ const imageSize = '210px' const StyledImage = styled('img')({ width: imageSize, height: imageSize, + objectFit: 'cover', }) type ImageListItemImageProps = { From de3585d004f15c097a648f3a7562576eba8a7ed9 Mon Sep 17 00:00:00 2001 From: Michal Zielenkiewicz Date: Mon, 4 Dec 2023 09:42:29 +0100 Subject: [PATCH 12/12] Simplify contract address param loader --- .../AccountNFTCollectionCard.tsx | 21 +++++++++---------- src/app/utils/route-utils.ts | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx index b19577d1c..aadcee489 100644 --- a/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx +++ b/src/app/pages/AccountDetailsPage/AccountNFTCollectionCard.tsx @@ -31,22 +31,23 @@ export const accountNFTCollectionContainerId = 'nftCollection' export const AccountNFTCollectionCard: FC = ({ scope, address }) => { const { t } = useTranslation() - const { ethContractAddress, oasisContractAddress } = useLoaderData() as { - ethContractAddress: string - oasisContractAddress: string - } + const oasisContractAddress = useLoaderData() as string const { inventory, isFetched, isLoading, isTotalCountClipped, pagination, totalCount } = useAccountTokenInventory(scope, address, oasisContractAddress) + const firstToken = inventory?.length ? inventory?.[0].token : undefined return ( - - - + isFetched && + firstToken && ( + + + + + ) } disableTypography title={ @@ -64,9 +65,7 @@ export const AccountNFTCollectionCard: FC = ({ scope, add {isFetched && ( - {inventory?.length && inventory?.[0].token.name - ? inventory?.[0].token.name - : t('common.collection')} + {firstToken?.name ? inventory?.[0].token.name : t('common.collection')} {!!totalCount && ( ({`${isTotalCountClipped ? ' > ' : ''}${totalCount}`}) diff --git a/src/app/utils/route-utils.ts b/src/app/utils/route-utils.ts index 646960fcd..7b22ba368 100644 --- a/src/app/utils/route-utils.ts +++ b/src/app/utils/route-utils.ts @@ -151,8 +151,8 @@ export const addressParamLoader = async ({ params }: LoaderFunctionArgs) => { export const contractAddressParamLoader = async ({ params }: LoaderFunctionArgs) => { validateAddressParam(params.contractAddress!) // TODO: remove conversion when API supports querying by EVM address - const oasisContractAddress = await getOasisAddress(params.contractAddress!) - return { ethContractAddress: params.contractAddress, oasisContractAddress } + const address = await getOasisAddress(params.contractAddress!) + return address } export const blockHeightParamLoader = async ({ params }: LoaderFunctionArgs) => {