Commit c479b608 authored by tom goriunov's avatar tom goriunov Committed by GitHub

Top bar and dimmed color theme (#1347)

* quick demo with slider

* change api host to eth

* add styles for color mode switch

* save selected hex in cookie

* style tweaks

* redesign and top bar

* mobile view of top bar

* top bat stats

* change api host for review env

* test and fix default color mode

* review fixes and refactoring

* update screenshots

* Remove "only" from extendedTest in
SearchResults.pw.tsx

* [skip ci] select first color when clicking on theme row

* Update color mode switch styles

* [skip ci] rollback review env values

* [skip ci] 💩💩💩 i need psychotherapy

* [skip ci] ":"

* fix footer layout
parent 7f1ad8c0
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.557 9.542a.75.75 0 0 1 .441.729 6.798 6.798 0 1 1-7.214-7.186.75.75 0 0 1 .548 1.307l-.149.133a3.786 3.786 0 0 0 5.364 5.344l.172-.173a.75.75 0 0 1 .838-.154ZM14.063 12a5.297 5.297 0 0 1-2.195.476 5.285 5.285 0 0 1-4.822-7.44A5.297 5.297 0 1 0 14.063 12Z" fill="currentColor"/>
<path d="m12.631 5.626.193.386a.75.75 0 0 0 .336.335l.386.194-.386.193a.75.75 0 0 0-.336.335l-.193.386-.193-.386a.75.75 0 0 0-.335-.335l-.386-.193.386-.194a.75.75 0 0 0 .335-.335l.193-.386Z" stroke="currentColor"/>
</svg>
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.557 7.712a.75.75 0 0 1 .442.729A7.983 7.983 0 1 1 7.526 0a.75.75 0 0 1 .548 1.308l-.18.161a4.675 4.675 0 0 0 6.619 6.603l.207-.207a.75.75 0 0 1 .837-.154Zm-1.446 2.504a6.176 6.176 0 0 1-8.373-8.311 6.481 6.481 0 0 0-2.282 10.657 6.482 6.482 0 0 0 10.655-2.346Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M15.557 9.542a.75.75 0 0 1 .441.729 6.798 6.798 0 1 1-7.214-7.186.75.75 0 0 1 .548 1.307l-.149.133a3.786 3.786 0 0 0 5.364 5.344l.172-.173a.75.75 0 0 1 .838-.154ZM14.063 12a5.297 5.297 0 0 1-2.195.476 5.285 5.285 0 0 1-4.822-7.44A5.297 5.297 0 1 0 14.063 12Z" fill="currentColor"/>
</svg> </svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m4.7 14.167-.592.591a.833.833 0 0 0 0 1.175.833.833 0 0 0 1.175 0l.592-.591A.833.833 0 0 0 4.7 14.167ZM4.166 10a.833.833 0 0 0-.833-.833H2.5a.833.833 0 1 0 0 1.666h.833A.833.833 0 0 0 4.166 10ZM10 4.167a.833.833 0 0 0 .833-.834V2.5a.833.833 0 1 0-1.667 0v.833a.833.833 0 0 0 .834.834ZM4.7 5.875a.833.833 0 0 0 1.175 0 .833.833 0 0 0 0-1.175l-.592-.592a.833.833 0 0 0-1.175 1.175l.592.592Zm10 .242a.833.833 0 0 0 .583-.242l.592-.592A.832.832 0 1 0 14.7 4.108l-.534.592a.833.833 0 0 0 0 1.175.833.833 0 0 0 .55.242H14.7Zm2.8 3.05h-.834a.833.833 0 0 0 0 1.666h.834a.833.833 0 1 0 0-1.666ZM10 15.833a.833.833 0 0 0-.834.834v.833a.833.833 0 1 0 1.667 0v-.833a.833.833 0 0 0-.833-.834Zm5.3-1.666a.833.833 0 0 0-1.134 1.133l.592.592a.833.833 0 0 0 1.175 0 .833.833 0 0 0 0-1.175l-.633-.55ZM10 5.417A4.583 4.583 0 1 0 14.583 10 4.592 4.592 0 0 0 10 5.417Zm0 7.5a2.917 2.917 0 1 1 0-5.833 2.917 2.917 0 0 1 0 5.833Z" fill="currentColor"/> <path d="m4.7 14.167-.592.591a.833.833 0 0 0 0 1.175.833.833 0 0 0 1.175 0l.592-.591A.833.833 0 0 0 4.7 14.167ZM4.167 10a.833.833 0 0 0-.834-.833H2.5a.833.833 0 0 0 0 1.666h.833A.833.833 0 0 0 4.167 10ZM10 4.167a.833.833 0 0 0 .833-.834V2.5a.833.833 0 1 0-1.666 0v.833a.833.833 0 0 0 .833.834ZM4.7 5.875a.833.833 0 0 0 1.175 0 .833.833 0 0 0 0-1.175l-.592-.592a.833.833 0 0 0-1.175 1.175l.592.592Zm10 .242a.833.833 0 0 0 .583-.242l.592-.592a.835.835 0 0 0-.574-1.465.833.833 0 0 0-.601.29l-.533.592a.833.833 0 0 0 0 1.175.833.833 0 0 0 .55.242H14.7Zm2.8 3.05h-.833a.833.833 0 0 0 0 1.666h.833a.833.833 0 0 0 0-1.666ZM10 15.833a.834.834 0 0 0-.833.834v.833a.833.833 0 0 0 1.666 0v-.833a.833.833 0 0 0-.833-.834Zm5.3-1.666a.834.834 0 0 0-1.133 1.133l.591.592a.833.833 0 0 0 1.175 0 .833.833 0 0 0 0-1.175l-.633-.55ZM10 5.417A4.583 4.583 0 1 0 14.583 10 4.592 4.592 0 0 0 10 5.417Zm0 7.5a2.917 2.917 0 1 1 0-5.834 2.917 2.917 0 0 1 0 5.834Z" fill="currentColor"/>
</svg> </svg>
...@@ -9,6 +9,7 @@ export enum NAMES { ...@@ -9,6 +9,7 @@ export enum NAMES {
CONFIRM_EMAIL_PAGE_VIEWED='confirm_email_page_viewed', CONFIRM_EMAIL_PAGE_VIEWED='confirm_email_page_viewed',
TXS_SORT='txs_sort', TXS_SORT='txs_sort',
COLOR_MODE='chakra-ui-color-mode', COLOR_MODE='chakra-ui-color-mode',
COLOR_MODE_HEX='chakra-ui-color-mode-hex',
INDEXING_ALERT='indexing_alert', INDEXING_ALERT='indexing_alert',
ADBLOCK_DETECTED='adblock_detected', ADBLOCK_DETECTED='adblock_detected',
MIXPANEL_DEBUG='_mixpanel_debug', MIXPANEL_DEBUG='_mixpanel_debug',
......
// https://unicode-table.com // https://symbl.cc/en/
export const asymp = String.fromCharCode(8776); // ~ export const asymp = String.fromCharCode(8776); // ≈
export const tilde = String.fromCharCode(126); // ~
export const hellip = String.fromCharCode(8230); // … export const hellip = String.fromCharCode(8230); // …
export const nbsp = String.fromCharCode(160); // no-break Space export const nbsp = String.fromCharCode(160); // no-break Space
export const thinsp = String.fromCharCode(8201); // thin Space export const thinsp = String.fromCharCode(8201); // thin Space
......
...@@ -3,7 +3,7 @@ export type HomeStats = { ...@@ -3,7 +3,7 @@ export type HomeStats = {
total_addresses: string; total_addresses: string;
total_transactions: string; total_transactions: string;
average_block_time: number; average_block_time: number;
coin_price: string; coin_price: string | null;
total_gas_used: string; total_gas_used: string;
transactions_today: string; transactions_today: string;
gas_used_today: string; gas_used_today: string;
......
...@@ -8,8 +8,6 @@ import contextWithEnvs from 'playwright/fixtures/contextWithEnvs'; ...@@ -8,8 +8,6 @@ import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import * as app from 'playwright/utils/app'; import * as app from 'playwright/utils/app';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
import * as configs from 'playwright/utils/configs';
import LayoutMainColumn from 'ui/shared/layout/components/MainColumn';
import SearchResults from './SearchResults'; import SearchResults from './SearchResults';
...@@ -47,17 +45,12 @@ test.describe('search by name ', () => { ...@@ -47,17 +45,12 @@ test.describe('search by name ', () => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<LayoutMainColumn> <SearchResults/>
<SearchResults/>
</LayoutMainColumn>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
await expect(component).toHaveScreenshot({ await expect(component.locator('main')).toHaveScreenshot();
mask: [ page.locator('header'), page.locator('form') ],
maskColor: configs.maskColor,
});
}); });
}); });
...@@ -78,17 +71,12 @@ test('search by address hash +@mobile', async({ mount, page }) => { ...@@ -78,17 +71,12 @@ test('search by address hash +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<LayoutMainColumn> <SearchResults/>
<SearchResults/>
</LayoutMainColumn>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
await expect(component).toHaveScreenshot({ await expect(component.locator('main')).toHaveScreenshot();
mask: [ page.locator('header'), page.locator('form') ],
maskColor: configs.maskColor,
});
}); });
test('search by block number +@mobile', async({ mount, page }) => { test('search by block number +@mobile', async({ mount, page }) => {
...@@ -109,17 +97,12 @@ test('search by block number +@mobile', async({ mount, page }) => { ...@@ -109,17 +97,12 @@ test('search by block number +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<LayoutMainColumn> <SearchResults/>
<SearchResults/>
</LayoutMainColumn>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
await expect(component).toHaveScreenshot({ await expect(component.locator('main')).toHaveScreenshot();
mask: [ page.locator('header'), page.locator('form') ],
maskColor: configs.maskColor,
});
}); });
test('search by block hash +@mobile', async({ mount, page }) => { test('search by block hash +@mobile', async({ mount, page }) => {
...@@ -139,17 +122,12 @@ test('search by block hash +@mobile', async({ mount, page }) => { ...@@ -139,17 +122,12 @@ test('search by block hash +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<LayoutMainColumn> <SearchResults/>
<SearchResults/>
</LayoutMainColumn>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
await expect(component).toHaveScreenshot({ await expect(component.locator('main')).toHaveScreenshot();
mask: [ page.locator('header'), page.locator('form') ],
maskColor: configs.maskColor,
});
}); });
test('search by tx hash +@mobile', async({ mount, page }) => { test('search by tx hash +@mobile', async({ mount, page }) => {
...@@ -169,17 +147,12 @@ test('search by tx hash +@mobile', async({ mount, page }) => { ...@@ -169,17 +147,12 @@ test('search by tx hash +@mobile', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<LayoutMainColumn> <SearchResults/>
<SearchResults/>
</LayoutMainColumn>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
await expect(component).toHaveScreenshot({ await expect(component.locator('main')).toHaveScreenshot();
mask: [ page.locator('header'), page.locator('form') ],
maskColor: configs.maskColor,
});
}); });
test.describe('with apps', () => { test.describe('with apps', () => {
...@@ -228,16 +201,11 @@ test.describe('with apps', () => { ...@@ -228,16 +201,11 @@ test.describe('with apps', () => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<LayoutMainColumn> <SearchResults/>
<SearchResults/>
</LayoutMainColumn>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
await expect(component).toHaveScreenshot({ await expect(component.locator('main')).toHaveScreenshot();
mask: [ page.locator('header'), page.locator('form') ],
maskColor: configs.maskColor,
});
}); });
}); });
...@@ -15,8 +15,9 @@ import * as Layout from 'ui/shared/layout/components'; ...@@ -15,8 +15,9 @@ import * as Layout from 'ui/shared/layout/components';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import Thead from 'ui/shared/TheadSticky'; import Thead from 'ui/shared/TheadSticky';
import Header from 'ui/snippets/header/Header';
import HeaderAlert from 'ui/snippets/header/HeaderAlert'; import HeaderAlert from 'ui/snippets/header/HeaderAlert';
import HeaderDesktop from 'ui/snippets/header/HeaderDesktop';
import HeaderMobile from 'ui/snippets/header/HeaderMobile';
import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery'; import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery';
const SearchResultsPageContent = () => { const SearchResultsPageContent = () => {
...@@ -181,13 +182,20 @@ const SearchResultsPageContent = () => { ...@@ -181,13 +182,20 @@ const SearchResultsPageContent = () => {
return ( return (
<> <>
<HeaderAlert/> <HeaderMobile renderSearchBar={ renderSearchBar }/>
<Header renderSearchBar={ renderSearchBar }/> <Layout.MainArea>
<AppErrorBoundary> <Layout.SideBar/>
<Layout.Content> <Layout.MainColumn>
{ pageContent } <HeaderAlert/>
</Layout.Content> <HeaderDesktop renderSearchBar={ renderSearchBar }/>
</AppErrorBoundary> <AppErrorBoundary>
<Layout.Content>
{ pageContent }
</Layout.Content>
</AppErrorBoundary>
</Layout.MainColumn>
</Layout.MainArea>
<Layout.Footer/>
</> </>
); );
}; };
......
...@@ -3,19 +3,22 @@ import React from 'react'; ...@@ -3,19 +3,22 @@ import React from 'react';
import type { Props } from './types'; import type { Props } from './types';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import Header from 'ui/snippets/header/Header';
import HeaderAlert from 'ui/snippets/header/HeaderAlert'; import HeaderAlert from 'ui/snippets/header/HeaderAlert';
import HeaderDesktop from 'ui/snippets/header/HeaderDesktop';
import HeaderMobile from 'ui/snippets/header/HeaderMobile';
import * as Layout from './components'; import * as Layout from './components';
const LayoutDefault = ({ children }: Props) => { const LayoutDefault = ({ children }: Props) => {
return ( return (
<Layout.Container> <Layout.Container>
<Layout.TopRow/>
<HeaderMobile/>
<Layout.MainArea> <Layout.MainArea>
<Layout.SideBar/> <Layout.SideBar/>
<Layout.MainColumn> <Layout.MainColumn>
<HeaderAlert/> <HeaderAlert/>
<Header/> <HeaderDesktop/>
<AppErrorBoundary> <AppErrorBoundary>
<Layout.Content> <Layout.Content>
{ children } { children }
......
...@@ -3,19 +3,22 @@ import React from 'react'; ...@@ -3,19 +3,22 @@ import React from 'react';
import type { Props } from './types'; import type { Props } from './types';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import Header from 'ui/snippets/header/Header';
import HeaderAlert from 'ui/snippets/header/HeaderAlert'; import HeaderAlert from 'ui/snippets/header/HeaderAlert';
import HeaderDesktop from 'ui/snippets/header/HeaderDesktop';
import HeaderMobile from 'ui/snippets/header/HeaderMobile';
import * as Layout from './components'; import * as Layout from './components';
const LayoutError = ({ children }: Props) => { const LayoutError = ({ children }: Props) => {
return ( return (
<Layout.Container> <Layout.Container>
<Layout.TopRow/>
<HeaderMobile/>
<Layout.MainArea> <Layout.MainArea>
<Layout.SideBar/> <Layout.SideBar/>
<Layout.MainColumn> <Layout.MainColumn>
<HeaderAlert/> <HeaderAlert/>
<Header/> <HeaderDesktop/>
<AppErrorBoundary> <AppErrorBoundary>
<main> <main>
{ children } { children }
......
...@@ -3,21 +3,22 @@ import React from 'react'; ...@@ -3,21 +3,22 @@ import React from 'react';
import type { Props } from './types'; import type { Props } from './types';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary'; import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import Header from 'ui/snippets/header/Header';
import HeaderAlert from 'ui/snippets/header/HeaderAlert'; import HeaderAlert from 'ui/snippets/header/HeaderAlert';
import HeaderMobile from 'ui/snippets/header/HeaderMobile';
import * as Layout from './components'; import * as Layout from './components';
const LayoutHome = ({ children }: Props) => { const LayoutHome = ({ children }: Props) => {
return ( return (
<Layout.Container> <Layout.Container>
<Layout.TopRow/>
<HeaderMobile isHomePage/>
<Layout.MainArea> <Layout.MainArea>
<Layout.SideBar/> <Layout.SideBar/>
<Layout.MainColumn <Layout.MainColumn
paddingTop={{ base: '88px', lg: 9 }} paddingTop={{ base: 6, lg: 9 }}
> >
<HeaderAlert/> <HeaderAlert/>
<Header isHomePage/>
<AppErrorBoundary> <AppErrorBoundary>
{ children } { children }
</AppErrorBoundary> </AppErrorBoundary>
......
...@@ -8,13 +8,8 @@ const LayoutSearchResults = ({ children }: Props) => { ...@@ -8,13 +8,8 @@ const LayoutSearchResults = ({ children }: Props) => {
return ( return (
<Layout.Container> <Layout.Container>
<Layout.MainArea> <Layout.TopRow/>
<Layout.SideBar/> { children }
<Layout.MainColumn>
{ children }
</Layout.MainColumn>
</Layout.MainArea>
<Layout.Footer/>
</Layout.Container> </Layout.Container>
); );
}; };
......
...@@ -14,7 +14,7 @@ const MainColumn = ({ children, className }: Props) => { ...@@ -14,7 +14,7 @@ const MainColumn = ({ children, className }: Props) => {
flexGrow={ 1 } flexGrow={ 1 }
w={{ base: '100%', lg: 'auto' }} w={{ base: '100%', lg: 'auto' }}
paddingX={{ base: 4, lg: 12 }} paddingX={{ base: 4, lg: 12 }}
paddingTop={{ base: '138px', lg: 9 }} paddingTop={{ base: `${ 32 + 60 }px`, lg: 9 }} // 32px is top padding of content area, 60px is search bar height
paddingBottom={ 10 } paddingBottom={ 10 }
> >
{ children } { children }
......
import Footer from 'ui/snippets/footer/Footer'; import Footer from 'ui/snippets/footer/Footer';
import TopRow from 'ui/snippets/topBar/TopBar';
import Container from './Container'; import Container from './Container';
import Content from './Content'; import Content from './Content';
...@@ -13,9 +14,11 @@ export { ...@@ -13,9 +14,11 @@ export {
SideBar, SideBar,
MainColumn, MainColumn,
Footer, Footer,
TopRow,
}; };
// Container // Container
// TopRow
// MainArea // MainArea
// SideBar // SideBar
// MainColumn // MainColumn
......
import type { GridProps } from '@chakra-ui/react';
import { Box, Grid, Flex, Text, Link, VStack, Skeleton } from '@chakra-ui/react'; import { Box, Grid, Flex, Text, Link, VStack, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
...@@ -18,7 +19,6 @@ import useFetch from 'lib/hooks/useFetch'; ...@@ -18,7 +19,6 @@ import useFetch from 'lib/hooks/useFetch';
import useIssueUrl from 'lib/hooks/useIssueUrl'; import useIssueUrl from 'lib/hooks/useIssueUrl';
import NetworkAddToWallet from 'ui/shared/NetworkAddToWallet'; import NetworkAddToWallet from 'ui/shared/NetworkAddToWallet';
import ColorModeToggler from '../header/ColorModeToggler';
import FooterLinkItem from './FooterLinkItem'; import FooterLinkItem from './FooterLinkItem';
import IntTxsIndexingStatus from './IntTxsIndexingStatus'; import IntTxsIndexingStatus from './IntTxsIndexingStatus';
import getApiVersionUrl from './utils/getApiVersionUrl'; import getApiVersionUrl from './utils/getApiVersionUrl';
...@@ -96,41 +96,43 @@ const Footer = () => { ...@@ -96,41 +96,43 @@ const Footer = () => {
const fetch = useFetch(); const fetch = useFetch();
const { isPending, data: linksData } = useQuery<unknown, ResourceError<unknown>, Array<CustomLinksGroup>>({ const { isPlaceholderData, data: linksData } = useQuery<unknown, ResourceError<unknown>, Array<CustomLinksGroup>>({
queryKey: [ 'footer-links' ], queryKey: [ 'footer-links' ],
queryFn: async() => fetch(config.UI.footer.links || '', undefined, { resource: 'footer-links' }), queryFn: async() => fetch(config.UI.footer.links || '', undefined, { resource: 'footer-links' }),
enabled: Boolean(config.UI.footer.links), enabled: Boolean(config.UI.footer.links),
staleTime: Infinity, staleTime: Infinity,
placeholderData: [],
}); });
const colNum = Math.min(linksData?.length || Infinity, MAX_LINKS_COLUMNS) + 1; const colNum = isPlaceholderData ? 1 : Math.min(linksData?.length || Infinity, MAX_LINKS_COLUMNS) + 1;
return ( const renderNetworkInfo = React.useCallback((gridArea?: GridProps['gridArea']) => {
<Flex return (
direction={{ base: 'column', lg: 'row' }} <Flex
px={{ base: 4, lg: 12 }} gridArea={ gridArea }
py={{ base: 4, lg: 9 }} flexWrap="wrap"
borderTop="1px solid" columnGap={ 8 }
borderColor="divider" rowGap={ 6 }
as="footer" mb={{ base: 5, lg: 10 }}
columnGap={{ lg: '32px', xl: '100px' }} _empty={{ display: 'none' }}
> >
<Box flexGrow="1" mb={{ base: 8, lg: 0 }} minW="195px"> { !config.UI.indexingAlert.intTxs.isHidden && <IntTxsIndexingStatus/> }
<Flex flexWrap="wrap" columnGap={ 8 } rowGap={ 6 }> <NetworkAddToWallet/>
<ColorModeToggler/> </Flex>
{ !config.UI.indexingAlert.intTxs.isHidden && <IntTxsIndexingStatus/> } );
<NetworkAddToWallet/> }, []);
</Flex>
<Box mt={{ base: 5, lg: '44px' }}> const renderProjectInfo = React.useCallback((gridArea?: GridProps['gridArea']) => {
<Link fontSize="xs" href="https://www.blockscout.com">blockscout.com</Link> return (
</Box> <Box gridArea={ gridArea }>
<Text mt={ 3 } maxW={{ base: 'unset', lg: '470px' }} fontSize="xs"> <Link fontSize="xs" href="https://www.blockscout.com">blockscout.com</Link>
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks. <Text mt={ 3 } fontSize="xs">
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text> </Text>
<VStack spacing={ 1 } mt={ 6 } alignItems="start"> <VStack spacing={ 1 } mt={ 6 } alignItems="start">
{ apiVersionUrl && ( { apiVersionUrl && (
<Text fontSize="xs"> <Text fontSize="xs">
Backend: <Link href={ apiVersionUrl } target="_blank">{ backendVersionData?.backend_version }</Link> Backend: <Link href={ apiVersionUrl } target="_blank">{ backendVersionData?.backend_version }</Link>
</Text> </Text>
) } ) }
{ frontendLink && ( { frontendLink && (
...@@ -140,64 +142,93 @@ const Footer = () => { ...@@ -140,64 +142,93 @@ const Footer = () => {
) } ) }
</VStack> </VStack>
</Box> </Box>
<Grid );
gap={{ base: 6, lg: config.UI.footer.links && colNum === MAX_LINKS_COLUMNS + 1 ? 2 : 8, xl: 12 }} }, [ apiVersionUrl, backendVersionData?.backend_version, frontendLink ]);
gridTemplateColumns={ config.UI.footer.links ?
{ const containerProps: GridProps = {
as: 'footer',
px: { base: 4, lg: 12 },
py: { base: 4, lg: 9 },
borderTop: '1px solid',
borderColor: 'divider',
gridTemplateColumns: { base: '1fr', lg: 'minmax(auto, 470px) 1fr' },
columnGap: { lg: '32px', xl: '100px' },
};
if (config.UI.footer.links) {
return (
<Grid { ...containerProps }>
<div>
{ renderNetworkInfo() }
{ renderProjectInfo() }
</div>
<Grid
gap={{ base: 6, lg: colNum === MAX_LINKS_COLUMNS + 1 ? 2 : 8, xl: 12 }}
gridTemplateColumns={{
base: 'repeat(auto-fill, 160px)', base: 'repeat(auto-fill, 160px)',
lg: `repeat(${ colNum }, 135px)`, lg: `repeat(${ colNum }, 135px)`,
xl: `repeat(${ colNum }, 160px)`, xl: `repeat(${ colNum }, 160px)`,
} : }}
'auto' justifyContent={{ lg: 'flex-end' }}
} mt={{ base: 8, lg: 0 }}
>
{
([
{ title: 'Blockscout', links: BLOCKSCOUT_LINKS },
...(linksData || []),
])
.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">
{ linkGroup.links.map(link => <FooterLinkItem { ...link } key={ link.text } isLoading={ isPlaceholderData }/>) }
</VStack>
</Box>
))
}
</Grid>
</Grid>
);
}
return (
<Grid
{ ...containerProps }
gridTemplateAreas={{
lg: `
"network links-top"
"info links-bottom"
`,
}}
>
{ renderNetworkInfo({ lg: 'network' }) }
{ renderProjectInfo({ lg: 'info' }) }
<Grid
gridArea={{ lg: 'links-bottom' }}
gap={ 1 }
gridTemplateColumns={{
base: 'repeat(auto-fill, 160px)',
lg: 'repeat(3, 160px)',
xl: 'repeat(4, 160px)',
}}
gridTemplateRows={{
base: 'auto',
lg: 'repeat(3, auto)',
xl: 'repeat(2, auto)',
}}
gridAutoFlow={{ base: 'row', lg: 'column' }}
alignContent="start"
justifyContent={{ lg: 'flex-end' }}
mt={{ base: 8, lg: 0 }}
> >
<Box> { BLOCKSCOUT_LINKS.map(link => <FooterLinkItem { ...link } key={ link.text }/>) }
{ config.UI.footer.links && <Text fontWeight={ 500 } mb={ 3 }>Blockscout</Text> }
<Grid
gap={ 1 }
gridTemplateColumns={
config.UI.footer.links ?
'1fr' :
{
base: 'repeat(auto-fill, 160px)',
lg: 'repeat(3, 160px)',
xl: 'repeat(4, 160px)',
}
}
gridTemplateRows={{
base: 'auto',
lg: config.UI.footer.links ? 'auto' : 'repeat(3, auto)',
xl: config.UI.footer.links ? 'auto' : 'repeat(2, auto)',
}}
gridAutoFlow={{ base: 'row', lg: config.UI.footer.links ? 'row' : 'column' }}
mt={{ base: 0, lg: config.UI.footer.links ? 0 : '100px' }}
>
{ BLOCKSCOUT_LINKS.map(link => <FooterLinkItem { ...link } key={ link.text }/>) }
</Grid>
</Box>
{ config.UI.footer.links && isPending && (
Array.from(Array(3)).map((i, index) => (
<Box key={ index }>
<Skeleton w="100%" h="20px" mb={ 6 }/>
<VStack spacing={ 5 } alignItems="start" mb={ 2 }>
{ Array.from(Array(5)).map((i, index) => <Skeleton w="100%" h="14px" key={ index }/>) }
</VStack>
</Box>
))
) }
{ config.UI.footer.links && linksData && (
linksData.slice(0, MAX_LINKS_COLUMNS).map(linkGroup => (
<Box key={ linkGroup.title }>
<Text fontWeight={ 500 } mb={ 3 }>{ linkGroup.title }</Text>
<VStack spacing={ 1 } alignItems="start">
{ linkGroup.links.map(link => <FooterLinkItem { ...link } key={ link.text }/>) }
</VStack>
</Box>
))
) }
</Grid> </Grid>
</Flex> </Grid>
); );
}; };
export default Footer; export default React.memo(Footer);
import { Center, Icon, Link } from '@chakra-ui/react'; import { Center, Icon, Link, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
type Props = { type Props = {
...@@ -6,9 +6,14 @@ type Props = { ...@@ -6,9 +6,14 @@ type Props = {
iconSize?: string; iconSize?: string;
text: string; text: string;
url: string; url: string;
isLoading?: boolean;
} }
const FooterLinkItem = ({ icon, iconSize, text, url }: Props) => { const FooterLinkItem = ({ icon, iconSize, text, url, isLoading }: Props) => {
if (isLoading) {
return <Skeleton my="3px">{ text }</Skeleton>;
}
return ( 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" variant="secondary" target="_blank" fontSize="xs">
{ icon && ( { icon && (
......
import type { UseCheckboxProps } from '@chakra-ui/checkbox';
import { useCheckbox } from '@chakra-ui/checkbox';
import { useColorMode, useColorModeValue, Icon } from '@chakra-ui/react';
import type {
SystemStyleObject,
ThemingProps,
HTMLChakraProps,
} from '@chakra-ui/system';
import {
chakra,
forwardRef,
omitThemingProps,
} from '@chakra-ui/system';
import { dataAttr, __DEV__ } from '@chakra-ui/utils';
import * as React from 'react';
import moonIcon from 'icons/moon.svg';
import sunIcon from 'icons/sun.svg';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
export interface ColorModeTogglerProps
extends Omit<UseCheckboxProps, 'isIndeterminate'>,
Omit<HTMLChakraProps<'label'>, keyof UseCheckboxProps>,
ThemingProps<'Switch'> {
trackBg?: string;
}
const ColorModeToggler = forwardRef<ColorModeTogglerProps, 'input'>((props, ref) => {
const ownProps = omitThemingProps(props);
const { toggleColorMode, colorMode } = useColorMode();
const {
state,
getInputProps,
getCheckboxProps,
getRootProps,
} = useCheckbox({ ...ownProps, isChecked: colorMode === 'light' });
const trackBg = useColorModeValue('blackAlpha.100', 'whiteAlpha.100');
const thumbBg = 'white';
const transitionProps = getDefaultTransitionProps();
const trackStyles: SystemStyleObject = React.useMemo(() => ({
bgColor: props.trackBg || trackBg,
width: '72px',
height: '32px',
borderRadius: 'full',
display: 'inline-flex',
flexShrink: 0,
justifyContent: 'space-between',
boxSizing: 'content-box',
cursor: 'pointer',
...transitionProps,
transitionDuration: 'ultra-slow',
}), [ props.trackBg, trackBg, transitionProps ]);
const thumbStyles: SystemStyleObject = React.useMemo(() => ({
bg: thumbBg,
boxShadow: 'md',
width: '24px',
height: '24px',
borderRadius: 'md',
position: 'absolute',
transform: state.isChecked ? 'translate(44px, 4px)' : 'translate(4px, 4px)',
...transitionProps,
transitionProperty: 'background-color, transform',
transitionDuration: 'ultra-slow',
}), [ thumbBg, transitionProps, state.isChecked ]);
return (
<chakra.label
{ ...getRootProps({ onChange: toggleColorMode }) }
display="inline-block"
position="relative"
verticalAlign="middle"
lineHeight={ 0 }
>
<chakra.input
{ ...getInputProps({}, ref) }
border="none"
height="1px"
width="1px"
margin="1px"
padding="0"
overflow="hidden"
whiteSpace="nowrap"
position="absolute"
/>
<chakra.div
{ ...getCheckboxProps() }
__css={ trackStyles }
aria-label="Toggle color mode"
>
<Icon
boxSize={ 4 }
margin={ 2 }
zIndex="docked"
as={ moonIcon }
color={ useColorModeValue('blue.300', 'blackAlpha.900') }
{ ...transitionProps }
/>
<chakra.div
data-checked={ dataAttr(state.isChecked) }
data-hover={ dataAttr(state.isHovered) }
__css={ thumbStyles }
/>
<Icon
boxSize={ 5 }
margin={ 1.5 }
zIndex="docked"
as={ sunIcon }
color={ useColorModeValue('blackAlpha.900', 'blue.300') }
{ ...transitionProps }
/>
</chakra.div>
</chakra.label>
);
});
if (__DEV__) {
ColorModeToggler.displayName = 'ColorModeToggler';
}
export default ColorModeToggler;
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import HeaderDesktop from './HeaderDesktop';
test('default view +@dark-mode', async({ mount }) => {
const component = await mount(
<TestApp>
<HeaderDesktop/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { HStack, Box } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import SearchBar from 'ui/snippets/searchBar/SearchBar';
type Props = {
renderSearchBar?: () => React.ReactNode;
}
const HeaderDesktop = ({ renderSearchBar }: Props) => {
const searchBar = renderSearchBar ? renderSearchBar() : <SearchBar/>;
return (
<HStack
as="header"
display={{ base: 'none', lg: 'flex' }}
width="100%"
alignItems="center"
justifyContent="center"
gap={ 12 }
>
<Box width="100%">
{ searchBar }
</Box>
{ config.features.account.isEnabled && <ProfileMenuDesktop/> }
</HStack>
);
};
export default React.memo(HeaderDesktop);
import { test, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import HeaderMobile from './HeaderMobile';
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('default view +@dark-mode', async({ mount, page }) => {
await mount(
<TestApp>
<HeaderMobile/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 150 } });
});
import { HStack, Box, Flex, useColorModeValue } from '@chakra-ui/react'; import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useInView } from 'react-intersection-observer';
import config from 'configs/app'; import config from 'configs/app';
import { useScrollDirection } from 'lib/contexts/scrollDirection'; import { useScrollDirection } from 'lib/contexts/scrollDirection';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile'; import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile';
import SearchBar from 'ui/snippets/searchBar/SearchBar'; import SearchBar from 'ui/snippets/searchBar/SearchBar';
...@@ -15,55 +15,43 @@ type Props = { ...@@ -15,55 +15,43 @@ type Props = {
renderSearchBar?: () => React.ReactNode; renderSearchBar?: () => React.ReactNode;
} }
const Header = ({ isHomePage, renderSearchBar }: Props) => { const HeaderMobile = ({ isHomePage, renderSearchBar }: Props) => {
const bgColor = useColorModeValue('white', 'black'); const bgColor = useColorModeValue('white', 'black');
const scrollDirection = useScrollDirection(); const scrollDirection = useScrollDirection();
const { ref, inView } = useInView({ threshold: 1 });
const searchBar = renderSearchBar ? renderSearchBar() : <SearchBar/>; const searchBar = renderSearchBar ? renderSearchBar() : <SearchBar/>;
return ( return (
<> <Box
<Box bgColor={ bgColor } display={{ base: 'block', lg: 'none' }}> ref={ ref }
<Flex bgColor={ bgColor }
as="header" display={{ base: 'block', lg: 'none' }}
position="fixed" position="sticky"
top={ 0 } top="-1px"
left={ 0 } left={ 0 }
paddingX={ 4 } zIndex="sticky2"
paddingY={ 2 } pt="1px"
bgColor={ bgColor } >
width="100%" <Flex
alignItems="center" as="header"
justifyContent="space-between" paddingX={ 4 }
zIndex="sticky2" paddingY={ 2 }
transitionProperty="box-shadow" bgColor={ bgColor }
transitionDuration="slow" width="100%"
boxShadow={ scrollDirection === 'down' ? 'md' : 'none' } alignItems="center"
> justifyContent="space-between"
<Burger/> transitionProperty="box-shadow"
<NetworkLogo/> transitionDuration="slow"
{ config.features.account.isEnabled ? <ProfileMenuMobile/> : <Box boxSize={ 10 }/> } boxShadow={ !inView && scrollDirection === 'down' ? 'md' : 'none' }
</Flex> >
{ !isHomePage && searchBar } <Burger/>
</Box> <NetworkLogo/>
<Box display={{ base: 'none', lg: 'block' }}> { config.features.account.isEnabled ? <ProfileMenuMobile/> : <Box boxSize={ 10 }/> }
{ !isHomePage && ( </Flex>
<HStack { !isHomePage && searchBar }
as="header" </Box>
width="100%"
alignItems="center"
justifyContent="center"
gap={ 12 }
>
<Box width="100%">
{ searchBar }
</Box>
{ config.features.account.isEnabled && <ProfileMenuDesktop/> }
</HStack>
) }
</Box>
</>
); );
}; };
export default Header; export default React.memo(HeaderMobile);
...@@ -25,7 +25,8 @@ const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, onHid ...@@ -25,7 +25,8 @@ const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, onHid
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const handleScroll = React.useCallback(() => { const handleScroll = React.useCallback(() => {
if (window.pageYOffset !== 0) { const TOP_BAR_HEIGHT = 36;
if (window.pageYOffset >= TOP_BAR_HEIGHT) {
setIsSticky(true); setIsSticky(true);
} else { } else {
setIsSticky(false); setIsSticky(false);
...@@ -70,10 +71,10 @@ const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, onHid ...@@ -70,10 +71,10 @@ const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, onHid
w="100%" w="100%"
backgroundColor={ bgColor } backgroundColor={ bgColor }
borderRadius={{ base: isHomepage ? 'base' : 'none', lg: 'base' }} borderRadius={{ base: isHomepage ? 'base' : 'none', lg: 'base' }}
position={{ base: isHomepage ? 'static' : 'fixed', lg: 'static' }} position={{ base: isHomepage ? 'static' : 'absolute', lg: 'static' }}
top={{ base: isHomepage ? 0 : 55, lg: 0 }} top={{ base: isHomepage ? 0 : 55, lg: 0 }}
left="0" left="0"
zIndex={{ base: isHomepage ? 'auto' : 'sticky1', lg: 'auto' }} zIndex={{ base: isHomepage ? 'auto' : '-1', lg: 'auto' }}
paddingX={{ base: isHomepage ? 0 : 4, lg: 0 }} paddingX={{ base: isHomepage ? 0 : 4, lg: 0 }}
paddingTop={{ base: isHomepage ? 0 : 1, lg: 0 }} paddingTop={{ base: isHomepage ? 0 : 1, lg: 0 }}
paddingBottom={{ base: isHomepage ? 0 : 4, lg: 0 }} paddingBottom={{ base: isHomepage ? 0 : 4, lg: 0 }}
......
import {
IconButton,
Icon,
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
useColorMode,
useDisclosure,
Skeleton,
} from '@chakra-ui/react';
import React from 'react';
import * as cookies from 'lib/cookies';
import ColorModeSwitchTheme from './ColorModeSwitchTheme';
import { COLOR_THEMES } from './utils';
const ColorModeSwitch = () => {
const { isOpen, onToggle, onClose } = useDisclosure();
const { setColorMode, colorMode } = useColorMode();
const [ activeHex, setActiveHex ] = React.useState<string>();
const setTheme = React.useCallback((hex: string) => {
const nextTheme = COLOR_THEMES.find((theme) => theme.colors.some((color) => color.hex === hex));
if (!nextTheme) {
return;
}
setColorMode(nextTheme.colorMode);
const varName = nextTheme.colorMode === 'light' ? '--chakra-colors-white' : '--chakra-colors-black';
window.document.documentElement.style.setProperty(varName, hex);
cookies.set(cookies.NAMES.COLOR_MODE_HEX, hex);
}, [ setColorMode ]);
React.useEffect(() => {
const cookieColorMode = cookies.get(cookies.NAMES.COLOR_MODE);
const nextColorMode = (() => {
if (!cookieColorMode) {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return colorMode;
})();
const fallbackHex = (COLOR_THEMES.find(theme => theme.colorMode === nextColorMode && theme.colors.length === 1) ?? COLOR_THEMES[0]).colors[0].hex;
const cookieHex = cookies.get(cookies.NAMES.COLOR_MODE_HEX) ?? fallbackHex;
setTheme(cookieHex);
setActiveHex(cookieHex);
// should run only on mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ]);
const handleSelect = React.useCallback((event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
const hex = event.currentTarget.getAttribute('data-hex');
if (!hex) {
return;
}
setTheme(hex);
setActiveHex(hex);
}, [ setTheme ]);
const activeTheme = COLOR_THEMES.find((theme) => theme.colors.some((color) => color.hex === activeHex));
return (
<Popover placement="bottom-start" isLazy trigger="click" isOpen={ isOpen } onClose={ onClose }>
<PopoverTrigger>
{ activeTheme ? (
<IconButton
variant="simple"
colorScheme="blue"
aria-label="color mode switch"
icon={ <Icon as={ activeTheme.icon } boxSize={ 5 }/> }
boxSize={ 5 }
onClick={ onToggle }
/>
) : <Skeleton boxSize={ 5 } borderRadius="sm"/> }
</PopoverTrigger>
<PopoverContent overflowY="hidden" w="164px" fontSize="sm">
<PopoverBody boxShadow="2xl" p={ 3 }>
{ COLOR_THEMES.map((theme) => <ColorModeSwitchTheme key={ theme.name } { ...theme } onClick={ handleSelect } activeHex={ activeHex }/>) }
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default ColorModeSwitch;
import {
Box,
useColorModeValue,
useToken,
} from '@chakra-ui/react';
import React from 'react';
import type { ColorThemeColor } from './utils';
interface Props extends ColorThemeColor {
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
isActive: boolean;
}
const ColorModeSwitchSample = ({ hex, sampleBg, onClick, isActive }: Props) => {
const bgColor = useColorModeValue('white', 'gray.900');
const activeBgColor = useColorModeValue('blue.50', 'blackAlpha.800');
const activeBorderColor = useToken('colors', useColorModeValue('blackAlpha.800', 'gray.50'));
const hoverBorderColor = useToken('colors', 'link_hovered');
return (
<Box
bg={ sampleBg }
boxSize={ 5 }
borderRadius="full"
borderWidth="1px"
borderColor={ isActive ? activeBgColor : bgColor }
position="relative"
_before={{
position: 'absolute',
display: 'block',
content: '""',
top: '-3px',
left: '-3px',
width: 'calc(100% + 2px)',
height: 'calc(100% + 2px)',
borderStyle: 'solid',
borderRadius: 'full',
borderWidth: '2px',
borderColor: isActive ? activeBorderColor : 'transparent',
}}
_hover={{
_before: {
borderColor: isActive ? activeBorderColor : hoverBorderColor,
},
}}
data-hex={ hex }
onClick={ onClick }
/>
);
};
export default ColorModeSwitchSample;
import {
Icon,
Flex,
useColorModeValue,
useToken,
} from '@chakra-ui/react';
import React from 'react';
import ColorModeSwitchSample from './ColorModeSwitchSample';
import type { ColorTheme } from './utils';
interface Props extends ColorTheme {
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
activeHex: string | undefined;
}
const ColorModeSwitchTheme = ({ icon, name, colors, onClick, activeHex }: Props) => {
const isActive = colors.some((sample) => sample.hex === activeHex);
const activeColor = useColorModeValue('blackAlpha.800', 'gray.50');
const activeBgColor = useColorModeValue('blue.50', 'blackAlpha.800');
const inactiveColor = useColorModeValue('blue.700', 'gray.400');
const hoverBorderColor = useToken('colors', 'link_hovered');
const hasOneColor = colors.length === 1;
return (
<Flex
alignItems="center"
py="10px"
px="6px"
cursor="pointer"
color={ isActive ? activeColor : inactiveColor }
bgColor={ isActive ? activeBgColor : undefined }
_hover={{
color: isActive ? undefined : 'link_hovered',
'& [data-hex]': !isActive && hasOneColor ? {
_before: {
borderColor: hoverBorderColor,
},
} : undefined,
}}
onClick={ onClick }
data-hex={ colors[0].hex }
fontWeight={ 500 }
borderRadius="base"
>
<Icon as={ icon } boxSize={ 5 } mr={ 2 }/>
<span>{ name }</span>
<Flex columnGap={ 2 } ml="auto" alignItems="center">
{ colors.map((sample) => <ColorModeSwitchSample key={ sample.hex } { ...sample } onClick={ onClick } isActive={ activeHex === sample.hex }/>) }
</Flex>
</Flex>
);
};
export default ColorModeSwitchTheme;
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as statsMock from 'mocks/stats/index';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import Header from './Header'; import TopBar from './TopBar';
test('no auth +@mobile', async({ mount, page }) => { test('default view +@dark-mode +@mobile', async({ mount, page }) => {
await mount( await page.route(buildApiUrl('homepage_stats'), (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
const component = await mount(
<TestApp> <TestApp>
<Header/> <TopBar/>
</TestApp>, </TestApp>,
); );
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 150 } }); await component.getByLabel('color mode switch').click();
});
test.describe('dark mode', () => {
test.use({ colorScheme: 'dark' });
test('+@mobile', async({ mount, page }) => {
await mount(
<TestApp>
<Header/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 150 } }); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 220 } });
});
}); });
import { Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import ColorModeSwitch from './ColorModeSwitch';
import TopBarStats from './TopBarStats';
const TopBar = () => {
const bgColor = useColorModeValue('gray.50', 'whiteAlpha.100');
return (
<Flex
py={ 2 }
px={ 6 }
bgColor={ bgColor }
justifyContent="space-between"
>
<TopBarStats/>
<ColorModeSwitch/>
</Flex>
);
};
export default React.memo(TopBar);
import { Flex, Skeleton } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { HOMEPAGE_STATS } from 'stubs/stats';
import TextSeparator from 'ui/shared/TextSeparator';
const TopBarStats = () => {
const { data, isPlaceholderData, isError } = useApiQuery('homepage_stats', {
queryOptions: {
placeholderData: HOMEPAGE_STATS,
refetchOnMount: false,
},
});
if (isError) {
return <div/>;
}
return (
<Flex
alignItems="center"
fontSize="xs"
fontWeight={ 500 }
>
{ data?.coin_price && (
<Skeleton isLoaded={ !isPlaceholderData }>
<span>{ config.chain.governanceToken.symbol || config.chain.currency.symbol }: </span>
<span>${ Number(data.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }) }</span>
</Skeleton>
) }
{ data?.coin_price && data.gas_prices && <TextSeparator color="divider"/> }
{ data?.gas_prices && (
<Skeleton isLoaded={ !isPlaceholderData }>
<span>Gas: { data.gas_prices.average } Gwei</span>
</Skeleton>
) }
</Flex>
);
};
export default React.memo(TopBarStats);
import moonWithStarIcon from 'icons/moon-with-star.svg';
import moonIcon from 'icons/moon.svg';
import sunIcon from 'icons/sun.svg';
export const COLOR_THEMES = [
{
name: 'Light',
colorMode: 'light',
icon: sunIcon,
colors: [
{ hex: '#FFFFFF', sampleBg: 'linear-gradient(154deg, #EFEFEF 50%, rgba(255, 255, 255, 0.00) 330.86%)' },
],
},
{
name: 'Dim',
colorMode: 'dark',
icon: moonWithStarIcon,
colors: [
{ hex: '#232B37', sampleBg: 'linear-gradient(152deg, #232B37 50%, rgba(255, 255, 255, 0.00) 290.71%)' },
{ hex: '#1B2E48', sampleBg: 'linear-gradient(150deg, #1B2E48 50%, rgba(255, 255, 255, 0.00) 312.75%)' },
],
},
{
name: 'Dark',
colorMode: 'dark',
icon: moonIcon,
colors: [
{ hex: '#101112', sampleBg: 'linear-gradient(161deg, #000 9.37%, #383838 92.52%)' },
],
},
];
export type ColorTheme = typeof COLOR_THEMES[number];
export type ColorThemeColor = ColorTheme['colors'][number];
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