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