Commit a3ff9bb2 authored by tom's avatar tom

simple table

parent ced55838
export type ExcludeNull<T> = T extends null ? never : T;
export type KeysOfObjectOrNull<T> = T extends null ? never : keyof T; import type { ExcludeNull } from './ExcludeNull';
export type KeysOfObjectOrNull<T> = keyof ExcludeNull<T>;
...@@ -11,9 +11,9 @@ interface Props { ...@@ -11,9 +11,9 @@ interface Props {
const LinkExternal = ({ href, children, className }: Props) => { const LinkExternal = ({ href, children, className }: Props) => {
return ( return (
<Link className={ className } fontSize="sm" display="inline-flex" alignItems="center" target="_blank" href={ href }> <Link className={ className } fontSize="sm" display="inline-block" alignItems="center" target="_blank" href={ href }>
{ children } { children }
<Icon as={ arrowIcon } boxSize={ 4 }/> <Icon as={ arrowIcon } boxSize={ 4 } verticalAlign="middle"/>
</Link> </Link>
); );
}; };
......
...@@ -10,18 +10,21 @@ interface Props { ...@@ -10,18 +10,21 @@ interface Props {
rightSlot?: React.ReactNode; rightSlot?: React.ReactNode;
beforeSlot?: React.ReactNode; beforeSlot?: React.ReactNode;
textareaMaxHeight?: string; textareaMaxHeight?: string;
showCopy?: boolean;
} }
const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textareaMaxHeight }: Props) => { const RawDataSnippet = ({ data, className, title, rightSlot, beforeSlot, textareaMaxHeight, showCopy = true }: Props) => {
// see issue in theme/components/Textarea.ts // see issue in theme/components/Textarea.ts
const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b'); const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b');
return ( return (
<Box className={ className } as="section" title={ title }> <Box className={ className } as="section" title={ title }>
<Flex justifyContent={ title ? 'space-between' : 'flex-end' } alignItems="center" mb={ 3 }> { (title || rightSlot || showCopy) && (
{ title && <Text fontWeight={ 500 }>{ title }</Text> } <Flex justifyContent={ title ? 'space-between' : 'flex-end' } alignItems="center" mb={ 3 }>
{ rightSlot } { title && <Text fontWeight={ 500 }>{ title }</Text> }
{ typeof data === 'string' && <CopyToClipboard text={ data }/> } { rightSlot }
</Flex> { typeof data === 'string' && showCopy && <CopyToClipboard text={ data }/> }
</Flex>
) }
{ beforeSlot } { beforeSlot }
<Box <Box
p={ 4 } p={ 4 }
......
...@@ -8,7 +8,6 @@ import useApiQuery from 'lib/api/useApiQuery'; ...@@ -8,7 +8,6 @@ import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import parseMetadata from 'lib/token/parseMetadata';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo'; import AddressHeadingInfo from 'ui/shared/AddressHeadingInfo';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
...@@ -50,10 +49,6 @@ const TokenInstanceContent = () => { ...@@ -50,10 +49,6 @@ const TokenInstanceContent = () => {
}, },
}); });
const metadata = React.useMemo(() => {
return parseMetadata(tokenInstanceQuery.data?.metadata);
}, [ tokenInstanceQuery.data?.metadata ]);
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id }/> }, { id: 'token_transfers', title: 'Token transfers', component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id }/> },
// there is no api for this tab yet // there is no api for this tab yet
...@@ -91,7 +86,7 @@ const TokenInstanceContent = () => { ...@@ -91,7 +86,7 @@ const TokenInstanceContent = () => {
<AddressHeadingInfo address={ address } token={ tokenInstanceQuery.data.token }/> <AddressHeadingInfo address={ address } token={ tokenInstanceQuery.data.token }/>
<TokenInstanceDetails data={ tokenInstanceQuery.data } metadata={ metadata } scrollRef={ scrollRef }/> <TokenInstanceDetails data={ tokenInstanceQuery.data } scrollRef={ scrollRef }/>
{ /* should stay before tabs to scroll up with pagination */ } { /* should stay before tabs to scroll up with pagination */ }
<Box ref={ scrollRef }></Box> <Box ref={ scrollRef }></Box>
...@@ -102,6 +97,8 @@ const TokenInstanceContent = () => { ...@@ -102,6 +97,8 @@ const TokenInstanceContent = () => {
rightSlot={ !isMobile && transfersQuery.isPaginationVisible && tab !== 'metadata' ? <Pagination { ...transfersQuery.pagination }/> : null } rightSlot={ !isMobile && transfersQuery.isPaginationVisible && tab !== 'metadata' ? <Pagination { ...transfersQuery.pagination }/> : null }
stickyEnabled={ !isMobile } stickyEnabled={ !isMobile }
/> />
{ !tokenInstanceQuery.isLoading && !tokenInstanceQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</> </>
); );
}; };
......
...@@ -2,8 +2,8 @@ import { Box, Flex, Grid, GridItem, useColorModeValue } from '@chakra-ui/react'; ...@@ -2,8 +2,8 @@ import { Box, Flex, Grid, GridItem, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInstance } from 'types/api/token'; import type { TokenInstance } from 'types/api/token';
import type { Metadata } from 'types/client/token';
import parseMetadata from 'lib/token/parseMetadata';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
...@@ -20,10 +20,9 @@ import TokenInstanceTransfersCount from './details/TokenInstanceTransfersCount'; ...@@ -20,10 +20,9 @@ import TokenInstanceTransfersCount from './details/TokenInstanceTransfersCount';
interface Props { interface Props {
data: TokenInstance; data: TokenInstance;
scrollRef?: React.RefObject<HTMLDivElement>; scrollRef?: React.RefObject<HTMLDivElement>;
metadata?: Metadata;
} }
const TokenInstanceDetails = ({ data, scrollRef, metadata }: Props) => { const TokenInstanceDetails = ({ data, scrollRef }: Props) => {
const handleCounterItemClick = React.useCallback(() => { const handleCounterItemClick = React.useCallback(() => {
window.setTimeout(() => { window.setTimeout(() => {
// cannot do scroll instantly, have to wait a little // cannot do scroll instantly, have to wait a little
...@@ -31,6 +30,7 @@ const TokenInstanceDetails = ({ data, scrollRef, metadata }: Props) => { ...@@ -31,6 +30,7 @@ const TokenInstanceDetails = ({ data, scrollRef, metadata }: Props) => {
}, 500); }, 500);
}, [ scrollRef ]); }, [ scrollRef ]);
const metadata = parseMetadata(data.metadata);
const hasMetadata = metadata && Boolean((metadata.name || metadata.description || metadata.attributes)); const hasMetadata = metadata && Boolean((metadata.name || metadata.description || metadata.attributes));
const attributeBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const attributeBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
......
import { Box } from '@chakra-ui/react'; import { Box, Flex, Select, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenInstance } from 'types/api/token'; import type { TokenInstance } from 'types/api/token';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import RawDataSnippet from 'ui/shared/RawDataSnippet'; import RawDataSnippet from 'ui/shared/RawDataSnippet';
import TokenInstanceMetadataAccordion from './metadata/TokenInstanceMetadataAccordion';
type Format = 'JSON' | 'Table'
interface Props { interface Props {
data: TokenInstance['metadata'] | undefined; data: TokenInstance['metadata'] | undefined;
} }
const TokenInstanceMetadata = ({ data }: Props) => { const TokenInstanceMetadata = ({ data }: Props) => {
const [ format, setFormat ] = React.useState<Format>('Table');
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setFormat(event.target.value as Format);
}, []);
if (!data) { if (!data) {
return <Box>There is no metadata for this NFT</Box>; return <Box>There is no metadata for this NFT</Box>;
} }
const content = format === 'Table' ?
<TokenInstanceMetadataAccordion data={ data }/> :
<RawDataSnippet data={ JSON.stringify(data, undefined, 4) } showCopy={ false }/>;
return ( return (
<RawDataSnippet <Box>
data={ JSON.stringify(data, undefined, 4) } <Flex alignItems="center" mb={ 6 }>
/> <chakra.span fontWeight={ 500 }>Metadata</chakra.span>
<Select size="xs" borderRadius="base" value={ format } onChange={ handleSelectChange } focusBorderColor="none" w="auto" ml={ 5 }>
<option value="Table">Table</option>
<option value="JSON">JSON</option>
</Select>
{ format === 'JSON' && <CopyToClipboard text={ JSON.stringify(data) } ml="auto"/> }
</Flex>
{ content }
</Box>
); );
}; };
export default TokenInstanceMetadata; export default React.memo(TokenInstanceMetadata);
import { Box, Flex } from '@chakra-ui/react';
import React from 'react';
import type { TokenInstance } from 'types/api/token';
import type { ExcludeNull } from 'types/utils/ExcludeNull';
import LinkExternal from 'ui/shared/LinkExternal';
import { formatName } from './utils';
interface Props {
data: ExcludeNull<TokenInstance['metadata']>;
}
const TokenInstanceMetadataAccordion = ({ data }: Props) => {
const renderContent = React.useCallback((value: unknown) => {
switch (typeof value) {
case 'string': {
try {
if (!value.includes('http')) {
throw new Error();
}
const url = new URL(value);
return <LinkExternal href={ url.toString() }>{ value }</LinkExternal>;
} catch (error) {
return value;
}
}
case 'number':
case 'boolean': {
return value;
}
default: {
return '-';
}
}
}, []);
return (
<ul>
{ Object.entries(data).map(([ key, value ]) => {
return (
<Flex
as="li"
key={ key }
alignItems="flex-start"
py={ 2 }
pl={{ base: 0, lg: 6 }}
columnGap={ 3 }
borderTopWidth="1px"
borderColor="divider"
_last={{
borderBottomWidth: '1px',
}}
wordBreak="break-word"
fontSize="sm"
>
<Box w="90px" flexShrink={ 0 } fontWeight={ 600 }>{ formatName(key) }</Box>
<Box>{ renderContent(value) }</Box>
</Flex>
);
}) }
</ul>
);
};
export default TokenInstanceMetadataAccordion;
import _upperFirst from 'lodash/upperFirst';
export function formatName(_name: string) {
const name = _name
.replaceAll('_', ' ')
.replaceAll(/\burl|nft|id\b/gi, (str) => str.toUpperCase());
return _upperFirst(name.trim());
}
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