Commit f566c916 authored by tom's avatar tom

token instance page

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