Commit a651daec authored by tom's avatar tom

display table of public tags

parent b96198c8
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/account/v1/user/public_tags/${ req.query.id }`;
};
const publicTagsHandler = handler(getUrl, [ 'DELETE', 'PUT' ]);
export default publicTagsHandler;
import type { PublicTags } from 'types/api/account';
import handler from 'lib/api/handler';
const publicKeysHandler = handler<PublicTags>(() => '/account/v1/user/public_tags', [ 'GET', 'POST' ]);
export default publicKeysHandler;
...@@ -71,3 +71,21 @@ export interface WatchlistAddressNew { ...@@ -71,3 +71,21 @@ export interface WatchlistAddressNew {
} }
export type WatchlistAddresses = Array<WatchlistAddress> export type WatchlistAddresses = Array<WatchlistAddress>
export interface PublicTag {
website: string;
tags: string; // tag_1;tag_2;tag_3 etc.
is_owner: boolean;
id: number;
full_name: string;
email: string;
company: string;
addresses: string; // address_1;<address_2;address_3 etc.
additional_comment: string;
}
export type PublicTagNew = Omit<PublicTag, 'addresses' | 'id'> & {
addresses_array: Array<string>;
}
export type PublicTags = Array<PublicTag>;
...@@ -54,7 +54,7 @@ const ApiKeysPage: React.FC = () => { ...@@ -54,7 +54,7 @@ const ApiKeysPage: React.FC = () => {
return ( return (
<> <>
<SkeletonTable columns={ [ '100%', '108px' ] }/> <SkeletonTable columns={ [ '100%', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/> <Skeleton height="48px" width="156px" marginTop={ 8 }/>
</> </>
); );
} }
......
...@@ -5,7 +5,8 @@ import { ...@@ -5,7 +5,8 @@ import {
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { animateScroll } from 'react-scroll'; import { animateScroll } from 'react-scroll';
import type { TPublicTagItem } from 'data/publicTags'; import type { PublicTag } from 'types/api/account';
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';
import AccountPageHeader from 'ui/shared/AccountPageHeader'; import AccountPageHeader from 'ui/shared/AccountPageHeader';
...@@ -20,9 +21,9 @@ const toastDescriptions = { ...@@ -20,9 +21,9 @@ const toastDescriptions = {
removed: 'Tags have been removed.', removed: 'Tags have been removed.',
} as Record<TToastAction, string>; } as Record<TToastAction, string>;
const PublicTags: React.FC = () => { const PublicTagsComponent: React.FC = () => {
const [ screen, setScreen ] = useState<TScreen>('data'); const [ screen, setScreen ] = useState<TScreen>('data');
const [ formData, setFormData ] = useState<TPublicTagItem>(); const [ formData, setFormData ] = useState<PublicTag>();
const toast = useToast(); const toast = useToast();
...@@ -39,7 +40,7 @@ const PublicTags: React.FC = () => { ...@@ -39,7 +40,7 @@ const PublicTags: React.FC = () => {
}); });
}, [ toast ]); }, [ toast ]);
const changeToFormScreen = useCallback((data?: TPublicTagItem) => { const changeToFormScreen = useCallback((data?: PublicTag) => {
setFormData(data); setFormData(data);
setScreen('form'); setScreen('form');
animateScroll.scrollToTop({ animateScroll.scrollToTop({
...@@ -82,4 +83,4 @@ const PublicTags: React.FC = () => { ...@@ -82,4 +83,4 @@ const PublicTags: React.FC = () => {
); );
}; };
export default PublicTags; export default PublicTagsComponent;
...@@ -2,22 +2,26 @@ import { Flex, Text, FormControl, FormLabel, Textarea } from '@chakra-ui/react'; ...@@ -2,22 +2,26 @@ import { Flex, Text, FormControl, FormLabel, Textarea } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import type { TPublicTag } from 'data/publicTags'; import type { PublicTag } from 'types/api/account';
import DeleteModal from 'ui/shared/DeleteModal'; import DeleteModal from 'ui/shared/DeleteModal';
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
tags: Array<TPublicTag>; data: PublicTag;
onDeleteSuccess: () => void; onDeleteSuccess: () => void;
} }
const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onDeleteSuccess }) => { const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDeleteSuccess }) => {
const tags = data.tags.split(';');
const onDelete = useCallback(() => { const onDelete = useCallback(() => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('delete', tags); console.log('delete', data);
onDeleteSuccess(); onDeleteSuccess();
}, [ tags, onDeleteSuccess ]); }, [ data, onDeleteSuccess ]);
const [ reason, setReason ] = useState<string>(''); const [ reason, setReason ] = useState<string>('');
...@@ -31,7 +35,7 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onD ...@@ -31,7 +35,7 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onD
text = ( text = (
<> <>
<Text display="flex">Public tag</Text> <Text display="flex">Public tag</Text>
<Text fontWeight="600" whiteSpace="pre">{ ` "${ tags[0].name }" ` }</Text> <Text fontWeight="600" whiteSpace="pre">{ ` "${ tags[0] }" ` }</Text>
<Text>will be removed.</Text> <Text>will be removed.</Text>
</> </>
); );
...@@ -40,15 +44,15 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onD ...@@ -40,15 +44,15 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onD
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.name }"` }</Text>); tagsText.push(<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag }"` }</Text>);
tagsText.push(','); tagsText.push(',');
} }
if (index === tags.length - 2) { if (index === tags.length - 2) {
tagsText.push(<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag.name }" ` }</Text>); tagsText.push(<Text fontWeight="600" whiteSpace="pre">{ ` "${ 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.name }" ` }</Text>); tagsText.push(<Text fontWeight="600" whiteSpace="pre">{ ` "${ tag }" ` }</Text>);
} }
}); });
text = ( text = (
...@@ -85,4 +89,4 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onD ...@@ -85,4 +89,4 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, tags = [], onD
); );
}; };
export default DeletePublicTagModal; export default React.memo(DeletePublicTagModal);
...@@ -8,14 +8,14 @@ import { ...@@ -8,14 +8,14 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TPublicTagItem, TPublicTags } from 'data/publicTags'; import type { PublicTags, PublicTag } from 'types/api/account';
import PublicTagTableItem from './PublicTagTableItem'; import PublicTagTableItem from './PublicTagTableItem';
interface Props { interface Props {
data: TPublicTags; data: PublicTags;
onEditClick: (data: TPublicTagItem) => void; onEditClick: (data: PublicTag) => void;
onDeleteClick: (data: TPublicTagItem) => void; onDeleteClick: (data: PublicTag) => void;
} }
const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => { const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => {
...@@ -26,12 +26,12 @@ const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => { ...@@ -26,12 +26,12 @@ const PublicTagTable = ({ data, onEditClick, onDeleteClick }: Props) => {
<Tr> <Tr>
<Th width="60%">Smart contract / Address (0x...)</Th> <Th width="60%">Smart contract / Address (0x...)</Th>
<Th width="40%">Public tag</Th> <Th width="40%">Public tag</Th>
<Th width="200px">Submission date</Th> <Th width="200px">Request status</Th>
<Th width="108px"></Th> <Th width="108px"></Th>
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item: TPublicTagItem) => ( { data.map((item) => (
<PublicTagTableItem <PublicTagTableItem
item={ item } item={ item }
key={ item.id } key={ item.id }
......
import { import {
Box, Box,
Tag, Tag,
Text,
Tr, Tr,
Td, Td,
HStack, HStack,
VStack, VStack,
useColorModeValue, Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { TPublicTagItem, TPublicTagAddress, TPublicTag } from 'data/publicTags'; import type { PublicTag } from 'types/api/account';
import AddressIcon from 'ui/shared/AddressIcon'; import AddressIcon from 'ui/shared/AddressIcon';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip';
import DeleteButton from 'ui/shared/DeleteButton'; import DeleteButton from 'ui/shared/DeleteButton';
...@@ -18,9 +18,9 @@ import EditButton from 'ui/shared/EditButton'; ...@@ -18,9 +18,9 @@ import EditButton from 'ui/shared/EditButton';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
interface Props { interface Props {
item: TPublicTagItem; item: PublicTag;
onEditClick: (data: TPublicTagItem) => void; onEditClick: (data: PublicTag) => void;
onDeleteClick: (data: TPublicTagItem) => void; onDeleteClick: (data: PublicTag) => void;
} }
const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
...@@ -32,19 +32,18 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -32,19 +32,18 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
return onDeleteClick(item); return onDeleteClick(item);
}, [ item, onDeleteClick ]); }, [ item, onDeleteClick ]);
const secondaryColor = useColorModeValue('gray.500', 'gray.400');
return ( return (
<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((adr: TPublicTagAddress) => { { item.addresses.split(';').map((address) => {
return ( return (
<HStack spacing={ 4 } key={ adr.address } overflow="hidden" alignItems="start"> <HStack spacing={ 4 } key={ address } overflow="hidden" alignItems="start">
<AddressIcon address={ adr.address }/> <AddressIcon address={ address }/>
<Box overflow="hidden"> <Box overflow="hidden">
<AddressLinkWithTooltip address={ adr.address }/> <AddressLinkWithTooltip address={ address }/>
{ adr.addressName && <Text fontSize="sm" color={ secondaryColor } mt={ 0.5 }>{ adr.addressName }</Text> } { /* todo_tom add address name */ }
<Text fontSize="sm" variant="secondary" mt={ 0.5 }>Address Name</Text>
</Box> </Box>
</HStack> </HStack>
); );
...@@ -53,19 +52,23 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -53,19 +52,23 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
</Td> </Td>
<Td> <Td>
<VStack spacing={ 2 } alignItems="baseline"> <VStack spacing={ 2 } alignItems="baseline">
{ item.tags.map((tag: TPublicTag) => { { item.tags.split(';').map((tag) => {
return ( return (
<TruncatedTextTooltip label={ tag.name } key={ tag.name }> <TruncatedTextTooltip label={ tag } key={ tag }>
<Tag color={ tag.colorHex || 'gray.600' } background={ tag.backgroundHex || 'gray.200' } lineHeight="24px"> <Tag variant="gray" lineHeight="24px">
{ tag.name } { tag }
</Tag> </Tag>
</TruncatedTextTooltip> </TruncatedTextTooltip>
); );
}) } }) }
</VStack> </VStack>
</Td> </Td>
{ /* todo_tom update tag date and status */ }
<Td> <Td>
<Text fontSize="sm" color={ secondaryColor }>{ item.date }</Text> <VStack alignItems="flex-start">
<Text fontSize="sm" color="green.500" fontWeight="500">Approved</Text>
<Text fontSize="sm" variant="secondary">Jun 10, 2022</Text>
</VStack>
</Td> </Td>
<Td> <Td>
<HStack spacing={ 6 }> <HStack spacing={ 6 }>
...@@ -77,4 +80,4 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -77,4 +80,4 @@ const PublicTagTableItem = ({ item, onEditClick, onDeleteClick }: Props) => {
); );
}; };
export default PublicTagTableItem; export default React.memo(PublicTagTableItem);
import { Box, Text, Button, useDisclosure } from '@chakra-ui/react'; import { Box, Text, Button, Skeleton, useDisclosure } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import type { TPublicTagItem, TPublicTag } from 'data/publicTags'; import type { PublicTags, PublicTag } from 'types/api/account';
import { publicTags } from 'data/publicTags';
import SkeletonTable from 'ui/shared/SkeletonTable';
import DeletePublicTagModal from './DeletePublicTagModal'; import DeletePublicTagModal from './DeletePublicTagModal';
import PublicTagTable from './PublicTagTable/PublicTagTable'; import PublicTagTable from './PublicTagTable/PublicTagTable';
type Props = { type Props = {
changeToFormScreen: (data?: TPublicTagItem) => void; changeToFormScreen: (data?: PublicTag) => void;
onTagDelete: () => void; onTagDelete: () => void;
} }
const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
const [ deleteModalData, setDeleteModalData ] = useState<Array<TPublicTag>>([]); const [ deleteModalData, setDeleteModalData ] = useState<PublicTag>();
const { data, isLoading, isError } = useQuery<unknown, unknown, PublicTags>([ 'public-tags' ], async() => {
const response = await fetch('/api/account/public-tags');
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
});
const onDeleteModalClose = useCallback(() => { const onDeleteModalClose = useCallback(() => {
setDeleteModalData([]); setDeleteModalData(undefined);
deleteModalProps.onClose(); deleteModalProps.onClose();
}, [ deleteModalProps ]); }, [ deleteModalProps ]);
...@@ -25,15 +35,41 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { ...@@ -25,15 +35,41 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
changeToFormScreen(); changeToFormScreen();
}, [ changeToFormScreen ]); }, [ changeToFormScreen ]);
const onItemEditClick = useCallback((item: TPublicTagItem) => { const onItemEditClick = useCallback((item: PublicTag) => {
changeToFormScreen(item); changeToFormScreen(item);
}, [ changeToFormScreen ]); }, [ changeToFormScreen ]);
const onItemDeleteClick = useCallback((item: TPublicTagItem) => { const onItemDeleteClick = useCallback((item: PublicTag) => {
setDeleteModalData(item.tags); setDeleteModalData(item);
deleteModalProps.onOpen(); deleteModalProps.onOpen();
}, [ deleteModalProps ]); }, [ deleteModalProps ]);
const content = (() => {
if (isLoading || isError) {
return (
<>
<SkeletonTable columns={ [ '60%', '40%', '200px', '108px' ] }/>
<Skeleton height="48px" width="270px" marginTop={ 8 }/>
</>
);
}
return (
<>
{ data.length > 0 && <PublicTagTable data={ data } onEditClick={ onItemEditClick } onDeleteClick={ onItemDeleteClick }/> }
<Box marginTop={ 8 }>
<Button
variant="primary"
size="lg"
onClick={ changeToForm }
>
Request to add public tag
</Button>
</Box>
</>
);
})();
return ( return (
<> <>
<Text marginBottom={ 12 }> <Text marginBottom={ 12 }>
...@@ -42,22 +78,15 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { ...@@ -42,22 +78,15 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
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> </Text>
<PublicTagTable data={ publicTags } onEditClick={ onItemEditClick } onDeleteClick={ onItemDeleteClick }/> { content }
<Box marginTop={ 8 }> { deleteModalData && (
<Button <DeletePublicTagModal
variant="primary" { ...deleteModalProps }
size="lg" onClose={ onDeleteModalClose }
onClick={ changeToForm } data={ deleteModalData }
> onDeleteSuccess={ onTagDelete }
Request to add public tag />
</Button> ) }
</Box>
<DeletePublicTagModal
{ ...deleteModalProps }
onClose={ onDeleteModalClose }
tags={ deleteModalData }
onDeleteSuccess={ onTagDelete }
/>
</> </>
); );
}; };
......
...@@ -10,7 +10,7 @@ import React, { useCallback } from 'react'; ...@@ -10,7 +10,7 @@ import React, { useCallback } from 'react';
import type { Path } from 'react-hook-form'; import type { Path } from 'react-hook-form';
import { useForm, useFieldArray } from 'react-hook-form'; import { useForm, useFieldArray } from 'react-hook-form';
import type { TPublicTagItem, TPublicTag, TPublicTagAddress } from 'data/publicTags'; import type { PublicTag } from 'types/api/account';
import PublicTagFormAction from './PublicTagFormAction'; import PublicTagFormAction from './PublicTagFormAction';
import PublicTagFormAddressInput from './PublicTagFormAddressInput'; import PublicTagFormAddressInput from './PublicTagFormAddressInput';
...@@ -19,7 +19,7 @@ import PublicTagsFormInput from './PublicTagsFormInput'; ...@@ -19,7 +19,7 @@ import PublicTagsFormInput from './PublicTagsFormInput';
type Props = { type Props = {
changeToDataScreen: (success?: boolean) => void; changeToDataScreen: (success?: boolean) => void;
data?: TPublicTagItem; data?: PublicTag;
} }
export type Inputs = { export type Inputs = {
...@@ -50,14 +50,14 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 170; ...@@ -50,14 +50,14 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 170;
const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const { control, handleSubmit, formState: { errors } } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors } } = useForm<Inputs>({
defaultValues: { defaultValues: {
userName: data?.userName, userName: data?.full_name,
userEmail: data?.userEmail, userEmail: data?.email,
companyName: data?.companyName, companyName: data?.company,
companyUrl: data?.companyUrl, companyUrl: data?.website,
tag: data?.tags.map((tag: TPublicTag) => tag.name).join('; '), tag: data?.tags.split(';').map((tag) => tag).join('; '),
addresses: data?.addresses.map((adr: TPublicTagAddress, index: number) => ({ name: `address.${ index }.address`, address: adr.address })) || addresses: data?.addresses.split(';').map((address, index: number) => ({ name: `address.${ index }.address`, address })) ||
[ { name: 'address.0.address', address: '' } ], [ { name: 'address.0.address', address: '' } ],
comment: data?.comment, comment: data?.additional_comment,
}, },
}); });
...@@ -135,4 +135,4 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { ...@@ -135,4 +135,4 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
); );
}; };
export default PublicTagsForm; export default React.memo(PublicTagsForm);
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