Commit f566c916 authored by tom's avatar tom

token instance page

parent 6e4b3187
...@@ -10,14 +10,14 @@ import fetchApi from 'nextjs/utils/fetchApi'; ...@@ -10,14 +10,14 @@ import fetchApi from 'nextjs/utils/fetchApi';
import config from 'configs/app'; import config from 'configs/app';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
// import TokenInstance from 'ui/pages/TokenInstance'; import TokenInstance from 'ui/pages/TokenInstance';
const pathname: Route['pathname'] = '/token/[hash]/instance/[id]'; const pathname: Route['pathname'] = '/token/[hash]/instance/[id]';
const Page: NextPage<Props<typeof pathname>> = (props: Props<typeof pathname>) => { const Page: NextPage<Props<typeof pathname>> = (props: Props<typeof pathname>) => {
return ( return (
<PageNextJs pathname={ pathname } query={ props.query } apiData={ props.apiData }> <PageNextJs pathname={ pathname } query={ props.query } apiData={ props.apiData }>
{ /* <TokenInstance/> */ } <TokenInstance/>
</PageNextJs> </PageNextJs>
); );
}; };
......
...@@ -22,7 +22,7 @@ export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps { ...@@ -22,7 +22,7 @@ export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {
highlighted?: boolean; highlighted?: boolean;
} }
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( export const Button = React.forwardRef<HTMLDivElement, ButtonProps>(
function Button(props, ref) { function Button(props, ref) {
const { loading, disabled, loadingText, children, expanded, selected, highlighted, loadingSkeleton = false, ...rest } = props; const { loading, disabled, loadingText, children, expanded, selected, highlighted, loadingSkeleton = false, ...rest } = props;
...@@ -51,7 +51,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( ...@@ -51,7 +51,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
})(); })();
return ( return (
<Skeleton loading={ loadingSkeleton } asChild> <Skeleton loading={ loadingSkeleton } asChild ref={ ref }>
<ChakraButton <ChakraButton
{ ...(expanded ? { 'data-expanded': true } : {}) } { ...(expanded ? { 'data-expanded': true } : {}) }
{ ...(selected ? { 'data-selected': true } : {}) } { ...(selected ? { 'data-selected': true } : {}) }
...@@ -59,7 +59,6 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( ...@@ -59,7 +59,6 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{ ...(loading ? { 'data-loading': true } : {}) } { ...(loading ? { 'data-loading': true } : {}) }
{ ...(loadingSkeleton ? { 'data-loading-skeleton': true } : {}) } { ...(loadingSkeleton ? { 'data-loading-skeleton': true } : {}) }
disabled={ !loadingSkeleton && (loading || disabled) } disabled={ !loadingSkeleton && (loading || disabled) }
ref={ ref }
{ ...rest } { ...rest }
> >
{ content } { content }
......
...@@ -42,7 +42,7 @@ export const DialogCloseTrigger = React.forwardRef< ...@@ -42,7 +42,7 @@ export const DialogCloseTrigger = React.forwardRef<
{ ...props } { ...props }
asChild asChild
> >
<CloseButton ref={ ref }> <CloseButton ref={ ref } variant="plain">
{ props.children } { props.children }
</CloseButton> </CloseButton>
</ChakraDialog.CloseTrigger> </ChakraDialog.CloseTrigger>
......
...@@ -6,7 +6,7 @@ export interface IconButtonProps extends ButtonProps {} ...@@ -6,7 +6,7 @@ export interface IconButtonProps extends ButtonProps {}
// TODO @tom2drum variants for icon buttons: prev-next, top-bar, copy-to-clipboard, filter column // TODO @tom2drum variants for icon buttons: prev-next, top-bar, copy-to-clipboard, filter column
export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>( export const IconButton = React.forwardRef<HTMLDivElement, IconButtonProps>(
function IconButton(props, ref) { function IconButton(props, ref) {
const { size, variant = 'plain', ...rest } = props; const { size, variant = 'plain', ...rest } = props;
......
...@@ -33,7 +33,7 @@ export const Toaster = () => { ...@@ -33,7 +33,7 @@ export const Toaster = () => {
return ( return (
<Toast.Root width={{ md: 'sm' }}> <Toast.Root width={{ md: 'sm' }}>
{ toast.type === 'loading' ? ( { toast.type === 'loading' ? (
<Spinner size="sm" color="blue.solid"/> <Spinner size="sm" color="blue.solid" my={ 1 }/>
) : null } ) : null }
<Stack gap="0" flex="1" maxWidth="100%"> <Stack gap="0" flex="1" maxWidth="100%">
{ toast.title && <Toast.Title>{ toast.title }</Toast.Title> } { toast.title && <Toast.Title>{ toast.title }</Toast.Title> }
......
...@@ -215,6 +215,19 @@ const semanticTokens: ThemingConfig['semanticTokens'] = { ...@@ -215,6 +215,19 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
error: { value: { _light: '{colors.red.100}', _dark: '{colors.red.900}' } }, error: { value: { _light: '{colors.red.100}', _dark: '{colors.red.900}' } },
}, },
}, },
toast: {
fg: {
DEFAULT: { value: '{colors.alert.fg}' },
},
bg: {
DEFAULT: { value: '{colors.alert.bg.info}' },
info: { value: { _light: '{colors.blue.100}', _dark: '{colors.blue.900}' } },
warning: { value: '{colors.alert.bg.warning}' },
success: { value: '{colors.alert.bg.success}' },
error: { value: '{colors.alert.bg.error}' },
loading: { value: { _light: '{colors.blue.100}', _dark: '{colors.blue.900}' } },
},
},
input: { input: {
fg: { fg: {
DEFAULT: { value: { _light: '{colors.gray.800}', _dark: '{colors.gray.50}' } }, DEFAULT: { value: { _light: '{colors.gray.800}', _dark: '{colors.gray.50}' } },
......
...@@ -26,31 +26,37 @@ export const recipe = defineSlotRecipe({ ...@@ -26,31 +26,37 @@ export const recipe = defineSlotRecipe({
transition: 'translate 400ms, scale 400ms, opacity 200ms', transition: 'translate 400ms, scale 400ms, opacity 200ms',
transitionTimingFunction: 'cubic-bezier(0.06, 0.71, 0.55, 1)', transitionTimingFunction: 'cubic-bezier(0.06, 0.71, 0.55, 1)',
}, },
bg: 'alert.bg.info', bg: 'toast.bg.info',
color: 'alert.fg', color: 'toast.fg',
boxShadow: 'xl', boxShadow: 'xl',
'--toast-trigger-bg': 'colors.bg.muted', '--toast-trigger-bg': 'colors.bg.muted',
'&[data-type=warning]': { '&[data-type=warning]': {
color: 'alert.fg', color: 'toast.fg',
bg: 'alert.bg.warning', bg: 'toast.bg.warning',
'--toast-trigger-bg': '{white/10}', '--toast-trigger-bg': '{white/10}',
'--toast-border-color': '{white/40}', '--toast-border-color': '{white/40}',
}, },
'&[data-type=success]': { '&[data-type=success]': {
color: 'alert.fg', color: 'toast.fg',
bg: 'alert.bg.success', bg: 'toast.bg.success',
'--toast-trigger-bg': '{white/10}', '--toast-trigger-bg': '{white/10}',
'--toast-border-color': '{white/40}', '--toast-border-color': '{white/40}',
}, },
'&[data-type=error]': { '&[data-type=error]': {
color: 'alert.fg', color: 'toast.fg',
bg: 'alert.bg.error', bg: 'toast.bg.error',
'--toast-trigger-bg': '{white/10}', '--toast-trigger-bg': '{white/10}',
'--toast-border-color': '{white/40}', '--toast-border-color': '{white/40}',
}, },
'&[data-type=info]': { '&[data-type=info]': {
color: 'alert.fg', color: 'toast.fg',
bg: 'alert.bg.info', bg: 'toast.bg.info',
'--toast-trigger-bg': '{white/10}',
'--toast-border-color': '{white/40}',
},
'&[data-type=loading]': {
color: 'toast.fg',
bg: 'toast.bg.info',
'--toast-trigger-bg': '{white/10}', '--toast-trigger-bg': '{white/10}',
'--toast-border-color': '{white/40}', '--toast-border-color': '{white/40}',
}, },
......
/* eslint-disable max-len */ /* eslint-disable max-len */
/* eslint-disable react/jsx-no-bind */
import { HStack, Spinner, VStack } from '@chakra-ui/react'; import { HStack, Spinner, VStack } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { Button } from 'toolkit/chakra/button';
import { useColorMode } from 'toolkit/chakra/color-mode'; import { useColorMode } from 'toolkit/chakra/color-mode';
import { Field } from 'toolkit/chakra/field'; import { Field } from 'toolkit/chakra/field';
import { Heading } from 'toolkit/chakra/heading'; import { Heading } from 'toolkit/chakra/heading';
...@@ -15,7 +14,6 @@ import { Skeleton } from 'toolkit/chakra/skeleton'; ...@@ -15,7 +14,6 @@ import { Skeleton } from 'toolkit/chakra/skeleton';
import { Switch } from 'toolkit/chakra/switch'; import { Switch } from 'toolkit/chakra/switch';
import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'toolkit/chakra/tabs'; import { TabsContent, TabsList, TabsRoot, TabsTrigger } from 'toolkit/chakra/tabs';
import { Textarea } from 'toolkit/chakra/textarea'; import { Textarea } from 'toolkit/chakra/textarea';
import { toaster } from 'toolkit/chakra/toaster';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import AccordionsShowcase from 'ui/showcases/Accordion'; import AccordionsShowcase from 'ui/showcases/Accordion';
...@@ -33,6 +31,7 @@ import RadioShowcase from 'ui/showcases/Radio'; ...@@ -33,6 +31,7 @@ import RadioShowcase from 'ui/showcases/Radio';
import SelectShowcase from 'ui/showcases/Select'; import SelectShowcase from 'ui/showcases/Select';
import TabsShowcase from 'ui/showcases/Tabs'; import TabsShowcase from 'ui/showcases/Tabs';
import TagShowcase from 'ui/showcases/Tag'; import TagShowcase from 'ui/showcases/Tag';
import ToastShowcase from 'ui/showcases/Toast';
import TooltipShowcase from 'ui/showcases/Tooltip'; import TooltipShowcase from 'ui/showcases/Tooltip';
const TEXT = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'; const TEXT = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';
...@@ -65,6 +64,7 @@ const ChakraShowcases = () => { ...@@ -65,6 +64,7 @@ const ChakraShowcases = () => {
<TabsTrigger value="select">Select</TabsTrigger> <TabsTrigger value="select">Select</TabsTrigger>
<TabsTrigger value="tabs">Tabs</TabsTrigger> <TabsTrigger value="tabs">Tabs</TabsTrigger>
<TabsTrigger value="tag">Tag</TabsTrigger> <TabsTrigger value="tag">Tag</TabsTrigger>
<TabsTrigger value="toast">Toast</TabsTrigger>
<TabsTrigger value="tooltip">Tooltip</TabsTrigger> <TabsTrigger value="tooltip">Tooltip</TabsTrigger>
<TabsTrigger value="unsorted">Unsorted</TabsTrigger> <TabsTrigger value="unsorted">Unsorted</TabsTrigger>
</TabsList> </TabsList>
...@@ -83,6 +83,7 @@ const ChakraShowcases = () => { ...@@ -83,6 +83,7 @@ const ChakraShowcases = () => {
<SelectShowcase/> <SelectShowcase/>
<TabsShowcase/> <TabsShowcase/>
<TagShowcase/> <TagShowcase/>
<ToastShowcase/>
<TooltipShowcase/> <TooltipShowcase/>
<TabsContent value="unsorted"> <TabsContent value="unsorted">
...@@ -130,13 +131,6 @@ const ChakraShowcases = () => { ...@@ -130,13 +131,6 @@ const ChakraShowcases = () => {
</HStack> </HStack>
</section> </section>
<section>
<Heading textStyle="heading.md" mb={ 2 }>Toasts</Heading>
<HStack gap={ 4 } whiteSpace="nowrap">
<Button onClick={ () => toaster.success({ title: 'Success', description: 'Toast content' }) }>Success</Button>
</HStack>
</section>
<section> <section>
<Heading textStyle="heading.md" mb={ 2 }>Select</Heading> <Heading textStyle="heading.md" mb={ 2 }>Select</Heading>
<HStack gap={ 4 } whiteSpace="nowrap" flexWrap="wrap"> <HStack gap={ 4 } whiteSpace="nowrap" flexWrap="wrap">
......
...@@ -2,8 +2,8 @@ import { Box } from '@chakra-ui/react'; ...@@ -2,8 +2,8 @@ import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { TabItemRegular } from 'toolkit/components/AdaptiveTabs/types';
import type { PaginationParams } from 'ui/shared/pagination/types'; import type { PaginationParams } from 'ui/shared/pagination/types';
import type { RoutedTab } from 'ui/shared/Tabs/types';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
...@@ -16,10 +16,10 @@ import { ...@@ -16,10 +16,10 @@ import {
getTokenInstanceTransfersStub, getTokenInstanceTransfersStub,
getTokenInstanceHoldersStub, getTokenInstanceHoldersStub,
} from 'stubs/token'; } from 'stubs/token';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TokenHolders from 'ui/token/TokenHolders/TokenHolders'; import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer'; import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
import { MetadataUpdateProvider } from 'ui/tokenInstance/contexts/metadataUpdate'; import { MetadataUpdateProvider } from 'ui/tokenInstance/contexts/metadataUpdate';
...@@ -93,14 +93,18 @@ const TokenInstanceContent = () => { ...@@ -93,14 +93,18 @@ const TokenInstanceContent = () => {
} }
}, [ tokenInstanceQuery.data, tokenInstanceQuery.isPlaceholderData, tokenQuery.data, tokenQuery.isPlaceholderData ]); }, [ tokenInstanceQuery.data, tokenInstanceQuery.isPlaceholderData, tokenQuery.data, tokenQuery.isPlaceholderData ]);
const tabs: Array<RoutedTab> = [ const tabs: Array<TabItemRegular> = [
{ {
id: 'token_transfers', id: 'token_transfers',
title: 'Token transfers', title: 'Token transfers',
component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id } tokenQuery={ tokenQuery } shouldRender={ !isLoading }/>, component: <TokenTransfer transfersQuery={ transfersQuery } tokenId={ id } tokenQuery={ tokenQuery } shouldRender={ !isLoading } tabsHeight={ 80 }/>,
}, },
shouldFetchHolders ? shouldFetchHolders ?
{ id: 'holders', title: 'Holders', component: <TokenHolders holdersQuery={ holdersQuery } token={ tokenQuery.data } shouldRender={ !isLoading }/> } : {
id: 'holders',
title: 'Holders',
component: <TokenHolders holdersQuery={ holdersQuery } token={ tokenQuery.data } shouldRender={ !isLoading } tabsHeight={ 80 }/>,
} :
undefined, undefined,
{ id: 'metadata', title: 'Metadata', component: ( { id: 'metadata', title: 'Metadata', component: (
<TokenInstanceMetadata <TokenInstanceMetadata
...@@ -138,7 +142,7 @@ const TokenInstanceContent = () => { ...@@ -138,7 +142,7 @@ const TokenInstanceContent = () => {
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
tabListProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } } listProps={ isMobile ? { mt: 8 } : { mt: 3, py: 5, marginBottom: 0 } }
isLoading={ isLoading } isLoading={ isLoading }
rightSlot={ !isMobile && pagination?.isVisible ? <Pagination { ...pagination }/> : null } rightSlot={ !isMobile && pagination?.isVisible ? <Pagination { ...pagination }/> : null }
stickyEnabled={ !isMobile } stickyEnabled={ !isMobile }
......
...@@ -81,7 +81,7 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -81,7 +81,7 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
} }
}, [ refCode, isRefCodeUsed, isSignUp ]); }, [ refCode, isRefCodeUsed, isSignUp ]);
const handleButtonClick = React.React.useCallback(() => { const handleButtonClick = React.useCallback(() => {
if (canTrySharedLogin) { if (canTrySharedLogin) {
return openAuthModal(Boolean(profileQuery.data?.email), true); return openAuthModal(Boolean(profileQuery.data?.email), true);
} }
...@@ -121,7 +121,7 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => { ...@@ -121,7 +121,7 @@ const LoginStepContent = ({ goNext, closeModal, openAuthModal }: Props) => {
src="/static/merits_program.png" src="/static/merits_program.png"
alt="Merits program" alt="Merits program"
mb={ 3 } mb={ 3 }
fallback={ <Skeleton w="full" h="120px" mb={ 3 }/> } fallback={ <Skeleton loading w="full" h="120px" mb={ 3 }/> }
/> />
<Box mb={ 6 }> <Box mb={ 6 }>
Merits are awarded for a variety of different Blockscout activities. Connect a wallet to get started. Merits are awarded for a variety of different Blockscout activities. Connect a wallet to get started.
......
...@@ -68,7 +68,7 @@ const PrivateTagMenuItem = ({ hash, entityType = 'address', type }: Props) => { ...@@ -68,7 +68,7 @@ const PrivateTagMenuItem = ({ hash, entityType = 'address', type }: Props) => {
<AuthGuard onAuthSuccess={ modal.onOpen }> <AuthGuard onAuthSuccess={ modal.onOpen }>
{ ({ onClick }) => ( { ({ onClick }) => (
<MenuItem onClick={ onClick } value="add-private-tag"> <MenuItem onClick={ onClick } value="add-private-tag">
<IconSvg name="privattags" boxSize={ 6 } mr={ 2 }/> <IconSvg name="privattags" boxSize={ 6 }/>
<span>Add private tag</span> <span>Add private tag</span>
</MenuItem> </MenuItem>
) } ) }
......
...@@ -22,7 +22,7 @@ const PublicTagMenuItem = ({ hash, type }: ItemProps) => { ...@@ -22,7 +22,7 @@ const PublicTagMenuItem = ({ hash, type }: ItemProps) => {
case 'menu_item': { case 'menu_item': {
return ( return (
<MenuItem onClick={ handleClick } value="add-public-tag"> <MenuItem onClick={ handleClick } value="add-public-tag">
<IconSvg name="publictags" boxSize={ 6 } mr={ 2 }/> <IconSvg name="publictags" boxSize={ 6 }/>
<span>Add public tag</span> <span>Add public tag</span>
</MenuItem> </MenuItem>
); );
......
import React from 'react'; import React from 'react';
import { DialogContent, DialogRoot } from 'toolkit/chakra/dialog'; import { DialogContent, DialogRoot, DialogHeader } from 'toolkit/chakra/dialog';
interface Props { interface Props {
open: boolean; open: boolean;
...@@ -12,6 +12,7 @@ const NftMediaFullscreenModal = ({ open, onOpenChange, children }: Props) => { ...@@ -12,6 +12,7 @@ const NftMediaFullscreenModal = ({ open, onOpenChange, children }: Props) => {
return ( return (
<DialogRoot open={ open } onOpenChange={ onOpenChange } motionPreset="none"> <DialogRoot open={ open } onOpenChange={ onOpenChange } motionPreset="none">
<DialogContent w="unset" maxW="100vw" p={ 0 } background="none" boxShadow="none"> <DialogContent w="unset" maxW="100vw" p={ 0 } background="none" boxShadow="none">
<DialogHeader/>
{ children } { children }
</DialogContent> </DialogContent>
</DialogRoot> </DialogRoot>
......
/* eslint-disable react/jsx-no-bind */
import React from 'react';
import { Button } from 'toolkit/chakra/button';
import { toaster } from 'toolkit/chakra/toaster';
import { Section, Container, SectionHeader, SamplesStack, Sample } from './parts';
const ToastShowcase = () => {
return (
<Container value="toast">
<Section>
<SectionHeader>Type</SectionHeader>
<SamplesStack>
<Sample label="type: info">
<Button onClick={ () => toaster.create({ title: 'Info', description: 'Toast content', type: 'info' }) }>
Info
</Button>
</Sample>
<Sample label="type: success">
<Button onClick={ () => toaster.success({ title: 'Success', description: 'Toast content' }) }>
Success
</Button>
</Sample>
<Sample label="type: warning">
<Button onClick={ () => toaster.create({ title: 'Warning', description: 'Toast content', type: 'warning' }) }>
Warning
</Button>
</Sample>
<Sample label="type: error">
<Button onClick={ () => toaster.error({ title: 'Error', description: 'Toast content' }) }>
Error
</Button>
</Sample>
<Sample label="type: loading">
<Button onClick={ () => toaster.loading({ title: 'Loading', description: 'Please wait for...' }) }>
Loading
</Button>
</Sample>
</SamplesStack>
</Section>
</Container>
);
};
export default React.memo(ToastShowcase);
...@@ -21,9 +21,10 @@ type Props = { ...@@ -21,9 +21,10 @@ type Props = {
token?: TokenInfo; token?: TokenInfo;
holdersQuery: QueryWithPagesResult<'token_holders'>; holdersQuery: QueryWithPagesResult<'token_holders'>;
shouldRender?: boolean; shouldRender?: boolean;
tabsHeight?: number;
}; };
const TokenHoldersContent = ({ holdersQuery, token, shouldRender = true }: Props) => { const TokenHoldersContent = ({ holdersQuery, token, shouldRender = true, tabsHeight = TABS_HEIGHT }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isMounted = useIsMounted(); const isMounted = useIsMounted();
...@@ -56,7 +57,7 @@ const TokenHoldersContent = ({ holdersQuery, token, shouldRender = true }: Props ...@@ -56,7 +57,7 @@ const TokenHoldersContent = ({ holdersQuery, token, shouldRender = true }: Props
<TokenHoldersTable <TokenHoldersTable
data={ items } data={ items }
token={ token } token={ token }
top={ TABS_HEIGHT } top={ tabsHeight }
isLoading={ holdersQuery.isPlaceholderData } isLoading={ holdersQuery.isPlaceholderData }
/> />
</Box> </Box>
......
...@@ -27,9 +27,10 @@ type Props = { ...@@ -27,9 +27,10 @@ type Props = {
tokenId?: string; tokenId?: string;
tokenQuery: UseQueryResult<TokenInfo, ResourceError<unknown>>; tokenQuery: UseQueryResult<TokenInfo, ResourceError<unknown>>;
shouldRender?: boolean; shouldRender?: boolean;
tabsHeight?: number;
}; };
const TokenTransfer = ({ transfersQuery, tokenId, tokenQuery, shouldRender = true }: Props) => { const TokenTransfer = ({ transfersQuery, tokenId, tokenQuery, tabsHeight = TABS_HEIGHT, shouldRender = true }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isMounted = useIsMounted(); const isMounted = useIsMounted();
const router = useRouter(); const router = useRouter();
...@@ -74,7 +75,7 @@ const TokenTransfer = ({ transfersQuery, tokenId, tokenQuery, shouldRender = tru ...@@ -74,7 +75,7 @@ const TokenTransfer = ({ transfersQuery, tokenId, tokenQuery, shouldRender = tru
<Box display={{ base: 'none', lg: 'block' }}> <Box display={{ base: 'none', lg: 'block' }}>
<TokenTransferTable <TokenTransferTable
data={ data?.items } data={ data?.items }
top={ TABS_HEIGHT } top={ tabsHeight }
showSocketInfo={ pagination.page === 1 } showSocketInfo={ pagination.page === 1 }
socketInfoAlert={ socketAlert } socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount } socketInfoNum={ newItemsCount }
......
...@@ -5,12 +5,11 @@ import type { TokenInfo, TokenInstance } from 'types/api/token'; ...@@ -5,12 +5,11 @@ import type { TokenInfo, TokenInstance } from 'types/api/token';
import config from 'configs/app'; import config from 'configs/app';
import useIsMounted from 'lib/hooks/useIsMounted'; import useIsMounted from 'lib/hooks/useIsMounted';
import { Skeleton } from 'toolkit/chakra/skeleton';
import AppActionButton from 'ui/shared/AppActionButton/AppActionButton'; import AppActionButton from 'ui/shared/AppActionButton/AppActionButton';
import useAppActionData from 'ui/shared/AppActionButton/useAppActionData'; import useAppActionData from 'ui/shared/AppActionButton/useAppActionData';
import Skeleton from 'ui/shared/chakra/Skeleton';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo';
import DetailedInfoSponsoredItem from 'ui/shared/DetailedInfo/DetailedInfoSponsoredItem'; import DetailedInfoSponsoredItem from 'ui/shared/DetailedInfo/DetailedInfoSponsoredItem';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
...@@ -80,7 +79,7 @@ const TokenInstanceDetails = ({ data, token, scrollRef, isLoading }: Props) => { ...@@ -80,7 +79,7 @@ const TokenInstanceDetails = ({ data, token, scrollRef, isLoading }: Props) => {
</DetailedInfo.ItemLabel> </DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue> <DetailedInfo.ItemValue>
<Flex alignItems="center" overflow="hidden"> <Flex alignItems="center" overflow="hidden">
<Skeleton isLoaded={ !isLoading } overflow="hidden" display="inline-block" w="100%"> <Skeleton loading={ isLoading } overflow="hidden" display="inline-block" w="100%">
<HashStringShortenDynamic hash={ data.id }/> <HashStringShortenDynamic hash={ data.id }/>
</Skeleton> </Skeleton>
<CopyToClipboard text={ data.id } isLoading={ isLoading }/> <CopyToClipboard text={ data.id } isLoading={ isLoading }/>
......
import { Alert, Box, Flex, chakra } from '@chakra-ui/react'; import { Box, Flex, chakra, createListCollection } 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 { Alert } from 'toolkit/chakra/alert';
import { SelectContent, SelectControl, SelectItem, SelectRoot, SelectValueText } from 'toolkit/chakra/select';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import RawDataSnippet from 'ui/shared/RawDataSnippet'; import RawDataSnippet from 'ui/shared/RawDataSnippet';
import Select from 'ui/shared/select/Select';
import { useMetadataUpdateContext } from './contexts/metadataUpdate'; import { useMetadataUpdateContext } from './contexts/metadataUpdate';
import MetadataAccordion from './metadata/MetadataAccordion'; import MetadataAccordion from './metadata/MetadataAccordion';
...@@ -16,6 +17,8 @@ const OPTIONS = [ ...@@ -16,6 +17,8 @@ const OPTIONS = [
{ label: 'JSON', value: 'JSON' as const }, { label: 'JSON', value: 'JSON' as const },
]; ];
const collection = createListCollection({ items: OPTIONS });
type Format = (typeof OPTIONS)[number]['value']; type Format = (typeof OPTIONS)[number]['value'];
interface Props { interface Props {
...@@ -28,6 +31,10 @@ const TokenInstanceMetadata = ({ data, isPlaceholderData }: Props) => { ...@@ -28,6 +31,10 @@ const TokenInstanceMetadata = ({ data, isPlaceholderData }: Props) => {
const { status: refetchStatus } = useMetadataUpdateContext() || {}; const { status: refetchStatus } = useMetadataUpdateContext() || {};
const handleValueChange = React.useCallback(({ value }: { value: Array<string> }) => {
setFormat(value[0] as Format);
}, []);
if (isPlaceholderData || refetchStatus === 'WAITING_FOR_RESPONSE') { if (isPlaceholderData || refetchStatus === 'WAITING_FOR_RESPONSE') {
return <ContentLoader/>; return <ContentLoader/>;
} }
...@@ -43,21 +50,30 @@ const TokenInstanceMetadata = ({ data, isPlaceholderData }: Props) => { ...@@ -43,21 +50,30 @@ const TokenInstanceMetadata = ({ data, isPlaceholderData }: Props) => {
return ( return (
<Box> <Box>
{ refetchStatus === 'ERROR' && ( { refetchStatus === 'ERROR' && (
<Alert status="warning" display="flow" mb={ 6 }> <Alert status="warning" mb={ 6 } title="Ooops!" display={{ base: 'block', lg: 'flex' }}>
<chakra.span fontWeight={ 600 }>Ooops! </chakra.span>
<span>We { `couldn't` } refresh metadata. Please try again now or later.</span> <span>We { `couldn't` } refresh metadata. Please try again now or later.</span>
</Alert> </Alert>
) } ) }
<Flex alignItems="center" mb={ 6 }> <Flex alignItems="center" mb={ 6 }>
<chakra.span fontWeight={ 500 }>Metadata</chakra.span> <chakra.span fontWeight={ 500 }>Metadata</chakra.span>
<Select <SelectRoot
options={ OPTIONS } collection={ collection }
name="metadata-format" variant="outline"
defaultValue="Table" onValueChange={ handleValueChange }
onChange={ setFormat } value={ [ format ] }
w="85px"
ml={ 5 } ml={ 5 }
/> >
<SelectControl w="100px">
<SelectValueText placeholder="Select format"/>
</SelectControl>
<SelectContent>
{ collection.items.map((item) => (
<SelectItem item={ item } key={ item.value }>
{ item.label }
</SelectItem>
)) }
</SelectContent>
</SelectRoot>
{ format === 'JSON' && <CopyToClipboard text={ JSON.stringify(data) } ml="auto"/> } { format === 'JSON' && <CopyToClipboard text={ JSON.stringify(data) } ml="auto"/> }
</Flex> </Flex>
{ content } { content }
......
import type { ToastId } from '@chakra-ui/react'; import { Spinner, Center } from '@chakra-ui/react';
import { Alert, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Spinner, Center } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
...@@ -11,9 +10,11 @@ import useApiFetch from 'lib/api/useApiFetch'; ...@@ -11,9 +10,11 @@ import useApiFetch from 'lib/api/useApiFetch';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import { MINUTE, SECOND } from 'lib/consts'; import { MINUTE, SECOND } from 'lib/consts';
import getErrorMessage from 'lib/errors/getErrorMessage'; import getErrorMessage from 'lib/errors/getErrorMessage';
import useToast from 'lib/hooks/useToast';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import { Alert } from 'toolkit/chakra/alert';
import { DialogBody, DialogContent, DialogHeader, DialogRoot } from 'toolkit/chakra/dialog';
import { toaster } from 'toolkit/chakra/toaster';
import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha'; import ReCaptcha from 'ui/shared/reCaptcha/ReCaptcha';
import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha'; import useReCaptcha from 'ui/shared/reCaptcha/useReCaptcha';
...@@ -26,24 +27,22 @@ interface Props { ...@@ -26,24 +27,22 @@ interface Props {
const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => { const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
const timeoutId = React.useRef<number>(); const timeoutId = React.useRef<number>();
const toastId = React.useRef<ToastId>(); const toastId = React.useRef<string>();
const { status, setStatus } = useMetadataUpdateContext() || {}; const { status, setStatus } = useMetadataUpdateContext() || {};
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const toast = useToast();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const recaptcha = useReCaptcha(); const recaptcha = useReCaptcha();
const handleRefreshError = React.useCallback(() => { const handleRefreshError = React.useCallback(() => {
setStatus?.('ERROR'); setStatus?.('ERROR');
toastId.current && toast.update(toastId.current, { toastId.current && toaster.update(toastId.current, {
title: 'Error', title: 'Error',
description: 'The refreshing process has failed. Please try again.', description: 'The refreshing process has failed. Please try again.',
status: 'warning', type: 'error',
duration: 5 * SECOND, duration: 5 * SECOND,
isClosable: true,
}); });
}, [ setStatus, toast ]); }, [ setStatus ]);
const initializeUpdate = React.useCallback(async(tokenProp?: string) => { const initializeUpdate = React.useCallback(async(tokenProp?: string) => {
try { try {
...@@ -56,28 +55,26 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => { ...@@ -56,28 +55,26 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
}, },
}); });
setStatus?.('WAITING_FOR_RESPONSE'); setStatus?.('WAITING_FOR_RESPONSE');
toastId.current = toast({ toastId.current = toaster.loading({
title: 'Please wait', title: 'Please wait',
description: 'Refetching metadata request sent', description: 'Refetching metadata request sent',
icon: <Spinner size="sm" mr={ 2 }/>, duration: Infinity,
status: 'warning',
duration: null,
isClosable: false,
}); });
timeoutId.current = window.setTimeout(handleRefreshError, 2 * MINUTE); timeoutId.current = window.setTimeout(handleRefreshError, 2 * MINUTE);
} catch (error) { } catch (error) {
toast({ toaster.error({
title: 'Error', title: 'Error',
description: getErrorMessage(error) || 'Unable to initialize metadata update', description: getErrorMessage(error) || 'Unable to initialize metadata update',
status: 'warning',
}); });
setStatus?.('ERROR'); setStatus?.('ERROR');
} }
}, [ apiFetch, handleRefreshError, hash, id, recaptcha, setStatus, toast ]); }, [ apiFetch, handleRefreshError, hash, id, recaptcha, setStatus ]);
const handleModalClose = React.useCallback(() => { const handleModalClose = React.useCallback(({ open }: { open: boolean }) => {
if (!open) {
setStatus?.('INITIAL'); setStatus?.('INITIAL');
}
}, [ setStatus ]); }, [ setStatus ]);
const handleSocketMessage: SocketMessage.TokenInstanceMetadataFetched['handler'] = React.useCallback((payload) => { const handleSocketMessage: SocketMessage.TokenInstanceMetadataFetched['handler'] = React.useCallback((payload) => {
...@@ -106,18 +103,17 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => { ...@@ -106,18 +103,17 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
}; };
}); });
toastId.current && toast.update(toastId.current, { toastId.current && toaster.update(toastId.current, {
title: 'Success!', title: 'Success!',
description: 'Metadata has been refreshed', description: 'Metadata has been refreshed',
status: 'success', type: 'success',
duration: 5 * SECOND, duration: 5 * SECOND,
isClosable: true,
}); });
setStatus?.('SUCCESS'); setStatus?.('SUCCESS');
window.clearTimeout(timeoutId.current); window.clearTimeout(timeoutId.current);
}, [ hash, id, queryClient, setStatus, toast ]); }, [ hash, id, queryClient, setStatus ]);
const channel = useSocketChannel({ const channel = useSocketChannel({
topic: `token_instances:${ hash.toLowerCase() }`, topic: `token_instances:${ hash.toLowerCase() }`,
...@@ -144,10 +140,9 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => { ...@@ -144,10 +140,9 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
React.useEffect(() => { React.useEffect(() => {
return () => { return () => {
timeoutId.current && window.clearTimeout(timeoutId.current); timeoutId.current && window.clearTimeout(timeoutId.current);
toastId.current && toast.close(toastId.current); toastId.current && toaster.remove(toastId.current);
}; };
// run only on mount/unmount // run only on mount/unmount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
if (status !== 'MODAL_OPENED') { if (status !== 'MODAL_OPENED') {
...@@ -155,12 +150,18 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => { ...@@ -155,12 +150,18 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
} }
return ( return (
<Modal isOpen={ status === 'MODAL_OPENED' } onClose={ handleModalClose } size={{ base: 'full', lg: 'sm' }}> <DialogRoot
<ModalOverlay/> open={ status === 'MODAL_OPENED' }
<ModalContent> onOpenChange={ handleModalClose }
<ModalHeader fontWeight="500" textStyle="h3" mb={ 4 }>Sending request</ModalHeader> size={{ lgDown: 'full', lg: 'sm' }}
<ModalCloseButton/> trapFocus={ false }
<ModalBody mb={ 0 } minH="78px"> preventScroll={ false }
modal={ false }
closeOnInteractOutside={ false }
>
<DialogContent>
<DialogHeader fontWeight="500" textStyle="h3" mb={ 4 }>Sending request</DialogHeader>
<DialogBody mb={ 0 } minH="78px">
{ config.services.reCaptchaV2.siteKey ? ( { config.services.reCaptchaV2.siteKey ? (
<> <>
<Center h="80px"> <Center h="80px">
...@@ -174,9 +175,9 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => { ...@@ -174,9 +175,9 @@ const TokenInstanceMetadataFetcher = ({ hash, id }: Props) => {
Please contact the service maintainer to make necessary changes in the service configuration. Please contact the service maintainer to make necessary changes in the service configuration.
</Alert> </Alert>
) } ) }
</ModalBody> </DialogBody>
</ModalContent> </DialogContent>
</Modal> </DialogRoot>
); );
}; };
......
...@@ -6,12 +6,12 @@ import type { TokenInfo, TokenInstance } from 'types/api/token'; ...@@ -6,12 +6,12 @@ import type { TokenInfo, TokenInstance } from 'types/api/token';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import * as regexp from 'lib/regexp'; import * as regexp from 'lib/regexp';
import { getTokenTypeName } from 'lib/token/tokenTypes'; import { getTokenTypeName } from 'lib/token/tokenTypes';
import { Link } from 'toolkit/chakra/link';
import { Tag } from 'toolkit/chakra/tag';
import AddressQrCode from 'ui/address/details/AddressQrCode'; import AddressQrCode from 'ui/address/details/AddressQrCode';
import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu'; import AccountActionsMenu from 'ui/shared/AccountActionsMenu/AccountActionsMenu';
import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet'; import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
import Tag from 'ui/shared/chakra/Tag';
import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import LinkExternal from 'ui/shared/links/LinkExternal';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
interface Props { interface Props {
...@@ -53,7 +53,7 @@ const TokenInstancePageTitle = ({ isLoading, token, instance, hash }: Props) => ...@@ -53,7 +53,7 @@ const TokenInstancePageTitle = ({ isLoading, token, instance, hash }: Props) =>
}; };
}, [ appProps.referrer, hash ]); }, [ appProps.referrer, hash ]);
const tokenTag = token ? <Tag isLoading={ isLoading }>{ getTokenTypeName(token.type) }</Tag> : null; const tokenTag = token ? <Tag loading={ isLoading }>{ getTokenTypeName(token.type) }</Tag> : null;
const appLink = (() => { const appLink = (() => {
if (!instance?.external_app_url) { if (!instance?.external_app_url) {
...@@ -66,15 +66,15 @@ const TokenInstancePageTitle = ({ isLoading, token, instance, hash }: Props) => ...@@ -66,15 +66,15 @@ const TokenInstancePageTitle = ({ isLoading, token, instance, hash }: Props) =>
new URL('https://' + instance.external_app_url); new URL('https://' + instance.external_app_url);
return ( return (
<LinkExternal href={ url.toString() } variant="subtle" isLoading={ isLoading } ml={{ base: 0, lg: 'auto' }}> <Link external href={ url.toString() } variant="underlaid" loading={ isLoading } ml={{ base: 0, lg: 'auto' }}>
{ url.hostname || instance.external_app_url } { url.hostname || instance.external_app_url }
</LinkExternal> </Link>
); );
} catch (error) { } catch (error) {
return ( return (
<LinkExternal href={ instance.external_app_url } isLoading={ isLoading } ml={{ base: 0, lg: 'auto' }}> <Link external href={ instance.external_app_url } variant="underlaid" loading={ isLoading } ml={{ base: 0, lg: 'auto' }}>
View in app View in app
</LinkExternal> </Link>
); );
} }
})(); })();
...@@ -103,8 +103,8 @@ const TokenInstancePageTitle = ({ isLoading, token, instance, hash }: Props) => ...@@ -103,8 +103,8 @@ const TokenInstancePageTitle = ({ isLoading, token, instance, hash }: Props) =>
maxW="700px" maxW="700px"
/> />
) } ) }
{ !isLoading && <AddressAddToWallet token={ token } variant="button"/> } { !isLoading && token && <AddressAddToWallet token={ token } variant="button"/> }
<AddressQrCode address={ address } isLoading={ isLoading }/> <AddressQrCode hash={ address.hash } isLoading={ isLoading }/>
<AccountActionsMenu isLoading={ isLoading } showUpdateMetadataItem/> <AccountActionsMenu isLoading={ isLoading } showUpdateMetadataItem/>
{ appLink } { appLink }
</Flex> </Flex>
......
import { Grid, GridItem, useColorModeValue } from '@chakra-ui/react'; import { Grid, GridItem } 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 { MetadataAttributes } from 'types/client/token'; import type { MetadataAttributes } from 'types/client/token';
import parseMetadata from 'lib/token/parseMetadata'; import parseMetadata from 'lib/token/parseMetadata';
import Skeleton from 'ui/shared/chakra/Skeleton'; import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo';
import LinkExternal from 'ui/shared/links/LinkExternal';
import TruncatedValue from 'ui/shared/TruncatedValue'; import TruncatedValue from 'ui/shared/TruncatedValue';
import { useMetadataUpdateContext } from '../contexts/metadataUpdate'; import { useMetadataUpdateContext } from '../contexts/metadataUpdate';
...@@ -24,24 +23,22 @@ interface ItemProps { ...@@ -24,24 +23,22 @@ interface ItemProps {
} }
const Item = ({ data, isLoading }: ItemProps) => { const Item = ({ data, isLoading }: ItemProps) => {
const attributeBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const value = (() => { const value = (() => {
if (data.value_type === 'URL') { if (data.value_type === 'URL') {
return ( return (
<LinkExternal <Link
external
whiteSpace="nowrap" whiteSpace="nowrap"
display="inline-flex" display="inline-flex"
alignItems="center" alignItems="center"
w="100%" w="100%"
overflow="hidden" overflow="hidden"
href={ data.value } href={ data.value }
fontSize="sm" textStyle="sm"
lineHeight={ 5 } loading={ isLoading }
isLoading={ isLoading }
> >
<TruncatedValue value={ data.value } w="calc(100% - 16px)" isLoading={ isLoading }/> <TruncatedValue value={ data.value } w="calc(100% - 16px)" isLoading={ isLoading }/>
</LinkExternal> </Link>
); );
} }
...@@ -50,7 +47,7 @@ const Item = ({ data, isLoading }: ItemProps) => { ...@@ -50,7 +47,7 @@ const Item = ({ data, isLoading }: ItemProps) => {
return ( return (
<GridItem <GridItem
bgColor={ attributeBgColor } bgColor={{ _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' }}
borderRadius="md" borderRadius="md"
px={ 4 } px={ 4 }
py={ 2 } py={ 2 }
...@@ -60,9 +57,8 @@ const Item = ({ data, isLoading }: ItemProps) => { ...@@ -60,9 +57,8 @@ const Item = ({ data, isLoading }: ItemProps) => {
> >
<TruncatedValue <TruncatedValue
value={ data.trait_type } value={ data.trait_type }
fontSize="xs" textStyle="xs"
w="100%" w="100%"
lineHeight={ 4 }
color="text_secondary" color="text_secondary"
fontWeight={ 500 } fontWeight={ 500 }
mb={ 1 } mb={ 1 }
...@@ -100,7 +96,7 @@ const TokenInstanceMetadataInfo = ({ data, isLoading: isLoadingProp }: Props) => ...@@ -100,7 +96,7 @@ const TokenInstanceMetadataInfo = ({ data, isLoading: isLoadingProp }: Props) =>
whiteSpace="normal" whiteSpace="normal"
wordBreak="break-word" wordBreak="break-word"
> >
<Skeleton isLoaded={ !isLoading }> <Skeleton loading={ isLoading }>
{ metadata.name } { metadata.name }
</Skeleton> </Skeleton>
</DetailedInfo.ItemValue> </DetailedInfo.ItemValue>
...@@ -118,7 +114,7 @@ const TokenInstanceMetadataInfo = ({ data, isLoading: isLoadingProp }: Props) => ...@@ -118,7 +114,7 @@ const TokenInstanceMetadataInfo = ({ data, isLoading: isLoadingProp }: Props) =>
whiteSpace="normal" whiteSpace="normal"
wordBreak="break-word" wordBreak="break-word"
> >
<Skeleton isLoaded={ !isLoading }> <Skeleton loading={ isLoading }>
{ metadata.description } { metadata.description }
</Skeleton> </Skeleton>
</DetailedInfo.ItemValue> </DetailedInfo.ItemValue>
......
...@@ -3,9 +3,9 @@ import React from 'react'; ...@@ -3,9 +3,9 @@ import React from 'react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import Skeleton from 'ui/shared/chakra/Skeleton'; import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo'; import * as DetailedInfo from 'ui/shared/DetailedInfo/DetailedInfo';
import LinkInternal from 'ui/shared/links/LinkInternal';
interface Props { interface Props {
hash: string; hash: string;
...@@ -45,13 +45,13 @@ const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => { ...@@ -45,13 +45,13 @@ const TokenInstanceTransfersCount = ({ hash, id, onClick }: Props) => {
Transfers Transfers
</DetailedInfo.ItemLabel> </DetailedInfo.ItemLabel>
<DetailedInfo.ItemValue> <DetailedInfo.ItemValue>
<Skeleton isLoaded={ !transfersCountQuery.isPlaceholderData } display="inline-block"> <Skeleton loading={ transfersCountQuery.isPlaceholderData } display="inline-block">
<LinkInternal <Link
href={ url } href={ url }
onClick={ transfersCountQuery.data.transfers_count > 0 ? onClick : undefined } onClick={ transfersCountQuery.data.transfers_count > 0 ? onClick : undefined }
> >
{ transfersCountQuery.data.transfers_count.toLocaleString() } { transfersCountQuery.data.transfers_count.toLocaleString() }
</LinkInternal> </Link>
</Skeleton> </Skeleton>
</DetailedInfo.ItemValue> </DetailedInfo.ItemValue>
</> </>
......
import { Accordion } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { AccordionRoot } from 'toolkit/chakra/accordion';
import MetadataItemArray from './MetadataItemArray'; import MetadataItemArray from './MetadataItemArray';
import MetadataItemObject from './MetadataItemObject'; import MetadataItemObject from './MetadataItemObject';
import MetadataItemPrimitive from './MetadataItemPrimitive'; import MetadataItemPrimitive from './MetadataItemPrimitive';
...@@ -31,14 +32,33 @@ const MetadataAccordion = ({ data, level = 0 }: Props) => { ...@@ -31,14 +32,33 @@ const MetadataAccordion = ({ data, level = 0 }: Props) => {
case 'string': case 'string':
case 'number': case 'number':
case 'boolean': { case 'boolean': {
return <MetadataItemPrimitive key={ name } name={ name } value={ value } isFlat={ isFlat } level={ level }/>; return (
<MetadataItemPrimitive
key={ name }
name={ name }
value={ value }
isItem
isFlat={ isFlat }
itemValue={ String(value) }
level={ level }
/>
);
} }
case 'object': { case 'object': {
if (value === null) { if (value === null) {
return <MetadataItemPrimitive key={ name } name={ name } value={ value } isFlat={ isFlat } level={ level }/>; return (
<MetadataItemPrimitive
key={ name }
name={ name }
value={ value }
isItem
itemValue={ String(value) }
isFlat={ isFlat }
level={ level }
/>
);
} }
if (Array.isArray(value) && value.length > 0) { if (Array.isArray(value) && value.length > 0) {
return <MetadataItemArray key={ name } name={ name } value={ value } level={ level }/>; return <MetadataItemArray key={ name } name={ name } value={ value } level={ level }/>;
} }
...@@ -49,15 +69,32 @@ const MetadataAccordion = ({ data, level = 0 }: Props) => { ...@@ -49,15 +69,32 @@ const MetadataAccordion = ({ data, level = 0 }: Props) => {
} }
// eslint-disable-next-line no-fallthrough // eslint-disable-next-line no-fallthrough
default: { default: {
return <MetadataItemPrimitive key={ name } name={ name } value={ String(value) } isFlat={ isFlat } level={ level }/>; return (
<MetadataItemPrimitive
key={ name }
name={ name }
value={ String(value) }
isItem
itemValue={ String(value) }
isFlat={ isFlat }
level={ level }
/>
);
} }
} }
}, [ level, isFlat ]); }, [ level, isFlat ]);
const entries = Object.entries(data).sort(sortFields);
return ( return (
<Accordion allowMultiple fontSize="sm" ml={{ base: level === 0 ? 0 : 6, lg: `${ ml }px` }} defaultIndex={ level === 0 ? [ 0 ] : undefined }> <AccordionRoot
{ Object.entries(data).sort(sortFields).map(([ key, value ]) => renderItem(key, value)) } multiple
</Accordion> textStyle="sm"
ml={{ base: level === 0 ? 0 : 6, lg: `${ ml }px` }}
defaultValue={ level === 0 ? entries.map(([ key ]) => key) : undefined }
>
{ entries.map(([ key, value ]) => renderItem(key, value)) }
</AccordionRoot>
); );
}; };
......
import { AccordionItem, chakra } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { AccordionItem } from 'toolkit/chakra/accordion';
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
level?: number; level?: number;
className?: string; className?: string;
isFlat?: boolean; isFlat?: boolean;
value: string;
} }
const MetadataAccordionItem = ({ children, className, level, isFlat }: Props) => { const MetadataAccordionItem = ({ children, className, level, isFlat, value }: Props) => {
return ( return (
<AccordionItem <AccordionItem
value={ value }
className={ className } className={ className }
display="flex" display="flex"
alignItems="flex-start" alignItems="flex-start"
...@@ -18,13 +22,9 @@ const MetadataAccordionItem = ({ children, className, level, isFlat }: Props) => ...@@ -18,13 +22,9 @@ const MetadataAccordionItem = ({ children, className, level, isFlat }: Props) =>
py={ 2 } py={ 2 }
pl={ isFlat ? 0 : 6 } pl={ isFlat ? 0 : 6 }
columnGap={ 3 } columnGap={ 3 }
borderTopWidth="1px"
borderColor="border.divider" borderColor="border.divider"
wordBreak="break-all" wordBreak="break-all"
rowGap={ 1 } rowGap={ 1 }
_last={{
borderBottomWidth: level === 0 ? '1px' : '0px',
}}
_first={{ _first={{
borderTopWidth: level === 0 ? '1px' : '0px', borderTopWidth: level === 0 ? '1px' : '0px',
}} }}
......
import { AccordionButton, AccordionIcon, AccordionPanel, Flex } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { AccordionItemContent, AccordionItemTrigger } from 'toolkit/chakra/accordion';
import MetadataAccordionItem from './MetadataAccordionItem'; import MetadataAccordionItem from './MetadataAccordionItem';
import MetadataAccordionItemTitle from './MetadataAccordionItemTitle'; import MetadataAccordionItemTitle from './MetadataAccordionItemTitle';
import MetadataItemPrimitive from './MetadataItemPrimitive'; import MetadataItemPrimitive from './MetadataItemPrimitive';
...@@ -15,12 +17,13 @@ const MetadataItemArray = ({ name, value, level }: Props) => { ...@@ -15,12 +17,13 @@ const MetadataItemArray = ({ name, value, level }: Props) => {
return ( return (
<MetadataAccordionItem <MetadataAccordionItem
value={ name }
flexDir={{ lg: 'column' }} flexDir={{ lg: 'column' }}
alignItems="stretch" alignItems="stretch"
pl={{ base: 0, lg: 0 }} pl={{ base: 0, lg: 0 }}
py={ 0 } py={ 0 }
> >
<AccordionButton <AccordionItemTrigger
px={ 0 } px={ 0 }
py={ 2 } py={ 2 }
_hover={{ bgColor: 'inherit' }} _hover={{ bgColor: 'inherit' }}
...@@ -30,18 +33,18 @@ const MetadataItemArray = ({ name, value, level }: Props) => { ...@@ -30,18 +33,18 @@ const MetadataItemArray = ({ name, value, level }: Props) => {
borderColor: 'border.divider', borderColor: 'border.divider',
borderBottomWidth: '1px', borderBottomWidth: '1px',
}} }}
indicatorPlacement="start"
> >
<AccordionIcon boxSize={ 6 } p={ 1 }/>
<MetadataAccordionItemTitle name={ name }/> <MetadataAccordionItemTitle name={ name }/>
</AccordionButton> </AccordionItemTrigger>
<AccordionPanel p={ 0 } ml={{ base: 6, lg: level === 0 ? '126px' : 6 }}> <AccordionItemContent p={ 0 } ml={{ base: 6, lg: level === 0 ? '126px' : 6 }}>
{ value.map((item, index) => { { value.map((item, index) => {
const content = (() => { const content = (() => {
switch (typeof item) { switch (typeof item) {
case 'string': case 'string':
case 'number': case 'number':
case 'boolean': { case 'boolean': {
return <MetadataItemPrimitive value={ item } isItem={ false } level={ level }/>; return <MetadataItemPrimitive name={ name } value={ item } level={ level }/>;
} }
case 'object': { case 'object': {
if (item) { if (item) {
...@@ -54,7 +57,6 @@ const MetadataItemArray = ({ name, value, level }: Props) => { ...@@ -54,7 +57,6 @@ const MetadataItemArray = ({ name, value, level }: Props) => {
<MetadataAccordionItemTitle name={ name } fontWeight={ 400 } w={{ base: '90px' }}/> <MetadataAccordionItemTitle name={ name } fontWeight={ 400 } w={{ base: '90px' }}/>
<MetadataItemPrimitive <MetadataItemPrimitive
value={ typeof value === 'object' ? JSON.stringify(value, undefined, 2) : value } value={ typeof value === 'object' ? JSON.stringify(value, undefined, 2) : value }
isItem={ false }
level={ level } level={ level }
/> />
</Flex> </Flex>
...@@ -83,7 +85,7 @@ const MetadataItemArray = ({ name, value, level }: Props) => { ...@@ -83,7 +85,7 @@ const MetadataItemArray = ({ name, value, level }: Props) => {
</Flex> </Flex>
); );
}) } }) }
</AccordionPanel> </AccordionItemContent>
</MetadataAccordionItem> </MetadataAccordionItem>
); );
}; };
......
import { AccordionButton, AccordionIcon, AccordionPanel, Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { AccordionItemContent, AccordionItemTrigger } from 'toolkit/chakra/accordion';
import MetadataAccordion from './MetadataAccordion'; import MetadataAccordion from './MetadataAccordion';
import MetadataAccordionItem from './MetadataAccordionItem'; import MetadataAccordionItem from './MetadataAccordionItem';
import MetadataAccordionItemTitle from './MetadataAccordionItemTitle'; import MetadataAccordionItemTitle from './MetadataAccordionItemTitle';
...@@ -15,7 +17,7 @@ const MetadataItemObject = ({ name, value, level }: Props) => { ...@@ -15,7 +17,7 @@ const MetadataItemObject = ({ name, value, level }: Props) => {
if (level >= 4) { if (level >= 4) {
return ( return (
<MetadataAccordionItem level={ level } isFlat> <MetadataAccordionItem value={ name } level={ level } isFlat>
<MetadataAccordionItemTitle name={ name }/> <MetadataAccordionItemTitle name={ name }/>
<Box whiteSpace="pre-wrap">{ JSON.stringify(value, undefined, 2) }</Box> <Box whiteSpace="pre-wrap">{ JSON.stringify(value, undefined, 2) }</Box>
</MetadataAccordionItem> </MetadataAccordionItem>
...@@ -24,13 +26,14 @@ const MetadataItemObject = ({ name, value, level }: Props) => { ...@@ -24,13 +26,14 @@ const MetadataItemObject = ({ name, value, level }: Props) => {
return ( return (
<MetadataAccordionItem <MetadataAccordionItem
value={ name }
flexDir={{ lg: 'column' }} flexDir={{ lg: 'column' }}
alignItems="stretch" alignItems="stretch"
py={ 0 } py={ 0 }
isFlat isFlat
level={ level } level={ level }
> >
<AccordionButton <AccordionItemTrigger
px={ 0 } px={ 0 }
py={ 2 } py={ 2 }
_hover={{ bgColor: 'inherit' }} _hover={{ bgColor: 'inherit' }}
...@@ -40,13 +43,13 @@ const MetadataItemObject = ({ name, value, level }: Props) => { ...@@ -40,13 +43,13 @@ const MetadataItemObject = ({ name, value, level }: Props) => {
borderColor: 'border.divider', borderColor: 'border.divider',
borderBottomWidth: '1px', borderBottomWidth: '1px',
}} }}
indicatorPlacement="start"
> >
<AccordionIcon boxSize={ 6 } p={ 1 }/>
<MetadataAccordionItemTitle name={ name }/> <MetadataAccordionItemTitle name={ name }/>
</AccordionButton> </AccordionItemTrigger>
<AccordionPanel p={ 0 }> <AccordionItemContent p={ 0 }>
<MetadataAccordion data={ value as Record<string, unknown> } level={ level + 1 }/> <MetadataAccordion data={ value as Record<string, unknown> } level={ level + 1 }/>
</AccordionPanel> </AccordionItemContent>
</MetadataAccordionItem> </MetadataAccordionItem>
); );
}; };
......
...@@ -3,29 +3,33 @@ import React from 'react'; ...@@ -3,29 +3,33 @@ import React from 'react';
import type { Primitive } from 'react-hook-form'; import type { Primitive } from 'react-hook-form';
import urlParser from 'lib/token/metadata/urlParser'; import urlParser from 'lib/token/metadata/urlParser';
import LinkExternal from 'ui/shared/links/LinkExternal'; import { Link } from 'toolkit/chakra/link';
import MetadataAccordionItem from './MetadataAccordionItem'; import MetadataAccordionItem from './MetadataAccordionItem';
import MetadataAccordionItemTitle from './MetadataAccordionItemTitle'; import MetadataAccordionItemTitle from './MetadataAccordionItemTitle';
interface Props { interface PropsItem {
name?: string; itemValue: string;
value: Primitive; isItem: true;
isItem?: boolean;
isFlat?: boolean; isFlat?: boolean;
level: number;
} }
const MetadataItemPrimitive = ({ name, value, isItem = true, isFlat, level }: Props) => { interface PropsBox {}
type Props = {
name?: string;
value: Primitive;
level: number;
} & (PropsItem | PropsBox);
const Component = isItem ? MetadataAccordionItem : Box; const MetadataItemPrimitive = ({ name, value, level, ...rest }: Props) => {
const content = (() => { const content = (() => {
switch (typeof value) { switch (typeof value) {
case 'string': { case 'string': {
const url = urlParser(value); const url = urlParser(value);
if (url) { if (url) {
return <LinkExternal href={ url.toString() }>{ value }</LinkExternal>; return <Link external href={ url.toString() }>{ value }</Link>;
} }
if (value === '') { if (value === '') {
return <div>&quot;&quot;</div>; return <div>&quot;&quot;</div>;
...@@ -38,11 +42,20 @@ const MetadataItemPrimitive = ({ name, value, isItem = true, isFlat, level }: Pr ...@@ -38,11 +42,20 @@ const MetadataItemPrimitive = ({ name, value, isItem = true, isFlat, level }: Pr
} }
})(); })();
if ('isItem' in rest) {
return (
<MetadataAccordionItem value={ rest.itemValue } level={ level } isFlat={ rest.isFlat }>
{ name && <MetadataAccordionItemTitle name={ name }/> }
{ content }
</MetadataAccordionItem>
);
}
return ( return (
<Component level={ level } { ...(isItem ? { isFlat } : {}) }> <Box>
{ name && <MetadataAccordionItemTitle name={ name }/> } { name && <MetadataAccordionItemTitle name={ name }/> }
{ content } { content }
</Component> </Box>
); );
}; };
......
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