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 { 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 = {
id: 1,
export const PRIVATE_TAG_TX: TransactionTag = {
id: '1',
name: 'placeholder',
transaction_hash: TX_HASH,
};
export const PUBLIC_TAG = {
export const PUBLIC_TAG: PublicTag = {
additional_comment: 'my comment',
addresses: [ ADDRESS_HASH ],
addresses_with_info: [ ADDRESS_PARAMS ],
company: null,
company: 'Blockscout',
email: 'john.doe@example.com',
full_name: 'name',
id: 1,
is_owner: true,
submission_date: '2022-11-11T11:11:11.000000Z',
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';
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,33 +20,38 @@ 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 watchlistAddresses = await apiFetch<'watchlist', Array<WatchlistAddress>>('watchlist');
const { data, isPlaceholderData, isError, error } = useQuery<unknown, ResourceError, TWatchlist>(
[ resourceKey('watchlist') ],
async() => {
const watchlistAddresses = await apiFetch<'watchlist', Array<WatchlistAddress>>('watchlist');
if (!Array.isArray(watchlistAddresses)) {
return;
}
const watchlistTokens = await Promise.all(watchlistAddresses.map(({ address }) => {
if (!address?.hash) {
return Promise.resolve(0);
if (!Array.isArray(watchlistAddresses)) {
return;
}
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 0;
});
}));
return watchlistAddresses.map((item, index) => ({ ...item, tokens_count: watchlistTokens[index] }));
});
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) => {
if ('result' in response && Array.isArray(response.result)) {
return response.result.length;
}
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 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 (
<>
{ description }
{ loader }
</>
);
}
const list = isMobile ? (
<Box>
{ data.map((item) => (
<WatchListItem
item={ item }
key={ item.address_hash }
const list = (
<>
<Box display={{ base: 'block', lg: 'none' }}>
{ data?.map((item, index) => (
<WatchListItem
key={ item.address_hash + (isPlaceholderData ? index : '') }
item={ item }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<WatchlistTable
data={ data }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Box>
) : (
<WatchlistTable
data={ data }
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
Add address
</Button>
</Box>
</Skeleton>
<AddressModal
{ ...addressModalProps }
onClose={ onAddressModalClose }
......
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 }>
......
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,38 +11,45 @@ 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 };
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
hash={ appConfig.network.currency.address }
name={ appConfig.network.name }
boxSize={ 4 }
boxSize={ 5 }
borderRadius="sm"
mr={ 2 }
isLoading={ isLoading }
/>
) }
<Text as="span" whiteSpace="pre">{ appConfig.network.currency.symbol } balance: </Text>
<CurrencyValue
value={ item.address_balance }
exchangeRate={ item.exchange_rate }
decimals={ String(appConfig.network.currency.decimals) }
accuracy={ 2 }
accuracyUsd={ 2 }
/>
<Skeleton isLoaded={ !isLoading } whiteSpace="pre" display="inline-flex">
<span>{ appConfig.network.currency.symbol } balance: </span>
<CurrencyValue
value={ item.address_balance }
exchangeRate={ item.exchange_rate }
decimals={ String(appConfig.network.currency.decimals) }
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>
{ /* api does not provide token prices */ }
{ /* <Text variant="secondary">{ `${ nbsp }($${ item.tokensUSD } USD)` }</Text> */ }
<Text variant="secondary">{ `${ nbsp }(N/A)` }</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> */ }
<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,27 +86,27 @@ 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>
<Switch
colorScheme="blue"
size="md"
isChecked={ notificationEnabled }
onChange={ onSwitch }
aria-label="Email notification"
isDisabled={ switchDisabled }
/>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<Switch
colorScheme="blue"
size="md"
isChecked={ notificationEnabled }
onChange={ onSwitch }
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,26 +89,24 @@ 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>
<Switch
colorScheme="blue"
size="md"
isChecked={ notificationEnabled }
onChange={ onSwitch }
isDisabled={ switchDisabled }
aria-label="Email notification"
/>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<Switch
colorScheme="blue"
size="md"
isChecked={ notificationEnabled }
onChange={ onSwitch }
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