Highlights banners on home page (#3175)
* basic implementation * fix loading state of image * tests * try to fix flaky tests
@@ -71,6 +71,7 @@ const UI = Object.freeze({
|
||||
charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_CHARTS')) || [],
|
||||
stats: homePageStats,
|
||||
heroBanner: parseEnvJson<HeroBannerConfig>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG')),
|
||||
highlights: getExternalAssetFilePath('NEXT_PUBLIC_HOMEPAGE_HIGHLIGHTS_CONFIG'),
|
||||
},
|
||||
views,
|
||||
indexingAlert: {
|
||||
|
||||
@@ -9,6 +9,8 @@ NEXT_PUBLIC_APP_PORT=3000
|
||||
NEXT_PUBLIC_APP_ENV=development
|
||||
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
|
||||
|
||||
NEXT_PUBLIC_HOMEPAGE_HIGHLIGHTS_CONFIG=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/homepage-highlights/test.json
|
||||
|
||||
# Instance ENVs
|
||||
NEXT_PUBLIC_AD_BANNER_ENABLE_SPECIFY=true
|
||||
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS=['talentprotocol', 'efp', 'webacy', 'deepdao', 'humanpassport', 'trustblock', 'smartmuv', 'blockscoutbadges', 'etherscore', 'gitpoap', 'drops', 'humanode']
|
||||
|
||||
@@ -27,6 +27,7 @@ ASSETS_ENVS=(
|
||||
"NEXT_PUBLIC_OG_IMAGE_URL"
|
||||
"NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS_CONFIG_URL"
|
||||
"NEXT_PUBLIC_ZETACHAIN_SERVICE_CHAINS_CONFIG_URL"
|
||||
"NEXT_PUBLIC_HOMEPAGE_HIGHLIGHTS_CONFIG"
|
||||
)
|
||||
|
||||
# Create the assets directory if it doesn't exist
|
||||
|
||||
@@ -48,6 +48,7 @@ async function validateEnvs(appEnvs: Record<string, string>) {
|
||||
'NEXT_PUBLIC_FOOTER_LINKS',
|
||||
'NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS_CONFIG_URL',
|
||||
'NEXT_PUBLIC_ZETACHAIN_SERVICE_CHAINS_CONFIG_URL',
|
||||
'NEXT_PUBLIC_HOMEPAGE_HIGHLIGHTS_CONFIG',
|
||||
];
|
||||
|
||||
for await (const envName of envsWithJsonConfig) {
|
||||
|
||||
@@ -164,6 +164,7 @@ const schema = yup
|
||||
.concat(featuresSchemas.beaconChainSchema)
|
||||
.concat(featuresSchemas.bridgedTokensSchema)
|
||||
.concat(featuresSchemas.defiDropdownSchema)
|
||||
.concat(featuresSchemas.highlightsConfigSchema)
|
||||
.concat(featuresSchemas.marketplaceSchema)
|
||||
.concat(featuresSchemas.megaEthSchema)
|
||||
.concat(featuresSchemas.rollupSchema)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { HighlightsBannerConfig } from "types/homepage";
|
||||
import { urlTest } from "../../utils";
|
||||
import * as yup from 'yup';
|
||||
|
||||
const highlightsBannerConfigSchema: yup.ObjectSchema<HighlightsBannerConfig> = yup.object({
|
||||
title: yup.string().required(),
|
||||
description: yup.string().required(),
|
||||
title_color: yup.array().max(2).of(yup.string()),
|
||||
description_color: yup.array().max(2).of(yup.string()),
|
||||
background: yup.array().max(2).of(yup.string()),
|
||||
side_img_url: yup.array().max(2).of(yup.string()),
|
||||
is_pinned: yup.boolean(),
|
||||
page_path: yup.string(),
|
||||
redirect_url: yup.string().test(urlTest),
|
||||
});
|
||||
|
||||
export const highlightsConfigSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
NEXT_PUBLIC_HOMEPAGE_HIGHLIGHTS_CONFIG: yup
|
||||
.array()
|
||||
.json()
|
||||
.of(highlightsBannerConfigSchema)
|
||||
.min(2)
|
||||
});
|
||||
@@ -4,6 +4,7 @@ export * from './apiDocs';
|
||||
export * from './beaconChain';
|
||||
export * from './bridgedToken';
|
||||
export * from './defiDropdown';
|
||||
export * from './highlights';
|
||||
export * from './marketplace';
|
||||
export * from './megaEth';
|
||||
export * from './rollup';
|
||||
|
||||
@@ -92,3 +92,4 @@ NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS=['widget-1', 'widget-2']
|
||||
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS_CONFIG_URL=https://example.com
|
||||
NEXT_PUBLIC_NAVIGATION_PROMO_BANNER_CONFIG={'img_url': 'https://example.com/promo.svg', 'text': 'Promo text', 'bg_color': {'light': 'rgb(250, 245, 255)', 'dark': 'rgb(68, 51, 122)'}, 'text_color': {'light': 'rgb(107, 70, 193)', 'dark': 'rgb(233, 216, 253)'}, 'link_url': 'https://example.com'}
|
||||
NEXT_PUBLIC_FLASHBLOCKS_SOCKET_URL=wss://example.com/ws
|
||||
NEXT_PUBLIC_HOMEPAGE_HIGHLIGHTS_CONFIG=https://example.com
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"title": "Duck Deep into Transactions",
|
||||
"description": "Explore and track all blockchain transactions",
|
||||
"title_color": ["#1a472a", "#4ade80"],
|
||||
"description_color": ["#374151", "#d1d5db"],
|
||||
"background": ["#e0f2fe", "#0f172a"],
|
||||
"side_img_url": [
|
||||
"https://example.com",
|
||||
"https://example.com"
|
||||
],
|
||||
"page_path": "/txs"
|
||||
},
|
||||
{
|
||||
"title": "Capybara Hot Spring Pools",
|
||||
"description": "Monitor liquidity and staking pools",
|
||||
"is_pinned": true,
|
||||
"redirect_url": "https://example.com"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -144,6 +144,7 @@ Also, be aware that if you customize the name of the currency or any of its deno
|
||||
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'daily_operational_txs' \| 'coin_price' \| 'secondary_coin_price' \| 'market_cap' \| 'tvl'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` | v1.0.x+ |
|
||||
| NEXT_PUBLIC_HOMEPAGE_STATS | `Array<'latest_batch' \| 'total_blocks' \| 'average_block_time' \| 'total_txs' \| 'total_operational_txs' \| 'latest_l1_state_batch' \| 'wallet_addresses' \| 'gas_tracker' \| 'btc_locked' \| 'current_epoch'>` | List of stats widgets displayed on the home page | - | For zkSync, zkEvm and Arbitrum rollups: `['latest_batch','average_block_time','total_txs','wallet_addresses','gas_tracker']`, for other cases: `['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker']` | `['total_blocks','total_txs','wallet_addresses']` | v1.35.x+ |
|
||||
| NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG | `HeroBannerConfig`, see details [below](#hero-banner-configuration-properties) | Configuration of hero banner appearance. | - | - | See [below](#hero-banner-configuration-properties) | v1.35.0+ |
|
||||
| NEXT_PUBLIC_HOMEPAGE_HIGHLIGHTS_CONFIG | `string` | URL of the file (in `.json` format only) that contains the configuration for banners on the application's homepage, showcasing some of its key functionality. See the full config format [below](#highlights-banner-configuration-properties). The config should contain at least 2 banners, but only 3 banners will be visible at the same time. A larger number of banners in the config allows for random banner rotation upon page load. | - | - | See [below](#highlights-banner-configuration-properties) | upcoming |
|
||||
|
||||
#### Hero banner configuration properties
|
||||
|
||||
@@ -157,6 +158,23 @@ _Note_ Here, all values are arrays of up to two strings. The first string repres
|
||||
| search | `{ border_width: [string, string] }` | Search bar customization. Currently supports only width of the border (in px). | - | - | `{ 'border_width': ['0px', '2px'] }` |
|
||||
| button | `Partial<Record<'_default' \| '_hover' \| '_selected', {'background'?: [string, string]; 'text_color?:[string, string]'}>>` | The button on the banner. It has three possible states: `_default`, `_hover`, and `_selected`. The `_selected` state reflects when the user is logged in or their wallet is connected to the app. | - | - | `{'_default':{'background':['deeppink'],'text_color':['white']}}` |
|
||||
|
||||
#### Highlights banner configuration properties
|
||||
|
||||
_Note_ Some properties can hold an array of up to two strings. The first string represents the value for the light color mode, while the second string represents the value for the dark color mode. If the array contains only one string, it will be used for both color modes.
|
||||
|
||||
| Variable | Type| Description | Compulsoriness | Default value | Example value |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| title | `string` | Title on the banner | Required | - | `Duck Deep into Transactions` |
|
||||
| description | `string` | Short description of the feature | Required | - | `Explore and track all blockchain transactions` |
|
||||
| title_color | `[string, string]` | Text color of the title. | - | `['#101112', '#F8FCFF']` | `['#FFB8D4', '#D9FE41']` |
|
||||
| description_color | `[string, string]` | Text color of the description. | - | `['#718096', '#AEB1B6']` | `['#FFB8D4', '#D9FE41']` |
|
||||
| background | `[string, string]` | Banner background (could be a solid color, gradient or picture). The string should be a valid `background` CSS property value. | - | `['#EFF7FF', '#2A3340']` | `['deeppink']` |
|
||||
| side_img_url | `[string, string]` | URL of an image that appears on the right side of the banner. | - | - | `https://placekitten/1400/200` |
|
||||
| is_pinned | `boolean` | Indicates whether the banner should remain always visible despite potential rotation. | - | - | `https://placekitten/1400/200` |
|
||||
| page_path | `string` | Internal page path for constructing the banner link. | - | - | `/pools` |
|
||||
| redirect_url | `string` | External link on the banner. | - | - | `https://example.com` |
|
||||
|
||||
|
||||
|
||||
|
||||
### Navigation
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export const HOMEPAGE_HIGHLIGHTS_BANNER = {
|
||||
title: 'Duck Deep into Transactions',
|
||||
description: 'Explore and track all blockchain transactions',
|
||||
};
|
||||
@@ -13,7 +13,7 @@ export interface ImageProps extends ChakraImageProps {
|
||||
|
||||
export const Image = React.forwardRef<HTMLImageElement, ImageProps>(
|
||||
function Image(props, ref) {
|
||||
const { fallback, src, onLoad, onError, skeletonWidth, skeletonHeight, ...rest } = props;
|
||||
const { fallback, src, onLoad, onError, skeletonWidth, skeletonHeight, alt, ...rest } = props;
|
||||
|
||||
const [ loading, setLoading ] = React.useState(true);
|
||||
const [ error, setError ] = React.useState(false);
|
||||
@@ -55,6 +55,7 @@ export const Image = React.forwardRef<HTMLImageElement, ImageProps>(
|
||||
<ChakraImage
|
||||
ref={ ref }
|
||||
src={ src }
|
||||
alt={ alt }
|
||||
onError={ handleLoadError }
|
||||
onLoad={ handleLoadSuccess }
|
||||
{ ...rest }
|
||||
|
||||
@@ -33,3 +33,15 @@ export interface HeroBannerConfig {
|
||||
border_width?: Array<string | undefined>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HighlightsBannerConfig {
|
||||
title: string;
|
||||
description: string;
|
||||
title_color?: Array<string | undefined>;
|
||||
description_color?: Array<string | undefined>;
|
||||
background?: Array<string | undefined>;
|
||||
side_img_url?: Array<string | undefined>;
|
||||
is_pinned?: boolean;
|
||||
page_path?: string;
|
||||
redirect_url?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { HighlightsBannerConfig } from 'types/homepage';
|
||||
|
||||
import { test, expect } from 'playwright/lib';
|
||||
|
||||
import Highlights from './Highlights';
|
||||
|
||||
const IMAGE_URL_1 = 'https://localhost:3000/my-image.png';
|
||||
const IMAGE_URL_2 = 'https://localhost:3000/my-image-2.png';
|
||||
const IMAGE_URL_3 = 'https://localhost:3000/my-image-3.png';
|
||||
const HIGHLIGHTS_CONFIG_URL = 'https://localhost:3000/homepage-highlights-config.json';
|
||||
const HIGHLIGHTS_CONFIG: Array<HighlightsBannerConfig> = [
|
||||
// no adaptive
|
||||
{
|
||||
title: 'Duck Deep into Transactions',
|
||||
description: 'Explore and track all blockchain transactions',
|
||||
side_img_url: [ IMAGE_URL_1 ],
|
||||
title_color: [ '#D9FE41' ],
|
||||
description_color: [ '#CBD0D7' ],
|
||||
background: [ '#06331B' ],
|
||||
redirect_url: 'https://example.com',
|
||||
is_pinned: true,
|
||||
},
|
||||
// adaptive
|
||||
{
|
||||
title: 'Geese Token Swap',
|
||||
description: 'Swap tokens across different protocols',
|
||||
title_color: [ '#1e40af', '#93c5fd' ],
|
||||
description_color: [ '#64748b', '#94a3b8' ],
|
||||
background: [
|
||||
'linear-gradient(82.75deg, #FDDCEF 0.08%, #FAF5FB 51.54%, #FFFBDB 104.15%)',
|
||||
'linear-gradient(82.75deg, #4F2D6A 0.08%, #3D425A 51.54%, #3A6024 104.15%)',
|
||||
],
|
||||
side_img_url: [ IMAGE_URL_2, IMAGE_URL_3 ],
|
||||
page_path: '/essential-dapps/swap',
|
||||
is_pinned: true,
|
||||
},
|
||||
// default
|
||||
{
|
||||
title: 'Duckling Smart Contracts',
|
||||
description: 'Discover newly deployed smart contracts',
|
||||
is_pinned: true,
|
||||
},
|
||||
];
|
||||
|
||||
test('three banners +@dark-mode', async({ render, mockEnvs, mockConfigResponse, mockAssetResponse }) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_HOMEPAGE_HIGHLIGHTS_CONFIG', HIGHLIGHTS_CONFIG_URL ],
|
||||
]);
|
||||
await mockConfigResponse('NEXT_PUBLIC_HOMEPAGE_HIGHLIGHTS_CONFIG', HIGHLIGHTS_CONFIG_URL, HIGHLIGHTS_CONFIG);
|
||||
await mockAssetResponse(IMAGE_URL_1, './playwright/mocks/image_s.jpg');
|
||||
await mockAssetResponse(IMAGE_URL_2, './playwright/mocks/image_md.jpg');
|
||||
await mockAssetResponse(IMAGE_URL_3, './playwright/mocks/image_long.jpg');
|
||||
|
||||
const component = await render(<Highlights/>);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test('two banners', async({ render, mockEnvs, mockConfigResponse, mockAssetResponse }) => {
|
||||
await mockEnvs([
|
||||
[ 'NEXT_PUBLIC_HOMEPAGE_HIGHLIGHTS_CONFIG', HIGHLIGHTS_CONFIG_URL ],
|
||||
]);
|
||||
await mockConfigResponse('NEXT_PUBLIC_HOMEPAGE_HIGHLIGHTS_CONFIG', HIGHLIGHTS_CONFIG_URL, HIGHLIGHTS_CONFIG.slice(0, 2));
|
||||
await mockAssetResponse(IMAGE_URL_1, './playwright/mocks/image_s.jpg');
|
||||
await mockAssetResponse(IMAGE_URL_2, './playwright/mocks/image_md.jpg');
|
||||
await mockAssetResponse(IMAGE_URL_3, './playwright/mocks/image_long.jpg');
|
||||
|
||||
const component = await render(<Highlights/>);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { StackProps } from '@chakra-ui/react';
|
||||
import { HStack } from '@chakra-ui/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { shuffle } from 'es-toolkit';
|
||||
import React from 'react';
|
||||
|
||||
import type { HighlightsBannerConfig } from 'types/homepage';
|
||||
|
||||
import config from 'configs/app';
|
||||
import useFetch from 'lib/hooks/useFetch';
|
||||
import { HOMEPAGE_HIGHLIGHTS_BANNER } from 'stubs/homepage';
|
||||
|
||||
import HighlightsItem from './highlights/HighlightsItem';
|
||||
|
||||
const HIGHLIGHTS_BANNER_COUNT = 3;
|
||||
|
||||
const Highlights = (props: StackProps) => {
|
||||
const fetch = useFetch();
|
||||
|
||||
const { isPlaceholderData, data } = useQuery({
|
||||
queryKey: [ 'homepage-highlights' ],
|
||||
queryFn: async() => fetch(config.UI.homepage.highlights || '', undefined, { resource: 'homepage-highlights' }) as Promise<Array<HighlightsBannerConfig>>,
|
||||
select: (data) => {
|
||||
const pinnedBanners = data.filter((banner) => banner.is_pinned);
|
||||
const otherBanners = data.filter((banner) => !banner.is_pinned);
|
||||
|
||||
return [
|
||||
...pinnedBanners,
|
||||
...shuffle(otherBanners),
|
||||
].slice(0, HIGHLIGHTS_BANNER_COUNT);
|
||||
},
|
||||
enabled: Boolean(config.UI.homepage.highlights),
|
||||
staleTime: Infinity,
|
||||
placeholderData: Array(HIGHLIGHTS_BANNER_COUNT).fill(HOMEPAGE_HIGHLIGHTS_BANNER),
|
||||
});
|
||||
|
||||
return (
|
||||
<HStack gap={ 3 } { ...props }>
|
||||
{ data?.map((banner, index) => (
|
||||
<HighlightsItem key={ index } data={ banner } isLoading={ isPlaceholderData } totalNum={ data.length }/>
|
||||
)) }
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Highlights);
|
||||
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 89 KiB |
@@ -0,0 +1,113 @@
|
||||
import type { JsxStyleProps } from '@chakra-ui/react';
|
||||
import { Text, VStack, HStack } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { HighlightsBannerConfig } from 'types/homepage';
|
||||
|
||||
import config from 'configs/app';
|
||||
import { useColorModeValue } from 'toolkit/chakra/color-mode';
|
||||
import { Heading } from 'toolkit/chakra/heading';
|
||||
import { Image } from 'toolkit/chakra/image';
|
||||
import { Link } from 'toolkit/chakra/link';
|
||||
import { Skeleton } from 'toolkit/chakra/skeleton';
|
||||
|
||||
interface ContainerProps extends Omit<Props, 'totalNum'>, JsxStyleProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Container = ({ children, data, isLoading, ...rest }: ContainerProps) => {
|
||||
if ('page_path' in data) {
|
||||
return (
|
||||
<Link href={ config.app.baseUrl + data.page_path } loading={ isLoading } { ...rest }>
|
||||
{ children }
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
if ('redirect_url' in data) {
|
||||
return (
|
||||
<Link href={ data.redirect_url } loading={ isLoading } external noIcon { ...rest }>
|
||||
{ children }
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Skeleton loading={ isLoading } { ...rest }>
|
||||
{ children }
|
||||
</Skeleton>
|
||||
);
|
||||
};
|
||||
|
||||
interface Props {
|
||||
data: HighlightsBannerConfig;
|
||||
isLoading: boolean;
|
||||
totalNum: number;
|
||||
}
|
||||
|
||||
const HighlightsItem = ({ data, isLoading, totalNum }: Props) => {
|
||||
|
||||
const imageSrc = useColorModeValue(
|
||||
data.side_img_url?.[0],
|
||||
data.side_img_url?.[1] || data.side_img_url?.[0],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container
|
||||
data={ data }
|
||||
isLoading={ isLoading }
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
minH="153px"
|
||||
pl={ 6 }
|
||||
w={ `calc((100% - ${ (totalNum - 1) * 12 }px) / ${ totalNum })` }
|
||||
overflow="hidden"
|
||||
borderRadius="md"
|
||||
bg={ !isLoading ? {
|
||||
_light: data.background?.[0] || '#EFF7FF',
|
||||
_dark: data.background?.[1] || data.background?.[0] || '#2A3340',
|
||||
} : undefined }
|
||||
>
|
||||
<HStack overflow="hidden" w="100%" gap={ 3 }>
|
||||
<VStack
|
||||
alignItems="flex-start"
|
||||
gap={ 3 }
|
||||
w={ totalNum === 2 ? '294px' : '193px' }
|
||||
flexShrink={ 0 }
|
||||
>
|
||||
<Heading
|
||||
level="3"
|
||||
color={{
|
||||
_light: data.title_color?.[0] || '#101112',
|
||||
_dark: data.title_color?.[1] || data.title_color?.[0] || '#F8FCFF',
|
||||
}}
|
||||
>
|
||||
{ data.title }
|
||||
</Heading>
|
||||
<Text
|
||||
textStyle="sm"
|
||||
color={{
|
||||
_light: data.description_color?.[0] || '#718096',
|
||||
_dark: data.description_color?.[1] || data.description_color?.[0] || '#AEB1B6',
|
||||
}}
|
||||
>
|
||||
{ data.description }
|
||||
</Text>
|
||||
</VStack>
|
||||
{ imageSrc && !isLoading && (
|
||||
<Image
|
||||
src={ imageSrc }
|
||||
width="210px"
|
||||
height="112px"
|
||||
skeletonWidth="0px"
|
||||
objectFit="cover"
|
||||
ml="auto"
|
||||
mr={ 6 }
|
||||
/>
|
||||
) }
|
||||
</HStack>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(HighlightsItem);
|
||||
@@ -4,6 +4,7 @@ import React from 'react';
|
||||
import config from 'configs/app';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import HeroBanner from 'ui/home/HeroBanner';
|
||||
import Highlights from 'ui/home/Highlights';
|
||||
import ChainIndicators from 'ui/home/indicators/ChainIndicators';
|
||||
import LatestArbitrumL2Batches from 'ui/home/latestBatches/LatestArbitrumL2Batches';
|
||||
import LatestZkEvmL2Batches from 'ui/home/latestBatches/LatestZkEvmL2Batches';
|
||||
@@ -37,6 +38,7 @@ const Home = () => {
|
||||
<Stats/>
|
||||
<ChainIndicators/>
|
||||
</Flex>
|
||||
{ !isMobile && config.UI.homepage.highlights && <Highlights mt={ 3 }/> }
|
||||
{ isMobile && <AdBanner mt={ 6 } mx="auto" justifyContent="center" format="mobile"/> }
|
||||
<Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 6 }>
|
||||
{ leftWidget }
|
||||
|
||||
@@ -26,7 +26,7 @@ test('default view', async({ render, mockTextAd, mockAssetResponse, mockApiRespo
|
||||
await mockApiResponse('general:optimistic_l2_interop_messages', MESSAGES_RESPONSE);
|
||||
await mockApiResponse('general:optimistic_l2_interop_messages_count', 4000000);
|
||||
const component = await render(<InteropMessages/>);
|
||||
await expect(component).toHaveScreenshot();
|
||||
await expect(component).toHaveScreenshot({ maxDiffPixels: 30 });
|
||||
});
|
||||
|
||||
test.describe('mobile', () => {
|
||||
@@ -37,6 +37,6 @@ test.describe('mobile', () => {
|
||||
await mockApiResponse('general:optimistic_l2_interop_messages', MESSAGES_RESPONSE);
|
||||
await mockApiResponse('general:optimistic_l2_interop_messages_count', 4000000);
|
||||
const component = await render(<InteropMessages/>);
|
||||
await expect(component).toHaveScreenshot({ maxDiffPixels: 20 });
|
||||
await expect(component).toHaveScreenshot({ maxDiffPixels: 30 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,11 +32,15 @@ test('email login', async({ render, page, mockApiResponse }) => {
|
||||
await page.getByLabel('pin code 4 of 6').fill('4');
|
||||
await page.getByLabel('pin code 5 of 6').fill('5');
|
||||
await page.getByLabel('pin code 6 of 6').fill('6');
|
||||
// make sure that the focus is removed from the button
|
||||
await page.getByText('Confirmation code').click();
|
||||
await expect(page).toHaveScreenshot();
|
||||
|
||||
// submit otp code
|
||||
await mockApiResponse('general:auth_confirm_otp', profileMock.base as never);
|
||||
await page.getByText('Submit').click();
|
||||
// make sure that the focus is removed from the button
|
||||
await page.getByText('Congrats').click();
|
||||
await expect(page).toHaveScreenshot();
|
||||
|
||||
await page.getByLabel('Close').click();
|
||||
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |