Commit c0984838 authored by tom's avatar tom

skeletons for verified addresses

parent b5039524
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle'; import getNetworkTitle from 'lib/networks/getNetworkTitle';
import VerifiedAddresses from 'ui/pages/VerifiedAddresses';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
const VerifiedAddresses = dynamic(() => import('ui/pages/VerifiedAddresses'), { ssr: false });
const VerifiedAddressesPage: NextPage = () => { const VerifiedAddressesPage: NextPage = () => {
const title = getNetworkTitle(); const title = getNetworkTitle();
return ( return (
......
import type { PublicTag, AddressTag, TransactionTag, ApiKey, CustomAbi } from 'types/api/account'; import type { PublicTag, AddressTag, TransactionTag, ApiKey, CustomAbi, VerifiedAddress, TokenInfoApplication } from 'types/api/account';
import type { TWatchlistItem } from 'types/client/account'; import type { TWatchlistItem } from 'types/client/account';
import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams'; import { ADDRESS_PARAMS, ADDRESS_HASH } from './addressParams';
...@@ -79,3 +79,28 @@ export const CUSTOM_ABI: CustomAbi = { ...@@ -79,3 +79,28 @@ export const CUSTOM_ABI: CustomAbi = {
id: '1', id: '1',
name: 'placeholder', name: 'placeholder',
}; };
export const VERIFIED_ADDRESS: VerifiedAddress = {
userId: 'john.doe@gmail.com',
chainId: '5',
contractAddress: ADDRESS_HASH,
verifiedDate: '2022-11-11',
metadata: {
tokenName: 'Placeholder Token',
tokenSymbol: 'PLC',
},
};
export const TOKEN_INFO_APPLICATION: TokenInfoApplication = {
id: '1',
tokenAddress: ADDRESS_HASH,
status: 'IN_PROCESS',
updatedAt: '2022-11-11 13:49:48.031453Z',
requesterName: 'John Doe',
requesterEmail: 'john.doe@gmail.com',
projectWebsite: 'http://example.com',
projectEmail: 'info@example.com',
iconUrl: 'https://example.com/100/100',
projectDescription: 'Hello!',
projectSector: 'DeFi',
};
import { OrderedList, ListItem, chakra, Button, useDisclosure, Show, Hide, Skeleton, Box, Link } from '@chakra-ui/react'; import { OrderedList, ListItem, chakra, Button, useDisclosure, Show, Hide, Skeleton, Link } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -9,12 +9,11 @@ import appConfig from 'configs/app/config'; ...@@ -9,12 +9,11 @@ import appConfig from 'configs/app/config';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { TOKEN_INFO_APPLICATION, VERIFIED_ADDRESS } from 'stubs/account';
import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal'; import AddressVerificationModal from 'ui/addressVerification/AddressVerificationModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataListDisplay from 'ui/shared/DataListDisplay'; import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import AdminSupportText from 'ui/shared/texts/AdminSupportText'; import AdminSupportText from 'ui/shared/texts/AdminSupportText';
import TokenInfoForm from 'ui/tokenInfo/TokenInfoForm'; import TokenInfoForm from 'ui/tokenInfo/TokenInfoForm';
import VerifiedAddressesListItem from 'ui/verifiedAddresses/VerifiedAddressesListItem'; import VerifiedAddressesListItem from 'ui/verifiedAddresses/VerifiedAddressesListItem';
...@@ -35,12 +34,18 @@ const VerifiedAddresses = () => { ...@@ -35,12 +34,18 @@ const VerifiedAddresses = () => {
}, [ ]); }, [ ]);
const modalProps = useDisclosure(); const modalProps = useDisclosure();
const queryClient = useQueryClient();
const addressesQuery = useApiQuery('verified_addresses', { const addressesQuery = useApiQuery('verified_addresses', {
pathParams: { chainId: appConfig.network.id }, pathParams: { chainId: appConfig.network.id },
queryOptions: {
placeholderData: { verifiedAddresses: Array(3).fill(VERIFIED_ADDRESS) },
},
}); });
const applicationsQuery = useApiQuery('token_info_applications', { const applicationsQuery = useApiQuery('token_info_applications', {
pathParams: { chainId: appConfig.network.id, id: undefined }, pathParams: { chainId: appConfig.network.id, id: undefined },
queryOptions: { queryOptions: {
placeholderData: { submissions: Array(3).fill(TOKEN_INFO_APPLICATION) },
select: (data) => { select: (data) => {
return { return {
...data, ...data,
...@@ -49,7 +54,8 @@ const VerifiedAddresses = () => { ...@@ -49,7 +54,8 @@ const VerifiedAddresses = () => {
}, },
}, },
}); });
const queryClient = useQueryClient();
const isLoading = addressesQuery.isPlaceholderData || applicationsQuery.isPlaceholderData;
const handleGoBack = React.useCallback(() => { const handleGoBack = React.useCallback(() => {
setSelectedAddress(undefined); setSelectedAddress(undefined);
...@@ -94,24 +100,11 @@ const VerifiedAddresses = () => { ...@@ -94,24 +100,11 @@ const VerifiedAddresses = () => {
}, [ queryClient ]); }, [ queryClient ]);
const addButton = ( const addButton = (
<Box marginTop={ 8 }> <Skeleton mt={ 8 } isLoaded={ !isLoading } display="inline-block">
<Button size="lg" onClick={ modalProps.onOpen }> <Button size="lg" onClick={ modalProps.onOpen }>
Add address Add address
</Button> </Button>
</Box> </Skeleton>
);
const skeleton = (
<>
<Box display={{ base: 'block', lg: 'none' }}>
<SkeletonListAccount/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/>
</Box>
<Box display={{ base: 'none', lg: 'block' }}>
<SkeletonTable columns={ [ '100%', '180px', '260px', '160px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/>
</Box>
</>
); );
const backLink = React.useMemo(() => { const backLink = React.useMemo(() => {
...@@ -144,13 +137,14 @@ const VerifiedAddresses = () => { ...@@ -144,13 +137,14 @@ const VerifiedAddresses = () => {
const content = addressesQuery.data?.verifiedAddresses ? ( const content = addressesQuery.data?.verifiedAddresses ? (
<> <>
<Show below="lg" key="content-mobile" ssr={ false }> <Show below="lg" key="content-mobile" ssr={ false }>
{ addressesQuery.data.verifiedAddresses.map((item) => ( { addressesQuery.data.verifiedAddresses.map((item, index) => (
<VerifiedAddressesListItem <VerifiedAddressesListItem
key={ item.contractAddress } key={ item.contractAddress + (isLoading ? index : '') }
item={ item } item={ item }
application={ applicationsQuery.data?.submissions?.find(({ tokenAddress }) => tokenAddress.toLowerCase() === item.contractAddress.toLowerCase()) } application={ applicationsQuery.data?.submissions?.find(({ tokenAddress }) => tokenAddress.toLowerCase() === item.contractAddress.toLowerCase()) }
onAdd={ handleItemAdd } onAdd={ handleItemAdd }
onEdit={ handleItemEdit } onEdit={ handleItemEdit }
isLoading={ isLoading }
/> />
)) } )) }
</Show> </Show>
...@@ -160,6 +154,7 @@ const VerifiedAddresses = () => { ...@@ -160,6 +154,7 @@ const VerifiedAddresses = () => {
applications={ applicationsQuery.data?.submissions } applications={ applicationsQuery.data?.submissions }
onItemEdit={ handleItemEdit } onItemEdit={ handleItemEdit }
onItemAdd={ handleItemAdd } onItemAdd={ handleItemAdd }
isLoading={ isLoading }
/> />
</Hide> </Hide>
</> </>
...@@ -192,12 +187,12 @@ const VerifiedAddresses = () => { ...@@ -192,12 +187,12 @@ const VerifiedAddresses = () => {
<AdminSupportText mt={ 5 }/> <AdminSupportText mt={ 5 }/>
</AccountPageDescription> </AccountPageDescription>
<DataListDisplay <DataListDisplay
isLoading={ addressesQuery.isLoading || applicationsQuery.isLoading } isLoading={ false }
isError={ addressesQuery.isError || applicationsQuery.isError } isError={ addressesQuery.isError || applicationsQuery.isError }
items={ addressesQuery.data?.verifiedAddresses } items={ addressesQuery.data?.verifiedAddresses }
content={ content } content={ content }
emptyText="" emptyText=""
skeletonProps={{ customSkeleton: skeleton }} skeletonProps={{ customSkeleton: null }}
/> />
{ addButton } { addButton }
<AddressVerificationModal <AddressVerificationModal
......
import { Icon, IconButton, Link, Tooltip } from '@chakra-ui/react'; import { IconButton, Link, Skeleton, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account'; import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account';
...@@ -6,6 +6,7 @@ import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account'; ...@@ -6,6 +6,7 @@ import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account';
import editIcon from 'icons/edit.svg'; import editIcon from 'icons/edit.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import AddressSnippet from 'ui/shared/AddressSnippet'; import AddressSnippet from 'ui/shared/AddressSnippet';
import Icon from 'ui/shared/chakra/Icon';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid'; import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import VerifiedAddressesStatus from './VerifiedAddressesStatus'; import VerifiedAddressesStatus from './VerifiedAddressesStatus';
...@@ -16,64 +17,89 @@ interface Props { ...@@ -16,64 +17,89 @@ interface Props {
application: TokenInfoApplication | undefined; application: TokenInfoApplication | undefined;
onAdd: (address: string) => void; onAdd: (address: string) => void;
onEdit: (address: string) => void; onEdit: (address: string) => void;
isLoading: boolean;
} }
const VerifiedAddressesListItem = ({ item, application, onAdd, onEdit }: Props) => { const VerifiedAddressesListItem = ({ item, application, onAdd, onEdit, isLoading }: Props) => {
const handleAddClick = React.useCallback(() => { const handleAddClick = React.useCallback(() => {
if (isLoading) {
return;
}
onAdd(item.contractAddress); onAdd(item.contractAddress);
}, [ item, onAdd ]); }, [ isLoading, item.contractAddress, onAdd ]);
const handleEditClick = React.useCallback(() => { const handleEditClick = React.useCallback(() => {
if (isLoading) {
return;
}
onEdit(item.contractAddress); onEdit(item.contractAddress);
}, [ item, onEdit ]); }, [ isLoading, item.contractAddress, onEdit ]);
const tokenInfo = (() => {
if (isLoading) {
return <Skeleton height={ 6 } width="140px"/>;
}
if (!item.metadata.tokenName) {
return <span>Not a token</span>;
}
if (!application) {
return <Link onClick={ handleAddClick }>Add details</Link>;
}
return (
<>
<VerifiedAddressesTokenSnippet application={ application } name={ item.metadata.tokenName }/>
<Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="simple"
boxSize={ 5 }
borderRadius="none"
flexShrink={ 0 }
onClick={ handleEditClick }
icon={ <Icon as={ editIcon }/> }
/>
</Tooltip>
</>
);
})();
return ( return (
<ListItemMobileGrid.Container> <ListItemMobileGrid.Container>
<ListItemMobileGrid.Label>Address</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Address</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px"> <ListItemMobileGrid.Value py="3px">
<AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}/> <AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }} isLoading={ isLoading }/>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
{ item.metadata.tokenName && ( { item.metadata.tokenName && (
<> <>
<ListItemMobileGrid.Label>Token Info</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Token Info</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py={ application ? '3px' : '5px' } display="flex" alignItems="center"> <ListItemMobileGrid.Value py={ application ? '3px' : '5px' } display="flex" alignItems="center">
{ application ? ( { tokenInfo }
<>
<VerifiedAddressesTokenSnippet application={ application } name={ item.metadata.tokenName }/>
<Tooltip label="Edit">
<IconButton
aria-label="edit"
variant="simple"
boxSize={ 5 }
borderRadius="none"
flexShrink={ 0 }
onClick={ handleEditClick }
icon={ <Icon as={ editIcon }/> }
/>
</Tooltip>
</>
) : (
<Link onClick={ handleAddClick }>Add details</Link>
) }
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
</> </>
) } ) }
{ item.metadata.tokenName && application && ( { item.metadata.tokenName && application && (
<> <>
<ListItemMobileGrid.Label>Status</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Status</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<VerifiedAddressesStatus status={ application.status }/> <Skeleton isLoaded={ !isLoading } display="inline-block">
<VerifiedAddressesStatus status={ application.status }/>
</Skeleton>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
</> </>
) } ) }
{ item.metadata.tokenName && application && ( { item.metadata.tokenName && application && (
<> <>
<ListItemMobileGrid.Label>Date</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Date</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
{ dayjs(application.updatedAt).format('MMM DD, YYYY') } <Skeleton isLoaded={ !isLoading } display="inline-block">
{ dayjs(application.updatedAt).format('MMM DD, YYYY') }
</Skeleton>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
</> </>
) } ) }
......
...@@ -10,9 +10,10 @@ interface Props { ...@@ -10,9 +10,10 @@ interface Props {
applications: Array<TokenInfoApplication> | undefined; applications: Array<TokenInfoApplication> | undefined;
onItemAdd: (address: string) => void; onItemAdd: (address: string) => void;
onItemEdit: (address: string) => void; onItemEdit: (address: string) => void;
isLoading: boolean;
} }
const VerifiedAddressesTable = ({ data, applications, onItemEdit, onItemAdd }: Props) => { const VerifiedAddressesTable = ({ data, applications, onItemEdit, onItemAdd, isLoading }: Props) => {
return ( return (
<Table variant="simple"> <Table variant="simple">
<Thead> <Thead>
...@@ -25,13 +26,14 @@ const VerifiedAddressesTable = ({ data, applications, onItemEdit, onItemAdd }: P ...@@ -25,13 +26,14 @@ const VerifiedAddressesTable = ({ data, applications, onItemEdit, onItemAdd }: P
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data.map((item, index) => (
<VerifiedAddressesTableItem <VerifiedAddressesTableItem
key={ item.contractAddress } key={ item.contractAddress + (isLoading ? index : '') }
item={ item } item={ item }
application={ applications?.find(({ tokenAddress }) => tokenAddress.toLowerCase() === item.contractAddress.toLowerCase()) } application={ applications?.find(({ tokenAddress }) => tokenAddress.toLowerCase() === item.contractAddress.toLowerCase()) }
onAdd={ onItemAdd } onAdd={ onItemAdd }
onEdit={ onItemEdit } onEdit={ onItemEdit }
isLoading={ isLoading }
/> />
)) } )) }
</Tbody> </Tbody>
......
import { Td, Tr, Link, Tooltip, IconButton, Icon } from '@chakra-ui/react'; import { Td, Tr, Link, Tooltip, IconButton, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account'; import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account';
...@@ -6,6 +6,7 @@ import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account'; ...@@ -6,6 +6,7 @@ import type { TokenInfoApplication, VerifiedAddress } from 'types/api/account';
import editIcon from 'icons/edit.svg'; import editIcon from 'icons/edit.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import AddressSnippet from 'ui/shared/AddressSnippet'; import AddressSnippet from 'ui/shared/AddressSnippet';
import Icon from 'ui/shared/chakra/Icon';
import VerifiedAddressesStatus from './VerifiedAddressesStatus'; import VerifiedAddressesStatus from './VerifiedAddressesStatus';
import VerifiedAddressesTokenSnippet from './VerifiedAddressesTokenSnippet'; import VerifiedAddressesTokenSnippet from './VerifiedAddressesTokenSnippet';
...@@ -15,19 +16,30 @@ interface Props { ...@@ -15,19 +16,30 @@ interface Props {
application: TokenInfoApplication | undefined; application: TokenInfoApplication | undefined;
onAdd: (address: string) => void; onAdd: (address: string) => void;
onEdit: (address: string) => void; onEdit: (address: string) => void;
isLoading: boolean;
} }
const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props) => { const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit, isLoading }: Props) => {
const handleAddClick = React.useCallback(() => { const handleAddClick = React.useCallback(() => {
if (isLoading) {
return;
}
onAdd(item.contractAddress); onAdd(item.contractAddress);
}, [ item, onAdd ]); }, [ isLoading, item.contractAddress, onAdd ]);
const handleEditClick = React.useCallback(() => { const handleEditClick = React.useCallback(() => {
if (isLoading) {
return;
}
onEdit(item.contractAddress); onEdit(item.contractAddress);
}, [ item, onEdit ]); }, [ isLoading, item.contractAddress, onEdit ]);
const tokenInfo = (() => { const tokenInfo = (() => {
if (isLoading) {
return <Skeleton height={ 6 } width="140px"/>;
}
if (!item.metadata.tokenName) { if (!item.metadata.tokenName) {
return <span>Not a token</span>; return <span>Not a token</span>;
} }
...@@ -42,14 +54,14 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props) ...@@ -42,14 +54,14 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props)
return ( return (
<Tr> <Tr>
<Td> <Td>
<AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }}/> <AddressSnippet address={{ hash: item.contractAddress, is_contract: true, implementation_name: null }} isLoading={ isLoading }/>
</Td> </Td>
<Td fontSize="sm" verticalAlign="middle" pr={ 1 }> <Td fontSize="sm" verticalAlign="middle" pr={ 1 }>
{ tokenInfo } { tokenInfo }
</Td> </Td>
<Td pl="0"> <Td pl="0">
{ item.metadata.tokenName && application ? ( { item.metadata.tokenName && application && !isLoading ? (
<Tooltip label="Edit"> <Tooltip label={ isLoading ? undefined : 'Edit' }>
<IconButton <IconButton
aria-label="edit" aria-label="edit"
variant="simple" variant="simple"
...@@ -62,8 +74,16 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props) ...@@ -62,8 +74,16 @@ const VerifiedAddressesTableItem = ({ item, application, onAdd, onEdit }: Props)
</Tooltip> </Tooltip>
) : null } ) : null }
</Td> </Td>
<Td fontSize="sm"><VerifiedAddressesStatus status={ item.metadata.tokenName ? application?.status : undefined }/></Td> <Td fontSize="sm">
<Td fontSize="sm" color="text_secondary">{ item.metadata.tokenName && application ? dayjs(application.updatedAt).format('MMM DD, YYYY') : null }</Td> <Skeleton isLoaded={ !isLoading } display="inline-block">
<VerifiedAddressesStatus status={ item.metadata.tokenName ? application?.status : undefined }/>
</Skeleton>
</Td>
<Td fontSize="sm" color="text_secondary">
<Skeleton isLoaded={ !isLoading } display="inline-block">
{ item.metadata.tokenName && application ? dayjs(application.updatedAt).format('MMM DD, YYYY') : null }
</Skeleton>
</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