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

Merge pull request #379 from blockscout/homepage-update

Homepage update
parents c4a1fd36 e15a14f1
......@@ -31,6 +31,9 @@ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_SU
NEXT_PUBLIC_LOGOUT_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_URL__
NEXT_PUBLIC_LOGOUT_RETURN_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_RETURN_URL__
NEXT_PUBLIC_HOMEPAGE_CHARTS=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_CHARTS__
NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT__
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER__
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME__
# api config
NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__
......
......@@ -11,7 +11,7 @@
"detail": "start local dev server",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......@@ -31,7 +31,7 @@
"detail": "start local dev server for POA network",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......@@ -51,7 +51,7 @@
"detail": "start local dev server for Goerli network",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......@@ -77,7 +77,7 @@
},
"presentation": {
"reveal": "never",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......@@ -91,7 +91,7 @@
"detail": "run eslint",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"revealProblems": "onProblem",
},
"icon": {
......@@ -112,7 +112,7 @@
"detail": "run visual components tests for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"focus": true,
},
"icon": {
......@@ -131,7 +131,7 @@
"detail": "run visual components tests for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"focus": true,
},
"icon": {
......@@ -150,7 +150,7 @@
"detail": "run visual components tests",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"focus": true,
},
"icon": {
......@@ -171,7 +171,7 @@
"detail": "run jest tests",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"focus": true,
},
"icon": {
......@@ -190,7 +190,7 @@
"detail": "run jest tests in watch mode",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"close": true,
"focus": true,
},
......@@ -210,7 +210,7 @@
"detail": "run jest tests in watch mode for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"focus": true,
},
"icon": {
......@@ -230,7 +230,7 @@
"detail": "build docker image",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
"focus": true,
......@@ -251,7 +251,7 @@
"detail": "run docker container for POA network",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......@@ -271,7 +271,7 @@
"detail": "format svg files with svgo",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......
......@@ -64,8 +64,6 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` *(optional)* | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` |
| NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` *(optional)* | Link to Telegram in the footer | `https://t.me/poa_network` |
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` *(optional)* | Set to false if network doesn't have gas tracker | `true` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` *(optional)* | Set to false if average block time is useless for the network | `true` |
| NEXT_PUBLIC_MARKETPLACE_APP_LIST | `Array<MarketplaceApp>` where `MarketplaceApp` can have following [properties](#marketplace-app-configuration-properties) | List of apps that will be shown on the marketplace page | `[{'author': 'Bob', 'id': 'app', 'external': true, 'title': 'The App', 'logo': 'https://foo.app/icon.png', 'categories': ['security'], 'shortDescription': 'Awesome app', 'site': 'https://foo.app', 'description': 'The best app', 'url': 'https://foo.app/launch'}]` |
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` |
| NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` |
......@@ -73,6 +71,9 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_LOGOUT_URL | `string` *(optional)* | Account logout url | `https://blockscoutcom.us.auth0.com/v2/logout` |
| NEXT_PUBLIC_LOGOUT_RETURN_URL | `string` *(optional)* | Account logout return url | `https://blockscout.com/poa/core/auth/logout` |
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cup'>` *(optional)* | List of charts displayed on the home page | `['daily_txs','coin_price','market_cup']` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT | `string` *(optional)* | Gradient value for hero plate on the homepage | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 72% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` *(optional)* | Set to false if network doesn't have gas tracker | `true` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` *(optional)* | Set to false if average block time is useless for the network | `true` |
### App configuration
......
......@@ -89,6 +89,8 @@ const config = Object.freeze({
},
homepage: {
charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_CHARTS)) || [],
plateGradient: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT) ||
'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%)',
showGasTracker: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER) === 'false' ? false : true,
showAvgBlockTime: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME) === 'false' ? false : true,
},
......
......@@ -4,6 +4,7 @@ NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60','type':'xdai_optimism'},{'title':'Arbitrum on xDai','url':'https://blockscout.com/xdai/aox','group':'mainnets'},{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'mainnets','type':'poa_core'},{'title':'RSK','url':'https://blockscout.com/rsk/mainnet','group':'mainnets','type':'rsk_mainnet'},{'title':'Gnosis Chain Testnet','url':'https://blockscout.com/xdai/testnet','group':'testnets','type':'xdai_testnet'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'testnets','type':'poa_sokol'},{'title':'ARTIS Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'Astar','url':'https://blockscout.com/astar','group':'other','type':'astar'}]
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction'}}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup']
NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=radial-gradient(at 12% 37%, hsla(324,73%,67%,1) 0px, transparent 50%), radial-gradient(at 62% 14%, hsla(256,87%,73%,1) 0px, transparent 50%), radial-gradient(at 84% 80%, hsla(128,75%,73%,1) 0px, transparent 50%), radial-gradient(at 57% 46%, hsla(285,63%,72%,1) 0px, transparent 50%), radial-gradient(at 37% 30%, hsla(174,70%,61%,1) 0px, transparent 50%), radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 67% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)
# network config
NEXT_PUBLIC_NETWORK_NAME=POA
......
export const base = {
chart_data: [
{
date: '2022-11-28',
tx_count: 26815,
},
{
date: '2022-11-27',
tx_count: 34784,
},
{
date: '2022-11-26',
tx_count: 77527,
},
{
date: '2022-11-25',
tx_count: 39687,
},
{
date: '2022-11-24',
tx_count: 40752,
},
{
date: '2022-11-23',
tx_count: 32569,
},
{
date: '2022-11-22',
tx_count: 34449,
},
{
date: '2022-11-21',
tx_count: 106047,
},
{
date: '2022-11-20',
tx_count: 107713,
},
{
date: '2022-11-19',
tx_count: 96311,
},
{
date: '2022-11-18',
tx_count: 30828,
},
{
date: '2022-11-17',
tx_count: 27422,
},
{
date: '2022-11-16',
tx_count: 75898,
},
{
date: '2022-11-15',
tx_count: 84084,
},
{
date: '2022-11-14',
tx_count: 62266,
},
{
date: '2022-11-13',
tx_count: 22338,
},
{
date: '2022-11-12',
tx_count: 86764,
},
{
date: '2022-11-11',
tx_count: 79493,
},
{
date: '2022-11-10',
tx_count: 92887,
},
{
date: '2022-11-09',
tx_count: 43691,
},
{
date: '2022-11-08',
tx_count: 74197,
},
{
date: '2022-11-07',
tx_count: 58131,
},
{
date: '2022-11-06',
tx_count: 62477,
},
{
date: '2022-11-05',
tx_count: 82897,
},
{
date: '2022-11-04',
tx_count: 91725,
},
{
date: '2022-11-03',
tx_count: 83667,
},
{
date: '2022-11-02',
tx_count: 63743,
},
{
date: '2022-11-01',
tx_count: 152059,
},
{
date: '2022-10-31',
tx_count: 62519,
},
{
date: '2022-10-30',
tx_count: 48569,
},
{
date: '2022-10-29',
tx_count: 36789,
},
],
};
import type { Stats } from 'types/api/stats';
export const base: Stats = {
average_block_time: 6212.0,
coin_price: '0.00199678',
gas_prices: {
average: 48.0,
fast: 67.5,
slow: 48.0,
},
gas_used_today: '4108680603',
market_cap: '330809.96443288102524',
network_utilization_percentage: 1.55372064,
static_gas_price: '10',
total_addresses: '19667249',
total_blocks: '30215608',
total_gas_used: '0',
total_transactions: '82258122',
transactions_today: '26815',
};
......@@ -61,7 +61,6 @@ export const base: Transaction = {
tx_tag: null,
tx_types: [
'contract_call',
'token_transfer',
],
type: 2,
value: '42000000000000000000',
......@@ -80,6 +79,9 @@ export const withContractCreation: Transaction = {
public_tags: [],
watchlist_names: [],
},
tx_types: [
'contract_creation',
],
};
export const withTokenTransfer: Transaction = {
......@@ -100,6 +102,9 @@ export const withTokenTransfer: Transaction = {
tokenTransferMock.erc1155,
tokenTransferMock.erc1155multiple,
],
tx_types: [
'token_transfer',
],
};
export const withDecodedRevertReason: Transaction = {
......
......@@ -4,16 +4,21 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppProps } from 'next/app';
import React, { useState } from 'react';
import appConfig from 'configs/app/config';
import { AppContextProvider } from 'lib/appContext';
import { Chakra } from 'lib/Chakra';
import useConfigSentry from 'lib/hooks/useConfigSentry';
import type { ErrorType } from 'lib/hooks/useFetch';
import useScrollDirection from 'lib/hooks/useScrollDirection';
import { SocketProvider } from 'lib/socket/context';
import theme from 'theme';
import ScrollDirectionContext from 'ui/ScrollDirectionContext';
import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
function MyApp({ Component, pageProps }: AppProps) {
useConfigSentry();
const directionContext = useScrollDirection();
const [ queryClient ] = useState(() => new QueryClient({
defaultOptions: {
......@@ -57,7 +62,11 @@ function MyApp({ Component, pageProps }: AppProps) {
<ErrorBoundary renderErrorScreen={ renderErrorScreen } onError={ handleError }>
<AppContextProvider pageProps={ pageProps }>
<QueryClientProvider client={ queryClient }>
<Component { ...pageProps }/>
<ScrollDirectionContext.Provider value={ directionContext }>
<SocketProvider url={ `${ appConfig.api.socket }${ appConfig.api.basePath }/socket/v2` }>
<Component { ...pageProps }/>
</SocketProvider>
</ScrollDirectionContext.Provider>
<ReactQueryDevtools/>
</QueryClientProvider>
</AppContextProvider>
......
......@@ -88,6 +88,14 @@ const config: PlaywrightTestConfig = {
colorScheme: 'dark',
},
},
{
name: 'dark color mode mobile',
grep: /\+@dark-mode-mobile/,
use: {
...devices['iPhone 13 Pro'],
colorScheme: 'dark',
},
},
],
};
......
......@@ -55,6 +55,8 @@ export const joinChannel = async(socket: WebSocket, channelName: string) => {
});
};
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void {
socket.send(JSON.stringify([
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import * as statsMock from 'mocks/stats/index';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import LatestBlocks from './LatestBlocks';
const STATS_API_URL = '/node-api/stats';
const BLOCKS_API_URL = '/node-api/index/blocks';
export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
test('default view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify([
blockMock.base,
blockMock.base2,
]),
}));
const component = await mount(
<TestApp>
<LatestBlocks/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test.describe('socket', () => {
test.describe.configure({ mode: 'serial' });
test('new item', async({ mount, page, createSocket }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify([
blockMock.base,
blockMock.base2,
]),
}));
const component = await mount(
<TestApp withSocket>
<LatestBlocks/>
</TestApp>,
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'blocks:new_block');
socketServer.sendMessage(socket, channel, 'new_block', {
average_block_time: '6212.0',
block: {
...blockMock.base,
height: blockMock.base.height + 1,
timestamp: '2022-11-11T11:59:58Z',
},
});
await expect(component).toHaveScreenshot();
});
});
......@@ -109,10 +109,10 @@ const LatestBlocks = () => {
}
return (
<>
<Heading as="h4" size="sm" mb={{ base: 4, lg: 7 }}>Latest Blocks</Heading>
<Box width={{ base: '100%', lg: '280px' }}>
<Heading as="h4" size="sm" mb={ 4 }>Latest Blocks</Heading>
{ content }
</>
</Box>
);
};
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { ROUTES } from 'lib/link/routes';
import * as statsMock from 'mocks/stats/index';
import * as txMock from 'mocks/txs/tx';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import LatestTxs from './LatestTxs';
export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
test('default view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route('/node-api/stats', (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route('/node-api/index/txs', (route) => route.fulfill({
status: 200,
body: JSON.stringify([
txMock.base,
txMock.withContractCreation,
txMock.withTokenTransfer,
]),
}));
const component = await mount(
<TestApp>
<LatestTxs/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test.describe('socket', () => {
test.describe.configure({ mode: 'serial' });
const hooksConfig = {
router: {
pathname: ROUTES.network_index.pattern,
query: {},
},
};
test('new item', async({ mount, page, createSocket }) => {
await page.route('/node-api/stats', (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route('/node-api/index/txs', (route) => route.fulfill({
status: 200,
body: JSON.stringify([
txMock.base,
txMock.withContractCreation,
txMock.withTokenTransfer,
]),
}));
const component = await mount(
<TestApp withSocket>
<LatestTxs/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'transactions:new_transaction');
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
await expect(component).toHaveScreenshot();
});
});
......@@ -27,7 +27,7 @@ const LatestTransactions = () => {
if (isLoading) {
content = (
<>
<Skeleton h="56px" w="100%"/>
<Skeleton h="56px" w="100%" borderBottomLeftRadius={ 0 } borderBottomRightRadius={ 0 }/>
{ Array.from(Array(txsCount)).map((item, index) => <LatestTxsItemSkeleton key={ index }/>) }
</>
);
......@@ -53,10 +53,10 @@ const LatestTransactions = () => {
}
return (
<>
<Box flexGrow={ 1 }>
<Heading as="h4" size="sm" mb={ 4 }>Latest transactions</Heading>
{ content }
</>
</Box>
);
};
......
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 Stats from './Stats';
const API_URL = '/node-api/stats';
test('all items +@mobile +@dark-mode +@desktop-xl', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
const component = await mount(
<TestApp>
<Stats/>
</TestApp>,
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
......@@ -2,7 +2,7 @@ import { Grid } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import { Stats } from 'types/api/stats';
import type { Stats as TStats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
......@@ -26,7 +26,7 @@ let itemsCount = 5;
const Stats = () => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Stats>(
const { data, isLoading, isError } = useQuery<unknown, unknown, TStats>(
[ QueryKeys.stats ],
async() => await fetch(`/node-api/stats`),
);
......@@ -79,10 +79,10 @@ const Stats = () => {
return (
<Grid
gridTemplateColumns={{ lg: `repeat(${ itemsCount }, 1fr)`, base: 'none' }}
gridTemplateRows={{ lg: 'none', base: `repeat(${ itemsCount }, 1fr)` }}
gridTemplateColumns={{ lg: `repeat(${ itemsCount }, 1fr)`, base: '1fr 1fr' }}
gridTemplateRows={{ lg: 'none', base: undefined }}
gridGap="10px"
marginTop="32px"
marginTop="24px"
>
{ content }
</Grid>
......
import { Flex, Icon, Center, Text, LightMode } from '@chakra-ui/react';
import { Flex, Icon, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
type Props = {
......@@ -9,30 +9,21 @@ type Props = {
const StatsItem = ({ icon, title, value }: Props) => {
return (
<LightMode>
<Flex
backgroundColor="blue.50"
padding={ 5 }
borderRadius="16px"
flexDirection={{ base: 'row', lg: 'column', xl: 'row' }}
alignItems="center"
>
<Center
backgroundColor="green.100"
borderRadius="12px"
w={ 10 }
h={ 10 }
mr={{ base: 4, lg: 0, xl: 4 }}
mb={{ base: 0, lg: 2, xl: 0 }}
>
<Icon as={ icon } boxSize={ 7 } color="black"/>
</Center>
<Flex flexDirection="column" alignItems={{ base: 'start', lg: 'center', xl: 'start' }}>
<Text variant="secondary" fontSize="xs" lineHeight="16px">{ title }</Text>
<Text fontWeight={ 500 } fontSize="md">{ value }</Text>
</Flex>
<Flex
backgroundColor={ useColorModeValue('blue.50', 'blue.800') }
padding={ 3 }
borderRadius="md"
flexDirection={{ base: 'row', lg: 'column', xl: 'row' }}
alignItems="center"
columnGap={ 3 }
rowGap={ 2 }
>
<Icon as={ icon } boxSize={ 7 }/>
<Flex flexDirection="column" alignItems={{ base: 'start', lg: 'center', xl: 'start' }}>
<Text variant="secondary" fontSize="xs" lineHeight="16px">{ title }</Text>
<Text fontWeight={ 500 } fontSize="md">{ value }</Text>
</Flex>
</LightMode>
</Flex>
);
};
......
......@@ -7,16 +7,16 @@ const StatsItemSkeleton = () => {
return (
<Flex
backgroundColor={ bgColor }
padding={ 5 }
borderRadius="16px"
padding={ 3 }
borderRadius="md"
flexDirection={{ base: 'row', lg: 'column', xl: 'row' }}
alignItems="center"
columnGap={ 3 }
rowGap={ 2 }
>
<Skeleton
w="40px"
h="40px"
mr={{ base: 4, lg: 0, xl: 4 }}
mb={{ base: 0, lg: 2, xl: 0 }}
/>
<Flex flexDirection="column" alignItems={{ base: 'start', lg: 'center', xl: 'start' }}>
<Skeleton w="69px" h="10px" mt="4px" mb="8px"/>
......
......@@ -18,9 +18,12 @@ interface Props {
}
const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats }: Props) => {
const bgColor = useColorModeValue('white', 'black');
const isMobile = useIsMobile();
const activeBgColorDesktop = useColorModeValue('white', 'gray.900');
const activeBgColorMobile = useColorModeValue('white', 'black');
const activeBgColor = isMobile ? activeBgColorMobile : activeBgColorDesktop;
const handleClick = React.useCallback(() => {
onClick(id);
}, [ id, onClick ]);
......@@ -58,11 +61,11 @@ const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats
borderRadius="md"
cursor="pointer"
onClick={ handleClick }
bgColor={ isSelected ? bgColor : 'inherit' }
bgColor={ isSelected ? activeBgColor : 'inherit' }
boxShadow={ isSelected ? 'lg' : 'none' }
zIndex={ isSelected ? 1 : 'initial' }
_hover={{
bgColor,
activeBgColor,
zIndex: 1,
}}
>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as dailyTxsMock from 'mocks/stats/daily_txs';
import * as statsMock from 'mocks/stats/index';
import TestApp from 'playwright/TestApp';
import ChainIndicators from './ChainIndicators';
const STATS_API_URL = '/node-api/stats';
const TX_CHART_API_URL = '/node-api/stats/charts/transactions';
test('daily txs chart +@mobile +@dark-mode +@dark-mode-mobile', async({ mount, page }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route(TX_CHART_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(dailyTxsMock.base),
}));
const component = await mount(
<TestApp>
<ChainIndicators/>
</TestApp>,
);
await page.waitForResponse(STATS_API_URL),
await page.hover('.ChartOverlay', { position: { x: 100, y: 100 } });
await expect(component).toHaveScreenshot();
});
......@@ -40,8 +40,10 @@ const ChainIndicators = () => {
() => fetch('/node-api/stats'),
);
const bgColor = useColorModeValue('white', 'black');
const listBgColor = useColorModeValue('gray.50', 'gray.900');
const bgColorDesktop = useColorModeValue('white', 'gray.900');
const bgColorMobile = useColorModeValue('white', 'black');
const listBgColorDesktop = useColorModeValue('gray.50', 'black');
const listBgColorMobile = useColorModeValue('gray.50', 'gray.900');
if (indicators.length === 0) {
return null;
......@@ -68,7 +70,7 @@ const ChainIndicators = () => {
p={{ base: 0, lg: 8 }}
borderRadius={{ base: 'none', lg: 'lg' }}
boxShadow={{ base: 'none', lg: 'xl' }}
bgColor={ bgColor }
bgColor={{ base: bgColorMobile, lg: bgColorDesktop }}
columnGap={ 12 }
rowGap={ 0 }
flexDir={{ base: 'column', lg: 'row' }}
......@@ -97,7 +99,7 @@ const ChainIndicators = () => {
as="ul"
p={ 3 }
borderRadius="lg"
bgColor={ listBgColor }
bgColor={{ base: listBgColorMobile, lg: listBgColorDesktop }}
rowGap={ 3 }
order={{ base: 1, lg: 2 }}
>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import * as dailyTxsMock from 'mocks/stats/daily_txs';
import * as statsMock from 'mocks/stats/index';
import * as txMock from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import Home from './Home';
test('default view -@default +@desktop-xl +@mobile +@dark-mode', async({ mount, page }) => {
await page.route('/node-api/stats', (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route('/node-api/index/blocks', (route) => route.fulfill({
status: 200,
body: JSON.stringify([
blockMock.base,
blockMock.base2,
]),
}));
await page.route('/node-api/index/txs', (route) => route.fulfill({
status: 200,
body: JSON.stringify([
txMock.base,
txMock.withContractCreation,
txMock.withTokenTransfer,
]),
}));
await page.route('/node-api/stats/charts/transactions', (route) => route.fulfill({
status: 200,
body: JSON.stringify(dailyTxsMock.base),
}));
const component = await mount(
<TestApp>
<Home/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Box, Heading, Flex, LightMode } from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
import ChainIndicators from 'ui/home/indicators/ChainIndicators';
import LatestBlocks from 'ui/home/LatestBlocks';
import LatestTxs from 'ui/home/LatestTxs';
import Stats from 'ui/home/Stats';
import Page from 'ui/shared/Page/Page';
import ColorModeToggler from 'ui/snippets/header/ColorModeToggler';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import SearchBar from 'ui/snippets/searchBar/SearchBar';
const Home = () => {
return (
<Page hasSearch={ false }>
<Page isHomePage>
<Box
w="100%"
backgroundImage="radial-gradient(farthest-corner at 0 0, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%)"
backgroundImage={ appConfig.homepage.plateGradient }
backgroundColor="blue.400"
borderRadius="24px"
padding={{ base: '24px 40px', lg: '48px' }}
padding={{ base: '24px', lg: '48px' }}
minW={{ base: 'unset', lg: '900px' }}
>
<Heading
as="h1"
size={{ base: 'lg', ld: 'xl' }}
fontWeight={{ base: 600, lg: 500 }}
color="white"
mb={{ base: 6, lg: 8 }}
>
Welcome to Blockscout explorer
</Heading>
<LightMode><SearchBar isHomepage/></LightMode>
<Flex mb={{ base: 6, lg: 8 }} justifyContent="space-between">
<Heading
as="h1"
size={{ base: 'md', lg: 'xl' }}
lineHeight={{ base: '32px', lg: '50px' }}
fontWeight={ 500 }
color="white"
>
Welcome to Blockscout explorer
</Heading>
<Flex
alignItems="center"
display={{ base: 'none', lg: 'flex' }}
columnGap={ 12 }
>
<ColorModeToggler trackBg="whiteAlpha.500"/>
<ProfileMenuDesktop/>
</Flex>
</Flex>
<LightMode>
<SearchBar isHomepage/>
</LightMode>
</Box>
<Stats/>
<ChainIndicators/>
<Flex mt={ 12 } direction={{ base: 'column', lg: 'row' }}>
<Box mr={{ base: 0, lg: 12 }} mb={{ base: 8, lg: 0 }} width={{ base: '100%', lg: '280px' }}><LatestBlocks/></Box>
<Box flexGrow={ 1 }><LatestTxs/></Box>
<Flex mt={ 12 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 8 }>
<LatestBlocks/>
<LatestTxs/>
</Flex>
</Page>
);
......
......@@ -4,12 +4,8 @@ import React from 'react';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch';
import useScrollDirection from 'lib/hooks/useScrollDirection';
import { SocketProvider } from 'lib/socket/context';
import ScrollDirectionContext from 'ui/ScrollDirectionContext';
import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
import PageContent from 'ui/shared/Page/PageContent';
......@@ -20,14 +16,14 @@ interface Props {
children: React.ReactNode;
wrapChildren?: boolean;
hideMobileHeaderOnScrollDown?: boolean;
hasSearch?: boolean;
isHomePage?: boolean;
}
const Page = ({
children,
wrapChildren = true,
hideMobileHeaderOnScrollDown,
hasSearch = true,
isHomePage,
}: Props) => {
const fetch = useFetch();
......@@ -35,32 +31,26 @@ const Page = ({
enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
});
const directionContext = useScrollDirection();
const renderErrorScreen = React.useCallback(() => {
return wrapChildren ?
<PageContent hasSearch={ hasSearch }><AppError statusCode={ 500 } mt="50px"/></PageContent> :
<PageContent isHomePage={ isHomePage }><AppError statusCode={ 500 } mt="50px"/></PageContent> :
<AppError statusCode={ 500 }/>;
}, [ wrapChildren, hasSearch ]);
}, [ isHomePage, wrapChildren ]);
const renderedChildren = wrapChildren ? (
<PageContent hasSearch={ hasSearch }>{ children }</PageContent>
<PageContent isHomePage={ isHomePage }>{ children }</PageContent>
) : children;
return (
<SocketProvider url={ `${ appConfig.api.socket }${ appConfig.api.basePath }/socket/v2` }>
<ScrollDirectionContext.Provider value={ directionContext }>
<Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/>
<Flex flexDir="column" width="100%">
<Header hasSearch={ hasSearch } hideOnScrollDown={ hideMobileHeaderOnScrollDown }/>
<ErrorBoundary renderErrorScreen={ renderErrorScreen }>
{ renderedChildren }
</ErrorBoundary>
</Flex>
</Flex>
</ScrollDirectionContext.Provider>
</SocketProvider>
<Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/>
<Flex flexDir="column" width="100%">
<Header isHomePage={ isHomePage } hideOnScrollDown={ hideMobileHeaderOnScrollDown }/>
<ErrorBoundary renderErrorScreen={ renderErrorScreen }>
{ renderedChildren }
</ErrorBoundary>
</Flex>
</Flex>
);
};
......
......@@ -3,17 +3,17 @@ import React from 'react';
interface Props {
children: React.ReactNode;
hasSearch?: boolean;
isHomePage?: boolean;
}
const PageContent = ({ children, hasSearch }: Props) => {
const PageContent = ({ children, isHomePage }: Props) => {
return (
<Box
as="main"
w="100%"
paddingX={{ base: 4, lg: 12 }}
paddingBottom={ 10 }
paddingTop={{ base: hasSearch ? '138px' : '88px', lg: 0 }}
paddingTop={{ base: isHomePage ? '88px' : '138px', lg: isHomePage ? 9 : 0 }}
>
{ children }
</Box>
......
......@@ -7,10 +7,12 @@ import type { UserInfo } from 'types/api/account';
import { useAppContext } from 'lib/appContext';
import * as cookies from 'lib/cookies';
const IdenticonComponent = typeof Identicon === 'object' && 'default' in Identicon ? Identicon.default : Identicon;
// for those who haven't got profile
// or if we cannot download the profile picture for some reasons
const FallbackImage = ({ size, id }: { size: number; id: string }) => {
const bgColor = useToken('colors', useColorModeValue('blackAlpha.100', 'white'));
const bgColor = useToken('colors', useColorModeValue('gray.100', 'white'));
return (
<Box
......@@ -19,7 +21,7 @@ const FallbackImage = ({ size, id }: { size: number; id: string }) => {
maxHeight={ `${ size }px` }
>
<Box boxSize={ `${ size * 2 }px` } transformOrigin="left top" transform="scale(0.5)" borderRadius="full" overflow="hidden">
<Identicon
<IdenticonComponent
bg={ bgColor }
string={ id }
// the displayed size is doubled for retina displays and then scaled down
......
......@@ -21,7 +21,9 @@ import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
export interface ColorModeTogglerProps
extends Omit<UseCheckboxProps, 'isIndeterminate'>,
Omit<HTMLChakraProps<'label'>, keyof UseCheckboxProps>,
ThemingProps<'Switch'> {}
ThemingProps<'Switch'> {
trackBg?: string;
}
const ColorModeToggler = forwardRef<ColorModeTogglerProps, 'input'>((props, ref) => {
const ownProps = omitThemingProps(props);
......@@ -39,7 +41,7 @@ const ColorModeToggler = forwardRef<ColorModeTogglerProps, 'input'>((props, ref)
const transitionProps = getDefaultTransitionProps();
const trackStyles: SystemStyleObject = React.useMemo(() => ({
bg: trackBg,
bgColor: props.trackBg || trackBg,
width: '72px',
height: '32px',
borderRadius: 'full',
......@@ -50,7 +52,7 @@ const ColorModeToggler = forwardRef<ColorModeTogglerProps, 'input'>((props, ref)
cursor: 'pointer',
...transitionProps,
transitionDuration: 'ultra-slow',
}), [ trackBg, transitionProps ]);
}), [ props.trackBg, trackBg, transitionProps ]);
const thumbStyles: SystemStyleObject = React.useMemo(() => ({
bg: thumbBg,
......
......@@ -11,11 +11,11 @@ import Burger from './Burger';
import ColorModeToggler from './ColorModeToggler';
type Props = {
hasSearch: boolean;
isHomePage?: boolean;
hideOnScrollDown?: boolean;
}
const Header = ({ hideOnScrollDown, hasSearch }: Props) => {
const Header = ({ hideOnScrollDown, isHomePage }: Props) => {
const bgColor = useColorModeValue('white', 'black');
return (
......@@ -43,22 +43,27 @@ const Header = ({ hideOnScrollDown, hasSearch }: Props) => {
<NetworkLogo/>
<ProfileMenuMobile/>
</Flex>
{ hasSearch && <SearchBar withShadow={ !hideOnScrollDown }/> }
</Box><HStack
as="header"
width="100%"
alignItems="center"
justifyContent="center"
gap={ 12 }
display={{ base: 'none', lg: 'flex' }}
paddingX={ 12 }
paddingTop={ 9 }
paddingBottom={ 8 }
>
<Box width="100%">{ hasSearch && <SearchBar/> }</Box>
<ColorModeToggler/>
<ProfileMenuDesktop/>
</HStack>
{ !isHomePage && <SearchBar withShadow={ !hideOnScrollDown }/> }
</Box>
{ !isHomePage && (
<HStack
as="header"
width="100%"
alignItems="center"
justifyContent="center"
gap={ 12 }
display={{ base: 'none', lg: 'flex' }}
paddingX={ 12 }
paddingTop={ 9 }
paddingBottom="52px"
>
<Box width="100%">
<SearchBar/>
</Box>
<ColorModeToggler/>
<ProfileMenuDesktop/>
</HStack>
) }
</>
) }
</ScrollDirectionContext.Consumer>
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { ROUTES } from 'lib/link/routes';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import TxsNewItemNotice from './TxsNewItemNotice';
const hooksConfig = {
router: {
pathname: ROUTES.txs.pattern,
query: {},
},
};
export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
test.describe.configure({ mode: 'serial' });
test('new item in validated txs list', async({ mount, createSocket }) => {
const component = await mount(
<TestApp withSocket>
<TxsNewItemNotice/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'transactions:new_transaction');
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
await expect(component).toHaveScreenshot();
});
test('2 new items in validated txs list', async({ mount, page, createSocket }) => {
const component = await mount(
<TestApp withSocket>
<TxsNewItemNotice/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'transactions:new_transaction');
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
await page.waitForSelector('text=2 more');
await expect(component).toHaveScreenshot();
});
test('connection loss', async({ mount, createSocket }) => {
const component = await mount(
<TestApp withSocket>
<TxsNewItemNotice/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
await socketServer.joinChannel(socket, 'transactions:new_transaction');
socket.close();
await expect(component).toHaveScreenshot();
});
test('fetching', async({ mount, createSocket }) => {
const component = await mount(
<TestApp withSocket>
<TxsNewItemNotice/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
await socketServer.joinChannel(socket, 'transactions:new_transaction');
await expect(component).toHaveScreenshot();
});
......@@ -8,7 +8,7 @@ interface InjectedProps {
}
interface Props {
children: (props: InjectedProps) => JSX.Element;
children?: (props: InjectedProps) => JSX.Element;
className?: string;
}
......@@ -51,7 +51,7 @@ const TxsNewItemNotice = ({ children, className }: Props) => {
);
})();
return children({ content });
return children ? children({ content }) : content;
};
export default chakra(TxsNewItemNotice);
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