Commit 5ff6ec76 authored by tom's avatar tom

skeletons for watchlist page

parent 3d4d8575
import type { PublicTag, AddressTag, TransactionTag } 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'; 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 = { export const PRIVATE_TAG_TX: TransactionTag = {
id: 1, id: '1',
name: 'placeholder', name: 'placeholder',
transaction_hash: TX_HASH, transaction_hash: TX_HASH,
}; };
export const PUBLIC_TAG = { export const PUBLIC_TAG: PublicTag = {
additional_comment: 'my comment', additional_comment: 'my comment',
addresses: [ ADDRESS_HASH ], addresses: [ ADDRESS_HASH ],
addresses_with_info: [ ADDRESS_PARAMS ], addresses_with_info: [ ADDRESS_PARAMS ],
company: null, company: 'Blockscout',
email: 'john.doe@example.com', email: 'john.doe@example.com',
full_name: 'name', full_name: 'name',
id: 1, id: 1,
is_owner: true, is_owner: true,
submission_date: '2022-11-11T11:11:11.000000Z',
tags: 'placeholder', tags: 'placeholder',
website: null, 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,
}; };
...@@ -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 }
......
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 }>
......
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,38 +11,45 @@ import AddressSnippet from 'ui/shared/AddressSnippet'; ...@@ -11,38 +11,45 @@ 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 };
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
hash={ appConfig.network.currency.address } hash={ appConfig.network.currency.address }
name={ appConfig.network.name } name={ appConfig.network.name }
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