Commit ee3337ae authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #820 from blockscout/skeletons/account

skeletons: account pages
parents 360a305a 2d9c9fa3
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import ApiKeys from 'ui/pages/ApiKeys';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
const ApiKeys = dynamic(() => import('ui/pages/ApiKeys'), { ssr: false });
const ApiKeysPage: NextPage = () => { const ApiKeysPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import CustomAbi from 'ui/pages/CustomAbi';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
const CustomAbi = dynamic(() => import('ui/pages/CustomAbi'), { ssr: false });
const CustomAbiPage: NextPage = () => { const CustomAbiPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import PublicTags from 'ui/pages/PublicTags';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
const PublicTags = dynamic(() => import('ui/pages/PublicTags'), { ssr: false });
const PublicTagsPage: NextPage = () => { const PublicTagsPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
......
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import PrivateTags from 'ui/pages/PrivateTags';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
const AddressTagsPage: NextPage = () => { const PrivateTags = dynamic(() => import('ui/pages/PrivateTags'), { ssr: false });
const PrivateTagsPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
<> <>
...@@ -18,6 +20,6 @@ const AddressTagsPage: NextPage = () => { ...@@ -18,6 +20,6 @@ const AddressTagsPage: NextPage = () => {
); );
}; };
export default AddressTagsPage; export default PrivateTagsPage;
export { getServerSideProps } from 'lib/next/account/getServerSideProps'; export { getServerSideProps } from 'lib/next/account/getServerSideProps';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import WatchList from 'ui/pages/Watchlist';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
const WatchList = dynamic(() => import('ui/pages/Watchlist'), { ssr: false });
const WatchListPage: NextPage = () => { const WatchListPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
......
import type { PublicTag, AddressTag, TransactionTag, ApiKey, CustomAbi } from 'types/api/account';
import type { TWatchlistItem } from 'types/client/account';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
import { TX_HASH } from './tx';
export const PRIVATE_TAG_ADDRESS = { export const PRIVATE_TAG_ADDRESS: AddressTag = {
address: ADDRESS_PARAMS, address: ADDRESS_PARAMS,
address_hash: ADDRESS_HASH, address_hash: ADDRESS_HASH,
id: '4', id: '4',
name: 'placeholder', name: 'placeholder',
}; };
export const PRIVATE_TAG_TX: TransactionTag = {
id: '1',
name: 'placeholder',
transaction_hash: TX_HASH,
};
export const PUBLIC_TAG: PublicTag = {
additional_comment: 'my comment',
addresses: [ ADDRESS_HASH ],
addresses_with_info: [ ADDRESS_PARAMS ],
company: 'Blockscout',
email: 'john.doe@example.com',
full_name: 'name',
id: 1,
is_owner: true,
tags: 'placeholder',
website: 'example.com',
};
export const WATCH_LIST_ITEM_WITH_TOKEN_INFO: TWatchlistItem = {
address: ADDRESS_PARAMS,
address_balance: '7072643779453701031672',
address_hash: ADDRESS_HASH,
exchange_rate: '0.00099052',
id: '18',
name: 'placeholder',
notification_methods: {
email: false,
},
notification_settings: {
'ERC-20': {
incoming: true,
outcoming: true,
},
'ERC-721': {
incoming: true,
outcoming: true,
},
'native': {
incoming: true,
outcoming: true,
},
},
tokens_count: 42,
};
export const API_KEY: ApiKey = {
api_key: '9c3ecf44-a1ca-4ff1-b28e-329e8b65f652',
name: 'placeholder',
};
export const CUSTOM_ABI: CustomAbi = {
abi: [
{
constant: false,
payable: false,
inputs: [ { name: 'target', type: 'address' } ],
name: 'unknownWriteMethod',
outputs: [ { name: 'result', type: 'address' } ],
stateMutability: 'nonpayable',
type: 'function',
},
],
contract_address: ADDRESS_PARAMS,
contract_address_hash: ADDRESS_HASH,
id: '1',
name: 'placeholder',
};
...@@ -102,7 +102,7 @@ export type CustomAbis = Array<CustomAbi> ...@@ -102,7 +102,7 @@ export type CustomAbis = Array<CustomAbi>
export interface CustomAbi { export interface CustomAbi {
name: string; name: string;
id: number; id: string;
contract_address_hash: string; contract_address_hash: string;
contract_address: AddressParam; contract_address: AddressParam;
abi: Array<AbiItem>; abi: Array<AbiItem>;
...@@ -119,7 +119,7 @@ export interface AbiItem { ...@@ -119,7 +119,7 @@ export interface AbiItem {
} }
interface AbiInputOutput { interface AbiInputOutput {
type: 'uint256'; type: 'uint256' | 'address';
name: string; name: string;
} }
......
...@@ -8,11 +8,12 @@ import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; ...@@ -8,11 +8,12 @@ import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props { interface Props {
item: ApiKey; item: ApiKey;
isLoading?: boolean;
onEditClick: (item: ApiKey) => void; onEditClick: (item: ApiKey) => void;
onDeleteClick: (item: ApiKey) => void; onDeleteClick: (item: ApiKey) => void;
} }
const ApiKeyListItem = ({ item, onEditClick, onDeleteClick }: Props) => { const ApiKeyListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
...@@ -24,8 +25,8 @@ const ApiKeyListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -24,8 +25,8 @@ const ApiKeyListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<ListItemMobile> <ListItemMobile>
<ApiKeySnippet apiKey={ item.api_key } name={ item.name }/> <ApiKeySnippet apiKey={ item.api_key } name={ item.name } isLoading={ isLoading }/>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</ListItemMobile> </ListItemMobile>
); );
}; };
......
...@@ -12,13 +12,14 @@ import type { ApiKeys, ApiKey } from 'types/api/account'; ...@@ -12,13 +12,14 @@ import type { ApiKeys, ApiKey } from 'types/api/account';
import ApiKeyTableItem from './ApiKeyTableItem'; import ApiKeyTableItem from './ApiKeyTableItem';
interface Props { interface Props {
data: ApiKeys; data?: ApiKeys;
isLoading?: boolean;
onEditClick: (item: ApiKey) => void; onEditClick: (item: ApiKey) => void;
onDeleteClick: (item: ApiKey) => void; onDeleteClick: (item: ApiKey) => void;
limit: number; limit: number;
} }
const ApiKeyTable = ({ data, onDeleteClick, onEditClick, limit }: Props) => { const ApiKeyTable = ({ data, isLoading, onDeleteClick, onEditClick, limit }: Props) => {
return ( return (
<Table variant="simple" minWidth="600px"> <Table variant="simple" minWidth="600px">
<Thead> <Thead>
...@@ -28,10 +29,11 @@ const ApiKeyTable = ({ data, onDeleteClick, onEditClick, limit }: Props) => { ...@@ -28,10 +29,11 @@ const ApiKeyTable = ({ data, onDeleteClick, onEditClick, limit }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data?.map((item, index) => (
<ApiKeyTableItem <ApiKeyTableItem
key={ item.api_key + (isLoading ? index : '') }
item={ item } item={ item }
key={ item.api_key } isLoading={ isLoading }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
......
...@@ -11,11 +11,12 @@ import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; ...@@ -11,11 +11,12 @@ import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props { interface Props {
item: ApiKey; item: ApiKey;
isLoading?: boolean;
onEditClick: (item: ApiKey) => void; onEditClick: (item: ApiKey) => void;
onDeleteClick: (item: ApiKey) => void; onDeleteClick: (item: ApiKey) => void;
} }
const ApiKeyTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { const ApiKeyTableItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
...@@ -28,10 +29,10 @@ const ApiKeyTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -28,10 +29,10 @@ const ApiKeyTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<Tr alignItems="top" key={ item.api_key }> <Tr alignItems="top" key={ item.api_key }>
<Td> <Td>
<ApiKeySnippet apiKey={ item.api_key } name={ item.name }/> <ApiKeySnippet apiKey={ item.api_key } name={ item.name } isLoading={ isLoading }/>
</Td> </Td>
<Td> <Td>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -48,7 +48,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -48,7 +48,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const customAbiKey = (data: Inputs & { id?: number }) => { const customAbiKey = (data: Inputs & { id?: string }) => {
const body = { name: data.name, contract_address_hash: data.contract_address_hash, abi: data.abi }; const body = { name: data.name, contract_address_hash: data.contract_address_hash, abi: data.abi };
if (!data.id) { if (!data.id) {
......
...@@ -8,11 +8,12 @@ import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; ...@@ -8,11 +8,12 @@ import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props { interface Props {
item: CustomAbi; item: CustomAbi;
isLoading?: boolean;
onEditClick: (item: CustomAbi) => void; onEditClick: (item: CustomAbi) => void;
onDeleteClick: (item: CustomAbi) => void; onDeleteClick: (item: CustomAbi) => void;
} }
const CustomAbiListItem = ({ item, onEditClick, onDeleteClick }: Props) => { const CustomAbiListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
...@@ -24,8 +25,8 @@ const CustomAbiListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -24,8 +25,8 @@ const CustomAbiListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<ListItemMobile> <ListItemMobile>
<AddressSnippet address={ item.contract_address } subtitle={ item.name }/> <AddressSnippet address={ item.contract_address } subtitle={ item.name } isLoading={ isLoading }/>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</ListItemMobile> </ListItemMobile>
); );
}; };
......
...@@ -12,12 +12,13 @@ import type { CustomAbis, CustomAbi } from 'types/api/account'; ...@@ -12,12 +12,13 @@ import type { CustomAbis, CustomAbi } from 'types/api/account';
import CustomAbiTableItem from './CustomAbiTableItem'; import CustomAbiTableItem from './CustomAbiTableItem';
interface Props { interface Props {
data: CustomAbis; data?: CustomAbis;
isLoading?: boolean;
onEditClick: (item: CustomAbi) => void; onEditClick: (item: CustomAbi) => void;
onDeleteClick: (item: CustomAbi) => void; onDeleteClick: (item: CustomAbi) => void;
} }
const CustomAbiTable = ({ data, onDeleteClick, onEditClick }: Props) => { const CustomAbiTable = ({ data, isLoading, onDeleteClick, onEditClick }: Props) => {
return ( return (
<Table variant="simple" minWidth="600px"> <Table variant="simple" minWidth="600px">
<Thead> <Thead>
...@@ -27,10 +28,11 @@ const CustomAbiTable = ({ data, onDeleteClick, onEditClick }: Props) => { ...@@ -27,10 +28,11 @@ const CustomAbiTable = ({ data, onDeleteClick, onEditClick }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data?.map((item, index) => (
<CustomAbiTableItem <CustomAbiTableItem
key={ item.id + (isLoading ? index : '') }
item={ item } item={ item }
key={ item.id } isLoading={ isLoading }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
......
...@@ -11,11 +11,12 @@ import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; ...@@ -11,11 +11,12 @@ import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props { interface Props {
item: CustomAbi; item: CustomAbi;
isLoading?: boolean;
onEditClick: (item: CustomAbi) => void; onEditClick: (item: CustomAbi) => void;
onDeleteClick: (item: CustomAbi) => void; onDeleteClick: (item: CustomAbi) => void;
} }
const CustomAbiTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { const CustomAbiTableItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
...@@ -28,10 +29,10 @@ const CustomAbiTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -28,10 +29,10 @@ const CustomAbiTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<Tr alignItems="top" key={ item.id }> <Tr alignItems="top" key={ item.id }>
<Td> <Td>
<AddressSnippet address={ item.contract_address } subtitle={ item.name }/> <AddressSnippet address={ item.contract_address } subtitle={ item.name } isLoading={ isLoading }/>
</Td> </Td>
<Td> <Td>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</Td> </Td>
</Tr> </Tr>
); );
......
import { Box, Button, Stack, Link, Text, Skeleton, useDisclosure } from '@chakra-ui/react'; import { Box, Button, Link, Text, Skeleton, useDisclosure } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { ApiKey } from 'types/api/account'; import type { ApiKey } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import { API_KEY } from 'stubs/account';
import ApiKeyModal from 'ui/apiKey/ApiKeyModal/ApiKeyModal'; import ApiKeyModal from 'ui/apiKey/ApiKeyModal/ApiKeyModal';
import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem'; import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem';
import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable'; import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable';
...@@ -14,21 +14,22 @@ import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal'; ...@@ -14,21 +14,22 @@ import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
const DATA_LIMIT = 3; const DATA_LIMIT = 3;
const ApiKeysPage: React.FC = () => { const ApiKeysPage: React.FC = () => {
const apiKeyModalProps = useDisclosure(); const apiKeyModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
useRedirectForInvalidAuthToken(); useRedirectForInvalidAuthToken();
const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>(); const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>();
const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>(); const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>();
const { data, isLoading, isError, error } = useApiQuery('api_keys'); const { data, isPlaceholderData, isError, error } = useApiQuery('api_keys', {
queryOptions: {
placeholderData: Array(3).fill(API_KEY),
},
});
const onEditClick = useCallback((data: ApiKey) => { const onEditClick = useCallback((data: ApiKey) => {
setApiKeyModalData(data); setApiKeyModalData(data);
...@@ -58,22 +59,6 @@ const ApiKeysPage: React.FC = () => { ...@@ -58,22 +59,6 @@ const ApiKeysPage: React.FC = () => {
); );
const content = (() => { const content = (() => {
if (isLoading && !data) {
const loader = isMobile ? <SkeletonListAccount/> : (
<>
<SkeletonTable columns={ [ '100%', '108px' ] }/>
<Skeleton height="48px" width="156px" marginTop={ 8 }/>
</>
);
return (
<>
{ description }
{ loader }
</>
);
}
if (isError) { if (isError) {
if (error.status === 403) { if (error.status === 403) {
throw new Error('Unverified email error', { cause: error }); throw new Error('Unverified email error', { cause: error });
...@@ -81,50 +66,59 @@ const ApiKeysPage: React.FC = () => { ...@@ -81,50 +66,59 @@ const ApiKeysPage: React.FC = () => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
const list = isMobile ? ( const list = (
<Box> <>
{ data.map((item) => ( <Box display={{ base: 'block', lg: 'none' }}>
<ApiKeyListItem { data?.map((item, index) => (
item={ item } <ApiKeyListItem
key={ item.api_key } key={ item.api_key + (isPlaceholderData ? index : '') }
item={ item }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<ApiKeyTable
data={ data }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
limit={ DATA_LIMIT }
/> />
)) } </Box>
</Box> </>
) : (
<ApiKeyTable
data={ data }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
limit={ DATA_LIMIT }
/>
); );
const canAdd = data.length < DATA_LIMIT; const canAdd = !isPlaceholderData ? (data?.length || 0) < DATA_LIMIT : true;
return ( return (
<> <>
{ description } { description }
{ Boolean(data.length) && list } { Boolean(data?.length) && list }
<Stack <Skeleton
marginTop={ 8 } marginTop={ 8 }
spacing={ 5 } flexDir={{ base: 'column', lg: 'row' }}
direction={{ base: 'column', lg: 'row' }} alignItems={{ base: 'start', lg: 'center' }}
align={{ base: 'start', lg: 'center' }} isLoaded={ !isPlaceholderData }
display="inline-flex"
columnGap={ 5 }
rowGap={ 5 }
> >
<Button <Button
size="lg" size="lg"
onClick={ apiKeyModalProps.onOpen } onClick={ apiKeyModalProps.onOpen }
isDisabled={ !canAdd } isDisabled={ !canAdd }
> >
Add API key Add API key
</Button> </Button>
{ !canAdd && ( { !canAdd && (
<Text fontSize="sm" variant="secondary"> <Text fontSize="sm" variant="secondary">
{ `You have added the maximum number of API keys (${ DATA_LIMIT }). Contact us to request additional keys.` } { `You have added the maximum number of API keys (${ DATA_LIMIT }). Contact us to request additional keys.` }
</Text> </Text>
) } ) }
</Stack> </Skeleton>
<ApiKeyModal { ...apiKeyModalProps } onClose={ onApiKeyModalClose } data={ apiKeyModalData }/> <ApiKeyModal { ...apiKeyModalProps } onClose={ onApiKeyModalClose } data={ apiKeyModalData }/>
{ deleteModalData && <DeleteApiKeyModal { ...deleteModalProps } onClose={ onDeleteModalClose } data={ deleteModalData }/> } { deleteModalData && <DeleteApiKeyModal { ...deleteModalProps } onClose={ onDeleteModalClose } data={ deleteModalData }/> }
</> </>
......
import { Box, Button, HStack, Skeleton, useDisclosure } from '@chakra-ui/react'; import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { CustomAbi } from 'types/api/account'; import type { CustomAbi } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { CUSTOM_ABI } from 'stubs/account';
import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal'; import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal';
import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem'; import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem';
import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable'; import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable';
...@@ -13,19 +13,20 @@ import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal'; ...@@ -13,19 +13,20 @@ import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
const CustomAbiPage: React.FC = () => { const CustomAbiPage: React.FC = () => {
const customAbiModalProps = useDisclosure(); const customAbiModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
useRedirectForInvalidAuthToken(); useRedirectForInvalidAuthToken();
const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>(); const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>();
const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>(); const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>();
const { data, isLoading, isError, error } = useApiQuery('custom_abi'); const { data, isPlaceholderData, isError, error } = useApiQuery('custom_abi', {
queryOptions: {
placeholderData: Array(3).fill(CUSTOM_ABI),
},
});
const onEditClick = useCallback((data: CustomAbi) => { const onEditClick = useCallback((data: CustomAbi) => {
setCustomAbiModalData(data); setCustomAbiModalData(data);
...@@ -54,22 +55,6 @@ const CustomAbiPage: React.FC = () => { ...@@ -54,22 +55,6 @@ const CustomAbiPage: React.FC = () => {
); );
const content = (() => { const content = (() => {
if (isLoading && !data) {
const loader = isMobile ? <SkeletonListAccount/> : (
<>
<SkeletonTable columns={ [ '100%', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/>
</>
);
return (
<>
{ description }
{ loader }
</>
);
}
if (isError) { if (isError) {
if (error.status === 403) { if (error.status === 403) {
throw new Error('Unverified email error', { cause: error }); throw new Error('Unverified email error', { cause: error });
...@@ -77,37 +62,42 @@ const CustomAbiPage: React.FC = () => { ...@@ -77,37 +62,42 @@ const CustomAbiPage: React.FC = () => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
const list = isMobile ? ( const list = (
<Box> <>
{ data.map((item) => ( <Box display={{ base: 'block', lg: 'none' }}>
<CustomAbiListItem { data?.map((item, index) => (
item={ item } <CustomAbiListItem
key={ item.id } key={ item.id + (isPlaceholderData ? index : '') }
item={ item }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<CustomAbiTable
data={ data }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
)) } </Box>
</Box> </>
) : (
<CustomAbiTable
data={ data }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
); );
return ( return (
<> <>
{ description } { description }
{ data.length > 0 && list } { Boolean(data?.length) && list }
<HStack marginTop={ 8 } spacing={ 5 }> <Skeleton mt={ 8 } isLoaded={ !isPlaceholderData } display="inline-block">
<Button <Button
size="lg" size="lg"
onClick={ customAbiModalProps.onOpen } onClick={ customAbiModalProps.onOpen }
> >
Add custom ABI Add custom ABI
</Button> </Button>
</HStack> </Skeleton>
<CustomAbiModal { ...customAbiModalProps } onClose={ onCustomAbiModalClose } data={ customAbiModalData }/> <CustomAbiModal { ...customAbiModalProps } onClose={ onCustomAbiModalClose } data={ customAbiModalData }/>
{ deleteModalData && <DeleteCustomAbiModal { ...deleteModalProps } onClose={ onDeleteModalClose } data={ deleteModalData }/> } { deleteModalData && <DeleteCustomAbiModal { ...deleteModalProps } onClose={ onDeleteModalClose } data={ deleteModalData }/> }
</> </>
......
...@@ -8,13 +8,11 @@ import type { TWatchlist, TWatchlistItem } from 'types/client/account'; ...@@ -8,13 +8,11 @@ import type { TWatchlist, TWatchlistItem } from 'types/client/account';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import { resourceKey } from 'lib/api/resources'; import { resourceKey } from 'lib/api/resources';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import { WATCH_LIST_ITEM_WITH_TOKEN_INFO } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import AddressModal from 'ui/watchlist/AddressModal/AddressModal'; import AddressModal from 'ui/watchlist/AddressModal/AddressModal';
import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal'; import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
import WatchListItem from 'ui/watchlist/WatchlistTable/WatchListItem'; import WatchListItem from 'ui/watchlist/WatchlistTable/WatchListItem';
...@@ -22,33 +20,38 @@ import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable'; ...@@ -22,33 +20,38 @@ import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
const WatchList: React.FC = () => { const WatchList: React.FC = () => {
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const { data, isLoading, isError, error } = useQuery<unknown, ResourceError, TWatchlist>([ resourceKey('watchlist') ], async() => { const { data, isPlaceholderData, isError, error } = useQuery<unknown, ResourceError, TWatchlist>(
const watchlistAddresses = await apiFetch<'watchlist', Array<WatchlistAddress>>('watchlist'); [ resourceKey('watchlist') ],
async() => {
const watchlistAddresses = await apiFetch<'watchlist', Array<WatchlistAddress>>('watchlist');
if (!Array.isArray(watchlistAddresses)) { if (!Array.isArray(watchlistAddresses)) {
return; return;
}
const watchlistTokens = await Promise.all(watchlistAddresses.map(({ address }) => {
if (!address?.hash) {
return Promise.resolve(0);
} }
return apiFetch<'old_api', WatchlistTokensResponse>('old_api', { queryParams: { address: address.hash, module: 'account', action: 'tokenlist' } })
.then((response) => { const watchlistTokens = await Promise.all(watchlistAddresses.map(({ address }) => {
if ('result' in response && Array.isArray(response.result)) { if (!address?.hash) {
return response.result.length; return Promise.resolve(0);
} }
return 0; return apiFetch<'old_api', WatchlistTokensResponse>('old_api', { queryParams: { address: address.hash, module: 'account', action: 'tokenlist' } })
}); .then((response) => {
})); if ('result' in response && Array.isArray(response.result)) {
return response.result.length;
return watchlistAddresses.map((item, index) => ({ ...item, tokens_count: watchlistTokens[index] })); }
}); return 0;
});
}));
return watchlistAddresses.map((item, index) => ({ ...item, tokens_count: watchlistTokens[index] }));
},
{
placeholderData: Array(3).fill(WATCH_LIST_ITEM_WITH_TOKEN_INFO),
},
);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
useRedirectForInvalidAuthToken(); useRedirectForInvalidAuthToken();
const [ addressModalData, setAddressModalData ] = useState<TWatchlistItem>(); const [ addressModalData, setAddressModalData ] = useState<TWatchlistItem>();
...@@ -100,53 +103,42 @@ const WatchList: React.FC = () => { ...@@ -100,53 +103,42 @@ const WatchList: React.FC = () => {
} }
const content = (() => { const content = (() => {
if (isLoading && !data) { const list = (
const loader = isMobile ? <SkeletonListAccount showFooterSlot/> : ( <>
<> <Box display={{ base: 'block', lg: 'none' }}>
<SkeletonTable columns={ [ '70%', '30%', '160px', '108px' ] }/> { data?.map((item, index) => (
<Skeleton height="44px" width="156px" marginTop={ 8 }/> <WatchListItem
</> key={ item.address_hash + (isPlaceholderData ? index : '') }
); item={ item }
isLoading={ isPlaceholderData }
return ( onDeleteClick={ onDeleteClick }
<> onEditClick={ onEditClick }
{ description } />
{ loader } )) }
</> </Box>
); <Box display={{ base: 'none', lg: 'block' }}>
} <WatchlistTable
data={ data }
const list = isMobile ? ( isLoading={ isPlaceholderData }
<Box>
{ data.map((item) => (
<WatchListItem
item={ item }
key={ item.address_hash }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
)) } </Box>
</Box> </>
) : (
<WatchlistTable
data={ data }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
); );
return ( return (
<> <>
{ description } { description }
{ Boolean(data?.length) && list } { Boolean(data?.length) && list }
<Box marginTop={ 8 }> <Skeleton mt={ 8 } isLoaded={ !isPlaceholderData } display="inline-block">
<Button <Button
size="lg" size="lg"
onClick={ addressModalProps.onOpen } onClick={ addressModalProps.onOpen }
> >
Add address Add address
</Button> </Button>
</Box> </Skeleton>
<AddressModal <AddressModal
{ ...addressModalProps } { ...addressModalProps }
onClose={ onAddressModalClose } onClose={ onAddressModalClose }
......
...@@ -4,7 +4,6 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,7 +4,6 @@ import React, { useCallback, useState } from 'react';
import type { AddressTag } from 'types/api/account'; import type { AddressTag } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import { PRIVATE_TAG_ADDRESS } from 'stubs/account'; import { PRIVATE_TAG_ADDRESS } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
...@@ -24,7 +23,6 @@ const PrivateAddressTags = () => { ...@@ -24,7 +23,6 @@ const PrivateAddressTags = () => {
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const [ addressModalData, setAddressModalData ] = useState<AddressTag>(); const [ addressModalData, setAddressModalData ] = useState<AddressTag>();
const [ deleteModalData, setDeleteModalData ] = useState<AddressTag>(); const [ deleteModalData, setDeleteModalData ] = useState<AddressTag>();
...@@ -60,25 +58,28 @@ const PrivateAddressTags = () => { ...@@ -60,25 +58,28 @@ const PrivateAddressTags = () => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
const list = isMobile ? ( const list = (
<Box> <>
{ addressTagsData?.map((item: AddressTag, index: number) => ( <Box display={{ base: 'block', lg: 'none' }}>
<AddressTagListItem { addressTagsData?.map((item: AddressTag, index: number) => (
item={ item } <AddressTagListItem
key={ item.id + (isPlaceholderData ? index : '') } item={ item }
key={ item.id + (isPlaceholderData ? index : '') }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
isLoading={ isPlaceholderData }
/>
)) }
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<AddressTagTable
isLoading={ isPlaceholderData }
data={ addressTagsData }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
isLoading={ isPlaceholderData }
/> />
)) } </Box>
</Box> </>
) : (
<AddressTagTable
isLoading={ isPlaceholderData }
data={ addressTagsData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
); );
return ( return (
...@@ -88,16 +89,14 @@ const PrivateAddressTags = () => { ...@@ -88,16 +89,14 @@ const PrivateAddressTags = () => {
Private tags are saved in your account and are only visible when you are logged in. Private tags are saved in your account and are only visible when you are logged in.
</AccountPageDescription> </AccountPageDescription>
{ Boolean(addressTagsData?.length) && list } { Boolean(addressTagsData?.length) && list }
<Box marginTop={ 8 }> <Skeleton mt={ 8 } isLoaded={ !isPlaceholderData } display="inline-block">
<Skeleton isLoaded={ !isPlaceholderData } display="inline-block"> <Button
<Button size="lg"
size="lg" onClick={ addressModalProps.onOpen }
onClick={ addressModalProps.onOpen } >
>
Add address tag Add address tag
</Button> </Button>
</Skeleton> </Skeleton>
</Box>
<AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData } onSuccess={ onAddOrEditSuccess }/> <AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData } onSuccess={ onAddOrEditSuccess }/>
{ deleteModalData && ( { deleteModalData && (
<DeletePrivateTagModal <DeletePrivateTagModal
......
...@@ -4,11 +4,9 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,11 +4,9 @@ import React, { useCallback, useState } from 'react';
import type { TransactionTag } from 'types/api/account'; import type { TransactionTag } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import { PRIVATE_TAG_TX } from 'stubs/account';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import DeletePrivateTagModal from './DeletePrivateTagModal'; import DeletePrivateTagModal from './DeletePrivateTagModal';
import TransactionModal from './TransactionModal/TransactionModal'; import TransactionModal from './TransactionModal/TransactionModal';
...@@ -16,11 +14,15 @@ import TransactionTagListItem from './TransactionTagTable/TransactionTagListItem ...@@ -16,11 +14,15 @@ import TransactionTagListItem from './TransactionTagTable/TransactionTagListItem
import TransactionTagTable from './TransactionTagTable/TransactionTagTable'; import TransactionTagTable from './TransactionTagTable/TransactionTagTable';
const PrivateTransactionTags = () => { const PrivateTransactionTags = () => {
const { data: transactionTagsData, isLoading, isError, error } = useApiQuery('private_tags_tx', { queryOptions: { refetchOnMount: false } }); const { data: transactionTagsData, isPlaceholderData, isError, error } = useApiQuery('private_tags_tx', {
queryOptions: {
refetchOnMount: false,
placeholderData: Array(3).fill(PRIVATE_TAG_TX),
},
});
const transactionModalProps = useDisclosure(); const transactionModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const [ transactionModalData, setTransactionModalData ] = useState<TransactionTag>(); const [ transactionModalData, setTransactionModalData ] = useState<TransactionTag>();
const [ deleteModalData, setDeleteModalData ] = useState<TransactionTag>(); const [ deleteModalData, setDeleteModalData ] = useState<TransactionTag>();
...@@ -52,22 +54,6 @@ const PrivateTransactionTags = () => { ...@@ -52,22 +54,6 @@ const PrivateTransactionTags = () => {
</AccountPageDescription> </AccountPageDescription>
); );
if (isLoading && !transactionTagsData) {
const loader = isMobile ? <SkeletonListAccount/> : (
<>
<SkeletonTable columns={ [ '75%', '25%', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/>
</>
);
return (
<>
{ description }
{ loader }
</>
);
}
if (isError) { if (isError) {
if (error.status === 403) { if (error.status === 403) {
throw new Error('Unverified email error', { cause: error }); throw new Error('Unverified email error', { cause: error });
...@@ -75,37 +61,42 @@ const PrivateTransactionTags = () => { ...@@ -75,37 +61,42 @@ const PrivateTransactionTags = () => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
const list = isMobile ? ( const list = (
<Box> <>
{ transactionTagsData.map((item) => ( <Box display={{ base: 'block', lg: 'none' }}>
<TransactionTagListItem { transactionTagsData?.map((item, index) => (
item={ item } <TransactionTagListItem
key={ item.id } key={ item.id + (isPlaceholderData ? index : '') }
item={ item }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<TransactionTagTable
data={ transactionTagsData }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
)) } </Box>
</Box> </>
) : (
<TransactionTagTable
data={ transactionTagsData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
); );
return ( return (
<> <>
{ description } { description }
{ Boolean(transactionTagsData.length) && list } { Boolean(transactionTagsData?.length) && list }
<Box marginTop={ 8 }> <Skeleton mt={ 8 } isLoaded={ !isPlaceholderData } display="inline-block">
<Button <Button
size="lg" size="lg"
onClick={ transactionModalProps.onOpen } onClick={ transactionModalProps.onOpen }
> >
Add transaction tag Add transaction tag
</Button> </Button>
</Box> </Skeleton>
<TransactionModal { ...transactionModalProps } onClose={ onAddressModalClose } data={ transactionModalData }/> <TransactionModal { ...transactionModalProps } onClose={ onAddressModalClose } data={ transactionModalData }/>
{ deleteModalData && ( { deleteModalData && (
<DeletePrivateTagModal <DeletePrivateTagModal
......
import { Tag, HStack, Text, Flex } from '@chakra-ui/react'; import { HStack, Text, Flex } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { TransactionTag } from 'types/api/account'; import type { TransactionTag } from 'types/api/account';
import Tag from 'ui/shared/chakra/Tag';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TransactionSnippet from 'ui/shared/TransactionSnippet'; import TransactionSnippet from 'ui/shared/TransactionSnippet';
interface Props { interface Props {
item: TransactionTag; item: TransactionTag;
isLoading?: boolean;
onEditClick: (data: TransactionTag) => void; onEditClick: (data: TransactionTag) => void;
onDeleteClick: (data: TransactionTag) => void; onDeleteClick: (data: TransactionTag) => void;
} }
const TransactionTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => { const TransactionTagListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
}, [ item, onEditClick ]); }, [ item, onEditClick ]);
...@@ -25,15 +27,13 @@ const TransactionTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => ...@@ -25,15 +27,13 @@ const TransactionTagListItem = ({ item, onEditClick, onDeleteClick }: Props) =>
return ( return (
<ListItemMobile> <ListItemMobile>
<Flex alignItems="flex-start" flexDirection="column" maxW="100%"> <Flex alignItems="flex-start" flexDirection="column" maxW="100%">
<TransactionSnippet hash={ item.transaction_hash }/> <TransactionSnippet hash={ item.transaction_hash } isLoading={ isLoading }/>
<HStack spacing={ 3 } mt={ 4 }> <HStack spacing={ 3 } mt={ 4 }>
<Text fontSize="sm" fontWeight={ 500 }>Private tag</Text> <Text fontSize="sm" fontWeight={ 500 }>Private tag</Text>
<Tag> <Tag isLoading={ isLoading } isTruncated>{ item.name }</Tag>
{ item.name }
</Tag>
</HStack> </HStack>
</Flex> </Flex>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</ListItemMobile> </ListItemMobile>
); );
}; };
......
...@@ -12,12 +12,13 @@ import type { TransactionTags, TransactionTag } from 'types/api/account'; ...@@ -12,12 +12,13 @@ import type { TransactionTags, TransactionTag } from 'types/api/account';
import TransactionTagTableItem from './TransactionTagTableItem'; import TransactionTagTableItem from './TransactionTagTableItem';
interface Props { interface Props {
data: TransactionTags; data?: TransactionTags;
isLoading: boolean;
onEditClick: (data: TransactionTag) => void; onEditClick: (data: TransactionTag) => void;
onDeleteClick: (data: TransactionTag) => void; onDeleteClick: (data: TransactionTag) => void;
} }
const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => { const AddressTagTable = ({ data, isLoading, onDeleteClick, onEditClick }: Props) => {
return ( return (
<Table variant="simple" minWidth="600px"> <Table variant="simple" minWidth="600px">
<Thead> <Thead>
...@@ -28,10 +29,11 @@ const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => { ...@@ -28,10 +29,11 @@ const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data?.map((item, index) => (
<TransactionTagTableItem <TransactionTagTableItem
key={ item.id + (isLoading ? index : '') }
item={ item } item={ item }
key={ item.id } isLoading={ isLoading }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
......
import { import {
Tag,
Tr, Tr,
Td, Td,
Tooltip,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { TransactionTag } from 'types/api/account'; import type { TransactionTag } from 'types/api/account';
import Tag from 'ui/shared/chakra/Tag';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TransactionSnippet from 'ui/shared/TransactionSnippet'; import TransactionSnippet from 'ui/shared/TransactionSnippet';
interface Props { interface Props {
item: TransactionTag; item: TransactionTag;
isLoading?: boolean;
onEditClick: (data: TransactionTag) => void; onEditClick: (data: TransactionTag) => void;
onDeleteClick: (data: TransactionTag) => void; onDeleteClick: (data: TransactionTag) => void;
} }
const TransactionTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { const TransactionTagTableItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
}, [ item, onEditClick ]); }, [ item, onEditClick ]);
...@@ -29,17 +29,13 @@ const TransactionTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => ...@@ -29,17 +29,13 @@ const TransactionTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) =>
return ( return (
<Tr alignItems="top" key={ item.id }> <Tr alignItems="top" key={ item.id }>
<Td> <Td>
<TransactionSnippet hash={ item.transaction_hash }/> <TransactionSnippet hash={ item.transaction_hash } isLoading={ isLoading }/>
</Td> </Td>
<Td> <Td>
<Tooltip label={ item.name }> <Tag isLoading={ isLoading } isTruncated>{ item.name }</Tag>
<Tag>
{ item.name }
</Tag>
</Tooltip>
</Td> </Td>
<Td> <Td>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</Td> </Td>
</Tr> </Tr>
); );
......
import { Tag, VStack, Text, HStack } from '@chakra-ui/react'; import { VStack, Text, HStack, Skeleton } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { PublicTag } from 'types/api/account'; import type { PublicTag } from 'types/api/account';
import AddressSnippet from 'ui/shared/AddressSnippet'; import AddressSnippet from 'ui/shared/AddressSnippet';
import Tag from 'ui/shared/chakra/Tag';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props { interface Props {
item: PublicTag; item: PublicTag;
isLoading?: boolean;
onEditClick: (data: PublicTag) => void; onEditClick: (data: PublicTag) => void;
onDeleteClick: (data: PublicTag) => void; onDeleteClick: (data: PublicTag) => void;
} }
const PublicTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => { const PublicTagListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
}, [ item, onEditClick ]); }, [ item, onEditClick ]);
...@@ -27,28 +28,22 @@ const PublicTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -27,28 +28,22 @@ const PublicTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
<ListItemMobile> <ListItemMobile>
<VStack spacing={ 3 } alignItems="flex-start" maxW="100%"> <VStack spacing={ 3 } alignItems="flex-start" maxW="100%">
<VStack spacing={ 4 } alignItems="unset" maxW="100%"> <VStack spacing={ 4 } alignItems="unset" maxW="100%">
{ item.addresses_with_info.map((address) => <AddressSnippet key={ address.hash } address={ address }/>) } { item.addresses_with_info.map((address) => <AddressSnippet key={ address.hash } address={ address } isLoading={ isLoading }/>) }
</VStack> </VStack>
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Public tags</Text> <Text fontSize="sm" fontWeight={ 500 }>Public tags</Text>
<HStack spacing={ 2 } alignItems="baseline"> <HStack spacing={ 2 } alignItems="baseline">
{ item.tags.split(';').map((tag) => { { item.tags.split(';').map((tag) => <Tag key={ tag } isLoading={ isLoading } isTruncated>{ tag }</Tag>) }
return (
<TruncatedTextTooltip label={ tag } key={ tag }>
<Tag>
{ tag }
</Tag>
</TruncatedTextTooltip>
);
}) }
</HStack> </HStack>
</HStack> </HStack>
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Status</Text> <Text fontSize="sm" fontWeight={ 500 }>Status</Text>
<Text fontSize="sm" variant="secondary">Submitted</Text> <Skeleton fontSize="sm" color="text_secondary" isLoaded={ !isLoading } display="inline-block">
<span>Submitted</span>
</Skeleton>
</HStack> </HStack>
</VStack> </VStack>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</ListItemMobile> </ListItemMobile>
); );
}; };
......
...@@ -12,12 +12,13 @@ import type { PublicTags, PublicTag } from 'types/api/account'; ...@@ -12,12 +12,13 @@ import type { PublicTags, PublicTag } from 'types/api/account';
import PublicTagTableItem from './PublicTagTableItem'; import PublicTagTableItem from './PublicTagTableItem';
interface Props { interface Props {
data: PublicTags; data?: PublicTags;
isLoading?: boolean;
onEditClick: (data: PublicTag) => void; onEditClick: (data: PublicTag) => void;
onDeleteClick: (data: PublicTag) => void; onDeleteClick: (data: PublicTag) => void;
} }
const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => { const PublicTagTable = ({ data, isLoading, onEditClick, onDeleteClick }: Props) => {
return ( return (
<Table variant="simple" minWidth="600px"> <Table variant="simple" minWidth="600px">
<Thead> <Thead>
...@@ -29,10 +30,11 @@ const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => { ...@@ -29,10 +30,11 @@ const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data?.map((item, index) => (
<PublicTagTableItem <PublicTagTableItem
key={ item.id + (isLoading ? String(index) : '') }
item={ item } item={ item }
key={ item.id } isLoading={ isLoading }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
......
import { import {
Tag,
Tr, Tr,
Td, Td,
VStack, VStack,
Text, Box,
Skeleton,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { PublicTag } from 'types/api/account'; import type { PublicTag } from 'types/api/account';
import AddressSnippet from 'ui/shared/AddressSnippet'; import AddressSnippet from 'ui/shared/AddressSnippet';
import Tag from 'ui/shared/chakra/Tag';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props { interface Props {
item: PublicTag; item: PublicTag;
isLoading?: boolean;
onEditClick: (data: PublicTag) => void; onEditClick: (data: PublicTag) => void;
onDeleteClick: (data: PublicTag) => void; onDeleteClick: (data: PublicTag) => void;
} }
const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { const PublicTagTableItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
}, [ item, onEditClick ]); }, [ item, onEditClick ]);
...@@ -32,29 +33,23 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -32,29 +33,23 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
<Tr alignItems="top" key={ item.id }> <Tr alignItems="top" key={ item.id }>
<Td> <Td>
<VStack spacing={ 4 } alignItems="unset"> <VStack spacing={ 4 } alignItems="unset">
{ item.addresses_with_info.map((address) => <AddressSnippet key={ address.hash } address={ address }/>) } { item.addresses_with_info.map((address) => <AddressSnippet key={ address.hash } address={ address } isLoading={ isLoading }/>) }
</VStack> </VStack>
</Td> </Td>
<Td> <Td>
<VStack spacing={ 2 } alignItems="baseline"> <VStack spacing={ 2 } alignItems="baseline">
{ item.tags.split(';').map((tag) => { { item.tags.split(';').map((tag) => <Tag key={ tag } isLoading={ isLoading } isTruncated>{ tag }</Tag>) }
return (
<TruncatedTextTooltip label={ tag } key={ tag }>
<Tag>
{ tag }
</Tag>
</TruncatedTextTooltip>
);
}) }
</VStack> </VStack>
</Td> </Td>
<Td> <Td>
<VStack alignItems="flex-start"> <Skeleton fontSize="sm" fontWeight="500" py="2px" isLoaded={ !isLoading } display="inline-block">
<Text fontSize="sm" fontWeight="500">Submitted</Text> Submitted
</VStack> </Skeleton>
</Td> </Td>
<Td> <Td>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <Box py="2px">
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</Box>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -4,12 +4,10 @@ import React, { useCallback, useState } from 'react'; ...@@ -4,12 +4,10 @@ import React, { useCallback, useState } from 'react';
import type { PublicTag } from 'types/api/account'; import type { PublicTag } from 'types/api/account';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import { PUBLIC_TAG } from 'stubs/account';
import PublicTagListItem from 'ui/publicTags/PublicTagTable/PublicTagListItem'; import PublicTagListItem from 'ui/publicTags/PublicTagTable/PublicTagListItem';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import DeletePublicTagModal from './DeletePublicTagModal'; import DeletePublicTagModal from './DeletePublicTagModal';
import PublicTagTable from './PublicTagTable/PublicTagTable'; import PublicTagTable from './PublicTagTable/PublicTagTable';
...@@ -22,9 +20,12 @@ type Props = { ...@@ -22,9 +20,12 @@ type Props = {
const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>(); const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>();
const isMobile = useIsMobile();
const { data, isLoading, isError, error } = useApiQuery('public_tags'); const { data, isPlaceholderData, isError, error } = useApiQuery('public_tags', {
queryOptions: {
placeholderData: Array(3).fill(PUBLIC_TAG),
},
});
const onDeleteModalClose = useCallback(() => { const onDeleteModalClose = useCallback(() => {
setDeleteModalData(undefined); setDeleteModalData(undefined);
...@@ -53,22 +54,6 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { ...@@ -53,22 +54,6 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
</AccountPageDescription> </AccountPageDescription>
); );
if (isLoading) {
const loader = isMobile ? <SkeletonListAccount/> : (
<>
<SkeletonTable columns={ [ '50%', '25%', '25%', '108px' ] }/>
<Skeleton height="48px" width="270px" marginTop={ 8 }/>
</>
);
return (
<>
{ description }
{ loader }
</>
);
}
if (isError) { if (isError) {
if (error.status === 403) { if (error.status === 403) {
throw new Error('Unverified email error', { cause: error }); throw new Error('Unverified email error', { cause: error });
...@@ -76,33 +61,37 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { ...@@ -76,33 +61,37 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
const list = isMobile ? ( const list = (
<Box> <>
{ data.map((item) => ( <Box display={{ base: 'block', lg: 'none' }}>
<PublicTagListItem { data?.map((item, index) => (
item={ item } <PublicTagListItem
key={ item.id } key={ item.id + (isPlaceholderData ? String(index) : '') }
onDeleteClick={ onItemDeleteClick } item={ item }
onEditClick={ onItemEditClick } isLoading={ isPlaceholderData }
/> onDeleteClick={ onItemDeleteClick }
)) } onEditClick={ onItemEditClick }
</Box> />
) : ( )) }
<PublicTagTable data={ data } onEditClick={ onItemEditClick } onDeleteClick={ onItemDeleteClick }/> </Box>
<Box display={{ base: 'none', lg: 'block' }}>
<PublicTagTable data={ data } isLoading={ isPlaceholderData } onEditClick={ onItemEditClick } onDeleteClick={ onItemDeleteClick }/>
</Box>
</>
); );
return ( return (
<> <>
{ description } { description }
{ data.length > 0 && list } { Boolean(data?.length) && list }
<Box marginTop={ 8 }> <Skeleton mt={ 8 } isLoaded={ !isPlaceholderData } display="inline-block">
<Button <Button
size="lg" size="lg"
onClick={ changeToForm } onClick={ changeToForm }
> >
Request to add public tag Request to add public tag
</Button> </Button>
</Box> </Skeleton>
{ deleteModalData && ( { deleteModalData && (
<DeletePublicTagModal <DeletePublicTagModal
{ ...deleteModalProps } { ...deleteModalProps }
......
import { Text, Box } from '@chakra-ui/react'; import { Box, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressParam } from 'types/api/addressParams'; import type { AddressParam } from 'types/api/addressParams';
...@@ -22,7 +22,11 @@ const AddressSnippet = ({ address, subtitle, isLoading }: Props) => { ...@@ -22,7 +22,11 @@ const AddressSnippet = ({ address, subtitle, isLoading }: Props) => {
<AddressLink type="address" hash={ address.hash } fontWeight="600" ml={ 2 } isLoading={ isLoading }/> <AddressLink type="address" hash={ address.hash } fontWeight="600" ml={ 2 } isLoading={ isLoading }/>
<CopyToClipboard text={ address.hash } isLoading={ isLoading }/> <CopyToClipboard text={ address.hash } isLoading={ isLoading }/>
</Address> </Address>
{ subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 } ml={ 8 }>{ subtitle }</Text> } { subtitle && (
<Skeleton fontSize="sm" color="text_secondary" mt={ 0.5 } ml={ 8 } display="inline-block" isLoaded={ !isLoading }>
<span>{ subtitle }</span>
</Skeleton>
) }
</Box> </Box>
); );
}; };
......
import { Box, HStack, Icon, Flex, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, HStack, Icon, Flex, Skeleton, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import keyIcon from 'icons/key.svg'; import keyIcon from 'icons/key.svg';
...@@ -7,18 +7,27 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard'; ...@@ -7,18 +7,27 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props { interface Props {
apiKey: string; apiKey: string;
name: string; name: string;
isLoading?: boolean;
} }
const ApiKeySnippet = ({ apiKey, name }: Props) => { const ApiKeySnippet = ({ apiKey, name, isLoading }: Props) => {
return ( return (
<HStack spacing={ 2 } alignItems="start"> <HStack spacing={ 2 } alignItems="start">
<Icon as={ keyIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') }/> <Skeleton isLoaded={ !isLoading } boxSize={ 6 } display="inline-block">
<Icon as={ keyIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') }/>
</Skeleton>
<Box> <Box>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }}> <Flex alignItems={{ base: 'flex-start', lg: 'center' }}>
<Text fontSize="md" lineHeight={ 6 } fontWeight={ 600 } mr={ 1 }>{ apiKey }</Text> <Skeleton isLoaded={ !isLoading } display="inline-block" fontWeight={ 600 } mr={ 1 }>
<CopyToClipboard text={ apiKey }/> <span>{ apiKey }</span>
</Skeleton>
<CopyToClipboard text={ apiKey } isLoading={ isLoading }/>
</Flex> </Flex>
{ name && <Text fontSize="sm" variant="secondary" mt={ 1 }>{ name }</Text> } { name && (
<Skeleton isLoaded={ !isLoading } display="inline-block" fontSize="sm" color="text_secondary" mt={ 1 }>
<span>{ name }</span>
</Skeleton>
) }
</Box> </Box>
</HStack> </HStack>
); );
......
import { Box, Text, chakra } from '@chakra-ui/react'; import { Box, Text, chakra, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
...@@ -11,9 +11,16 @@ interface Props { ...@@ -11,9 +11,16 @@ interface Props {
accuracy?: number; accuracy?: number;
accuracyUsd?: number; accuracyUsd?: number;
decimals?: string | null; decimals?: string | null;
isLoading?: boolean;
} }
const CurrencyValue = ({ value, currency = '', decimals, exchangeRate, className, accuracy, accuracyUsd }: Props) => { const CurrencyValue = ({ value, currency = '', decimals, exchangeRate, className, accuracy, accuracyUsd, isLoading }: Props) => {
if (isLoading) {
return (
<Skeleton className={ className } display="inline-block">0.00 ($0.00)</Skeleton>
);
}
if (value === undefined || value === null) { if (value === undefined || value === null) {
return ( return (
<Box as="span" className={ className }> <Box as="span" className={ className }>
......
...@@ -17,8 +17,8 @@ const TableItemActionButtons = ({ onEditClick, onDeleteClick, isLoading }: Props ...@@ -17,8 +17,8 @@ const TableItemActionButtons = ({ onEditClick, onDeleteClick, isLoading }: Props
if (isLoading) { if (isLoading) {
return ( return (
<HStack spacing={ 6 } alignSelf="flex-end"> <HStack spacing={ 6 } alignSelf="flex-end">
<Skeleton boxSize={ 5 } flexShrink={ 0 } borderRadius="base"/> <Skeleton boxSize={ 5 } flexShrink={ 0 } borderRadius="sm"/>
<Skeleton boxSize={ 5 } flexShrink={ 0 } borderRadius="base"/> <Skeleton boxSize={ 5 } flexShrink={ 0 } borderRadius="sm"/>
</HStack> </HStack>
); );
} }
......
import { Icon, useColorModeValue } from '@chakra-ui/react'; import { Icon, Skeleton, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import transactionIcon from 'icons/transactions.svg'; import transactionIcon from 'icons/transactions.svg';
...@@ -8,14 +8,17 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard'; ...@@ -8,14 +8,17 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props { interface Props {
hash: string; hash: string;
isLoading?: boolean;
} }
const TransactionSnippet = ({ hash }: Props) => { const TransactionSnippet = ({ hash, isLoading }: Props) => {
return ( return (
<Address maxW="100%"> <Address maxW="100%">
<Icon as={ transactionIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') }/> <Skeleton isLoaded={ !isLoading } boxSize={ 6 } borderRadius="base">
<AddressLink hash={ hash } fontWeight="600" type="transaction" ml={ 2 }/> <Icon as={ transactionIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') }/>
<CopyToClipboard text={ hash } ml={ 1 }/> </Skeleton>
<AddressLink hash={ hash } fontWeight="600" type="transaction" ml={ 2 } isLoading={ isLoading }/>
<CopyToClipboard text={ hash } isLoading={ isLoading }/>
</Address> </Address>
); );
}; };
......
import { HStack, VStack, Text, Icon, Flex } from '@chakra-ui/react'; import { HStack, VStack, chakra, Icon, Flex, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TWatchlistItem } from 'types/client/account'; import type { TWatchlistItem } from 'types/client/account';
...@@ -11,7 +11,7 @@ import AddressSnippet from 'ui/shared/AddressSnippet'; ...@@ -11,7 +11,7 @@ import AddressSnippet from 'ui/shared/AddressSnippet';
import CurrencyValue from 'ui/shared/CurrencyValue'; import CurrencyValue from 'ui/shared/CurrencyValue';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => { const WatchListAddressItem = ({ item, isLoading }: { item: TWatchlistItem; isLoading?: boolean }) => {
const infoItemsPaddingLeft = { base: 1, lg: 8 }; const infoItemsPaddingLeft = { base: 1, lg: 8 };
const nativeTokenData = React.useMemo(() => ({ const nativeTokenData = React.useMemo(() => ({
...@@ -22,32 +22,39 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => { ...@@ -22,32 +22,39 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
return ( return (
<VStack spacing={ 2 } align="stretch" fontWeight={ 500 }> <VStack spacing={ 2 } align="stretch" fontWeight={ 500 }>
<AddressSnippet address={ item.address }/> <AddressSnippet address={ item.address } isLoading={ isLoading }/>
<Flex fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft } flexWrap="wrap" alignItems="center" rowGap={ 1 }> <Flex fontSize="sm" pl={ infoItemsPaddingLeft } flexWrap="wrap" alignItems="center" rowGap={ 1 }>
{ appConfig.network.currency.address && ( { appConfig.network.currency.address && (
<TokenLogo <TokenLogo
data={ nativeTokenData } data={ nativeTokenData }
boxSize={ 4 } boxSize={ 5 }
borderRadius="sm" borderRadius="sm"
mr={ 2 } mr={ 2 }
isLoading={ isLoading }
/> />
) } ) }
<Text as="span" whiteSpace="pre">{ appConfig.network.currency.symbol } balance: </Text> <Skeleton isLoaded={ !isLoading } whiteSpace="pre" display="inline-flex">
<CurrencyValue <span>{ appConfig.network.currency.symbol } balance: </span>
value={ item.address_balance } <CurrencyValue
exchangeRate={ item.exchange_rate } value={ item.address_balance }
decimals={ String(appConfig.network.currency.decimals) } exchangeRate={ item.exchange_rate }
accuracy={ 2 } decimals={ String(appConfig.network.currency.decimals) }
accuracyUsd={ 2 } accuracy={ 2 }
/> accuracyUsd={ 2 }
/>
</Skeleton>
</Flex> </Flex>
{ item.tokens_count && ( { item.tokens_count && (
<HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }> <HStack spacing={ 0 } fontSize="sm" pl={ infoItemsPaddingLeft }>
<Icon as={ TokensIcon } mr={ 2 } w="17px" h="16px"/> <Skeleton isLoaded={ !isLoading } boxSize={ 5 } mr={ 2 } borderRadius="sm">
<Text>{ `Tokens:${ nbsp }` + item.tokens_count }</Text> <Icon as={ TokensIcon } boxSize={ 5 }/>
{ /* api does not provide token prices */ } </Skeleton>
{ /* <Text variant="secondary">{ `${ nbsp }($${ item.tokensUSD } USD)` }</Text> */ } <Skeleton isLoaded={ !isLoading } display="inline-flex">
<Text variant="secondary">{ `${ nbsp }(N/A)` }</Text> <span>{ `Tokens:${ nbsp }` + item.tokens_count }</span>
{ /* api does not provide token prices */ }
{ /* <Text variant="secondary">{ `${ nbsp }($${ item.tokensUSD } USD)` }</Text> */ }
<chakra.span color="text_secondary">{ `${ nbsp }(N/A)` }</chakra.span>
</Skeleton>
</HStack> </HStack>
) } ) }
{ /* api does not provide token prices */ } { /* api does not provide token prices */ }
......
import { Tag, Box, Switch, Text, HStack, Flex } from '@chakra-ui/react'; import { Box, Switch, Text, HStack, Flex, Skeleton } from '@chakra-ui/react';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
...@@ -6,6 +6,7 @@ import type { TWatchlistItem } from 'types/client/account'; ...@@ -6,6 +6,7 @@ import type { TWatchlistItem } from 'types/client/account';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import Tag from 'ui/shared/chakra/Tag';
import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
...@@ -13,11 +14,12 @@ import WatchListAddressItem from './WatchListAddressItem'; ...@@ -13,11 +14,12 @@ import WatchListAddressItem from './WatchListAddressItem';
interface Props { interface Props {
item: TWatchlistItem; item: TWatchlistItem;
isLoading?: boolean;
onEditClick: (data: TWatchlistItem) => void; onEditClick: (data: TWatchlistItem) => void;
onDeleteClick: (data: TWatchlistItem) => void; onDeleteClick: (data: TWatchlistItem) => void;
} }
const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => { const WatchListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) => {
const [ notificationEnabled, setNotificationEnabled ] = useState(item.notification_methods.email); const [ notificationEnabled, setNotificationEnabled ] = useState(item.notification_methods.email);
const [ switchDisabled, setSwitchDisabled ] = useState(false); const [ switchDisabled, setSwitchDisabled ] = useState(false);
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
...@@ -84,27 +86,27 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -84,27 +86,27 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<ListItemMobile> <ListItemMobile>
<Box maxW="100%"> <Box maxW="100%">
<WatchListAddressItem item={ item }/> <WatchListAddressItem item={ item } isLoading={ isLoading }/>
<HStack spacing={ 3 } mt={ 6 }> <HStack spacing={ 3 } mt={ 6 }>
<Text fontSize="sm" fontWeight={ 500 }>Private tag</Text> <Text fontSize="sm" fontWeight={ 500 }>Private tag</Text>
<Tag> <Tag isLoading={ isLoading } isTruncated>{ item.name }</Tag>
{ item.name }
</Tag>
</HStack> </HStack>
</Box> </Box>
<Flex alignItems="center" justifyContent="space-between" mt={ 6 } w="100%"> <Flex alignItems="center" justifyContent="space-between" mt={ 6 } w="100%">
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Email notification</Text> <Text fontSize="sm" fontWeight={ 500 }>Email notification</Text>
<Switch <Skeleton isLoaded={ !isLoading } display="inline-block">
colorScheme="blue" <Switch
size="md" colorScheme="blue"
isChecked={ notificationEnabled } size="md"
onChange={ onSwitch } isChecked={ notificationEnabled }
aria-label="Email notification" onChange={ onSwitch }
isDisabled={ switchDisabled } aria-label="Email notification"
/> isDisabled={ switchDisabled }
/>
</Skeleton>
</HStack> </HStack>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</Flex> </Flex>
</ListItemMobile> </ListItemMobile>
); );
......
import { import {
Tag,
Tr, Tr,
Td, Td,
Switch, Switch,
Skeleton,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
...@@ -11,18 +11,19 @@ import type { TWatchlistItem } from 'types/client/account'; ...@@ -11,18 +11,19 @@ import type { TWatchlistItem } from 'types/client/account';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import Tag from 'ui/shared/chakra/Tag';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import WatchListAddressItem from './WatchListAddressItem'; import WatchListAddressItem from './WatchListAddressItem';
interface Props { interface Props {
item: TWatchlistItem; item: TWatchlistItem;
isLoading?: boolean;
onEditClick: (data: TWatchlistItem) => void; onEditClick: (data: TWatchlistItem) => void;
onDeleteClick: (data: TWatchlistItem) => void; onDeleteClick: (data: TWatchlistItem) => void;
} }
const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { const WatchlistTableItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) => {
const [ notificationEnabled, setNotificationEnabled ] = useState(item.notification_methods.email); const [ notificationEnabled, setNotificationEnabled ] = useState(item.notification_methods.email);
const [ switchDisabled, setSwitchDisabled ] = useState(false); const [ switchDisabled, setSwitchDisabled ] = useState(false);
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
...@@ -88,26 +89,24 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -88,26 +89,24 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<Tr alignItems="top" key={ item.address_hash }> <Tr alignItems="top" key={ item.address_hash }>
<Td><WatchListAddressItem item={ item }/></Td> <Td><WatchListAddressItem item={ item } isLoading={ isLoading }/></Td>
<Td> <Td>
<TruncatedTextTooltip label={ item.name }> <Tag isLoading={ isLoading } isTruncated>{ item.name }</Tag>
<Tag>
{ item.name }
</Tag>
</TruncatedTextTooltip>
</Td> </Td>
<Td> <Td>
<Switch <Skeleton isLoaded={ !isLoading } display="inline-block">
colorScheme="blue" <Switch
size="md" colorScheme="blue"
isChecked={ notificationEnabled } size="md"
onChange={ onSwitch } isChecked={ notificationEnabled }
isDisabled={ switchDisabled } onChange={ onSwitch }
aria-label="Email notification" isDisabled={ switchDisabled }
/> aria-label="Email notification"
/>
</Skeleton>
</Td> </Td>
<Td> <Td>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick } isLoading={ isLoading }/>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -12,12 +12,13 @@ import type { TWatchlist, TWatchlistItem } from 'types/client/account'; ...@@ -12,12 +12,13 @@ import type { TWatchlist, TWatchlistItem } from 'types/client/account';
import WatchlistTableItem from './WatchListTableItem'; import WatchlistTableItem from './WatchListTableItem';
interface Props { interface Props {
data: TWatchlist; data?: TWatchlist;
isLoading?: boolean;
onEditClick: (data: TWatchlistItem) => void; onEditClick: (data: TWatchlistItem) => void;
onDeleteClick: (data: TWatchlistItem) => void; onDeleteClick: (data: TWatchlistItem) => void;
} }
const WatchlistTable = ({ data, onDeleteClick, onEditClick }: Props) => { const WatchlistTable = ({ data, isLoading, onDeleteClick, onEditClick }: Props) => {
return ( return (
<Table variant="simple" minWidth="600px"> <Table variant="simple" minWidth="600px">
<Thead> <Thead>
...@@ -29,10 +30,11 @@ const WatchlistTable = ({ data, onDeleteClick, onEditClick }: Props) => { ...@@ -29,10 +30,11 @@ const WatchlistTable = ({ data, onDeleteClick, onEditClick }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data?.map((item, index) => (
<WatchlistTableItem <WatchlistTableItem
key={ item.address_hash + (isLoading ? index : '') }
item={ item } item={ item }
key={ item.address_hash } isLoading={ isLoading }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
......
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