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
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_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_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
......
......@@ -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_TRANSACTION_INTERPRETATION_PROVIDER: blockscout
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS: true
NEXT_PUBLIC_AD_BANNER_PROVIDER: getit
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_AD_BANNER_PROVIDER: slise
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED: true
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED: true
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
### Banner ads
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 |
| --- | --- | --- | --- | --- | --- | --- |
......
<svg viewBox="0 0 22 22" 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"/>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<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 viewBox="0 0 30 30" 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"/>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<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 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 xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 31 30">
<g fill="currentColor" clip-path="url(#wallet_svg__a)">
<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="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 viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<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="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"/>
</svg>
......@@ -17,7 +17,7 @@ const TEST_URLS: Record<AdBannerProviders, string> = {
adbutler: 'https://servedbyadbutler.com/app.js',
hype: 'https://api.hypelab.com/v1/scripts/hp-sdk.js',
// I don't have an url for getit to test
getit: DEFAULT_URL,
// getit: DEFAULT_URL,
none: DEFAULT_URL,
};
......
......@@ -39,7 +39,8 @@ export function ad(): CspDev.DirectiveDescriptor {
// adbutler
'servedbyadbutler.com',
`'sha256-${ Base64.stringify(sha256(connectAdbutler)) }'`,
`'sha256-${ Base64.stringify(sha256(placeAd ?? '')) }'`,
`'sha256-${ Base64.stringify(sha256(placeAd(undefined) ?? '')) }'`,
`'sha256-${ Base64.stringify(sha256(placeAd('mobile') ?? '')) }'`,
// slise
'*.slise.xyz',
......
......@@ -17,7 +17,6 @@ const sizes = {
xl: defineStyle({
fontSize: '40px',
lineHeight: '48px',
letterSpacing: '-1px',
}),
lg: defineStyle({
fontSize: '32px',
......
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 const SUPPORTED_AD_BANNER_ADDITIONAL_PROVIDERS = [ 'adbutler' ] as const;
......
......@@ -53,7 +53,7 @@ const TokenBalances = () => {
name="Net Worth"
value={ addressData?.exchange_rate ? `${ prefix }$${ totalUsd.toFormat(2) }` : 'N/A' }
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
name={ `${ currencyUnits.ether } Balance` }
......
......@@ -3,7 +3,6 @@ import React from 'react';
import * as statsMock from 'mocks/stats/index';
import { test, expect } from 'playwright/lib';
import * as pwConfig from 'playwright/utils/config';
import Stats from './Stats';
......@@ -18,14 +17,6 @@ test.describe('all items', () => {
test('+@mobile +@dark-mode', async() => {
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 }) => {
......
......@@ -2,8 +2,6 @@ import { Grid } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery';
import { WEI } from 'lib/consts';
......@@ -11,8 +9,7 @@ import { HOMEPAGE_STATS } from 'stubs/stats';
import GasInfoTooltip from 'ui/shared/gas/GasInfoTooltip';
import GasPrice from 'ui/shared/gas/GasPrice';
import IconSvg from 'ui/shared/IconSvg';
import StatsItem from './StatsItem';
import StatsWidget from 'ui/shared/stats/StatsWidget';
const hasAvgBlockTime = config.UI.homepage.showAvgBlockTime;
const rollupFeature = config.features.rollup;
......@@ -58,7 +55,7 @@ const Stats = () => {
let content;
const lastItemTouchStyle = { gridColumn: { base: 'span 2', lg: 'unset' } };
const lastItemStyle = { gridColumn: 'span 2' };
let itemsCount = 5;
!hasGasTracker && itemsCount--;
......@@ -75,12 +72,9 @@ const Stats = () => {
isLoading={ isLoading }
name="info"
boxSize={ 5 }
display="block"
flexShrink={ 0 }
cursor="pointer"
_hover={{ color: 'link_hovered' }}
position="absolute"
top={{ base: 'calc(50% - 12px)', lg: '10px', xl: 'calc(50% - 12px)' }}
right="10px"
/>
</GasInfoTooltip>
) : null;
......@@ -88,80 +82,80 @@ const Stats = () => {
content = (
<>
{ rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && (
<StatsItem
icon="txn_batches"
title="Latest batch"
<StatsWidget
icon="txn_batches_slim"
label="Latest batch"
value={ (zkEvmLatestBatchQuery.data || 0).toLocaleString() }
url={ route({ pathname: '/batches' }) }
href={{ pathname: '/batches' }}
isLoading={ isLoading }
/>
) }
{ rollupFeature.isEnabled && rollupFeature.type === 'zkSync' && (
<StatsItem
icon="txn_batches"
title="Latest batch"
<StatsWidget
icon="txn_batches_slim"
label="Latest batch"
value={ (zkSyncLatestBatchQuery.data || 0).toLocaleString() }
url={ route({ pathname: '/batches' }) }
href={{ pathname: '/batches' }}
isLoading={ isLoading }
/>
) }
{ !(rollupFeature.isEnabled && (rollupFeature.type === 'zkEvm' || rollupFeature.type === 'zkSync')) && (
<StatsItem
icon="block"
title="Total blocks"
<StatsWidget
icon="block_slim"
label="Total blocks"
value={ Number(data.total_blocks).toLocaleString() }
url={ route({ pathname: '/blocks' }) }
href={{ pathname: '/blocks' }}
isLoading={ isLoading }
/>
) }
{ hasAvgBlockTime && (
<StatsItem
icon="clock-light"
title="Average block time"
<StatsWidget
icon="clock"
label="Average block time"
value={ `${ (data.average_block_time / 1000).toFixed(1) }s` }
isLoading={ isLoading }
/>
) }
<StatsItem
icon="transactions"
title="Total transactions"
<StatsWidget
icon="transactions_slim"
label="Total transactions"
value={ Number(data.total_transactions).toLocaleString() }
url={ route({ pathname: '/txs' }) }
href={{ pathname: '/txs' }}
isLoading={ isLoading }
/>
{ rollupFeature.isEnabled && data.last_output_root_size && (
<StatsItem
icon="txn_batches"
title="Latest L1 state batch"
<StatsWidget
icon="txn_batches_slim"
label="Latest L1 state batch"
value={ data.last_output_root_size }
url={ route({ pathname: '/batches' }) }
href={{ pathname: '/batches' }}
isLoading={ isLoading }
/>
) }
<StatsItem
<StatsWidget
icon="wallet"
title="Wallet addresses"
label="Wallet addresses"
value={ Number(data.total_addresses).toLocaleString() }
_last={ isOdd ? lastItemTouchStyle : undefined }
isLoading={ isLoading }
_last={ isOdd ? lastItemStyle : undefined }
/>
{ hasGasTracker && data.gas_prices && (
<StatsItem
<StatsWidget
icon="gas"
title="Gas tracker"
label="Gas tracker"
value={ data.gas_prices.average ? <GasPrice data={ data.gas_prices.average }/> : 'N/A' }
_last={ isOdd ? lastItemTouchStyle : undefined }
tooltip={ gasInfoTooltip }
hint={ gasInfoTooltip }
isLoading={ isLoading }
_last={ isOdd ? lastItemStyle : undefined }
/>
) }
{ data.rootstock_locked_btc && (
<StatsItem
<StatsWidget
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` }
_last={ isOdd ? lastItemTouchStyle : undefined }
isLoading={ isLoading }
_last={ isOdd ? lastItemStyle : undefined }
/>
) }
</>
......@@ -170,10 +164,10 @@ const Stats = () => {
return (
<Grid
gridTemplateColumns={{ lg: `repeat(${ itemsCount }, 1fr)`, base: '1fr 1fr' }}
gridTemplateRows={{ lg: 'none', base: undefined }}
gridTemplateColumns="1fr 1fr"
gridGap={{ base: 1, lg: 2 }}
marginTop={ 3 }
flexBasis="50%"
flexGrow={ 1 }
>
{ content }
</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 React from 'react';
......@@ -15,21 +15,25 @@ const ChainIndicatorChartContainer = ({ data, isError, isPending }: Props) => {
const content = (() => {
if (isPending) {
return <ContentLoader mt="auto"/>;
return <ContentLoader mt="auto" fontSize="xs"/>;
}
if (isError) {
return <DataFetchAlert/>;
return <DataFetchAlert fontSize="xs" p={ 3 }/>;
}
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);
......@@ -6,8 +6,6 @@ import type { HomeStats } from 'types/api/stats';
import type { ChainIndicatorId } from 'types/homepage';
import type { ResourceError } from 'lib/api/resources';
import useIsMobile from 'lib/hooks/useIsMobile';
import IconSvg from 'ui/shared/IconSvg';
interface Props {
id: ChainIndicatorId;
......@@ -21,42 +19,27 @@ interface Props {
}
const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onClick, stats }: Props) => {
const isMobile = useIsMobile();
const activeBgColorDesktop = useColorModeValue('white', 'gray.900');
const activeBgColorMobile = useColorModeValue('white', 'black');
const activeBgColor = isMobile ? activeBgColorMobile : activeBgColorDesktop;
const activeColor = useColorModeValue('gray.500', 'gray.400');
const activeBgColor = useColorModeValue('white', 'black');
const handleClick = React.useCallback(() => {
onClick(id);
}, [ id, onClick ]);
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) {
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 = (() => {
if (isMobile || !valueDiff) {
if (!valueDiff) {
return null;
}
const diff = valueDiff(stats.data);
......@@ -67,8 +50,8 @@ const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onC
const diffColor = diff >= 0 ? 'green.500' : 'red.500';
return (
<Skeleton isLoaded={ !stats.isPlaceholderData } ml={ 3 } display="flex" alignItems="center" color={ diffColor }>
<IconSvg name="arrows/up-head" boxSize={ 5 } mr={ 1 } transform={ diff < 0 ? 'rotate(180deg)' : 'rotate(0)' }/>
<Skeleton isLoaded={ !stats.isPlaceholderData } ml={ 1 } display="flex" alignItems="center" color={ diffColor }>
<span>{ diff >= 0 ? '+' : '-' }</span>
<Text color={ diffColor } fontWeight={ 600 }>{ diff }%</Text>
</Skeleton>
);
......@@ -77,25 +60,28 @@ const ChainIndicatorItem = ({ id, title, value, valueDiff, icon, isSelected, onC
return (
<Flex
alignItems="center"
columnGap={ 3 }
px={ 4 }
py={ 2 }
columnGap={ 2 }
flexGrow={{ base: 0, lg: 1 }}
px={{ base: '6px', lg: 2 }}
py="6px"
as="li"
borderRadius="md"
borderRadius="base"
cursor="pointer"
color={ isSelected ? activeColor : 'link' }
bgColor={ isSelected ? activeBgColor : undefined }
onClick={ handleClick }
bgColor={ isSelected ? activeBgColor : 'inherit' }
boxShadow={ isSelected ? 'lg' : 'none' }
zIndex={ isSelected ? 1 : 'initial' }
fontSize="xs"
fontWeight={ 500 }
_hover={{
activeBgColor,
bgColor: activeBgColor,
color: isSelected ? activeColor : 'link_hovered',
zIndex: 1,
}}
>
{ icon }
<Box>
<Text fontFamily="heading" fontWeight={ 500 }>{ title }</Text>
<Flex alignItems="center">
<Box display={{ base: 'none', lg: 'block' }}>
<span>{ title }</span>
<Flex alignItems="center" color="text">
{ valueContent }
{ valueDiffContent }
</Flex>
......
......@@ -22,7 +22,7 @@ test.describe('daily txs chart', () => {
await mockApiResponse('stats_charts_txs', dailyTxsMock.base);
await mockAssetResponse(statsMock.withSecondaryCoin.coin_image as string, './playwright/mocks/image_svg.svg');
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() => {
......
......@@ -38,10 +38,7 @@ const ChainIndicators = () => {
},
});
const bgColorDesktop = useColorModeValue('white', 'gray.900');
const bgColorMobile = useColorModeValue('white', 'black');
const listBgColorDesktop = useColorModeValue('gray.50', 'black');
const listBgColorMobile = useColorModeValue('gray.50', 'gray.900');
const bgColor = useColorModeValue('gray.50', 'whiteAlpha.100');
if (indicators.length === 0) {
return null;
......@@ -49,15 +46,15 @@ const ChainIndicators = () => {
const valueTitle = (() => {
if (statsQueryResult.isPlaceholderData) {
return <Skeleton h="48px" w="215px" mt={ 3 } mb={ 4 }/>;
return <Skeleton h="36px" w="215px"/>;
}
if (!statsQueryResult.data) {
return <Text mt={ 3 } mb={ 4 }>There is no data</Text>;
return <Text fontSize="xs">There is no data</Text>;
}
return (
<Text fontWeight={ 600 } fontFamily="heading" fontSize="48px" lineHeight="48px" mt={ 3 }>
<Text fontWeight={ 700 } fontSize="30px" lineHeight="36px">
{ indicator?.value(statsQueryResult.data) }
</Text>
);
......@@ -85,23 +82,22 @@ const ChainIndicators = () => {
return (
<Flex
p={{ base: 0, lg: 8 }}
borderRadius={{ base: 'none', lg: 'lg' }}
boxShadow={{ base: 'none', lg: 'xl' }}
bgColor={{ base: bgColorMobile, lg: bgColorDesktop }}
columnGap={ 6 }
px={{ base: 3, lg: 4 }}
py={ 3 }
borderRadius="base"
bgColor={ bgColor }
columnGap={{ base: 3, lg: 4 }}
rowGap={ 0 }
flexDir={{ base: 'column', lg: 'row' }}
w="100%"
flexBasis="50%"
flexGrow={ 1 }
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">
<Text fontWeight={ 500 } fontFamily="heading" fontSize="lg">{ indicator?.title }</Text>
<Text fontWeight={ 500 }>{ indicator?.title }</Text>
{ indicator?.hint && <Hint label={ indicator.hint } ml={ 1 }/> }
</Flex>
<Flex mb={ 4 } alignItems="end">
<Flex mb={{ base: 0, lg: 2 }} mt={ 1 } alignItems="end">
{ valueTitle }
{ valueDiff }
</Flex>
......@@ -112,11 +108,9 @@ const ChainIndicators = () => {
flexShrink={ 0 }
flexDir="column"
as="ul"
p={ 3 }
borderRadius="lg"
bgColor={{ base: listBgColorMobile, lg: listBgColorDesktop }}
rowGap={ 3 }
order={{ base: 1, lg: 2 }}
rowGap="6px"
m={{ base: 'auto 0', lg: 0 }}
>
{ indicators.map((indicator) => (
<ChainIndicatorItem
......
......@@ -7,6 +7,7 @@ import config from 'configs/app';
import { sortByDateDesc } from 'ui/shared/chart/utils/sorts';
import IconSvg from 'ui/shared/IconSvg';
import NativeTokenIcon from 'ui/shared/NativeTokenIcon';
import TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
const nonNullTailReducer = (result: Array<TimeChartItemRaw>, item: TimeChartItemRaw) => {
if (item.value === null && result.length === 0) {
......@@ -70,7 +71,7 @@ const secondaryCoinPriceIndicator: TChainIndicator<'stats_charts_secondary_coin_
'$N/A' :
'$' + Number(stats.secondary_coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
valueDiff: () => null,
icon: <NativeTokenIcon boxSize={ 6 }/>,
icon: <TokenLogoPlaceholder boxSize={ 6 }/>,
hint: `${ config.chain.secondaryCoin.symbol } token daily price in USD.`,
api: {
resourceName: 'stats_charts_secondary_coin_price',
......
......@@ -2,6 +2,7 @@ import { Box, Flex, Heading } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import ChainIndicators from 'ui/home/indicators/ChainIndicators';
import LatestBlocks from 'ui/home/LatestBlocks';
import LatestZkEvmL2Batches from 'ui/home/LatestZkEvmL2Batches';
......@@ -15,44 +16,51 @@ import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop';
const rollupFeature = config.features.rollup;
const Home = () => {
const isMobile = useIsMobile();
return (
<Box as="main">
<Box
<Flex
w="100%"
background={ config.UI.homepage.plate.background }
borderRadius={{ base: 'md', lg: 'xl' }}
px={{ base: 4, lg: 10 }}
py={{ base: 3, lg: 8 }}
minW={{ base: 'unset', lg: '900px' }}
borderRadius="md"
p={{ base: 4, lg: 8 }}
columnGap={ 8 }
alignItems="center"
data-label="hero plate"
>
<Flex mb={{ base: 2, lg: 6 }} justifyContent="space-between" alignItems="center">
<Heading
as="h1"
fontSize={{ base: '18px', lg: '40px' }}
lineHeight={{ base: '24px', lg: '48px' }}
fontWeight={ 600 }
color={ config.UI.homepage.plate.textColor }
>
{
config.meta.seo.enhancedDataEnabled ?
`${ config.chain.name } blockchain explorer` :
`${ config.chain.name } explorer`
}
</Heading>
{ config.UI.navigation.layout === 'vertical' && (
<Box display={{ base: 'none', lg: 'flex' }}>
{ config.features.account.isEnabled && <ProfileMenuDesktop isHomePage/> }
{ config.features.blockchainInteraction.isEnabled && <WalletMenuDesktop isHomePage/> }
</Box>
) }
</Flex>
<SearchBar isHomepage/>
</Box>
<Stats/>
<ChainIndicators/>
<AdBanner mt={ 6 } mx="auto" display="flex" justifyContent="center"/>
<Flex mt={ 6 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 6 }>
<Box flexGrow={ 1 }>
<Flex mb={{ base: 2, lg: 3 }} justifyContent="space-between" alignItems="center" columnGap={ 2 }>
<Heading
as="h1"
fontSize={{ base: '18px', lg: '30px' }}
lineHeight={{ base: '24px', lg: '36px' }}
fontWeight={{ base: 500, lg: 700 }}
color={ config.UI.homepage.plate.textColor }
>
{
config.meta.seo.enhancedDataEnabled ?
`${ config.chain.name } blockchain explorer` :
`${ config.chain.name } explorer`
}
</Heading>
{ config.UI.navigation.layout === 'vertical' && (
<Box display={{ base: 'none', lg: 'flex' }}>
{ config.features.account.isEnabled && <ProfileMenuDesktop isHomePage/> }
{ config.features.blockchainInteraction.isEnabled && <WalletMenuDesktop isHomePage/> }
</Box>
) }
</Flex>
<SearchBar isHomepage/>
</Box>
{ !isMobile && <AdBanner platform="mobile" w="fit-content" flexShrink={ 0 } borderRadius="md" overflow="hidden"/> }
</Flex>
<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/> }
<Box flexGrow={ 1 }>
<Transactions/>
......
import { chakra } from '@chakra-ui/react';
import React from 'react';
import type { BannerPlatform } from './types';
import config from 'configs/app';
import { useAppContext } from 'lib/contexts/app';
import * as cookies from 'lib/cookies';
......@@ -9,7 +11,13 @@ import AdBannerContent from './AdBannerContent';
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 hasAdblockCookie = cookies.get(cookies.NAMES.ADBLOCK_DETECTED, useAppContext().cookies);
......@@ -23,6 +31,7 @@ const AdBanner = ({ className, isLoading }: { className?: string; isLoading?: bo
className={ className }
isLoading={ isLoading }
provider={ provider }
platform={ platform }
/>
);
};
......
import { chakra, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { BannerPlatform } from './types';
import type { AdBannerProviders } from 'types/client/adProviders';
import config from 'configs/app';
import AdbutlerBanner from './AdbutlerBanner';
import CoinzillaBanner from './CoinzillaBanner';
import GetitBanner from './GetitBanner';
// import GetitBanner from './GetitBanner';
import HypeBanner from './HypeBanner';
import SliseBanner from './SliseBanner';
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 = (() => {
switch (provider) {
case 'adbutler':
return <AdbutlerBanner/>;
return <AdbutlerBanner platform={ platform }/>;
case 'coinzilla':
return <CoinzillaBanner/>;
case 'getit':
return <GetitBanner/>;
return <CoinzillaBanner platform={ platform }/>;
// case 'getit':
// return <GetitBanner platform={ platform }/>;
case 'hype':
return <HypeBanner/>;
return <HypeBanner platform={ platform }/>;
case 'slise':
return <SliseBanner/>;
return <SliseBanner platform={ platform }/>;
}
})();
......
......@@ -3,6 +3,8 @@ import { useRouter } from 'next/navigation';
import Script from 'next/script';
import React from 'react';
import type { BannerProps } from './types';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import isBrowser from 'lib/isBrowser';
......@@ -10,9 +12,10 @@ import { connectAdbutler, placeAd, ADBUTLER_ACCOUNT } from 'ui/shared/ad/adbutle
const feature = config.features.adsBanner;
const AdbutlerBanner = ({ className }: { className?: string }) => {
const AdbutlerBanner = ({ className, platform }: BannerProps) => {
const router = useRouter();
const isMobile = useIsMobile();
const isMobileViewport = useIsMobile();
const isMobile = platform === 'mobile' || isMobileViewport;
React.useEffect(() => {
if (!('adButler' in feature)) {
......@@ -24,10 +27,10 @@ const AdbutlerBanner = ({ className }: { className?: string }) => {
if (!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
// @ts-ignore:
let plc = window[`plc${ feature.adButler.config.mobile.id }`] || 0;
const adButlerConfig = isMobile ? feature.adButler.config.mobile : feature.adButler.config.desktop;
let plc = window[`plc${ adButlerConfig.id }`] || 0;
const banner = document.getElementById('ad-banner');
if (banner) {
banner.innerHTML = '<' + 'div id="placement_' + adButlerConfig?.id + '_' + plc + '"></' + 'div>';
......@@ -46,10 +49,34 @@ const AdbutlerBanner = ({ className }: { className?: string }) => {
}
}, [ 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 (
<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-2">{ placeAd }</Script>
<Script strategy="lazyOnload" id="ad-butler-2">{ placeAd(platform) }</Script>
<div id="ad-banner"></div>
</Flex>
);
......
......@@ -2,25 +2,44 @@ import { Flex, chakra } from '@chakra-ui/react';
import Script from 'next/script';
import React from 'react';
import type { BannerProps } from './types';
import isBrowser from 'lib/isBrowser';
const CoinzillaBanner = ({ className }: { className?: string }) => {
const CoinzillaBanner = ({ className, platform }: BannerProps) => {
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(() => {
if (isInBrowser) {
window.coinzilla_display = window.coinzilla_display || [];
const cDisplayPreferences = {
zone: '26660bf627543e46851',
width: '728',
height: '90',
width: width ? String(width) : '728',
height: height ? String(height) : '90',
};
window.coinzilla_display.push(cDisplayPreferences);
}
}, [ isInBrowser ]);
}, [ height, isInBrowser, width ]);
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"/>
<div className="coinzilla" data-zone="C-26660bf627543e46851"></div>
</Flex>
......
......@@ -2,6 +2,8 @@ import { Flex, chakra } from '@chakra-ui/react';
import dynamic from 'next/dynamic';
import React from 'react';
import type { BannerProps } from './types';
import useIsMobile from 'lib/hooks/useIsMobile';
import useAccount from 'lib/web3/useAccount';
......@@ -9,17 +11,17 @@ const GetitAdPlugin = dynamic(() => import('getit-sdk').then(module => module.Ge
const GETIT_API_KEY = 'ZmGXVvwYUAW4yXL8RzWQHNKmpSyQmt3TDXsXUxqFqXPdoaiSSFyca3BOyunDcWdyOwTkX3UVVQel28qbjoOoWPxYVpPdNzbUNkAHyFyJX7Lk9TVcPDZKTQmwHlSMzO3a';
const GetitBanner = ({ className }: { className?: string }) => {
const GetitBanner = ({ className, platform }: BannerProps) => {
const isMobile = Boolean(useIsMobile());
const { address } = useAccount();
return (
<Flex className={ className } h="90px">
<Flex className={ className } h="90px" w={{ base: '270px', lg: platform === 'mobile' ? '270px' : undefined }}>
<GetitAdPlugin
key={ isMobile.toString() }
apiKey={ GETIT_API_KEY }
walletConnected={ address ? address : '' }
isMobile={ isMobile }
isMobile={ platform === 'mobile' || isMobile }
slotId="0"
/>
</Flex>
......
......@@ -3,6 +3,8 @@ import { Banner, setWalletAddresses } from '@hypelab/sdk-react';
import Script from 'next/script';
import React from 'react';
import type { BannerProps } from './types';
import useAccount from 'lib/web3/useAccount';
import { hypeInit } from './hypeBannerScript';
......@@ -10,7 +12,7 @@ import { hypeInit } from './hypeBannerScript';
const DESKTOP_BANNER_SLUG = 'b1559fc3e7';
const MOBILE_BANNER_SLUG = '668ed80a9e';
const HypeBanner = ({ className }: { className?: string }) => {
const HypeBanner = ({ className, platform }: BannerProps) => {
const { address } = useAccount();
React.useEffect(() => {
......@@ -19,18 +21,46 @@ const HypeBanner = ({ className }: { className?: string }) => {
}
}, [ 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 (
<>
<Script
id="hypelab"
strategy="afterInteractive"
>{ hypeInit }</Script>
<Flex className={ className } h="90px" display={{ base: 'none', lg: 'flex' }}>
<Banner placement={ DESKTOP_BANNER_SLUG }/>
</Flex>
<Flex className={ className } h="50px" display={{ base: 'flex', lg: 'none' }}>
<Banner placement={ MOBILE_BANNER_SLUG }/>
</Flex>
>
{ hypeInit }
</Script>
{ banner }
</>
);
};
......
......@@ -2,9 +2,35 @@ import { Flex, chakra } from '@chakra-ui/react';
import { SliseAd } from '@slise/embed-react';
import React from 'react';
import type { BannerProps } from './types';
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 (
<>
......
/* eslint-disable max-len */
import type { BannerPlatform } from './types';
import config from 'configs/app';
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 placeAd = (() => {
export const placeAd = ((platform: BannerPlatform | undefined) => {
const feature = config.features.adsBanner;
if (!('adButler' in feature)) {
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 `
var AdButler = AdButler || {}; AdButler.ads = AdButler.ads || [];
var abkw = window.abkw || '';
......@@ -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' }});
}
`;
})();
});
export type BannerPlatform = 'mobile' | 'desktop';
export interface BannerProps {
className?: string;
platform?: BannerPlatform;
}
......@@ -5,20 +5,23 @@ import React from 'react';
import type { Route } from 'nextjs-routes';
import Hint from 'ui/shared/Hint';
import IconSvg, { type IconName } from 'ui/shared/IconSvg';
import TruncatedValue from 'ui/shared/TruncatedValue';
type Props = {
className?: string;
label: string;
value: string;
value: string | React.ReactNode;
valuePrefix?: string;
valuePostfix?: string;
hint?: string;
hint?: string | React.ReactNode;
isLoading?: boolean;
diff?: string | number;
diffFormatted?: string;
diffPeriod?: '24h';
period?: '1h' | '24h';
href?: Route;
icon?: IconName;
}
const Container = ({ href, children }: { href?: Route; children: JSX.Element }) => {
......@@ -33,31 +36,57 @@ const Container = ({ href, children }: { href?: Route; children: JSX.Element })
return children;
};
const StatsWidget = ({ label, value, valuePrefix, valuePostfix, isLoading, hint, diff, diffPeriod = '24h', diffFormatted, period, href }: Props) => {
const bgColor = useColorModeValue('blue.50', 'whiteAlpha.100');
const StatsWidget = ({
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 hintColor = useColorModeValue('gray.600', 'gray.400');
return (
<Container href={ !isLoading ? href : undefined }>
<Flex
alignItems="flex-start"
className={ className }
alignItems="center"
bgColor={ isLoading ? skeletonBgColor : bgColor }
px={ 3 }
py={{ base: 2, lg: 3 }}
borderRadius="md"
p={ 3 }
borderRadius="base"
justifyContent="space-between"
columnGap={ 3 }
columnGap={ 2 }
{ ...(href && !isLoading ? {
as: 'a',
href,
} : {}) }
>
{ icon && (
<IconSvg
name={ icon }
p={ 2 }
boxSize="40px"
isLoading={ isLoading }
borderRadius="base"
display={{ base: 'none', lg: 'block' }}
flexShrink={ 0 }
/>
) }
<Box w="100%">
<Skeleton
isLoaded={ !isLoading }
color="text_secondary"
fontSize="xs"
lineHeight="16px"
w="fit-content"
>
<span>{ label }</span>
......@@ -66,30 +95,36 @@ const StatsWidget = ({ label, value, valuePrefix, valuePostfix, isLoading, hint,
isLoaded={ !isLoading }
display="flex"
alignItems="baseline"
mt={ 1 }
fontWeight={ 500 }
fontSize="lg"
lineHeight={ 6 }
>
{ valuePrefix && <chakra.span fontWeight={ 500 } fontSize="lg" lineHeight={ 6 } whiteSpace="pre">{ valuePrefix }</chakra.span> }
<TruncatedValue isLoading={ isLoading } fontWeight={ 500 } fontSize="lg" lineHeight={ 6 } value={ value }/>
{ valuePostfix && <chakra.span fontWeight={ 500 } fontSize="lg" lineHeight={ 6 } whiteSpace="pre">{ valuePostfix }</chakra.span> }
{ valuePrefix && <chakra.span whiteSpace="pre">{ valuePrefix }</chakra.span> }
{ typeof value === 'string' ? (
<TruncatedValue isLoading={ isLoading } value={ value }/>
) : (
value
) }
{ valuePostfix && <chakra.span whiteSpace="pre">{ valuePostfix }</chakra.span> }
{ 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() }
</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>
</Box>
{ hint && (
{ typeof hint === 'string' ? (
<Skeleton isLoaded={ !isLoading } alignSelf="center" borderRadius="base">
<Hint label={ hint } boxSize={ 6 } color={ hintColor }/>
</Skeleton>
) }
) : hint }
</Flex>
</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