Commit 730d6ddb authored by tom's avatar tom

block details tab

parent 34f281d3
......@@ -9,18 +9,21 @@ NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_CELO_ENABLED=true
NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK=26369280
# Instance ENVs
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=celo-alfajores.blockscout.com
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_CELO_ENABLED=true
NEXT_PUBLIC_CELO_L2_UPGRADE_BLOCK=26369280
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/celo-alfajores-testnet.json
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
NEXT_PUBLIC_GAS_TRACKER_ENABLED=false
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x9767ce30754afad2a3279b9df2d13257f467c3dad4e0e601271e66d16dfd1641
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(252, 255, 82, 1)
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=rgba(0, 0, 0, 1)
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(252, 255, 82, 1)'],'text_color':['rgba(0, 0, 0, 1)']}
NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_MARKETPLACE_ENABLED=false
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
......@@ -32,12 +35,14 @@ NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/front
NEXT_PUBLIC_NETWORK_ID=44787
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/celo-logo-light.svg
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/celo-logo-dark.svg
NEXT_PUBLIC_NETWORK_MULTIPLE_GAS_CURRENCIES=true
NEXT_PUBLIC_NETWORK_NAME=Celo Alfajores
NEXT_PUBLIC_NETWORK_RPC_URL=https://alfajores-forno.celo-testnet.org
NEXT_PUBLIC_NETWORK_SHORT_NAME=Alfajores
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/celo.png
NEXT_PUBLIC_STATS_API_HOST=https://stats-alfajores-testnet.k8s-prod-1.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VIEWS_BLOCK_HIDDEN_FIELDS=['burnt_fees']
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
\ No newline at end of file
NEXT_PUBLIC_HOMEPAGE_STATS=['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker','current_epoch']
\ No newline at end of file
......@@ -26,10 +26,11 @@ const RESTRICTED_MODULES = {
{ name: '@metamask/post-message-stream', message: 'Please lazy-load @metamask/post-message-stream or use useProvider hook instead' },
{ name: 'playwright/TestApp', message: 'Please use render() fixture from test() function of playwright/lib module' },
{ name: 'ui/shared/chakra/Skeleton', message: 'Please use Skeleton component from toolkit/chakra instead' },
{ name: 'ui/shared/Tabs/RoutedTabs', message: 'Please use RoutedTabs component from toolkit/components/RoutedTabs instead' },
{
name: '@chakra-ui/react',
importNames: [
'Menu', 'useToast', 'useDisclosure', 'useClipboard', 'Tooltip', 'Skeleton', 'IconButton', 'Button', 'Link',
'Menu', 'useToast', 'useDisclosure', 'useClipboard', 'Tooltip', 'Skeleton', 'IconButton', 'Button', 'Link', 'Tag',
'Image', 'Popover', 'PopoverTrigger', 'PopoverContent', 'PopoverBody', 'PopoverFooter',
'DrawerRoot', 'DrawerBody', 'DrawerContent', 'DrawerOverlay', 'DrawerBackdrop', 'DrawerTrigger', 'Drawer',
'Alert', 'AlertIcon', 'AlertTitle', 'AlertDescription',
......
......@@ -5,12 +5,12 @@ import React from 'react';
import type { Props } from 'nextjs/getServerSideProps';
import PageNextJs from 'nextjs/PageNextJs';
// const Block = dynamic(() => import('ui/pages/Block'), { ssr: false });
const Block = dynamic(() => import('ui/pages/Block'), { ssr: false });
const Page: NextPage<Props> = (props: Props) => {
return (
<PageNextJs pathname="/block/[height_or_hash]" query={ props.query }>
{ /* <Block/> */ }
<Block/>
</PageNextJs>
);
};
......
......@@ -4,6 +4,7 @@ import * as React from 'react';
import IconSvg from 'ui/shared/IconSvg';
import { CloseButton } from './close-button';
import { Skeleton } from './skeleton';
export interface AlertProps extends Omit<ChakraAlert.RootProps, 'title'> {
startElement?: React.ReactNode;
......@@ -12,6 +13,7 @@ export interface AlertProps extends Omit<ChakraAlert.RootProps, 'title'> {
icon?: React.ReactElement;
closable?: boolean;
onClose?: () => void;
loading?: boolean;
}
export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
......@@ -24,12 +26,14 @@ export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
onClose,
startElement,
endElement,
loading,
...rest
} = props;
const defaultIcon = <IconSvg name="info_filled"/>;
const defaultIcon = <IconSvg name="info_filled" w="100%" h="100%"/>;
return (
<Skeleton loading={ loading } asChild>
<ChakraAlert.Root ref={ ref } { ...rest }>
{ startElement !== undefined || icon !== undefined ? startElement : <ChakraAlert.Indicator>{ icon || defaultIcon }</ChakraAlert.Indicator> }
{ children ? (
......@@ -51,6 +55,7 @@ export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
/>
) }
</ChakraAlert.Root>
</Skeleton>
);
},
);
......@@ -14,10 +14,10 @@ export interface BadgeProps extends ChakraBadgeProps {
export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
function Badge(props, ref) {
const { loading, iconStart, children, ...rest } = props;
const { loading, iconStart, children, asChild = true, ...rest } = props;
return (
<Skeleton loading={ loading }>
<Skeleton loading={ loading } asChild={ asChild }>
<ChakraBadge ref={ ref } display="flex" alignItems="center" gap={ 1 } { ...rest }>
{ iconStart && <IconSvg name={ iconStart } boxSize="10px"/> }
{ children }
......
......@@ -5,12 +5,14 @@ import { Skeleton } from './skeleton';
export interface IconButtonProps extends ButtonProps {}
// TODO @tom2drum variants for icon buttons: prev-next, top-bar, copy-to-clipboard
export const IconButton = React.forwardRef<HTMLDivElement, IconButtonProps>(
function IconButton(props, ref) {
const { loading, size, variant = 'plain', ...rest } = props;
return (
<Skeleton loading={ loading } ref={ ref }>
<Skeleton loading={ loading } ref={ ref } asChild>
<Button
display="inline-flex"
px="0"
......
......@@ -12,6 +12,7 @@ export interface TooltipProps extends ChakraTooltip.RootProps {
content: React.ReactNode;
contentProps?: ChakraTooltip.ContentProps;
disabled?: boolean;
disableOnMobile?: boolean;
}
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
......@@ -23,6 +24,7 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
selected,
children,
disabled,
disableOnMobile,
portalled = true,
content,
contentProps,
......@@ -36,6 +38,7 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
const [ open, setOpen ] = React.useState(defaultOpen);
const isMobile = useIsMobile();
// TODO @tom2drum merge refs
const triggerRef = useClickAway<HTMLButtonElement>(() => setOpen(false));
const handleOpenChange = React.useCallback((details: { open: boolean }) => {
......@@ -47,7 +50,7 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
setOpen((prev) => !prev);
}, [ ]);
if (disabled) return children;
if (disabled || (disableOnMobile && isMobile)) return children;
const defaultShowArrow = visual === 'popover' ? false : true;
const showArrow = showArrowProp !== undefined ? showArrowProp : defaultShowArrow;
......
import React from 'react';
import { scroller, Element } from 'react-scroll';
import type { LinkProps } from 'toolkit/chakra/link';
import { Link } from 'toolkit/chakra/link';
interface Props {
interface Props extends LinkProps {
children: React.ReactNode;
id?: string;
onClick?: () => void;
isLoading?: boolean;
}
const ID = 'CutLink';
const CutLink = (props: Props) => {
const { children, id = ID, onClick, isLoading } = props;
const { children, id = ID, onClick, ...rest } = props;
const [ isExpanded, setIsExpanded ] = React.useState(false);
......@@ -27,19 +26,20 @@ const CutLink = (props: Props) => {
onClick?.();
}, [ id, onClick ]);
const text = isExpanded ? 'Hide details' : 'View details';
return (
<>
<Element name={ id }>
<Link
textStyle="sm"
textDecorationLine="underline"
textDecorationStyle="dashed"
onClick={ handleClick }
loading={ isLoading }
asChild
{ ...rest }
>
{ isExpanded ? 'Hide details' : 'View details' }
<Element name={ id }>{ text }</Element>
</Link>
</Element>
{ isExpanded && children }
</>
);
......
import { Flex, chakra, Box, useColorModeValue } from '@chakra-ui/react';
import { Flex, chakra, Box } from '@chakra-ui/react';
import React from 'react';
import type { RoutedTab } from '../Tabs/types';
import type { TabItemRegular } from '../AdaptiveTabs/types';
import Skeleton from 'ui/shared/chakra/Skeleton';
import useTabIndexFromQuery from 'ui/shared/Tabs/useTabIndexFromQuery';
import { Skeleton } from 'toolkit/chakra/skeleton';
import type { TabsProps } from 'toolkit/chakra/tabs';
type TabSize = 'sm' | 'md';
import useActiveTabFromQuery from './useActiveTabFromQuery';
const SkeletonTabText = ({ size, title }: { size: TabSize; title: RoutedTab['title'] }) => (
const SkeletonTabText = ({ size, title }: { size: TabsProps['size']; title: TabItemRegular['title'] }) => (
<Skeleton
borderRadius="base"
borderWidth={ size === 'sm' ? '2px' : 0 }
......@@ -22,18 +22,19 @@ const SkeletonTabText = ({ size, title }: { size: TabSize; title: RoutedTab['tit
interface Props {
className?: string;
tabs: Array<RoutedTab>;
tabs: Array<TabItemRegular>;
size?: 'sm' | 'md';
}
const TabsSkeleton = ({ className, tabs, size = 'md' }: Props) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const tabIndex = useTabIndexFromQuery(tabs || []);
const RoutedTabsSkeleton = ({ className, tabs, size = 'md' }: Props) => {
const activeTab = useActiveTabFromQuery(tabs);
if (tabs.length === 1) {
return null;
}
const tabIndex = activeTab ? tabs.findIndex((tab) => tab.id === activeTab.id) : 0;
return (
<Flex className={ className } my={ 8 } alignItems="center" overflow="hidden">
{ tabs.slice(0, tabIndex).map(({ title, id }) => (
......@@ -44,7 +45,13 @@ const TabsSkeleton = ({ className, tabs, size = 'md' }: Props) => {
/>
)) }
{ tabs.slice(tabIndex, tabIndex + 1).map(({ title, id }) => (
<Box key={ id.toString() } bgColor={ bgColor } py={ size === 'sm' ? 1 : 2 } borderRadius="base" flexShrink={ 0 }>
<Box
key={ id.toString() }
bgColor={{ _light: 'blackAlpha.50', _dark: 'whiteAlpha.50' }}
py={ size === 'sm' ? 1 : 2 }
borderRadius="base"
flexShrink={ 0 }
>
<SkeletonTabText
key={ id.toString() }
title={ title }
......@@ -63,4 +70,4 @@ const TabsSkeleton = ({ className, tabs, size = 'md' }: Props) => {
);
};
export default chakra(TabsSkeleton);
export default chakra(RoutedTabsSkeleton);
......@@ -27,6 +27,7 @@ export const recipe = defineSlotRecipe({
height: '5',
_icon: { boxSize: 'full' },
color: 'alert.fg',
'--layer-bg': 'transparent',
},
content: {
display: 'flex',
......@@ -35,24 +36,48 @@ export const recipe = defineSlotRecipe({
},
variants: {
visual: {
status: {
info: {
root: { bg: 'alert.bg.info', color: 'alert.fg' },
root: {
bg: 'alert.bg.info',
'--layer-bg': '{colors.alert.bg.info}',
color: 'alert.fg',
},
},
warning: {
root: { bg: 'alert.bg.warning', color: 'alert.fg' },
root: {
bg: 'alert.bg.warning',
'--layer-bg': '{colors.alert.bg.warning}',
color: 'alert.fg',
},
},
warning_table: {
root: { bg: 'alert.bg.warning_table', color: 'alert.fg' },
root: {
bg: 'alert.bg.warning_table',
'--layer-bg': '{colors.alert.bg.warning_table}',
color: 'alert.fg',
},
},
success: {
root: { bg: 'alert.bg.success', color: 'alert.fg' },
root: {
bg: 'alert.bg.success',
'--layer-bg': '{colors.alert.bg.success}',
color: 'alert.fg',
},
},
error: {
root: { bg: 'alert.bg.error', color: 'alert.fg' },
root: {
bg: 'alert.bg.error',
'--layer-bg': '{colors.alert.bg.error}',
color: 'alert.fg',
},
},
neutral: {
root: { bg: 'alert.bg.neutral', color: 'alert.fg' },
root: {
bg: 'alert.bg.neutral',
'--layer-bg': '{colors.alert.bg.neutral}',
color: 'alert.fg',
},
},
},
......@@ -92,7 +117,7 @@ export const recipe = defineSlotRecipe({
},
defaultVariants: {
visual: 'neutral',
status: 'neutral',
size: 'md',
inline: true,
},
......
......@@ -17,46 +17,57 @@ export const recipe = defineRecipe({
colorPalette: {
gray: {
bg: 'badge.gray.bg',
'--layer-bg': '{colors.badge.gray.bg}',
color: 'badge.gray.fg',
},
green: {
bg: 'badge.green.bg',
'--layer-bg': '{colors.badge.green.bg}',
color: 'badge.green.fg',
},
red: {
bg: 'badge.red.bg',
'--layer-bg': '{colors.badge.red.bg}',
color: 'badge.red.fg',
},
purple: {
bg: 'badge.purple.bg',
'--layer-bg': '{colors.badge.purple.bg}',
color: 'badge.purple.fg',
},
orange: {
bg: 'badge.orange.bg',
'--layer-bg': '{colors.badge.orange.bg}',
color: 'badge.orange.fg',
},
blue: {
bg: 'badge.blue.bg',
'--layer-bg': '{colors.badge.blue.bg}',
color: 'badge.blue.fg',
},
yellow: {
bg: 'badge.yellow.bg',
'--layer-bg': '{colors.badge.yellow.bg}',
color: 'badge.yellow.fg',
},
teal: {
bg: 'badge.teal.bg',
'--layer-bg': '{colors.badge.teal.bg}',
color: 'badge.teal.fg',
},
cyan: {
bg: 'badge.cyan.bg',
'--layer-bg': '{colors.badge.cyan.bg}',
color: 'badge.cyan.fg',
},
purple_alt: {
bg: 'badge.purple_alt.bg',
'--layer-bg': '{colors.badge.purple_alt.bg}',
color: 'badge.purple_alt.fg',
},
blue_alt: {
bg: 'badge.blue_alt.bg',
'--layer-bg': '{colors.badge.blue_alt.bg}',
color: 'badge.blue_alt.fg',
},
},
......
import { chakra, Tooltip, Hide, Flex } from '@chakra-ui/react';
import { chakra, Flex } from '@chakra-ui/react';
import React from 'react';
import type { CsvExportParams } from 'types/client/address';
......@@ -8,9 +8,10 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import useIsInitialLoading from 'lib/hooks/useIsInitialLoading';
import useIsMobile from 'lib/hooks/useIsMobile';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/links/LinkInternal';
interface Props {
address: string;
......@@ -30,27 +31,23 @@ const AddressCsvExportLink = ({ className, address, params, isLoading }: Props)
if (isInitialLoading) {
return (
<Flex className={ className } flexShrink={ 0 } alignItems="center">
<Skeleton boxSize={{ base: '32px', lg: 6 }} borderRadius="base"/>
<Hide ssr={ false } below="lg">
<Skeleton w="112px" h={ 6 } ml={ 1 }/>
</Hide>
<Skeleton boxSize={{ base: 8, lg: 6 }}/>
<Skeleton loading hideBelow="lg" w="112px" h={ 6 } ml={ 1 }/>
</Flex>
);
}
return (
<Tooltip isDisabled={ !isMobile } label="Download CSV">
<LinkInternal
<Tooltip disabled={ !isMobile } content="Download CSV">
<Link
className={ className }
display="inline-flex"
alignItems="center"
whiteSpace="nowrap"
href={ route({ pathname: '/csv-export', query: { ...params, address } }) }
flexShrink={ 0 }
>
<IconSvg name="files/csv" boxSize={{ base: '30px', lg: 6 }}/>
<Hide ssr={ false } below="lg"><chakra.span ml={ 1 }>Download CSV</chakra.span></Hide>
</LinkInternal>
<chakra.span ml={ 1 } hideBelow="lg">Download CSV</chakra.span>
</Link>
</Tooltip>
);
};
......
import { Tag, Tooltip, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import LinkInternal from 'ui/shared/links/LinkInternal';
import { Link } from 'toolkit/chakra/link';
import { Tag } from 'toolkit/chakra/tag';
import { Tooltip } from 'toolkit/chakra/tooltip';
import type { BlockQuery } from './useBlockQuery';
......@@ -13,9 +14,6 @@ interface Props {
}
const BlockCeloEpochTag = ({ blockQuery }: Props) => {
// have to implement controlled tooltip because of the issue - https://github.com/chakra-ui/chakra-ui/issues/7107
const { isOpen, onOpen, onToggle, onClose } = useDisclosure();
if (!blockQuery.data?.celo) {
return null;
}
......@@ -26,28 +24,19 @@ const BlockCeloEpochTag = ({ blockQuery }: Props) => {
blockQuery.data.celo.epoch_number * celoConfig.BLOCKS_PER_EPOCH :
undefined;
const tag = (
<Tag
colorScheme={ epochBlockNumber ? 'gray-blue' : 'gray' }
onClick={ epochBlockNumber ? undefined : onToggle }
onMouseEnter={ onOpen }
onMouseLeave={ onClose }
>
Epoch #{ blockQuery.data.celo.epoch_number }
</Tag>
// TODO @tom2drum revise tag color scheme
<Tag colorScheme={ epochBlockNumber ? 'gray-blue' : 'gray' } >Epoch #{ blockQuery.data.celo.epoch_number }</Tag>
);
const content = epochBlockNumber ? (
<LinkInternal href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: String(epochBlockNumber) } }) }>
<Link href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: String(epochBlockNumber) } }) }>
{ tag }
</LinkInternal>
</Link>
) : tag;
return (
<Tooltip
label="Displays the epoch this block belongs to before the epoch is finalized"
maxW="280px"
textAlign="center"
isOpen={ isOpen }
onClose={ onClose }
key="epoch-tag-before-finalized"
content="Displays the epoch this block belongs to before the epoch is finalized"
>
{ content }
</Tooltip>
......@@ -56,15 +45,10 @@ const BlockCeloEpochTag = ({ blockQuery }: Props) => {
return (
<Tooltip
label="Displays the epoch finalized by this block"
maxW="280px"
textAlign="center"
isOpen={ isOpen }
onClose={ onClose }
key="epoch-tag"
content="Displays the epoch finalized by this block"
>
<Tag bgColor="celo" color="blackAlpha.800" onClick={ onToggle } onMouseEnter={ onOpen } onMouseLeave={ onClose }>
Finalized epoch #{ blockQuery.data.celo.epoch_number }
</Tag>
<Tag bgColor="celo" color="blackAlpha.800" > Finalized epoch #{ blockQuery.data.celo.epoch_number } </Tag>
</Tooltip>
);
};
......
import { Grid, GridItem, Text, Link, Box, Tooltip } from '@chakra-ui/react';
import { Grid, GridItem, Text, Box } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { capitalize } from 'es-toolkit';
import { useRouter } from 'next/router';
import React from 'react';
import { scroller, Element } from 'react-scroll';
import { ZKSYNC_L2_TX_BATCH_STATUSES } from 'types/api/zkSyncL2';
......@@ -18,9 +17,12 @@ import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import * as arbitrum from 'lib/rollups/arbitrum';
import getQueryParamString from 'lib/router/getQueryParamString';
import { currencyUnits } from 'lib/units';
import { Link } from 'toolkit/chakra/link';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip';
import CutLink from 'toolkit/components/CutLink/CutLink';
import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
import Skeleton from 'ui/shared/chakra/Skeleton';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
......@@ -31,7 +33,6 @@ import BlockEntityL1 from 'ui/shared/entities/block/BlockEntityL1';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/links/LinkInternal';
import PrevNext from 'ui/shared/PrevNext';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
import StatusTag from 'ui/shared/statusTag/StatusTag';
......@@ -51,20 +52,11 @@ interface Props {
const rollupFeature = config.features.rollup;
const BlockDetails = ({ query }: Props) => {
const [ isExpanded, setIsExpanded ] = React.useState(false);
const router = useRouter();
const heightOrHash = getQueryParamString(router.query.height_or_hash);
const { data, isPlaceholderData } = query;
const handleCutClick = React.useCallback(() => {
setIsExpanded((flag) => !flag);
scroller.scrollTo('BlockDetails__cutLink', {
duration: 500,
smooth: true,
});
}, []);
const handlePrevNextClick = React.useCallback((direction: 'prev' | 'next') => {
if (!data) {
return;
......@@ -90,18 +82,18 @@ const BlockDetails = ({ query }: Props) => {
}
if (isPlaceholderData) {
return <Skeleton w="525px" h="20px"/>;
return <Skeleton loading w="525px" h="20px"/>;
}
return (
<Text variant="secondary" whiteSpace="break-spaces">
<Tooltip label="Static block reward">
<Text color="text.secondary" whiteSpace="break-spaces">
<Tooltip content="Static block reward">
<span>{ staticReward.dividedBy(WEI).toFixed() }</span>
</Tooltip>
{ !txFees.isEqualTo(ZERO) && (
<>
{ space }+{ space }
<Tooltip label="Txn fees">
<Tooltip content="Txn fees">
<span>{ txFees.dividedBy(WEI).toFixed() }</span>
</Tooltip>
</>
......@@ -109,7 +101,7 @@ const BlockDetails = ({ query }: Props) => {
{ !burntFees.isEqualTo(ZERO) && (
<>
{ space }-{ space }
<Tooltip label="Burnt fees">
<Tooltip content="Burnt fees">
<span>{ burntFees.dividedBy(WEI).toFixed() }</span>
</Tooltip>
</>
......@@ -122,17 +114,17 @@ const BlockDetails = ({ query }: Props) => {
const txsNum = (() => {
const blockTxsNum = (
<LinkInternal href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash, tab: 'txs' } }) }>
<Link href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash, tab: 'txs' } }) }>
{ data.transaction_count } txn{ data.transaction_count === 1 ? '' : 's' }
</LinkInternal>
</Link>
);
const blockBlobTxsNum = (config.features.dataAvailability.isEnabled && data.blob_transaction_count) ? (
<>
<span> including </span>
<LinkInternal href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash, tab: 'blob_txs' } }) }>
<Link href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash, tab: 'blob_txs' } }) }>
{ data.blob_transaction_count } blob txn{ data.blob_transaction_count === 1 ? '' : 's' }
</LinkInternal>
</Link>
</>
) : null;
......@@ -170,7 +162,7 @@ const BlockDetails = ({ query }: Props) => {
{ blockTypeLabel } height
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
<Skeleton loading={ isPlaceholderData }>
{ data.height }
</Skeleton>
{ data.height === 0 && <Text whiteSpace="pre"> - Genesis Block</Text> }
......@@ -209,7 +201,7 @@ const BlockDetails = ({ query }: Props) => {
<DetailsInfoItem.Value>
{ data.arbitrum.batch_number ?
<BatchEntityL2 isLoading={ isPlaceholderData } number={ data.arbitrum.batch_number }/> :
<Skeleton isLoaded={ !isPlaceholderData }>Pending</Skeleton> }
<Skeleton loading={ isPlaceholderData }>Pending</Skeleton> }
</DetailsInfoItem.Value>
</>
) }
......@@ -225,7 +217,7 @@ const BlockDetails = ({ query }: Props) => {
<DetailsInfoItem.Value columnGap={ 3 }>
{ data.optimism.internal_id ?
<BatchEntityL2 isLoading={ isPlaceholderData } number={ data.optimism.internal_id }/> :
<Skeleton isLoaded={ !isPlaceholderData }>Pending</Skeleton> }
<Skeleton loading={ isPlaceholderData }>Pending</Skeleton> }
{ data.optimism.batch_data_container && (
<OptimisticL2TxnBatchDA
container={ data.optimism.batch_data_container }
......@@ -243,7 +235,7 @@ const BlockDetails = ({ query }: Props) => {
Size
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
<Skeleton loading={ isPlaceholderData }>
{ data.size.toLocaleString() }
</Skeleton>
</DetailsInfoItem.Value>
......@@ -265,7 +257,7 @@ const BlockDetails = ({ query }: Props) => {
Transactions
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
<Skeleton loading={ isPlaceholderData }>
{ txsNum }
</Skeleton>
</DetailsInfoItem.Value>
......@@ -279,10 +271,10 @@ const BlockDetails = ({ query }: Props) => {
Withdrawals
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
<LinkInternal href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash, tab: 'withdrawals' } }) }>
<Skeleton loading={ isPlaceholderData }>
<Link href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash, tab: 'withdrawals' } }) }>
{ data.withdrawals_count } withdrawal{ data.withdrawals_count === 1 ? '' : 's' }
</LinkInternal>
</Link>
</Skeleton>
</DetailsInfoItem.Value>
</>
......@@ -299,7 +291,7 @@ const BlockDetails = ({ query }: Props) => {
<DetailsInfoItem.Value>
{ data.zksync.batch_number ?
<BatchEntityL2 isLoading={ isPlaceholderData } number={ data.zksync.batch_number }/> :
<Skeleton isLoaded={ !isPlaceholderData }>Pending</Skeleton> }
<Skeleton loading={ isPlaceholderData }>Pending</Skeleton> }
</DetailsInfoItem.Value>
</>
) }
......@@ -393,7 +385,7 @@ const BlockDetails = ({ query }: Props) => {
Block reward
</DetailsInfoItem.Label>
<DetailsInfoItem.Value columnGap={ 1 }>
<Skeleton isLoaded={ !isPlaceholderData }>
<Skeleton loading={ isPlaceholderData }>
{ totalReward.dividedBy(WEI).toFixed() } { currencyUnits.ether }
</Skeleton>
{ rewardBreakDown }
......@@ -426,7 +418,7 @@ const BlockDetails = ({ query }: Props) => {
View
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
<Skeleton loading={ isPlaceholderData }>
{ data.zilliqa.view }
</Skeleton>
</DetailsInfoItem.Value>
......@@ -444,15 +436,15 @@ const BlockDetails = ({ query }: Props) => {
Gas used
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
<Skeleton loading={ isPlaceholderData }>
{ BigNumber(data.gas_used || 0).toFormat() }
</Skeleton>
<BlockGasUsed
gasUsed={ data.gas_used }
gasUsed={ data.gas_used || undefined }
gasLimit={ data.gas_limit }
isLoading={ isPlaceholderData }
ml={ 4 }
gasTarget={ data.gas_target_percentage }
gasTarget={ data.gas_target_percentage || undefined }
/>
</DetailsInfoItem.Value>
......@@ -463,7 +455,7 @@ const BlockDetails = ({ query }: Props) => {
Gas limit
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
<Skeleton loading={ isPlaceholderData }>
{ BigNumber(data.gas_limit).toFormat() }
</Skeleton>
</DetailsInfoItem.Value>
......@@ -477,7 +469,7 @@ const BlockDetails = ({ query }: Props) => {
Minimum gas price
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
<Skeleton loading={ isPlaceholderData }>
{ BigNumber(data.minimum_gas_price).dividedBy(GWEI).toFormat() } { currencyUnits.gwei }
</Skeleton>
</DetailsInfoItem.Value>
......@@ -494,11 +486,11 @@ const BlockDetails = ({ query }: Props) => {
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ isPlaceholderData ? (
<Skeleton isLoaded={ !isPlaceholderData } h="20px" maxW="380px" w="100%"/>
<Skeleton loading={ isPlaceholderData } h="20px" maxW="380px" w="100%"/>
) : (
<>
<Text>{ BigNumber(data.base_fee_per_gas).dividedBy(WEI).toFixed() } { currencyUnits.ether } </Text>
<Text variant="secondary" whiteSpace="pre">
<Text color="text.secondary" whiteSpace="pre">
{ space }({ BigNumber(data.base_fee_per_gas).dividedBy(WEI_IN_GWEI).toFixed() } { currencyUnits.gwei })
</Text>
</>
......@@ -520,11 +512,11 @@ const BlockDetails = ({ query }: Props) => {
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<IconSvg name="flame" boxSize={ 5 } color="gray.500" isLoading={ isPlaceholderData }/>
<Skeleton isLoaded={ !isPlaceholderData } ml={ 2 }>
<Skeleton loading={ isPlaceholderData } ml={ 2 }>
{ burntFees.dividedBy(WEI).toFixed() } { currencyUnits.ether }
</Skeleton>
{ !txFees.isEqualTo(ZERO) && (
<Tooltip label="Burnt fees / Txn fees * 100%">
<Tooltip content="Burnt fees / Txn fees * 100%">
<Box>
<Utilization
ml={ 4 }
......@@ -547,32 +539,15 @@ const BlockDetails = ({ query }: Props) => {
Priority fee / Tip
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
<Skeleton loading={ isPlaceholderData }>
{ BigNumber(data.priority_fee).dividedBy(WEI).toFixed() } { currencyUnits.ether }
</Skeleton>
</DetailsInfoItem.Value>
</>
) }
{ /* CUT */ }
<GridItem colSpan={{ base: undefined, lg: 2 }}>
<Element name="BlockDetails__cutLink">
<Skeleton isLoaded={ !isPlaceholderData } mt={ 6 } display="inline-block">
<Link
fontSize="sm"
textDecorationLine="underline"
textDecorationStyle="dashed"
onClick={ handleCutClick }
>
{ isExpanded ? 'Hide details' : 'View details' }
</Link>
</Skeleton>
</Element>
</GridItem>
{ /* ADDITIONAL INFO */ }
{ isExpanded && !isPlaceholderData && (
<>
<CutLink loading={ isPlaceholderData } mt={ 6 } gridColumn={{ base: undefined, lg: '1 / 3' }}>
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>
{ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && data.zksync &&
......@@ -698,7 +673,7 @@ const BlockDetails = ({ query }: Props) => {
Parent hash
</DetailsInfoItem.Label>
<DetailsInfoItem.Value flexWrap="nowrap">
<LinkInternal
<Link
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: String(data.height - 1) } }) }
overflow="hidden"
whiteSpace="nowrap"
......@@ -706,7 +681,7 @@ const BlockDetails = ({ query }: Props) => {
<HashStringShortenDynamic
hash={ data.parent_hash }
/>
</LinkInternal>
</Link>
<CopyToClipboard text={ data.parent_hash }/>
</DetailsInfoItem.Value>
</>
......@@ -771,8 +746,8 @@ const BlockDetails = ({ query }: Props) => {
) }
</>
) }
</>
) }
</CutLink>
</Grid>
);
};
......
import { Box, Flex, Link } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -7,6 +7,7 @@ import type { BlockBaseFeeCelo } from 'types/api/block';
import type { TokenInfo } from 'types/api/token';
import { WEI, ZERO_ADDRESS } from 'lib/consts';
import { Link } from 'toolkit/chakra/link';
import AddressFromTo from 'ui/shared/address/AddressFromTo';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
......@@ -50,7 +51,7 @@ const BlockDetailsBaseFeeCelo = ({ data }: Props) => {
const totalFeeLabel = (
<Box whiteSpace="pre-wrap">
<span>The FeeHandler regularly burns 80% of its tokens. Non-CELO tokens are swapped to CELO beforehand. The remaining 20% are sent to the </span>
<Link isExternal href="https://www.ultragreen.money">Green Fund</Link>
<Link external href="https://www.ultragreen.money">Green Fund</Link>
<span>.</span>
</Box>
);
......@@ -65,10 +66,7 @@ const BlockDetailsBaseFeeCelo = ({ data }: Props) => {
<DetailsInfoItem.Value>
<AddressEntity address={ data.recipient }/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
hint={ totalFeeLabel }
type="popover"
>
<DetailsInfoItem.Label hint={ totalFeeLabel }>
Base fee total
</DetailsInfoItem.Label>
<DetailsInfoItem.Value display="block">
......
import { Text, Tooltip } from '@chakra-ui/react';
import { Text } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
......@@ -7,6 +7,7 @@ import type { Block } from 'types/api/block';
import { WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import { space } from 'lib/html-entities';
import { currencyUnits } from 'lib/units';
import { Tooltip } from 'toolkit/chakra/tooltip';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsInfoItemDivider from 'ui/shared/DetailsInfoItemDivider';
import IconSvg from 'ui/shared/IconSvg';
......@@ -42,7 +43,7 @@ const BlockDetailsBlobInfo = ({ data }: Props) => {
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Text>{ BigNumber(data.blob_gas_price).dividedBy(WEI).toFixed() } { currencyUnits.ether } </Text>
<Text variant="secondary" whiteSpace="pre">
<Text color="text.secondary" whiteSpace="pre">
{ space }({ BigNumber(data.blob_gas_price).dividedBy(WEI_IN_GWEI).toFixed() } { currencyUnits.gwei })
</Text>
</DetailsInfoItem.Value>
......@@ -71,7 +72,7 @@ const BlockDetailsBlobInfo = ({ data }: Props) => {
<IconSvg name="flame" boxSize={ 5 } color="gray.500" mr={ 2 }/>
{ burntBlobFees.dividedBy(WEI).toFixed() } { currencyUnits.ether }
{ !blobFees.isEqualTo(ZERO) && (
<Tooltip label="Blob burnt fees / Txn fees * 100%">
<Tooltip content="Blob burnt fees / Txn fees * 100%">
<div>
<Utilization ml={ 4 } value={ burntBlobFees.dividedBy(blobFees).toNumber() }/>
</div>
......@@ -89,7 +90,7 @@ const BlockDetailsBlobInfo = ({ data }: Props) => {
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Text>{ BigNumber(data.excess_blob_gas).dividedBy(WEI).toFixed() } { currencyUnits.ether } </Text>
<Text variant="secondary" whiteSpace="pre">
<Text color="text.secondary" whiteSpace="pre">
{ space }({ BigNumber(data.excess_blob_gas).dividedBy(WEI_IN_GWEI).toFixed() } { currencyUnits.gwei })
</Text>
</DetailsInfoItem.Value>
......
......@@ -13,6 +13,9 @@ import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile';
import getNetworkValidationActionText from 'lib/networks/getNetworkValidationActionText';
import getQueryParamString from 'lib/router/getQueryParamString';
import { Skeleton } from 'toolkit/chakra/skeleton';
import RoutedTabs from 'toolkit/components/RoutedTabs/RoutedTabs';
import RoutedTabsSkeleton from 'toolkit/components/RoutedTabs/RoutedTabsSkeleton';
import BlockCeloEpochTag from 'ui/block/BlockCeloEpochTag';
import BlockDetails from 'ui/block/BlockDetails';
import BlockEpochRewards from 'ui/block/BlockEpochRewards';
......@@ -23,13 +26,10 @@ import useBlockTxsQuery from 'ui/block/useBlockTxsQuery';
import useBlockWithdrawalsQuery from 'ui/block/useBlockWithdrawalsQuery';
import TextAd from 'ui/shared/ad/TextAd';
import ServiceDegradationWarning from 'ui/shared/alerts/ServiceDegradationWarning';
import Skeleton from 'ui/shared/chakra/Skeleton';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import NetworkExplorers from 'ui/shared/NetworkExplorers';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting';
const TAB_LIST_PROPS = {
......@@ -68,40 +68,40 @@ const BlockPageContent = () => {
</>
),
},
{
id: 'txs',
title: 'Transactions',
component: (
<>
{ blockTxsQuery.isDegradedData && <ServiceDegradationWarning isLoading={ blockTxsQuery.isPlaceholderData } mb={ 6 }/> }
<TxsWithFrontendSorting query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>
</>
),
},
config.features.dataAvailability.isEnabled && blockQuery.data?.blob_transaction_count ?
{
id: 'blob_txs',
title: 'Blob txns',
component: (
<TxsWithFrontendSorting query={ blockBlobTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/>
),
} : null,
config.features.beaconChain.isEnabled && Boolean(blockQuery.data?.withdrawals_count) ?
{
id: 'withdrawals',
title: 'Withdrawals',
component: (
<>
{ blockWithdrawalsQuery.isDegradedData && <ServiceDegradationWarning isLoading={ blockWithdrawalsQuery.isPlaceholderData } mb={ 6 }/> }
<BlockWithdrawals blockWithdrawalsQuery={ blockWithdrawalsQuery }/>
</>
),
} : null,
blockQuery.data?.celo?.is_epoch_block ? {
id: 'epoch_rewards',
title: 'Epoch rewards',
component: <BlockEpochRewards heightOrHash={ heightOrHash }/>,
} : null,
// {
// id: 'txs',
// title: 'Transactions',
// component: (
// <>
// { blockTxsQuery.isDegradedData && <ServiceDegradationWarning isLoading={ blockTxsQuery.isPlaceholderData } mb={ 6 }/> }
// <TxsWithFrontendSorting query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>
// </>
// ),
// },
// config.features.dataAvailability.isEnabled && blockQuery.data?.blob_transaction_count ?
// {
// id: 'blob_txs',
// title: 'Blob txns',
// component: (
// <TxsWithFrontendSorting query={ blockBlobTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/>
// ),
// } : null,
// config.features.beaconChain.isEnabled && Boolean(blockQuery.data?.withdrawals_count) ?
// {
// id: 'withdrawals',
// title: 'Withdrawals',
// component: (
// <>
// { blockWithdrawalsQuery.isDegradedData && <ServiceDegradationWarning isLoading={ blockWithdrawalsQuery.isPlaceholderData } mb={ 6 }/> }
// <BlockWithdrawals blockWithdrawalsQuery={ blockWithdrawalsQuery }/>
// </>
// ),
// } : null,
// blockQuery.data?.celo?.is_epoch_block ? {
// id: 'epoch_rewards',
// title: 'Epoch rewards',
// component: <BlockEpochRewards heightOrHash={ heightOrHash }/>,
// } : null,
].filter(Boolean)), [ blockBlobTxsQuery, blockQuery, blockTxsQuery, blockWithdrawalsQuery, hasPagination, heightOrHash ]);
let pagination;
......@@ -152,7 +152,7 @@ const BlockPageContent = () => {
<>
{ !config.UI.views.block.hiddenFields?.miner && blockQuery.data?.miner && (
<Skeleton
isLoaded={ !blockQuery.isPlaceholderData }
loading={ blockQuery.isPlaceholderData }
fontFamily="heading"
display="flex"
minW={ 0 }
......@@ -165,7 +165,11 @@ const BlockPageContent = () => {
<AddressEntity address={ blockQuery.data.miner }/>
</Skeleton>
) }
<NetworkExplorers type="block" pathParam={ heightOrHash } ml={{ base: config.UI.views.block.hiddenFields?.miner ? 0 : 3, lg: 'auto' }}/>
<NetworkExplorers
type="block"
pathParam={ heightOrHash }
ml={{ base: config.UI.views.block.hiddenFields?.miner ? 0 : 3, lg: 'auto' }}
/>
</>
);
......@@ -179,10 +183,10 @@ const BlockPageContent = () => {
secondRow={ titleSecondRow }
isLoading={ blockQuery.isPlaceholderData }
/>
{ blockQuery.isPlaceholderData ? <TabsSkeleton tabs={ tabs }/> : (
{ blockQuery.isPlaceholderData ? <RoutedTabsSkeleton tabs={ tabs }/> : (
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
listProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination ? <Pagination { ...(pagination as PaginationParams) }/> : null }
stickyEnabled={ hasPagination }
/>
......
import { chakra, GridItem, Flex, Text } from '@chakra-ui/react';
import React from 'react';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { Skeleton } from 'toolkit/chakra/skeleton';
import * as ContainerWithScrollY from 'ui/shared/ContainerWithScrollY';
import Hint from 'ui/shared/Hint';
import HintPopover from 'ui/shared/HintPopover';
const LabelScrollText = () => (
<Text fontWeight={ 500 } variant="secondary" fontSize="xs" className="note" align="right">
<Text fontWeight={ 500 } color="text.secondary" fontSize="xs" className="note" textAlign="right">
Scroll to see more
</Text>
);
......@@ -19,23 +18,20 @@ interface LabelProps {
className?: string;
id?: string;
hasScroll?: boolean;
type?: 'tooltip' | 'popover';
}
const Label = chakra(({ hint, children, isLoading, id, className, hasScroll, type }: LabelProps) => {
const Label = chakra(({ hint, children, isLoading, id, className, hasScroll }: LabelProps) => {
return (
<GridItem
id={ id }
className={ className }
py={ 1 }
lineHeight={{ base: 5, lg: 6 }}
textStyle="md"
_notFirst={{ mt: { base: 3, lg: 0 } }}
>
<Flex columnGap={ 2 } alignItems="flex-start">
{ hint && (type === 'popover' ?
<HintPopover label={ hint } isLoading={ isLoading } my={{ lg: '2px' }}/> :
<Hint label={ hint } isLoading={ isLoading } my={{ lg: '2px' }}/>) }
<Skeleton isLoaded={ !isLoading } fontWeight={{ base: 700, lg: 500 }}>
{ hint && <Hint label={ hint } isLoading={ isLoading } my={{ lg: '2px' }}/> }
<Skeleton loading={ isLoading } fontWeight={{ base: 700, lg: 500 }}>
{ children }
{ hasScroll && <LabelScrollText/> }
</Skeleton>
......@@ -59,7 +55,7 @@ const Value = chakra(({ children, className }: ValueProps) => {
rowGap={ 3 }
pl={{ base: 7, lg: 0 }}
py={ 1 }
lineHeight={{ base: 5, lg: 6 }}
textStyle="md"
whiteSpace="nowrap"
>
{ children }
......
import React from 'react';
import dayjs from 'lib/date/dayjs';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { Skeleton } from 'toolkit/chakra/skeleton';
import IconSvg from 'ui/shared/IconSvg';
import TextSeparator from 'ui/shared/TextSeparator';
......@@ -15,11 +15,11 @@ const DetailsTimestamp = ({ timestamp, isLoading }: Props) => {
return (
<>
<IconSvg name="clock" boxSize={ 5 } color="gray.500" isLoading={ isLoading }/>
<Skeleton isLoaded={ !isLoading } ml={ 2 }>
<Skeleton loading={ isLoading } ml={ 2 }>
{ dayjs(timestamp).fromNow() }
</Skeleton>
<TextSeparator color="gray.500"/>
<Skeleton isLoaded={ !isLoading } whiteSpace="normal">
<Skeleton loading={ isLoading } whiteSpace="normal">
{ dayjs(timestamp).format('llll') }
</Skeleton>
</>
......
......@@ -32,6 +32,7 @@ const Hint = ({ label, className, tooltipProps, isLoading }: Props) => {
<Tooltip
content={ label }
positioning={{ placement: 'top' }}
interactive
{ ...tooltipProps }
>
<IconButton
......
import type {
PopoverBodyProps,
PopoverContentProps,
PopoverProps } from '@chakra-ui/react';
import {
DarkMode,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
Portal,
chakra,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react';
import Popover from 'ui/shared/chakra/Popover';
import Skeleton from 'ui/shared/chakra/Skeleton';
import IconSvg from './IconSvg';
interface Props {
label: React.ReactNode;
className?: string;
isLoading?: boolean;
popoverProps?: Partial<PopoverProps>;
popoverContentProps?: Partial<PopoverContentProps>;
popoverBodyProps?: Partial<PopoverBodyProps>;
}
const HintPopover = ({ label, isLoading, className, popoverProps, popoverContentProps, popoverBodyProps }: Props) => {
const bgColor = useColorModeValue('gray.700', 'gray.900');
if (isLoading) {
return <Skeleton className={ className } boxSize={ 5 } borderRadius="sm"/>;
}
return (
<Popover trigger="hover" isLazy placement="top" { ...popoverProps }>
<PopoverTrigger>
<IconSvg className={ className } name="info" boxSize={ 5 } color="icon_info" _hover={{ color: 'link_hovered' }} cursor="pointer"/>
</PopoverTrigger>
<Portal>
<PopoverContent bgColor={ bgColor } maxW={{ base: 'calc(100vw - 8px)', lg: '320px' }} borderRadius="sm" { ...popoverContentProps }>
<PopoverArrow bgColor={ bgColor }/>
<PopoverBody color="white" fontSize="sm" lineHeight="20px" px={ 2 } py="2px" { ...popoverBodyProps }>
<DarkMode>
{ label }
</DarkMode>
</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
);
};
export default React.memo(chakra(HintPopover));
import { Grid, chakra, GridItem } from '@chakra-ui/react';
import { motion } from 'framer-motion';
import React from 'react';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { Skeleton } from 'toolkit/chakra/skeleton';
interface ContainerProps {
className?: string;
isAnimated?: boolean;
animation?: string;
children: React.ReactNode;
}
const Container = chakra(({ isAnimated, children, className }: ContainerProps) => {
const Container = chakra(({ animation, children, className }: ContainerProps) => {
return (
<Grid
as={ motion.div }
w="100%"
initial={ isAnimated ? { opacity: 0, scale: 0.97 } : { opacity: 1, scale: 1 } }
animate={{ opacity: 1, scale: 1 }}
transitionDuration="normal"
transitionTimingFunction="linear"
animation={ animation }
rowGap={ 2 }
columnGap={ 2 }
gridTemplateColumns="86px auto"
......@@ -48,7 +43,7 @@ const Label = chakra(({ children, className, isLoading }: LabelProps) => {
return (
<Skeleton
className={ className }
isLoaded={ !isLoading }
loading={ isLoading }
fontWeight={ 500 }
my="5px"
justifySelf="start"
......
import {
Image,
useColorModeValue,
chakra,
} from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import React from 'react';
import type { NetworkExplorer as TNetworkExplorer } from 'types/networks';
import config from 'configs/app';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import { Image } from 'toolkit/chakra/image';
import { Link } from 'toolkit/chakra/link';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal';
import VerifyWith from 'ui/shared/VerifyWith';
interface Props {
......@@ -20,24 +17,22 @@ interface Props {
}
const NetworkExplorers = ({ className, type, pathParam }: Props) => {
const defaultIconColor = useColorModeValue('gray.400', 'gray.500');
const explorersLinks = React.useMemo(() => {
return config.UI.explorers.items
.filter((explorer) => typeof explorer.paths[type] === 'string')
.map((explorer) => {
const url = new URL(stripTrailingSlash(explorer.paths[type] || '') + '/' + pathParam, explorer.baseUrl);
return (
<LinkExternal h="34px" key={ explorer.baseUrl } href={ url.toString() } alignItems="center" display="inline-flex" minW="120px">
<Link external h="34px" key={ explorer.baseUrl } href={ url.toString() } alignItems="center" display="inline-flex" minW="120px">
{ explorer.logo ?
<Image boxSize={ 5 } mr={ 2 } src={ explorer.logo } alt={ `${ explorer.title } icon` }/> :
<IconSvg name="explorer" boxSize={ 5 } color={ defaultIconColor } mr={ 2 }/>
<IconSvg name="explorer" boxSize={ 5 } color={{ _light: 'gray.400', _dark: 'gray.500' }} mr={ 2 }/>
}
{ explorer.title }
</LinkExternal>
</Link>
);
});
}, [ pathParam, type, defaultIconColor ]);
}, [ pathParam, type ]);
if (explorersLinks.length === 0) {
return null;
......
import { Tooltip, chakra } from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import Skeleton from 'ui/shared/chakra/Skeleton';
type Props = {
label: string;
isLoading?: boolean;
className?: string;
children: React.ReactNode;
};
const PopoverTriggerTooltip = ({ label, isLoading, className, children }: Props, ref: React.ForwardedRef<HTMLDivElement>) => {
const isMobile = useIsMobile();
return (
// tooltip need to be wrapped in div for proper popover positioning
<Skeleton isLoaded={ !isLoading } borderRadius="base" ref={ ref } className={ className }>
<Tooltip
label={ label }
isDisabled={ isMobile }
// need a delay to avoid flickering when closing the popover
openDelay={ 100 }
>
{ children }
</Tooltip>
</Skeleton>
);
};
export default chakra(React.forwardRef(PopoverTriggerTooltip));
import { Box, IconButton, chakra, Tooltip, Flex } from '@chakra-ui/react';
import { Box, chakra, Flex } from '@chakra-ui/react';
import React from 'react';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { IconButton } from 'toolkit/chakra/icon-button';
import { Skeleton } from 'toolkit/chakra/skeleton';
import { Tooltip } from 'toolkit/chakra/tooltip';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
......@@ -26,38 +28,44 @@ const PrevNext = ({ className, onClick, prevLabel, nextLabel, isPrevDisabled, is
if (isLoading) {
return (
<Flex columnGap="10px" className={ className }>
<Skeleton boxSize={ 6 } borderRadius="sm"/>
<Skeleton boxSize={ 6 } borderRadius="sm"/>
<Skeleton loading boxSize={ 6 } borderRadius="sm"/>
<Skeleton loading boxSize={ 6 } borderRadius="sm"/>
</Flex>
);
}
return (
<Box className={ className } display="flex">
<Tooltip label={ prevLabel }>
<Tooltip content={ prevLabel }>
<IconButton
aria-label="prev"
icon={ <IconSvg name="arrows/east-mini" boxSize={ 6 }/> }
h={ 6 }
borderRadius="sm"
variant="subtle"
colorScheme="gray"
bg="gray.100"
color="gray.600"
_hover={{
color: 'link.primary.hover',
}}
onClick={ handelPrevClick }
isDisabled={ isPrevDisabled }
/>
disabled={ isPrevDisabled }
>
<IconSvg name="arrows/east-mini" boxSize={ 6 }/>
</IconButton>
</Tooltip>
<Tooltip label={ nextLabel }>
<Tooltip content={ nextLabel }>
<IconButton
aria-label="next"
icon={ <IconSvg name="arrows/east-mini" boxSize={ 6 } transform="rotate(180deg)"/> }
h={ 6 }
borderRadius="sm"
variant="subtle"
colorScheme="gray"
bg="gray.100"
color="gray.600"
_hover={{
color: 'link.primary.hover',
}}
ml="10px"
onClick={ handelNextClick }
isDisabled={ isNextDisabled }
/>
disabled={ isNextDisabled }
>
<IconSvg name="arrows/east-mini" boxSize={ 6 } transform="rotate(180deg)"/>
</IconButton>
</Tooltip>
</Box>
);
......
import type { ChakraProps } from '@chakra-ui/react';
import { Box, Flex, chakra, useColorModeValue } from '@chakra-ui/react';
import type { HTMLChakraProps } from '@chakra-ui/react';
import { Box, Flex, chakra } from '@chakra-ui/react';
import React from 'react';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { Skeleton } from 'toolkit/chakra/skeleton';
import CopyToClipboard from './CopyToClipboard';
......@@ -16,7 +16,7 @@ interface Props {
textareaMinHeight?: string;
showCopy?: boolean;
isLoading?: boolean;
contentProps?: ChakraProps;
contentProps?: HTMLChakraProps<'div'>;
}
const RawDataSnippet = ({
......@@ -36,12 +36,12 @@ const RawDataSnippet = ({
// so blackAlpha.50 here is replaced with #f5f5f6
// and whiteAlpha.50 is replaced with #1a1b1b
// const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const bgColor = useColorModeValue('#f5f5f6', '#1a1b1b');
const bgColor = { _light: '#f5f5f6', _dark: '#1a1b1b' };
return (
<Box className={ className } as="section" title={ title }>
{ (title || rightSlot || showCopy) && (
<Flex justifyContent={ title ? 'space-between' : 'flex-end' } alignItems="center" mb={ 3 }>
{ title && <Skeleton fontWeight={ 500 } isLoaded={ !isLoading }>{ title }</Skeleton> }
{ title && <Skeleton fontWeight={ 500 } loading={ isLoading }>{ title }</Skeleton> }
{ rightSlot }
{ typeof data === 'string' && showCopy && <CopyToClipboard text={ data } isLoading={ isLoading }/> }
</Flex>
......@@ -58,7 +58,7 @@ const RawDataSnippet = ({
whiteSpace="pre-wrap"
overflowX="hidden"
overflowY="auto"
isLoaded={ !isLoading }
loading={ isLoading }
{ ...contentProps }
>
{ data }
......
import {
Button,
PopoverTrigger,
PopoverBody,
PopoverContent,
Show,
Hide,
chakra,
useDisclosure,
Grid,
} from '@chakra-ui/react';
import { Box, chakra, Grid } from '@chakra-ui/react';
import React from 'react';
import Popover from 'ui/shared/chakra/Popover';
import { Button } from 'toolkit/chakra/button';
import { PopoverBody, PopoverContent, PopoverRoot, PopoverTrigger } from 'toolkit/chakra/popover';
import { Tooltip } from 'toolkit/chakra/tooltip';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import IconSvg from 'ui/shared/IconSvg';
import PopoverTriggerTooltip from 'ui/shared/PopoverTriggerTooltip';
interface Props {
className?: string;
......@@ -24,39 +16,34 @@ interface Props {
}
const VerifyWith = ({ className, links, label, longText, shortText }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const { open, onOpenChange } = useDisclosure();
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverRoot positioning={{ placement: 'bottom-start' }} open={ open } onOpenChange={ onOpenChange }>
<PopoverTrigger>
<PopoverTriggerTooltip label={ label } className={ className }>
<Box className={ className }>
<Tooltip content={ label } disableOnMobile disabled={ open }>
<Button
size="sm"
variant="outline"
colorScheme="gray"
onClick={ onToggle }
isActive={ isOpen }
variant="dropdown"
expanded={ open }
aria-label={ label }
fontWeight={ 500 }
px={ shortText ? 2 : 1 }
h="32px"
flexShrink={ 0 }
columnGap={ 1 }
>
<IconSvg name="explorer" boxSize={ 5 }/>
<Show above="xl">
<chakra.span ml={ 1 }>{ longText }</chakra.span>
</Show>
{ shortText && (
<Hide above="xl">
<chakra.span ml={ 1 }>{ shortText }</chakra.span>
</Hide>
) }
<chakra.span hideBelow="xl">{ longText }</chakra.span>
{ shortText && <chakra.span hideFrom="xl">{ shortText }</chakra.span> }
</Button>
</PopoverTriggerTooltip>
</Tooltip>
</Box>
</PopoverTrigger>
<PopoverContent w="auto">
<PopoverBody >
<chakra.span color="text_secondary" fontSize="xs">{ label }</chakra.span>
<chakra.span color="text.secondary" textStyle="xs">{ label }</chakra.span>
<Grid
alignItems="center"
templateColumns={ links.length > 1 ? 'auto auto' : '1fr' }
......@@ -68,7 +55,7 @@ const VerifyWith = ({ className, links, label, longText, shortText }: Props) =>
</Grid>
</PopoverBody>
</PopoverContent>
</Popover>
</PopoverRoot>
);
};
......
import { Alert, Spinner, chakra } from '@chakra-ui/react';
import { Spinner, chakra } from '@chakra-ui/react';
import React from 'react';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { Alert } from 'toolkit/chakra/alert';
interface Props {
isLoading?: boolean;
......@@ -10,12 +10,14 @@ interface Props {
const ServiceDegradationWarning = ({ isLoading, className }: Props) => {
return (
<Skeleton className={ className } isLoaded={ !isLoading }>
<Alert status="warning" colorScheme="gray" alignItems={{ base: 'flex-start', lg: 'center' }}>
<Spinner size="sm" mr={ 2 } my={{ base: '3px', lg: 0 }} flexShrink={ 0 }/>
<Alert
loading={ isLoading }
status="neutral"
className={ className }
startElement={ <Spinner size="sm" my="3px" flexShrink={ 0 }/> }
>
Data sync in progress... page will refresh automatically once data is available
</Alert>
</Skeleton>
);
};
......
......@@ -3,7 +3,7 @@ import React from 'react';
import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
import type { ExcludeUndefined } from 'types/utils';
import Tag from 'ui/shared/chakra/Tag';
import { Badge } from 'toolkit/chakra/badge';
export interface Props {
container: ExcludeUndefined<OptimisticL2TxnBatchesItem['batch_data_container']>;
......@@ -24,9 +24,9 @@ const OptimisticL2TxnBatchDA = ({ container, isLoading }: Props) => {
})();
return (
<Tag colorScheme="yellow" isLoading={ isLoading }>
<Badge colorScheme="yellow" loading={ isLoading }>
{ text }
</Tag>
</Badge>
);
};
......
......@@ -3,7 +3,7 @@ import React from 'react';
import type { Step } from './types';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { Skeleton } from 'toolkit/chakra/skeleton';
import VerificationStep from './VerificationStep';
......@@ -25,7 +25,7 @@ const VerificationSteps = ({ currentStep, currentStepPending, steps, isLoading,
return (
<Skeleton
className={ className }
isLoaded={ !isLoading }
loading={ isLoading }
display="flex"
gap={ 2 }
alignItems="center"
......
import { Box, Table } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import React from 'react';
import { Alert } from 'toolkit/chakra/alert';
import { TableBody, TableColumnHeader, TableHeader, TableRoot, TableRow } from 'toolkit/chakra/table';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import { Section, Container, SectionHeader, SamplesStack, Sample, SectionSubHeader } from './parts';
......@@ -12,20 +13,20 @@ const AlertsShowcase = () => {
<Section>
<SectionHeader>Status</SectionHeader>
<SamplesStack>
<Sample label="visual: info">
<Alert visual="info" title="Info"> Alert content </Alert>
<Sample label="status: info">
<Alert status="info" title="Info"> Alert content </Alert>
</Sample>
<Sample label="visual: neutral">
<Alert visual="neutral" title="Neutral"> Alert content </Alert>
<Sample label="status: neutral">
<Alert status="neutral" title="Neutral"> Alert content </Alert>
</Sample>
<Sample label="visual: warning">
<Alert visual="warning" title="Warning"> Alert content </Alert>
<Sample label="status: warning">
<Alert status="warning" title="Warning"> Alert content </Alert>
</Sample>
<Sample label="visual: success">
<Alert visual="success" title="Success"> Alert content </Alert>
<Sample label="status: success">
<Alert status="success" title="Success"> Alert content </Alert>
</Sample>
<Sample label="visual: error">
<Alert visual="error" title="Error"> Alert content </Alert>
<Sample label="status: error">
<Alert status="error" title="Error"> Alert content </Alert>
</Sample>
</SamplesStack>
</Section>
......@@ -33,7 +34,7 @@ const AlertsShowcase = () => {
<SectionHeader>Variant</SectionHeader>
<SamplesStack>
<Sample label="variant: subtle">
<Alert visual="info" title="Info"> Alert content </Alert>
<Alert status="info" title="Info"> Alert content </Alert>
</Sample>
</SamplesStack>
</Section>
......@@ -42,56 +43,56 @@ const AlertsShowcase = () => {
<SectionSubHeader>Inside table (SocketNewItemsNotice)</SectionSubHeader>
<SamplesStack>
<Sample label="loading">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.ColumnHeader w="100px">Block</Table.ColumnHeader>
<Table.ColumnHeader w="100px">Age</Table.ColumnHeader>
<Table.ColumnHeader w="100px">Gas used</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
<TableRoot tableLayout="auto">
<TableHeader>
<TableRow>
<TableColumnHeader w="100px">Block</TableColumnHeader>
<TableColumnHeader w="100px">Age</TableColumnHeader>
<TableColumnHeader w="100px">Gas used</TableColumnHeader>
</TableRow>
</TableHeader>
<TableBody>
<SocketNewItemsNotice.Desktop
url={ window.location.href }
num={ 1234 }
type="block"
isLoading
/>
</Table.Body>
</Table.Root>
</TableBody>
</TableRoot>
</Sample>
<Sample label="success">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.ColumnHeader w="100px">Block</Table.ColumnHeader>
<Table.ColumnHeader w="100px">Age</Table.ColumnHeader>
<Table.ColumnHeader w="100px">Gas used</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
<TableRoot tableLayout="auto">
<TableHeader>
<TableRow>
<TableColumnHeader w="100px">Block</TableColumnHeader>
<TableColumnHeader w="100px">Age</TableColumnHeader>
<TableColumnHeader w="100px">Gas used</TableColumnHeader>
</TableRow>
</TableHeader>
<TableBody>
<SocketNewItemsNotice.Desktop
url={ window.location.href }
num={ 1234 }
type="block"
isLoading={ false }
/>
</Table.Body>
</Table.Root>
</TableBody>
</TableRoot>
</Sample>
</SamplesStack>
<SectionSubHeader>Multiple lines</SectionSubHeader>
<SamplesStack>
<Sample label="multiple lines, with title, inline=false">
<Alert visual="warning" title="Warning" inline={ false } maxWidth="500px">
<Alert status="warning" title="Warning" inline={ false } maxWidth="500px">
<Box>
Participated in our recent Blockscout activities? Check your eligibility and claim your NFT Scout badges. More exciting things are coming soon!
</Box>
</Alert>
</Sample>
<Sample label="multiple lines, no title">
<Alert visual="warning" maxWidth="500px">
<Alert status="warning" maxWidth="500px">
<Box>
Participated in our recent Blockscout activities? Check your eligibility and claim your NFT Scout badges. More exciting things are coming soon!
</Box>
......
......@@ -102,7 +102,7 @@ const LinksShowcase = () => {
</CutLink>
</Sample>
<Sample label="Loading" flexDirection="column" alignItems="flex-start">
<CutLink id="CutLink_2" isLoading>
<CutLink id="CutLink_2" loading>
<Box maxW="500px">{ TEXT }</Box>
</CutLink>
</Sample>
......
......@@ -3,7 +3,7 @@ import React from 'react';
import type { ZkSyncBatch } from 'types/api/zkSyncL2';
import Skeleton from 'ui/shared/chakra/Skeleton';
import { Skeleton } from 'toolkit/chakra/skeleton';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsTimestamp from 'ui/shared/DetailsTimestamp';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
......@@ -48,7 +48,7 @@ const ZkSyncL2TxnBatchHashesInfo = ({ isLoading, data }: Props) => {
</Flex>
) }
</>
) : <Skeleton isLoaded={ !isLoading }>Pending</Skeleton> }
) : <Skeleton loading={ isLoading }>Pending</Skeleton> }
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
......@@ -75,7 +75,7 @@ const ZkSyncL2TxnBatchHashesInfo = ({ isLoading, data }: Props) => {
</Flex>
) }
</>
) : <Skeleton isLoaded={ !isLoading }>Pending</Skeleton> }
) : <Skeleton loading={ isLoading }>Pending</Skeleton> }
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
......@@ -102,7 +102,7 @@ const ZkSyncL2TxnBatchHashesInfo = ({ isLoading, data }: Props) => {
</Flex>
) }
</>
) : <Skeleton isLoaded={ !isLoading }>Pending</Skeleton> }
) : <Skeleton loading={ isLoading }>Pending</Skeleton> }
</DetailsInfoItem.Value>
</>
);
......
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