Commit e830a617 authored by Igor Stuev's avatar Igor Stuev Committed by GitHub

Merge pull request #86 from blockscout/watchlist-backend

watchlist + backend
parents 60a9e905 238330dd
import { useQuery } from '@tanstack/react-query';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
...@@ -5,6 +6,14 @@ import React from 'react'; ...@@ -5,6 +6,14 @@ import React from 'react';
import WatchList from 'ui/pages/Watchlist'; import WatchList from 'ui/pages/Watchlist';
const WatchListPage: NextPage = () => { const WatchListPage: NextPage = () => {
useQuery([ 'watchlist' ], async() => {
const response = await fetch('/api/account/watchlist/get-with-tokens');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
});
return ( return (
<> <>
<Head><title>Watch list</title></Head> <Head><title>Watch list</title></Head>
......
...@@ -6,6 +6,6 @@ const getUrl = (req: NextApiRequest) => { ...@@ -6,6 +6,6 @@ const getUrl = (req: NextApiRequest) => {
return `/account/v1/user/tags/address/${ req.query.id }`; return `/account/v1/user/tags/address/${ req.query.id }`;
}; };
const addressDeleteHandler = handler(getUrl, [ 'DELETE', 'PUT' ]); const addressEditHandler = handler(getUrl, [ 'DELETE', 'PUT' ]);
export default addressDeleteHandler; export default addressEditHandler;
...@@ -6,6 +6,6 @@ const getUrl = (req: NextApiRequest) => { ...@@ -6,6 +6,6 @@ const getUrl = (req: NextApiRequest) => {
return `/account/v1/user/tags/transaction/${ req.query.id }`; return `/account/v1/user/tags/transaction/${ req.query.id }`;
}; };
const transactionDeleteHandler = handler(getUrl, [ 'DELETE', 'PUT' ]); const transactionEditHandler = handler(getUrl, [ 'DELETE', 'PUT' ]);
export default transactionDeleteHandler; export default transactionEditHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/account/v1/user/watchlist/${ req.query.id }`;
};
const addressEditHandler = handler(getUrl, [ 'DELETE', 'PUT' ]);
export default addressEditHandler;
import type { NextApiRequest, NextApiResponse } from 'next';
import type { WatchlistAddresses } from 'types/api/account';
import type { Tokenlist } from 'types/api/tokenlist';
import type { TWatchlistItem } from 'types/client/account';
import fetch from 'lib/api/fetch';
const watchlistWithTokensHandler = async(_req: NextApiRequest, res: NextApiResponse<Array<TWatchlistItem>>) => {
const watchlistResponse = await fetch('/account/v1/user/watchlist', { method: 'GET' });
if (watchlistResponse.status !== 200) {
// eslint-disable-next-line no-console
console.error(watchlistResponse.statusText);
res.status(500).end('Unknown error');
return;
}
const watchlistData = await watchlistResponse.json() as WatchlistAddresses;
const data = await Promise.all(watchlistData.map(async item => {
const tokens = await fetch(`?module=account&action=tokenlist&address=${ item.address_hash }`);
const tokensData = await tokens.json() as Tokenlist;
return ({ ...item, tokens_count: Array.isArray(tokensData.result) ? tokensData.result.length : 0 });
}));
res.status(200).json(data);
};
export default watchlistWithTokensHandler;
import type { WatchlistAddresses } from 'types/api/account';
import handler from 'lib/api/handler';
const watchlistHandler = handler<WatchlistAddresses>(() => '/account/v1/user/watchlist', [ 'GET', 'POST' ]);
export default watchlistHandler;
...@@ -23,9 +23,13 @@ export interface NotificationDirection { ...@@ -23,9 +23,13 @@ export interface NotificationDirection {
} }
export interface NotificationSettings { export interface NotificationSettings {
_native?: NotificationDirection; 'native': NotificationDirection;
erc20?: NotificationDirection; 'ERC-20': NotificationDirection;
erc7211155?: NotificationDirection; 'ERC-721': NotificationDirection;
}
export interface NotificationMethods {
email: boolean;
} }
export interface Transaction { export interface Transaction {
...@@ -51,12 +55,14 @@ export interface UserInfo { ...@@ -51,12 +55,14 @@ export interface UserInfo {
} }
export interface WatchlistAddress { export interface WatchlistAddress {
addressHash: string; address_hash: string;
addressName: string; name: string;
addressBalance: number; address_balance: number;
coinName: string; coin_name: string;
exchangeRate?: number; exchange_rate: number;
notificationSettings: NotificationSettings; notification_settings: NotificationSettings;
notification_methods: NotificationMethods;
id: string;
} }
export interface WatchlistAddressNew { export interface WatchlistAddressNew {
......
export type Tokenlist = {
message: string;
result: Array<TokenlistItem> | string;
}
export type TokenlistItem = {
balance: number;
contractAddress: string;
decimals?: number;
id: number;
name: string;
symbol: string;
type: string;
}
// here will be types if some back-end models are needed to be extended import type { WatchlistAddress } from '../api/account';
// in order to fit the client's needs
export {}; export type TWatchlistItem = WatchlistAddress & {tokens_count: number};
export type TWatchlist = Array<TWatchlistItem>;
import { Box, Button, Text, useDisclosure } from '@chakra-ui/react'; import { Box, Button, Text, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { TWatchlistItem } from 'data/watchlist'; import type { TWatchlist, TWatchlistItem } from 'types/client/account';
import { watchlist } from 'data/watchlist';
import AccountPageHeader from 'ui/shared/AccountPageHeader'; import AccountPageHeader from 'ui/shared/AccountPageHeader';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
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 WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable'; import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
const WatchList: React.FC = () => { const WatchList: React.FC = () => {
const queryClient = useQueryClient();
const watchlistData = queryClient.getQueryData([ 'watchlist' ]) as TWatchlist;
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const [ addressModalData, setAddressModalData ] = useState<TWatchlistItem>(); const [ addressModalData, setAddressModalData ] = useState<TWatchlistItem>();
const [ deleteModalData, setDeleteModalData ] = useState<string>(); const [ deleteModalData, setDeleteModalData ] = useState<TWatchlistItem>();
const onEditClick = useCallback((data: TWatchlistItem) => { const onEditClick = useCallback((data: TWatchlistItem) => {
setAddressModalData(data); setAddressModalData(data);
...@@ -27,7 +32,7 @@ const WatchList: React.FC = () => { ...@@ -27,7 +32,7 @@ const WatchList: React.FC = () => {
}, [ addressModalProps ]); }, [ addressModalProps ]);
const onDeleteClick = useCallback((data: TWatchlistItem) => { const onDeleteClick = useCallback((data: TWatchlistItem) => {
setDeleteModalData(data.address); setDeleteModalData(data);
deleteModalProps.onOpen(); deleteModalProps.onOpen();
}, [ deleteModalProps ]); }, [ deleteModalProps ]);
...@@ -41,25 +46,33 @@ const WatchList: React.FC = () => { ...@@ -41,25 +46,33 @@ const WatchList: React.FC = () => {
<Box h="100%"> <Box h="100%">
<AccountPageHeader text="Watch list"/> <AccountPageHeader text="Watch list"/>
<Text marginBottom={ 12 }>An email notification can be sent to you when an address on your watch list sends or receives any transactions.</Text> <Text marginBottom={ 12 }>An email notification can be sent to you when an address on your watch list sends or receives any transactions.</Text>
{ Boolean(watchlist.length) && ( { !watchlistData && (
<WatchlistTable <>
data={ watchlist } <SkeletonTable columns={ [ '70%', '30%', '160px', '108px' ] }/>
onDeleteClick={ onDeleteClick } <Skeleton height="44px" width="156px" marginTop={ 8 }/>
onEditClick={ onEditClick } </>
/> ) }
{ Boolean(watchlistData?.length) && (
<>
<WatchlistTable
data={ watchlistData }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
<Box marginTop={ 8 }>
<Button
variant="primary"
size="lg"
onClick={ addressModalProps.onOpen }
>
Add address
</Button>
</Box>
</>
) } ) }
<Box marginTop={ 8 }>
<Button
variant="primary"
size="lg"
onClick={ addressModalProps.onOpen }
>
Add address
</Button>
</Box>
</Box> </Box>
<AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData }/> <AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData }/>
<DeleteAddressModal { ...deleteModalProps } onClose={ onDeleteModalClose } address={ deleteModalData }/> <DeleteAddressModal { ...deleteModalProps } onClose={ onDeleteModalClose } data={ deleteModalData }/>
</Page> </Page>
); );
}; };
......
...@@ -6,40 +6,105 @@ import { ...@@ -6,40 +6,105 @@ import {
Grid, Grid,
GridItem, GridItem,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useCallback, useEffect } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useEffect, useState } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form'; import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form'; import { useForm, Controller } from 'react-hook-form';
import type { TWatchlistItem } from 'data/watchlist'; import type { TWatchlistItem } from 'types/client/account';
import AddressInput from 'ui/shared/AddressInput'; import AddressInput from 'ui/shared/AddressInput';
import TagInput from 'ui/shared/TagInput'; import TagInput from 'ui/shared/TagInput';
const NOTIFICATIONS = [ 'xDAI', 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ]; const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721' ] as const;
const NOTIFICATIONS_NAMES = [ 'xDAI', 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ];
const ADDRESS_LENGTH = 42; const ADDRESS_LENGTH = 42;
const TAG_MAX_LENGTH = 35; const TAG_MAX_LENGTH = 35;
type Props = { type Props = {
data?: TWatchlistItem; data?: TWatchlistItem;
onClose: () => void;
} }
type Inputs = { type Inputs = {
address: string; address: string;
tag: string; tag: string;
notification: boolean; notification: boolean;
notification_settings: {
'native': {
outcoming: boolean;
incoming: boolean;
};
'ERC-721': {
outcoming: boolean;
incoming: boolean;
};
'ERC-20': {
outcoming: boolean;
incoming: boolean;
};
};
} }
const AddressForm: React.FC<Props> = ({ data }) => { type Checkboxes = 'notification' |
'notification_settings.native.outcoming' |
'notification_settings.native.incoming' |
'notification_settings.ERC-20.outcoming' |
'notification_settings.ERC-20.incoming' |
'notification_settings.ERC-721.outcoming' |
'notification_settings.ERC-721.incoming';
// TODO: mb we need to create an abstract form here?
const AddressForm: React.FC<Props> = ({ data, onClose }) => {
const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>(); const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>();
const queryClient = useQueryClient();
const { mutate } = useMutation((formData: Inputs) => {
const requestParams = {
name: formData?.tag,
address_hash: formData?.address,
notification_settings: formData.notification_settings,
notification_methods: {
email: formData.notification,
},
};
if (data) {
// edit address
return fetch(`/api/account/watchlist/${ data.id }`, { method: 'PUT', body: JSON.stringify(requestParams) });
} else {
// add address
return fetch('/api/account/watchlist', { method: 'POST', body: JSON.stringify(requestParams) });
}
}, {
onError: () => {
// eslint-disable-next-line no-console
console.log('error');
},
onSuccess: () => {
queryClient.refetchQueries([ 'watchlist' ]).then(() => {
onClose();
setPending(false);
});
},
});
const onSubmit: SubmitHandler<Inputs> = (formData) => {
setPending(true);
mutate(formData);
};
useEffect(() => { useEffect(() => {
setValue('address', data?.address || ''); const notificationsDefault = {} as Inputs['notification_settings'];
setValue('tag', data?.tag || ''); NOTIFICATIONS.forEach(n => notificationsDefault[n] = { incoming: true, outcoming: true });
setValue('notification', Boolean(data?.notification)); setValue('address', data?.address_hash || '');
setValue('tag', data?.name || '');
setValue('notification', data ? data.notification_methods.email : true);
setValue('notification_settings', data ? data.notification_settings : notificationsDefault);
}, [ setValue, data ]); }, [ setValue, data ]);
// eslint-disable-next-line no-console
const onSubmit: SubmitHandler<Inputs> = data => console.log(data);
const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'address'>}) => { const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'address'>}) => {
return <AddressInput<Inputs, 'address'> field={ field } isInvalid={ Boolean(errors.address) }/>; return <AddressInput<Inputs, 'address'> field={ field } isInvalid={ Boolean(errors.address) }/>;
}, [ errors ]); }, [ errors ]);
...@@ -48,7 +113,8 @@ const AddressForm: React.FC<Props> = ({ data }) => { ...@@ -48,7 +113,8 @@ const AddressForm: React.FC<Props> = ({ data }) => {
return <TagInput field={ field } isInvalid={ Boolean(errors.tag) }/>; return <TagInput field={ field } isInvalid={ Boolean(errors.tag) }/>;
}, [ errors ]); }, [ errors ]);
const renderCheckbox = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'notification'>}) => ( // eslint-disable-next-line react/display-name
const renderCheckbox = useCallback((text: string) => ({ field }: {field: ControllerRenderProps<Inputs, Checkboxes>}) => (
<Checkbox <Checkbox
isChecked={ field.value } isChecked={ field.value }
onChange={ field.onChange } onChange={ field.onChange }
...@@ -56,7 +122,7 @@ const AddressForm: React.FC<Props> = ({ data }) => { ...@@ -56,7 +122,7 @@ const AddressForm: React.FC<Props> = ({ data }) => {
colorScheme="blue" colorScheme="blue"
size="lg" size="lg"
> >
Email notifications { text }
</Checkbox> </Checkbox>
), []); ), []);
...@@ -87,14 +153,29 @@ const AddressForm: React.FC<Props> = ({ data }) => { ...@@ -87,14 +153,29 @@ const AddressForm: React.FC<Props> = ({ data }) => {
Please select what types of notifications you will receive Please select what types of notifications you will receive
</Text> </Text>
<Box marginBottom={ 8 }> <Box marginBottom={ 8 }>
{ /* add them to the form later */ }
<Grid templateColumns="repeat(3, max-content)" gap="20px 24px"> <Grid templateColumns="repeat(3, max-content)" gap="20px 24px">
{ NOTIFICATIONS.map((notification: string) => { { NOTIFICATIONS.map((notification: string, index: number) => {
const incomingFieldName = `notification_settings.${ notification }.incoming` as Checkboxes;
const outgoingFieldName = `notification_settings.${ notification }.outcoming` as Checkboxes;
return ( return (
<React.Fragment key={ notification }> <React.Fragment key={ notification }>
<GridItem>{ notification }</GridItem> <GridItem>{ NOTIFICATIONS_NAMES[index] }</GridItem>
<GridItem><Checkbox colorScheme="blue" size="lg">Incoming</Checkbox></GridItem> <GridItem>
<GridItem><Checkbox colorScheme="blue" size="lg">Outgoing</Checkbox></GridItem> <Controller
name={ incomingFieldName }
control={ control }
render={ renderCheckbox('Incoming') }
/>
</GridItem>
<GridItem>
<Controller
name={ outgoingFieldName }
control={ control }
render={ renderCheckbox('Outgoing') }
/>
</GridItem>
</React.Fragment> </React.Fragment>
); );
}) } }) }
...@@ -102,15 +183,16 @@ const AddressForm: React.FC<Props> = ({ data }) => { ...@@ -102,15 +183,16 @@ const AddressForm: React.FC<Props> = ({ data }) => {
</Box> </Box>
<Text variant="secondary" fontSize="sm" marginBottom={ 5 }>Notification methods</Text> <Text variant="secondary" fontSize="sm" marginBottom={ 5 }>Notification methods</Text>
<Controller <Controller
name="notification" name={ 'notification' as Checkboxes }
control={ control } control={ control }
render={ renderCheckbox } render={ renderCheckbox('Email notifications') }
/> />
<Box marginTop={ 8 }> <Box marginTop={ 8 }>
<Button <Button
size="lg" size="lg"
variant="primary" variant="primary"
onClick={ handleSubmit(onSubmit) } onClick={ handleSubmit(onSubmit) }
isLoading={ pending }
disabled={ Object.keys(errors).length > 0 } disabled={ Object.keys(errors).length > 0 }
> >
{ data ? 'Save changes' : 'Add address' } { data ? 'Save changes' : 'Add address' }
......
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { TWatchlistItem } from 'data/watchlist'; import type { TWatchlistItem } from 'types/client/account';
import FormModal from 'ui/shared/FormModal'; import FormModal from 'ui/shared/FormModal';
import AddressForm from './AddressForm'; import AddressForm from './AddressForm';
...@@ -16,8 +17,8 @@ const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { ...@@ -16,8 +17,8 @@ const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const text = 'An email notification can be sent to you when an address on your watch list sends or receives any transactions.'; const text = 'An email notification can be sent to you when an address on your watch list sends or receives any transactions.';
const renderForm = useCallback(() => { const renderForm = useCallback(() => {
return <AddressForm data={ data }/>; return <AddressForm data={ data } onClose={ onClose }/>;
}, [ data ]); }, [ data, onClose ]);
return ( return (
<FormModal<TWatchlistItem> <FormModal<TWatchlistItem>
isOpen={ isOpen } isOpen={ isOpen }
......
import { Text } from '@chakra-ui/react'; import { Text } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { TWatchlistItem } from 'types/client/account';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
address?: string; data?: TWatchlistItem;
} }
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, address }) => { const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const [ pending, setPending ] = useState(false);
const queryClient = useQueryClient();
const { mutate } = useMutation(() => {
return fetch(`/api/account/watchlist/${ data?.id }`, { method: 'DELETE' });
}, {
onError: () => {
// eslint-disable-next-line no-console
console.log('error');
},
onSuccess: () => {
queryClient.refetchQueries([ 'watchlist' ]).then(() => {
onClose();
setPending(false);
});
},
});
const onDelete = useCallback(() => { const onDelete = useCallback(() => {
// eslint-disable-next-line no-console setPending(true);
console.log('delete', address); mutate();
}, [ address ]); }, [ mutate ]);
const address = data?.address_hash;
const renderText = useCallback(() => { const renderText = useCallback(() => {
return ( return (
...@@ -28,6 +52,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, address }) => { ...@@ -28,6 +52,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, address }) => {
onDelete={ onDelete } onDelete={ onDelete }
title="Remove address from watch list" title="Remove address from watch list"
renderContent={ renderText } renderContent={ renderText }
pending={ pending }
/> />
); );
}; };
......
import { Link, HStack, VStack, Image, Text, Icon, useColorModeValue } from '@chakra-ui/react'; import { HStack, VStack, Image, Text, Icon, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TWatchlistItem } from 'data/watchlist'; 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 AddressIcon from 'ui/shared/AddressIcon';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip';
// now this component works only for xDAI
// for other networks later we will use config or smth
const DECIMALS = 18;
const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => { const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
const mainTextColor = useColorModeValue('gray.700', 'gray.50'); const mainTextColor = useColorModeValue('gray.700', 'gray.50');
const nativeBalance = ((item.address_balance || 0) / 10 ** DECIMALS).toFixed(1);
const nativeBalanceUSD = item.exchange_rate ? `$${ Number(nativeBalance) * item.exchange_rate } USD` : 'N/A';
return ( return (
<HStack spacing={ 3 } align="top"> <HStack spacing={ 3 } align="top">
<AddressIcon address={ item.address }/> <AddressIcon address={ item.address_hash }/>
<VStack spacing={ 2 } align="stretch" overflow="hidden" fontWeight={ 500 } color="gray.700"> <VStack spacing={ 2 } align="stretch" overflow="hidden" fontWeight={ 500 } color="gray.700">
<AddressLinkWithTooltip address={ item.address }/> <AddressLinkWithTooltip address={ item.address_hash }/>
{ item.tokenBalance && ( <HStack spacing={ 0 } fontSize="sm" h={ 6 }>
<HStack spacing={ 0 } fontSize="sm" h={ 6 }> <Image src="/xdai.png" alt="chain-logo" marginRight="10px" w="16px" h="16px"/>
<Image src="/xdai.png" alt="chain-logo" marginRight="10px" w="16px" h="16px"/> <Text color={ mainTextColor }>{ `xDAI balance:${ nbsp }` + nativeBalance }</Text>
<Text color={ mainTextColor }>{ `xDAI balance:${ nbsp }` + item.tokenBalance }</Text> <Text variant="secondary">{ `${ nbsp }(${ nativeBalanceUSD })` }</Text>
<Text variant="secondary">{ `${ nbsp }($${ item.tokenBalanceUSD } USD)` }</Text> </HStack>
</HStack> { item.tokens_count && (
) }
{ item.tokensAmount && (
<HStack spacing={ 0 } fontSize="sm" h={ 6 }> <HStack spacing={ 0 } fontSize="sm" h={ 6 }>
<Icon as={ TokensIcon } marginRight="10px" w="17px" h="16px"/> <Icon as={ TokensIcon } marginRight="10px" w="17px" h="16px"/>
<Text color={ mainTextColor }>{ `Tokens:${ nbsp }` + item.tokensAmount }</Text> <Text color={ mainTextColor }>{ `Tokens:${ nbsp }` + item.tokens_count }</Text>
<Text variant="secondary">{ `${ nbsp }($${ item.tokensUSD } USD)` }</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.totalUSD && ( { /* api does not provide token prices */ }
{ /* { item.address_balance && (
<HStack spacing={ 0 } fontSize="sm" h={ 6 }> <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> </HStack>
); );
......
...@@ -5,9 +5,11 @@ import { ...@@ -5,9 +5,11 @@ import {
Switch, Switch,
HStack, HStack,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import { useMutation } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react';
import type { TWatchlistItem } from 'types/client/account';
import type { TWatchlistItem } from 'data/watchlist';
import DeleteButton from 'ui/shared/DeleteButton'; import DeleteButton from 'ui/shared/DeleteButton';
import EditButton from 'ui/shared/EditButton'; import EditButton from 'ui/shared/EditButton';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
...@@ -21,6 +23,7 @@ interface Props { ...@@ -21,6 +23,7 @@ interface Props {
} }
const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const [ notificationEnabled, setNotificationEnabled ] = useState(item.notification_methods.email);
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
}, [ item, onEditClick ]); }, [ item, onEditClick ]);
...@@ -29,17 +32,34 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -29,17 +32,34 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return onDeleteClick(item); return onDeleteClick(item);
}, [ item, onDeleteClick ]); }, [ 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 ( return (
<Tr alignItems="top" key={ item.address }> <Tr alignItems="top" key={ item.address_hash }>
<Td><WatchListAddressItem item={ item }/></Td> <Td><WatchListAddressItem item={ item }/></Td>
<Td> <Td>
<TruncatedTextTooltip label={ item.tag }> <TruncatedTextTooltip label={ item.name }>
<Tag variant="gray" lineHeight="24px"> <Tag variant="gray" lineHeight="24px">
{ item.tag } { item.name }
</Tag> </Tag>
</TruncatedTextTooltip> </TruncatedTextTooltip>
</Td> </Td>
<Td><Switch colorScheme="blue" size="md" isChecked={ item.notification }/></Td> <Td><Switch colorScheme="blue" size="md" isChecked={ notificationEnabled } onChange={ onSwitch }/></Td>
<Td> <Td>
<HStack spacing={ 6 }> <HStack spacing={ 6 }>
<EditButton onClick={ onItemEditClick }/> <EditButton onClick={ onItemEditClick }/>
......
...@@ -8,7 +8,7 @@ import { ...@@ -8,7 +8,7 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TWatchlist, TWatchlistItem } from 'data/watchlist'; import type { TWatchlist, TWatchlistItem } from 'types/client/account';
import WatchlistTableItem from './WatchListTableItem'; import WatchlistTableItem from './WatchListTableItem';
...@@ -34,7 +34,7 @@ const WatchlistTable = ({ data, onDeleteClick, onEditClick }: Props) => { ...@@ -34,7 +34,7 @@ const WatchlistTable = ({ data, onDeleteClick, onEditClick }: Props) => {
{ data.map((item) => ( { data.map((item) => (
<WatchlistTableItem <WatchlistTableItem
item={ item } item={ item }
key={ item.address } key={ item.address_hash }
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