Commit 24ff3fa8 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into internal-link

parents dfcb4027 758af1c4
...@@ -27,7 +27,7 @@ export default function useNavItems() { ...@@ -27,7 +27,7 @@ export default function useNavItems() {
const mainNavItems = [ const mainNavItems = [
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block'), isNewUi: true }, { text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block'), isNewUi: true },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx'), isNewUi: true }, { text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx'), isNewUi: true },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens', isNewUi: true }, { text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute.startsWith('token'), isNewUi: true },
{ text: 'Accounts', url: link('accounts'), icon: walletIcon, isActive: currentRoute === 'accounts', isNewUi: true }, { text: 'Accounts', url: link('accounts'), icon: walletIcon, isActive: currentRoute === 'accounts', isNewUi: true },
isMarketplaceFilled ? isMarketplaceFilled ?
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute.startsWith('app'), isNewUi: true } : null, { text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute.startsWith('app'), isNewUi: true } : null,
......
...@@ -5,6 +5,7 @@ import React from 'react'; ...@@ -5,6 +5,7 @@ import React from 'react';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
import type { ResourceError } from 'lib/api/resources';
import * as addressMock from 'mocks/address/address'; import * as addressMock from 'mocks/address/address';
import * as countersMock from 'mocks/address/counters'; import * as countersMock from 'mocks/address/counters';
import * as tokenBalanceMock from 'mocks/address/tokenBalance'; import * as tokenBalanceMock from 'mocks/address/tokenBalance';
...@@ -36,7 +37,7 @@ test('contract +@mobile', async({ mount, page }) => { ...@@ -36,7 +37,7 @@ test('contract +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<AddressDetails addressQuery={{ data: addressMock.contract } as UseQueryResult<Address, unknown>}/> <AddressDetails addressQuery={{ data: addressMock.contract } as UseQueryResult<Address, ResourceError>}/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -65,7 +66,7 @@ test('token', async({ mount, page }) => { ...@@ -65,7 +66,7 @@ test('token', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<MockAddressPage> <MockAddressPage>
<AddressDetails addressQuery={{ data: addressMock.token } as UseQueryResult<Address, unknown>}/> <AddressDetails addressQuery={{ data: addressMock.token } as UseQueryResult<Address, ResourceError>}/>
</MockAddressPage> </MockAddressPage>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
...@@ -86,7 +87,7 @@ test('validator +@mobile', async({ mount, page }) => { ...@@ -86,7 +87,7 @@ test('validator +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<AddressDetails addressQuery={{ data: addressMock.validator } as UseQueryResult<Address, unknown>}/> <AddressDetails addressQuery={{ data: addressMock.validator } as UseQueryResult<Address, ResourceError>}/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
......
...@@ -7,6 +7,7 @@ import type { Address as TAddress } from 'types/api/address'; ...@@ -7,6 +7,7 @@ import type { Address as TAddress } from 'types/api/address';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg'; import blockIcon from 'icons/block.svg';
import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link'; import link from 'lib/link/link';
...@@ -30,7 +31,7 @@ import AddressQrCode from './details/AddressQrCode'; ...@@ -30,7 +31,7 @@ import AddressQrCode from './details/AddressQrCode';
import TokenSelect from './tokenSelect/TokenSelect'; import TokenSelect from './tokenSelect/TokenSelect';
interface Props { interface Props {
addressQuery: UseQueryResult<TAddress>; addressQuery: UseQueryResult<TAddress, ResourceError>;
scrollRef?: React.RefObject<HTMLDivElement>; scrollRef?: React.RefObject<HTMLDivElement>;
} }
...@@ -38,8 +39,10 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -38,8 +39,10 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const addressHash = router.query.id?.toString();
const countersQuery = useApiQuery('address_counters', { const countersQuery = useApiQuery('address_counters', {
pathParams: { id: router.query.id?.toString() }, pathParams: { id: addressHash },
queryOptions: { queryOptions: {
enabled: Boolean(router.query.id) && Boolean(addressQuery.data), enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
}, },
...@@ -52,33 +55,54 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -52,33 +55,54 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
}, 500); }, 500);
}, [ scrollRef ]); }, [ scrollRef ]);
if (addressQuery.isError) { const errorData = React.useMemo(() => ({
hash: addressHash || '',
is_contract: false,
implementation_name: null,
implementation_address: null,
token: null,
watchlist_names: null,
creation_tx_hash: null,
block_number_balance_updated_at: null,
name: null,
exchange_rate: null,
coin_balance: null,
has_tokens: true,
has_token_transfers: true,
has_validated_blocks: false,
}), [ addressHash ]);
const is404Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 404;
const is422Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 422;
if (addressQuery.isError && is422Error) {
throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error }); throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error });
} }
if (addressQuery.isLoading) { if (addressQuery.isError && !is404Error) {
return <AddressDetailsSkeleton/>; return <DataFetchAlert/>;
} }
if (addressQuery.isError) { if (addressQuery.isLoading) {
return <DataFetchAlert/>; return <AddressDetailsSkeleton/>;
} }
const explorers = appConfig.network.explorers.filter(({ paths }) => paths.address); const explorers = appConfig.network.explorers.filter(({ paths }) => paths.address);
const data = addressQuery.isError ? errorData : addressQuery.data;
return ( return (
<Box> <Box>
<Flex alignItems="center"> <Flex alignItems="center">
<AddressIcon address={ addressQuery.data }/> <AddressIcon address={ data }/>
<Text ml={ 2 } fontFamily="heading" fontWeight={ 500 }> <Text ml={ 2 } fontFamily="heading" fontWeight={ 500 }>
{ isMobile ? <HashStringShorten hash={ addressQuery.data.hash }/> : addressQuery.data.hash } { isMobile ? <HashStringShorten hash={ data.hash }/> : data.hash }
</Text> </Text>
<CopyToClipboard text={ addressQuery.data.hash }/> <CopyToClipboard text={ data.hash }/>
{ addressQuery.data.is_contract && addressQuery.data.token && <AddressAddToMetaMask ml={ 2 } token={ addressQuery.data.token }/> } { data.is_contract && data.token && <AddressAddToMetaMask ml={ 2 } token={ data.token }/> }
{ !addressQuery.data.is_contract && ( { !data.is_contract && (
<AddressFavoriteButton hash={ addressQuery.data.hash } isAdded={ Boolean(addressQuery.data.watchlist_names?.length) } ml={ 3 }/> <AddressFavoriteButton hash={ data.hash } isAdded={ Boolean(data.watchlist_names?.length) } ml={ 3 }/>
) } ) }
<AddressQrCode hash={ addressQuery.data.hash } ml={ 2 }/> <AddressQrCode hash={ data.hash } ml={ 2 }/>
</Flex> </Flex>
{ explorers.length > 0 && ( { explorers.length > 0 && (
<Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap"> <Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap">
...@@ -95,71 +119,79 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -95,71 +119,79 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
rowGap={{ base: 1, lg: 3 }} rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden" templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
> >
<AddressNameInfo data={ addressQuery.data }/> <AddressNameInfo data={ data }/>
{ addressQuery.data.is_contract && addressQuery.data.creation_tx_hash && addressQuery.data.creator_address_hash && ( { data.is_contract && data.creation_tx_hash && data.creator_address_hash && (
<DetailsInfoItem <DetailsInfoItem
title="Creator" title="Creator"
hint="Transaction and address of creation." hint="Transaction and address of creation."
> >
<AddressLink type="address" hash={ addressQuery.data.creator_address_hash } truncation="constant"/> <AddressLink type="address" hash={ data.creator_address_hash } truncation="constant"/>
<Text whiteSpace="pre"> at txn </Text> <Text whiteSpace="pre"> at txn </Text>
<AddressLink hash={ addressQuery.data.creation_tx_hash } type="transaction" truncation="constant"/> <AddressLink hash={ data.creation_tx_hash } type="transaction" truncation="constant"/>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ addressQuery.data.is_contract && addressQuery.data.implementation_address && ( { data.is_contract && data.implementation_address && (
<DetailsInfoItem <DetailsInfoItem
title="Implementation" title="Implementation"
hint="Implementation address of the proxy contract." hint="Implementation address of the proxy contract."
columnGap={ 1 } columnGap={ 1 }
> >
<LinkInternal href={ link('address_index', { id: addressQuery.data.implementation_address }) }> <LinkInternal href={ link('address_index', { id: data.implementation_address }) }>
{ addressQuery.data.implementation_name } { data.implementation_name }
</LinkInternal> </LinkInternal>
<Text variant="secondary" overflow="hidden"> <Text variant="secondary" overflow="hidden">
<HashStringShortenDynamic hash={ `(${ addressQuery.data.implementation_address })` }/> <HashStringShortenDynamic hash={ `(${ data.implementation_address })` }/>
</Text> </Text>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<AddressBalance data={ addressQuery.data }/> <AddressBalance data={ data }/>
{ addressQuery.data.has_tokens && ( { data.has_tokens && (
<DetailsInfoItem <DetailsInfoItem
title="Tokens" title="Tokens"
hint="All tokens in the account and total value." hint="All tokens in the account and total value."
alignSelf="center" alignSelf="center"
py={ 0 } py={ 0 }
> >
<TokenSelect/> { addressQuery.data ? <TokenSelect onClick={ handleCounterItemClick }/> : <Box py="6px">0</Box> }
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<DetailsInfoItem <DetailsInfoItem
title="Transactions" title="Transactions"
hint="Number of transactions related to this address." hint="Number of transactions related to this address."
> >
<AddressCounterItem prop="transactions_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/> { addressQuery.data ?
<AddressCounterItem prop="transactions_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
0 }
</DetailsInfoItem> </DetailsInfoItem>
{ addressQuery.data.has_token_transfers && ( { data.has_token_transfers && (
<DetailsInfoItem <DetailsInfoItem
title="Transfers" title="Transfers"
hint="Number of transfers to/from this address." hint="Number of transfers to/from this address."
> >
<AddressCounterItem prop="token_transfers_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/> { addressQuery.data ?
<AddressCounterItem prop="token_transfers_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
0 }
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
<DetailsInfoItem <DetailsInfoItem
title="Gas used" title="Gas used"
hint="Gas used by the address." hint="Gas used by the address."
> >
<AddressCounterItem prop="gas_usage_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/> { addressQuery.data ?
<AddressCounterItem prop="gas_usage_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
0 }
</DetailsInfoItem> </DetailsInfoItem>
{ addressQuery.data.has_validated_blocks && ( { data.has_validated_blocks && (
<DetailsInfoItem <DetailsInfoItem
title="Blocks validated" title="Blocks validated"
hint="Number of blocks validated by this validator." hint="Number of blocks validated by this validator."
> >
<AddressCounterItem prop="validations_count" query={ countersQuery } address={ addressQuery.data.hash } onClick={ handleCounterItemClick }/> { addressQuery.data ?
<AddressCounterItem prop="validations_count" query={ countersQuery } address={ data.hash } onClick={ handleCounterItemClick }/> :
0 }
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
{ addressQuery.data.block_number_balance_updated_at && ( { data.block_number_balance_updated_at && (
<DetailsInfoItem <DetailsInfoItem
title="Last balance update" title="Last balance update"
hint="Block number in which the address was updated." hint="Block number in which the address was updated."
...@@ -167,12 +199,12 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -167,12 +199,12 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
py={{ base: '2px', lg: 1 }} py={{ base: '2px', lg: 1 }}
> >
<LinkInternal <LinkInternal
href={ link('block', { id: String(addressQuery.data.block_number_balance_updated_at) }) } href={ link('block', { id: String(data.block_number_balance_updated_at) }) }
display="flex" display="flex"
alignItems="center" alignItems="center"
> >
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 }/> <Icon as={ blockIcon } boxSize={ 6 } mr={ 2 }/>
{ addressQuery.data.block_number_balance_updated_at } { data.block_number_balance_updated_at }
</LinkInternal> </LinkInternal>
</DetailsInfoItem> </DetailsInfoItem>
) } ) }
......
...@@ -13,7 +13,7 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; ...@@ -13,7 +13,7 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
interface Props { interface Props {
data: Address; data: Pick<Address, 'block_number_balance_updated_at' | 'coin_balance' | 'hash' | 'exchange_rate'>;
} }
const AddressBalance = ({ data }: Props) => { const AddressBalance = ({ data }: Props) => {
...@@ -63,10 +63,6 @@ const AddressBalance = ({ data }: Props) => { ...@@ -63,10 +63,6 @@ const AddressBalance = ({ data }: Props) => {
handler: handleNewCoinBalanceMessage, handler: handleNewCoinBalanceMessage,
}); });
if (!data.coin_balance) {
return null;
}
return ( return (
<DetailsInfoItem <DetailsInfoItem
title="Balance" title="Balance"
...@@ -82,7 +78,7 @@ const AddressBalance = ({ data }: Props) => { ...@@ -82,7 +78,7 @@ const AddressBalance = ({ data }: Props) => {
fontSize="sm" fontSize="sm"
/> />
<CurrencyValue <CurrencyValue
value={ data.coin_balance } value={ data.coin_balance || '0' }
exchangeRate={ data.exchange_rate } exchangeRate={ data.exchange_rate }
decimals={ String(appConfig.network.currency.decimals) } decimals={ String(appConfig.network.currency.decimals) }
currency={ appConfig.network.currency.symbol } currency={ appConfig.network.currency.symbol }
......
...@@ -29,7 +29,7 @@ const AddressCounterItem = ({ prop, query, address, onClick }: Props) => { ...@@ -29,7 +29,7 @@ const AddressCounterItem = ({ prop, query, address, onClick }: Props) => {
const data = query.data?.[prop]; const data = query.data?.[prop];
if (query.isError || data === null || data === undefined) { if (query.isError || data === null || data === undefined) {
return <span>no data</span>; return <span>0</span>;
} }
switch (prop) { switch (prop) {
......
...@@ -7,7 +7,7 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; ...@@ -7,7 +7,7 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
interface Props { interface Props {
data: Address; data: Pick<Address, 'name' | 'token' | 'is_contract'>;
} }
const AddressNameInfo = ({ data }: Props) => { const AddressNameInfo = ({ data }: Props) => {
......
...@@ -17,7 +17,11 @@ import useSocketMessage from 'lib/socket/useSocketMessage'; ...@@ -17,7 +17,11 @@ import useSocketMessage from 'lib/socket/useSocketMessage';
import TokenSelectDesktop from './TokenSelectDesktop'; import TokenSelectDesktop from './TokenSelectDesktop';
import TokenSelectMobile from './TokenSelectMobile'; import TokenSelectMobile from './TokenSelectMobile';
const TokenSelect = () => { interface Props {
onClick?: () => void;
}
const TokenSelect = ({ onClick }: Props) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -88,6 +92,7 @@ const TokenSelect = () => { ...@@ -88,6 +92,7 @@ const TokenSelect = () => {
pr="6px" pr="6px"
icon={ <Icon as={ walletIcon } boxSize={ 5 }/> } icon={ <Icon as={ walletIcon } boxSize={ 5 }/> }
as="a" as="a"
onClick={ onClick }
/> />
</NextLink> </NextLink>
</Box> </Box>
......
...@@ -7,6 +7,7 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; ...@@ -7,6 +7,7 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import iconSuccess from 'icons/status/success.svg'; import iconSuccess from 'icons/status/success.svg';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import notEmpty from 'lib/notEmpty'; import notEmpty from 'lib/notEmpty';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated'; import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance'; import AddressCoinBalance from 'ui/address/AddressCoinBalance';
...@@ -37,6 +38,10 @@ const TOKEN_TABS = Object.values(tokenTabsByType); ...@@ -37,6 +38,10 @@ const TOKEN_TABS = Object.values(tokenTabsByType);
const AddressPageContent = () => { const AddressPageContent = () => {
const router = useRouter(); const router = useRouter();
const appProps = useAppContext();
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/accounts');
const tabsScrollRef = React.useRef<HTMLDivElement>(null); const tabsScrollRef = React.useRef<HTMLDivElement>(null);
const addressQuery = useApiQuery('address', { const addressQuery = useApiQuery('address', {
...@@ -113,6 +118,8 @@ const AddressPageContent = () => { ...@@ -113,6 +118,8 @@ const AddressPageContent = () => {
const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null; const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null;
const content = addressQuery.isError ? null : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/>;
return ( return (
<Page> <Page>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
...@@ -121,13 +128,15 @@ const AddressPageContent = () => { ...@@ -121,13 +128,15 @@ const AddressPageContent = () => {
) : ( ) : (
<PageTitle <PageTitle
text={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` } text={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
additionals={ tagsNode } additionalsRight={ tagsNode }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
backLinkLabel="Back to top accounts list"
/> />
) } ) }
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/> <AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up whith pagination */ } { /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ tabsScrollRef }></Box> <Box ref={ tabsScrollRef }></Box>
{ addressQuery.isLoading ? <SkeletonTabs/> : <RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/> } { addressQuery.isLoading ? <SkeletonTabs/> : content }
</Page> </Page>
); );
}; };
......
...@@ -22,6 +22,11 @@ const hooksConfig = { ...@@ -22,6 +22,11 @@ const hooksConfig = {
// FIXME: idk why mobile test doesn't work (it's ok locally) // FIXME: idk why mobile test doesn't work (it's ok locally)
// test('base view +@mobile +@dark-mode', async({ mount, page }) => { // test('base view +@mobile +@dark-mode', async({ mount, page }) => {
test('base view +@dark-mode', async({ mount, page }) => { test('base view +@dark-mode', async({ mount, page }) => {
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
status: 200,
body: '',
}));
await page.route(TOKEN_API_URL, (route) => route.fulfill({ await page.route(TOKEN_API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(tokenInfo), body: JSON.stringify(tokenInfo),
......
import { Skeleton, Box } from '@chakra-ui/react'; import { Skeleton, Box, Flex, SkeletonCircle } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TokenLogo from 'ui/shared/TokenLogo';
import TokenContractInfo from 'ui/token/TokenContractInfo'; import TokenContractInfo from 'ui/token/TokenContractInfo';
import TokenDetails from 'ui/token/TokenDetails'; import TokenDetails from 'ui/token/TokenDetails';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders'; import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
...@@ -23,6 +26,10 @@ const TokenPageContent = () => { ...@@ -23,6 +26,10 @@ const TokenPageContent = () => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const appProps = useAppContext();
const hasGoBackLink = appProps.referrer && appProps.referrer.includes('/tokens');
const scrollRef = React.useRef<HTMLDivElement>(null); const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenQuery = useApiQuery('token', { const tokenQuery = useApiQuery('token', {
...@@ -82,9 +89,24 @@ const TokenPageContent = () => { ...@@ -82,9 +89,24 @@ const TokenPageContent = () => {
return ( return (
<Page> <Page>
{ tokenQuery.isLoading ? { tokenQuery.isLoading ? (
<Skeleton w="500px" h={ 10 } mb={ 6 }/> : <Flex alignItems="center" mb={ 6 }>
<PageTitle text={ `${ tokenQuery.data?.name } (${ tokenQuery.data?.symbol }) token` }/> } <SkeletonCircle w={ 6 } h={ 6 } mr={ 3 }/>
<Skeleton w="500px" h={ 10 }/>
</Flex>
) : (
<>
<TextAd mb={ 6 }/>
<PageTitle
text={ `${ tokenQuery.data?.name } (${ tokenQuery.data?.symbol }) token` }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
backLinkLabel="Back to tokens list"
additionalsLeft={ (
<TokenLogo hash={ tokenQuery.data?.address } name={ tokenQuery.data?.name } boxSize={ 6 }/>
) }
/>
</>
) }
<TokenContractInfo tokenQuery={ tokenQuery }/> <TokenContractInfo tokenQuery={ tokenQuery }/>
<TokenDetails tokenQuery={ tokenQuery }/> <TokenDetails tokenQuery={ tokenQuery }/>
......
...@@ -69,7 +69,7 @@ const TransactionPageContent = () => { ...@@ -69,7 +69,7 @@ const TransactionPageContent = () => {
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
<PageTitle <PageTitle
text="Transaction details" text="Transaction details"
additionals={ additionals } additionalsRight={ additionals }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined } backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
backLinkLabel="Back to transactions list" backLinkLabel="Back to transactions list"
/> />
......
// import { Icon } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
// import plusIcon from 'icons/plus.svg';
import * as textAdMock from 'mocks/ad/textAd'; import * as textAdMock from 'mocks/ad/textAd';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
...@@ -32,6 +34,10 @@ test('default view +@mobile', async({ mount }) => { ...@@ -32,6 +34,10 @@ test('default view +@mobile', async({ mount }) => {
}); });
test('with text ad, back link and addons +@mobile +@dark-mode', async({ mount }) => { test('with text ad, back link and addons +@mobile +@dark-mode', async({ mount }) => {
// https://github.com/microsoft/playwright/issues/15620
// not possible to pass component as a prop in tests
// const left = <Icon as={ plusIcon }/>;
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<PageTitle <PageTitle
...@@ -39,7 +45,8 @@ test('with text ad, back link and addons +@mobile +@dark-mode', async({ mount }) ...@@ -39,7 +45,8 @@ test('with text ad, back link and addons +@mobile +@dark-mode', async({ mount })
withTextAd withTextAd
backLinkLabel="Back" backLinkLabel="Back"
backLinkUrl="back" backLinkUrl="back"
additionals="Privet" // additionalsLeft={ left }
additionalsRight="Privet"
/> />
</TestApp>, </TestApp>,
); );
...@@ -55,7 +62,7 @@ test('long title with text ad, back link and addons +@mobile', async({ mount }) ...@@ -55,7 +62,7 @@ test('long title with text ad, back link and addons +@mobile', async({ mount })
withTextAd withTextAd
backLinkLabel="Back" backLinkLabel="Back"
backLinkUrl="back" backLinkUrl="back"
additionals="Privet, kak dela?" additionalsRight="Privet, kak dela?"
/> />
</TestApp>, </TestApp>,
); );
......
import { Heading, Flex, Tooltip, Icon, chakra } from '@chakra-ui/react'; import { Heading, Flex, Grid, Tooltip, Icon, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import eastArrowIcon from 'icons/arrows/east.svg'; import eastArrowIcon from 'icons/arrows/east.svg';
...@@ -7,23 +7,20 @@ import LinkInternal from 'ui/shared/LinkInternal'; ...@@ -7,23 +7,20 @@ import LinkInternal from 'ui/shared/LinkInternal';
type Props = { type Props = {
text: string; text: string;
additionals?: React.ReactNode; additionalsLeft?: React.ReactNode;
additionalsRight?: React.ReactNode;
withTextAd?: boolean; withTextAd?: boolean;
className?: string; className?: string;
backLinkLabel?: string; backLinkLabel?: string;
backLinkUrl?: string; backLinkUrl?: string;
} }
const PageTitle = ({ text, additionals, withTextAd, backLinkUrl, backLinkLabel, className }: Props) => { const PageTitle = ({ text, additionalsLeft, additionalsRight, withTextAd, backLinkUrl, backLinkLabel, className }: Props) => {
const title = ( const title = (
<Heading <Heading
as="h1" as="h1"
size="lg" size="lg"
flex="none" flex="none"
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
width={ backLinkUrl ? 'calc(100% - 36px)' : '100%' }
> >
{ text } { text }
</Heading> </Heading>
...@@ -40,22 +37,25 @@ const PageTitle = ({ text, additionals, withTextAd, backLinkUrl, backLinkLabel, ...@@ -40,22 +37,25 @@ const PageTitle = ({ text, additionals, withTextAd, backLinkUrl, backLinkLabel,
className={ className } className={ className }
> >
<Flex flexWrap="wrap" columnGap={ 3 } alignItems="center" width={ withTextAd ? 'unset' : '100%' }> <Flex flexWrap="wrap" columnGap={ 3 } alignItems="center" width={ withTextAd ? 'unset' : '100%' }>
<Flex <Grid
flexWrap="nowrap" templateColumns={ [ backLinkUrl && 'auto', additionalsLeft && 'auto', '1fr' ].filter(Boolean).join(' ') }
alignItems="center"
columnGap={ 3 } columnGap={ 3 }
overflow="hidden"
> >
{ backLinkUrl && ( { backLinkUrl && (
<Tooltip label={ backLinkLabel }> <Tooltip label={ backLinkLabel }>
<LinkInternal display="inline-flex" href={ backLinkUrl }> <LinkInternal display="inline-flex" href={ backLinkUrl } h="40px">
<Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)"/> <Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)" margin="auto"/>
</LinkInternal> </LinkInternal>
</Tooltip> </Tooltip>
) } ) }
{ additionalsLeft !== undefined && (
<Flex h="40px" alignItems="center">
{ additionalsLeft }
</Flex>
) }
{ title } { title }
</Flex> </Grid>
{ additionals } { additionalsRight }
</Flex> </Flex>
{ withTextAd && <TextAd flexShrink={ 100 }/> } { withTextAd && <TextAd flexShrink={ 100 }/> }
</Flex> </Flex>
......
...@@ -48,14 +48,14 @@ const TokenTransferListItem = ({ ...@@ -48,14 +48,14 @@ const TokenTransferListItem = ({
const addressWidth = `calc((100% - ${ baseAddress ? '50px' : '0px' }) / 2)`; const addressWidth = `calc((100% - ${ baseAddress ? '50px' : '0px' }) / 2)`;
return ( return (
<ListItemMobile rowGap={ 3 } isAnimated> <ListItemMobile rowGap={ 3 } isAnimated>
<Flex w="100%" flexWrap="wrap" rowGap={ 1 } position="relative"> <Flex w="100%" justifyContent="space-between">
<TokenSnippet hash={ token.address } w="auto" maxW="calc(100% - 140px)" name={ token.name || 'Unnamed token' }/> <Flex flexWrap="wrap" rowGap={ 1 } mr={ showTxInfo && txHash ? 2 : 0 }>
<Tag flexShrink={ 0 } ml={ 2 } mr={ 2 }>{ token.type }</Tag> <TokenSnippet hash={ token.address } w="auto" maxW="calc(100% - 140px)" name={ token.name || 'Unnamed token' }/>
<Tag colorScheme="orange">{ getTokenTransferTypeText(type) }</Tag> <Tag flexShrink={ 0 } ml={ 2 } mr={ 2 }>{ token.type }</Tag>
<Tag colorScheme="orange">{ getTokenTransferTypeText(type) }</Tag>
</Flex>
{ showTxInfo && txHash && ( { showTxInfo && txHash && (
<Flex position="absolute" top={ 0 } right={ 0 }> <TxAdditionalInfo hash={ txHash } isMobile/>
<TxAdditionalInfo hash={ txHash } isMobile/>
</Flex>
) } ) }
</Flex> </Flex>
{ 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> } { 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> }
......
...@@ -69,11 +69,6 @@ const TokenDetails = ({ tokenQuery }: Props) => { ...@@ -69,11 +69,6 @@ const TokenDetails = ({ tokenQuery }: Props) => {
); );
} }
// we show error in parent component, this is only for TS
if (tokenQuery.isError) {
return null;
}
const { const {
exchange_rate: exchangeRate, exchange_rate: exchangeRate,
total_supply: totalSupply, total_supply: totalSupply,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment