Commit 3114ef88 authored by tom's avatar tom

page base layout

parent f8eaad89
...@@ -25,7 +25,7 @@ import type { RawTracesResponse } from 'types/api/rawTrace'; ...@@ -25,7 +25,7 @@ import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search'; import type { SearchResult, SearchResultFilters } from 'types/api/search';
import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats'; import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo'; import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo';
import type { TokensResponse, TokensFilters } from 'types/api/tokens'; import type { TokensResponse, TokensFilters, TokenInstance } from 'types/api/tokens';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction'; import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TTxsFilters } from 'types/api/txsFilters';
...@@ -236,6 +236,11 @@ export const RESOURCES = { ...@@ -236,6 +236,11 @@ export const RESOURCES = {
filterFields: [ 'filter' as const, 'type' as const ], filterFields: [ 'filter' as const, 'type' as const ],
}, },
// TOKEN INSTANCE
token_instance: {
path: '/api/v2/tokens/:hash/instances/:id',
},
// HOMEPAGE // HOMEPAGE
homepage_stats: { homepage_stats: {
path: '/api/v2/stats', path: '/api/v2/stats',
...@@ -357,6 +362,7 @@ Q extends 'token' ? TokenInfo : ...@@ -357,6 +362,7 @@ Q extends 'token' ? TokenInfo :
Q extends 'token_counters' ? TokenCounters : Q extends 'token_counters' ? TokenCounters :
Q extends 'token_transfers' ? TokenTransferResponse : Q extends 'token_transfers' ? TokenTransferResponse :
Q extends 'token_holders' ? TokenHolders : Q extends 'token_holders' ? TokenHolders :
Q extends 'token_instance' ? TokenInstance :
Q extends 'tokens' ? TokensResponse : Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult : Q extends 'search' ? SearchResult :
Q extends 'contract' ? SmartContract : Q extends 'contract' ? SmartContract :
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import type { PageParams } from 'lib/next/token/types';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import TokenInstance from 'ui/pages/TokenInstance';
const TokenInstancePage: NextPage<PageParams> = () => {
const title = getNetworkTitle();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<TokenInstance/>
</>
);
};
export default TokenInstancePage;
export { getServerSideProps } from 'lib/next/token/getServerSideProps';
export type Tokenlist = {
message: string;
result: Array<TokenlistItem> | string;
}
export type TokenlistItem = {
balance: number;
contractAddress: string;
decimals: number | null;
id: number;
name: string;
symbol: string;
type: string;
}
import type { AddressParam } from './addressParams';
import type { TokenInfo, TokenType } from './tokenInfo'; import type { TokenInfo, TokenType } from './tokenInfo';
export type TokensResponse = { export type TokensResponse = {
...@@ -10,3 +11,15 @@ export type TokensResponse = { ...@@ -10,3 +11,15 @@ export type TokensResponse = {
} }
export type TokensFilters = { filter: string; type: Array<TokenType> | undefined }; export type TokensFilters = { filter: string; type: Array<TokenType> | undefined };
export interface TokenInstance {
is_unique: string;
id: string;
holder_address_hash: string | null;
image_url: string | null;
animation_url: string | null;
external_app_url: string | null;
metadata: unknown;
owner: AddressParam;
token: TokenInfo;
}
...@@ -9,25 +9,19 @@ import appConfig from 'configs/app/config'; ...@@ -9,25 +9,19 @@ 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 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 link from 'lib/link/link'; import link from 'lib/link/link';
import AddressCounterItem from 'ui/address/details/AddressCounterItem'; import AddressCounterItem from 'ui/address/details/AddressCounterItem';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/LinkExternal'; import LinkExternal from 'ui/shared/LinkExternal';
import LinkInternal from 'ui/shared/LinkInternal'; import LinkInternal from 'ui/shared/LinkInternal';
import AddressAddToMetaMask from './details/AddressAddToMetaMask';
import AddressBalance from './details/AddressBalance'; import AddressBalance from './details/AddressBalance';
import AddressDetailsSkeleton from './details/AddressDetailsSkeleton'; import AddressDetailsSkeleton from './details/AddressDetailsSkeleton';
import AddressFavoriteButton from './details/AddressFavoriteButton';
import AddressNameInfo from './details/AddressNameInfo'; import AddressNameInfo from './details/AddressNameInfo';
import AddressQrCode from './details/AddressQrCode';
import TokenSelect from './tokenSelect/TokenSelect'; import TokenSelect from './tokenSelect/TokenSelect';
interface Props { interface Props {
...@@ -37,7 +31,6 @@ interface Props { ...@@ -37,7 +31,6 @@ interface Props {
const AddressDetails = ({ addressQuery, scrollRef }: Props) => { const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile();
const addressHash = router.query.id?.toString(); const addressHash = router.query.id?.toString();
...@@ -92,18 +85,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -92,18 +85,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
return ( return (
<Box> <Box>
<Flex alignItems="center"> <AddressHeadingInfo address={ data } token={ data.token } isLinkDisabled/>
<AddressIcon address={ data }/>
<Text ml={ 2 } fontFamily="heading" fontWeight={ 500 }>
{ isMobile ? <HashStringShorten hash={ data.hash }/> : data.hash }
</Text>
<CopyToClipboard text={ data.hash }/>
{ data.is_contract && data.token && <AddressAddToMetaMask ml={ 2 } token={ data.token }/> }
{ !data.is_contract && (
<AddressFavoriteButton hash={ data.hash } isAdded={ Boolean(data.watchlist_names?.length) } ml={ 3 }/>
) }
<AddressQrCode hash={ data.hash } ml={ 2 }/>
</Flex>
{ explorers.length > 0 && ( { explorers.length > 0 && (
<Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap"> <Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap">
<Text fontSize="sm">Verify with other explorers</Text> <Text fontSize="sm">Verify with other explorers</Text>
......
import { Skeleton, Box, Flex, SkeletonCircle, Tag } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import AdBanner from 'ui/shared/ad/AdBanner';
import TextAd from 'ui/shared/ad/TextAd';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TokenLogo from 'ui/shared/TokenLogo';
export type TokenTabs = 'token_transfers' | 'holders'
const TokenInstance = () => {
const router = useRouter();
const isMobile = useIsMobile();
const appProps = useAppContext();
const hash = router.query.hash?.toString();
const id = router.query.id?.toString();
const hasGoBackLink = appProps.referrer && appProps.referrer.includes(`/token/${ hash }`);
const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenInstanceQuery = useApiQuery('token_instance', {
pathParams: { hash, id },
queryOptions: { enabled: Boolean(hash && id) },
});
const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <span>Token transfers</span> },
{ id: 'holders', title: 'Holders', component: <span>Holders</span> },
{ id: 'metadata', title: 'Metadata', component: <span>Metadata</span> },
];
const content = (() => {
if (tokenInstanceQuery.isError) {
return <DataFetchAlert/>;
}
if (tokenInstanceQuery.isLoading) {
return (
<Flex alignItems="center" mb={ 6 }>
<SkeletonCircle w={ 6 } h={ 6 } mr={ 3 }/>
<Skeleton w="500px" h={ 10 }/>
</Flex>
);
}
const tokenLogo = <TokenLogo hash={ tokenInstanceQuery.data.token.address } name={ tokenInstanceQuery.data.token.name } boxSize={ 6 }/>;
const tokenTag = <Tag>{ tokenInstanceQuery.data.token.type }</Tag>;
const address = {
hash: hash || '',
is_contract: true,
implementation_name: null,
watchlist_names: [],
};
return (
<>
<TextAd mb={ 6 }/>
<PageTitle
text={ `${ tokenInstanceQuery.data.token.name } #${ tokenInstanceQuery.data.id }` }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
backLinkLabel="Back to token page"
additionalsLeft={ tokenLogo }
additionalsRight={ tokenTag }
/>
<AddressHeadingInfo address={ address } token={ tokenInstanceQuery.data.token }/>
<AdBanner mt={{ base: 6, lg: 8 }} justifyContent="center"/>
{ /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box>
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
/>
</>
);
})();
return (
<Page>{ content }</Page>
);
};
export default TokenInstance;
import { Flex } from '@chakra-ui/react';
import React from 'react';
import type { AddressParam } from 'types/api/addressParams';
import type { TokenInfo } from 'types/api/tokenInfo';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
import AddressFavoriteButton from 'ui/address/details/AddressFavoriteButton';
import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props {
address: Pick<AddressParam, 'hash' | 'is_contract' | 'implementation_name' | 'watchlist_names'>;
token?: TokenInfo | null;
isLinkDisabled?: boolean;
}
const AddressHeadingInfo = ({ address, token, isLinkDisabled }: Props) => {
const isMobile = useIsMobile();
return (
<Flex alignItems="center">
<AddressIcon address={ address }/>
<AddressLink
type="address"
hash={ address.hash }
ml={ 2 }
fontFamily="heading"
fontWeight={ 500 }
truncation={ isMobile ? 'constant' : 'none' }
isDisabled={ isLinkDisabled }
/>
<CopyToClipboard text={ address.hash }/>
{ address.is_contract && token && <AddressAddToMetaMask ml={ 2 } token={ token }/> }
{ !address.is_contract && (
<AddressFavoriteButton hash={ address.hash } isAdded={ Boolean(address.watchlist_names?.length) } ml={ 3 }/>
) }
<AddressQrCode hash={ address.hash } ml={ 2 }/>
</Flex>
);
};
export default AddressHeadingInfo;
...@@ -6,12 +6,7 @@ import React from 'react'; ...@@ -6,12 +6,7 @@ import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo'; import type { TokenInfo } from 'types/api/tokenInfo';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import AddressAddToMetaMask from 'ui/address/details/AddressAddToMetaMask';
import AddressQrCode from 'ui/address/details/AddressQrCode';
import AddressContractIcon from 'ui/shared/address/AddressContractIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props { interface Props {
tokenQuery: UseQueryResult<TokenInfo>; tokenQuery: UseQueryResult<TokenInfo>;
...@@ -19,7 +14,6 @@ interface Props { ...@@ -19,7 +14,6 @@ interface Props {
const TokenContractInfo = ({ tokenQuery }: Props) => { const TokenContractInfo = ({ tokenQuery }: Props) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile();
const contractQuery = useApiQuery('address', { const contractQuery = useApiQuery('address', {
pathParams: { id: router.query.hash?.toString() }, pathParams: { id: router.query.hash?.toString() },
...@@ -43,17 +37,14 @@ const TokenContractInfo = ({ tokenQuery }: Props) => { ...@@ -43,17 +37,14 @@ const TokenContractInfo = ({ tokenQuery }: Props) => {
return null; return null;
} }
const hash = tokenQuery.data.address; const address = {
hash: tokenQuery.data.address,
return ( is_contract: true,
<Flex alignItems="center"> implementation_name: null,
<AddressContractIcon/> watchlist_names: [],
<AddressLink type="address" hash={ hash } ml={ 2 } truncation={ isMobile ? 'constant' : 'none' }/> };
<CopyToClipboard text={ hash } ml={ 1 }/>
{ contractQuery.data?.token && <AddressAddToMetaMask token={ contractQuery.data?.token } ml={ 2 }/> } return <AddressHeadingInfo address={ address } token={ contractQuery.data?.token }/>;
<AddressQrCode hash={ hash } ml={ 2 }/>
</Flex>
);
}; };
export default React.memo(TokenContractInfo); export default React.memo(TokenContractInfo);
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