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,7 +20,9 @@ import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable'; ...@@ -22,7 +20,9 @@ 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>(
[ resourceKey('watchlist') ],
async() => {
const watchlistAddresses = await apiFetch<'watchlist', Array<WatchlistAddress>>('watchlist'); const watchlistAddresses = await apiFetch<'watchlist', Array<WatchlistAddress>>('watchlist');
if (!Array.isArray(watchlistAddresses)) { if (!Array.isArray(watchlistAddresses)) {
...@@ -43,12 +43,15 @@ const WatchList: React.FC = () => { ...@@ -43,12 +43,15 @@ const WatchList: React.FC = () => {
})); }));
return watchlistAddresses.map((item, index) => ({ ...item, tokens_count: watchlistTokens[index] })); 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/> : (
<>
<SkeletonTable columns={ [ '70%', '30%', '160px', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/>
</>
);
return (
<> <>
{ description } <Box display={{ base: 'block', lg: 'none' }}>
{ loader } { data?.map((item, index) => (
</>
);
}
const list = isMobile ? (
<Box>
{ data.map((item) => (
<WatchListItem <WatchListItem
key={ item.address_hash + (isPlaceholderData ? index : '') }
item={ item } item={ item }
key={ item.address_hash } isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
)) } )) }
</Box> </Box>
) : ( <Box display={{ base: 'none', lg: 'block' }}>
<WatchlistTable <WatchlistTable
data={ data } data={ data }
isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
</Box>
</>
); );
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,23 +11,25 @@ import AddressSnippet from 'ui/shared/AddressSnippet'; ...@@ -11,23 +11,25 @@ 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">
<span>{ appConfig.network.currency.symbol } balance: </span>
<CurrencyValue <CurrencyValue
value={ item.address_balance } value={ item.address_balance }
exchangeRate={ item.exchange_rate } exchangeRate={ item.exchange_rate }
...@@ -35,14 +37,19 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => { ...@@ -35,14 +37,19 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
accuracy={ 2 } accuracy={ 2 }
accuracyUsd={ 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 }/>
</Skeleton>
<Skeleton isLoaded={ !isLoading } display="inline-flex">
<span>{ `Tokens:${ nbsp }` + item.tokens_count }</span>
{ /* api does not provide token prices */ } { /* api does not provide token prices */ }
{ /* <Text variant="secondary">{ `${ nbsp }($${ item.tokensUSD } USD)` }</Text> */ } { /* <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> </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,17 +86,16 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -84,17 +86,16 @@ 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>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<Switch <Switch
colorScheme="blue" colorScheme="blue"
size="md" size="md"
...@@ -103,8 +104,9 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -103,8 +104,9 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
aria-label="Email notification" aria-label="Email notification"
isDisabled={ switchDisabled } 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,15 +89,12 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -88,15 +89,12 @@ 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>
<Skeleton isLoaded={ !isLoading } display="inline-block">
<Switch <Switch
colorScheme="blue" colorScheme="blue"
size="md" size="md"
...@@ -105,9 +103,10 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -105,9 +103,10 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
isDisabled={ switchDisabled } isDisabled={ switchDisabled }
aria-label="Email notification" 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