Commit af2ec198 authored by tom's avatar tom

watchlist page

parent ba040562
import { Box, Button, Text, Skeleton, useDisclosure } from '@chakra-ui/react'; import { Box, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { TWatchlist, TWatchlistItem } from 'types/client/account'; import type { TWatchlist, TWatchlistItem } from 'types/client/account';
import fetch from 'lib/client/fetch'; import fetch from 'lib/client/fetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import AccountPageHeader from 'ui/shared/AccountPageHeader'; import AccountPageHeader from 'ui/shared/AccountPageHeader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/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 WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable'; import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
const WatchList: React.FC = () => { const WatchList: React.FC = () => {
...@@ -19,6 +22,7 @@ const WatchList: React.FC = () => { ...@@ -19,6 +22,7 @@ const WatchList: React.FC = () => {
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const [ addressModalData, setAddressModalData ] = useState<TWatchlistItem>(); const [ addressModalData, setAddressModalData ] = useState<TWatchlistItem>();
const [ deleteModalData, setDeleteModalData ] = useState<TWatchlistItem>(); const [ deleteModalData, setDeleteModalData ] = useState<TWatchlistItem>();
...@@ -44,9 +48,9 @@ const WatchList: React.FC = () => { ...@@ -44,9 +48,9 @@ const WatchList: React.FC = () => {
}, [ deleteModalProps ]); }, [ deleteModalProps ]);
const description = ( const description = (
<Text marginBottom={ 12 }> <AccountPageDescription>
An email notification can be sent to you when an address on your watch list sends or receives any transactions. An email notification can be sent to you when an address on your watch list sends or receives any transactions.
</Text> </AccountPageDescription>
); );
let content; let content;
...@@ -61,15 +65,29 @@ const WatchList: React.FC = () => { ...@@ -61,15 +65,29 @@ const WatchList: React.FC = () => {
} else if (isError) { } else if (isError) {
content = <DataFetchAlert/>; content = <DataFetchAlert/>;
} else { } else {
content = ( const list = isMobile ? (
<> <Box>
{ Boolean(data?.length) && ( { data.map((item) => (
<WatchlistTable <WatchListItem
data={ data } item={ item }
key={ item.address_hash }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
) } )) }
</Box>
) : (
<WatchlistTable
data={ data }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
);
content = (
<>
{ description }
{ Boolean(data?.length) && list }
<Box marginTop={ 8 }> <Box marginTop={ 8 }>
<Button <Button
variant="primary" variant="primary"
......
import { Tooltip, IconButton, Icon } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import DeleteIcon from 'icons/delete.svg';
type Props = {
onClick: () => void;
}
const DeleteButton = ({ onClick }: Props) => {
const onFocusCapture = useCallback((e: React.SyntheticEvent) => e.stopPropagation(), []);
return (
<Tooltip label="Delete">
<IconButton
aria-label="delete"
variant="icon"
w="30px"
h="30px"
onClick={ onClick }
icon={ <Icon as={ DeleteIcon } w="20px" h="20px"/> }
onFocusCapture={ onFocusCapture }
/>
</Tooltip>
);
};
export default DeleteButton;
import { Tooltip, IconButton, Icon } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import EditIcon from 'icons/edit.svg';
type Props = {
onClick: () => void;
}
const EditButton = ({ onClick }: Props) => {
const onFocusCapture = useCallback((e: React.SyntheticEvent) => e.stopPropagation(), []);
return (
<Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="icon"
w="30px"
h="30px"
onClick={ onClick }
icon={ <Icon as={ EditIcon } w="20px" h="20px"/> }
onFocusCapture={ onFocusCapture }
/>
</Tooltip>
);
};
export default EditButton;
...@@ -46,7 +46,7 @@ export default function FormModal<TData>({ ...@@ -46,7 +46,7 @@ export default function FormModal<TData>({
<ModalCloseButton/> <ModalCloseButton/>
<ModalBody mb={ 0 }> <ModalBody mb={ 0 }>
{ (isAlertVisible || text) && ( { (isAlertVisible || text) && (
<Box marginBottom={ 12 }> <Box marginBottom={{ base: 6, lg: 12 }}>
{ text && ( { text && (
<Text lineHeight="30px" mb={ 3 }> <Text lineHeight="30px" mb={ 3 }>
{ text } { text }
......
...@@ -150,7 +150,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -150,7 +150,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
return ( return (
<> <>
<Box marginBottom={ 5 } marginTop={ 5 }> <Box marginBottom={ 5 }>
<Controller <Controller
name="address" name="address"
control={ control } control={ control }
...@@ -176,7 +176,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -176,7 +176,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Box marginBottom={ 8 }> <Box marginBottom={ 8 }>
<AddressFormNotifications control={ control }/> <AddressFormNotifications control={ control }/>
</Box> </Box>
<Text variant="secondary" fontSize="sm" marginBottom={ 5 }>Notification methods</Text> <Text variant="secondary" fontSize="sm" marginBottom={{ base: '10px', lg: 5 }}>Notification methods</Text>
<Controller <Controller
name={ 'notification' as Checkboxes } name={ 'notification' as Checkboxes }
control={ control } control={ control }
......
...@@ -20,13 +20,21 @@ export default function AddressFormNotifications<Inputs extends FieldValues, Che ...@@ -20,13 +20,21 @@ export default function AddressFormNotifications<Inputs extends FieldValues, Che
), []); ), []);
return ( return (
<Grid templateColumns="repeat(3, max-content)" gap="20px 24px"> <Grid templateColumns={{ base: 'repeat(2, max-content)', lg: 'repeat(3, max-content)' }} gap={{ base: '10px 24px', lg: '20px 24px' }}>
{ NOTIFICATIONS.map((notification: string, index: number) => { { NOTIFICATIONS.map((notification: string, index: number) => {
const incomingFieldName = `notification_settings.${ notification }.incoming` as Checkboxes; const incomingFieldName = `notification_settings.${ notification }.incoming` as Checkboxes;
const outgoingFieldName = `notification_settings.${ notification }.outcoming` as Checkboxes; const outgoingFieldName = `notification_settings.${ notification }.outcoming` as Checkboxes;
return ( return (
<React.Fragment key={ notification }> <React.Fragment key={ notification }>
<GridItem>{ NOTIFICATIONS_NAMES[index] }</GridItem> <GridItem
gridColumnStart={{ base: 1, lg: 1 }}
gridColumnEnd={{ base: 3, lg: 1 }}
_notFirst={{
mt: { base: 3, lg: 0 },
}}
>
{ NOTIFICATIONS_NAMES[index] }
</GridItem>
<GridItem> <GridItem>
<Controller <Controller
name={ incomingFieldName } name={ incomingFieldName }
......
...@@ -5,6 +5,7 @@ import React, { useCallback } from 'react'; ...@@ -5,6 +5,7 @@ import React, { useCallback } from 'react';
import type { TWatchlistItem, TWatchlist } from 'types/client/account'; import type { TWatchlistItem, TWatchlist } from 'types/client/account';
import fetch from 'lib/client/fetch'; import fetch from 'lib/client/fetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
type Props = { type Props = {
...@@ -15,6 +16,7 @@ type Props = { ...@@ -15,6 +16,7 @@ type Props = {
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile();
const mutationFn = useCallback(() => { const mutationFn = useCallback(() => {
return fetch(`/api/account1/watchlist/${ data?.id }`, { method: 'DELETE' }); return fetch(`/api/account1/watchlist/${ data?.id }`, { method: 'DELETE' });
...@@ -29,10 +31,11 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { ...@@ -29,10 +31,11 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const address = data?.address_hash; const address = data?.address_hash;
const renderModalContent = useCallback(() => { const renderModalContent = useCallback(() => {
const addressString = isMobile ? [ address.slice(0, 4), address.slice(-4) ].join('...') : address;
return ( return (
<Text display="flex">Address <Text fontWeight="600" whiteSpace="pre"> { address || 'address' } </Text> will be deleted</Text> <Text display="flex">Address <Text fontWeight="600" whiteSpace="pre"> { addressString || 'address' } </Text> will be deleted</Text>
); );
}, [ address ]); }, [ address, isMobile ]);
return ( return (
<DeleteModal <DeleteModal
......
...@@ -6,8 +6,7 @@ import type { TWatchlistItem } from 'types/client/account'; ...@@ -6,8 +6,7 @@ import type { TWatchlistItem } from 'types/client/account';
import TokensIcon from 'icons/tokens.svg'; import TokensIcon from 'icons/tokens.svg';
// import WalletIcon from 'icons/wallet.svg'; // import WalletIcon from 'icons/wallet.svg';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import AddressIcon from 'ui/shared/AddressIcon'; import AddressSnippet from 'ui/shared/AddressSnippet';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip';
// now this component works only for xDAI // now this component works only for xDAI
// for other networks later we will use config or smth // for other networks later we will use config or smth
...@@ -18,36 +17,34 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => { ...@@ -18,36 +17,34 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
const nativeBalance = ((item.address_balance || 0) / 10 ** DECIMALS).toFixed(1); const nativeBalance = ((item.address_balance || 0) / 10 ** DECIMALS).toFixed(1);
const nativeBalanceUSD = item.exchange_rate ? `$${ Number(nativeBalance) * item.exchange_rate } USD` : 'N/A'; const nativeBalanceUSD = item.exchange_rate ? `$${ Number(nativeBalance) * item.exchange_rate } USD` : 'N/A';
const infoItemsPaddingLeft = { base: 0, lg: 10 };
return ( return (
<HStack spacing={ 3 } align="top"> <VStack spacing={ 2 } align="stretch" overflow="hidden" fontWeight={ 500 } color="gray.700">
<AddressIcon address={ item.address_hash }/> <AddressSnippet address={ item.address_hash }/>
<VStack spacing={ 2 } align="stretch" overflow="hidden" fontWeight={ 500 } color="gray.700"> <HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
<AddressLinkWithTooltip address={ item.address_hash }/> <Image src="/xdai.png" alt="chain-logo" marginRight="10px" w="16px" h="16px"/>
<HStack spacing={ 0 } fontSize="sm" h={ 6 }> <Text color={ mainTextColor }>{ `xDAI balance:${ nbsp }` + nativeBalance }</Text>
<Image src="/xdai.png" alt="chain-logo" marginRight="10px" w="16px" h="16px"/> <Text variant="secondary">{ `${ nbsp }(${ nativeBalanceUSD })` }</Text>
<Text color={ mainTextColor }>{ `xDAI balance:${ nbsp }` + nativeBalance }</Text> </HStack>
<Text variant="secondary">{ `${ nbsp }(${ nativeBalanceUSD })` }</Text> { item.tokens_count && (
<HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
<Icon as={ TokensIcon } marginRight="10px" w="17px" h="16px"/>
<Text color={ mainTextColor }>{ `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> </HStack>
{ item.tokens_count && ( ) }
<HStack spacing={ 0 } fontSize="sm" h={ 6 }> { /* api does not provide token prices */ }
<Icon as={ TokensIcon } marginRight="10px" w="17px" h="16px"/> { /* { item.address_balance && (
<Text color={ mainTextColor }>{ `Tokens:${ nbsp }` + item.tokens_count }</Text> <HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
{ /* api does not provide token prices */ }
{ /* <Text variant="secondary">{ `${ nbsp }($${ item.tokensUSD } USD)` }</Text> */ }
<Text variant="secondary">{ `${ nbsp }(N/A)` }</Text>
</HStack>
) }
{ /* api does not provide token prices */ }
{ /* { item.address_balance && (
<HStack spacing={ 0 } fontSize="sm" h={ 6 }>
<Icon as={ WalletIcon } marginRight="10px" w="16px" h="16px"/> <Icon as={ WalletIcon } marginRight="10px" w="16px" h="16px"/>
<Text color={ mainTextColor }>{ `Net worth:${ nbsp }` }</Text> <Text color={ mainTextColor }>{ `Net worth:${ nbsp }` }</Text>
<Link href="#">{ `$${ item.totalUSD } USD` }</Link> <Link href="#">{ `$${ item.totalUSD } USD` }</Link>
</HStack> </HStack>
) } */ } ) } */ }
</VStack> </VStack>
</HStack>
); );
}; };
......
import { Tag, Box, Switch, Text, HStack, Flex } from '@chakra-ui/react';
import { useMutation } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { TWatchlistItem } from 'types/client/account';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import WatchListAddressItem from './WatchListAddressItem';
interface Props {
item: TWatchlistItem;
onEditClick: (data: TWatchlistItem) => void;
onDeleteClick: (data: TWatchlistItem) => void;
}
const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const [ notificationEnabled, setNotificationEnabled ] = useState(item.notification_methods.email);
const onItemEditClick = useCallback(() => {
return onEditClick(item);
}, [ item, onEditClick ]);
const onItemDeleteClick = useCallback(() => {
return onDeleteClick(item);
}, [ item, onDeleteClick ]);
const { mutate } = useMutation(() => {
const data = { ...item, notification_methods: { email: !notificationEnabled } };
return fetch(`/api/account/watchlist/${ item.id }`, { method: 'PUT', body: JSON.stringify(data) });
}, {
onError: () => {
// eslint-disable-next-line no-console
console.log('error');
},
onSuccess: () => {
setNotificationEnabled(prevState => !prevState);
},
});
const onSwitch = useCallback(() => {
return mutate();
}, [ mutate ]);
return (
<AccountListItemMobile>
<Box maxW="100%">
<WatchListAddressItem item={ item }/>
<HStack spacing={ 3 } mt={ 6 }>
<Text fontSize="sm" fontWeight={ 500 }>Private tag</Text>
<Tag variant="gray" lineHeight="24px">
{ 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 }/>
</HStack>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
</Flex>
</AccountListItemMobile>
);
};
export default WatchListItem;
...@@ -3,15 +3,13 @@ import { ...@@ -3,15 +3,13 @@ import {
Tr, Tr,
Td, Td,
Switch, Switch,
HStack,
} 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';
import type { TWatchlistItem } from 'types/client/account'; import type { TWatchlistItem } from 'types/client/account';
import DeleteButton from 'ui/shared/DeleteButton'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import EditButton from 'ui/shared/EditButton';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import WatchListAddressItem from './WatchListAddressItem'; import WatchListAddressItem from './WatchListAddressItem';
...@@ -61,10 +59,7 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -61,10 +59,7 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
</Td> </Td>
<Td><Switch colorScheme="blue" size="md" isChecked={ notificationEnabled } onChange={ onSwitch }/></Td> <Td><Switch colorScheme="blue" size="md" isChecked={ notificationEnabled } onChange={ onSwitch }/></Td>
<Td> <Td>
<HStack spacing={ 6 }> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
<EditButton onClick={ onItemEditClick }/>
<DeleteButton onClick={ onItemDeleteClick }/>
</HStack>
</Td> </Td>
</Tr> </Tr>
); );
......
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