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
@@ -0,0 +1,4 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 630 B |
@@ -1,3 +1,3 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
||||
<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"/>
|
||||
<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"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 419 B After Width: | Height: | Size: 413 B |
@@ -1,3 +1,3 @@
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 1021 B After Width: | Height: | Size: 1.0 KiB |
@@ -9,6 +9,7 @@ export enum NAMES {
|
||||
CONFIRM_EMAIL_PAGE_VIEWED='confirm_email_page_viewed',
|
||||
TXS_SORT='txs_sort',
|
||||
COLOR_MODE='chakra-ui-color-mode',
|
||||
COLOR_MODE_HEX='chakra-ui-color-mode-hex',
|
||||
INDEXING_ALERT='indexing_alert',
|
||||
ADBLOCK_DETECTED='adblock_detected',
|
||||
MIXPANEL_DEBUG='_mixpanel_debug',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// https://unicode-table.com
|
||||
export const asymp = String.fromCharCode(8776); // ~
|
||||
// https://symbl.cc/en/
|
||||
export const asymp = String.fromCharCode(8776); // ≈
|
||||
export const tilde = String.fromCharCode(126); // ~
|
||||
export const hellip = String.fromCharCode(8230); // …
|
||||
export const nbsp = String.fromCharCode(160); // no-break Space
|
||||
export const thinsp = String.fromCharCode(8201); // thin Space
|
||||
|
||||
@@ -3,7 +3,7 @@ export type HomeStats = {
|
||||
total_addresses: string;
|
||||
total_transactions: string;
|
||||
average_block_time: number;
|
||||
coin_price: string;
|
||||
coin_price: string | null;
|
||||
total_gas_used: string;
|
||||
transactions_today: string;
|
||||
gas_used_today: string;
|
||||
|
||||
@@ -8,8 +8,6 @@ import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
|
||||
import TestApp from 'playwright/TestApp';
|
||||
import * as app from 'playwright/utils/app';
|
||||
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';
|
||||
|
||||
@@ -47,17 +45,12 @@ test.describe('search by name ', () => {
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<LayoutMainColumn>
|
||||
<SearchResults/>
|
||||
</LayoutMainColumn>
|
||||
<SearchResults/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot({
|
||||
mask: [ page.locator('header'), page.locator('form') ],
|
||||
maskColor: configs.maskColor,
|
||||
});
|
||||
await expect(component.locator('main')).toHaveScreenshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,17 +71,12 @@ test('search by address hash +@mobile', async({ mount, page }) => {
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<LayoutMainColumn>
|
||||
<SearchResults/>
|
||||
</LayoutMainColumn>
|
||||
<SearchResults/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot({
|
||||
mask: [ page.locator('header'), page.locator('form') ],
|
||||
maskColor: configs.maskColor,
|
||||
});
|
||||
await expect(component.locator('main')).toHaveScreenshot();
|
||||
});
|
||||
|
||||
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(
|
||||
<TestApp>
|
||||
<LayoutMainColumn>
|
||||
<SearchResults/>
|
||||
</LayoutMainColumn>
|
||||
<SearchResults/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot({
|
||||
mask: [ page.locator('header'), page.locator('form') ],
|
||||
maskColor: configs.maskColor,
|
||||
});
|
||||
await expect(component.locator('main')).toHaveScreenshot();
|
||||
});
|
||||
|
||||
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(
|
||||
<TestApp>
|
||||
<LayoutMainColumn>
|
||||
<SearchResults/>
|
||||
</LayoutMainColumn>
|
||||
<SearchResults/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot({
|
||||
mask: [ page.locator('header'), page.locator('form') ],
|
||||
maskColor: configs.maskColor,
|
||||
});
|
||||
await expect(component.locator('main')).toHaveScreenshot();
|
||||
});
|
||||
|
||||
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(
|
||||
<TestApp>
|
||||
<LayoutMainColumn>
|
||||
<SearchResults/>
|
||||
</LayoutMainColumn>
|
||||
<SearchResults/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot({
|
||||
mask: [ page.locator('header'), page.locator('form') ],
|
||||
maskColor: configs.maskColor,
|
||||
});
|
||||
await expect(component.locator('main')).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test.describe('with apps', () => {
|
||||
@@ -228,16 +201,11 @@ test.describe('with apps', () => {
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<LayoutMainColumn>
|
||||
<SearchResults/>
|
||||
</LayoutMainColumn>
|
||||
<SearchResults/>
|
||||
</TestApp>,
|
||||
{ hooksConfig },
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot({
|
||||
mask: [ page.locator('header'), page.locator('form') ],
|
||||
maskColor: configs.maskColor,
|
||||
});
|
||||
await expect(component.locator('main')).toHaveScreenshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,8 +15,9 @@ import * as Layout from 'ui/shared/layout/components';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import Pagination from 'ui/shared/pagination/Pagination';
|
||||
import Thead from 'ui/shared/TheadSticky';
|
||||
import Header from 'ui/snippets/header/Header';
|
||||
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';
|
||||
|
||||
const SearchResultsPageContent = () => {
|
||||
@@ -181,13 +182,20 @@ const SearchResultsPageContent = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderAlert/>
|
||||
<Header renderSearchBar={ renderSearchBar }/>
|
||||
<AppErrorBoundary>
|
||||
<Layout.Content>
|
||||
{ pageContent }
|
||||
</Layout.Content>
|
||||
</AppErrorBoundary>
|
||||
<HeaderMobile renderSearchBar={ renderSearchBar }/>
|
||||
<Layout.MainArea>
|
||||
<Layout.SideBar/>
|
||||
<Layout.MainColumn>
|
||||
<HeaderAlert/>
|
||||
<HeaderDesktop renderSearchBar={ renderSearchBar }/>
|
||||
<AppErrorBoundary>
|
||||
<Layout.Content>
|
||||
{ pageContent }
|
||||
</Layout.Content>
|
||||
</AppErrorBoundary>
|
||||
</Layout.MainColumn>
|
||||
</Layout.MainArea>
|
||||
<Layout.Footer/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 34 KiB |
@@ -3,19 +3,22 @@ import React from 'react';
|
||||
import type { Props } from './types';
|
||||
|
||||
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
|
||||
import Header from 'ui/snippets/header/Header';
|
||||
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';
|
||||
|
||||
const LayoutDefault = ({ children }: Props) => {
|
||||
return (
|
||||
<Layout.Container>
|
||||
<Layout.TopRow/>
|
||||
<HeaderMobile/>
|
||||
<Layout.MainArea>
|
||||
<Layout.SideBar/>
|
||||
<Layout.MainColumn>
|
||||
<HeaderAlert/>
|
||||
<Header/>
|
||||
<HeaderDesktop/>
|
||||
<AppErrorBoundary>
|
||||
<Layout.Content>
|
||||
{ children }
|
||||
|
||||
@@ -3,19 +3,22 @@ import React from 'react';
|
||||
import type { Props } from './types';
|
||||
|
||||
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
|
||||
import Header from 'ui/snippets/header/Header';
|
||||
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';
|
||||
|
||||
const LayoutError = ({ children }: Props) => {
|
||||
return (
|
||||
<Layout.Container>
|
||||
<Layout.TopRow/>
|
||||
<HeaderMobile/>
|
||||
<Layout.MainArea>
|
||||
<Layout.SideBar/>
|
||||
<Layout.MainColumn>
|
||||
<HeaderAlert/>
|
||||
<Header/>
|
||||
<HeaderDesktop/>
|
||||
<AppErrorBoundary>
|
||||
<main>
|
||||
{ children }
|
||||
|
||||
@@ -3,21 +3,22 @@ import React from 'react';
|
||||
import type { Props } from './types';
|
||||
|
||||
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
|
||||
import Header from 'ui/snippets/header/Header';
|
||||
import HeaderAlert from 'ui/snippets/header/HeaderAlert';
|
||||
import HeaderMobile from 'ui/snippets/header/HeaderMobile';
|
||||
|
||||
import * as Layout from './components';
|
||||
|
||||
const LayoutHome = ({ children }: Props) => {
|
||||
return (
|
||||
<Layout.Container>
|
||||
<Layout.TopRow/>
|
||||
<HeaderMobile isHomePage/>
|
||||
<Layout.MainArea>
|
||||
<Layout.SideBar/>
|
||||
<Layout.MainColumn
|
||||
paddingTop={{ base: '88px', lg: 9 }}
|
||||
paddingTop={{ base: 6, lg: 9 }}
|
||||
>
|
||||
<HeaderAlert/>
|
||||
<Header isHomePage/>
|
||||
<AppErrorBoundary>
|
||||
{ children }
|
||||
</AppErrorBoundary>
|
||||
|
||||
@@ -8,13 +8,8 @@ const LayoutSearchResults = ({ children }: Props) => {
|
||||
|
||||
return (
|
||||
<Layout.Container>
|
||||
<Layout.MainArea>
|
||||
<Layout.SideBar/>
|
||||
<Layout.MainColumn>
|
||||
{ children }
|
||||
</Layout.MainColumn>
|
||||
</Layout.MainArea>
|
||||
<Layout.Footer/>
|
||||
<Layout.TopRow/>
|
||||
{ children }
|
||||
</Layout.Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 32 KiB |
@@ -14,7 +14,7 @@ const MainColumn = ({ children, className }: Props) => {
|
||||
flexGrow={ 1 }
|
||||
w={{ base: '100%', lg: 'auto' }}
|
||||
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 }
|
||||
>
|
||||
{ children }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Footer from 'ui/snippets/footer/Footer';
|
||||
import TopRow from 'ui/snippets/topBar/TopBar';
|
||||
|
||||
import Container from './Container';
|
||||
import Content from './Content';
|
||||
@@ -13,9 +14,11 @@ export {
|
||||
SideBar,
|
||||
MainColumn,
|
||||
Footer,
|
||||
TopRow,
|
||||
};
|
||||
|
||||
// Container
|
||||
// TopRow
|
||||
// MainArea
|
||||
// SideBar
|
||||
// MainColumn
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { GridProps } from '@chakra-ui/react';
|
||||
import { Box, Grid, Flex, Text, Link, VStack, Skeleton } from '@chakra-ui/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
@@ -18,7 +19,6 @@ import useFetch from 'lib/hooks/useFetch';
|
||||
import useIssueUrl from 'lib/hooks/useIssueUrl';
|
||||
import NetworkAddToWallet from 'ui/shared/NetworkAddToWallet';
|
||||
|
||||
import ColorModeToggler from '../header/ColorModeToggler';
|
||||
import FooterLinkItem from './FooterLinkItem';
|
||||
import IntTxsIndexingStatus from './IntTxsIndexingStatus';
|
||||
import getApiVersionUrl from './utils/getApiVersionUrl';
|
||||
@@ -96,41 +96,43 @@ const Footer = () => {
|
||||
|
||||
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' ],
|
||||
queryFn: async() => fetch(config.UI.footer.links || '', undefined, { resource: 'footer-links' }),
|
||||
enabled: Boolean(config.UI.footer.links),
|
||||
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 (
|
||||
<Flex
|
||||
direction={{ base: 'column', lg: 'row' }}
|
||||
px={{ base: 4, lg: 12 }}
|
||||
py={{ base: 4, lg: 9 }}
|
||||
borderTop="1px solid"
|
||||
borderColor="divider"
|
||||
as="footer"
|
||||
columnGap={{ lg: '32px', xl: '100px' }}
|
||||
>
|
||||
<Box flexGrow="1" mb={{ base: 8, lg: 0 }} minW="195px">
|
||||
<Flex flexWrap="wrap" columnGap={ 8 } rowGap={ 6 }>
|
||||
<ColorModeToggler/>
|
||||
{ !config.UI.indexingAlert.intTxs.isHidden && <IntTxsIndexingStatus/> }
|
||||
<NetworkAddToWallet/>
|
||||
</Flex>
|
||||
<Box mt={{ base: 5, lg: '44px' }}>
|
||||
<Link fontSize="xs" href="https://www.blockscout.com">blockscout.com</Link>
|
||||
</Box>
|
||||
<Text mt={ 3 } maxW={{ base: 'unset', lg: '470px' }} fontSize="xs">
|
||||
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
|
||||
const renderNetworkInfo = React.useCallback((gridArea?: GridProps['gridArea']) => {
|
||||
return (
|
||||
<Flex
|
||||
gridArea={ gridArea }
|
||||
flexWrap="wrap"
|
||||
columnGap={ 8 }
|
||||
rowGap={ 6 }
|
||||
mb={{ base: 5, lg: 10 }}
|
||||
_empty={{ display: 'none' }}
|
||||
>
|
||||
{ !config.UI.indexingAlert.intTxs.isHidden && <IntTxsIndexingStatus/> }
|
||||
<NetworkAddToWallet/>
|
||||
</Flex>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const renderProjectInfo = React.useCallback((gridArea?: GridProps['gridArea']) => {
|
||||
return (
|
||||
<Box gridArea={ gridArea }>
|
||||
<Link fontSize="xs" href="https://www.blockscout.com">blockscout.com</Link>
|
||||
<Text mt={ 3 } fontSize="xs">
|
||||
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
|
||||
</Text>
|
||||
<VStack spacing={ 1 } mt={ 6 } alignItems="start">
|
||||
{ apiVersionUrl && (
|
||||
<Text fontSize="xs">
|
||||
Backend: <Link href={ apiVersionUrl } target="_blank">{ backendVersionData?.backend_version }</Link>
|
||||
Backend: <Link href={ apiVersionUrl } target="_blank">{ backendVersionData?.backend_version }</Link>
|
||||
</Text>
|
||||
) }
|
||||
{ frontendLink && (
|
||||
@@ -140,64 +142,93 @@ const Footer = () => {
|
||||
) }
|
||||
</VStack>
|
||||
</Box>
|
||||
<Grid
|
||||
gap={{ base: 6, lg: config.UI.footer.links && colNum === MAX_LINKS_COLUMNS + 1 ? 2 : 8, xl: 12 }}
|
||||
gridTemplateColumns={ config.UI.footer.links ?
|
||||
{
|
||||
);
|
||||
}, [ apiVersionUrl, backendVersionData?.backend_version, frontendLink ]);
|
||||
|
||||
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)',
|
||||
lg: `repeat(${ colNum }, 135px)`,
|
||||
xl: `repeat(${ colNum }, 160px)`,
|
||||
} :
|
||||
'auto'
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
{ 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>
|
||||
))
|
||||
) }
|
||||
}}
|
||||
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>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
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 }}
|
||||
>
|
||||
{ BLOCKSCOUT_LINKS.map(link => <FooterLinkItem { ...link } key={ link.text }/>) }
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
export default React.memo(Footer);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Center, Icon, Link } from '@chakra-ui/react';
|
||||
import { Center, Icon, Link, Skeleton } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
@@ -6,9 +6,14 @@ type Props = {
|
||||
iconSize?: string;
|
||||
text: 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 (
|
||||
<Link href={ url } display="flex" alignItems="center" h="30px" variant="secondary" target="_blank" fontSize="xs">
|
||||
{ icon && (
|
||||
|
||||
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 20 KiB |
@@ -1,124 +0,0 @@
|
||||
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;
|
||||
@@ -1,30 +0,0 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import React from 'react';
|
||||
|
||||
import TestApp from 'playwright/TestApp';
|
||||
|
||||
import Header from './Header';
|
||||
|
||||
test('no auth +@mobile', async({ mount, page }) => {
|
||||
await mount(
|
||||
<TestApp>
|
||||
<Header/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 150 } });
|
||||
});
|
||||
|
||||
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 } });
|
||||
});
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
import { HStack, Box, Flex, useColorModeValue } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { useScrollDirection } from 'lib/contexts/scrollDirection';
|
||||
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
|
||||
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
|
||||
import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile';
|
||||
import SearchBar from 'ui/snippets/searchBar/SearchBar';
|
||||
|
||||
import Burger from './Burger';
|
||||
|
||||
type Props = {
|
||||
isHomePage?: boolean;
|
||||
renderSearchBar?: () => React.ReactNode;
|
||||
}
|
||||
|
||||
const Header = ({ isHomePage, renderSearchBar }: Props) => {
|
||||
const bgColor = useColorModeValue('white', 'black');
|
||||
const scrollDirection = useScrollDirection();
|
||||
|
||||
const searchBar = renderSearchBar ? renderSearchBar() : <SearchBar/>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box bgColor={ bgColor } display={{ base: 'block', lg: 'none' }}>
|
||||
<Flex
|
||||
as="header"
|
||||
position="fixed"
|
||||
top={ 0 }
|
||||
left={ 0 }
|
||||
paddingX={ 4 }
|
||||
paddingY={ 2 }
|
||||
bgColor={ bgColor }
|
||||
width="100%"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
zIndex="sticky2"
|
||||
transitionProperty="box-shadow"
|
||||
transitionDuration="slow"
|
||||
boxShadow={ scrollDirection === 'down' ? 'md' : 'none' }
|
||||
>
|
||||
<Burger/>
|
||||
<NetworkLogo/>
|
||||
{ config.features.account.isEnabled ? <ProfileMenuMobile/> : <Box boxSize={ 10 }/> }
|
||||
</Flex>
|
||||
{ !isHomePage && searchBar }
|
||||
</Box>
|
||||
<Box display={{ base: 'none', lg: 'block' }}>
|
||||
{ !isHomePage && (
|
||||
<HStack
|
||||
as="header"
|
||||
width="100%"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
gap={ 12 }
|
||||
>
|
||||
<Box width="100%">
|
||||
{ searchBar }
|
||||
</Box>
|
||||
{ config.features.account.isEnabled && <ProfileMenuDesktop/> }
|
||||
</HStack>
|
||||
) }
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,16 @@
|
||||
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();
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
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);
|
||||
@@ -0,0 +1,18 @@
|
||||
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 } });
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Box, Flex, useColorModeValue } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { useScrollDirection } from 'lib/contexts/scrollDirection';
|
||||
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
|
||||
import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile';
|
||||
import SearchBar from 'ui/snippets/searchBar/SearchBar';
|
||||
|
||||
import Burger from './Burger';
|
||||
|
||||
type Props = {
|
||||
isHomePage?: boolean;
|
||||
renderSearchBar?: () => React.ReactNode;
|
||||
}
|
||||
|
||||
const HeaderMobile = ({ isHomePage, renderSearchBar }: Props) => {
|
||||
const bgColor = useColorModeValue('white', 'black');
|
||||
const scrollDirection = useScrollDirection();
|
||||
const { ref, inView } = useInView({ threshold: 1 });
|
||||
|
||||
const searchBar = renderSearchBar ? renderSearchBar() : <SearchBar/>;
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={ ref }
|
||||
bgColor={ bgColor }
|
||||
display={{ base: 'block', lg: 'none' }}
|
||||
position="sticky"
|
||||
top="-1px"
|
||||
left={ 0 }
|
||||
zIndex="sticky2"
|
||||
pt="1px"
|
||||
>
|
||||
<Flex
|
||||
as="header"
|
||||
paddingX={ 4 }
|
||||
paddingY={ 2 }
|
||||
bgColor={ bgColor }
|
||||
width="100%"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
transitionProperty="box-shadow"
|
||||
transitionDuration="slow"
|
||||
boxShadow={ !inView && scrollDirection === 'down' ? 'md' : 'none' }
|
||||
>
|
||||
<Burger/>
|
||||
<NetworkLogo/>
|
||||
{ config.features.account.isEnabled ? <ProfileMenuMobile/> : <Box boxSize={ 10 }/> }
|
||||
</Flex>
|
||||
{ !isHomePage && searchBar }
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(HeaderMobile);
|
||||
|
Before Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
@@ -25,7 +25,8 @@ const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, onHid
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const handleScroll = React.useCallback(() => {
|
||||
if (window.pageYOffset !== 0) {
|
||||
const TOP_BAR_HEIGHT = 36;
|
||||
if (window.pageYOffset >= TOP_BAR_HEIGHT) {
|
||||
setIsSticky(true);
|
||||
} else {
|
||||
setIsSticky(false);
|
||||
@@ -70,10 +71,10 @@ const SearchBarInput = ({ onChange, onSubmit, isHomepage, onFocus, onBlur, onHid
|
||||
w="100%"
|
||||
backgroundColor={ bgColor }
|
||||
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 }}
|
||||
left="0"
|
||||
zIndex={{ base: isHomepage ? 'auto' : 'sticky1', lg: 'auto' }}
|
||||
zIndex={{ base: isHomepage ? 'auto' : '-1', lg: 'auto' }}
|
||||
paddingX={{ base: isHomepage ? 0 : 4, lg: 0 }}
|
||||
paddingTop={{ base: isHomepage ? 0 : 1, lg: 0 }}
|
||||
paddingBottom={{ base: isHomepage ? 0 : 4, lg: 0 }}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
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;
|
||||
@@ -0,0 +1,54 @@
|
||||
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;
|
||||
@@ -0,0 +1,55 @@
|
||||
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;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { test, expect } from '@playwright/experimental-ct-react';
|
||||
import React from 'react';
|
||||
|
||||
import * as statsMock from 'mocks/stats/index';
|
||||
import TestApp from 'playwright/TestApp';
|
||||
import buildApiUrl from 'playwright/utils/buildApiUrl';
|
||||
|
||||
import TopBar from './TopBar';
|
||||
|
||||
test('default view +@dark-mode +@mobile', async({ mount, page }) => {
|
||||
await page.route(buildApiUrl('homepage_stats'), (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(statsMock.base),
|
||||
}));
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<TopBar/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await component.getByLabel('color mode switch').click();
|
||||
|
||||
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 220 } });
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
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);
|
||||
@@ -0,0 +1,43 @@
|
||||
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);
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,35 @@
|
||||
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];
|
||||