Commit 3430953e authored by isstuev's avatar isstuev

public tags page

parent 0854840b
export const publicTags = [
{
addresses: [
{
address: '0x35317007D203b8a86CA727ad44E473E40450E377',
addressName: 'DarkForest',
},
{
address: '0x35317007D203b8a86CA727ad44E473E40450E378',
addressName: 'DarkForest2',
},
],
tags: [
{
name: 'darkforest',
// colorHex: '#4A5568',
// backgroundHex: '#E2E8F0',
},
],
date: 'Jun 10, 2022',
id: '123',
userName: 'Tatyana',
userEmail: 'sample@gmail.com',
companyName: 'Contract name',
companyUrl: 'contractname.com',
comment: 'Please use #ED8936 color for tag...',
},
{
addresses: [
{
address: '0x35317007D203b8a86CA727ad44E473E40450E377',
},
],
tags: [
{
name: 'OMNI',
colorHex: '#FFFFFF',
backgroundHex: '#1A202C',
},
{
name: '123456789012345678901237123123',
colorHex: '#FFFFFF',
backgroundHex: '#6B46C1',
},
],
date: 'Jun 5, 2022',
id: '456',
},
{
addresses: [
{
address: '0x35317007D203b8a86CA727ad44E473E40450E377',
addressName: 'Contract name',
},
],
tags: [
{
name: 'SANA',
colorHex: '#FFFFFF',
backgroundHex: '#ED8936',
},
],
date: 'Jun 1, 2022',
id: '789',
},
];
export type TPublicTags = Array<TPublicTagItem>
export type TPublicTagItem = {
addresses: Array<TPublicTagAddress>;
tags: Array<TPublicTag>;
// status: typeof STATUS;
date: string;
// id is for react element key, as tag or address may not be unique
id: string;
userName?: string;
userEmail?: string;
companyName?: string;
companyUrl?: string;
comment?: string;
}
export type TPublicTagAddress = {
address: string;
addressName?: string;
}
export type TPublicTag = {
name: string;
colorHex?: string;
backgroundHex?: string;
}
<svg width="16" height="2" viewBox="0 0 16 2" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M.5 1a.937.937 0 0 1 .938-.938h13.124a.938.938 0 0 1 0 1.875H1.438A.937.937 0 0 1 .5 1Z" fill="#2B6CB0"/></svg>
\ No newline at end of file
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 .5a.937.937 0 0 1 .938.938v5.625h5.624a.937.937 0 0 1 0 1.875H8.939v5.624a.937.937 0 0 1-1.876 0V8.939H1.438a.937.937 0 1 1 0-1.876h5.625V1.438A.937.937 0 0 1 8 .5Z" fill="#2B6CB0"/></svg>
\ No newline at end of file
import React from 'react';
import type { NextPage } from 'next';
import Head from 'next/head'
import PublicTags from 'ui/pages/PublicTags';
const PublicTagsPage: NextPage = () => {
return (
<>
<Head><title>Public tags</title></Head>
<PublicTags/>
</>
);
}
export default PublicTagsPage
...@@ -41,10 +41,26 @@ const variantIconBlue: SystemStyleFunction = (props) => { ...@@ -41,10 +41,26 @@ const variantIconBlue: SystemStyleFunction = (props) => {
} }
} }
const variantIconBorderBlue: SystemStyleFunction = (props) => {
return {
color: mode('blue.600', 'blue.300')(props),
borderColor: mode('blue.600', 'blue.300')(props),
border: '2px solid',
_hover: {
color: mode('blue.400', 'blue.200')(props),
borderColor: mode('blue.400', 'blue.200')(props),
},
_disabled: {
opacity: 0.2,
},
}
}
const variants = { const variants = {
primary: variantPrimary, primary: variantPrimary,
secondary: variantSecondary, secondary: variantSecondary,
iconBlue: variantIconBlue, iconBlue: variantIconBlue,
iconBorderBlue: variantIconBorderBlue,
} }
const Button: ComponentStyleConfig = { const Button: ComponentStyleConfig = {
......
...@@ -32,6 +32,9 @@ const variantFloating: PartsStyleFunction<typeof parts> = (props: StyleFunctionP ...@@ -32,6 +32,9 @@ const variantFloating: PartsStyleFunction<typeof parts> = (props: StyleFunctionP
input: { input: {
...activeInputStyles, ...activeInputStyles,
}, },
textarea: {
...activeInputStyles,
},
'label .chakra-form__required-indicator': { 'label .chakra-form__required-indicator': {
color: getColor(theme, fc), color: getColor(theme, fc),
}, },
...@@ -53,26 +56,47 @@ const variantFloating: PartsStyleFunction<typeof parts> = (props: StyleFunctionP ...@@ -53,26 +56,47 @@ const variantFloating: PartsStyleFunction<typeof parts> = (props: StyleFunctionP
'input:not(:placeholder-shown) + label': { 'input:not(:placeholder-shown) + label': {
...activeLabelStyles, ...activeLabelStyles,
}, },
'textarea:not(:placeholder-shown) + label': {
...activeLabelStyles,
},
'input[aria-invalid=true] + label': { 'input[aria-invalid=true] + label': {
color: getColor(theme, ec), color: getColor(theme, ec),
}, },
'textarea[aria-invalid=true] + label': {
color: getColor(theme, ec),
},
// input's styles // input's styles
input: { input: {
padding: '20px', padding: '20px',
}, },
textarea: {
padding: '20px',
},
'input:not(:placeholder-shown)': { 'input:not(:placeholder-shown)': {
...activeInputStyles, ...activeInputStyles,
}, },
'textarea:not(:placeholder-shown)': {
...activeInputStyles,
},
'input[aria-invalid=true]': { 'input[aria-invalid=true]': {
borderColor: getColor(theme, ec), borderColor: getColor(theme, ec),
}, },
'textarea[aria-invalid=true]': {
borderColor: getColor(theme, ec),
},
// indicator's styles // indicator's styles
'input:not(:placeholder-shown) + label .chakra-form__required-indicator': { 'input:not(:placeholder-shown) + label .chakra-form__required-indicator': {
color: getColor(theme, fc), color: getColor(theme, fc),
}, },
'textarea:not(:placeholder-shown) + label .chakra-form__required-indicator': {
color: getColor(theme, fc),
},
'input[aria-invalid=true] + label .chakra-form__required-indicator': { 'input[aria-invalid=true] + label .chakra-form__required-indicator': {
color: getColor(theme, ec), color: getColor(theme, ec),
}, },
'textarea[aria-invalid=true] + label .chakra-form__required-indicator': {
color: getColor(theme, ec),
},
}, },
requiredIndicator: { requiredIndicator: {
marginStart: 0, marginStart: 0,
......
import type { inputAnatomy as parts } from '@chakra-ui/anatomy'; import type { inputAnatomy as parts } from '@chakra-ui/anatomy';
import type { ComponentStyleConfig } from '@chakra-ui/theme'; import type { ComponentStyleConfig } from '@chakra-ui/theme';
import type { PartsStyleFunction, SystemStyleObject } from '@chakra-ui/theme-tools'; import type { PartsStyleFunction, SystemStyleObject } from '@chakra-ui/theme-tools';
import { getColor, mode } from '@chakra-ui/theme-tools'; import { mode } from '@chakra-ui/theme-tools';
import getDefaultFormColors from '../utils/getDefaultFormColors';
import getDefaultTransitionProps from '../utils/getDefaultTransitionProps'; import getDefaultTransitionProps from '../utils/getDefaultTransitionProps';
import getOutlinedFieldStyles from '../utils/getOutlinedFieldStyles';
const sizes: Record<string, SystemStyleObject> = { const sizes: Record<string, SystemStyleObject> = {
md: { md: {
...@@ -14,42 +14,21 @@ const sizes: Record<string, SystemStyleObject> = { ...@@ -14,42 +14,21 @@ const sizes: Record<string, SystemStyleObject> = {
h: '60px', h: '60px',
borderRadius: 'base', borderRadius: 'base',
}, },
lg: {
fontSize: 'md',
lineHeight: '20px',
px: '24px',
py: '28px',
h: '80px',
borderRadius: 'base',
},
} }
const variantOutline: PartsStyleFunction<typeof parts> = (props) => { const variantOutline: PartsStyleFunction<typeof parts> = (props) => {
const { theme } = props
const { focusColor: fc, errorColor: ec } = getDefaultFormColors(props);
const transitionProps = getDefaultTransitionProps(); const transitionProps = getDefaultTransitionProps();
return { return {
field: { field: getOutlinedFieldStyles(props),
border: '2px solid',
bg: 'inherit',
borderColor: mode('gray.100', 'whiteAlpha.200')(props),
...transitionProps,
_hover: {
borderColor: mode('gray.300', 'whiteAlpha.400')(props),
},
_readOnly: {
boxShadow: 'none !important',
userSelect: 'all',
},
_disabled: {
opacity: 1,
background: mode('gray.200', 'whiteAlpha.400')(props),
border: 'none',
cursor: 'not-allowed',
},
_invalid: {
borderColor: getColor(theme, ec),
boxShadow: `none`,
},
_focusVisible: {
zIndex: 1,
borderColor: getColor(theme, fc),
boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -1px rgba(0, 0, 0, 0.06)',
},
},
addon: { addon: {
border: '2px solid', border: '2px solid',
borderColor: mode('gray.100', 'whiteAlpha.200')(props), borderColor: mode('gray.100', 'whiteAlpha.200')(props),
...@@ -65,6 +44,10 @@ const Input: ComponentStyleConfig = { ...@@ -65,6 +44,10 @@ const Input: ComponentStyleConfig = {
field: sizes.md, field: sizes.md,
addon: sizes.md, addon: sizes.md,
}, },
lg: {
field: sizes.lg,
addon: sizes.lg,
},
}, },
defaultProps: { defaultProps: {
size: 'md', size: 'md',
......
import type {
SystemStyleObject,
} from '@chakra-ui/theme-tools'
import type { ComponentStyleConfig } from '@chakra-ui/theme';
import getOutlinedFieldStyles from '../utils/getOutlinedFieldStyles';
const sizes: Record<string, SystemStyleObject> = {
lg: {
fontSize: 'md',
lineHeight: '20px',
px: '24px',
py: '28px',
h: '160px',
borderRadius: 'base',
},
}
const Textarea: ComponentStyleConfig = {
sizes,
variants: {
outline: (props) => getOutlinedFieldStyles(props),
},
defaultProps: {
size: 'md',
variant: 'outline',
},
}
export default Textarea;
...@@ -7,6 +7,7 @@ import Modal from './Modal'; ...@@ -7,6 +7,7 @@ import Modal from './Modal';
import Table from './Table'; import Table from './Table';
import Tabs from './Tabs'; import Tabs from './Tabs';
import Tag from './Tag'; import Tag from './Tag';
import Textarea from './Textarea';
import Tooltip from './Tooltip'; import Tooltip from './Tooltip';
const components = { const components = {
...@@ -19,6 +20,7 @@ const components = { ...@@ -19,6 +20,7 @@ const components = {
Tabs, Tabs,
Table, Table,
Tag, Tag,
Textarea,
Tooltip, Tooltip,
} }
......
import type { StyleFunctionProps } from '@chakra-ui/theme-tools';
import { mode, getColor } from '@chakra-ui/theme-tools';
import getDefaultFormColors from './getDefaultFormColors';
import getDefaultTransitionProps from './getDefaultTransitionProps';
export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
const { theme } = props
const { focusColor: fc, errorColor: ec } = getDefaultFormColors(props);
const transitionProps = getDefaultTransitionProps();
return {
border: '2px solid',
bg: 'inherit',
borderColor: mode('gray.100', 'whiteAlpha.200')(props),
...transitionProps,
_hover: {
borderColor: mode('gray.300', 'whiteAlpha.400')(props),
},
_readOnly: {
boxShadow: 'none !important',
userSelect: 'all',
},
_disabled: {
opacity: 1,
background: mode('gray.200', 'whiteAlpha.400')(props),
border: 'none',
cursor: 'not-allowed',
},
_invalid: {
borderColor: getColor(theme, ec),
boxShadow: `none`,
},
_focusVisible: {
zIndex: 1,
borderColor: getColor(theme, fc),
boxShadow: '0px 4px 6px -1px rgba(0, 0, 0, 0.1), 0px 2px 4px -1px rgba(0, 0, 0, 0.06)',
},
':-webkit-autofill': { transition: 'background-color 5000s ease-in-out 0s' },
':-webkit-autofill:hover': { transition: 'background-color 5000s ease-in-out 0s' },
':-webkit-autofill:focus': { transition: 'background-color 5000s ease-in-out 0s' },
}
}
...@@ -25,7 +25,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, name }) => { ...@@ -25,7 +25,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, name }) => {
onClose={ onClose } onClose={ onClose }
onDelete={ onDelete } onDelete={ onDelete }
title="Remove API key" title="Remove API key"
renderText={ renderText } renderContent={ renderText }
/> />
) )
} }
......
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { Box, Button, Heading, HStack, Link, Text, useColorModeValue, useDisclosure } from '@chakra-ui/react'; import { Box, Button, HStack, Link, Text, useColorModeValue, useDisclosure } from '@chakra-ui/react';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import AccountPageHeader from 'ui/shared/AccountPageHeader';
import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable'; import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable';
import ApiKeyModal from 'ui/apiKey/ApiKeyModal/ApiKeyModal'; import ApiKeyModal from 'ui/apiKey/ApiKeyModal/ApiKeyModal';
import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal'; import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal';
...@@ -49,7 +49,7 @@ const ApiKeys: React.FC = () => { ...@@ -49,7 +49,7 @@ const ApiKeys: React.FC = () => {
return ( return (
<Page> <Page>
<Box h="100%"> <Box h="100%">
<Heading as="h1" size="lg" marginBottom={ 8 }>API keys</Heading> <AccountPageHeader header="API keys"/>
<Text marginBottom={ 12 }> <Text marginBottom={ 12 }>
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>.
......
...@@ -2,7 +2,6 @@ import React from 'react'; ...@@ -2,7 +2,6 @@ import React from 'react';
import { import {
Box, Box,
Heading,
Tab, Tab,
Tabs, Tabs,
TabList, TabList,
...@@ -11,6 +10,7 @@ import { ...@@ -11,6 +10,7 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import AccountPageHeader from 'ui/shared/AccountPageHeader';
import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags'; import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags';
import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags'; import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags';
...@@ -22,7 +22,7 @@ const PrivateTags: React.FC = () => { ...@@ -22,7 +22,7 @@ const PrivateTags: React.FC = () => {
return ( return (
<Page> <Page>
<Box h="100%"> <Box h="100%">
<Heading as="h1" size="lg" marginBottom={ 8 }>Private tags</Heading> <AccountPageHeader header="Private tags"/>
<Tabs variant="soft-rounded" colorScheme="blue" isLazy> <Tabs variant="soft-rounded" colorScheme="blue" isLazy>
<TabList marginBottom={ 8 }> <TabList marginBottom={ 8 }>
<Tab>Address</Tab> <Tab>Address</Tab>
......
import React, { useCallback, useState } from 'react';
import { animateScroll } from 'react-scroll';
import {
Box,
useToast,
} from '@chakra-ui/react';
import Page from 'ui/shared/Page/Page';
import AccountPageHeader from 'ui/shared/AccountPageHeader';
import PublicTagsData from 'ui/publicTags/PublicTagsData';
import PublicTagsForm from 'ui/publicTags/PublicTagsForm/PublicTagsForm';
type TScreen = 'data' | 'form';
type TToastAction = 'added' | 'removed';
const toastDescriptions = {
added: 'Your request sent to moderator. Waiting for...',
removed: 'Tags have been removed.',
} as Record<TToastAction, string>;
const PublicTags: React.FC = () => {
const [ screen, setScreen ] = useState<TScreen>('data');
const [ formData, setFormData ] = useState();
const toast = useToast()
const showToast = useCallback((action: TToastAction) => {
toast({
position: 'top-right',
title: 'Success',
description: toastDescriptions[action],
colorScheme: 'green',
status: 'success',
variant: 'subtle',
isClosable: true,
icon: null,
});
}, [ toast ]);
const changeToFormScreen = useCallback((data: any) => {
setFormData(data);
setScreen('form');
animateScroll.scrollToTop({
duration: 500,
delay: 100,
});
}, []);
const changeToDataScreen = useCallback((success?: boolean) => {
if (success) {
showToast('added');
}
setScreen('data');
animateScroll.scrollToTop({
duration: 500,
delay: 100,
});
}, [ showToast ]);
const onTagDelete = useCallback(() => showToast('removed'), [ showToast ]);
let content;
let header;
if (screen === 'data') {
content = <PublicTagsData changeToFormScreen={ changeToFormScreen } onTagDelete={ onTagDelete }/>
header = 'Public tags'
} else {
content = <PublicTagsForm changeToDataScreen={ changeToDataScreen } data={ formData }/>
header = formData ? 'Request to edit a public tag/label' : 'Request a public tag/label';
}
return (
<Page>
<Box h="100%">
<AccountPageHeader header={ header }/>
{ content }
</Box>
</Page>
);
};
export default PublicTags;
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { Box, Button, Text, useDisclosure, Heading } from '@chakra-ui/react'; import { Box, Button, Text, useDisclosure } from '@chakra-ui/react';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import AccountPageHeader from 'ui/shared/AccountPageHeader';
import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable'; import WatchlistTable from 'ui/watchlist/WatchlistTable/WatchlistTable';
import AddressModal from 'ui/watchlist/AddressModal/AddressModal'; import AddressModal from 'ui/watchlist/AddressModal/AddressModal';
...@@ -41,7 +41,7 @@ const WatchList: React.FC = () => { ...@@ -41,7 +41,7 @@ const WatchList: React.FC = () => {
return ( return (
<Page> <Page>
<Box h="100%"> <Box h="100%">
<Heading as="h1" size="lg" marginBottom={ 8 }>Watch list</Heading> <AccountPageHeader header="Watch list"/>
<Text marginBottom={ 12 }>An email notification can be sent to you when an address on your watch list sends or receives any transactions.</Text> <Text marginBottom={ 12 }>An email notification can be sent to you when an address on your watch list sends or receives any transactions.</Text>
{ Boolean(watchlist.length) && ( { Boolean(watchlist.length) && (
<WatchlistTable <WatchlistTable
......
...@@ -26,7 +26,7 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, tag }) => { ...@@ -26,7 +26,7 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, tag }) => {
onClose={ onClose } onClose={ onClose }
onDelete={ onDelete } onDelete={ onDelete }
title="Removal of private tag" title="Removal of private tag"
renderText={ renderText } renderContent={ renderText }
/> />
) )
} }
......
import React, { useCallback, useState } from 'react';
import type { ChangeEvent } from 'react';
import { Flex, Text, FormControl, FormLabel, Textarea } from '@chakra-ui/react';
import DeleteModal from 'ui/shared/DeleteModal';
import type { TPublicTag } from 'data/publicTags';
type Props = {
isOpen: boolean;
onClose: () => void;
tags: Array<TPublicTag>;
onDeleteSuccess: () => void;
}
const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onDeleteSuccess }) => {
const onDelete = useCallback(() => {
// eslint-disable-next-line no-console
console.log('delete', tags);
onDeleteSuccess();
}, [ tags, onDeleteSuccess ]);
const [ reason, setReason ] = useState<string>('');
const onFieldChange = useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
setReason(event.currentTarget.value);
}, []);
const renderContent = useCallback(() => {
let text;
if (tags.length === 1) {
text = (
<>
<Text display="flex">Public tag</Text>
<Text fontWeight="600" whiteSpace="pre">{ ` "${ tags[0].name }" ` }</Text>
<Text>will be removed.</Text>
</>
)
}
if (tags.length > 1) {
const tagsText: Array<JSX.Element | string> = [];
tags.forEach((tag, index) => {
if (index < tags.length - 2) {
tagsText.push(<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag.name }"` }</Text>);
tagsText.push(',');
}
if (index === tags.length - 2) {
tagsText.push(<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag.name }" ` }</Text>);
tagsText.push('and');
}
if (index === tags.length - 1) {
tagsText.push(<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag.name }" ` }</Text>);
}
})
text = (
<>
<Text>Public tags</Text>{ tagsText }<Text>will be removed.</Text>
</>
)
}
return (
<>
<Flex marginBottom={ 12 }>
{ text }
</Flex>
<FormControl variant="floating" id="tag-delete">
<Textarea
placeholder=" "
size="lg"
value={ reason }
onChange={ onFieldChange }
/>
<FormLabel>Why do you want to remove tags?</FormLabel>
</FormControl>
</>
)
}, [ tags, reason, onFieldChange ]);
return (
<DeleteModal
isOpen={ isOpen }
onClose={ onClose }
onDelete={ onDelete }
title="Request to remove a public tag"
renderContent={ renderContent }
/>
)
}
export default DeletePublicTagModal;
import React from 'react';
import {
Table,
Thead,
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react'
import type { TPublicTagItem, TPublicTags } from 'data/publicTags';
import PublicTagTableItem from './PublicTagTableItem';
interface Props {
data: TPublicTags;
onEditClick: (data: TPublicTagItem) => void;
onDeleteClick: (data: TPublicTagItem) => void;
}
const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => {
return (
<TableContainer width="100%">
<Table variant="simple" minWidth="600px">
<Thead>
<Tr>
<Th width="60%">Smart contract / Address (0x...)</Th>
<Th width="40%">Public tag</Th>
<Th width="200px">Submission date</Th>
<Th width="108px"></Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item: TPublicTagItem) => (
<PublicTagTableItem
item={ item }
key={ item.id }
onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick }
/>
)) }
</Tbody>
</Table>
</TableContainer>
);
};
export default PublicTagTable;
import React, { useCallback } from 'react';
import {
Box,
Tag,
Text,
Tr,
Td,
HStack,
VStack,
useColorModeValue,
} from '@chakra-ui/react'
import AddressIcon from 'ui/shared/AddressIcon';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import type { TPublicTagItem, TPublicTagAddress, TPublicTag } from 'data/publicTags';
import EditButton from 'ui/shared/EditButton';
import DeleteButton from 'ui/shared/DeleteButton';
interface Props {
item: TPublicTagItem;
onEditClick: (data: TPublicTagItem) => void;
onDeleteClick: (data: TPublicTagItem) => void;
}
const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
const onItemEditClick = useCallback(() => {
return onEditClick(item);
}, [ item, onEditClick ]);
const onItemDeleteClick = useCallback(() => {
return onDeleteClick(item);
}, [ item, onDeleteClick ]);
const secondaryColor = useColorModeValue('gray.500', 'gray.400');
return (
<Tr alignItems="top" key={ item.id }>
<Td>
<VStack spacing={ 4 } alignItems="unset">
{ item.addresses.map((adr: TPublicTagAddress) => {
return (
<HStack spacing={ 4 } key={ adr.address } overflow="hidden" alignItems="start">
<AddressIcon address={ adr.address }/>
<Box overflow="hidden">
<AddressLinkWithTooltip address={ adr.address }/>
{ adr.addressName && <Text fontSize="sm" color={ secondaryColor } mt={ 0.5 }>{ adr.addressName }</Text> }
</Box>
</HStack>
)
}) }
</VStack>
</Td>
<Td>
<VStack spacing={ 2 } alignItems="baseline">
{ item.tags.map((tag: TPublicTag) => {
return (
<TruncatedTextTooltip label={ tag.name } key={ tag.name }>
<Tag color={ tag.colorHex || 'gray.600' } background={ tag.backgroundHex || 'gray.200' } lineHeight="24px">
{ tag.name }
</Tag>
</TruncatedTextTooltip>
)
}) }
</VStack>
</Td>
<Td>
<Text fontSize="sm" color={ secondaryColor }>{ item.date }</Text>
</Td>
<Td>
<HStack spacing={ 6 }>
<EditButton onClick={ onItemEditClick }/>
<DeleteButton onClick={ onItemDeleteClick }/>
</HStack>
</Td>
</Tr>
)
};
export default PublicTagTableItem;
import { Box, Text, Button, useDisclosure } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react';
import type { TPublicTagItem, TPublicTag } from 'data/publicTags';
import { publicTags } from 'data/publicTags';
import PublicTagTable from './PublicTagTable/PublicTagTable';
import DeletePublicTagModal from './DeletePublicTagModal'
type Props = {
changeToFormScreen: (data?: any) => void;
onTagDelete: () => void;
}
const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
const deleteModalProps = useDisclosure();
const [ deleteModalData, setDeleteModalData ] = useState<Array<TPublicTag>>([]);
const onDeleteModalClose = useCallback(() => {
setDeleteModalData([]);
deleteModalProps.onClose();
}, [ deleteModalProps ]);
const changeToForm = useCallback(() => {
changeToFormScreen();
}, [ changeToFormScreen ]);
const onItemEditClick = useCallback((item: TPublicTagItem) => {
changeToFormScreen(item);
}, [ changeToFormScreen ])
const onItemDeleteClick = useCallback((item: TPublicTagItem) => {
setDeleteModalData(item.tags);
deleteModalProps.onOpen();
}, [ deleteModalProps ]);
return (
<>
<Text marginBottom={ 12 }>
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.
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.
</Text>
<PublicTagTable data={ publicTags } onEditClick={ onItemEditClick } onDeleteClick={ onItemDeleteClick }/>
<Box marginTop={ 8 }>
<Button
variant="primary"
size="lg"
onClick={ changeToForm }
>
Request to add public tag
</Button>
</Box>
<DeletePublicTagModal
{ ...deleteModalProps }
onClose={ onDeleteModalClose }
tags={ deleteModalData }
onDeleteSuccess={ onTagDelete }
/>
</>
)
}
export default PublicTagsData;
import React, { useCallback } from 'react';
import type { ControllerRenderProps, Control } from 'react-hook-form';
import { RadioGroup, Radio, Stack } from '@chakra-ui/react';
import { Controller } from 'react-hook-form';
import type { Inputs } from './PublicTagsForm';
interface Props {
control: Control<Inputs, object>;
canReport: boolean;
}
export default function PublicTagFormAction({ control, canReport }: Props) {
const renderRadioGroup = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'action'>}) => {
return (
<RadioGroup defaultValue="add" value={ field.value } colorScheme="blue">
<Stack spacing={ 5 }>
<Radio value="add">
I want to add tags for my project
</Radio>
<Radio value="report" isDisabled={ canReport }>
I want to report an incorrect public tag
</Radio>
</Stack>
</RadioGroup>
)
}, [ canReport ])
return (
<Controller
name="action"
control={ control }
render={ renderRadioGroup }
/>
)
}
import React, { useCallback } from 'react';
import type { ControllerRenderProps, Control } from 'react-hook-form';
import { IconButton, Icon } from '@chakra-ui/react';
import { Controller } from 'react-hook-form';
import type { Inputs } from './PublicTagsForm';
import AddressInput from 'ui/shared/AddressInput';
import PlusIcon from 'icons/plus.svg';
import MinusIcon from 'icons/minus.svg';
interface Props {
control: Control<Inputs, object>;
index: number;
fieldsLength: number;
hasError: boolean;
onAddFieldClick: (e: React.SyntheticEvent) => void;
onRemoveFieldClick: (index: number) => (e: React.SyntheticEvent) => void;
}
const MAX_INPUTS_NUM = 10;
export default function PublicTagFormAction({ control, index, fieldsLength, hasError, onAddFieldClick, onRemoveFieldClick }: Props) {
const renderAddressInput = useCallback(({ field }: {field: ControllerRenderProps<Inputs, `addresses.${ number }.address`>}) => {
return <AddressInput field={ field } isInvalid={ hasError } size="lg" placeholder="Smart contract / Address (0x...)"/>
}, [ hasError ]);
return (
<>
<Controller
name={ `addresses.${ index }.address` }
control={ control }
render={ renderAddressInput }
/>
{ index === fieldsLength - 1 && fieldsLength < MAX_INPUTS_NUM && (
<IconButton
aria-label="add"
variant="iconBorderBlue"
w="30px"
h="30px"
onClick={ onAddFieldClick }
icon={ <Icon as={ PlusIcon } w="20px" h="20px"/> }
position="absolute"
right={ index === 0 ? '-50px' : '-100px' }
top="25px"
/>
) }
{ fieldsLength > 1 && (
<IconButton
aria-label="add"
variant="iconBorderBlue"
w="30px"
h="30px"
onClick={ onRemoveFieldClick(index) }
icon={ <Icon as={ MinusIcon } w="20px" h="20px"/> }
position="absolute"
right="-50px"
top="25px"
/>
) }</>
)
}
import React, { useCallback } from 'react';
import type { ControllerRenderProps, Control } from 'react-hook-form';
import { FormControl, FormLabel, Textarea } from '@chakra-ui/react';
import { Controller } from 'react-hook-form';
import type { Inputs } from './PublicTagsForm';
interface Props {
control: Control<Inputs, object>;
}
export default function PublicTagFormComment({ control }: Props) {
const renderComment = useCallback(({ field }: {field: ControllerRenderProps<Inputs, 'comment'>}) => {
return (
<FormControl variant="floating" id={ field.name }>
<Textarea
{ ...field }
placeholder=" "
size="lg"
/>
<FormLabel>Specify the reason for adding tags and color preference(s).</FormLabel>
</FormControl>
)
}, [])
return (
<Controller
name="comment"
control={ control }
render={ renderComment }
/>
)
}
import {
Button,
Box,
Grid,
GridItem,
Text,
HStack,
} from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { TPublicTagItem, TPublicTag, TPublicTagAddress } from 'data/publicTags';
import type { Path } from 'react-hook-form';
import { useForm, useFieldArray } from 'react-hook-form';
import PublicTagFormAction from './PublicTagFormAction';
import PublicTagFormComment from './PublicTagFormComment';
import PublicTagsFormInput from './PublicTagsFormInput';
import PublicTagFormAddressInput from './PublicTagFormAddressInput';
type Props = {
changeToDataScreen: (success?: boolean) => void;
data?: TPublicTagItem;
}
export type Inputs = {
userName: string;
userEmail: string;
companyName: string;
companyUrl: string;
action: 'add' | 'report';
tag: string;
addresses: Array<{
name: string;
address: string;
}>;
comment: string;
}
const placeholders = {
userName: 'Your name',
userEmail: 'Email',
companyName: 'Company name',
companyUrl: 'Company website',
tag: 'Public tag (max 35 characters)',
comment: 'Specify the reason for adding tags and color preference(s).',
} as Record<Path<Inputs>, string>;
const ADDRESS_INPUT_BUTTONS_WIDTH = 170;
const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const { control, handleSubmit, formState: { errors } } = useForm<Inputs>({
defaultValues: {
userName: data?.userName,
userEmail: data?.userEmail,
companyName: data?.companyName,
companyUrl: data?.companyUrl,
tag: data?.tags.map((tag: TPublicTag) => tag.name).join('; '),
addresses: data?.addresses.map((adr: TPublicTagAddress, index: number) => ({ name: `address.${ index }.address`, address: adr.address })) ||
[ { name: 'address.0.address', address: '' } ],
comment: data?.comment,
},
});
const { fields, append, remove } = useFieldArray({
name: 'addresses',
control,
});
const onAddFieldClick = useCallback(() => append({ address: '' }), [ append ]);
const onRemoveFieldClick = useCallback((index: number) => () => remove(index), [ remove ]);
const changeToData = useCallback(() => {
changeToDataScreen(true);
}, [ changeToDataScreen ]);
return (
<Box width={ `calc(100% - ${ ADDRESS_INPUT_BUTTONS_WIDTH }px)` } maxWidth="844px">
<Text size="sm" color="gray.500" paddingBottom={ 5 }>Company info</Text>
<Grid templateColumns="1fr 1fr" rowGap={ 4 } columnGap={ 5 }>
<GridItem>
<PublicTagsFormInput<Inputs> fieldName="userName" control={ control } label={ placeholders.userName } required/>
</GridItem>
<GridItem>
<PublicTagsFormInput<Inputs> fieldName="companyName" control={ control } label={ placeholders.companyName }/>
</GridItem>
<GridItem>
<PublicTagsFormInput<Inputs> fieldName="userEmail" control={ control } label={ placeholders.userEmail } required/>
</GridItem>
<GridItem>
<PublicTagsFormInput<Inputs> fieldName="companyUrl" control={ control } label={ placeholders.companyUrl }/>
</GridItem>
</Grid>
<Box marginTop={ 4 } marginBottom={ 8 }>
<PublicTagFormAction canReport={ Boolean(data) } control={ control }/>
</Box>
<Text size="sm" color="gray.500" marginBottom={ 5 }>Public tags (2 tags maximum, please use &quot;;&quot; as a divider)</Text>
<Box marginBottom={ 4 }>
<PublicTagsFormInput<Inputs> fieldName="tag" control={ control } label={ placeholders.tag } required/>
</Box>
{ fields.map((field, index) => {
return (
<Box position="relative" key={ field.id } marginBottom={ 4 }>
<PublicTagFormAddressInput
control={ control }
hasError={ Boolean(errors.addresses) }
index={ index }
fieldsLength={ fields.length }
onAddFieldClick={ onAddFieldClick }
onRemoveFieldClick={ onRemoveFieldClick }
/>
</Box>
)
}) }
<Box marginBottom={ 8 }>
<PublicTagFormComment control={ control }/>
</Box>
<HStack spacing={ 6 }>
<Button
size="lg"
variant="primary"
onClick={ handleSubmit(changeToData) }
disabled={ Object.keys(errors).length > 0 }
>
Send request
</Button>
<Button
size="lg"
variant="secondary"
onClick={ changeToData }
>
Cancel
</Button>
</HStack>
</Box>
)
}
export default PublicTagsForm;
import React, { useCallback } from 'react';
import type { ControllerRenderProps, FieldValues, Path, Control } from 'react-hook-form';
import { FormControl, FormLabel, Input } from '@chakra-ui/react';
import { Controller } from 'react-hook-form';
interface Props<TInputs extends FieldValues> {
fieldName: Path<TInputs>;
label: string;
required?: boolean;
control: Control<TInputs, object>;
}
export default function PublicTagsFormInput<I extends FieldValues>({ label, control, required, fieldName }: Props<I>) {
const renderInput = useCallback(({ field }: {field: ControllerRenderProps<I, typeof fieldName>}) => {
return (
<FormControl variant="floating" id={ field.name } isRequired={ required }>
<Input
{ ...field }
placeholder=" "
size="lg"
required={ required }
/>
<FormLabel>{ label }</FormLabel>
</FormControl>
)
}, [ label, required ]);
return (
<Controller
name={ fieldName }
control={ control }
render={ renderInput }
/>
)
}
import React from 'react';
import { Heading } from '@chakra-ui/react';
const PageHeader = ({ header }: {header: string}) => {
return (
<Heading as="h1" size="lg" marginBottom={ 8 }>{ header }</Heading>
)
}
export default PageHeader;
...@@ -12,9 +12,11 @@ const ADDRESS_LENGTH = 42; ...@@ -12,9 +12,11 @@ const ADDRESS_LENGTH = 42;
type Props = { type Props = {
field: ControllerRenderProps<any, 'address'>; field: ControllerRenderProps<any, 'address'>;
isInvalid: boolean; isInvalid: boolean;
size?: string;
placeholder?: string;
} }
const AddressInput: React.FC<Props> = ({ field, isInvalid }) => { const AddressInput: React.FC<Props> = ({ field, isInvalid, size, placeholder = 'Address (0x...)' }) => {
return ( return (
<FormControl variant="floating" id="address" isRequired> <FormControl variant="floating" id="address" isRequired>
<Input <Input
...@@ -22,14 +24,9 @@ const AddressInput: React.FC<Props> = ({ field, isInvalid }) => { ...@@ -22,14 +24,9 @@ const AddressInput: React.FC<Props> = ({ field, isInvalid }) => {
placeholder=" " placeholder=" "
isInvalid={ isInvalid } isInvalid={ isInvalid }
maxLength={ ADDRESS_LENGTH } maxLength={ ADDRESS_LENGTH }
// TODO: move this to input theme size={ size }
css={{
':-webkit-autofill': { transition: 'background-color 5000s ease-in-out 0s' },
':-webkit-autofill:hover': { transition: 'background-color 5000s ease-in-out 0s' },
':-webkit-autofill:focus': { transition: 'background-color 5000s ease-in-out 0s' },
}}
/> />
<FormLabel>Address (0x...)</FormLabel> <FormLabel>{ placeholder }</FormLabel>
</FormControl> </FormControl>
) )
} }
......
...@@ -16,10 +16,10 @@ type Props = { ...@@ -16,10 +16,10 @@ type Props = {
onClose: () => void; onClose: () => void;
onDelete: () => void; onDelete: () => void;
title: string; title: string;
renderText: () => JSX.Element; renderContent: () => JSX.Element;
} }
const DeleteModal: React.FC<Props> = ({ isOpen, onClose, onDelete, title, renderText }) => { const DeleteModal: React.FC<Props> = ({ isOpen, onClose, onDelete, title, renderContent }) => {
const onDeleteClick = useCallback(() => { const onDeleteClick = useCallback(() => {
onDelete(); onDelete();
...@@ -33,7 +33,7 @@ const DeleteModal: React.FC<Props> = ({ isOpen, onClose, onDelete, title, render ...@@ -33,7 +33,7 @@ const DeleteModal: React.FC<Props> = ({ isOpen, onClose, onDelete, title, render
<ModalHeader fontWeight="500" textStyle="h3">{ title }</ModalHeader> <ModalHeader fontWeight="500" textStyle="h3">{ title }</ModalHeader>
<ModalCloseButton/> <ModalCloseButton/>
<ModalBody> <ModalBody>
{ renderText() } { renderContent() }
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button variant="primary" size="lg" onClick={ onDeleteClick }> <Button variant="primary" size="lg" onClick={ onDeleteClick }>
......
...@@ -26,7 +26,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, address }) => { ...@@ -26,7 +26,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, address }) => {
onClose={ onClose } onClose={ onClose }
onDelete={ onDelete } onDelete={ onDelete }
title="Remove address from watch list" title="Remove address from watch list"
renderText={ renderText } renderContent={ renderText }
/> />
) )
} }
......
...@@ -919,6 +919,13 @@ ...@@ -919,6 +919,13 @@
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react-scroll@^1.8.4":
version "1.8.4"
resolved "https://registry.yarnpkg.com/@types/react-scroll/-/react-scroll-1.8.4.tgz#2b6258fb34104d3fcc7a22e8eceaadc669ba3ad1"
integrity sha512-DpHA9PYw42/rBrfKbGE/kAEvHRfyDL/ACfKB/ORWUYuCLi/yGrntxSzYXmg/7TLgQsJ5ma13GCDOzFSOz+8XOA==
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@18.0.9": "@types/react@*", "@types/react@18.0.9":
version "18.0.9" version "18.0.9"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.9.tgz#d6712a38bd6cd83469603e7359511126f122e878"
...@@ -2413,6 +2420,11 @@ lodash.mergewith@4.6.2: ...@@ -2413,6 +2420,11 @@ lodash.mergewith@4.6.2:
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55"
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
lodash.throttle@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==
lodash@^4.0.0: lodash@^4.0.0:
version "4.17.21" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
...@@ -2756,7 +2768,7 @@ prelude-ls@^1.2.1: ...@@ -2756,7 +2768,7 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prop-types@^15.6.2, prop-types@^15.8.1: prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
...@@ -2853,6 +2865,14 @@ react-remove-scroll@^2.5.4: ...@@ -2853,6 +2865,14 @@ react-remove-scroll@^2.5.4:
use-callback-ref "^1.3.0" use-callback-ref "^1.3.0"
use-sidecar "^1.1.2" use-sidecar "^1.1.2"
react-scroll@^1.8.7:
version "1.8.7"
resolved "https://registry.yarnpkg.com/react-scroll/-/react-scroll-1.8.7.tgz#8020035329efad00f03964e18aff6822137de3aa"
integrity sha512-fBOIwweAlhicx8RqP9tQXn/Uhd+DTtVRjw+0VBsIn1Z+MjRYLhTZ0tMoTAU1vOD3dce8mI6copexI4yWII+Luw==
dependencies:
lodash.throttle "^4.1.1"
prop-types "^15.7.2"
react-style-singleton@^2.2.1: react-style-singleton@^2.2.1:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
......
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