Commit 1db67c5b authored by tom goriunov's avatar tom goriunov Committed by GitHub

Page title updates (#1265)

* block page title

* tx page title

* address page title

* token page title

* NFT page title

* [skip ci] update envs for review

* update title for NFT instance page

* update screenshots
parent 973d9496
......@@ -10,7 +10,6 @@ import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_COUNTERS } from 'stubs/address';
import AddressCounterItem from 'ui/address/details/AddressCounterItem';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
......@@ -83,62 +82,76 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
}
return (
<Box>
<AddressHeadingInfo address={ data } token={ data.token } isLoading={ addressQuery.isPlaceholderData } isLinkDisabled/>
<Grid
mt={ 8 }
columnGap={ 8 }
rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
>
<AddressNameInfo data={ data } isLoading={ addressQuery.isPlaceholderData }/>
{ data.is_contract && data.creation_tx_hash && data.creator_address_hash && (
<DetailsInfoItem
title="Creator"
hint="Transaction and address of creation"
<Grid
columnGap={ 8 }
rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
>
<AddressNameInfo data={ data } isLoading={ addressQuery.isPlaceholderData }/>
{ data.is_contract && data.creation_tx_hash && data.creator_address_hash && (
<DetailsInfoItem
title="Creator"
hint="Transaction and address of creation"
isLoading={ addressQuery.isPlaceholderData }
>
<AddressEntity
address={{ hash: data.creator_address_hash }}
truncation="constant"
noIcon
/>
<Text whiteSpace="pre"> at txn </Text>
<TxEntity hash={ data.creation_tx_hash } truncation="constant" noIcon noCopy={ false }/>
</DetailsInfoItem>
) }
{ data.is_contract && data.implementation_address && (
<DetailsInfoItem
title="Implementation"
hint="Implementation address of the proxy contract"
columnGap={ 1 }
>
<AddressEntity
address={{ hash: data.implementation_address, name: data.implementation_name, is_contract: true }}
isLoading={ addressQuery.isPlaceholderData }
>
<AddressEntity
address={{ hash: data.creator_address_hash }}
truncation="constant"
noIcon
/>
<Text whiteSpace="pre"> at txn </Text>
<TxEntity hash={ data.creation_tx_hash } truncation="constant" noIcon noCopy={ false }/>
</DetailsInfoItem>
) }
{ data.is_contract && data.implementation_address && (
<DetailsInfoItem
title="Implementation"
hint="Implementation address of the proxy contract"
columnGap={ 1 }
>
<AddressEntity
address={{ hash: data.implementation_address, name: data.implementation_name, is_contract: true }}
isLoading={ addressQuery.isPlaceholderData }
noIcon
/>
</DetailsInfoItem>
) }
<AddressBalance data={ data } isLoading={ addressQuery.isPlaceholderData }/>
{ data.has_tokens && (
<DetailsInfoItem
title="Tokens"
hint="All tokens in the account and total value"
alignSelf="center"
py={ 0 }
>
{ addressQuery.data ? <TokenSelect onClick={ handleCounterItemClick }/> : <Box py="6px">0</Box> }
</DetailsInfoItem>
) }
noIcon
/>
</DetailsInfoItem>
) }
<AddressBalance data={ data } isLoading={ addressQuery.isPlaceholderData }/>
{ data.has_tokens && (
<DetailsInfoItem
title="Transactions"
hint="Number of transactions related to this address"
title="Tokens"
hint="All tokens in the account and total value"
alignSelf="center"
py={ 0 }
>
{ addressQuery.data ? <TokenSelect onClick={ handleCounterItemClick }/> : <Box py="6px">0</Box> }
</DetailsInfoItem>
) }
<DetailsInfoItem
title="Transactions"
hint="Number of transactions related to this address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ? (
<AddressCounterItem
prop="transactions_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 }
</DetailsInfoItem>
{ data.has_token_transfers && (
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ? (
<AddressCounterItem
prop="transactions_count"
prop="token_transfers_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
......@@ -147,32 +160,32 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
) :
0 }
</DetailsInfoItem>
{ data.has_token_transfers && (
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ? (
<AddressCounterItem
prop="token_transfers_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 }
</DetailsInfoItem>
) }
) }
<DetailsInfoItem
title="Gas used"
hint="Gas used by the address"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ? (
<AddressCounterItem
prop="gas_usage_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 }
</DetailsInfoItem>
{ data.has_validated_blocks && (
<DetailsInfoItem
title="Gas used"
hint="Gas used by the address"
title="Blocks validated"
hint="Number of blocks validated by this validator"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ? (
<AddressCounterItem
prop="gas_usage_count"
prop="validations_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
......@@ -181,41 +194,23 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
) :
0 }
</DetailsInfoItem>
{ data.has_validated_blocks && (
<DetailsInfoItem
title="Blocks validated"
hint="Number of blocks validated by this validator"
isLoading={ addressQuery.isPlaceholderData || countersQuery.isPlaceholderData }
>
{ addressQuery.data ? (
<AddressCounterItem
prop="validations_count"
query={ countersQuery }
address={ data.hash }
onClick={ handleCounterItemClick }
isAddressQueryLoading={ addressQuery.isPlaceholderData }
/>
) :
0 }
</DetailsInfoItem>
) }
{ data.block_number_balance_updated_at && (
<DetailsInfoItem
title="Last balance update"
hint="Block number in which the address was updated"
alignSelf="center"
py={{ base: '2px', lg: 1 }}
) }
{ data.block_number_balance_updated_at && (
<DetailsInfoItem
title="Last balance update"
hint="Block number in which the address was updated"
alignSelf="center"
py={{ base: '2px', lg: 1 }}
isLoading={ addressQuery.isPlaceholderData }
>
<BlockEntity
number={ data.block_number_balance_updated_at }
isLoading={ addressQuery.isPlaceholderData }
>
<BlockEntity
number={ data.block_number_balance_updated_at }
isLoading={ addressQuery.isPlaceholderData }
/>
</DetailsInfoItem>
) }
<DetailsSponsoredItem isLoading={ addressQuery.isPlaceholderData }/>
</Grid>
</Box>
/>
</DetailsInfoItem>
) }
<DetailsSponsoredItem isLoading={ addressQuery.isPlaceholderData }/>
</Grid>
);
};
......
......@@ -3,6 +3,7 @@ import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import config from 'configs/app';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import { getResourceKey } from 'lib/api/useApiQuery';
......@@ -15,7 +16,7 @@ import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
interface Props {
className?: string;
hash: string;
watchListId: number | null;
watchListId: number | null | undefined;
}
const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
......@@ -24,6 +25,7 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
const queryClient = useQueryClient();
const router = useRouter();
const isAccountActionAllowed = useIsAccountActionAllowed();
const onFocusCapture = usePreventFocusAfterModalClosing();
const handleClick = React.useCallback(() => {
if (!isAccountActionAllowed()) {
......@@ -54,6 +56,10 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
};
}, [ hash, watchListId ]);
if (!config.features.account.isEnabled) {
return null;
}
return (
<>
<Tooltip label={ `${ watchListId ? 'Remove address from Watch list' : 'Add address to Watch list' }` }>
......@@ -68,7 +74,7 @@ const AddressFavoriteButton = ({ className, hash, watchListId }: Props) => {
flexShrink={ 0 }
onClick={ handleClick }
icon={ <Icon as={ watchListId ? starFilledIcon : starOutlineIcon } boxSize={ 5 }/> }
onFocusCapture={ usePreventFocusAfterModalClosing() }
onFocusCapture={ onFocusCapture }
/>
</Tooltip>
<WatchlistAddModal
......
......@@ -79,6 +79,7 @@ const AddressQrCode = ({ address, className, isLoading }: Props) => {
pr="6px"
onClick={ onOpen }
icon={ <Icon as={ qrCodeIcon } boxSize={ 5 }/> }
flexShrink={ 0 }
/>
</Tooltip>
......
import { Box, Icon } from '@chakra-ui/react';
import { Box, Flex, Icon } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -10,7 +10,6 @@ import iconSuccess from 'icons/status/success.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import useContractTabs from 'lib/hooks/useContractTabs';
import useIsMobile from 'lib/hooks/useIsMobile';
import useIsSafeAddress from 'lib/hooks/useIsSafeAddress';
import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_INFO, ADDRESS_TABS_COUNTERS } from 'stubs/address';
......@@ -24,7 +23,12 @@ import AddressTokens from 'ui/address/AddressTokens';
import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
import AddressTxs from 'ui/address/AddressTxs';
import AddressWithdrawals from 'ui/address/AddressWithdrawals';
import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton';
import AddressQrCode from 'ui/address/details/AddressQrCode';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu';
import TextAd from 'ui/shared/ad/TextAd';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import EntityTags from 'ui/shared/EntityTags';
import NetworkExplorers from 'ui/shared/NetworkExplorers';
import PageTitle from 'ui/shared/Page/PageTitle';
......@@ -41,7 +45,6 @@ const TOKEN_TABS = Object.values(tokenTabsByType);
const AddressPageContent = () => {
const router = useRouter();
const isMobile = useIsMobile();
const appProps = useAppContext();
const tabsScrollRef = React.useRef<HTMLDivElement>(null);
......@@ -148,14 +151,11 @@ const AddressPageContent = () => {
data={ addressQuery.data }
isLoading={ addressQuery.isPlaceholderData }
tagsBefore={ [
addressQuery.data?.is_contract ? { label: 'contract', display_name: 'Contract' } : { label: 'eoa', display_name: 'EOA' },
!addressQuery.data?.is_contract ? { label: 'eoa', display_name: 'EOA' } : undefined,
addressQuery.data?.implementation_address ? { label: 'proxy', display_name: 'Proxy' } : undefined,
addressQuery.data?.token ? { label: 'token', display_name: 'Token' } : undefined,
isSafeAddress ? { label: 'safe', display_name: 'Multisig: Safe' } : undefined,
] }
contentAfter={
<NetworkExplorers type="address" pathParam={ hash } ml="auto" hideText={ isMobile }/>
}
/>
);
......@@ -174,6 +174,30 @@ const AddressPageContent = () => {
};
}, [ appProps.referrer ]);
const isLoading = addressQuery.isPlaceholderData;
const titleSecondRow = (
<Flex alignItems="center" w="100%" columnGap={ 2 } rowGap={ 2 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
<AddressEntity
address={{ ...addressQuery.data, name: '' }}
isLoading={ isLoading }
fontFamily="heading"
fontSize="lg"
fontWeight={ 500 }
noLink
isSafeAddress={ isSafeAddress }
/>
{ !isLoading && addressQuery.data?.is_contract && addressQuery.data.token &&
<AddressAddToWallet token={ addressQuery.data.token } variant="button"/> }
{ !isLoading && !addressQuery.data?.is_contract && config.features.account.isEnabled && (
<AddressFavoriteButton hash={ hash } watchListId={ addressQuery.data?.watchlist_address_id }/>
) }
<AddressQrCode address={ addressQuery.data } isLoading={ isLoading }/>
<AccountActionsMenu isLoading={ isLoading }/>
<NetworkExplorers type="address" pathParam={ hash } ml="auto"/>
</Flex>
);
return (
<>
<TextAd mb={ 6 }/>
......@@ -181,7 +205,8 @@ const AddressPageContent = () => {
title={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
backLink={ backLink }
contentAfter={ tags }
isLoading={ addressQuery.isPlaceholderData }
secondRow={ titleSecondRow }
isLoading={ isLoading }
/>
<AddressDetails addressQuery={ addressQuery } scrollRef={ tabsScrollRef }/>
{ /* should stay before tabs to scroll up with pagination */ }
......
import { chakra, Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -16,6 +17,7 @@ import { WITHDRAWAL } from 'stubs/withdrawals';
import BlockDetails from 'ui/block/BlockDetails';
import BlockWithdrawals from 'ui/block/BlockWithdrawals';
import TextAd from 'ui/shared/ad/TextAd';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import NetworkExplorers from 'ui/shared/NetworkExplorers';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
......@@ -112,6 +114,24 @@ const BlockPageContent = () => {
}, [ appProps.referrer ]);
const title = blockQuery.data?.type === 'reorg' ? `Reorged block #${ blockQuery.data?.height }` : `Block #${ blockQuery.data?.height }`;
const titleSecondRow = (
<>
<Skeleton
isLoaded={ !blockQuery.isPlaceholderData }
fontFamily="heading"
display="flex"
minW={ 0 }
columnGap={ 2 }
fontWeight={ 500 }
>
<chakra.span flexShrink={ 0 }>
{ config.chain.verificationType === 'validation' ? 'Validated by' : 'Mined by' }
</chakra.span>
<AddressEntity address={ blockQuery.data?.miner }/>
</Skeleton>
<NetworkExplorers type="block" pathParam={ heightOrHash } ml={{ base: 3, lg: 'auto' }}/>
</>
);
return (
<>
......@@ -119,7 +139,7 @@ const BlockPageContent = () => {
<PageTitle
title={ title }
backLink={ backLink }
contentAfter={ <NetworkExplorers type="block" pathParam={ heightOrHash } ml={{ base: 'initial', lg: 'auto' }}/> }
secondRow={ titleSecondRow }
isLoading={ blockQuery.isPlaceholderData }
/>
{ blockQuery.isPlaceholderData ? <TabsSkeleton tabs={ tabs }/> : (
......
import { Box, Icon, Tooltip } from '@chakra-ui/react';
import { Box, Flex, Icon, Tooltip } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React, { useEffect } from 'react';
......@@ -23,8 +23,11 @@ import * as addressStubs from 'stubs/address';
import * as tokenStubs from 'stubs/token';
import { generateListStub } from 'stubs/utils';
import AddressContract from 'ui/address/AddressContract';
import AddressQrCode from 'ui/address/details/AddressQrCode';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu';
import TextAd from 'ui/shared/ad/TextAd';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EntityTags from 'ui/shared/EntityTags';
import NetworkExplorers from 'ui/shared/NetworkExplorers';
......@@ -261,41 +264,56 @@ const TokenPageContent = () => {
[ { label: verifiedInfoQuery.data.projectSector, display_name: verifiedInfoQuery.data.projectSector } ] :
undefined
}
contentAfter={
<NetworkExplorers type="token" pathParam={ hashString } ml="auto" hideText={ isMobile }/>
}
flexGrow={ 1 }
/>
</>
);
const isLoading = tokenQuery.isPlaceholderData || contractQuery.isPlaceholderData;
const titleSecondRow = (
<Flex alignItems="center" w="100%" minW={ 0 } columnGap={ 2 } rowGap={ 2 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
<AddressEntity
address={{ ...contractQuery.data, name: '' }}
isLoading={ isLoading }
fontFamily="heading"
fontSize="lg"
fontWeight={ 500 }
/>
{ !isLoading && tokenQuery.data && <AddressAddToWallet token={ tokenQuery.data } variant="button"/> }
<AddressQrCode address={ contractQuery.data } isLoading={ isLoading }/>
<AccountActionsMenu isLoading={ isLoading }/>
<Flex ml={{ base: 0, lg: 'auto' }} columnGap={ 2 } flexGrow={{ base: 1, lg: 0 }}>
<TokenVerifiedInfo verifiedInfoQuery={ verifiedInfoQuery }/>
<NetworkExplorers type="token" pathParam={ hashString } ml={{ base: 'auto', lg: 0 }}/>
</Flex>
</Flex>
);
return (
<>
<TextAd mb={ 6 }/>
<PageTitle
title={ `${ tokenQuery.data?.name || 'Unnamed token' }${ tokenSymbolText }` }
isLoading={ tokenQuery.isPlaceholderData }
isLoading={ isLoading }
backLink={ backLink }
beforeTitle={ tokenQuery.data ? (
<TokenEntity.Icon
token={ tokenQuery.data }
isLoading={ tokenQuery.isPlaceholderData }
isLoading={ isLoading }
iconSize="lg"
/>
) : null }
contentAfter={ titleContentAfter }
secondRow={ titleSecondRow }
/>
<AddressHeadingInfo
address={ contractQuery.data }
token={ tokenQuery.data }
isLoading={ tokenQuery.isPlaceholderData || contractQuery.isPlaceholderData }
/>
<TokenVerifiedInfo verifiedInfoQuery={ verifiedInfoQuery }/>
<TokenDetails tokenQuery={ tokenQuery }/>
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box>
{ tokenQuery.isPlaceholderData || contractQuery.isPlaceholderData ?
{ isLoading ?
<TabsSkeleton tabs={ tabs }/> :
(
<RoutedTabs
......
import { Box, Icon, Skeleton } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { PaginationParams } from 'ui/shared/pagination/types';
import type { RoutedTab } from 'ui/shared/Tabs/types';
import nftIcon from 'icons/nft_shield.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import useIsMobile from 'lib/hooks/useIsMobile';
......@@ -14,9 +13,12 @@ import * as regexp from 'lib/regexp';
import { TOKEN_INSTANCE } from 'stubs/token';
import * as tokenStubs from 'stubs/token';
import { generateListStub } from 'stubs/utils';
import AddressQrCode from 'ui/address/details/AddressQrCode';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu';
import TextAd from 'ui/shared/ad/TextAd';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import Tag from 'ui/shared/chakra/Tag';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import LinkExternal from 'ui/shared/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
......@@ -119,10 +121,8 @@ const TokenInstanceContent = () => {
throw Error('Token instance fetch failed', { cause: tokenInstanceQuery.error });
}
const nftShieldIcon = tokenInstanceQuery.isPlaceholderData ?
<Skeleton boxSize={ 6 } display="inline-block" borderRadius="base" mr={ 2 } my={ 2 } verticalAlign="text-bottom"/> :
<Icon as={ nftIcon } boxSize={ 6 } mr={ 2 }/>;
const tokenTag = <Tag isLoading={ tokenInstanceQuery.isPlaceholderData }>{ tokenInstanceQuery.data?.token.type }</Tag>;
const address = {
hash: hash || '',
is_contract: true,
......@@ -130,6 +130,9 @@ const TokenInstanceContent = () => {
watchlist_names: [],
watchlist_address_id: null,
};
const isLoading = tokenInstanceQuery.isPlaceholderData;
const appLink = (() => {
if (!tokenInstanceQuery.data?.external_app_url) {
return null;
......@@ -141,20 +144,15 @@ const TokenInstanceContent = () => {
new URL('https://' + tokenInstanceQuery.data.external_app_url);
return (
<Skeleton isLoaded={ !tokenInstanceQuery.isPlaceholderData } display="inline-block" fontSize="sm" mt={ 6 }>
<span>View in app </span>
<LinkExternal href={ url.toString() }>
{ url.hostname || tokenInstanceQuery.data.external_app_url }
</LinkExternal>
</Skeleton>
<LinkExternal href={ url.toString() } variant="subtle" isLoading={ isLoading } ml={{ base: 0, lg: 'auto' }}>
{ url.hostname || tokenInstanceQuery.data.external_app_url }
</LinkExternal>
);
} catch (error) {
return (
<Box fontSize="sm" mt={ 6 }>
<LinkExternal href={ tokenInstanceQuery.data.external_app_url }>
<LinkExternal href={ tokenInstanceQuery.data.external_app_url } isLoading={ isLoading } ml={{ base: 0, lg: 'auto' }}>
View in app
</LinkExternal>
</Box>
</LinkExternal>
);
}
})();
......@@ -167,27 +165,44 @@ const TokenInstanceContent = () => {
pagination = holdersQuery.pagination;
}
const titleSecondRow = (
<Flex alignItems="center" w="100%" minW={ 0 } columnGap={ 2 } rowGap={ 2 } flexWrap={{ base: 'wrap', lg: 'nowrap' }}>
<TokenEntity
token={ tokenInstanceQuery.data?.token }
isLoading={ isLoading }
noSymbol
noCopy
jointSymbol
fontFamily="heading"
fontSize="lg"
fontWeight={ 500 }
w="auto"
maxW="700px"
/>
{ !isLoading && tokenInstanceQuery.data && <AddressAddToWallet token={ tokenInstanceQuery.data.token } variant="button"/> }
<AddressQrCode address={ address } isLoading={ isLoading }/>
<AccountActionsMenu isLoading={ isLoading }/>
{ appLink }
</Flex>
);
return (
<>
<TextAd mb={ 6 }/>
<PageTitle
title={ `${ tokenInstanceQuery.data?.token.name || 'Unnamed token' } #${ tokenInstanceQuery.data?.id }` }
title={ `ID ${ tokenInstanceQuery.data?.id }` }
backLink={ backLink }
beforeTitle={ nftShieldIcon }
contentAfter={ tokenTag }
isLoading={ tokenInstanceQuery.isPlaceholderData }
secondRow={ titleSecondRow }
isLoading={ isLoading }
/>
<AddressHeadingInfo address={ address } token={ tokenInstanceQuery.data?.token } isLoading={ tokenInstanceQuery.isPlaceholderData }/>
{ appLink }
<TokenInstanceDetails data={ tokenInstanceQuery?.data } isLoading={ tokenInstanceQuery.isPlaceholderData } scrollRef={ scrollRef }/>
<TokenInstanceDetails data={ tokenInstanceQuery?.data } isLoading={ isLoading } scrollRef={ scrollRef }/>
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box>
{ tokenInstanceQuery.isPlaceholderData ? <TabsSkeleton tabs={ tabs }/> : (
{ isLoading ? <TabsSkeleton tabs={ tabs }/> : (
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
......
......@@ -6,10 +6,11 @@ import type { RoutedTab } from 'ui/shared/Tabs/types';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString';
import { TX } from 'stubs/tx';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu';
import TextAd from 'ui/shared/ad/TextAd';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import EntityTags from 'ui/shared/EntityTags';
import NetworkExplorers from 'ui/shared/NetworkExplorers';
import PageTitle from 'ui/shared/Page/PageTitle';
......@@ -27,7 +28,6 @@ import TxTokenTransfer from 'ui/tx/TxTokenTransfer';
const TransactionPageContent = () => {
const router = useRouter();
const appProps = useAppContext();
const isMobile = useIsMobile();
const hash = getQueryParamString(router.query.hash);
......@@ -57,9 +57,6 @@ const TransactionPageContent = () => {
<EntityTags
isLoading={ isPlaceholderData }
tagsBefore={ [ data?.tx_tag ? { label: data.tx_tag, display_name: data.tx_tag } : undefined ] }
contentAfter={
<NetworkExplorers type="tx" pathParam={ hash } ml={{ base: 'initial', lg: 'auto' }} hideText={ isMobile && Boolean(data?.tx_tag) }/>
}
/>
);
......@@ -76,6 +73,14 @@ const TransactionPageContent = () => {
};
}, [ appProps.referrer ]);
const titleSecondRow = (
<>
<TxEntity hash={ hash } noLink noCopy={ false } fontWeight={ 500 } mr={ 2 } fontFamily="heading"/>
{ !data?.tx_tag && <AccountActionsMenu mr={{ base: 0, lg: 3 }}/> }
<NetworkExplorers type="tx" pathParam={ hash } ml={{ base: 3, lg: 'auto' }}/>
</>
);
return (
<>
<TextAd mb={ 6 }/>
......@@ -83,6 +88,7 @@ const TransactionPageContent = () => {
title="Transaction details"
backLink={ backLink }
contentAfter={ tags }
secondRow={ titleSecondRow }
/>
{ isPlaceholderData ? (
<>
......
......@@ -21,7 +21,7 @@ import TransactionInput from 'ui/shared/TransactionInput';
const TAG_MAX_LENGTH = 35;
type Props = {
data?: TransactionTag;
data?: Partial<TransactionTag>;
onClose: () => void;
onSuccess: () => Promise<void>;
setAlertVisible: (isAlertVisible: boolean) => void;
......
......@@ -11,10 +11,11 @@ import TransactionForm from './TransactionForm';
type Props = {
isOpen: boolean;
onClose: () => void;
data?: TransactionTag;
onSuccess?: () => Promise<void>;
data?: Partial<TransactionTag>;
}
const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const AddressModal: React.FC<Props> = ({ isOpen, onClose, onSuccess, data }) => {
const title = data ? 'Edit transaction tag' : 'New transaction tag';
const text = !data ? 'Label any transaction with a private transaction tag (up to 35 chars) to customize your explorer experience.' : '';
......@@ -28,13 +29,14 @@ const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
}, [ data?.id, isOpen ]);
const handleSuccess = React.useCallback(async() => {
onSuccess?.();
if (!data?.id) {
mixpanel.logEvent(
mixpanel.EventTypes.PRIVATE_TAG,
{ Action: 'Submit', 'Page type': PAGE_TYPE_DICT['/account/tag-address'], 'Tag type': 'Tx' },
);
}
}, [ data?.id ]);
}, [ data?.id, onSuccess ]);
const renderForm = useCallback(() => {
return <TransactionForm data={ data } onClose={ onClose } onSuccess={ handleSuccess } setAlertVisible={ setAlertVisible }/>;
......
import { Button, Menu, MenuButton, MenuList, Icon, Flex, Skeleton } from '@chakra-ui/react';
import { Button, Menu, MenuButton, MenuList, Icon, Flex, Skeleton, chakra } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
......@@ -8,28 +8,34 @@ import useIsAccountActionAllowed from 'lib/hooks/useIsAccountActionAllowed';
import * as mixpanel from 'lib/mixpanel/index';
import getQueryParamString from 'lib/router/getQueryParamString';
import PrivateTagMenuItem from './PrivateTagMenuItem';
import PublicTagMenuItem from './PublicTagMenuItem';
import TokenInfoMenuItem from './TokenInfoMenuItem';
import PrivateTagMenuItem from './items/PrivateTagMenuItem';
import PublicTagMenuItem from './items/PublicTagMenuItem';
import TokenInfoMenuItem from './items/TokenInfoMenuItem';
interface Props {
isLoading?: boolean;
className?: string;
}
const AddressActions = ({ isLoading }: Props) => {
const AccountActionsMenu = ({ isLoading, className }: Props) => {
const router = useRouter();
const hash = getQueryParamString(router.query.hash);
const isTokenPage = router.pathname === '/token/[hash]';
const isTxPage = router.pathname === '/tx/[hash]';
const isAccountActionAllowed = useIsAccountActionAllowed();
const handleButtonClick = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.PAGE_WIDGET, { Type: 'Address actions (more button)' });
}, []);
if (!config.features.account.isEnabled) {
return null;
}
return (
<Menu>
<Skeleton isLoaded={ !isLoading } borderRadius="base">
<Skeleton isLoaded={ !isLoading } borderRadius="base" className={ className }>
<MenuButton
as={ Button }
size="sm"
......@@ -45,11 +51,17 @@ const AddressActions = ({ isLoading }: Props) => {
<MenuList minWidth="180px" zIndex="popover">
{ isTokenPage && config.features.addressVerification.isEnabled &&
<TokenInfoMenuItem py={ 2 } px={ 4 } hash={ hash } onBeforeClick={ isAccountActionAllowed }/> }
<PrivateTagMenuItem py={ 2 } px={ 4 } hash={ hash } onBeforeClick={ isAccountActionAllowed }/>
<PublicTagMenuItem py={ 2 } px={ 4 } hash={ hash } onBeforeClick={ isAccountActionAllowed }/>
<PrivateTagMenuItem
py={ 2 }
px={ 4 }
hash={ hash }
onBeforeClick={ isAccountActionAllowed }
type={ isTxPage ? 'tx' : 'address' }
/>
{ !isTxPage && <PublicTagMenuItem py={ 2 } px={ 4 } hash={ hash } onBeforeClick={ isAccountActionAllowed }/> }
</MenuList>
</Menu>
);
};
export default React.memo(AddressActions);
export default React.memo(chakra(AccountActionsMenu));
......@@ -4,25 +4,28 @@ import { useRouter } from 'next/router';
import React from 'react';
import type { Address } from 'types/api/address';
import type { Transaction } from 'types/api/transaction';
import iconPrivateTags from 'icons/privattags.svg';
import { getResourceKey } from 'lib/api/useApiQuery';
import getPageType from 'lib/mixpanel/getPageType';
import PrivateTagModal from 'ui/privateTags/AddressModal/AddressModal';
import AddressModal from 'ui/privateTags/AddressModal/AddressModal';
import TransactionModal from 'ui/privateTags/TransactionModal/TransactionModal';
interface Props {
className?: string;
hash: string;
onBeforeClick: () => boolean;
type?: 'address' | 'tx';
}
const PrivateTagMenuItem = ({ className, hash, onBeforeClick }: Props) => {
const PrivateTagMenuItem = ({ className, hash, onBeforeClick, type = 'address' }: Props) => {
const modal = useDisclosure();
const queryClient = useQueryClient();
const router = useRouter();
const queryKey = getResourceKey('address', { pathParams: { hash } });
const addressData = queryClient.getQueryData<Address>(queryKey);
const queryKey = getResourceKey(type === 'tx' ? 'tx' : 'address', { pathParams: { hash } });
const queryData = queryClient.getQueryData<Address | Transaction>(queryKey);
const handleClick = React.useCallback(() => {
if (!onBeforeClick()) {
......@@ -37,17 +40,23 @@ const PrivateTagMenuItem = ({ className, hash, onBeforeClick }: Props) => {
modal.onClose();
}, [ queryClient, queryKey, modal ]);
const formData = React.useMemo(() => {
return {
address_hash: hash,
};
}, [ hash ]);
if (addressData?.private_tags?.length) {
if (
queryData &&
(
('private_tags' in queryData && queryData.private_tags?.length) ||
('tx_tag' in queryData && queryData.tx_tag)
)
) {
return null;
}
const pageType = getPageType(router.pathname);
const modalProps = {
isOpen: modal.isOpen,
onClose: modal.onClose,
onSuccess: handleAddPrivateTag,
pageType,
};
return (
<>
......@@ -55,13 +64,10 @@ const PrivateTagMenuItem = ({ className, hash, onBeforeClick }: Props) => {
<Icon as={ iconPrivateTags } boxSize={ 6 } mr={ 2 }/>
<span>Add private tag</span>
</MenuItem>
<PrivateTagModal
data={ formData }
pageType={ pageType }
isOpen={ modal.isOpen }
onClose={ modal.onClose }
onSuccess={ handleAddPrivateTag }
/>
{ type === 'tx' ?
<TransactionModal { ...modalProps } data={{ transaction_hash: hash }}/> :
<AddressModal { ...modalProps } data={{ address_hash: hash }}/>
}
</>
);
};
......
import { Flex } from '@chakra-ui/react';
import React from 'react';
import type { Address } from 'types/api/address';
import type { TokenInfo } from 'types/api/token';
import config from 'configs/app';
import useIsSafeAddress from 'lib/hooks/useIsSafeAddress';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton';
import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import AddressActionsMenu from 'ui/shared/AddressActions/Menu';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import LinkExternal from 'ui/shared/LinkExternal';
interface Props {
address?: Pick<Address, 'hash' | 'is_contract' | 'implementation_name' | 'watchlist_names' | 'watchlist_address_id'>;
token?: TokenInfo | null;
isLinkDisabled?: boolean;
isLoading?: boolean;
}
const AddressHeadingInfo = ({ address, token, isLinkDisabled, isLoading }: Props) => {
const isSafeAddress = useIsSafeAddress(!isLoading && address?.is_contract ? address.hash : undefined);
if (!address) {
return null;
}
const tokenOriginalLink = (() => {
const feature = config.features.bridgedTokens;
if (!token?.foreign_address || !token.origin_chain_id || !feature.isEnabled) {
return null;
}
const chainBaseUrl = feature.chains.find(({ id }) => id === token.origin_chain_id)?.base_url;
if (!chainBaseUrl) {
return null;
}
try {
const url = new URL(stripTrailingSlash(chainBaseUrl) + '/' + token.foreign_address);
return (
<LinkExternal href={ url } variant="subtle" ml="auto">
Original token
</LinkExternal>
);
} catch (error) {
return null;
}
})();
return (
<Flex alignItems="center" flexWrap={{ base: tokenOriginalLink ? 'wrap' : 'nowrap', lg: 'nowrap' }} rowGap={ 3 } columnGap={ 2 }>
<AddressEntity
address={{ ...address, name: '' }}
isLoading={ isLoading }
fontFamily="heading"
fontSize="lg"
fontWeight={ 500 }
noLink={ isLinkDisabled }
isSafeAddress={ isSafeAddress }
/>
{ !isLoading && address?.is_contract && token && <AddressAddToWallet token={ token }/> }
{ !isLoading && !address.is_contract && config.features.account.isEnabled && (
<AddressFavoriteButton hash={ address.hash } watchListId={ address.watchlist_address_id }/>
) }
<AddressQrCode address={ address } isLoading={ isLoading } flexShrink={ 0 }/>
{ config.features.account.isEnabled && <AddressActionsMenu isLoading={ isLoading }/> }
{ tokenOriginalLink }
</Flex>
);
};
export default AddressHeadingInfo;
......@@ -16,25 +16,42 @@ const LinkExternal = ({ href, children, className, isLoading, variant }: Props)
const subtleLinkBg = useColorModeValue('gray.100', 'gray.700');
const styleProps: ChakraProps = (() => {
const commonProps = {
fontSize: 'sm',
lineHeight: 5,
display: 'inline-block',
alignItems: 'center',
};
switch (variant) {
case 'subtle': {
return {
...commonProps,
px: '10px',
py: '5px',
py: '6px',
bgColor: subtleLinkBg,
borderRadius: 'base',
};
}
default:{
return {};
return commonProps;
}
}
})();
if (isLoading) {
if (variant === 'subtle') {
return (
<Skeleton className={ className } { ...styleProps } bgColor="inherit">
{ children }
<Box boxSize={ 4 } display="inline-block"/>
</Skeleton>
);
}
return (
<Box className={ className } { ...styleProps } fontSize="sm" lineHeight={ 5 } display="inline-block" alignItems="center">
<Box className={ className } { ...styleProps }>
{ children }
<Skeleton boxSize={ 4 } verticalAlign="middle" display="inline-block"/>
</Box>
......@@ -42,7 +59,7 @@ const LinkExternal = ({ href, children, className, isLoading, variant }: Props)
}
return (
<Link className={ className } { ...styleProps } fontSize="sm" lineHeight={ 5 } display="inline-block" alignItems="center" target="_blank" href={ href }>
<Link className={ className } { ...styleProps } target="_blank" href={ href }>
{ children }
<Icon as={ arrowIcon } boxSize={ 4 } verticalAlign="middle" color="gray.400"/>
</Link>
......
......@@ -12,10 +12,9 @@ interface Props {
className?: string;
type: keyof TNetworkExplorer['paths'];
pathParam: string;
hideText?: boolean;
}
const NetworkExplorers = ({ className, type, pathParam, hideText }: Props) => {
const NetworkExplorers = ({ className, type, pathParam }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const explorersLinks = config.UI.explorers.items
......@@ -41,11 +40,11 @@ const NetworkExplorers = ({ className, type, pathParam, hideText }: Props) => {
aria-label="Verify in other explorers"
fontWeight={ 500 }
px={ 2 }
h="30px"
h="32px"
flexShrink={ 0 }
>
<Icon as={ explorerIcon } boxSize={ 5 } mr={ hideText ? 0 : 1 }/>
{ !hideText && <span>Explorers</span> }
<Icon as={ arrowIcon } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 } ml={ 1 }/>
<Icon as={ explorerIcon } boxSize={ 5 }/>
<Icon as={ arrowIcon } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 }/>
</Button>
</PopoverTrigger>
<PopoverContent w="240px">
......
......@@ -16,6 +16,7 @@ type Props = {
beforeTitle?: React.ReactNode;
afterTitle?: React.ReactNode;
contentAfter?: React.ReactNode;
secondRow?: React.ReactNode;
isLoading?: boolean;
withTextAd?: boolean;
}
......@@ -31,7 +32,7 @@ const BackLink = (props: BackLinkProp & { isLoading?: boolean }) => {
return <Skeleton boxSize={ 6 } display="inline-block" borderRadius="base" mr={ 3 } my={ 2 } verticalAlign="text-bottom" isLoaded={ !props.isLoading }/>;
}
const icon = <Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)" margin="auto"/>;
const icon = <Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)" margin="auto" color="gray.400"/>;
if ('url' in props) {
return (
......@@ -52,7 +53,7 @@ const BackLink = (props: BackLinkProp & { isLoading?: boolean }) => {
);
};
const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoading, afterTitle, beforeTitle }: Props) => {
const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoading, afterTitle, beforeTitle, secondRow }: Props) => {
const tooltip = useDisclosure();
const isMobile = useIsMobile();
const [ isTextTruncated, setIsTextTruncated ] = React.useState(false);
......@@ -90,57 +91,62 @@ const PageTitle = ({ title, contentAfter, withTextAd, backLink, className, isLoa
}, [ updatedTruncateState ]);
return (
<Flex
className={ className }
mb={ 6 }
flexDir="row"
flexWrap="wrap"
rowGap={ 3 }
columnGap={ 3 }
alignItems="center"
>
<Flex h={{ base: 'auto', lg: isLoading ? 10 : 'auto' }} maxW="100%" alignItems="center">
{ backLink && <BackLink { ...backLink } isLoading={ isLoading }/> }
{ beforeTitle }
<Skeleton
isLoaded={ !isLoading }
overflow="hidden"
>
<Tooltip
label={ title }
isOpen={ tooltip.isOpen }
onClose={ tooltip.onClose }
maxW={{ base: 'calc(100vw - 32px)', lg: '500px' }}
closeOnScroll={ isMobile ? true : false }
isDisabled={ !isTextTruncated }
<Flex className={ className } flexDir="column" rowGap={ 3 } mb={ 6 }>
<Flex
flexDir="row"
flexWrap="wrap"
rowGap={ 3 }
columnGap={ 3 }
alignItems="center"
>
<Flex h={{ base: 'auto', lg: isLoading ? 10 : 'auto' }} maxW="100%" alignItems="center">
{ backLink && <BackLink { ...backLink } isLoading={ isLoading }/> }
{ beforeTitle }
<Skeleton
isLoaded={ !isLoading }
overflow="hidden"
>
<Heading
ref={ headingRef }
as="h1"
size="lg"
whiteSpace="normal"
wordBreak="break-all"
style={{
WebkitLineClamp: TEXT_MAX_LINES,
WebkitBoxOrient: 'vertical',
display: '-webkit-box',
}}
overflow="hidden"
textOverflow="ellipsis"
onMouseEnter={ tooltip.onOpen }
onMouseLeave={ tooltip.onClose }
onClick={ isMobile ? tooltip.onToggle : undefined }
<Tooltip
label={ title }
isOpen={ tooltip.isOpen }
onClose={ tooltip.onClose }
maxW={{ base: 'calc(100vw - 32px)', lg: '500px' }}
closeOnScroll={ isMobile ? true : false }
isDisabled={ !isTextTruncated }
>
<span ref={ textRef }>
{ title }
</span>
</Heading>
</Tooltip>
</Skeleton>
{ afterTitle }
<Heading
ref={ headingRef }
as="h1"
size="lg"
whiteSpace="normal"
wordBreak="break-all"
style={{
WebkitLineClamp: TEXT_MAX_LINES,
WebkitBoxOrient: 'vertical',
display: '-webkit-box',
}}
overflow="hidden"
textOverflow="ellipsis"
onMouseEnter={ tooltip.onOpen }
onMouseLeave={ tooltip.onClose }
onClick={ isMobile ? tooltip.onToggle : undefined }
>
<span ref={ textRef }>
{ title }
</span>
</Heading>
</Tooltip>
</Skeleton>
{ afterTitle }
</Flex>
{ contentAfter }
{ withTextAd && <TextAd order={{ base: -1, lg: 100 }} mb={{ base: 6, lg: 0 }} ml="auto" w={{ base: '100%', lg: 'auto' }}/> }
</Flex>
{ contentAfter }
{ withTextAd && <TextAd order={{ base: -1, lg: 100 }} mb={{ base: 6, lg: 0 }} ml="auto" w={{ base: '100%', lg: 'auto' }}/> }
{ secondRow && (
<Flex alignItems="center" minH={ 10 } overflow="hidden">
{ secondRow }
</Flex>
) }
</Flex>
);
};
......
......@@ -4,7 +4,8 @@ import React from 'react';
import type { TokenInfo } from 'types/api/token';
import iconVerifiedToken from 'icons/verified_token.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import * as addressMock from 'mocks/address/address';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EntityTags from 'ui/shared/EntityTags';
import NetworkExplorers from 'ui/shared/NetworkExplorers';
......@@ -12,8 +13,6 @@ import NetworkExplorers from 'ui/shared/NetworkExplorers';
import PageTitle from '../PageTitle';
const DefaultView = () => {
const isMobile = useIsMobile();
const tokenData: TokenInfo = {
address: '0x363574E6C5C71c343d7348093D84320c76d5Dd29',
circulating_market_cap: '117629601.61913824',
......@@ -39,12 +38,23 @@ const DefaultView = () => {
tagsBefore={ [
{ label: 'example', display_name: 'Example label' },
] }
contentAfter={ <NetworkExplorers type="token" pathParam="token-hash" ml="auto" hideText={ isMobile }/> }
flexGrow={ 1 }
/>
</>
);
const secondRow = (
<>
<AddressEntity
address={{ ...addressMock.token, name: '' }}
fontFamily="heading"
fontSize="lg"
fontWeight={ 500 }
/>
<NetworkExplorers type="token" pathParam={ addressMock.hash } ml="auto"/>
</>
);
return (
<PageTitle
title="Shavukha Token (SHVKH) token"
......@@ -56,6 +66,7 @@ const DefaultView = () => {
) }
backLink={ backLink }
contentAfter={ contentAfter }
secondRow={ secondRow }
/>
);
};
......
......@@ -5,7 +5,6 @@ import React from 'react';
import type { TokenInfo } from 'types/api/token';
import iconVerifiedToken from 'icons/verified_token.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import { publicTag, privateTag, watchlistName } from 'mocks/address/tag';
import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EntityTags from 'ui/shared/EntityTags';
......@@ -14,8 +13,6 @@ import NetworkExplorers from 'ui/shared/NetworkExplorers';
import PageTitle from '../PageTitle';
const LongNameAndManyTags = () => {
const isMobile = useIsMobile();
const tokenData: TokenInfo = {
address: '0xa77A39CC9680B10C00af5D4ABFc92e1F07406c64',
circulating_market_cap: null,
......@@ -45,7 +42,7 @@ const LongNameAndManyTags = () => {
{ label: 'after_1', display_name: 'Another tag' },
{ label: 'after_2', display_name: 'And yet more' },
] }
contentAfter={ <NetworkExplorers type="token" pathParam="token-hash" ml="auto" hideText={ isMobile }/> }
contentAfter={ <NetworkExplorers type="token" pathParam="token-hash" ml="auto"/> }
flexGrow={ 1 }
/>
</>
......
import { Box, chakra, Icon, Skeleton, Tooltip } from '@chakra-ui/react';
import { Box, chakra, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/token';
......@@ -16,10 +16,11 @@ interface Props {
className?: string;
token: TokenInfo;
isLoading?: boolean;
variant?: 'icon' | 'button';
iconSize?: number;
}
const AddressAddToWallet = ({ className, token, isLoading, iconSize = 6 }: Props) => {
const AddressAddToWallet = ({ className, token, isLoading, variant = 'icon', iconSize = 6 }: Props) => {
const toast = useToast();
const { provider, wallet } = useProvider();
const addOrSwitchChain = useAddOrSwitchChain();
......@@ -86,9 +87,26 @@ const AddressAddToWallet = ({ className, token, isLoading, iconSize = 6 }: Props
return null;
}
if (variant === 'button') {
return (
<Tooltip label={ `Add token to ${ WALLETS_INFO[wallet].name }` }>
<IconButton
className={ className }
aria-label="Add token to wallet"
variant="outline"
size="sm"
px="6px"
onClick={ handleClick }
icon={ <Icon as={ WALLETS_INFO[wallet].icon } boxSize={ 6 }/> }
flexShrink={ 0 }
/>
</Tooltip>
);
}
return (
<Tooltip label={ `Add token to ${ WALLETS_INFO[wallet].name }` }>
<Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick }>
<Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick } flexShrink={ 0 }>
<Icon as={ WALLETS_INFO[wallet].icon } boxSize={ iconSize }/>
</Box>
</Tooltip>
......
......@@ -86,7 +86,6 @@ const TokenDetails = ({ tokenQuery }: Props) => {
return (
<Grid
mt={ 8 }
columnGap={ 8 }
rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
......
......@@ -20,10 +20,10 @@ const TriggerButton = ({ isOpen, onClick }: Props, ref: React.ForwardedRef<HTMLB
aria-label="Show project info"
fontWeight={ 500 }
px={ 2 }
h="30px"
h="32px"
>
<Icon as={ rocketIcon } boxSize={ 4 } mr={ 1 }/>
<span>Project info</span>
<Icon as={ rocketIcon } boxSize={ 5 } mr={ 1 }/>
<span>Info</span>
<Icon as={ arrowIcon } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 } ml={ 1 }/>
</Button>
);
......
import { Flex, Skeleton } from '@chakra-ui/react';
import { Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
......@@ -25,9 +25,9 @@ const TokenVerifiedInfo = ({ verifiedInfoQuery }: Props) => {
if (isLoading) {
return (
<>
<Skeleton w="130px" h="30px" borderRadius="base"/>
<Skeleton w="130px" h="30px" borderRadius="base"/>
<Skeleton w="120px" h="30px" borderRadius="base"/>
<Skeleton w="100px" h="30px" borderRadius="base"/>
<Skeleton w="100px" h="30px" borderRadius="base"/>
<Skeleton w="80px" h="30px" borderRadius="base"/>
</>
);
}
......@@ -40,7 +40,9 @@ const TokenVerifiedInfo = ({ verifiedInfoQuery }: Props) => {
try {
const url = new URL(data.projectWebsite);
return (
<LinkExternal href={ data.projectWebsite } variant="subtle">{ url.host }</LinkExternal>
<LinkExternal href={ data.projectWebsite } variant="subtle" flexShrink={ 0 }>
{ url.host }
</LinkExternal>
);
} catch (error) {
return null;
......@@ -55,7 +57,7 @@ const TokenVerifiedInfo = ({ verifiedInfoQuery }: Props) => {
);
})();
return <Flex columnGap={ 3 } rowGap={ 3 } mt={ 5 } flexWrap="wrap" _empty={{ display: 'none' }}>{ content }</Flex>;
return content;
};
export default React.memo(TokenVerifiedInfo);
......@@ -8,7 +8,6 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import DetailsSponsoredItem from 'ui/shared/DetailsSponsoredItem';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import NftMedia from 'ui/shared/nft/NftMedia';
import TokenNftMarketplaces from 'ui/token/TokenNftMarketplaces';
......@@ -37,7 +36,7 @@ const TokenInstanceDetails = ({ data, scrollRef, isLoading }: Props) => {
return (
<>
<Flex alignItems="flex-start" mt={ 8 } flexDir={{ base: 'column-reverse', lg: 'row' }} columnGap={ 6 } rowGap={ 6 }>
<Flex alignItems="flex-start" flexDir={{ base: 'column-reverse', lg: 'row' }} columnGap={ 6 } rowGap={ 6 }>
<Grid
flexGrow={ 1 }
columnGap={ 8 }
......@@ -45,17 +44,6 @@ const TokenInstanceDetails = ({ data, scrollRef, isLoading }: Props) => {
templateColumns={{ base: 'minmax(0, 1fr)', lg: '200px minmax(0, 1fr)' }}
overflow="hidden"
>
<DetailsInfoItem
title="Token"
hint="Token name"
isLoading={ isLoading }
>
<TokenEntity
token={ data.token }
isLoading={ isLoading }
noCopy
/>
</DetailsInfoItem>
{ data.is_unique && data.owner && (
<DetailsInfoItem
title="Owner"
......
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