Commit 2fd25692 authored by tom's avatar tom

Merge branch 'main' of github.com:tom2drum/block-scout into csp

parents ecf46692 e564b95f
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 9a7 7 0 1 1 7 7h-2v2h-2v2H9v2H2v-5.414l6.148-6.148A7.025 7.025 0 0 1 8 9Zm3 5h4a5 5 0 1 0-4.786-3.547l.174.573L4 17.414V20h3v-2h2v-2h2v-2Zm4-3a2 2 0 1 1 0-4 2 2 0 0 1 0 4Z" fill="currentColor"/>
</svg>
\ No newline at end of file
<svg width="30" height="30" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M23.433 12.64a1.212 1.212 0 0 1-.857.354H6.212a1.212 1.212 0 0 1-1.12-.745 1.212 1.212 0 0 1 .266-1.321L8.994 7.29a1.212 1.212 0 0 1 1.71 1.71L9.14 10.57h13.436a1.212 1.212 0 0 1 .857 2.068ZM7.424 17.236h16.363a1.213 1.213 0 0 1 1.121.745 1.212 1.212 0 0 1-.266 1.321l-3.636 3.637a1.213 1.213 0 1 1-1.71-1.71l1.564-1.569H7.424a1.212 1.212 0 0 1 0-2.424Z"/></svg> <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
\ No newline at end of file <path fill-rule="evenodd" clip-rule="evenodd" d="M23.433 12.64a1.212 1.212 0 0 1-.857.354H6.212a1.212 1.212 0 0 1-1.12-.745 1.212 1.212 0 0 1 .266-1.321L8.994 7.29a1.212 1.212 0 0 1 1.71 1.71L9.14 10.57h13.436a1.212 1.212 0 0 1 .857 2.068ZM7.424 17.236h16.363a1.213 1.213 0 0 1 1.121.745 1.212 1.212 0 0 1-.266 1.321l-3.636 3.637a1.213 1.213 0 1 1-1.71-1.71l1.564-1.569H7.424a1.212 1.212 0 0 1 0-2.424Z" fill="currentColor"/>
</svg>
\ No newline at end of file
import * as Sentry from '@sentry/nextjs';
import type { NextApiRequest } from 'next'; import type { NextApiRequest } from 'next';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
...@@ -7,8 +8,7 @@ export default function getUrlWithNetwork(_req: NextApiRequest, path: string) { ...@@ -7,8 +8,7 @@ export default function getUrlWithNetwork(_req: NextApiRequest, path: string) {
const networkSubType = _req.cookies[cookies.NAMES.NETWORK_SUB_TYPE]; const networkSubType = _req.cookies[cookies.NAMES.NETWORK_SUB_TYPE];
if (!networkType || !networkSubType) { if (!networkType || !networkSubType) {
// eslint-disable-next-line no-console Sentry.captureException(new Error('Incorrect network'), { extra: { networkType, networkSubType } });
console.error(`Incorrect network: NETWORK_TYPE=${ networkType } NETWORK_SUB_TYPE=${ networkSubType }`);
} }
return `/${ networkType }/${ networkSubType }/${ path }`; return `/${ networkType }/${ networkSubType }/${ path }`;
......
import * as Sentry from '@sentry/nextjs';
export interface ErrorType<T> { export interface ErrorType<T> {
error?: T; error?: T;
status: Response['status']; status: Response['status'];
...@@ -13,10 +15,15 @@ export default function clientFetch<Success, Error>(path: string, init?: Request ...@@ -13,10 +15,15 @@ export default function clientFetch<Success, Error>(path: string, init?: Request
status: response.status, status: response.status,
statusText: response.statusText, statusText: response.statusText,
}), }),
() => Promise.reject({ () => {
const error = {
status: response.status, status: response.status,
statusText: response.statusText, statusText: response.statusText,
}), };
Sentry.captureException(new Error('Client fetch failed'), { extra: error, tags: { source: 'fetch' } });
return Promise.reject(error);
},
); );
} else { } else {
......
...@@ -33,6 +33,9 @@ const sentryWebpackPluginOptions = { ...@@ -33,6 +33,9 @@ const sentryWebpackPluginOptions = {
silent: true, // Suppresses all logs silent: true, // Suppresses all logs
// For all available options, see: // For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options. // https://github.com/getsentry/sentry-webpack-plugin#options.
deploy: {
env: process.env.VERCEL_ENV || process.env.NODE_ENV,
},
}; };
module.exports = withReactSvg(withSentryConfig(moduleExports, sentryWebpackPluginOptions)); module.exports = withReactSvg(withSentryConfig(moduleExports, sentryWebpackPluginOptions));
...@@ -5,8 +5,10 @@ ...@@ -5,8 +5,10 @@
import * as Sentry from '@sentry/nextjs'; import * as Sentry from '@sentry/nextjs';
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN; const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
const ENV = process.env.NEXT_PUBLIC_VERCEL_ENV || process.env.NODE_ENV;
Sentry.init({ Sentry.init({
environment: ENV,
dsn: SENTRY_DSN, dsn: SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control // Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0, tracesSampleRate: 1.0,
......
...@@ -5,8 +5,10 @@ ...@@ -5,8 +5,10 @@
import * as Sentry from '@sentry/nextjs'; import * as Sentry from '@sentry/nextjs';
const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN; const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN;
const ENV = process.env.VERCEL_ENV || process.env.NODE_ENV;
Sentry.init({ Sentry.init({
environment: ENV,
dsn: SENTRY_DSN, dsn: SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control // Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1.0, tracesSampleRate: 1.0,
......
import { alertAnatomy as parts } from '@chakra-ui/anatomy'; import { alertAnatomy as parts } from '@chakra-ui/anatomy';
import type { StyleFunctionProps } from '@chakra-ui/styled-system';
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system'; import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
import { getColor, mode, transparentize } from '@chakra-ui/theme-tools';
const { definePartsStyle, defineMultiStyleConfig } = const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(parts.keys); createMultiStyleConfigHelpers(parts.keys);
function getBg(props: StyleFunctionProps): string {
const { theme, colorScheme: c } = props;
const lightBg = getColor(theme, `${ c }.100`, c);
const darkBg = transparentize(`${ c }.200`, 0.16)(theme);
return mode(lightBg, darkBg)(props);
}
const baseStyle = definePartsStyle({ const baseStyle = definePartsStyle({
container: { container: {
borderRadius: 'md', borderRadius: 'md',
...@@ -12,8 +22,21 @@ const baseStyle = definePartsStyle({ ...@@ -12,8 +22,21 @@ const baseStyle = definePartsStyle({
}, },
}); });
const variantSubtle = definePartsStyle((props) => {
return {
container: {
bgColor: getBg(props),
},
};
});
const variants = {
subtle: variantSubtle,
};
const Alert = defineMultiStyleConfig({ const Alert = defineMultiStyleConfig({
baseStyle, baseStyle,
variants,
}); });
export default Alert; export default Alert;
...@@ -125,6 +125,9 @@ const variantFloating = definePartsStyle((props) => { ...@@ -125,6 +125,9 @@ const variantFloating = definePartsStyle((props) => {
margin: 0, margin: 0,
transformOrigin: 'top left', transformOrigin: 'top left',
transitionProperty: 'font-size, line-height, padding, top, background-color', transitionProperty: 'font-size, line-height, padding, top, background-color',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}, },
'input:not(:placeholder-shown) + label, textarea:not(:placeholder-shown) + label': { 'input:not(:placeholder-shown) + label, textarea:not(:placeholder-shown) + label': {
...activeLabelStyles, ...activeLabelStyles,
......
...@@ -35,6 +35,7 @@ const baseStyleHeader = defineStyle((props) => ({ ...@@ -35,6 +35,7 @@ const baseStyleHeader = defineStyle((props) => ({
const baseStyleBody = defineStyle({ const baseStyleBody = defineStyle({
padding: 0, padding: 0,
marginBottom: 8, marginBottom: 8,
flex: 'initial',
}); });
const baseStyleFooter = defineStyle({ const baseStyleFooter = defineStyle({
...@@ -70,16 +71,33 @@ const baseStyle = definePartsStyle((props) => ({ ...@@ -70,16 +71,33 @@ const baseStyle = definePartsStyle((props) => ({
const sizes = { const sizes = {
md: definePartsStyle({ md: definePartsStyle({
dialogContainer: {
height: '100%',
},
dialog: { dialog: {
maxW: '760px', maxW: '760px',
}, },
}), }),
full: definePartsStyle({ full: definePartsStyle({
dialogContainer: {
height: '100%',
},
dialog: { dialog: {
maxW: '100vw', maxW: '100vw',
minH: '100vh',
my: '0', my: '0',
borderRadius: '0', borderRadius: '0',
padding: '80px 16px 32px 16px',
height: '100%',
overflowY: 'scroll',
},
closeButton: {
top: 4,
right: 6,
width: 6,
height: 6,
},
header: {
mb: 6,
}, },
}), }),
}; };
......
...@@ -4,6 +4,12 @@ import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; ...@@ -4,6 +4,12 @@ import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system';
import getOutlinedFieldStyles from '../utils/getOutlinedFieldStyles'; import getOutlinedFieldStyles from '../utils/getOutlinedFieldStyles';
const sizes = { const sizes = {
md: defineStyle({
fontSize: 'md',
lineHeight: '20px',
h: '160px',
borderRadius: 'base',
}),
lg: defineStyle({ lg: defineStyle({
fontSize: 'md', fontSize: 'md',
lineHeight: '20px', lineHeight: '20px',
......
import React, { useCallback } from 'react';
import type { ApiKey } from 'types/api/account';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import ApiKeySnippet from 'ui/shared/ApiKeySnippet';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props {
item: ApiKey;
onEditClick: (item: ApiKey) => void;
onDeleteClick: (item: ApiKey) => void;
}
const ApiKeyListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => {
return onEditClick(item);
}, [ item, onEditClick ]);
const onItemDeleteClick = useCallback(() => {
return onDeleteClick(item);
}, [ item, onDeleteClick ]);
return (
<AccountListItemMobile>
<ApiKeySnippet apiKey={ item.api_key } name={ item.name }/>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
</AccountListItemMobile>
);
};
export default ApiKeyListItem;
import { import {
Tr, Tr,
Td, Td,
HStack,
Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { ApiKey } from 'types/api/account'; import type { ApiKey } from 'types/api/account';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import ApiKeySnippet from 'ui/shared/ApiKeySnippet';
import DeleteButton from 'ui/shared/DeleteButton'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import EditButton from 'ui/shared/EditButton';
interface Props { interface Props {
item: ApiKey; item: ApiKey;
...@@ -31,17 +28,10 @@ const ApiKeyTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -31,17 +28,10 @@ const ApiKeyTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<Tr alignItems="top" key={ item.api_key }> <Tr alignItems="top" key={ item.api_key }>
<Td> <Td>
<HStack> <ApiKeySnippet apiKey={ item.api_key } name={ item.name }/>
<Text fontSize="md" fontWeight={ 600 }>{ item.api_key }</Text>
<CopyToClipboard text={ item.api_key }/>
</HStack>
<Text fontSize="sm" marginTop={ 0.5 } variant="secondary">{ item.name }</Text>
</Td> </Td>
<Td> <Td>
<HStack spacing={ 6 }> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
<EditButton onClick={ onItemEditClick }/>
<DeleteButton onClick={ onItemDeleteClick }/>
</HStack>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -28,7 +28,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { ...@@ -28,7 +28,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const renderText = useCallback(() => { const renderText = useCallback(() => {
return ( return (
<Text display="flex">API key for<Text fontWeight="600" whiteSpace="pre">{ ` "${ data.name || 'name' }" ` }</Text>will be deleted</Text> <Text> API key for <Text fontWeight="600" as="span">{ ` "${ data.name || 'name' }" ` }</Text> will be deleted </Text>
); );
}, [ data.name ]); }, [ data.name ]);
......
...@@ -28,7 +28,7 @@ const Header = () => { ...@@ -28,7 +28,7 @@ const Header = () => {
width="100%" width="100%"
alignItems="center" alignItems="center"
justifyContent="space-between" justifyContent="space-between"
zIndex={ 10 } zIndex="sticky"
> >
<Burger/> <Burger/>
<NetworkLogo/> <NetworkLogo/>
......
...@@ -56,6 +56,7 @@ const SearchBarMobile = ({ onChange, onSubmit }: Props) => { ...@@ -56,6 +56,7 @@ const SearchBarMobile = ({ onChange, onSubmit }: Props) => {
position="fixed" position="fixed"
top="56px" top="56px"
left="0" left="0"
zIndex="docked"
bgColor={ bgColor } bgColor={ bgColor }
transform={ isVisible ? 'translateY(0)' : 'translateY(-100%)' } transform={ isVisible ? 'translateY(0)' : 'translateY(-100%)' }
transitionProperty="transform" transitionProperty="transform"
......
...@@ -128,6 +128,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -128,6 +128,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Textarea <Textarea
{ ...field } { ...field }
size="lg" size="lg"
minH="300px"
isInvalid={ Boolean(errors.abi) } isInvalid={ Boolean(errors.abi) }
/> />
<FormLabel>{ getPlaceholderWithError(`Custom ABI [{...}] (JSON format)`, errors.abi?.message) }</FormLabel> <FormLabel>{ getPlaceholderWithError(`Custom ABI [{...}] (JSON format)`, errors.abi?.message) }</FormLabel>
......
import React, { useCallback } from 'react';
import type { CustomAbi } from 'types/api/account';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import AddressSnippet from 'ui/shared/AddressSnippet';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props {
item: CustomAbi;
onEditClick: (item: CustomAbi) => void;
onDeleteClick: (item: CustomAbi) => void;
}
const CustomAbiListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => {
return onEditClick(item);
}, [ item, onEditClick ]);
const onItemDeleteClick = useCallback(() => {
return onDeleteClick(item);
}, [ item, onDeleteClick ]);
return (
<AccountListItemMobile>
<AddressSnippet address={ item.contract_address_hash } subtitle={ item.name }/>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
</AccountListItemMobile>
);
};
export default React.memo(CustomAbiListItem);
import { import {
Tr, Tr,
Td, Td,
HStack,
Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { CustomAbi } from 'types/api/account'; import type { CustomAbi } from 'types/api/account';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import AddressSnippet from 'ui/shared/AddressSnippet';
import DeleteButton from 'ui/shared/DeleteButton'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import EditButton from 'ui/shared/EditButton';
interface Props { interface Props {
item: CustomAbi; item: CustomAbi;
...@@ -31,17 +28,10 @@ const CustomAbiTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -31,17 +28,10 @@ const CustomAbiTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<Tr alignItems="top" key={ item.id }> <Tr alignItems="top" key={ item.id }>
<Td> <Td>
<HStack> <AddressSnippet address={ item.contract_address_hash } subtitle={ item.name }/>
<Text fontSize="md" fontWeight={ 600 }>{ item.contract_address_hash }</Text>
<CopyToClipboard text={ item.contract_address_hash }/>
</HStack>
<Text fontSize="sm" marginTop={ 0.5 } variant="secondary">{ item.name }</Text>
</Td> </Td>
<Td> <Td>
<HStack spacing={ 6 }> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
<EditButton onClick={ onItemEditClick }/>
<DeleteButton onClick={ onItemDeleteClick }/>
</HStack>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -28,7 +28,7 @@ const DeleteCustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data }) => { ...@@ -28,7 +28,7 @@ const DeleteCustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const renderText = useCallback(() => { const renderText = useCallback(() => {
return ( return (
<Text display="flex">Custom ABI for<Text fontWeight="600" whiteSpace="pre">{ ` "${ data.name || 'name' }" ` }</Text>will be deleted</Text> <Text>Custom ABI for<Text fontWeight="600" as="span">{ ` "${ data.name || 'name' }" ` }</Text>will be deleted</Text>
); );
}, [ data.name ]); }, [ data.name ]);
......
import { Box, Button, HStack, Link, Text, Skeleton, useDisclosure } from '@chakra-ui/react'; import { Box, Button, Stack, Link, Text, 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 { ApiKey, ApiKeys } from 'types/api/account'; import type { ApiKey, ApiKeys } from 'types/api/account';
import fetch from 'lib/client/fetch'; import fetch from 'lib/client/fetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import ApiKeyModal from 'ui/apiKey/ApiKeyModal/ApiKeyModal'; import ApiKeyModal from 'ui/apiKey/ApiKeyModal/ApiKeyModal';
import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem';
import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable'; import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable';
import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal'; import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
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 SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
import DataFetchAlert from '../shared/DataFetchAlert'; import DataFetchAlert from '../shared/DataFetchAlert';
...@@ -20,6 +24,7 @@ const DATA_LIMIT = 3; ...@@ -20,6 +24,7 @@ const DATA_LIMIT = 3;
const ApiKeysPage: React.FC = () => { const ApiKeysPage: React.FC = () => {
const apiKeyModalProps = useDisclosure(); const apiKeyModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>(); const [ apiKeyModalData, setApiKeyModalData ] = useState<ApiKey>();
const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>(); const [ deleteModalData, setDeleteModalData ] = useState<ApiKey>();
...@@ -47,40 +52,59 @@ const ApiKeysPage: React.FC = () => { ...@@ -47,40 +52,59 @@ const ApiKeysPage: React.FC = () => {
}, [ deleteModalProps ]); }, [ deleteModalProps ]);
const description = ( const description = (
<Text marginBottom={ 12 }> <AccountPageDescription>
Create API keys to use for your RPC and EthRPC API requests. For more information, see { space } Create API keys to use for your RPC and EthRPC API requests. For more information, see { space }
<Link href="#">“How to use a Blockscout API key”</Link>. <Link href="#">“How to use a Blockscout API key”</Link>.
</Text> </AccountPageDescription>
); );
const content = (() => { const content = (() => {
if (isLoading && !data) { if (isLoading && !data) {
return ( const loader = isMobile ? <SkeletonAccountMobile/> : (
<> <>
{ description }
<SkeletonTable columns={ [ '100%', '108px' ] }/> <SkeletonTable columns={ [ '100%', '108px' ] }/>
<Skeleton height="48px" width="156px" marginTop={ 8 }/> <Skeleton height="48px" width="156px" marginTop={ 8 }/>
</> </>
); );
return (
<>
{ description }
{ loader }
</>
);
} }
if (isError) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
const canAdd = data.length < DATA_LIMIT; const list = isMobile ? (
return ( <Box>
<> { data.map((item) => (
{ description } <ApiKeyListItem
{ Boolean(data.length) && ( item={ item }
key={ item.api_key }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Box>
) : (
<ApiKeyTable <ApiKeyTable
data={ data } data={ data }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
limit={ DATA_LIMIT } limit={ DATA_LIMIT }
/> />
) } );
<HStack marginTop={ 8 } spacing={ 5 }>
const canAdd = data.length < DATA_LIMIT;
return (
<>
{ description }
{ Boolean(data.length) && list }
<Stack marginTop={ 8 } spacing={ 5 } direction={{ base: 'column', lg: 'row' }}>
<Button <Button
variant="primary" variant="primary"
size="lg" size="lg"
...@@ -94,7 +118,7 @@ const ApiKeysPage: React.FC = () => { ...@@ -94,7 +118,7 @@ const ApiKeysPage: React.FC = () => {
{ `You have added the maximum number of API keys (${ DATA_LIMIT }). Contact us to request additional keys.` } { `You have added the maximum number of API keys (${ DATA_LIMIT }). Contact us to request additional keys.` }
</Text> </Text>
) } ) }
</HStack> </Stack>
<ApiKeyModal { ...apiKeyModalProps } onClose={ onApiKeyModalClose } data={ apiKeyModalData }/> <ApiKeyModal { ...apiKeyModalProps } onClose={ onApiKeyModalClose } data={ apiKeyModalData }/>
{ deleteModalData && <DeleteApiKeyModal { ...deleteModalProps } onClose={ onDeleteModalClose } data={ deleteModalData }/> } { deleteModalData && <DeleteApiKeyModal { ...deleteModalProps } onClose={ onDeleteModalClose } data={ deleteModalData }/> }
</> </>
......
import { Box, Button, HStack, Text, Skeleton, useDisclosure } from '@chakra-ui/react'; import { Box, Button, HStack, 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 { CustomAbi, CustomAbis } from 'types/api/account'; import type { CustomAbi, CustomAbis } from 'types/api/account';
import fetch from 'lib/client/fetch'; import fetch from 'lib/client/fetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal'; import CustomAbiModal from 'ui/customAbi/CustomAbiModal/CustomAbiModal';
import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem';
import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable'; import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable';
import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal'; import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
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 SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
import DataFetchAlert from '../shared/DataFetchAlert'; import DataFetchAlert from '../shared/DataFetchAlert';
...@@ -17,6 +21,7 @@ import DataFetchAlert from '../shared/DataFetchAlert'; ...@@ -17,6 +21,7 @@ import DataFetchAlert from '../shared/DataFetchAlert';
const CustomAbiPage: React.FC = () => { const CustomAbiPage: React.FC = () => {
const customAbiModalProps = useDisclosure(); const customAbiModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>(); const [ customAbiModalData, setCustomAbiModalData ] = useState<CustomAbi>();
const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>(); const [ deleteModalData, setDeleteModalData ] = useState<CustomAbi>();
...@@ -44,36 +49,55 @@ const CustomAbiPage: React.FC = () => { ...@@ -44,36 +49,55 @@ const CustomAbiPage: React.FC = () => {
}, [ deleteModalProps ]); }, [ deleteModalProps ]);
const description = ( const description = (
<Text marginBottom={ 12 }> <AccountPageDescription>
Add custom ABIs for any contract and access when logged into your account. Helpful for debugging, functional testing and contract interaction. Add custom ABIs for any contract and access when logged into your account. Helpful for debugging, functional testing and contract interaction.
</Text> </AccountPageDescription>
); );
const content = (() => { const content = (() => {
if (isLoading && !data) { if (isLoading && !data) {
return ( const loader = isMobile ? <SkeletonAccountMobile/> : (
<> <>
{ description }
<SkeletonTable columns={ [ '100%', '108px' ] }/> <SkeletonTable columns={ [ '100%', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/> <Skeleton height="44px" width="156px" marginTop={ 8 }/>
</> </>
); );
return (
<>
{ description }
{ loader }
</>
);
} }
if (isError) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
return ( const list = isMobile ? (
<> <Box>
{ description } { data.map((item) => (
{ data.length > 0 && ( <CustomAbiListItem
item={ item }
key={ item.id }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Box>
) : (
<CustomAbiTable <CustomAbiTable
data={ data } data={ data }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
) } );
return (
<>
{ description }
{ data.length > 0 && list }
<HStack marginTop={ 8 } spacing={ 5 }> <HStack marginTop={ 8 } spacing={ 5 }>
<Button <Button
variant="primary" variant="primary"
......
...@@ -38,7 +38,7 @@ const PrivateTags = ({ tab }: Props) => { ...@@ -38,7 +38,7 @@ const PrivateTags = ({ tab }: Props) => {
<Box h="100%"> <Box h="100%">
<AccountPageHeader text="Private tags"/> <AccountPageHeader text="Private tags"/>
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ onChangeTab } defaultIndex={ TABS.indexOf(tab) }> <Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ onChangeTab } defaultIndex={ TABS.indexOf(tab) }>
<TabList marginBottom={ 8 }> <TabList marginBottom={{ base: 6, lg: 8 }}>
<Tab>Address</Tab> <Tab>Address</Tab>
<Tab>Transaction</Tab> <Tab>Transaction</Tab>
</TabList> </TabList>
......
import { Box } from '@chakra-ui/react'; import { ArrowBackIcon } from '@chakra-ui/icons';
import { Box, Link, Text } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { animateScroll } from 'react-scroll'; import { animateScroll } from 'react-scroll';
import type { PublicTag } from 'types/api/account'; import type { PublicTag } from 'types/api/account';
import useIsMobile from 'lib/hooks/useIsMobile';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import PublicTagsData from 'ui/publicTags/PublicTagsData'; import PublicTagsData from 'ui/publicTags/PublicTagsData';
import PublicTagsForm from 'ui/publicTags/PublicTagsForm/PublicTagsForm'; import PublicTagsForm from 'ui/publicTags/PublicTagsForm/PublicTagsForm';
...@@ -24,6 +26,7 @@ const PublicTagsComponent: React.FC = () => { ...@@ -24,6 +26,7 @@ const PublicTagsComponent: React.FC = () => {
const [ formData, setFormData ] = useState<PublicTag>(); const [ formData, setFormData ] = useState<PublicTag>();
const toast = useToast(); const toast = useToast();
const isMobile = useIsMobile();
const showToast = useCallback((action: TToastAction) => { const showToast = useCallback((action: TToastAction) => {
toast({ toast({
...@@ -59,6 +62,7 @@ const PublicTagsComponent: React.FC = () => { ...@@ -59,6 +62,7 @@ const PublicTagsComponent: React.FC = () => {
}, [ showToast ]); }, [ showToast ]);
const onTagDelete = useCallback(() => showToast('removed'), [ showToast ]); const onTagDelete = useCallback(() => showToast('removed'), [ showToast ]);
const onGoBack = useCallback(() => setScreen('data'), [ ]);
let content; let content;
let header; let header;
...@@ -74,6 +78,12 @@ const PublicTagsComponent: React.FC = () => { ...@@ -74,6 +78,12 @@ const PublicTagsComponent: React.FC = () => {
return ( return (
<Page> <Page>
<Box h="100%"> <Box h="100%">
{ isMobile && screen === 'form' && (
<Link display="inline-flex" alignItems="center" mb={ 6 } onClick={ onGoBack }>
<ArrowBackIcon w={ 6 } h={ 6 }/>
<Text variant="inherit" fontSize="sm" ml={ 2 }>Public tags</Text>
</Link>
) }
<AccountPageHeader text={ header }/> <AccountPageHeader text={ header }/>
{ content } { content }
</Box> </Box>
......
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 SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
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 +23,7 @@ const WatchList: React.FC = () => { ...@@ -19,6 +23,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,32 +49,52 @@ const WatchList: React.FC = () => { ...@@ -44,32 +49,52 @@ 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;
if (isLoading && !data) { if (isLoading && !data) {
content = ( const loader = isMobile ? <SkeletonAccountMobile showFooterSlot/> : (
<> <>
{ description }
<SkeletonTable columns={ [ '70%', '30%', '160px', '108px' ] }/> <SkeletonTable columns={ [ '70%', '30%', '160px', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/> <Skeleton height="44px" width="156px" marginTop={ 8 }/>
</> </>
); );
content = (
<>
{ description }
{ loader }
</>
);
} else if (isError) { } else if (isError) {
content = <DataFetchAlert/>; content = <DataFetchAlert/>;
} else { } else {
content = ( const list = isMobile ? (
<> <Box>
{ Boolean(data?.length) && ( { data.map((item) => (
<WatchListItem
item={ item }
key={ item.address_hash }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Box>
) : (
<WatchlistTable <WatchlistTable
data={ data } data={ data }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
) } );
content = (
<>
{ description }
{ Boolean(data?.length) && list }
<Box marginTop={ 8 }> <Box marginTop={ 8 }>
<Button <Button
variant="primary" variant="primary"
......
import { Tag, Flex, HStack, Text } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { AddressTag } from 'types/api/account';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import AddressSnippet from 'ui/shared/AddressSnippet';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props {
item: AddressTag;
onEditClick: (data: AddressTag) => void;
onDeleteClick: (data: AddressTag) => void;
}
const AddressTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => {
return onEditClick(item);
}, [ item, onEditClick ]);
const onItemDeleteClick = useCallback(() => {
return onDeleteClick(item);
}, [ item, onDeleteClick ]);
return (
<AccountListItemMobile>
<Flex alignItems="flex-start" flexDirection="column" maxW="100%">
<AddressSnippet address={ item.address_hash }/>
<HStack spacing={ 3 } mt={ 4 }>
<Text fontSize="sm" fontWeight={ 500 }>Private tag</Text>
<Tag variant="gray" lineHeight="24px">
{ item.name }
</Tag>
</HStack>
</Flex>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
</AccountListItemMobile>
);
};
export default React.memo(AddressTagListItem);
...@@ -2,16 +2,13 @@ import { ...@@ -2,16 +2,13 @@ import {
Tag, Tag,
Tr, Tr,
Td, Td,
HStack,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { AddressTag } from 'types/api/account'; import type { AddressTag } from 'types/api/account';
import AddressIcon from 'ui/shared/AddressIcon'; import AddressSnippet from 'ui/shared/AddressSnippet';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import DeleteButton from 'ui/shared/DeleteButton';
import EditButton from 'ui/shared/EditButton';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props { interface Props {
...@@ -32,10 +29,7 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -32,10 +29,7 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<Tr alignItems="top" key={ item.id }> <Tr alignItems="top" key={ item.id }>
<Td> <Td>
<HStack spacing={ 4 }> <AddressSnippet address={ item.address_hash }/>
<AddressIcon address={ item.address_hash }/>
<AddressLinkWithTooltip address={ item.address_hash }/>
</HStack>
</Td> </Td>
<Td> <Td>
<TruncatedTextTooltip label={ item.name }> <TruncatedTextTooltip label={ item.name }>
...@@ -45,10 +39,7 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -45,10 +39,7 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
</TruncatedTextTooltip> </TruncatedTextTooltip>
</Td> </Td>
<Td> <Td>
<HStack spacing={ 6 }> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
<EditButton onClick={ onItemEditClick }/>
<DeleteButton onClick={ onItemDeleteClick }/>
</HStack>
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -37,7 +37,7 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type }) ...@@ -37,7 +37,7 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type })
const renderText = useCallback(() => { const renderText = useCallback(() => {
return ( return (
<Text display="flex">Tag<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag || 'tag' }" ` }</Text>will be deleted</Text> <Text>Tag<Text fontWeight="600" as="span">{ ` "${ tag || 'tag' }" ` }</Text>will be deleted</Text>
); );
}, [ tag ]); }, [ tag ]);
......
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 { AddressTags, AddressTag } from 'types/api/account'; import type { AddressTags, AddressTag } from 'types/api/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 DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
import AddressModal from './AddressModal/AddressModal'; import AddressModal from './AddressModal/AddressModal';
import AddressTagListItem from './AddressTagTable/AddressTagListItem';
import AddressTagTable from './AddressTagTable/AddressTagTable'; import AddressTagTable from './AddressTagTable/AddressTagTable';
import DeletePrivateTagModal from './DeletePrivateTagModal'; import DeletePrivateTagModal from './DeletePrivateTagModal';
...@@ -18,6 +22,7 @@ const PrivateAddressTags = () => { ...@@ -18,6 +22,7 @@ const PrivateAddressTags = () => {
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const [ addressModalData, setAddressModalData ] = useState<AddressTag>(); const [ addressModalData, setAddressModalData ] = useState<AddressTag>();
const [ deleteModalData, setDeleteModalData ] = useState<AddressTag>(); const [ deleteModalData, setDeleteModalData ] = useState<AddressTag>();
...@@ -43,36 +48,55 @@ const PrivateAddressTags = () => { ...@@ -43,36 +48,55 @@ const PrivateAddressTags = () => {
}, [ deleteModalProps ]); }, [ deleteModalProps ]);
const description = ( const description = (
<Text marginBottom={ 12 }> <AccountPageDescription>
Use private transaction tags to label any transactions of interest. Use private address tags to track any addresses of interest.
Private tags are saved in your account and are only visible when you are logged in. Private tags are saved in your account and are only visible when you are logged in.
</Text> </AccountPageDescription>
); );
if (isLoading && !addressTagsData) { if (isLoading && !addressTagsData) {
return ( const loader = isMobile ? <SkeletonAccountMobile/> : (
<> <>
{ description }
<SkeletonTable columns={ [ '60%', '40%', '108px' ] }/> <SkeletonTable columns={ [ '60%', '40%', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/> <Skeleton height="44px" width="156px" marginTop={ 8 }/>
</> </>
); );
return (
<>
{ description }
{ loader }
</>
);
} }
if (isError) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
return ( const list = isMobile ? (
<> <Box>
{ description } { addressTagsData.map((item: AddressTag) => (
{ Boolean(addressTagsData?.length) && ( <AddressTagListItem
item={ item }
key={ item.id }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Box>
) : (
<AddressTagTable <AddressTagTable
data={ addressTagsData } data={ addressTagsData }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
) } );
return (
<>
{ description }
{ Boolean(addressTagsData?.length) && list }
<Box marginTop={ 8 }> <Box marginTop={ 8 }>
<Button <Button
variant="primary" variant="primary"
......
import { Box, Button, Skeleton, Text, 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 { TransactionTags, TransactionTag } from 'types/api/account'; import type { TransactionTags, TransactionTag } from 'types/api/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 DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
import DeletePrivateTagModal from './DeletePrivateTagModal'; import DeletePrivateTagModal from './DeletePrivateTagModal';
import TransactionModal from './TransactionModal/TransactionModal'; import TransactionModal from './TransactionModal/TransactionModal';
import TransactionTagListItem from './TransactionTagTable/TransactionTagListItem';
import TransactionTagTable from './TransactionTagTable/TransactionTagTable'; import TransactionTagTable from './TransactionTagTable/TransactionTagTable';
const PrivateTransactionTags = () => { const PrivateTransactionTags = () => {
...@@ -18,6 +22,7 @@ const PrivateTransactionTags = () => { ...@@ -18,6 +22,7 @@ const PrivateTransactionTags = () => {
const transactionModalProps = useDisclosure(); const transactionModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const isMobile = useIsMobile();
const [ transactionModalData, setTransactionModalData ] = useState<TransactionTag>(); const [ transactionModalData, setTransactionModalData ] = useState<TransactionTag>();
const [ deleteModalData, setDeleteModalData ] = useState<TransactionTag>(); const [ deleteModalData, setDeleteModalData ] = useState<TransactionTag>();
...@@ -43,36 +48,55 @@ const PrivateTransactionTags = () => { ...@@ -43,36 +48,55 @@ const PrivateTransactionTags = () => {
}, [ deleteModalProps ]); }, [ deleteModalProps ]);
const description = ( const description = (
<Text marginBottom={ 12 }> <AccountPageDescription>
Use private transaction tags to label any transactions of interest. 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. Private tags are saved in your account and are only visible when you are logged in.
</Text> </AccountPageDescription>
); );
if (isLoading && !transactionTagsData) { if (isLoading && !transactionTagsData) {
return ( const loader = isMobile ? <SkeletonAccountMobile/> : (
<> <>
{ description }
<SkeletonTable columns={ [ '75%', '25%', '108px' ] }/> <SkeletonTable columns={ [ '75%', '25%', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/> <Skeleton height="44px" width="156px" marginTop={ 8 }/>
</> </>
); );
return (
<>
{ description }
{ loader }
</>
);
} }
if (isError) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
return ( const list = isMobile ? (
<> <Box>
{ description } { transactionTagsData.map((item) => (
{ Boolean(transactionTagsData.length) && ( <TransactionTagListItem
item={ item }
key={ item.id }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Box>
) : (
<TransactionTagTable <TransactionTagTable
data={ transactionTagsData } data={ transactionTagsData }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
/> />
) } );
return (
<>
{ description }
{ Boolean(transactionTagsData.length) && list }
<Box marginTop={ 8 }> <Box marginTop={ 8 }>
<Button <Button
variant="primary" variant="primary"
......
import { Tag, HStack, Text, Flex } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { TransactionTag } from 'types/api/account';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TransactionSnippet from 'ui/shared/TransactionSnippet';
interface Props {
item: TransactionTag;
onEditClick: (data: TransactionTag) => void;
onDeleteClick: (data: TransactionTag) => void;
}
const TransactionTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => {
return onEditClick(item);
}, [ item, onEditClick ]);
const onItemDeleteClick = useCallback(() => {
return onDeleteClick(item);
}, [ item, onDeleteClick ]);
return (
<AccountListItemMobile>
<Flex alignItems="flex-start" flexDirection="column" maxW="100%">
<TransactionSnippet hash={ item.transaction_hash }/>
<HStack spacing={ 3 } mt={ 4 }>
<Text fontSize="sm" fontWeight={ 500 }>Private tag</Text>
<Tag variant="gray" lineHeight="24px">
{ item.name }
</Tag>
</HStack>
</Flex>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
</AccountListItemMobile>
);
};
export default React.memo(TransactionTagListItem);
...@@ -2,16 +2,14 @@ import { ...@@ -2,16 +2,14 @@ import {
Tag, Tag,
Tr, Tr,
Td, Td,
HStack,
Tooltip, Tooltip,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { TransactionTag } from 'types/api/account'; import type { TransactionTag } from 'types/api/account';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import DeleteButton from 'ui/shared/DeleteButton'; import TransactionSnippet from 'ui/shared/TransactionSnippet';
import EditButton from 'ui/shared/EditButton';
interface Props { interface Props {
item: TransactionTag; item: TransactionTag;
...@@ -19,7 +17,7 @@ interface Props { ...@@ -19,7 +17,7 @@ interface Props {
onDeleteClick: (data: TransactionTag) => void; onDeleteClick: (data: TransactionTag) => void;
} }
const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { const TransactionTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
}, [ item, onEditClick ]); }, [ item, onEditClick ]);
...@@ -31,7 +29,7 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -31,7 +29,7 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return ( return (
<Tr alignItems="top" key={ item.id }> <Tr alignItems="top" key={ item.id }>
<Td> <Td>
<AddressLinkWithTooltip address={ item.transaction_hash } type="transaction"/> <TransactionSnippet hash={ item.transaction_hash }/>
</Td> </Td>
<Td> <Td>
<Tooltip label={ item.name }> <Tooltip label={ item.name }>
...@@ -41,13 +39,10 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -41,13 +39,10 @@ const AddressTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
</Tooltip> </Tooltip>
</Td> </Td>
<Td> <Td>
<HStack spacing={ 6 }> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
<EditButton onClick={ onItemEditClick }/>
<DeleteButton onClick={ onItemDeleteClick }/>
</HStack>
</Td> </Td>
</Tr> </Tr>
); );
}; };
export default AddressTagTableItem; export default TransactionTagTableItem;
import { Flex, Text, FormControl, FormLabel, Textarea } from '@chakra-ui/react'; import { Box, Text, FormControl, FormLabel, Textarea, useColorModeValue } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
...@@ -22,6 +22,7 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete ...@@ -22,6 +22,7 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
const tags = data.tags.split(';'); const tags = data.tags.split(';');
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const deleteApiKey = useCallback(() => { const deleteApiKey = useCallback(() => {
const body = JSON.stringify({ remove_reason: reason }); const body = JSON.stringify({ remove_reason: reason });
...@@ -44,9 +45,9 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete ...@@ -44,9 +45,9 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
if (tags.length === 1) { if (tags.length === 1) {
text = ( text = (
<> <>
<Text display="flex">Public tag</Text> <Text display="inline" as="span">Public tag</Text>
<Text fontWeight="600" whiteSpace="pre">{ ` "${ tags[0] }" ` }</Text> <Text fontWeight="600" whiteSpace="pre" as="span">{ ` "${ tags[0] }" ` }</Text>
<Text>will be removed.</Text> <Text as="span">will be removed.</Text>
</> </>
); );
} }
...@@ -54,29 +55,29 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete ...@@ -54,29 +55,29 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
const tagsText: Array<JSX.Element | string> = []; const tagsText: Array<JSX.Element | string> = [];
tags.forEach((tag, index) => { tags.forEach((tag, index) => {
if (index < tags.length - 2) { if (index < tags.length - 2) {
tagsText.push(<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag }"` }</Text>); tagsText.push(<Text fontWeight="600" whiteSpace="pre" as="span">{ ` "${ tag }"` }</Text>);
tagsText.push(','); tagsText.push(',');
} }
if (index === tags.length - 2) { if (index === tags.length - 2) {
tagsText.push(<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag }" ` }</Text>); tagsText.push(<Text fontWeight="600" whiteSpace="pre" as="span">{ ` "${ tag }" ` }</Text>);
tagsText.push('and'); tagsText.push('and');
} }
if (index === tags.length - 1) { if (index === tags.length - 1) {
tagsText.push(<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag }" ` }</Text>); tagsText.push(<Text fontWeight="600" whiteSpace="pre" as="span">{ ` "${ tag }" ` }</Text>);
} }
}); });
text = ( text = (
<> <>
<Text>Public tags</Text>{ tagsText }<Text>will be removed.</Text> <Text as="span">Public tags</Text>{ tagsText }<Text as="span">will be removed.</Text>
</> </>
); );
} }
return ( return (
<> <>
<Flex marginBottom={ 12 }> <Box marginBottom={ 12 }>
{ text } { text }
</Flex> </Box>
<FormControl variant="floating" id="tag-delete"> <FormControl variant="floating" id="tag-delete" backgroundColor={ formBackgroundColor }>
<Textarea <Textarea
size="lg" size="lg"
value={ reason } value={ reason }
...@@ -86,7 +87,7 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete ...@@ -86,7 +87,7 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
</FormControl> </FormControl>
</> </>
); );
}, [ tags, reason, onFieldChange ]); }, [ tags, reason, onFieldChange, formBackgroundColor ]);
return ( return (
<DeleteModal <DeleteModal
......
import { Tag, VStack, Text, HStack } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { PublicTag } from 'types/api/account';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import AddressSnippet from 'ui/shared/AddressSnippet';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props {
item: PublicTag;
onEditClick: (data: PublicTag) => void;
onDeleteClick: (data: PublicTag) => void;
}
const PublicTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => {
return onEditClick(item);
}, [ item, onEditClick ]);
const onItemDeleteClick = useCallback(() => {
return onDeleteClick(item);
}, [ item, onDeleteClick ]);
return (
<AccountListItemMobile>
<VStack spacing={ 3 } alignItems="flex-start" maxW="100%">
<VStack spacing={ 4 } alignItems="unset" maxW="100%">
{ item.addresses.map((address) => <AddressSnippet key={ address } address={ address }/>) }
</VStack>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Public tags</Text>
<HStack spacing={ 2 } alignItems="baseline">
{ item.tags.split(';').map((tag) => {
return (
<TruncatedTextTooltip label={ tag } key={ tag }>
<Tag variant="gray" lineHeight="24px">
{ tag }
</Tag>
</TruncatedTextTooltip>
);
}) }
</HStack>
</HStack>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Status</Text>
<Text fontSize="sm" variant="secondary">Submitted</Text>
</HStack>
</VStack>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
</AccountListItemMobile>
);
};
export default React.memo(PublicTagListItem);
import { import {
Box,
Tag, Tag,
Tr, Tr,
Td, Td,
HStack,
VStack, VStack,
Text, Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
...@@ -11,10 +9,8 @@ import React, { useCallback } from 'react'; ...@@ -11,10 +9,8 @@ import React, { useCallback } from 'react';
import type { PublicTag } from 'types/api/account'; import type { PublicTag } from 'types/api/account';
import AddressIcon from 'ui/shared/AddressIcon'; import AddressSnippet from 'ui/shared/AddressSnippet';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import DeleteButton from 'ui/shared/DeleteButton';
import EditButton from 'ui/shared/EditButton';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props { interface Props {
...@@ -36,18 +32,7 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -36,18 +32,7 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
<Tr alignItems="top" key={ item.id }> <Tr alignItems="top" key={ item.id }>
<Td> <Td>
<VStack spacing={ 4 } alignItems="unset"> <VStack spacing={ 4 } alignItems="unset">
{ item.addresses.map((address) => { { item.addresses.map((address) => <AddressSnippet key={ address } address={ address }/>) }
return (
<HStack spacing={ 4 } key={ address } overflow="hidden" alignItems="start">
<AddressIcon address={ address }/>
<Box overflow="hidden">
<AddressLinkWithTooltip address={ address }/>
{ /* will be added later */ }
{ /* <Text fontSize="sm" variant="secondary" mt={ 0.5 }>Address Name</Text> */ }
</Box>
</HStack>
);
}) }
</VStack> </VStack>
</Td> </Td>
<Td> <Td>
...@@ -69,10 +54,7 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -69,10 +54,7 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
</VStack> </VStack>
</Td> </Td>
<Td> <Td>
<HStack spacing={ 6 }> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
<EditButton onClick={ onItemEditClick }/>
<DeleteButton onClick={ onItemDeleteClick }/>
</HStack>
</Td> </Td>
</Tr> </Tr>
); );
......
import { Box, Text, Button, 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 { PublicTags, PublicTag } from 'types/api/account'; import type { PublicTags, PublicTag } from 'types/api/account';
import fetch from 'lib/client/fetch'; import fetch from 'lib/client/fetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import PublicTagListItem from 'ui/publicTags/PublicTagTable/PublicTagListItem';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
import DeletePublicTagModal from './DeletePublicTagModal'; import DeletePublicTagModal from './DeletePublicTagModal';
...@@ -19,6 +23,7 @@ type Props = { ...@@ -19,6 +23,7 @@ type Props = {
const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>(); const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>();
const isMobile = useIsMobile();
const { data, isLoading, isError } = useQuery<unknown, unknown, PublicTags>([ 'public-tags' ], async() => await fetch('/api/account/public-tags')); const { data, isLoading, isError } = useQuery<unknown, unknown, PublicTags>([ 'public-tags' ], async() => await fetch('/api/account/public-tags'));
...@@ -41,32 +46,53 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { ...@@ -41,32 +46,53 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
}, [ deleteModalProps ]); }, [ deleteModalProps ]);
const description = ( const description = (
<Text marginBottom={ 12 }> <AccountPageDescription>
You can request a public category tag which is displayed to all Blockscout users. You can request a public category tag which is displayed to all Blockscout users.
Public tags may be added to contract or external addresses, and any associated transactions will inherit that tag. Public tags may be added to contract or external addresses, and any associated transactions will inherit that tag.
Clicking a tag opens a page with related information and helps provide context and data organization. Clicking a tag opens a page with related information and helps provide context and data organization.
Requests are sent to a moderator for review and approval. This process can take several days. Requests are sent to a moderator for review and approval. This process can take several days.
</Text> </AccountPageDescription>
); );
if (isLoading) { if (isLoading) {
return ( const loader = isMobile ? <SkeletonAccountMobile/> : (
<> <>
{ description }
<SkeletonTable columns={ [ '50%', '25%', '25%', '108px' ] }/> <SkeletonTable columns={ [ '50%', '25%', '25%', '108px' ] }/>
<Skeleton height="48px" width="270px" marginTop={ 8 }/> <Skeleton height="48px" width="270px" marginTop={ 8 }/>
</> </>
); );
return (
<>
{ description }
{ loader }
</>
);
} }
if (isError) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
const list = isMobile ? (
<Box>
{ data.map((item) => (
<PublicTagListItem
item={ item }
key={ item.id }
onDeleteClick={ onItemDeleteClick }
onEditClick={ onItemEditClick }
/>
)) }
</Box>
) : (
<PublicTagTable data={ data } onEditClick={ onItemEditClick } onDeleteClick={ onItemDeleteClick }/>
);
return ( return (
<> <>
{ description } { description }
{ data.length > 0 && <PublicTagTable data={ data } onEditClick={ onItemEditClick } onDeleteClick={ onItemDeleteClick }/> } { data.length > 0 && list }
<Box marginTop={ 8 }> <Box marginTop={ 8 }>
<Button <Button
variant="primary" variant="primary"
......
import { IconButton, Icon } from '@chakra-ui/react'; import { IconButton, Icon, Flex } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { ControllerRenderProps, Control, FieldError } from 'react-hook-form'; import type { ControllerRenderProps, Control, FieldError } from 'react-hook-form';
import { Controller } from 'react-hook-form'; import { Controller } from 'react-hook-form';
...@@ -17,24 +17,25 @@ interface Props { ...@@ -17,24 +17,25 @@ interface Props {
error?: FieldError; error?: FieldError;
onAddFieldClick: (e: React.SyntheticEvent) => void; onAddFieldClick: (e: React.SyntheticEvent) => void;
onRemoveFieldClick: (index: number) => (e: React.SyntheticEvent) => void; onRemoveFieldClick: (index: number) => (e: React.SyntheticEvent) => void;
size?: string;
} }
const MAX_INPUTS_NUM = 10; const MAX_INPUTS_NUM = 10;
export default function PublicTagFormAction({ control, index, fieldsLength, error, onAddFieldClick, onRemoveFieldClick }: Props) { export default function PublicTagFormAction({ control, index, fieldsLength, error, onAddFieldClick, onRemoveFieldClick, size }: Props) {
const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, `addresses.${ number }.address`>}) => { const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, `addresses.${ number }.address`>}) => {
return ( return (
<AddressInput<Inputs, `addresses.${ number }.address`> <AddressInput<Inputs, `addresses.${ number }.address`>
field={ field } field={ field }
error={ error } error={ error }
size="lg" size={ size }
placeholder="Smart contract / Address (0x...)" placeholder="Smart contract / Address (0x...)"
/> />
); );
}, [ error ]); }, [ error, size ]);
return ( return (
<> <Flex flexDir="column" rowGap={ 5 } alignItems="flex-end">
<Controller <Controller
name={ `addresses.${ index }.address` } name={ `addresses.${ index }.address` }
control={ control } control={ control }
...@@ -44,31 +45,34 @@ export default function PublicTagFormAction({ control, index, fieldsLength, erro ...@@ -44,31 +45,34 @@ export default function PublicTagFormAction({ control, index, fieldsLength, erro
required: index === 0, required: index === 0,
}} }}
/> />
{ index === fieldsLength - 1 && fieldsLength < MAX_INPUTS_NUM && ( <Flex
columnGap={ 5 }
position={{ base: 'static', lg: 'absolute' }}
left={{ base: 'auto', lg: 'calc(100% + 20px)' }}
h="100%"
alignItems="center"
>
{ fieldsLength > 1 && (
<IconButton <IconButton
aria-label="add" aria-label="delete"
variant="iconBorder" variant="iconBorder"
w="30px" w="30px"
h="30px" h="30px"
onClick={ onAddFieldClick } onClick={ onRemoveFieldClick(index) }
icon={ <Icon as={ PlusIcon } w="20px" h="20px"/> } icon={ <Icon as={ MinusIcon } w="20px" h="20px"/> }
position="absolute"
right={ index === 0 ? '-50px' : '-100px' }
top="25px"
/> />
) } ) }
{ fieldsLength > 1 && ( { index === fieldsLength - 1 && fieldsLength < MAX_INPUTS_NUM && (
<IconButton <IconButton
aria-label="delete" aria-label="add"
variant="iconBorder" variant="iconBorder"
w="30px" w="30px"
h="30px" h="30px"
onClick={ onRemoveFieldClick(index) } onClick={ onAddFieldClick }
icon={ <Icon as={ MinusIcon } w="20px" h="20px"/> } icon={ <Icon as={ PlusIcon } w="20px" h="20px"/> }
position="absolute"
right="-50px"
top="25px"
/> />
) }</> ) }
</Flex>
</Flex>
); );
} }
...@@ -12,23 +12,24 @@ const TEXT_INPUT_MAX_LENGTH = 255; ...@@ -12,23 +12,24 @@ const TEXT_INPUT_MAX_LENGTH = 255;
interface Props { interface Props {
control: Control<Inputs>; control: Control<Inputs>;
error?: FieldError; error?: FieldError;
size?: string;
} }
export default function PublicTagFormComment({ control, error }: Props) { export default function PublicTagFormComment({ control, error, size }: Props) {
const renderComment = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'comment'>}) => { const renderComment = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'comment'>}) => {
return ( return (
<FormControl variant="floating" id={ field.name } size="lg" isRequired> <FormControl variant="floating" id={ field.name } size={ size } isRequired>
<Textarea <Textarea
{ ...field } { ...field }
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
size="lg" size={ size }
/> />
<FormLabel> <FormLabel>
{ getPlaceholderWithError('Specify the reason for adding tags and color preference(s)', error?.message) } { getPlaceholderWithError('Specify the reason for adding tags and color preference(s)', error?.message) }
</FormLabel> </FormLabel>
</FormControl> </FormControl>
); );
}, [ error ]); }, [ error, size ]);
return ( return (
<Controller <Controller
......
...@@ -16,6 +16,7 @@ import type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } from 'types ...@@ -16,6 +16,7 @@ import type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } from 'types
import type { ErrorType } from 'lib/client/fetch'; import type { ErrorType } from 'lib/client/fetch';
import fetch from 'lib/client/fetch'; import fetch from 'lib/client/fetch';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import useIsMobile from 'lib/hooks/useIsMobile';
import { EMAIL_REGEXP } from 'lib/validations/email'; import { EMAIL_REGEXP } from 'lib/validations/email';
import FormSubmitAlert from 'ui/shared/FormSubmitAlert'; import FormSubmitAlert from 'ui/shared/FormSubmitAlert';
...@@ -52,10 +53,12 @@ const placeholders = { ...@@ -52,10 +53,12 @@ const placeholders = {
comment: 'Specify the reason for adding tags and color preference(s).', comment: 'Specify the reason for adding tags and color preference(s).',
} as Record<Path<Inputs>, string>; } as Record<Path<Inputs>, string>;
const ADDRESS_INPUT_BUTTONS_WIDTH = 170; const ADDRESS_INPUT_BUTTONS_WIDTH = 100;
const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile();
const inputSize = isMobile ? 'md' : 'lg';
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
defaultValues: { defaultValues: {
...@@ -149,10 +152,10 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -149,10 +152,10 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
}, [ changeToDataScreen ]); }, [ changeToDataScreen ]);
return ( return (
<Box width={ `calc(100% - ${ ADDRESS_INPUT_BUTTONS_WIDTH }px)` } maxWidth="844px"> <Box width={{ base: 'auto', lg: `calc(100% - ${ ADDRESS_INPUT_BUTTONS_WIDTH }px)` }} maxWidth="844px">
{ isAlertVisible && <Box mb={ 4 }><FormSubmitAlert/></Box> } { isAlertVisible && <Box mb={ 4 }><FormSubmitAlert/></Box> }
<Text size="sm" variant="secondary" paddingBottom={ 5 }>Company info</Text> <Text size="sm" variant="secondary" paddingBottom={ 5 }>Company info</Text>
<Grid templateColumns="1fr 1fr" rowGap={ 4 } columnGap={ 5 }> <Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 5 }>
<GridItem> <GridItem>
<PublicTagsFormInput<Inputs> <PublicTagsFormInput<Inputs>
fieldName="fullName" fieldName="fullName"
...@@ -160,6 +163,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -160,6 +163,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
label={ placeholders.fullName } label={ placeholders.fullName }
error={ errors.fullName } error={ errors.fullName }
required required
size={ inputSize }
/> />
</GridItem> </GridItem>
<GridItem> <GridItem>
...@@ -168,6 +172,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -168,6 +172,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
control={ control } control={ control }
label={ placeholders.companyName } label={ placeholders.companyName }
error={ errors.companyName } error={ errors.companyName }
size={ inputSize }
/> />
</GridItem> </GridItem>
<GridItem> <GridItem>
...@@ -178,6 +183,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -178,6 +183,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
pattern={ EMAIL_REGEXP } pattern={ EMAIL_REGEXP }
error={ errors.email } error={ errors.email }
required required
size={ inputSize }
/> />
</GridItem> </GridItem>
<GridItem> <GridItem>
...@@ -186,10 +192,11 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -186,10 +192,11 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
control={ control } control={ control }
label={ placeholders.companyUrl } label={ placeholders.companyUrl }
error={ errors?.companyUrl } error={ errors?.companyUrl }
size={ inputSize }
/> />
</GridItem> </GridItem>
</Grid> </Grid>
<Box marginTop={ 4 } marginBottom={ 8 }> <Box marginTop={{ base: 5, lg: 8 }} marginBottom={{ base: 5, lg: 8 }}>
<PublicTagFormAction control={ control }/> <PublicTagFormAction control={ control }/>
</Box> </Box>
<Text size="sm" variant="secondary" marginBottom={ 5 }>Public tags (2 tags maximum, please use &quot;;&quot; as a divider)</Text> <Text size="sm" variant="secondary" marginBottom={ 5 }>Public tags (2 tags maximum, please use &quot;;&quot; as a divider)</Text>
...@@ -199,7 +206,9 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -199,7 +206,9 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
control={ control } control={ control }
label={ placeholders.tags } label={ placeholders.tags }
error={ errors.tags } error={ errors.tags }
required/> required
size={ inputSize }
/>
</Box> </Box>
{ fields.map((field, index) => { { fields.map((field, index) => {
return ( return (
...@@ -211,12 +220,13 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -211,12 +220,13 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
fieldsLength={ fields.length } fieldsLength={ fields.length }
onAddFieldClick={ onAddFieldClick } onAddFieldClick={ onAddFieldClick }
onRemoveFieldClick={ onRemoveFieldClick } onRemoveFieldClick={ onRemoveFieldClick }
size={ inputSize }
/> />
</Box> </Box>
); );
}) } }) }
<Box marginBottom={ 8 }> <Box marginBottom={ 8 }>
<PublicTagFormComment control={ control } error={ errors.comment }/> <PublicTagFormComment control={ control } error={ errors.comment } size={ inputSize }/>
</Box> </Box>
<HStack spacing={ 6 }> <HStack spacing={ 6 }>
<Button <Button
......
...@@ -14,6 +14,7 @@ interface Props<TInputs extends FieldValues> { ...@@ -14,6 +14,7 @@ interface Props<TInputs extends FieldValues> {
control: Control<TInputs, object>; control: Control<TInputs, object>;
pattern?: RegExp; pattern?: RegExp;
error?: FieldError; error?: FieldError;
size?: string;
} }
export default function PublicTagsFormInput<Inputs extends FieldValues>({ export default function PublicTagsFormInput<Inputs extends FieldValues>({
...@@ -23,13 +24,14 @@ export default function PublicTagsFormInput<Inputs extends FieldValues>({ ...@@ -23,13 +24,14 @@ export default function PublicTagsFormInput<Inputs extends FieldValues>({
fieldName, fieldName,
pattern, pattern,
error, error,
size,
}: Props<Inputs>) { }: Props<Inputs>) {
const renderInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, typeof fieldName>}) => { const renderInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, typeof fieldName>}) => {
return ( return (
<FormControl variant="floating" id={ field.name } isRequired={ required } size="lg"> <FormControl variant="floating" id={ field.name } isRequired={ required } size={ size }>
<Input <Input
{ ...field } { ...field }
size="lg" size={ size }
required={ required } required={ required }
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
maxLength={ TEXT_INPUT_MAX_LENGTH } maxLength={ TEXT_INPUT_MAX_LENGTH }
...@@ -37,7 +39,7 @@ export default function PublicTagsFormInput<Inputs extends FieldValues>({ ...@@ -37,7 +39,7 @@ export default function PublicTagsFormInput<Inputs extends FieldValues>({
<FormLabel>{ getPlaceholderWithError(label, error?.message) }</FormLabel> <FormLabel>{ getPlaceholderWithError(label, error?.message) }</FormLabel>
</FormControl> </FormControl>
); );
}, [ label, required, error ]); }, [ label, required, error, size ]);
return ( return (
<Controller <Controller
name={ fieldName } name={ fieldName }
......
import { VStack, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
interface Props {
children: React.ReactNode;
}
const AccountListItemMobile = ({ children }: Props) => {
return (
<VStack
gap={ 4 }
alignItems="flex-start"
paddingY={ 6 }
borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderTopWidth="1px"
_last={{
borderBottomWidth: '1px',
}}
>
{ children }
</VStack>
);
};
export default AccountListItemMobile;
import { Text } from '@chakra-ui/react';
import React from 'react';
const AccountPageDescription = ({ children }: {children: React.ReactNode}) => {
return (
<Text marginBottom={{ base: 6, lg: 12 }}>
{ children }
</Text>
);
};
export default AccountPageDescription;
import { Heading } from '@chakra-ui/react'; import { Heading } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
const PageHeader = ({ text }: {text: string}) => { const AccountPageHeader = ({ text }: {text: string}) => {
return ( return (
<Heading as="h1" size="lg" marginBottom={ 8 }>{ text }</Heading> <Heading as="h1" size="lg" marginBottom={{ base: 6, lg: 8 }}>{ text }</Heading>
); );
}; };
export default PageHeader; export default AccountPageHeader;
...@@ -4,7 +4,7 @@ import Jazzicon, { jsNumberForAddress } from 'react-jazzicon'; ...@@ -4,7 +4,7 @@ import Jazzicon, { jsNumberForAddress } from 'react-jazzicon';
const AddressIcon = ({ address }: {address: string}) => { const AddressIcon = ({ address }: {address: string}) => {
return ( return (
<Box width="24px"> <Box width="24px" display="inline-flex">
<Jazzicon diameter={ 24 } seed={ jsNumberForAddress(address) }/> <Jazzicon diameter={ 24 } seed={ jsNumberForAddress(address) }/>
</Box> </Box>
); );
......
...@@ -22,13 +22,14 @@ const AddressLinkWithTooltip = ({ address, type = 'address' }: Props) => { ...@@ -22,13 +22,14 @@ const AddressLinkWithTooltip = ({ address, type = 'address' }: Props) => {
url = basePath + '/address/' + address + '/tokens#address-tabs'; url = basePath + '/address/' + address + '/tokens#address-tabs';
} }
return ( return (
<HStack spacing={ 2 } alignContent="center" overflow="hidden"> <HStack spacing={ 2 } alignContent="center" overflow="hidden" maxW="100%">
<Link <Link
href={ url } href={ url }
target="_blank" target="_blank"
overflow="hidden" overflow="hidden"
fontWeight={ FONT_WEIGHT } fontWeight={ FONT_WEIGHT }
lineHeight="24px" lineHeight="24px"
whiteSpace="nowrap"
> >
<AddressWithDots address={ address } fontWeight={ FONT_WEIGHT }/> <AddressWithDots address={ address } fontWeight={ FONT_WEIGHT }/>
</Link> </Link>
......
import { Box, HStack, Text } from '@chakra-ui/react';
import React from 'react';
import AddressIcon from 'ui/shared/AddressIcon';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip';
interface Props {
address: string;
subtitle?: string;
}
const AddressSnippet = ({ address, subtitle }: Props) => {
return (
<HStack spacing={ 4 } key={ address } overflow="hidden" alignItems="start" maxW="100%">
<AddressIcon address={ address }/>
<Box overflow="hidden">
<AddressLinkWithTooltip address={ address }/>
{ subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 }>{ subtitle }</Text> }
</Box>
</HStack>
);
};
export default React.memo(AddressSnippet);
import { Box, HStack, Icon, Flex, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import keyIcon from 'icons/key.svg';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props {
apiKey: string;
name: string;
}
const ApiKeySnippet = ({ apiKey, name }: Props) => {
return (
<HStack spacing={ 2 } alignItems="start">
<Icon as={ keyIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') }/>
<Box>
<Flex alignItems={{ base: 'flex-start', lg: 'center' }}>
<Text fontSize="md" lineHeight={ 6 } fontWeight={ 600 } mr={ 1 }>{ apiKey }</Text>
<CopyToClipboard text={ apiKey }/>
</Flex>
{ name && <Text fontSize="sm" variant="secondary" mt={ 1 }>{ name }</Text> }
</Box>
</HStack>
);
};
export default React.memo(ApiKeySnippet);
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;
...@@ -54,7 +54,7 @@ const DeleteModal: React.FC<Props> = ({ ...@@ -54,7 +54,7 @@ const DeleteModal: React.FC<Props> = ({
}, [ setAlertVisible, mutation ]); }, [ setAlertVisible, mutation ]);
return ( return (
<Modal isOpen={ isOpen } onClose={ onModalClose } size="md"> <Modal isOpen={ isOpen } onClose={ onModalClose } size={{ base: 'full', lg: 'md' }}>
<ModalOverlay/> <ModalOverlay/>
<ModalContent> <ModalContent>
<ModalHeader fontWeight="500" textStyle="h3">{ title }</ModalHeader> <ModalHeader fontWeight="500" textStyle="h3">{ title }</ModalHeader>
......
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;
...@@ -39,14 +39,14 @@ export default function FormModal<TData>({ ...@@ -39,14 +39,14 @@ export default function FormModal<TData>({
}, [ onClose, setAlertVisible ]); }, [ onClose, setAlertVisible ]);
return ( return (
<Modal isOpen={ isOpen } onClose={ onModalClose } size="md" > <Modal isOpen={ isOpen } onClose={ onModalClose } size={{ base: 'full', lg: 'md' }}>
<ModalOverlay/> <ModalOverlay/>
<ModalContent> <ModalContent>
<ModalHeader fontWeight="500" textStyle="h3">{ title }</ModalHeader> <ModalHeader fontWeight="500" textStyle="h3">{ title }</ModalHeader>
<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 }
......
import { Box, Flex, Skeleton, SkeletonCircle, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
interface Props {
showFooterSlot?: boolean;
}
const SkeletonAccountMobile = ({ showFooterSlot }: Props) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
_first={{
borderTopWidth: '0',
pt: '0',
}}
>
<Flex columnGap={ 2 } w="100%" alignItems="center">
<SkeletonCircle size="6" flexShrink="0"/>
<Skeleton h={ 4 } w="100%"/>
</Flex>
<Skeleton h={ 4 } w="164px"/>
<Skeleton h={ 4 } w="164px"/>
<Flex columnGap={ 3 } mt={ 7 }>
{ showFooterSlot && (
<Flex alignItems="center" columnGap={ 2 }>
<Skeleton h={ 4 } w="164px"/>
<SkeletonCircle size="6" flexShrink="0"/>
</Flex>
) }
<SkeletonCircle size="6" flexShrink="0" ml="auto"/>
<SkeletonCircle size="6" flexShrink="0"/>
</Flex>
</Flex>
)) }
</Box>
);
};
export default SkeletonAccountMobile;
import { Tooltip, IconButton, Icon, HStack } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import DeleteIcon from 'icons/delete.svg';
import EditIcon from 'icons/edit.svg';
type Props = {
onEditClick: () => void;
onDeleteClick: () => void;
}
const TableItemActionButtons = ({ onEditClick, onDeleteClick }: Props) => {
// prevent set focus on button when closing modal
const onFocusCapture = useCallback((e: React.SyntheticEvent) => e.stopPropagation(), []);
return (
<HStack spacing={ 6 } alignSelf="flex-end">
<Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="icon"
w="30px"
h="30px"
onClick={ onEditClick }
icon={ <Icon as={ EditIcon } w="20px" h="20px"/> }
onFocusCapture={ onFocusCapture }
/>
</Tooltip>
<Tooltip label="Delete">
<IconButton
aria-label="delete"
variant="icon"
w="30px"
h="30px"
onClick={ onDeleteClick }
icon={ <Icon as={ DeleteIcon } w="20px" h="20px"/> }
onFocusCapture={ onFocusCapture }
/>
</Tooltip>
</HStack>
);
};
export default React.memo(TableItemActionButtons);
import { Box, HStack, Icon, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import transactionIcon from 'icons/transactions.svg';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip';
interface Props {
hash: string;
}
const TransactionSnippet = ({ hash }: Props) => {
return (
<HStack spacing={ 2 } overflow="hidden" alignItems="start" maxW="100%">
<Icon as={ transactionIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') }/>
<Box overflow="hidden">
<AddressLinkWithTooltip address={ hash } type="transaction"/>
</Box>
</HStack>
);
};
export default React.memo(TransactionSnippet);
...@@ -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 }
...@@ -178,7 +178,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -178,7 +178,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>Address <Text fontWeight="600" as="span"> { 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,19 +17,18 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => { ...@@ -18,19 +17,18 @@ 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">
<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_hash }/> <AddressSnippet address={ item.address_hash }/>
<HStack spacing={ 0 } fontSize="sm" h={ 6 }> <HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
<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 }` + nativeBalance }</Text>
<Text variant="secondary">{ `${ nbsp }(${ nativeBalanceUSD })` }</Text> <Text variant="secondary">{ `${ nbsp }(${ nativeBalanceUSD })` }</Text>
</HStack> </HStack>
{ item.tokens_count && ( { item.tokens_count && (
<HStack spacing={ 0 } fontSize="sm" h={ 6 }> <HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
<Icon as={ TokensIcon } marginRight="10px" w="17px" h="16px"/> <Icon as={ TokensIcon } marginRight="10px" w="17px" h="16px"/>
<Text color={ mainTextColor }>{ `Tokens:${ nbsp }` + item.tokens_count }</Text> <Text color={ mainTextColor }>{ `Tokens:${ nbsp }` + item.tokens_count }</Text>
{ /* api does not provide token prices */ } { /* api does not provide token prices */ }
...@@ -40,14 +38,13 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => { ...@@ -40,14 +38,13 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
) } ) }
{ /* api does not provide token prices */ } { /* api does not provide token prices */ }
{ /* { item.address_balance && ( { /* { item.address_balance && (
<HStack spacing={ 0 } fontSize="sm" h={ 6 }> <HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
<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,15 @@ import { ...@@ -3,15 +3,15 @@ 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 fetch from 'lib/client/fetch';
import EditButton from 'ui/shared/EditButton'; import useToast from 'lib/hooks/useToast';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import WatchListAddressItem from './WatchListAddressItem'; import WatchListAddressItem from './WatchListAddressItem';
...@@ -24,6 +24,7 @@ interface Props { ...@@ -24,6 +24,7 @@ interface Props {
const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { const WatchlistTableItem = ({ item, 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 onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
return onEditClick(item); return onEditClick(item);
}, [ item, onEditClick ]); }, [ item, onEditClick ]);
...@@ -32,16 +33,33 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -32,16 +33,33 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return onDeleteClick(item); return onDeleteClick(item);
}, [ item, onDeleteClick ]); }, [ item, onDeleteClick ]);
const toast = useToast();
const showToast = useCallback(() => {
toast({
position: 'top-right',
description: 'There has been an error processing your request',
colorScheme: 'red',
status: 'error',
variant: 'subtle',
isClosable: true,
icon: null,
});
}, [ toast ]);
const { mutate } = useMutation(() => { const { mutate } = useMutation(() => {
setSwitchDisabled(true);
const data = { ...item, notification_methods: { email: !notificationEnabled } }; const data = { ...item, notification_methods: { email: !notificationEnabled } };
return fetch(`/api/account/watchlist/${ item.id }`, { method: 'PUT', body: JSON.stringify(data) }); setNotificationEnabled(prevState => !prevState);
return fetch(`/api/account1/watchlist/${ item.id }`, { method: 'PUT', body: JSON.stringify(data) });
}, { }, {
onError: () => { onError: () => {
// eslint-disable-next-line no-console showToast();
console.log('error'); setNotificationEnabled(prevState => !prevState);
setSwitchDisabled(false);
}, },
onSuccess: () => { onSuccess: () => {
setNotificationEnabled(prevState => !prevState); setSwitchDisabled(false);
}, },
}); });
...@@ -59,12 +77,17 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -59,12 +77,17 @@ const WatchlistTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
</Tag> </Tag>
</TruncatedTextTooltip> </TruncatedTextTooltip>
</Td> </Td>
<Td><Switch colorScheme="blue" size="md" isChecked={ notificationEnabled } onChange={ onSwitch }/></Td>
<Td> <Td>
<HStack spacing={ 6 }> <Switch
<EditButton onClick={ onItemEditClick }/> colorScheme="blue"
<DeleteButton onClick={ onItemDeleteClick }/> size="md"
</HStack> isChecked={ notificationEnabled }
onChange={ onSwitch }
isDisabled={ switchDisabled }
/>
</Td>
<Td>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
</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