Commit b8e5ff57 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge pull request #35 from blockscout/private-tag

private tag
parents 9c7b5e2a 78605faa
export const privateTagsAddress = [
{
address: '0x4831c121879d3de0e2b181d9d55e9b0724f5d926',
tag: 'some_tag',
},
{
address: '0x8c461F78760988c4135e363a87dd736f8b671ff0',
tag: 'some_other_tag',
},
{
address: '0x930F381E649c84579Ef58117E923714964C55D16',
tag: '12345678901234567890123456789012345',
},
];
export type TPrivateTagsAddress = Array<TPrivateTagsAddressItem>
export type TPrivateTagsAddressItem = {
address: string;
tag: string;
}
export const privateTagsTransaction = [
{
transaction: '0x44b51ef7746ff48f74f45699d33557faa96059eb8655fdd7bf14a5f529ea3528',
tag: 'some_tag',
},
{
transaction: '0x44b51ef7746ff48f74f45699d33557faa96059eb8655fdd7bf14a5f529ea9999',
tag: 'some_other_tag',
},
];
export type TPrivateTagsTransaction = Array<TPrivateTagsTransactionItem>
export type TPrivateTagsTransactionItem = {
transaction: string;
tag: string;
}
import React from 'react';
import type { NextPage } from 'next';
import Head from 'next/head'
import PrivateTags from '../ui/pages/PrivateTags';
const PrivateTagsPage: NextPage = () => {
return (
<>
<Head><title>Private tags</title></Head>
<PrivateTags/>
</>
);
}
export default PrivateTagsPage
...@@ -8,6 +8,7 @@ const Button: ComponentStyleConfig = { ...@@ -8,6 +8,7 @@ const Button: ComponentStyleConfig = {
primary: { primary: {
bg: 'blue.600', bg: 'blue.600',
color: 'white', color: 'white',
fontWeight: 600,
_hover: { _hover: {
bg: 'blue.400', bg: 'blue.400',
_disabled: { _disabled: {
...@@ -21,6 +22,7 @@ const Button: ComponentStyleConfig = { ...@@ -21,6 +22,7 @@ const Button: ComponentStyleConfig = {
secondary: { secondary: {
bg: 'white', bg: 'white',
color: 'blue.600', color: 'blue.600',
fontWeight: 600,
borderColor: 'blue.600', borderColor: 'blue.600',
border: '2px solid', border: '2px solid',
_hover: { _hover: {
......
...@@ -42,6 +42,9 @@ const Table: ComponentMultiStyleConfig = { ...@@ -42,6 +42,9 @@ const Table: ComponentMultiStyleConfig = {
th: { th: {
border: 0, border: 0,
}, },
td: {
borderColor: 'gray.200',
},
}, },
}, },
} }
......
import type { ComponentStyleConfig } from '@chakra-ui/theme';
import type {
PartsStyleFunction,
} from '@chakra-ui/theme-tools'
import { getColor } from '@chakra-ui/theme-tools'
import type { tabsAnatomy as parts } from '@chakra-ui/anatomy'
const variantSoftRounded: PartsStyleFunction<typeof parts> = (props) => {
const { colorScheme: c, theme } = props
return {
tab: {
borderRadius: '12px',
fontWeight: 'semibold',
color: 'gray.600',
_selected: {
color: getColor(theme, `${ c }.700`),
bg: getColor(theme, `${ c }.50`),
},
},
}
}
const Tabs: ComponentStyleConfig = {
variants: {
'soft-rounded': variantSoftRounded,
},
}
export default Tabs;
...@@ -3,6 +3,7 @@ import Modal from './Modal'; ...@@ -3,6 +3,7 @@ import Modal from './Modal';
import Table from './Table'; import Table from './Table';
import Form from './Form'; import Form from './Form';
import Input from './Input'; import Input from './Input';
import Tabs from './Tabs';
import Tag from './Tag'; import Tag from './Tag';
import Tooltip from './Tooltip'; import Tooltip from './Tooltip';
...@@ -12,6 +13,7 @@ const components = { ...@@ -12,6 +13,7 @@ const components = {
Table, Table,
Input, Input,
Form, Form,
Tabs,
Tag, Tag,
Tooltip, Tooltip,
} }
......
import React from 'react';
import {
Box,
Tab,
Tabs,
TabList,
TabPanel,
TabPanels,
} from '@chakra-ui/react';
import Page from '../Page/Page';
import PrivateAddressTags from '../privateTags/PrivateAddressTags';
import PrivateTransactionTags from '../privateTags/PrivateTransactionTags';
const PrivateTags: React.FC = () => {
return (
<Page>
<Box h="100%">
<Box as="h1" textStyle="h2" marginBottom={ 8 }>Private tags</Box>
<Tabs variant="soft-rounded" colorScheme="blue">
<TabList marginBottom={ 8 }>
<Tab>Address</Tab>
<Tab>Transaction</Tab>
</TabList>
<TabPanels>
<TabPanel padding={ 0 }>
<PrivateAddressTags/>
</TabPanel>
<TabPanel padding={ 0 }>
<PrivateTransactionTags/>
</TabPanel>
</TabPanels>
</Tabs>
</Box>
</Page>
);
};
export default PrivateTags
...@@ -4,12 +4,12 @@ import { Box, Button, Text, useDisclosure } from '@chakra-ui/react'; ...@@ -4,12 +4,12 @@ import { Box, Button, Text, useDisclosure } from '@chakra-ui/react';
import Page from '../Page/Page'; import Page from '../Page/Page';
import WatchlistTable from '../WatchlistTable/WatchlistTable'; import WatchlistTable from '../watchlist/WatchlistTable/WatchlistTable';
import AddressModal from '../AddressModal/AddressModal'; import AddressModal from '../watchlist/AddressModal/AddressModal';
import type { TWatchlistItem } from '../../data/watchlist'; import type { TWatchlistItem } from '../../data/watchlist';
import { watchlist } from '../../data/watchlist'; import { watchlist } from '../../data/watchlist';
import DeleteModal from '../DeleteModal/DeleteModal'; import DeleteAddressModal from '../watchlist/DeleteAddressModal';
const WatchList: React.FC = () => { const WatchList: React.FC = () => {
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
...@@ -61,7 +61,7 @@ const WatchList: React.FC = () => { ...@@ -61,7 +61,7 @@ const WatchList: React.FC = () => {
</Box> </Box>
</Box> </Box>
<AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData }/> <AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData }/>
<DeleteModal { ...deleteModalProps } onClose={ onDeleteModalClose } address={ deleteModalData }/> <DeleteAddressModal { ...deleteModalProps } onClose={ onDeleteModalClose } address={ deleteModalData }/>
</Page> </Page>
); );
}; };
......
import React, { useCallback, useEffect } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import {
Box,
Button,
} from '@chakra-ui/react';
import AddressInput from '../../shared/AddressInput';
import TagInput from '../../shared/TagInput';
import type { TPrivateTagsAddressItem } from '../../../data/privateTagsAddress';
const ADDRESS_LENGTH = 42;
const TAG_MAX_LENGTH = 35;
type Props = {
data?: TPrivateTagsAddressItem;
}
type Inputs = {
address: string;
tag: string;
}
const AddressModal: React.FC<Props> = ({ data }) => {
const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>();
useEffect(() => {
setValue('address', data?.address || '');
setValue('tag', data?.tag || '');
}, [ setValue, data ]);
// eslint-disable-next-line no-console
const onSubmit: SubmitHandler<Inputs> = data => console.log(data);
const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'address'>}) => {
return <AddressInput field={ field } isInvalid={ Boolean(errors.address) }/>
}, [ errors ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput field={ field } isInvalid={ Boolean(errors.tag) }/>
}, [ errors ]);
return (
<>
<Box marginBottom={ 5 }>
<Controller
name="address"
control={ control }
rules={{
maxLength: ADDRESS_LENGTH,
minLength: ADDRESS_LENGTH,
}}
render={ renderAddressInput }
/>
</Box>
<Box marginBottom={ 8 }>
<Controller
name="tag"
control={ control }
rules={{
maxLength: TAG_MAX_LENGTH,
}}
render={ renderTagInput }
/>
</Box>
<Box marginTop={ 8 }>
<Button
size="lg"
variant="primary"
onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 }
>
{ data ? 'Save changes' : 'Add tag' }
</Button>
</Box>
</>
)
}
export default AddressModal;
import React, { useCallback } from 'react';
import type { TPrivateTagsAddressItem } from '../../../data/privateTagsAddress';
import AddressForm from './AddressForm';
import FormModal from '../../shared/FormModal';
type Props = {
isOpen: boolean;
onClose: () => void;
data?: TPrivateTagsAddressItem;
}
const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const title = data ? 'Edit address tag' : 'New address tag';
const text = 'Label any address with a private address tag (up to 35 chars) to customize your explorer experience.'
const renderForm = useCallback(() => {
return <AddressForm data={ data }/>
}, [ data ]);
return (
<FormModal<TPrivateTagsAddressItem>
isOpen={ isOpen }
onClose={ onClose }
title={ title }
text={ text }
data={ data }
renderForm={ renderForm }
/>
)
}
export default AddressModal;
import React from 'react';
import {
Table,
Thead,
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react'
import type { TPrivateTagsAddress, TPrivateTagsAddressItem } from '../../../data/privateTagsAddress';
import AddressTagTableItem from './AddressTagTableItem';
interface Props {
data: TPrivateTagsAddress;
onEditClick: (data: TPrivateTagsAddressItem) => void;
onDeleteClick: (data: TPrivateTagsAddressItem) => void;
}
const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
return (
<TableContainer width="100%">
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th width="60%">Address</Th>
<Th width="40%">Private tag</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<AddressTagTableItem
item={ item }
key={ item.address }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
</TableContainer>
);
};
export default AddressTagTable;
import React, { useCallback } from 'react';
import {
Tag,
Tr,
Td,
Icon,
IconButton,
HStack,
Tooltip,
} from '@chakra-ui/react'
import EditIcon from '../../../icons/edit.svg';
import DeleteIcon from '../../../icons/delete.svg';
import AddressIcon from '../../shared/AddressIcon';
import AddressLinkWithTooltip from '../../shared/AddressLinkWithTooltip';
import type { TPrivateTagsAddressItem } from '../../../data/privateTagsAddress';
interface Props {
item: TPrivateTagsAddressItem;
onEditClick: (data: TPrivateTagsAddressItem) => void;
onDeleteClick: (data: TPrivateTagsAddressItem) => void;
}
const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => {
return onEditClick(item);
}, [ item, onEditClick ]);
const onItemDeleteClick = useCallback(() => {
return onDeleteClick(item);
}, [ item, onDeleteClick ]);
return (
<Tr alignItems="top" key={ item.address }>
<Td>
<HStack spacing={ 4 }>
<AddressIcon address={ item.address }/>
<AddressLinkWithTooltip address={ item.address }/>
</HStack>
</Td>
<Td>
<Tooltip label={ item.tag }>
<Tag variant="gray" lineHeight="24px">
{ item.tag }
</Tag>
</Tooltip>
</Td>
<Td>
<HStack spacing={ 6 }>
<Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="iconBlue"
w="30px"
h="30px"
onClick={ onItemEditClick }
icon={ <Icon as={ EditIcon } w="20px" h="20px"/> }
/>
</Tooltip>
<Tooltip label="Delete">
<IconButton
aria-label="delete"
variant="iconBlue"
w="30px"
h="30px"
onClick={ onItemDeleteClick }
icon={ <Icon as={ DeleteIcon } w="20px" h="20px"/> }
/>
</Tooltip>
</HStack>
</Td>
</Tr>
)
};
export default AddressTagTableItem;
import React, { useCallback } from 'react';
import DeleteModal from '../shared/DeleteModal'
type Props = {
isOpen: boolean;
onClose: () => void;
address?: string;
}
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, address }) => {
const onDelete = useCallback(() => {
// eslint-disable-next-line no-console
console.log('delete', address);
}, [ address ]);
return (
<DeleteModal
isOpen={ isOpen }
onClose={ onClose }
onDelete={ onDelete }
title="Remove address private tag"
text={ `Address ${ address || 'address' } will be deleted` }
/>
)
}
export default DeleteAddressModal;
import React, { useCallback } from 'react';
import DeleteModal from '../shared/DeleteModal'
type Props = {
isOpen: boolean;
onClose: () => void;
transaction?: string;
}
const DeleteTransactionModal: React.FC<Props> = ({ isOpen, onClose, transaction }) => {
const onDelete = useCallback(() => {
// eslint-disable-next-line no-console
console.log('delete', transaction);
}, [ transaction ]);
return (
<DeleteModal
isOpen={ isOpen }
onClose={ onClose }
onDelete={ onDelete }
title="Remove transaction private tag"
text={ `Transaction ${ transaction || 'transaction' } will be deleted` }
/>
)
}
export default DeleteTransactionModal;
import React, { useCallback, useState } from 'react';
import { Box, Button, Text, useDisclosure } from '@chakra-ui/react';
import AddressTagTable from './AddressTagTable/AddressTagTable';
import AddressModal from './AddressModal/AddressModal';
import type { TPrivateTagsAddressItem } from './../../data/privateTagsAddress';
import { privateTagsAddress } from './../../data/privateTagsAddress';
import DeleteAddressModal from './DeleteAddressModal';
const PrivateAddressTags: React.FC = () => {
const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const [ addressModalData, setAddressModalData ] = useState<TPrivateTagsAddressItem>();
const [ deleteModalData, setDeleteModalData ] = useState<string>();
const onEditClick = useCallback((data: TPrivateTagsAddressItem) => {
setAddressModalData(data);
addressModalProps.onOpen();
}, [ addressModalProps ])
const onAddressModalClose = useCallback(() => {
setAddressModalData(undefined);
addressModalProps.onClose();
}, [ addressModalProps ]);
const onDeleteClick = useCallback((data: TPrivateTagsAddressItem) => {
setDeleteModalData(data.address);
deleteModalProps.onOpen();
}, [ deleteModalProps ])
const onDeleteModalClose = useCallback(() => {
setDeleteModalData(undefined);
deleteModalProps.onClose();
}, [ deleteModalProps ]);
return (
<>
<Text marginBottom={ 12 }>
Use private transaction tags to label any transactions of interest.
Private tags are saved in your account and are only visible when you are logged in.
</Text>
{ Boolean(privateTagsAddress.length) && (
<AddressTagTable
data={ privateTagsAddress }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
) }
<Box marginTop={ 8 }>
<Button
variant="primary"
size="lg"
onClick={ addressModalProps.onOpen }
>
Add address tag
</Button>
</Box>
<AddressModal { ...addressModalProps } onClose={ onAddressModalClose } data={ addressModalData }/>
<DeleteAddressModal { ...deleteModalProps } onClose={ onDeleteModalClose } address={ deleteModalData }/>
</>
);
};
export default PrivateAddressTags
import React, { useCallback, useState } from 'react';
import { Box, Button, Text, useDisclosure } from '@chakra-ui/react';
import TransactionTagTable from './TransactionTagTable/TransactionTagTable';
import TransactionModal from './TransactionModal/TransactionModal';
import type { TPrivateTagsTransactionItem } from './../../data/privateTagsTransaction';
import { privateTagsTransaction } from './../../data/privateTagsTransaction';
import DeleteTransactionModal from './DeleteTransactionModal';
const PrivateTransactionTags: React.FC = () => {
const transactionModalProps = useDisclosure();
const deleteModalProps = useDisclosure();
const [ transactionModalData, setTransactionModalData ] = useState<TPrivateTagsTransactionItem>();
const [ deleteModalData, setDeleteModalData ] = useState<string>();
const onEditClick = useCallback((data: TPrivateTagsTransactionItem) => {
setTransactionModalData(data);
transactionModalProps.onOpen();
}, [ transactionModalProps ])
const onAddressModalClose = useCallback(() => {
setTransactionModalData(undefined);
transactionModalProps.onClose();
}, [ transactionModalProps ]);
const onDeleteClick = useCallback((data: TPrivateTagsTransactionItem) => {
setDeleteModalData(data.transaction);
deleteModalProps.onOpen();
}, [ deleteModalProps ])
const onDeleteModalClose = useCallback(() => {
setDeleteModalData(undefined);
deleteModalProps.onClose();
}, [ deleteModalProps ]);
return (
<>
<Text marginBottom={ 12 }>
Use private transaction tags to label any transactions of interest.
Private tags are saved in your account and are only visible when you are logged in.
</Text>
{ Boolean(privateTagsTransaction.length) && (
<TransactionTagTable
data={ privateTagsTransaction }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
) }
<Box marginTop={ 8 }>
<Button
variant="primary"
size="lg"
onClick={ transactionModalProps.onOpen }
>
Add transaction tag
</Button>
</Box>
<TransactionModal { ...transactionModalProps } onClose={ onAddressModalClose } data={ transactionModalData }/>
<DeleteTransactionModal { ...deleteModalProps } onClose={ onDeleteModalClose } transaction={ deleteModalData }/>
</>
);
};
export default PrivateTransactionTags
import React, { useCallback, useEffect } from 'react';
import type { SubmitHandler, ControllerRenderProps } from 'react-hook-form';
import { useForm, Controller } from 'react-hook-form';
import {
Box,
Button,
} from '@chakra-ui/react';
import TransactionInput from '../../shared/TransactionInput';
import TagInput from '../../shared/TagInput';
import type { TPrivateTagsTransactionItem } from '../../../data/privateTagsTransaction';
const HASH_LENGTH = 66;
const TAG_MAX_LENGTH = 35;
type Props = {
data?: TPrivateTagsTransactionItem;
}
type Inputs = {
transaction: string;
tag: string;
}
const TransactionForm: React.FC<Props> = ({ data }) => {
const { control, handleSubmit, formState: { errors }, setValue } = useForm<Inputs>();
useEffect(() => {
setValue('transaction', data?.transaction || '');
setValue('tag', data?.tag || '');
}, [ setValue, data ]);
// eslint-disable-next-line no-console
const onSubmit: SubmitHandler<Inputs> = data => console.log(data);
const renderTransactionInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'transaction'>}) => {
return <TransactionInput field={ field } isInvalid={ Boolean(errors.transaction) }/>
}, [ errors ]);
const renderTagInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'tag'>}) => {
return <TagInput field={ field } isInvalid={ Boolean(errors.tag) }/>
}, [ errors ]);
return (
<>
<Box marginBottom={ 5 }>
<Controller
name="transaction"
control={ control }
rules={{
maxLength: HASH_LENGTH,
minLength: HASH_LENGTH,
}}
render={ renderTransactionInput }
/>
</Box>
<Box marginBottom={ 8 }>
<Controller
name="tag"
control={ control }
rules={{
maxLength: TAG_MAX_LENGTH,
}}
render={ renderTagInput }
/>
</Box>
<Box marginTop={ 8 }>
<Button
size="lg"
variant="primary"
onClick={ handleSubmit(onSubmit) }
disabled={ Object.keys(errors).length > 0 }
>
{ data ? 'Save changes' : 'Add tag' }
</Button>
</Box>
</>
)
}
export default TransactionForm;
import React, { useCallback } from 'react';
import type { TPrivateTagsTransactionItem } from '../../../data/privateTagsTransaction';
import TransactionForm from './TransactionForm';
import FormModal from '../../shared/FormModal';
type Props = {
isOpen: boolean;
onClose: () => void;
data?: TPrivateTagsTransactionItem;
}
const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const title = data ? 'Edit transaction tag' : 'New transaction tag';
const text = 'Label any transaction with a private transaction tag (up to 35 chars) to customize your explorer experience.'
const renderForm = useCallback(() => {
return <TransactionForm data={ data }/>
}, [ data ]);
return (
<FormModal<TPrivateTagsTransactionItem>
isOpen={ isOpen }
onClose={ onClose }
title={ title }
text={ text }
data={ data }
renderForm={ renderForm }
/>
)
}
export default AddressModal;
import React from 'react';
import {
Table,
Thead,
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react'
import type { TPrivateTagsTransaction, TPrivateTagsTransactionItem } from '../../../data/privateTagsTransaction';
import TransactionTagTableItem from './TransactionTagTableItem';
interface Props {
data: TPrivateTagsTransaction;
onEditClick: (data: TPrivateTagsTransactionItem) => void;
onDeleteClick: (data: TPrivateTagsTransactionItem) => void;
}
const AddressTagTable = ({ data, onDeleteClick, onEditClick }: Props) => {
return (
<TableContainer width="100%">
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th width="75%">Address</Th>
<Th width="25%">Private tag</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<TransactionTagTableItem
item={ item }
key={ item.transaction }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
</TableContainer>
);
};
export default AddressTagTable;
import React, { useCallback } from 'react';
import {
Tag,
Tr,
Td,
Icon,
IconButton,
HStack,
Tooltip,
} from '@chakra-ui/react'
import EditIcon from '../../../icons/edit.svg';
import DeleteIcon from '../../../icons/delete.svg';
import AddressLinkWithTooltip from '../../shared/AddressLinkWithTooltip';
import type { TPrivateTagsTransactionItem } from '../../../data/privateTagsTransaction';
interface Props {
item: TPrivateTagsTransactionItem;
onEditClick: (data: TPrivateTagsTransactionItem) => void;
onDeleteClick: (data: TPrivateTagsTransactionItem) => void;
}
const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => {
return onEditClick(item);
}, [ item, onEditClick ]);
const onItemDeleteClick = useCallback(() => {
return onDeleteClick(item);
}, [ item, onDeleteClick ]);
return (
<Tr alignItems="top" key={ item.transaction }>
<Td>
<HStack spacing={ 4 }>
<AddressLinkWithTooltip address={ item.transaction }/>
</HStack>
</Td>
<Td>
<Tooltip label={ item.tag }>
<Tag variant="gray" lineHeight="24px">
{ item.tag }
</Tag>
</Tooltip>
</Td>
<Td>
<HStack spacing={ 6 }>
<Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="iconBlue"
w="30px"
h="30px"
onClick={ onItemEditClick }
icon={ <Icon as={ EditIcon } w="20px" h="20px"/> }
/>
</Tooltip>
<Tooltip label="Delete">
<IconButton
aria-label="delete"
variant="iconBlue"
w="30px"
h="30px"
onClick={ onItemDeleteClick }
icon={ <Icon as={ DeleteIcon } w="20px" h="20px"/> }
/>
</Tooltip>
</HStack>
</Td>
</Tr>
)
};
export default AddressTagTableItem;
import React from 'react';
import Jazzicon, { jsNumberForAddress } from 'react-jazzicon';
import { Box } from '@chakra-ui/react';
const AddressIcon = ({ address }: {address: string}) => {
return (
<Box width="24px">
<Jazzicon diameter={ 24 } seed={ jsNumberForAddress(address) }/>
</Box>
);
};
export default AddressIcon;
import React from 'react';
import { HStack, Link, Box, Tooltip } from '@chakra-ui/react';
import AddressWithDots from './AddressWithDots';
import CopyToClipboard from './CopyToClipboard';
const AddressLinkWithTooltip = ({ address }: {address: string}) => {
return (
<HStack spacing={ 2 } alignContent="center" overflow="hidden">
<Link
href="#"
color="blue.500"
overflow="hidden"
fontWeight={ 600 }
lineHeight="24px"
// need theme
_hover={{ color: 'blue.400' }}
>
<Tooltip label={ address }>
<Box overflow="hidden"><AddressWithDots address={ address }/></Box>
</Tooltip>
</Link>
<CopyToClipboard text={ address }/>
</HStack>
)
}
export default AddressLinkWithTooltip;
...@@ -14,25 +14,26 @@ import { ...@@ -14,25 +14,26 @@ import {
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
address?: string; onDelete: () => void;
title: string;
text: string;
} }
const DeleteModal: React.FC<Props> = ({ isOpen, onClose, address }) => { const DeleteModal: React.FC<Props> = ({ isOpen, onClose, onDelete, title, text }) => {
const onDeleteClick = useCallback(() => { const onDeleteClick = useCallback(() => {
// eslint-disable-next-line no-console onDelete();
console.log('delete ', address);
onClose() onClose()
}, [ address, onClose ]); }, [ onClose, onDelete ]);
return ( return (
<Modal isOpen={ isOpen } onClose={ onClose } size="md"> <Modal isOpen={ isOpen } onClose={ onClose } size="md">
<ModalOverlay/> <ModalOverlay/>
<ModalContent> <ModalContent>
<ModalHeader fontWeight="500" textStyle="h3">Remove address from watch list</ModalHeader> <ModalHeader fontWeight="500" textStyle="h3">{ title }</ModalHeader>
<ModalCloseButton/> <ModalCloseButton/>
<ModalBody> <ModalBody>
{ `Address ${ address || 'address' } will be deleted` } { text }
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button variant="primary" size="lg" onClick={ onDeleteClick }> <Button variant="primary" size="lg" onClick={ onDeleteClick }>
......
...@@ -10,19 +10,16 @@ import { ...@@ -10,19 +10,16 @@ import {
Text, Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import type { TWatchlistItem } from '../../data/watchlist'; interface Props<TData> {
import AddressForm from './AddressForm';
type Props = {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
data?: TWatchlistItem; data?: TData;
title: string;
text: string;
renderForm: (data?: TData) => JSX.Element;
} }
const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { export default function FormModal<TData>({ isOpen, onClose, data, title, text, renderForm }: Props<TData>) {
const title = data ? 'Edit watch list address' : 'New address to watch list';
return ( return (
<Modal isOpen={ isOpen } onClose={ onClose } size="md"> <Modal isOpen={ isOpen } onClose={ onClose } size="md">
<ModalOverlay/> <ModalOverlay/>
...@@ -32,14 +29,12 @@ const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { ...@@ -32,14 +29,12 @@ const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
<ModalBody mb={ 0 }> <ModalBody mb={ 0 }>
{ !data && ( { !data && (
<Text lineHeight="30px" marginBottom={ 12 }> <Text lineHeight="30px" marginBottom={ 12 }>
An email notification can be sent to you when an address on your watch list sends or receives any transactions. { text }
</Text> </Text>
) } ) }
<AddressForm data={ data }/> { renderForm(data) }
</ModalBody> </ModalBody>
</ModalContent> </ModalContent>
</Modal> </Modal>
) )
} }
export default AddressModal;
import React from 'react'
import type { ControllerRenderProps } from 'react-hook-form';
import {
Input,
FormControl,
FormLabel,
} from '@chakra-ui/react';
const HASH_LENGTH = 66;
type Props = {
field: ControllerRenderProps<any, 'transaction'>;
isInvalid: boolean;
}
const AddressInput: React.FC<Props> = ({ field, isInvalid }) => {
return (
<FormControl variant="floating" id="transaction" isRequired>
<Input
{ ...field }
placeholder=" "
isInvalid={ isInvalid }
maxLength={ HASH_LENGTH }
/>
<FormLabel>Transaction hash (0x...)</FormLabel>
</FormControl>
)
}
export default AddressInput
...@@ -11,10 +11,10 @@ import { ...@@ -11,10 +11,10 @@ import {
GridItem, GridItem,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import AddressInput from './AddressInput'; import AddressInput from '../../shared/AddressInput';
import TagInput from './TagInput'; import TagInput from '../../shared/TagInput';
import type { TWatchlistItem } from '../../data/watchlist'; import type { TWatchlistItem } from '../../../data/watchlist';
const NOTIFICATIONS = [ 'xDAI', 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ]; const NOTIFICATIONS = [ 'xDAI', 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ];
const ADDRESS_LENGTH = 42; const ADDRESS_LENGTH = 42;
......
import React, { useCallback } from 'react';
import type { TWatchlistItem } from '../../../data/watchlist';
import AddressForm from './AddressForm';
import FormModal from '../../shared/FormModal';
type Props = {
isOpen: boolean;
onClose: () => void;
data?: TWatchlistItem;
}
const AddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const title = data ? 'Edit watch list address' : 'New address to watch list';
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(() => {
return <AddressForm data={ data }/>
}, [ data ]);
return (
<FormModal<TWatchlistItem>
isOpen={ isOpen }
onClose={ onClose }
title={ title }
text={ text }
data={ data }
renderForm={ renderForm }
/>
)
}
export default AddressModal;
import React, { useCallback } from 'react';
import DeleteModal from '../shared/DeleteModal'
type Props = {
isOpen: boolean;
onClose: () => void;
address?: string;
}
const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, address }) => {
const onDelete = useCallback(() => {
// eslint-disable-next-line no-console
console.log('delete', address);
}, [ address ]);
return (
<DeleteModal
isOpen={ isOpen }
onClose={ onClose }
onDelete={ onDelete }
title="Remove address from watch list"
text={ `Address ${ address || 'address' } will be deleted` }
/>
)
}
export default DeleteAddressModal;
import React from 'react'; import React from 'react';
import Jazzicon, { jsNumberForAddress } from 'react-jazzicon';
import { Box, Link, HStack, VStack, Image, Text, Icon, Tooltip } from '@chakra-ui/react'; import { Link, HStack, VStack, Image, Text, Icon } from '@chakra-ui/react';
import AddressWithDots from '../shared/AddressWithDots'; import AddressIcon from '../../shared/AddressIcon';
import CopyToClipboard from '../shared/CopyToClipboard'; import AddressLinkWithTooltip from '../../shared/AddressLinkWithTooltip';
import type { TWatchlistItem } from '../../data/watchlist'; import type { TWatchlistItem } from '../../../data/watchlist';
import { nbsp } from '../../lib/html-entities'; import { nbsp } from '../../../lib/html-entities';
import TokensIcon from '../../icons/tokens.svg'; import TokensIcon from '../../../icons/tokens.svg';
import WalletIcon from '../../icons/wallet.svg'; import WalletIcon from '../../../icons/wallet.svg';
const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => { const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
const image = <Jazzicon diameter={ 24 } seed={ jsNumberForAddress(item.address) }/>
return ( return (
<HStack spacing={ 3 } align="top"> <HStack spacing={ 3 } align="top">
<Box width="24px">{ image }</Box> <AddressIcon address={ item.address }/>
<VStack spacing={ 2 } align="stretch" overflow="hidden" fontWeight={ 500 } color="gray.700"> <VStack spacing={ 2 } align="stretch" overflow="hidden" fontWeight={ 500 } color="gray.700">
<HStack spacing={ 2 } alignContent="center"> <AddressLinkWithTooltip address={ item.address }/>
<Link
href="#"
color="blue.500"
overflow="hidden"
fontWeight={ 600 }
lineHeight="24px"
// need theme
_hover={{ color: 'blue.400' }}
>
<Tooltip label={ item.address }>
<Box overflow="hidden"><AddressWithDots address={ item.address }/></Box>
</Tooltip>
</Link>
<CopyToClipboard text={ item.address }/>
</HStack>
{ item.tokenBalance && ( { 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"/>
......
...@@ -11,10 +11,10 @@ import { ...@@ -11,10 +11,10 @@ import {
Tooltip, Tooltip,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import EditIcon from '../../icons/edit.svg'; import EditIcon from '../../../icons/edit.svg';
import DeleteIcon from '../../icons/delete.svg'; import DeleteIcon from '../../../icons/delete.svg';
import type { TWatchlistItem } from '../../data/watchlist'; import type { TWatchlistItem } from '../../../data/watchlist';
import WatchListAddressItem from './WatchListAddressItem'; import WatchListAddressItem from './WatchListAddressItem';
......
...@@ -9,7 +9,7 @@ import { ...@@ -9,7 +9,7 @@ import {
TableContainer, TableContainer,
} from '@chakra-ui/react' } from '@chakra-ui/react'
import type { TWatchlist, TWatchlistItem } from '../../data/watchlist'; import type { TWatchlist, TWatchlistItem } from '../../../data/watchlist';
import WatchlistTableItem from './WatchListTableItem'; import WatchlistTableItem from './WatchListTableItem';
......
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