Commit 18b419ef authored by tom goriunov's avatar tom goriunov Committed by GitHub

Redesign hero section on home page (#2050)

* change hero section

* add AdBanner to hero block

* show icon instead of text on "connect wallet" button on md desktops

* redesign stats widgets

* new design for home page charts

* fix skeletons

* update demo values

* disable getit provider

* fixes for adbutler provider

* show token logo placeholder for secondary coin logo

* add width to hype banner

* update demo values

* remove letter spacing for large headings

* indicators block fixes

* update icons

* update screenshots
parent e21d6b73
...@@ -43,7 +43,7 @@ NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com ...@@ -43,7 +43,7 @@ NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_AD_BANNER_PROVIDER=hype NEXT_PUBLIC_AD_BANNER_PROVIDER=slise
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-mainnet.safe.global
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
......
...@@ -87,10 +87,7 @@ frontend: ...@@ -87,10 +87,7 @@ frontend:
NEXT_PUBLIC_CONTRACT_CODE_IDES: "[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]" NEXT_PUBLIC_CONTRACT_CODE_IDES: "[{'title':'Remix IDE','url':'https://remix.blockscout.com/?address={hash}&blockscout=eth-goerli.blockscout.com','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]"
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER: blockscout NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER: blockscout
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: true NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: true
NEXT_PUBLIC_AD_BANNER_PROVIDER: getit NEXT_PUBLIC_AD_BANNER_PROVIDER: slise
NEXT_PUBLIC_AD_BANNER_ADDITIONAL_PROVIDER: adbutler
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP: "{ \"id\": \"632019\", \"width\": \"728\", \"height\": \"90\" }"
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE: "{ \"id\": \"632018\", \"width\": \"320\", \"height\": \"100\" }"
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: true NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: true
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: true NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: true
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: "['/apps']" NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES: "['/apps']"
......
...@@ -359,6 +359,7 @@ This feature is **enabled by default**. To switch it off pass `NEXT_PUBLIC_GAS_T ...@@ -359,6 +359,7 @@ This feature is **enabled by default**. To switch it off pass `NEXT_PUBLIC_GAS_T
### Banner ads ### Banner ads
This feature is **enabled by default** with the `slise` ads provider. To switch it off pass `NEXT_PUBLIC_AD_BANNER_PROVIDER=none`. This feature is **enabled by default** with the `slise` ads provider. To switch it off pass `NEXT_PUBLIC_AD_BANNER_PROVIDER=none`.
*Note* that the `getit` ad provider is temporary disabled.
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | | Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- | --- |
......
<svg viewBox="0 0 22 22" 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 fill-rule="evenodd" clip-rule="evenodd" d="M3.5 11a7.5 7.5 0 1 1 15 0 7.5 7.5 0 0 1-15 0ZM11 1C5.477 1 1 5.477 1 11s4.477 10 10 10 10-4.477 10-10S16.523 1 11 1Zm1.25 5a1.25 1.25 0 1 0-2.5 0v5c0 .69.56 1.25 1.25 1.25h5a1.25 1.25 0 1 0 0-2.5h-3.75V6Z" fill="currentColor" stroke="transparent" stroke-width=".6" stroke-linecap="round" stroke-linejoin="round"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M10 .3C4.643.3.3 4.643.3 10c0 5.357 4.343 9.7 9.7 9.7 5.357 0 9.7-4.343 9.7-9.7 0-5.357-4.343-9.7-9.7-9.7ZM2.2 10a7.8 7.8 0 1 1 15.6 0 7.8 7.8 0 0 1-15.6 0ZM10 4.05a.95.95 0 0 0-.95.95v5c0 .525.425.95.95.95h5a.95.95 0 1 0 0-1.9h-3.75a.3.3 0 0 1-.3-.3V5a.95.95 0 0 0-.95-.95Z" fill="currentColor"/>
</svg> </svg>
<svg viewBox="0 0 30 30" 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="M17.5 23.75h1.25v2.5H2.5v-2.5h1.25V5A1.25 1.25 0 0 1 5 3.75h11.25A1.25 1.25 0 0 1 17.5 5v10H20a2.5 2.5 0 0 1 2.5 2.5v5a1.25 1.25 0 0 0 2.5 0v-8.75h-2.5a1.25 1.25 0 0 1-1.25-1.25V8.018l-2.071-2.072 1.767-1.767 6.188 6.187a1.244 1.244 0 0 1 .366.884V22.5a3.75 3.75 0 0 1-7.5 0v-5h-2.5v6.25Zm-11.25 0H15v-7.5H6.25v7.5Zm0-17.5v7.5H15v-7.5H6.25Z" fill="currentColor"/> <path d="M11.666 15.833h.834V17.5H1.666v-1.667H2.5v-12.5a.833.833 0 0 1 .833-.833h7.5a.833.833 0 0 1 .833.833V10h1.667A1.667 1.667 0 0 1 15 11.667V15a.833.833 0 0 0 1.666 0V9.167H15a.833.833 0 0 1-.834-.834V5.345l-1.38-1.38 1.178-1.18 4.125 4.126a.831.831 0 0 1 .244.589V15a2.5 2.5 0 0 1-5 0v-3.333h-1.667v4.166Zm-7.5 0H10v-5H4.167v5Zm0-11.666v5H10v-5H4.167Z" 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 fill-rule="evenodd" clip-rule="evenodd" d="M5.16 3A2.16 2.16 0 0 0 3 5.16v10.18a2.16 2.16 0 0 0 2.16 2.16h10.18a2.16 2.16 0 0 0 2.16-2.16v-3.272h-1.614v3.114c0 .389-.315.704-.704.704H5.318a.705.705 0 0 1-.704-.704V5.318c0-.389.315-.704.704-.704h3.114V3H5.159Zm6.135 0a.41.41 0 0 0-.41.41v.793c0 .226.184.409.41.409h3.453l-4.244 4.245a.409.409 0 0 0 0 .578l.56.561c.16.16.42.16.58 0l4.244-4.245v3.454c0 .226.183.41.41.41h.793a.41.41 0 0 0 .409-.41V3h-6.205Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M5.16 3A2.16 2.16 0 0 0 3 5.16v10.18a2.16 2.16 0 0 0 2.16 2.16h10.18a2.16 2.16 0 0 0 2.16-2.16v-3.272h-1.614v3.114a.704.704 0 0 1-.704.704H5.318a.705.705 0 0 1-.704-.704V5.318c0-.389.315-.704.704-.704h3.114V3H5.159Zm6.135 0a.41.41 0 0 0-.41.41v.793a.41.41 0 0 0 .41.409h3.453l-4.244 4.245a.409.409 0 0 0 0 .578l.56.561c.16.16.42.16.58 0l4.244-4.245v3.454c0 .226.183.41.41.41h.793a.41.41 0 0 0 .409-.41V3h-6.205Z" fill="currentColor"/>
</svg> </svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 31 30"> <svg viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g fill="currentColor" clip-path="url(#wallet_svg__a)"> <path d="M17.139 20H4.83a3.847 3.847 0 0 1-3.846-3.845V5.39a.769.769 0 0 1 .77-.769h15.384a3.847 3.847 0 0 1 3.846 3.845v7.69A3.844 3.844 0 0 1 17.139 20ZM2.524 6.16v9.996a2.307 2.307 0 0 0 2.307 2.307H17.14a2.308 2.308 0 0 0 2.308-2.307v-7.69a2.306 2.306 0 0 0-2.308-2.306H2.524Z" fill="currentColor"/>
<path d="M22.75 27.188h-15A4.688 4.688 0 0 1 3.062 22.5V9.375A.937.937 0 0 1 4 8.437h18.75a4.688 4.688 0 0 1 4.688 4.688V22.5a4.688 4.688 0 0 1-4.688 4.688ZM4.937 10.313V22.5a2.812 2.812 0 0 0 2.813 2.813h15a2.812 2.812 0 0 0 2.813-2.813v-9.375a2.812 2.812 0 0 0-2.813-2.813H4.937Z"/> <path d="M18.677 6.159a.77.77 0 0 1-.769-.77V3.276a1.79 1.79 0 0 0-.6-1.438 1.493 1.493 0 0 0-1.284-.246l-12.9 2.93a.77.77 0 0 0-.6.769.769.769 0 0 1-1.539 0A2.306 2.306 0 0 1 2.778 3.02L15.685.091a3 3 0 0 1 2.585.546 3.338 3.338 0 0 1 1.177 2.638V5.39a.77.77 0 0 1-.77.769ZM20.216 15.386H14.83a3.077 3.077 0 0 1-2.175-5.25c.577-.578 1.36-.902 2.175-.902h5.385a.77.77 0 0 1 .769.77v4.613a.77.77 0 0 1-.77.769Zm-5.385-4.614a1.539 1.539 0 1 0 0 3.076h4.616v-3.076H14.83Z" fill="currentColor"/>
<path d="M24.625 10.312a.937.937 0 0 1-.937-.937V6.797a2.184 2.184 0 0 0-.732-1.754 1.82 1.82 0 0 0-1.565-.3L5.669 8.315a.938.938 0 0 0-.731.938.938.938 0 0 1-1.875 0 2.813 2.813 0 0 1 2.184-2.766l15.731-3.572a3.656 3.656 0 0 1 3.15.666 4.069 4.069 0 0 1 1.435 3.216v2.578a.938.938 0 0 1-.938.937ZM26.5 21.563h-6.563a3.75 3.75 0 1 1 0-7.5H26.5a.938.938 0 0 1 .938.937v5.625a.938.938 0 0 1-.938.938Zm-6.563-5.625a1.875 1.875 0 1 0 0 3.75h5.625v-3.75h-5.625Z"/>
</g>
<defs>
<clipPath id="wallet_svg__a">
<path fill="#fff" d="M.25 0h30v30h-30z"/>
</clipPath>
</defs>
</svg> </svg>
...@@ -17,7 +17,7 @@ const TEST_URLS: Record<AdBannerProviders, string> = { ...@@ -17,7 +17,7 @@ const TEST_URLS: Record<AdBannerProviders, string> = {
adbutler: 'https://servedbyadbutler.com/app.js', adbutler: 'https://servedbyadbutler.com/app.js',
hype: 'https://api.hypelab.com/v1/scripts/hp-sdk.js', hype: 'https://api.hypelab.com/v1/scripts/hp-sdk.js',
// I don't have an url for getit to test // I don't have an url for getit to test
getit: DEFAULT_URL, // getit: DEFAULT_URL,
none: DEFAULT_URL, none: DEFAULT_URL,
}; };
......
...@@ -39,7 +39,8 @@ export function ad(): CspDev.DirectiveDescriptor { ...@@ -39,7 +39,8 @@ export function ad(): CspDev.DirectiveDescriptor {
// adbutler // adbutler
'servedbyadbutler.com', 'servedbyadbutler.com',
`'sha256-${ Base64.stringify(sha256(connectAdbutler)) }'`, `'sha256-${ Base64.stringify(sha256(connectAdbutler)) }'`,
`'sha256-${ Base64.stringify(sha256(placeAd ?? '')) }'`, `'sha256-${ Base64.stringify(sha256(placeAd(undefined) ?? '')) }'`,
`'sha256-${ Base64.stringify(sha256(placeAd('mobile') ?? '')) }'`,
// slise // slise
'*.slise.xyz', '*.slise.xyz',
......
...@@ -17,7 +17,6 @@ const sizes = { ...@@ -17,7 +17,6 @@ const sizes = {
xl: defineStyle({ xl: defineStyle({
fontSize: '40px', fontSize: '40px',
lineHeight: '48px', lineHeight: '48px',
letterSpacing: '-1px',
}), }),
lg: defineStyle({ lg: defineStyle({
fontSize: '32px', fontSize: '32px',
......
import type { ArrayElement } from 'types/utils'; import type { ArrayElement } from 'types/utils';
export const SUPPORTED_AD_BANNER_PROVIDERS = [ 'slise', 'adbutler', 'coinzilla', 'hype', 'getit', 'none' ] as const; export const SUPPORTED_AD_BANNER_PROVIDERS = [
'slise',
'adbutler',
'coinzilla',
'hype',
// 'getit', // temporary disabled
'none',
] as const;
export type AdBannerProviders = ArrayElement<typeof SUPPORTED_AD_BANNER_PROVIDERS>; export type AdBannerProviders = ArrayElement<typeof SUPPORTED_AD_BANNER_PROVIDERS>;
export const SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS = [ 'adbutler' ] as const; export const SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS = [ 'adbutler' ] as const;
......
...@@ -53,7 +53,7 @@ const TokenBalances = () => { ...@@ -53,7 +53,7 @@ const TokenBalances = () => {
name="Net Worth" name="Net Worth"
value={ addressData?.exchange_rate ? `${ prefix }$${ totalUsd.toFormat(2) }` : 'N/A' } value={ addressData?.exchange_rate ? `${ prefix }$${ totalUsd.toFormat(2) }` : 'N/A' }
isLoading={ addressQuery.isPending || tokenQuery.isPending } isLoading={ addressQuery.isPending || tokenQuery.isPending }
icon={ <IconSvg name="wallet" boxSize="24px" flexShrink={ 0 } color="text_secondary"/> } icon={ <IconSvg name="wallet" boxSize="20px" flexShrink={ 0 } color="text_secondary"/> }
/> />
<TokenBalancesItem <TokenBalancesItem
name={ `${ currencyUnits.ether } Balance` } name={ `${ currencyUnits.ether } Balance` }
......
...@@ -3,7 +3,6 @@ import React from 'react'; ...@@ -3,7 +3,6 @@ import React from 'react';
import * as statsMock from 'mocks/stats/index'; import * as statsMock from 'mocks/stats/index';
import { test, expect } from 'playwright/lib'; import { test, expect } from 'playwright/lib';
import * as pwConfig from 'playwright/utils/config';
import Stats from './Stats'; import Stats from './Stats';
...@@ -18,14 +17,6 @@ test.describe('all items', () => { ...@@ -18,14 +17,6 @@ test.describe('all items', () => {
test('+@mobile +@dark-mode', async() => { test('+@mobile +@dark-mode', async() => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test.describe('screen xl', () => {
test.use({ viewport: pwConfig.viewport.xl });
test('', async() => {
await expect(component).toHaveScreenshot();
});
});
}); });
test('4 items default view +@mobile -@default', async({ render, mockApiResponse, mockEnvs }) => { test('4 items default view +@mobile -@default', async({ render, mockApiResponse, mockEnvs }) => {
......
...@@ -2,8 +2,6 @@ import { Grid } from '@chakra-ui/react'; ...@@ -2,8 +2,6 @@ import { Grid } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { WEI } from 'lib/consts'; import { WEI } from 'lib/consts';
...@@ -11,8 +9,7 @@ import { HOMEPAGE_STATS } from 'stubs/stats'; ...@@ -11,8 +9,7 @@ import { HOMEPAGE_STATS } from 'stubs/stats';
import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip'; import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip';
import GasPrice from 'ui/shared/gas/GasPrice'; import GasPrice from 'ui/shared/gas/GasPrice';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import StatsWidget from 'ui/shared/stats/StatsWidget';
import StatsItem from './StatsItem';
const hasAvgBlockTime = config.UI.homepage.showAvgBlockTime; const hasAvgBlockTime = config.UI.homepage.showAvgBlockTime;
const rollupFeature = config.features.rollup; const rollupFeature = config.features.rollup;
...@@ -58,7 +55,7 @@ const Stats = () => { ...@@ -58,7 +55,7 @@ const Stats = () => {
let content; let content;
const lastItemTouchStyle = { gridColumn: { base: 'span 2', lg: 'unset' } }; const lastItemStyle = { gridColumn: 'span 2' };
let itemsCount = 5; let itemsCount = 5;
!hasGasTracker && itemsCount--; !hasGasTracker && itemsCount--;
...@@ -75,12 +72,9 @@ const Stats = () => { ...@@ -75,12 +72,9 @@ const Stats = () => {
isLoading={ isLoading } isLoading={ isLoading }
name="info" name="info"
boxSize={ 5 } boxSize={ 5 }
display="block" flexShrink={ 0 }
cursor="pointer" cursor="pointer"
_hover={{ color: 'link_hovered' }} _hover={{ color: 'link_hovered' }}
position="absolute"
top={{ base: 'calc(50% - 12px)', lg: '10px', xl: 'calc(50% - 12px)' }}
right="10px"
/> />
</GasInfoTooltip> </GasInfoTooltip>
) : null; ) : null;
...@@ -88,80 +82,80 @@ const Stats = () => { ...@@ -88,80 +82,80 @@ const Stats = () => {
content = ( content = (
<> <>
{ rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && ( { rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && (
<StatsItem <StatsWidget
icon="txn_batches" icon="txn_batches_slim"
title="Latest batch" label="Latest batch"
value={ (zkEvmLatestBatchQuery.data || 0).toLocaleString() } value={ (zkEvmLatestBatchQuery.data || 0).toLocaleString() }
url={ route({ pathname: '/batches' }) } href={{ pathname: '/batches' }}
isLoading={ isLoading } isLoading={ isLoading }
/> />
) } ) }
{ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && ( { rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && (
<StatsItem <StatsWidget
icon="txn_batches" icon="txn_batches_slim"
title="Latest batch" label="Latest batch"
value={ (zkSyncLatestBatchQuery.data || 0).toLocaleString() } value={ (zkSyncLatestBatchQuery.data || 0).toLocaleString() }
url={ route({ pathname: '/batches' }) } href={{ pathname: '/batches' }}
isLoading={ isLoading } isLoading={ isLoading }
/> />
) } ) }
{ !(rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync')) && ( { !(rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync')) && (
<StatsItem <StatsWidget
icon="block" icon="block_slim"
title="Total blocks" label="Total blocks"
value={ Number(data.total_blocks).toLocaleString() } value={ Number(data.total_blocks).toLocaleString() }
url={ route({ pathname: '/blocks' }) } href={{ pathname: '/blocks' }}
isLoading={ isLoading } isLoading={ isLoading }
/> />
) } ) }
{ hasAvgBlockTime && ( { hasAvgBlockTime && (
<StatsItem <StatsWidget
icon="clock-light" icon="clock"
title="Average block time" label="Average block time"
value={ `${ (data.average_block_time / 1000).toFixed(1) }s` } value={ `${ (data.average_block_time / 1000).toFixed(1) }s` }
isLoading={ isLoading } isLoading={ isLoading }
/> />
) } ) }
<StatsItem <StatsWidget
icon="transactions" icon="transactions_slim"
title="Total transactions" label="Total transactions"
value={ Number(data.total_transactions).toLocaleString() } value={ Number(data.total_transactions).toLocaleString() }
url={ route({ pathname: '/txs' }) } href={{ pathname: '/txs' }}
isLoading={ isLoading } isLoading={ isLoading }
/> />
{ rollupFeature.isEnabled && data.last_output_root_size && ( { rollupFeature.isEnabled && data.last_output_root_size && (
<StatsItem <StatsWidget
icon="txn_batches" icon="txn_batches_slim"
title="Latest L1 state batch" label="Latest L1 state batch"
value={ data.last_output_root_size } value={ data.last_output_root_size }
url={ route({ pathname: '/batches' }) } href={{ pathname: '/batches' }}
isLoading={ isLoading } isLoading={ isLoading }
/> />
) } ) }
<StatsItem <StatsWidget
icon="wallet" icon="wallet"
title="Wallet addresses" label="Wallet addresses"
value={ Number(data.total_addresses).toLocaleString() } value={ Number(data.total_addresses).toLocaleString() }
_last={ isOdd ? lastItemTouchStyle : undefined }
isLoading={ isLoading } isLoading={ isLoading }
_last={ isOdd ? lastItemStyle : undefined }
/> />
{ hasGasTracker && data.gas_prices && ( { hasGasTracker && data.gas_prices && (
<StatsItem <StatsWidget
icon="gas" icon="gas"
title="Gas tracker" label="Gas tracker"
value={ data.gas_prices.average ? <GasPrice data={ data.gas_prices.average }/> : 'N/A' } value={ data.gas_prices.average ? <GasPrice data={ data.gas_prices.average }/> : 'N/A' }
_last={ isOdd ? lastItemTouchStyle : undefined } hint={ gasInfoTooltip }
tooltip={ gasInfoTooltip }
isLoading={ isLoading } isLoading={ isLoading }
_last={ isOdd ? lastItemStyle : undefined }
/> />
) } ) }
{ data.rootstock_locked_btc && ( { data.rootstock_locked_btc && (
<StatsItem <StatsWidget
icon="coins/bitcoin" icon="coins/bitcoin"
title="BTC Locked in 2WP" label="BTC Locked in 2WP"
value={ `${ BigNumber(data.rootstock_locked_btc).div(WEI).dp(0).toFormat() } RBTC` } value={ `${ BigNumber(data.rootstock_locked_btc).div(WEI).dp(0).toFormat() } RBTC` }
_last={ isOdd ? lastItemTouchStyle : undefined }
isLoading={ isLoading } isLoading={ isLoading }
_last={ isOdd ? lastItemStyle : undefined }
/> />
) } ) }
</> </>
...@@ -170,10 +164,10 @@ const Stats = () => { ...@@ -170,10 +164,10 @@ const Stats = () => {
return ( return (
<Grid <Grid
gridTemplateColumns={{ lg: `repeat(${ itemsCount }, 1fr)`, base: '1fr 1fr' }} gridTemplateColumns="1fr 1fr"
gridTemplateRows={{ lg: 'none', base: undefined }}
gridGap={{ base: 1, lg: 2 }} gridGap={{ base: 1, lg: 2 }}
marginTop={ 3 } flexBasis="50%"
flexGrow={ 1 }
> >
{ content } { content }
</Grid> </Grid>
......
import type { SystemStyleObject } from '@chakra-ui/react';
import { Skeleton, Flex, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react';
import breakpoints from 'theme/foundations/breakpoints';
import type { IconName } from 'ui/shared/IconSvg';
import IconSvg from 'ui/shared/IconSvg';
type Props = {
icon: IconName;
title: string;
value: string | React.ReactNode;
className?: string;
tooltip?: React.ReactNode;
url?: string;
isLoading?: boolean;
}
const LARGEST_BREAKPOINT = '1240px';
const StatsItem = ({ icon, title, value, className, tooltip, url, isLoading }: Props) => {
const sxContainer: SystemStyleObject = {
[`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { flexDirection: 'column' },
};
const sxText: SystemStyleObject = {
[`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { alignItems: 'center' },
};
const bgColor = useColorModeValue('blue.50', 'whiteAlpha.100');
const loadingBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
return (
<Flex
backgroundColor={ isLoading ? loadingBgColor : bgColor }
padding={ 3 }
borderRadius="md"
flexDirection="row"
sx={ sxContainer }
alignItems="center"
columnGap={ 3 }
rowGap={ 2 }
className={ className }
color={ useColorModeValue('black', 'white') }
position="relative"
{ ...(url && !isLoading ? {
as: 'a',
href: url,
} : {}) }
>
<IconSvg name={ icon } boxSize={ 7 } isLoading={ isLoading } borderRadius="base"/>
<Flex
flexDirection="column"
alignItems="start"
sx={ sxText }
>
<Skeleton isLoaded={ !isLoading } color="text_secondary" fontSize="xs" lineHeight="16px" borderRadius="base">
<span>{ title }</span>
</Skeleton>
<Skeleton isLoaded={ !isLoading } fontWeight={ 500 } fontSize="md" color={ useColorModeValue('black', 'white') } borderRadius="base">
{ typeof value === 'string' ? <span>{ value }</span> : value }
</Skeleton>
</Flex>
{ tooltip }
</Flex>
);
};
export default chakra(StatsItem);
import { Flex } from '@chakra-ui/react'; import { chakra, Flex, Box } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
...@@ -15,21 +15,25 @@ const ChainIndicatorChartContainer = ({ data, isError, isPending }: Props) => { ...@@ -15,21 +15,25 @@ const ChainIndicatorChartContainer = ({ data, isError, isPending }: Props) => {
const content = (() => { const content = (() => {
if (isPending) { if (isPending) {
return <ContentLoader mt="auto"/>; return <ContentLoader mt="auto" fontSize="xs"/>;
} }
if (isError) { if (isError) {
return <DataFetchAlert/>; return <DataFetchAlert fontSize="xs" p={ 3 }/>;
} }
if (data[0].items.length === 0) { if (data[0].items.length === 0) {
return <span>no data</span>; return <chakra.span fontSize="xs">no data</chakra.span>;
} }
return <ChainIndicatorChart data={ data }/>; return (
<Box mx="-10px" my="-5px" h="calc(100% + 10px)" w="calc(100% + 20px)">
<ChainIndicatorChart data={ data }/>
</Box>
);
})(); })();
return <Flex h={{ base: '150px', lg: 'auto' }} minH="150px" alignItems="flex-start" flexGrow={ 1 }>{ content }</Flex>; return <Flex h={{ base: '80px', lg: '110px' }} alignItems="flex-start" flexGrow={ 1 }>{ content }</Flex>;
}; };
export default React.memo(ChainIndicatorChartContainer); export default React.memo(ChainIndicatorChartContainer);
...@@ -6,8 +6,6 @@ import type { HomeStats } from 'types/api/stats'; ...@@ -6,8 +6,6 @@ import type { HomeStats } from 'types/api/stats';
import type { ChainIndicatorId } from 'types/homepage'; import type { ChainIndicatorId } from 'types/homepage';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useIsMobile from 'lib/hooks/useIsMobile';
import IconSvg from 'ui/shared/IconSvg';
interface Props { interface Props {
id: ChainIndicatorId; id: ChainIndicatorId;
...@@ -21,42 +19,27 @@ interface Props { ...@@ -21,42 +19,27 @@ interface Props {
} }
const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onClick, stats }: Props) => { const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onClick, stats }: Props) => {
const isMobile = useIsMobile(); const activeColor = useColorModeValue('gray.500', 'gray.400');
const activeBgColor = useColorModeValue('white', 'black');
const activeBgColorDesktop = useColorModeValue('white', 'gray.900');
const activeBgColorMobile = useColorModeValue('white', 'black');
const activeBgColor = isMobile ? activeBgColorMobile : activeBgColorDesktop;
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
onClick(id); onClick(id);
}, [ id, onClick ]); }, [ id, onClick ]);
const valueContent = (() => { const valueContent = (() => {
if (isMobile) {
return null;
}
if (stats.isPlaceholderData) {
return (
<Skeleton
h={ 3 }
w="70px"
my={ 1.5 }
// ssr: isMobile = undefined, isLoading = true
display={{ base: 'none', lg: 'block' }}
/>
);
}
if (!stats.data) { if (!stats.data) {
return <Text variant="secondary" fontWeight={ 400 }>no data</Text>; return <Text variant="secondary" fontWeight={ 400 }>no data</Text>;
} }
return <Text variant="secondary" fontWeight={ 600 }>{ value(stats.data) }</Text>; return (
<Skeleton isLoaded={ !stats.isPlaceholderData } variant="secondary" fontWeight={ 600 } minW="30px">
{ value(stats.data) }
</Skeleton>
);
})(); })();
const valueDiffContent = (() => { const valueDiffContent = (() => {
if (isMobile || !valueDiff) { if (!valueDiff) {
return null; return null;
} }
const diff = valueDiff(stats.data); const diff = valueDiff(stats.data);
...@@ -67,8 +50,8 @@ const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onC ...@@ -67,8 +50,8 @@ const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onC
const diffColor = diff >= 0 ? 'green.500' : 'red.500'; const diffColor = diff >= 0 ? 'green.500' : 'red.500';
return ( return (
<Skeleton isLoaded={ !stats.isPlaceholderData } ml={ 3 } display="flex" alignItems="center" color={ diffColor }> <Skeleton isLoaded={ !stats.isPlaceholderData } ml={ 1 } display="flex" alignItems="center" color={ diffColor }>
<IconSvg name="arrows/up-head" boxSize={ 5 } mr={ 1 } transform={ diff < 0 ? 'rotate(180deg)' : 'rotate(0)' }/> <span>{ diff >= 0 ? '+' : '-' }</span>
<Text color={ diffColor } fontWeight={ 600 }>{ diff }%</Text> <Text color={ diffColor } fontWeight={ 600 }>{ diff }%</Text>
</Skeleton> </Skeleton>
); );
...@@ -77,25 +60,28 @@ const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onC ...@@ -77,25 +60,28 @@ const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onC
return ( return (
<Flex <Flex
alignItems="center" alignItems="center"
columnGap={ 3 } columnGap={ 2 }
px={ 4 } flexGrow={{ base: 0, lg: 1 }}
py={ 2 } px={{ base: '6px', lg: 2 }}
py="6px"
as="li" as="li"
borderRadius="md" borderRadius="base"
cursor="pointer" cursor="pointer"
color={ isSelected ? activeColor : 'link' }
bgColor={ isSelected ? activeBgColor : undefined }
onClick={ handleClick } onClick={ handleClick }
bgColor={ isSelected ? activeBgColor : 'inherit' } fontSize="xs"
boxShadow={ isSelected ? 'lg' : 'none' } fontWeight={ 500 }
zIndex={ isSelected ? 1 : 'initial' }
_hover={{ _hover={{
activeBgColor, bgColor: activeBgColor,
color: isSelected ? activeColor : 'link_hovered',
zIndex: 1, zIndex: 1,
}} }}
> >
{ icon } { icon }
<Box> <Box display={{ base: 'none', lg: 'block' }}>
<Text fontFamily="heading" fontWeight={ 500 }>{ title }</Text> <span>{ title }</span>
<Flex alignItems="center"> <Flex alignItems="center" color="text">
{ valueContent } { valueContent }
{ valueDiffContent } { valueDiffContent }
</Flex> </Flex>
......
...@@ -22,7 +22,7 @@ test.describe('daily txs chart', () => { ...@@ -22,7 +22,7 @@ test.describe('daily txs chart', () => {
await mockApiResponse('stats_charts_txs', dailyTxsMock.base); await mockApiResponse('stats_charts_txs', dailyTxsMock.base);
await mockAssetResponse(statsMock.withSecondaryCoin.coin_image as string, './playwright/mocks/image_svg.svg'); await mockAssetResponse(statsMock.withSecondaryCoin.coin_image as string, './playwright/mocks/image_svg.svg');
component = await render(<ChainIndicators/>); component = await render(<ChainIndicators/>);
await page.hover('.ChartOverlay', { position: { x: 100, y: 100 } }); await page.hover('.ChartOverlay', { position: { x: 50, y: 50 } });
}); });
test('+@mobile', async() => { test('+@mobile', async() => {
......
...@@ -38,10 +38,7 @@ const ChainIndicators = () => { ...@@ -38,10 +38,7 @@ const ChainIndicators = () => {
}, },
}); });
const bgColorDesktop = useColorModeValue('white', 'gray.900'); const bgColor = useColorModeValue('gray.50', 'whiteAlpha.100');
const bgColorMobile = useColorModeValue('white', 'black');
const listBgColorDesktop = useColorModeValue('gray.50', 'black');
const listBgColorMobile = useColorModeValue('gray.50', 'gray.900');
if (indicators.length === 0) { if (indicators.length === 0) {
return null; return null;
...@@ -49,15 +46,15 @@ const ChainIndicators = () => { ...@@ -49,15 +46,15 @@ const ChainIndicators = () => {
const valueTitle = (() => { const valueTitle = (() => {
if (statsQueryResult.isPlaceholderData) { if (statsQueryResult.isPlaceholderData) {
return <Skeleton h="48px" w="215px" mt={ 3 } mb={ 4 }/>; return <Skeleton h="36px" w="215px"/>;
} }
if (!statsQueryResult.data) { if (!statsQueryResult.data) {
return <Text mt={ 3 } mb={ 4 }>There is no data</Text>; return <Text fontSize="xs">There is no data</Text>;
} }
return ( return (
<Text fontWeight={ 600 } fontFamily="heading" fontSize="48px" lineHeight="48px" mt={ 3 }> <Text fontWeight={ 700 } fontSize="30px" lineHeight="36px">
{ indicator?.value(statsQueryResult.data) } { indicator?.value(statsQueryResult.data) }
</Text> </Text>
); );
...@@ -85,23 +82,22 @@ const ChainIndicators = () => { ...@@ -85,23 +82,22 @@ const ChainIndicators = () => {
return ( return (
<Flex <Flex
p={{ base: 0, lg: 8 }} px={{ base: 3, lg: 4 }}
borderRadius={{ base: 'none', lg: 'lg' }} py={ 3 }
boxShadow={{ base: 'none', lg: 'xl' }} borderRadius="base"
bgColor={{ base: bgColorMobile, lg: bgColorDesktop }} bgColor={ bgColor }
columnGap={ 6 } columnGap={{ base: 3, lg: 4 }}
rowGap={ 0 } rowGap={ 0 }
flexDir={{ base: 'column', lg: 'row' }} flexBasis="50%"
w="100%" flexGrow={ 1 }
alignItems="stretch" alignItems="stretch"
mt={{ base: 1, lg: 3 }}
> >
<Flex flexGrow={ 1 } flexDir="column" order={{ base: 2, lg: 1 }} p={{ base: 6, lg: 0 }}> <Flex flexGrow={ 1 } flexDir="column">
<Flex alignItems="center"> <Flex alignItems="center">
<Text fontWeight={ 500 } fontFamily="heading" fontSize="lg">{ indicator?.title }</Text> <Text fontWeight={ 500 }>{ indicator?.title }</Text>
{ indicator?.hint && <Hint label={ indicator.hint } ml={ 1 }/> } { indicator?.hint && <Hint label={ indicator.hint } ml={ 1 }/> }
</Flex> </Flex>
<Flex mb={ 4 } alignItems="end"> <Flex mb={{ base: 0, lg: 2 }} mt={ 1 } alignItems="end">
{ valueTitle } { valueTitle }
{ valueDiff } { valueDiff }
</Flex> </Flex>
...@@ -112,11 +108,9 @@ const ChainIndicators = () => { ...@@ -112,11 +108,9 @@ const ChainIndicators = () => {
flexShrink={ 0 } flexShrink={ 0 }
flexDir="column" flexDir="column"
as="ul" as="ul"
p={ 3 }
borderRadius="lg" borderRadius="lg"
bgColor={{ base: listBgColorMobile, lg: listBgColorDesktop }} rowGap="6px"
rowGap={ 3 } m={{ base: 'auto 0', lg: 0 }}
order={{ base: 1, lg: 2 }}
> >
{ indicators.map((indicator) => ( { indicators.map((indicator) => (
<ChainIndicatorItem <ChainIndicatorItem
......
...@@ -7,6 +7,7 @@ import config from 'configs/app'; ...@@ -7,6 +7,7 @@ import config from 'configs/app';
import { sortByDateDesc } from 'ui/shared/chart/utils/sorts'; import { sortByDateDesc } from 'ui/shared/chart/utils/sorts';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import NativeTokenIcon from 'ui/shared/NativeTokenIcon'; import NativeTokenIcon from 'ui/shared/NativeTokenIcon';
import TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
const nonNullTailReducer = (result: Array<TimeChartItemRaw>, item: TimeChartItemRaw) => { const nonNullTailReducer = (result: Array<TimeChartItemRaw>, item: TimeChartItemRaw) => {
if (item.value === null && result.length === 0) { if (item.value === null && result.length === 0) {
...@@ -70,7 +71,7 @@ const secondaryCoinPriceIndicator: TChainIndicator<'stats_charts_secondary_coin_ ...@@ -70,7 +71,7 @@ const secondaryCoinPriceIndicator: TChainIndicator<'stats_charts_secondary_coin_
'$N/A' : '$N/A' :
'$' + Number(stats.secondary_coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }), '$' + Number(stats.secondary_coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
valueDiff: () => null, valueDiff: () => null,
icon: <NativeTokenIcon boxSize={ 6 }/>, icon: <TokenLogoPlaceholder boxSize={ 6 }/>,
hint: `${ config.chain.secondaryCoin.symbol } token daily price in USD.`, hint: `${ config.chain.secondaryCoin.symbol } token daily price in USD.`,
api: { api: {
resourceName: 'stats_charts_secondary_coin_price', resourceName: 'stats_charts_secondary_coin_price',
......
...@@ -2,6 +2,7 @@ import { Box, Flex, Heading } from '@chakra-ui/react'; ...@@ -2,6 +2,7 @@ import { Box, Flex, Heading } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import ChainIndicators from 'ui/home/indicators/ChainIndicators'; import ChainIndicators from 'ui/home/indicators/ChainIndicators';
import LatestBlocks from 'ui/home/LatestBlocks'; import LatestBlocks from 'ui/home/LatestBlocks';
import LatestZkEvmL2Batches from 'ui/home/LatestZkEvmL2Batches'; import LatestZkEvmL2Batches from 'ui/home/LatestZkEvmL2Batches';
...@@ -15,44 +16,51 @@ import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop'; ...@@ -15,44 +16,51 @@ import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop';
const rollupFeature = config.features.rollup; const rollupFeature = config.features.rollup;
const Home = () => { const Home = () => {
const isMobile = useIsMobile();
return ( return (
<Box as="main"> <Box as="main">
<Box <Flex
w="100%" w="100%"
background={ config.UI.homepage.plate.background } background={ config.UI.homepage.plate.background }
borderRadius={{ base: 'md', lg: 'xl' }} borderRadius="md"
px={{ base: 4, lg: 10 }} p={{ base: 4, lg: 8 }}
py={{ base: 3, lg: 8 }} columnGap={ 8 }
minW={{ base: 'unset', lg: '900px' }} alignItems="center"
data-label="hero plate" data-label="hero plate"
> >
<Flex mb={{ base: 2, lg: 6 }} justifyContent="space-between" alignItems="center"> <Box flexGrow={ 1 }>
<Heading <Flex mb={{ base: 2, lg: 3 }} justifyContent="space-between" alignItems="center" columnGap={ 2 }>
as="h1" <Heading
fontSize={{ base: '18px', lg: '40px' }} as="h1"
lineHeight={{ base: '24px', lg: '48px' }} fontSize={{ base: '18px', lg: '30px' }}
fontWeight={ 600 } lineHeight={{ base: '24px', lg: '36px' }}
color={ config.UI.homepage.plate.textColor } fontWeight={{ base: 500, lg: 700 }}
> color={ config.UI.homepage.plate.textColor }
{ >
config.meta.seo.enhancedDataEnabled ? {
`${ config.chain.name } blockchain explorer` : config.meta.seo.enhancedDataEnabled ?
`${ config.chain.name } explorer` `${ config.chain.name } blockchain explorer` :
} `${ config.chain.name } explorer`
</Heading> }
{ config.UI.navigation.layout === 'vertical' && ( </Heading>
<Box display={{ base: 'none', lg: 'flex' }}> { config.UI.navigation.layout === 'vertical' && (
{ config.features.account.isEnabled && <ProfileMenuDesktop isHomePage/> } <Box display={{ base: 'none', lg: 'flex' }}>
{ config.features.blockchainInteraction.isEnabled && <WalletMenuDesktop isHomePage/> } { config.features.account.isEnabled && <ProfileMenuDesktop isHomePage/> }
</Box> { config.features.blockchainInteraction.isEnabled && <WalletMenuDesktop isHomePage/> }
) } </Box>
</Flex> ) }
<SearchBar isHomepage/> </Flex>
</Box> <SearchBar isHomepage/>
<Stats/> </Box>
<ChainIndicators/> { !isMobile && <AdBanner platform="mobile" w="fit-content" flexShrink={ 0 } borderRadius="md" overflow="hidden"/> }
<AdBanner mt={ 6 } mx="auto" display="flex" justifyContent="center"/> </Flex>
<Flex mt={ 6 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 6 }> <Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 2 } rowGap={ 1 } mt={ 3 } _empty={{ mt: 0 }}>
<Stats/>
<ChainIndicators/>
</Flex>
{ isMobile && <AdBanner mt={ 6 } mx="auto" display="flex" justifyContent="center"/> }
<Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 6 }>
{ rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' ? <LatestZkEvmL2Batches/> : <LatestBlocks/> } { rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' ? <LatestZkEvmL2Batches/> : <LatestBlocks/> }
<Box flexGrow={ 1 }> <Box flexGrow={ 1 }>
<Transactions/> <Transactions/>
......
import { chakra } from '@chakra-ui/react'; import { chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { BannerPlatform } from './types';
import config from 'configs/app'; import config from 'configs/app';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
...@@ -9,7 +11,13 @@ import AdBannerContent from './AdBannerContent'; ...@@ -9,7 +11,13 @@ import AdBannerContent from './AdBannerContent';
const feature = config.features.adsBanner; const feature = config.features.adsBanner;
const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: boolean }) => { interface Props {
className?: string;
isLoading?: boolean;
platform?: BannerPlatform;
}
const AdBanner = ({ className, isLoading, platform }: Props) => {
const provider = useAppContext().adBannerProvider; const provider = useAppContext().adBannerProvider;
const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies); const hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies);
...@@ -23,6 +31,7 @@ const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: bo ...@@ -23,6 +31,7 @@ const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: bo
className={ className } className={ className }
isLoading={ isLoading } isLoading={ isLoading }
provider={ provider } provider={ provider }
platform={ platform }
/> />
); );
}; };
......
import { chakra, Skeleton } from '@chakra-ui/react'; import { chakra, Skeleton } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { BannerPlatform } from './types';
import type { AdBannerProviders } from 'types/client/adProviders'; import type { AdBannerProviders } from 'types/client/adProviders';
import config from 'configs/app'; import config from 'configs/app';
import AdbutlerBanner from './AdbutlerBanner'; import AdbutlerBanner from './AdbutlerBanner';
import CoinzillaBanner from './CoinzillaBanner'; import CoinzillaBanner from './CoinzillaBanner';
import GetitBanner from './GetitBanner'; // import GetitBanner from './GetitBanner';
import HypeBanner from './HypeBanner'; import HypeBanner from './HypeBanner';
import SliseBanner from './SliseBanner'; import SliseBanner from './SliseBanner';
const feature = config.features.adsBanner; const feature = config.features.adsBanner;
const AdBannerContent = ({ className, isLoading, provider }: { className?: string; isLoading?: boolean; provider: AdBannerProviders }) => { interface Props {
className?: string;
isLoading?: boolean;
platform?: BannerPlatform;
provider: AdBannerProviders;
}
const AdBannerContent = ({ className, isLoading, provider, platform }: Props) => {
const content = (() => { const content = (() => {
switch (provider) { switch (provider) {
case 'adbutler': case 'adbutler':
return <AdbutlerBanner/>; return <AdbutlerBanner platform={ platform }/>;
case 'coinzilla': case 'coinzilla':
return <CoinzillaBanner/>; return <CoinzillaBanner platform={ platform }/>;
case 'getit': // case 'getit':
return <GetitBanner/>; // return <GetitBanner platform={ platform }/>;
case 'hype': case 'hype':
return <HypeBanner/>; return <HypeBanner platform={ platform }/>;
case 'slise': case 'slise':
return <SliseBanner/>; return <SliseBanner platform={ platform }/>;
} }
})(); })();
......
...@@ -3,6 +3,8 @@ import { useRouter } from 'next/navigation'; ...@@ -3,6 +3,8 @@ import { useRouter } from 'next/navigation';
import Script from 'next/script'; import Script from 'next/script';
import React from 'react'; import React from 'react';
import type { BannerProps } from './types';
import config from 'configs/app'; import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import isBrowser from 'lib/isBrowser'; import isBrowser from 'lib/isBrowser';
...@@ -10,9 +12,10 @@ import { connectAdbutler, placeAd, ADBUTLER_ACCOUNT } from 'ui/shared/ad/adbutle ...@@ -10,9 +12,10 @@ import { connectAdbutler, placeAd, ADBUTLER_ACCOUNT } from 'ui/shared/ad/adbutle
const feature = config.features.adsBanner; const feature = config.features.adsBanner;
const AdbutlerBanner = ({ className }: { className?: string }) => { const AdbutlerBanner = ({ className, platform }: BannerProps) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobileViewport = useIsMobile();
const isMobile = platform === 'mobile' || isMobileViewport;
React.useEffect(() => { React.useEffect(() => {
if (!('adButler' in feature)) { if (!('adButler' in feature)) {
...@@ -24,10 +27,10 @@ const AdbutlerBanner = ({ className }: { className?: string }) => { ...@@ -24,10 +27,10 @@ const AdbutlerBanner = ({ className }: { className?: string }) => {
if (!window.AdButler.ads) { if (!window.AdButler.ads) {
window.AdButler.ads = []; window.AdButler.ads = [];
} }
const adButlerConfig = isMobile ? feature.adButler.config.mobile : feature.adButler.config.desktop;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: // @ts-ignore:
let plc = window[`plc${ feature.adButler.config.mobile.id }`] || 0; let plc = window[`plc${ adButlerConfig.id }`] || 0;
const adButlerConfig = isMobile ? feature.adButler.config.mobile : feature.adButler.config.desktop;
const banner = document.getElementById('ad-banner'); const banner = document.getElementById('ad-banner');
if (banner) { if (banner) {
banner.innerHTML = '<' + 'div id="placement_' + adButlerConfig?.id + '_' + plc + '"></' + 'div>'; banner.innerHTML = '<' + 'div id="placement_' + adButlerConfig?.id + '_' + plc + '"></' + 'div>';
...@@ -46,10 +49,34 @@ const AdbutlerBanner = ({ className }: { className?: string }) => { ...@@ -46,10 +49,34 @@ const AdbutlerBanner = ({ className }: { className?: string }) => {
} }
}, [ router, isMobile ]); }, [ router, isMobile ]);
if (!('adButler' in feature)) {
return null;
}
const { width, height } = (() => {
switch (platform) {
case 'desktop':
return { width: `${ feature.adButler.config.desktop.width }px`, height: `${ feature.adButler.config.desktop.height }px` };
case 'mobile':
return { width: `${ feature.adButler.config.mobile.width }px`, height: `${ feature.adButler.config.mobile.height }px` };
default:
return {
width: {
base: `${ feature.adButler.config.mobile.width }px`,
lg: `${ feature.adButler.config.desktop.width }px`,
},
height: {
base: `${ feature.adButler.config.mobile.height }px`,
lg: `${ feature.adButler.config.desktop.height }px`,
},
};
}
})() ?? { width: '0', height: '0' };
return ( return (
<Flex className={ className } id="adBanner" h={{ base: '100px', lg: '90px' }}> <Flex className={ className } id="adBanner" h={ height } w={ width }>
<Script strategy="lazyOnload" id="ad-butler-1">{ connectAdbutler }</Script> <Script strategy="lazyOnload" id="ad-butler-1">{ connectAdbutler }</Script>
<Script strategy="lazyOnload" id="ad-butler-2">{ placeAd }</Script> <Script strategy="lazyOnload" id="ad-butler-2">{ placeAd(platform) }</Script>
<div id="ad-banner"></div> <div id="ad-banner"></div>
</Flex> </Flex>
); );
......
...@@ -2,25 +2,44 @@ import { Flex, chakra } from '@chakra-ui/react'; ...@@ -2,25 +2,44 @@ import { Flex, chakra } from '@chakra-ui/react';
import Script from 'next/script'; import Script from 'next/script';
import React from 'react'; import React from 'react';
import type { BannerProps } from './types';
import isBrowser from 'lib/isBrowser'; import isBrowser from 'lib/isBrowser';
const CoinzillaBanner = ({ className }: { className?: string }) => { const CoinzillaBanner = ({ className, platform }: BannerProps) => {
const isInBrowser = isBrowser(); const isInBrowser = isBrowser();
const { width, height } = (() => {
switch (platform) {
case 'desktop':
return { width: 728, height: 90 };
case 'mobile':
return { width: 320, height: 100 };
default:
return { width: undefined, height: undefined };
}
})();
React.useEffect(() => { React.useEffect(() => {
if (isInBrowser) { if (isInBrowser) {
window.coinzilla_display = window.coinzilla_display || []; window.coinzilla_display = window.coinzilla_display || [];
const cDisplayPreferences = { const cDisplayPreferences = {
zone: '26660bf627543e46851', zone: '26660bf627543e46851',
width: '728', width: width ? String(width) : '728',
height: '90', height: height ? String(height) : '90',
}; };
window.coinzilla_display.push(cDisplayPreferences); window.coinzilla_display.push(cDisplayPreferences);
} }
}, [ isInBrowser ]); }, [ height, isInBrowser, width ]);
return ( return (
<Flex className={ className } id="adBanner" h={{ base: '100px', lg: '90px' }}> <Flex
className={ className }
id={ 'adBanner' + (platform ? `_${ platform }` : '') }
h={ height ? `${ height }px` : { base: '100px', lg: '90px' } }
w={ width ? `${ width }px` : undefined }
>
<Script strategy="lazyOnload" src="https://coinzillatag.com/lib/display.js"/> <Script strategy="lazyOnload" src="https://coinzillatag.com/lib/display.js"/>
<div className="coinzilla" data-zone="C-26660bf627543e46851"></div> <div className="coinzilla" data-zone="C-26660bf627543e46851"></div>
</Flex> </Flex>
......
...@@ -2,6 +2,8 @@ import { Flex, chakra } from '@chakra-ui/react'; ...@@ -2,6 +2,8 @@ import { Flex, chakra } from '@chakra-ui/react';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import React from 'react'; import React from 'react';
import type { BannerProps } from './types';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useAccount from 'lib/web3/useAccount'; import useAccount from 'lib/web3/useAccount';
...@@ -9,17 +11,17 @@ const GetitAdPlugin = dynamic(() => import('getit-sdk').then(module => module.Ge ...@@ -9,17 +11,17 @@ const GetitAdPlugin = dynamic(() => import('getit-sdk').then(module => module.Ge
const GETIT_API_KEY = 'ZmGXVvwYUAW4yXL8RzWQHNKmpSyQmt3TDXsXUxqFqXPdoaiSSFyca3BOyunDcWdyOwTkX3UVVQel28qbjoOoWPxYVpPdNzbUNkAHyFyJX7Lk9TVcPDZKTQmwHlSMzO3a'; const GETIT_API_KEY = 'ZmGXVvwYUAW4yXL8RzWQHNKmpSyQmt3TDXsXUxqFqXPdoaiSSFyca3BOyunDcWdyOwTkX3UVVQel28qbjoOoWPxYVpPdNzbUNkAHyFyJX7Lk9TVcPDZKTQmwHlSMzO3a';
const GetitBanner = ({ className }: { className?: string }) => { const GetitBanner = ({ className, platform }: BannerProps) => {
const isMobile = Boolean(useIsMobile()); const isMobile = Boolean(useIsMobile());
const { address } = useAccount(); const { address } = useAccount();
return ( return (
<Flex className={ className } h="90px"> <Flex className={ className } h="90px" w={{ base: '270px', lg: platform === 'mobile' ? '270px' : undefined }}>
<GetitAdPlugin <GetitAdPlugin
key={ isMobile.toString() } key={ isMobile.toString() }
apiKey={ GETIT_API_KEY } apiKey={ GETIT_API_KEY }
walletConnected={ address ? address : '' } walletConnected={ address ? address : '' }
isMobile={ isMobile } isMobile={ platform === 'mobile' || isMobile }
slotId="0" slotId="0"
/> />
</Flex> </Flex>
......
...@@ -3,6 +3,8 @@ import { Banner, setWalletAddresses } from '@hypelab/sdk-react'; ...@@ -3,6 +3,8 @@ import { Banner, setWalletAddresses } from '@hypelab/sdk-react';
import Script from 'next/script'; import Script from 'next/script';
import React from 'react'; import React from 'react';
import type { BannerProps } from './types';
import useAccount from 'lib/web3/useAccount'; import useAccount from 'lib/web3/useAccount';
import { hypeInit } from './hypeBannerScript'; import { hypeInit } from './hypeBannerScript';
...@@ -10,7 +12,7 @@ import { hypeInit } from './hypeBannerScript'; ...@@ -10,7 +12,7 @@ import { hypeInit } from './hypeBannerScript';
const DESKTOP_BANNER_SLUG = 'b1559fc3e7'; const DESKTOP_BANNER_SLUG = 'b1559fc3e7';
const MOBILE_BANNER_SLUG = '668ed80a9e'; const MOBILE_BANNER_SLUG = '668ed80a9e';
const HypeBanner = ({ className }: { className?: string }) => { const HypeBanner = ({ className, platform }: BannerProps) => {
const { address } = useAccount(); const { address } = useAccount();
React.useEffect(() => { React.useEffect(() => {
...@@ -19,18 +21,46 @@ const HypeBanner = ({ className }: { className?: string }) => { ...@@ -19,18 +21,46 @@ const HypeBanner = ({ className }: { className?: string }) => {
} }
}, [ address ]); }, [ address ]);
const banner = (() => {
switch (platform) {
case 'desktop': {
return (
<Flex className={ className } w="728px" h="90px">
<Banner placement={ DESKTOP_BANNER_SLUG }/>
</Flex>
);
}
case 'mobile': {
return (
<Flex className={ className } w="320px" h="50px">
<Banner placement={ MOBILE_BANNER_SLUG }/>
</Flex>
);
}
default: {
return (
<>
<Flex className={ className } w="728px" h="90px" display={{ base: 'none', lg: 'flex' }}>
<Banner placement={ DESKTOP_BANNER_SLUG }/>
</Flex>
<Flex className={ className } w="320px" h="50px" display={{ base: 'flex', lg: 'none' }}>
<Banner placement={ MOBILE_BANNER_SLUG }/>
</Flex>
</>
);
}
}
})();
return ( return (
<> <>
<Script <Script
id="hypelab" id="hypelab"
strategy="afterInteractive" strategy="afterInteractive"
>{ hypeInit }</Script> >
<Flex className={ className } h="90px" display={{ base: 'none', lg: 'flex' }}> { hypeInit }
<Banner placement={ DESKTOP_BANNER_SLUG }/> </Script>
</Flex> { banner }
<Flex className={ className } h="50px" display={{ base: 'flex', lg: 'none' }}>
<Banner placement={ MOBILE_BANNER_SLUG }/>
</Flex>
</> </>
); );
}; };
......
...@@ -2,9 +2,35 @@ import { Flex, chakra } from '@chakra-ui/react'; ...@@ -2,9 +2,35 @@ import { Flex, chakra } from '@chakra-ui/react';
import { SliseAd } from '@slise/embed-react'; import { SliseAd } from '@slise/embed-react';
import React from 'react'; import React from 'react';
import type { BannerProps } from './types';
import config from 'configs/app'; import config from 'configs/app';
const SliseBanner = ({ className }: { className?: string }) => { const SliseBanner = ({ className, platform }: BannerProps) => {
if (platform === 'desktop') {
return (
<Flex className={ className } h="90px">
<SliseAd
slotId={ config.chain.name || '' }
pub="pub-10"
format="728x90"
style={{ width: '728px', height: '90px' }}/>
</Flex>
);
}
if (platform === 'mobile') {
return (
<Flex className={ className } h="90px">
<SliseAd
slotId={ config.chain.name || '' }
pub="pub-10"
format="270x90"
style={{ width: '270px', height: '90px' }}/>
</Flex>
);
}
return ( return (
<> <>
......
/* eslint-disable max-len */ /* eslint-disable max-len */
import type { BannerPlatform } from './types';
import config from 'configs/app'; import config from 'configs/app';
export const ADBUTLER_ACCOUNT = 182226; export const ADBUTLER_ACCOUNT = 182226;
export const connectAdbutler = `if (!window.AdButler){(function(){var s = document.createElement("script"); s.async = true; s.type = "text/javascript";s.src = 'https://servedbyadbutler.com/app.js';var n = document.getElementsByTagName("script")[0]; n.parentNode.insertBefore(s, n);}());}`; export const connectAdbutler = `if (!window.AdButler){(function(){var s = document.createElement("script"); s.async = true; s.type = "text/javascript";s.src = 'https://servedbyadbutler.com/app.js';var n = document.getElementsByTagName("script")[0]; n.parentNode.insertBefore(s, n);}());}`;
export const placeAd = (() => { export const placeAd = ((platform: BannerPlatform | undefined) => {
const feature = config.features.adsBanner; const feature = config.features.adsBanner;
if (!('adButler' in feature)) { if (!('adButler' in feature)) {
return; return;
} }
if (platform === 'mobile') {
return `
var AdButler = AdButler || {}; AdButler.ads = AdButler.ads || [];
var abkw = window.abkw || '';
var plc${ feature.adButler.config.mobile.id } = window.plc${ feature.adButler.config.mobile.id } || 0;
document.getElementById('ad-banner').innerHTML = '<'+'div id="placement_${ feature.adButler.config.mobile.id }_'+plc${ feature.adButler.config.mobile.id }+'"></'+'div>';
AdButler.ads.push({handler: function(opt){ AdButler.register(${ ADBUTLER_ACCOUNT }, ${ feature.adButler.config.mobile.id }, [${ feature.adButler.config.mobile.width },${ feature.adButler.config.mobile.height }], 'placement_${ feature.adButler.config.mobile.id }_'+opt.place, opt); }, opt: { place: plc${ feature.adButler.config.mobile.id }++, keywords: abkw, domain: 'servedbyadbutler.com', click:'CLICK_MACRO_PLACEHOLDER' }});
`;
}
return ` return `
var AdButler = AdButler || {}; AdButler.ads = AdButler.ads || []; var AdButler = AdButler || {}; AdButler.ads = AdButler.ads || [];
var abkw = window.abkw || ''; var abkw = window.abkw || '';
...@@ -26,4 +38,4 @@ export const placeAd = (() => { ...@@ -26,4 +38,4 @@ export const placeAd = (() => {
AdButler.ads.push({handler: function(opt){ AdButler.register(${ ADBUTLER_ACCOUNT }, ${ feature.adButler.config.desktop.id }, [${ feature.adButler.config.desktop.width },${ feature.adButler.config.desktop.height }], 'placement_${ feature.adButler.config.desktop.id }_'+opt.place, opt); }, opt: { place: plc${ feature.adButler.config.desktop.id }++, keywords: abkw, domain: 'servedbyadbutler.com', click:'CLICK_MACRO_PLACEHOLDER' }}); AdButler.ads.push({handler: function(opt){ AdButler.register(${ ADBUTLER_ACCOUNT }, ${ feature.adButler.config.desktop.id }, [${ feature.adButler.config.desktop.width },${ feature.adButler.config.desktop.height }], 'placement_${ feature.adButler.config.desktop.id }_'+opt.place, opt); }, opt: { place: plc${ feature.adButler.config.desktop.id }++, keywords: abkw, domain: 'servedbyadbutler.com', click:'CLICK_MACRO_PLACEHOLDER' }});
} }
`; `;
})(); });
export type BannerPlatform = 'mobile' | 'desktop';
export interface BannerProps {
className?: string;
platform?: BannerPlatform;
}
...@@ -5,20 +5,23 @@ import React from 'react'; ...@@ -5,20 +5,23 @@ import React from 'react';
import type { Route } from 'nextjs-routes'; import type { Route } from 'nextjs-routes';
import Hint from 'ui/shared/Hint'; import Hint from 'ui/shared/Hint';
import IconSvg, { type IconName } from 'ui/shared/IconSvg';
import TruncatedValue from 'ui/shared/TruncatedValue'; import TruncatedValue from 'ui/shared/TruncatedValue';
type Props = { type Props = {
className?: string;
label: string; label: string;
value: string; value: string | React.ReactNode;
valuePrefix?: string; valuePrefix?: string;
valuePostfix?: string; valuePostfix?: string;
hint?: string; hint?: string | React.ReactNode;
isLoading?: boolean; isLoading?: boolean;
diff?: string | number; diff?: string | number;
diffFormatted?: string; diffFormatted?: string;
diffPeriod?: '24h'; diffPeriod?: '24h';
period?: '1h' | '24h'; period?: '1h' | '24h';
href?: Route; href?: Route;
icon?: IconName;
} }
const Container = ({ href, children }: { href?: Route; children: JSX.Element }) => { const Container = ({ href, children }: { href?: Route; children: JSX.Element }) => {
...@@ -33,31 +36,57 @@ const Container = ({ href, children }: { href?: Route; children: JSX.Element }) ...@@ -33,31 +36,57 @@ const Container = ({ href, children }: { href?: Route; children: JSX.Element })
return children; return children;
}; };
const StatsWidget = ({ label, value, valuePrefix, valuePostfix, isLoading, hint, diff, diffPeriod = '24h', diffFormatted, period, href }: Props) => { const StatsWidget = ({
const bgColor = useColorModeValue('blue.50', 'whiteAlpha.100'); className,
icon,
label,
value,
valuePrefix,
valuePostfix,
isLoading,
hint,
diff,
diffPeriod = '24h',
diffFormatted,
period,
href,
}: Props) => {
const bgColor = useColorModeValue('gray.50', 'whiteAlpha.100');
const skeletonBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const skeletonBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const hintColor = useColorModeValue('gray.600', 'gray.400'); const hintColor = useColorModeValue('gray.600', 'gray.400');
return ( return (
<Container href={ !isLoading ? href : undefined }> <Container href={ !isLoading ? href : undefined }>
<Flex <Flex
alignItems="flex-start" className={ className }
alignItems="center"
bgColor={ isLoading ? skeletonBgColor : bgColor } bgColor={ isLoading ? skeletonBgColor : bgColor }
px={ 3 } p={ 3 }
py={{ base: 2, lg: 3 }} borderRadius="base"
borderRadius="md"
justifyContent="space-between" justifyContent="space-between"
columnGap={ 3 } columnGap={ 2 }
{ ...(href && !isLoading ? { { ...(href && !isLoading ? {
as: 'a', as: 'a',
href, href,
} : {}) } } : {}) }
> >
{ icon && (
<IconSvg
name={ icon }
p={ 2 }
boxSize="40px"
isLoading={ isLoading }
borderRadius="base"
display={{ base: 'none', lg: 'block' }}
flexShrink={ 0 }
/>
) }
<Box w="100%"> <Box w="100%">
<Skeleton <Skeleton
isLoaded={ !isLoading } isLoaded={ !isLoading }
color="text_secondary" color="text_secondary"
fontSize="xs" fontSize="xs"
lineHeight="16px"
w="fit-content" w="fit-content"
> >
<span>{ label }</span> <span>{ label }</span>
...@@ -66,30 +95,36 @@ const StatsWidget = ({ label, value, valuePrefix, valuePostfix, isLoading, hint, ...@@ -66,30 +95,36 @@ const StatsWidget = ({ label, value, valuePrefix, valuePostfix, isLoading, hint,
isLoaded={ !isLoading } isLoaded={ !isLoading }
display="flex" display="flex"
alignItems="baseline" alignItems="baseline"
mt={ 1 } fontWeight={ 500 }
fontSize="lg"
lineHeight={ 6 }
> >
{ valuePrefix && <chakra.span fontWeight={ 500 } fontSize="lg" lineHeight={ 6 } whiteSpace="pre">{ valuePrefix }</chakra.span> } { valuePrefix && <chakra.span whiteSpace="pre">{ valuePrefix }</chakra.span> }
<TruncatedValue isLoading={ isLoading } fontWeight={ 500 } fontSize="lg" lineHeight={ 6 } value={ value }/> { typeof value === 'string' ? (
{ valuePostfix && <chakra.span fontWeight={ 500 } fontSize="lg" lineHeight={ 6 } whiteSpace="pre">{ valuePostfix }</chakra.span> } <TruncatedValue isLoading={ isLoading } value={ value }/>
) : (
value
) }
{ valuePostfix && <chakra.span whiteSpace="pre">{ valuePostfix }</chakra.span> }
{ diff && Number(diff) > 0 && ( { diff && Number(diff) > 0 && (
<> <>
<Text fontWeight={ 500 } ml={ 2 } mr={ 1 } fontSize="lg" lineHeight={ 6 } color="green.500"> <Text ml={ 2 } mr={ 1 } color="green.500">
+{ diffFormatted || Number(diff).toLocaleString() } +{ diffFormatted || Number(diff).toLocaleString() }
</Text> </Text>
<Text variant="secondary" fontSize="sm">({ diffPeriod })</Text> <Text variant="secondary" fontSize="sm">({ diffPeriod })</Text>
</> </>
) } ) }
{ period && <Text variant="secondary" fontSize="xs" ml={ 1 }>({ period })</Text> } { period && <Text variant="secondary" fontSize="xs" fontWeight={ 400 } ml={ 1 }>({ period })</Text> }
</Skeleton> </Skeleton>
</Box> </Box>
{ hint && ( { typeof hint === 'string' ? (
<Skeleton isLoaded={ !isLoading } alignSelf="center" borderRadius="base"> <Skeleton isLoaded={ !isLoading } alignSelf="center" borderRadius="base">
<Hint label={ hint } boxSize={ 6 } color={ hintColor }/> <Hint label={ hint } boxSize={ 6 } color={ hintColor }/>
</Skeleton> </Skeleton>
) } ) : hint }
</Flex> </Flex>
</Container> </Container>
); );
}; };
export default StatsWidget; export default chakra(StatsWidget);
This diff is collapsed.
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