Commit 632ae278 authored by tom's avatar tom

refactor Footer

parent 4a508d9d
......@@ -27,7 +27,7 @@ const RESTRICTED_MODULES = {
{ name: 'playwright/TestApp', message: 'Please use render() fixture from test() function of playwright/lib module' },
{
name: '@chakra-ui/react',
importNames: [ 'Popover', 'Menu', 'PinInput', 'useToast' ],
importNames: [ 'Menu', 'PinInput', 'useToast' ],
message: 'Please use corresponding component or hook from ui/shared/chakra component instead',
},
{
......
......@@ -48,12 +48,24 @@ const ERROR_SCREEN_STYLES: HTMLChakraProps<'div'> = {
};
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
// TODO @tom2drum currently there is hydration mismatch between server and client
// because we use useColorMode hook in the layout component
// not sure how to fix it though
const [ mounted, setMounted ] = React.useState(false);
React.useEffect(() => {
setMounted(true);
}, []);
useLoadFeatures();
useNotifyOnNavigation();
const queryClient = useQueryClientConfig();
if (!mounted) {
return null;
}
const getLayout = Component.getLayout ?? ((page) => <Layout>{ page }</Layout>);
return (
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const Chakra = dynamic(() => import('ui/pages/Chakra'), { ssr: false });
import Chakra from 'ui/pages/Chakra';
const Page: NextPage = () => {
return (
......
import type {
SkeletonProps as ChakraSkeletonProps,
CircleProps,
} from '@chakra-ui/react';
import { Skeleton as ChakraSkeleton, Circle, Stack } from '@chakra-ui/react';
import * as React from 'react';
export interface SkeletonCircleProps extends ChakraSkeletonProps {
size?: CircleProps['size'];
}
export const SkeletonCircle = React.forwardRef<
HTMLDivElement,
SkeletonCircleProps
>(function SkeletonCircle(props, ref) {
const { size, ...rest } = props;
return (
<Circle size={ size } asChild ref={ ref }>
<ChakraSkeleton { ...rest }/>
</Circle>
);
});
export interface SkeletonTextProps extends ChakraSkeletonProps {
noOfLines?: number;
}
export const SkeletonText = React.forwardRef<HTMLDivElement, SkeletonTextProps>(
function SkeletonText(props, ref) {
const { noOfLines = 3, gap, ...rest } = props;
return (
<Stack gap={ gap } width="full" ref={ ref }>
{ Array.from({ length: noOfLines }).map((_, index) => (
<ChakraSkeleton
height="4"
key={ index }
{ ...props }
_last={{ maxW: '80%' }}
{ ...rest }
/>
)) }
</Stack>
);
},
);
export const Skeleton = React.forwardRef<HTMLDivElement, ChakraSkeletonProps>(
function Skeleton(props, ref) {
const { loading = false, variant = 'shine', ...rest } = props;
return <ChakraSkeleton loading={ loading } variant={ variant } { ...rest } ref={ ref }/>;
},
);
import { Tooltip as ChakraTooltip, Portal } from '@chakra-ui/react';
import { useClickAway } from '@uidotdev/usehooks';
import * as React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
export interface TooltipProps extends ChakraTooltip.RootProps {
showArrow?: boolean;
portalled?: boolean;
......@@ -13,7 +16,7 @@ export interface TooltipProps extends ChakraTooltip.RootProps {
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
function Tooltip(props, ref) {
const {
showArrow,
showArrow = true,
children,
disabled,
portalled,
......@@ -23,14 +26,50 @@ export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
...rest
} = props;
const [ open, setOpen ] = React.useState(false);
const isMobile = useIsMobile();
const triggerRef = useClickAway<HTMLButtonElement>(() => setOpen(false));
const handleOpenChange = React.useCallback(({ open }: { open: boolean }) => {
setOpen(open);
}, []);
const handleTriggerClick = React.useCallback(() => {
// FIXME on mobile tooltip will open and close simultaneously
setOpen((prev) => !prev);
}, [ ]);
if (disabled) return children;
const positioning = {
...rest.positioning,
offset: {
mainAxis: 4,
...rest.positioning?.offset,
},
};
return (
<ChakraTooltip.Root { ...rest }>
<ChakraTooltip.Trigger asChild>{ children }</ChakraTooltip.Trigger>
<ChakraTooltip.Root
openDelay={ 100 }
closeDelay={ 100 }
open={ open }
onOpenChange={ handleOpenChange }
{ ...rest }
positioning={ positioning }
closeOnClick={ false }
>
<ChakraTooltip.Trigger
ref={ triggerRef }
asChild
onClick={ isMobile ? handleTriggerClick : undefined }
>
{ children }
</ChakraTooltip.Trigger>
<Portal disabled={ !portalled } container={ portalRef }>
<ChakraTooltip.Positioner>
<ChakraTooltip.Content ref={ ref } { ...contentProps }>
<ChakraTooltip.Content ref={ ref } p={ 2 } { ...contentProps }>
{ showArrow && (
<ChakraTooltip.Arrow>
<ChakraTooltip.ArrowTip/>
......
......@@ -13,7 +13,7 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
DEFAULT: { value: '{colors.blue.400}' },
},
},
anchor: {
dropdown: {
fg: {
DEFAULT: { value: { base: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
selected: { value: { base: '{colors.blue.600}', _dark: '{colors.gray.50}' } },
......@@ -27,28 +27,39 @@ const semanticTokens: ThemingConfig['semanticTokens'] = {
},
},
},
links: {
primary: {
DEFAULT: { value: { base: '{colors.blue.600}', _dark: '{colors.blue.300}' } },
hover: { value: { base: '{colors.blue.400}' } },
},
secondary: {
DEFAULT: { value: { base: '{colors.gray.400}', _dark: '{colors.gray.500}' } },
},
subtle: {
DEFAULT: { value: { base: '{colors.blackAlpha.800}', _dark: '{colors.gray.400}' } },
hover: { value: { base: '{colors.blackAlpha.800}', _dark: '{colors.gray.400}' } },
},
},
text: {
primary: { value: { base: '{colors.blackAlpha.800}', _dark: '{colors.whiteAlpha.800}' } },
secondary: { value: { base: '{colors.gray.500}', _dark: '{colors.gray.400}' } },
},
globals: {
body: {
bg: {
DEFAULT: { value: { base: '{colors.white}', _dark: '{colors.black}' } },
},
bg: { value: { base: '{colors.white}', _dark: '{colors.black}' } },
fg: { value: '{colors.text.primary}' },
},
mark: {
bg: {
DEFAULT: { value: { base: '{colors.green.100}', _dark: '{colors.green.800}' } },
},
bg: { value: { base: '{colors.green.100}', _dark: '{colors.green.800}' } },
},
},
// OLD TOKENS
divider: {
DEFAULT: { value: '{colors.blackAlpha.200}' },
_dark: { value: '{colors.whiteAlpha.200}' },
},
text: {
DEFAULT: { value: '{colors.blackAlpha.800}' },
_dark: { value: '{colors.whiteAlpha.800}' },
},
divider: { value: { base: '{colors.blackAlpha.200}', _dark: '{colors.whiteAlpha.200}' } },
// text: {
// DEFAULT: { value: '{colors.blackAlpha.800}' },
// _dark: { value: '{colors.whiteAlpha.800}' },
// },
text_secondary: {
DEFAULT: { value: '{colors.gray.500}' },
_dark: { value: '{colors.gray.400}' },
......
......@@ -10,6 +10,7 @@ import scrollbar from './globals/scrollbar';
const globalCss: Record<string, SystemStyleObject> = {
body: {
bg: 'globals.body.bg',
color: 'globals.body.fg',
'-webkit-tap-highlight-color': 'transparent',
'font-variant-ligatures': 'no-contextual',
},
......
......@@ -32,41 +32,41 @@ export const recipe = defineRecipe({
borderColor: 'buttons.outline.hover',
},
},
anchor: {
dropdown: {
borderWidth: '2px',
borderStyle: 'solid',
bg: 'transparent',
color: 'buttons.anchor.fg',
borderColor: 'buttons.anchor.border',
color: 'buttons.dropdown.fg',
borderColor: 'buttons.dropdown.border',
_hover: {
bg: 'transparent',
color: 'buttons.anchor.hover',
borderColor: 'buttons.anchor.hover',
color: 'buttons.dropdown.hover',
borderColor: 'buttons.dropdown.hover',
},
// When the dropdown is open, the button should be active
_active: {
bg: 'transparent',
color: 'buttons.anchor.hover',
borderColor: 'buttons.anchor.hover',
color: 'buttons.dropdown.hover',
borderColor: 'buttons.dropdown.hover',
},
// We have a special state for this button variant that serves as a popover trigger.
// When any items (filters) are selected in the popover, the button should change its background and text color.
// The last CSS selector is for redefining styles for the TabList component.
_selected: {
bg: 'buttons.anchor.border.selected',
color: 'buttons.anchor.fg.selected',
borderColor: 'buttons.anchor.border.selected',
bg: 'buttons.dropdown.border.selected',
color: 'buttons.dropdown.fg.selected',
borderColor: 'buttons.dropdown.border.selected',
_hover: {
bg: 'buttons.anchor.border.selected',
color: 'buttons.anchor.fg.selected',
borderColor: 'buttons.anchor.border.selected',
bg: 'buttons.dropdown.border.selected',
color: 'buttons.dropdown.fg.selected',
borderColor: 'buttons.dropdown.border.selected',
},
},
},
},
size: {
xs: { px: 2, h: 6, fontSize: '12px' },
sm: { px: 3, h: 8, fontSize: '14px' },
sm: { px: 2, h: 8, fontSize: '14px' },
md: { px: 4, h: 10, fontSize: '16px' },
lg: { px: 6, h: 12, fontSize: '20px' },
},
......
export { recipe as button } from './button.recipe';
export { recipe as link } from './link.recipe';
import { defineRecipe } from '@chakra-ui/react';
export const recipe = defineRecipe({
variants: {
visual: {
primary: {
color: 'links.primary',
_hover: {
textDecoration: 'none',
color: 'links.primary.hover',
},
},
secondary: {
color: 'links.secondary',
_hover: {
textDecoration: 'none',
color: 'links.primary.hover',
},
},
subtle: {
color: 'links.subtle',
_hover: {
color: 'links.subtle.hover',
textDecorationColor: 'links.subtle.hover',
},
},
},
},
defaultVariants: {
visual: 'primary',
},
});
import { Heading, HStack } from '@chakra-ui/react';
import { Heading, HStack, Link, VStack } from '@chakra-ui/react';
import React from 'react';
import { Button } from 'toolkit/chakra/button';
......@@ -15,13 +15,27 @@ const ChakraShowcases = () => {
<Switch onCheckedChange={ colorMode.toggleColorMode } checked={ colorMode.colorMode === 'dark' } mb={ 4 }>
Color mode: { colorMode.colorMode }
</Switch>
<Heading textStyle="heading.md" mb={ 4 }>Buttons</Heading>
<HStack gap={ 4 }>
<Button>Solid</Button>
<Button visual="outline">Outline</Button>
<Button visual="anchor">Anchor</Button>
<Button visual="anchor" selected>Anchor selected</Button>
</HStack>
<VStack align="flex-start" gap={ 6 }>
<section>
<Heading textStyle="heading.md" mb={ 2 }>Buttons</Heading>
<HStack gap={ 4 }>
<Button>Solid</Button>
<Button visual="outline">Outline</Button>
<Button visual="dropdown">Dropdown</Button>
<Button visual="dropdown" selected>Dropdown selected</Button>
</HStack>
</section>
<section>
<Heading textStyle="heading.md" mb={ 2 }>Links</Heading>
<HStack gap={ 4 }>
<Link>Primary</Link>
<Link visual="secondary">Secondary</Link>
<Link visual="subtle">Subtle</Link>
</HStack>
</section>
</VStack>
</>
);
};
......
import type { HTMLChakraProps } from '@chakra-ui/react';
import { Skeleton, chakra } from '@chakra-ui/react';
import { chakra } from '@chakra-ui/react';
import { type IconName } from 'public/icons/name';
import React from 'react';
import config from 'configs/app';
import { Skeleton } from 'toolkit/chakra/skeleton';
export const href = config.app.spriteHash ? `/icons/sprite.${ config.app.spriteHash }.svg` : '/icons/sprite.svg';
......@@ -14,9 +15,9 @@ interface Props extends HTMLChakraProps<'div'> {
isLoading?: boolean;
}
const IconSvg = ({ name, isLoading, ...props }: Props, ref: React.ForwardedRef<HTMLDivElement>) => {
const IconSvg = ({ name, isLoading = false, ...props }: Props, ref: React.ForwardedRef<HTMLDivElement>) => {
return (
<Skeleton isLoaded={ !isLoading } display="inline-block" { ...props } ref={ ref }>
<Skeleton loading={ isLoading } display="inline-block" { ...props } ref={ ref }>
<chakra.svg w="100%" h="100%">
<use href={ `${ href }#${ name }` }/>
</chakra.svg>
......
import { Button } from '@chakra-ui/react';
import React from 'react';
import { toaster } from 'toolkit/chakra/toaster';
import config from 'configs/app';
import * as mixpanel from 'lib/mixpanel/index';
import useAddOrSwitchChain from 'lib/web3/useAddOrSwitchChain';
import useProvider from 'lib/web3/useProvider';
import { WALLETS_INFO } from 'lib/web3/wallets';
import { Button } from 'toolkit/chakra/button';
import { toaster } from 'toolkit/chakra/toaster';
import IconSvg from 'ui/shared/IconSvg';
const feature = config.features.web3Wallet;
......@@ -46,8 +46,8 @@ const NetworkAddToWallet = () => {
}
return (
<Button variant="outline" size="sm" onClick={ handleClick }>
<IconSvg name={ WALLETS_INFO[wallet].icon } boxSize={ 5 } mr={ 2 }/>
<Button visual="outline" size="sm" onClick={ handleClick }>
<IconSvg name={ WALLETS_INFO[wallet].icon } boxSize={ 5 }/>
Add { config.chain.name }
</Button>
);
......
......@@ -28,7 +28,7 @@ const LayoutDefault = ({ children }: Props) => {
</AppErrorBoundary>
</Layout.MainColumn>
</Layout.MainArea>
{ /* <Layout.Footer/> */ }
<Layout.Footer/>
</Layout.Container>
);
};
......
import type { GridProps, HTMLChakraProps } from '@chakra-ui/react';
import { Box, Grid, Flex, Text, Link, VStack, Skeleton, useColorModeValue } from '@chakra-ui/react';
import { Box, Grid, Flex, Text, Link, VStack } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
......@@ -11,6 +11,7 @@ import useApiQuery from 'lib/api/useApiQuery';
import useFetch from 'lib/hooks/useFetch';
import useIssueUrl from 'lib/hooks/useIssueUrl';
import { copy } from 'lib/html-entities';
import { Skeleton } from 'toolkit/chakra/skeleton';
import IconSvg from 'ui/shared/IconSvg';
import { CONTENT_MAX_WIDTH } from 'ui/shared/layout/utils';
import NetworkAddToWallet from 'ui/shared/NetworkAddToWallet';
......@@ -33,7 +34,6 @@ const Footer = () => {
});
const apiVersionUrl = getApiVersionUrl(backendVersionData?.backend_version);
const issueUrl = useIssueUrl(backendVersionData?.backend_version);
const logoColor = useColorModeValue('blue.600', 'white');
const BLOCKSCOUT_LINKS = [
{
......@@ -115,11 +115,13 @@ const Footer = () => {
}, []);
const renderProjectInfo = React.useCallback((gridArea?: GridProps['gridArea']) => {
const logoColor = { base: 'blue.600', _dark: 'white' };
return (
<Box gridArea={ gridArea }>
<Flex columnGap={ 2 } fontSize="xs" lineHeight={ 5 } alignItems="center" color="text">
<Flex columnGap={ 2 } textStyle="xs" alignItems="center">
<span>Made with</span>
<Link href="https://www.blockscout.com" isExternal display="inline-flex" color={ logoColor } _hover={{ color: logoColor }}>
<Link href="https://www.blockscout.com" target="_blank" display="inline-flex" color={ logoColor } _hover={{ color: logoColor }}>
<IconSvg
name="networks/logo-placeholder"
width="80px"
......@@ -130,7 +132,7 @@ const Footer = () => {
<Text mt={ 3 } fontSize="xs">
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text>
<Box mt={ 6 } alignItems="start" fontSize="xs" lineHeight={ 5 }>
<Box mt={ 6 } alignItems="start" textStyle="xs">
{ apiVersionUrl && (
<Text>
Backend: <Link href={ apiVersionUrl } target="_blank">{ backendVersionData?.backend_version }</Link>
......@@ -147,12 +149,12 @@ const Footer = () => {
</Box>
</Box>
);
}, [ apiVersionUrl, backendVersionData?.backend_version, frontendLink, logoColor ]);
}, [ apiVersionUrl, backendVersionData?.backend_version, frontendLink ]);
const containerProps: HTMLChakraProps<'div'> = {
as: 'footer',
borderTopWidth: '1px',
borderTopColor: 'solid',
borderTopColor: 'divider',
};
const contentProps: GridProps = {
......@@ -170,11 +172,11 @@ const Footer = () => {
}
return (
<Box gridArea={ gridArea } fontSize="xs" lineHeight={ 5 } mt={ 6 } color="text">
<Box gridArea={ gridArea } textStyle="xs" mt={ 6 }>
<span>This site is protected by reCAPTCHA and the Google </span>
<Link href="https://policies.google.com/privacy" isExternal>Privacy Policy</Link>
<Link href="https://policies.google.com/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</Link>
<span> and </span>
<Link href="https://policies.google.com/terms" isExternal>Terms of Service</Link>
<Link href="https://policies.google.com/terms" target="_blank" rel="noopener noreferrer">Terms of Service</Link>
<span> apply.</span>
</Box>
);
......@@ -208,8 +210,8 @@ const Footer = () => {
.slice(0, colNum)
.map(linkGroup => (
<Box key={ linkGroup.title }>
<Skeleton fontWeight={ 500 } mb={ 3 } display="inline-block" isLoaded={ !isPlaceholderData }>{ linkGroup.title }</Skeleton>
<VStack spacing={ 1 } alignItems="start">
<Skeleton fontWeight={ 500 } mb={ 3 } display="inline-block" loading={ isPlaceholderData }>{ linkGroup.title }</Skeleton>
<VStack gap={ 1 } alignItems="start">
{ linkGroup.links.map(link => <FooterLinkItem { ...link } key={ link.text } isLoading={ isPlaceholderData }/>) }
</VStack>
</Box>
......
import { Center, Link, Skeleton } from '@chakra-ui/react';
import { Center, Link } from '@chakra-ui/react';
import React from 'react';
import { Skeleton } from 'toolkit/chakra/skeleton';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
......@@ -18,7 +19,7 @@ const FooterLinkItem = ({ icon, iconSize, text, url, isLoading }: Props) => {
}
return (
<Link href={ url } display="flex" alignItems="center" h="30px" variant="secondary" target="_blank" fontSize="xs">
<Link href={ url } display="flex" alignItems="center" h="30px" visual="subtle" target="_blank" textStyle="xs">
{ icon && (
<Center minW={ 6 } mr={ 2 }>
<IconSvg boxSize={ iconSize || 5 } name={ icon }/>
......
import { IconButton, PopoverTrigger, PopoverContent, PopoverBody, Flex, Text, useColorModeValue } from '@chakra-ui/react';
import { Flex, Text } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import React from 'react';
......@@ -9,15 +9,14 @@ import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import { apos, nbsp, ndash } from 'lib/html-entities';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import Popover from 'ui/shared/chakra/Popover';
import { Tooltip } from 'toolkit/chakra/tooltip';
import IconSvg from 'ui/shared/IconSvg';
const IntTxsIndexingStatus = () => {
const { data, isError, isPending } = useApiQuery('homepage_indexing_status');
const bgColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.100');
const hintTextcolor = useColorModeValue('black', 'white');
const bgColor = { base: 'blackAlpha.100', _dark: 'whiteAlpha.100' };
const queryClient = useQueryClient();
......@@ -52,7 +51,7 @@ const IntTxsIndexingStatus = () => {
}
const hint = (
<Text fontSize="xs" color={ hintTextcolor }>
<Text textStyle="xs">
{ data.indexed_internal_transactions_ratio &&
`${ Math.floor(Number(data.indexed_internal_transactions_ratio) * 100) }% Blocks With Internal Transactions Indexed${ nbsp }${ ndash } ` }
We{ apos }re indexing this chain right now. Some of the counts may be inaccurate.
......@@ -67,18 +66,13 @@ const IntTxsIndexingStatus = () => {
borderRadius="base"
alignItems="center"
justifyContent="center"
columnGap={ 1 }
color="green.400"
_hover={{ color: 'blue.400' }}
>
<IconButton
colorScheme="none"
aria-label="hint"
icon={ <IconSvg name="info" boxSize={ 5 }/> }
boxSize={ 6 }
variant="simple"
/>
<IconSvg name="info" boxSize={ 5 }/>
{ data.indexed_internal_transactions_ratio && (
<Text fontWeight={ 600 } fontSize="xs" color="inherit">
<Text fontWeight={ 600 } textStyle="xs" color="inherit">
{ Math.floor(Number(data.indexed_internal_transactions_ratio) * 100) + '%' }
</Text>
) }
......@@ -86,16 +80,9 @@ const IntTxsIndexingStatus = () => {
);
return (
<Popover placement="bottom-start" isLazy trigger="hover">
<PopoverTrigger>
{ trigger }
</PopoverTrigger>
<PopoverContent maxH="450px" overflowY="hidden" w="240px">
<PopoverBody p={ 4 } bgColor={ bgColor } boxShadow="2xl">
{ hint }
</PopoverBody>
</PopoverContent>
</Popover>
<Tooltip content={ hint } interactive positioning={{ placement: 'bottom-start' }} lazyMount>
{ trigger }
</Tooltip>
);
};
......
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