Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions configs/app/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
2 changes: 2 additions & 0 deletions configs/envs/.env.main
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
1 change: 1 addition & 0 deletions deploy/scripts/download_assets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions deploy/tools/envs-validator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions deploy/tools/envs-validator/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 25 additions & 0 deletions deploy/tools/envs-validator/schemas/features/highlights.ts
Original file line number Diff line number Diff line change
@@ -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)
});
1 change: 1 addition & 0 deletions deploy/tools/envs-validator/schemas/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions deploy/tools/envs-validator/test/.env.base
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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"
}
]

18 changes: 18 additions & 0 deletions docs/ENVS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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` |


&nbsp;

### Navigation
Expand Down
4 changes: 4 additions & 0 deletions stubs/homepage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const HOMEPAGE_HIGHLIGHTS_BANNER = {
title: 'Duck Deep into Transactions',
description: 'Explore and track all blockchain transactions',
};
3 changes: 2 additions & 1 deletion toolkit/chakra/image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -55,6 +55,7 @@ export const Image = React.forwardRef<HTMLImageElement, ImageProps>(
<ChakraImage
ref={ ref }
src={ src }
alt={ alt }
onError={ handleLoadError }
onLoad={ handleLoadSuccess }
{ ...rest }
Expand Down
12 changes: 12 additions & 0 deletions types/homepage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
73 changes: 73 additions & 0 deletions ui/home/Highlights.pw.tsx
Original file line number Diff line number Diff line change
@@ -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();
});
46 changes: 46 additions & 0 deletions ui/home/Highlights.tsx
Original file line number Diff line number Diff line change
@@ -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);
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading