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

Hero banner: more customizations (#2199)

* add ENVs to configure typefaces

* migrate current evns to new config

* add env to configure the banner border

* make button variants for hero and header

* add customization for hero button

* tests for new button variants

* envs for demo

* update button styles and tests

* add tests for banner

* remove unnecessary test

* remove demo values
parent fc1deede
import type { ContractCodeIde } from 'types/client/contract';
import { NAVIGATION_LINK_IDS, type NavItemExternal, type NavigationLinkId, type NavigationLayout } from 'types/client/navigation';
import type { ChainIndicatorId } from 'types/homepage';
import type { ChainIndicatorId, HeroBannerConfig } from 'types/homepage';
import type { NetworkExplorer } from 'types/networks';
import type { ColorThemeId } from 'types/settings';
import type { FontFamily } from 'types/ui';
import { COLOR_THEMES } from 'lib/settings/colorTheme';
......@@ -34,9 +35,6 @@ const defaultColorTheme = (() => {
return COLOR_THEMES.find((theme) => theme.id === envValue);
})();
// eslint-disable-next-line max-len
const HOMEPAGE_PLATE_BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)';
const UI = Object.freeze({
navigation: {
logo: {
......@@ -60,9 +58,10 @@ const UI = Object.freeze({
},
homepage: {
charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_CHARTS')) || [],
heroBanner: parseEnvJson<HeroBannerConfig>(getEnvValue('NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG')),
plate: {
background: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND') || HOMEPAGE_PLATE_BACKGROUND_DEFAULT,
textColor: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR') || 'white',
background: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND'),
textColor: getEnvValue('NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR'),
},
showAvgBlockTime: getEnvValue('NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME') === 'false' ? false : true,
},
......@@ -88,6 +87,10 @@ const UI = Object.freeze({
colorTheme: {
'default': defaultColorTheme,
},
fonts: {
heading: parseEnvJson<FontFamily>(getEnvValue('NEXT_PUBLIC_FONT_FAMILY_HEADING')),
body: parseEnvJson<FontFamily>(getEnvValue('NEXT_PUBLIC_FONT_FAMILY_BODY')),
},
});
export default UI;
......@@ -20,6 +20,7 @@ async function run() {
return result;
}, {} as Record<string, string>);
printDeprecationWarning(appEnvs);
await checkPlaceholdersCongruity(appEnvs);
await validateEnvs(appEnvs);
......@@ -135,3 +136,15 @@ function getEnvsPlaceholders(filePath: string): Promise<Array<string>> {
});
});
}
function printDeprecationWarning(envsMap: Record<string, string>) {
if (
envsMap.NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR ||
envsMap.NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND
) {
console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗');
// eslint-disable-next-line max-len
console.warn('The NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR and NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND variables are now deprecated and will be removed in the next release. Please migrate to the NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG variable.');
console.log('❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗❗\n');
}
}
......@@ -30,9 +30,10 @@ import type { WalletType } from '../../../types/client/wallets';
import { SUPPORTED_WALLETS } from '../../../types/client/wallets';
import type { CustomLink, CustomLinksGroup } from '../../../types/footerLinks';
import { CHAIN_INDICATOR_IDS } from '../../../types/homepage';
import type { ChainIndicatorId } from '../../../types/homepage';
import type { ChainIndicatorId, HeroBannerButtonState, HeroBannerConfig } from '../../../types/homepage';
import { type NetworkVerificationTypeEnvs, type NetworkExplorer, type FeaturedNetwork, NETWORK_GROUPS } from '../../../types/networks';
import { COLOR_THEME_IDS } from '../../../types/settings';
import type { FontFamily } from '../../../types/ui';
import type { AddressViewId } from '../../../types/views/address';
import { ADDRESS_VIEWS_IDS, IDENTICON_TYPES } from '../../../types/views/address';
import { BLOCK_FIELDS_IDS } from '../../../types/views/block';
......@@ -390,6 +391,34 @@ const navItemExternalSchema: yup.ObjectSchema<NavItemExternal> = yup
url: yup.string().test(urlTest).required(),
});
const fontFamilySchema: yup.ObjectSchema<FontFamily> = yup
.object()
.transform(replaceQuotes)
.json()
.shape({
name: yup.string().required(),
url: yup.string().test(urlTest).required(),
});
const heroBannerButtonStateSchema: yup.ObjectSchema<HeroBannerButtonState> = yup.object({
background: yup.array().max(2).of(yup.string()),
text_color: yup.array().max(2).of(yup.string()),
});
const heroBannerSchema: yup.ObjectSchema<HeroBannerConfig> = yup.object()
.transform(replaceQuotes)
.json()
.shape({
background: yup.array().max(2).of(yup.string()),
text_color: yup.array().max(2).of(yup.string()),
border: yup.array().max(2).of(yup.string()),
button: yup.object({
_default: heroBannerButtonStateSchema,
_hover: heroBannerButtonStateSchema,
_selected: heroBannerButtonStateSchema,
}),
});
const footerLinkSchema: yup.ObjectSchema<CustomLink> = yup
.object({
text: yup.string().required(),
......@@ -540,6 +569,23 @@ const schema = yup
.of(yup.string<ChainIndicatorId>().oneOf(CHAIN_INDICATOR_IDS)),
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR: yup.string(),
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND: yup.string(),
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG: yup
.mixed()
.test(
'shape',
(ctx) => {
try {
heroBannerSchema.validateSync(ctx.originalValue);
throw new Error('Unknown validation error');
} catch (error: unknown) {
const message = typeof error === 'object' && error !== null && 'errors' in error && Array.isArray(error.errors) ? error.errors.join(', ') : '';
return 'Invalid schema were provided for NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG' + (message ? `: ${ message }` : '');
}
},
(data) => {
const isUndefined = data === undefined;
return isUndefined || heroBannerSchema.isValidSync(data);
}),
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME: yup.boolean(),
// b. sidebar
......@@ -634,6 +680,18 @@ const schema = yup
NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS: yup.boolean(),
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE: yup.string(),
NEXT_PUBLIC_COLOR_THEME_DEFAULT: yup.string().oneOf(COLOR_THEME_IDS),
NEXT_PUBLIC_FONT_FAMILY_HEADING: yup
.mixed()
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_FONT_FAMILY_HEADING', (data) => {
const isUndefined = data === undefined;
return isUndefined || fontFamilySchema.isValidSync(data);
}),
NEXT_PUBLIC_FONT_FAMILY_BODY: yup
.mixed()
.test('shape', 'Invalid schema were provided for NEXT_PUBLIC_FONT_FAMILY_BODY', (data) => {
const isUndefined = data === undefined;
return isUndefined || fontFamilySchema.isValidSync(data);
}),
// 5. Features configuration
NEXT_PUBLIC_API_SPEC_URL: yup
......
......@@ -28,6 +28,8 @@ NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
NEXT_PUBLIC_FEATURED_NETWORKS=https://example.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/accounts','/apps']
NEXT_PUBLIC_NAVIGATION_LAYOUT=horizontal
NEXT_PUBLIC_FONT_FAMILY_HEADING={'name':'Montserrat','url':'https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap'}
NEXT_PUBLIC_FONT_FAMILY_BODY={'name':'Raleway','url':'https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;600;700&display=swap'}
NEXT_PUBLIC_FOOTER_LINKS=https://example.com
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0xf7d4972356e6ae44ae948d0cf19ef2beaf0e574c180997e969a2837da15e349d
NEXT_PUBLIC_HIDE_INDEXING_ALERT_BLOCKS=false
......@@ -35,6 +37,7 @@ NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS=false
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR='#fff'
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND='rgb(255, 145, 0)'
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['lightpink'],'text_color':['deepskyblue','white'],'border':['3px solid black']}
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_GAS_TRACKER_ENABLED=true
NEXT_PUBLIC_GAS_TRACKER_UNITS=['gwei']
......
......@@ -118,9 +118,21 @@ Please be aware that all environment variables prefixed with `NEXT_PUBLIC_` will
| Variable | Type| Description | Compulsoriness | Default value | Example value | Version |
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_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_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `white` | `\#DCFE76` | v1.0.x+ |
| NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND | `string` | Background css value for hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead) | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%)` \| `no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)` | v1.1.0+ |
| NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead). **DEPRECATED** _Use `NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG` instead_ | - | `white` | `\#DCFE76` | v1.0.x+ |
| NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND | `string` | Background css value for hero plate on the homepage (escape "#" symbol if you use HEX color codes or use rgba-value instead). **DEPRECATED** _Use `NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG` instead_ | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%)` \| `no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)` | v1.1.0+ |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` | Set to false if average block time is useless for the network | - | `true` | `false` | v1.0.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+ |
#### Hero banner configuration properties
_Note_ Here, all values are arrays of up to two strings. The first string represents the value for the light color mode, and 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 |
| --- | --- | --- | --- | --- | --- |
| background | `[string, string]` | Banner background (could be a solid color, gradient or picture). The string should be a valid `background` CSS property value. | - | `['radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)']` | `['lightpink','no-repeat bottom 20% right 0px/100% url(https://placekitten/1400/200)']` |
| text_color | `[string, string]` | Banner text background. The string should be a valid `color` CSS property value. | - | `['white']` | `['lightpink','#DCFE76']` |
| border | `[string, string]` | Banner border. The string should be a valid `border` CSS property value. | - | - | `['1px solid yellow','4px dashed #DCFE76']` |
| 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']}}` |
&nbsp;
......@@ -286,6 +298,8 @@ Settings for meta tags, OG tags and SEO
| NEXT_PUBLIC_HIDE_INDEXING_ALERT_INT_TXS | `boolean` | Set to `true` to hide indexing alert in the page footer about indexing block's internal transactions | - | `false` | `true` | v1.17.0+ |
| NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE | `string` | Used for displaying custom announcements or alerts in the header of the site. Could be a regular string or a HTML code. | - | - | `Hello world! 🤪` | v1.13.0+ |
| NEXT_PUBLIC_COLOR_THEME_DEFAULT | `'light' \| 'dim' \| 'midnight' \| 'dark'` | Preferred color theme of the app | - | - | `midnight` | v1.30.0+ |
| NEXT_PUBLIC_FONT_FAMILY_HEADING | `FontFamily`, see full description [below](#font-family-configuration-properties) | Special typeface to use in page headings (`<h1>`, `<h2>`, etc.) | - | - | `{'name':'Montserrat','url':'https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap'}` | v1.35.0+ |
| NEXT_PUBLIC_FONT_FAMILY_BODY | `FontFamily`, see full description [below](#font-family-configuration-properties) | Main typeface to use in page content elements. | - | - | `{'name':'Raleway','url':'https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;600;700&display=swap'}` | v1.35.0+ |
#### Network explorer configuration properties
......@@ -306,6 +320,13 @@ Settings for meta tags, OG tags and SEO
| url | `string` | URL of the IDE with placeholders for contract hash (`{hash}`) and current domain (`{domain}`) | Required | - | `https://remix.blockscout.com/?address={hash}&blockscout={domain}` |
| icon_url | `string` | URL of the IDE icon | Required | - | `https://example.com/icon.svg` |
#### Font family configuration properties
| Variable | Type| Description | Compulsoriness | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| name | `string` | Font family name; used to define the `font-family` CSS property. | Required | - | `Montserrat` |
| url | `string` | URL for external font. Ensure the font supports the following weights: 400, 500, 600, and 700. | Required | - | `https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap` |
&nbsp;
## App features
......
......@@ -30,6 +30,18 @@ const getCspReportUrl = () => {
}
};
const externalFontsDomains = (() => {
try {
return [
config.UI.fonts.heading?.url,
config.UI.fonts.body?.url,
]
.filter(Boolean)
.map((urlString) => new URL(urlString))
.map((url) => url.hostname);
} catch (error) {}
})();
export function app(): CspDev.DirectiveDescriptor {
return {
'default-src': [
......@@ -116,6 +128,7 @@ export function app(): CspDev.DirectiveDescriptor {
'font-src': [
KEY_WORDS.DATA,
...MAIN_DOMAINS,
...(externalFontsDomains || []),
],
'object-src': [
......
......@@ -6,6 +6,7 @@ import React from 'react';
import logRequestFromBot from 'nextjs/utils/logRequestFromBot';
import * as serverTiming from 'nextjs/utils/serverTiming';
import config from 'configs/app';
import theme from 'theme/theme';
import * as svgSprite from 'ui/shared/IconSvg';
......@@ -35,11 +36,11 @@ class MyDocument extends Document {
<Head>
{ /* FONTS */ }
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
href={ config.UI.fonts.heading?.url ?? 'https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap' }
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
href={ config.UI.fonts.body?.url ?? 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap' }
rel="stylesheet"
/>
......
......@@ -13,6 +13,8 @@ test.use({ viewport: { width: 150, height: 350 } });
{ variant: 'ghost', withDarkMode: true, states: [ 'default', 'hovered', 'active' ] },
{ variant: 'subtle', states: [ 'default', 'hovered' ] },
{ variant: 'subtle', colorScheme: 'gray', states: [ 'default', 'hovered' ], withDarkMode: true },
{ variant: 'hero', states: [ 'default', 'hovered' ], withDarkMode: true },
{ variant: 'header', states: [ 'default', 'hovered', 'selected' ], withDarkMode: true },
].forEach(({ variant, colorScheme, withDarkMode, states }) => {
test.describe(`variant ${ variant }${ colorScheme ? ` with ${ colorScheme } color scheme` : '' }${ withDarkMode ? ' +@dark-mode' : '' }`, () => {
test('', async({ render }) => {
......
......@@ -2,6 +2,8 @@ import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
import { runIfFn } from '@chakra-ui/utils';
import config from 'configs/app';
const variantSolid = defineStyle((props) => {
const { colorScheme: c } = props;
......@@ -150,12 +152,76 @@ const variantSubtle = defineStyle((props) => {
};
});
// for buttons in the hero banner
const variantHero = defineStyle((props) => {
return {
bg: mode(
config.UI.homepage.heroBanner?.button?._default?.background?.[0] || 'blue.600',
config.UI.homepage.heroBanner?.button?._default?.background?.[1] || 'blue.600',
)(props),
color: mode(
config.UI.homepage.heroBanner?.button?._default?.text_color?.[0] || 'white',
config.UI.homepage.heroBanner?.button?._default?.text_color?.[1] || 'white',
)(props),
_hover: {
bg: mode(
config.UI.homepage.heroBanner?.button?._hover?.background?.[0] || 'blue.400',
config.UI.homepage.heroBanner?.button?._hover?.background?.[1] || 'blue.400',
)(props),
color: mode(
config.UI.homepage.heroBanner?.button?._hover?.text_color?.[0] || 'white',
config.UI.homepage.heroBanner?.button?._hover?.text_color?.[1] || 'white',
)(props),
},
'&[data-selected=true]': {
bg: mode(
config.UI.homepage.heroBanner?.button?._selected?.background?.[0] || 'blue.50',
config.UI.homepage.heroBanner?.button?._selected?.background?.[1] || 'blue.50',
)(props),
color: mode(
config.UI.homepage.heroBanner?.button?._selected?.text_color?.[0] || 'blackAlpha.800',
config.UI.homepage.heroBanner?.button?._selected?.text_color?.[1] || 'blackAlpha.800',
)(props),
},
};
});
// for buttons in the page header
const variantHeader = defineStyle((props) => {
return {
bgColor: 'transparent',
color: mode('blackAlpha.800', 'gray.400')(props),
borderColor: mode('gray.300', 'gray.600')(props),
borderWidth: props.borderWidth || '2px',
borderStyle: 'solid',
_hover: {
color: 'link_hovered',
borderColor: 'link_hovered',
},
'&[data-selected=true]': {
bgColor: mode('blackAlpha.50', 'whiteAlpha.100')(props),
color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
borderColor: 'transparent',
borderWidth: props.borderWidth || '0px',
},
'&[data-selected=true][data-warning=true]': {
bgColor: mode('orange.100', 'orange.900')(props),
color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
borderColor: 'transparent',
borderWidth: props.borderWidth || '0px',
},
};
});
const variants = {
solid: variantSolid,
outline: variantOutline,
simple: variantSimple,
ghost: variantGhost,
subtle: variantSubtle,
hero: variantHero,
header: variantHeader,
};
const baseStyle = defineStyle({
......
import { theme } from '@chakra-ui/react';
export const BODY_TYPEFACE = 'Inter';
export const HEADING_TYPEFACE = 'Poppins';
import config from 'configs/app';
export const BODY_TYPEFACE = config.UI.fonts.body?.name ?? 'Inter';
export const HEADING_TYPEFACE = config.UI.fonts.heading?.name ?? 'Poppins';
const typography = {
fonts: {
......
export const CHAIN_INDICATOR_IDS = [ 'daily_txs', 'coin_price', 'secondary_coin_price', 'market_cap', 'tvl' ] as const;
export type ChainIndicatorId = typeof CHAIN_INDICATOR_IDS[number];
export interface HeroBannerButtonState {
background?: Array<string | undefined>;
text_color?: Array<string | undefined>;
}
export interface HeroBannerConfig {
background?: Array<string | undefined>;
text_color?: Array<string | undefined>;
border?: Array<string | undefined>;
button?: {
_default?: HeroBannerButtonState;
_hover?: HeroBannerButtonState;
_selected?: HeroBannerButtonState;
};
}
export interface FontFamily {
name: string;
url: string;
}
import type { BrowserContext } from '@playwright/test';
import React from 'react';
import * as profileMock from 'mocks/user/profile';
import { contextWithAuth } from 'playwright/fixtures/auth';
import { test, expect } from 'playwright/lib';
import * as pwConfig from 'playwright/utils/config';
import HeroBanner from './HeroBanner';
const authTest = test.extend<{ context: BrowserContext }>({
context: contextWithAuth,
});
authTest('customization +@dark-mode', async({ render, page, mockEnvs, mockApiResponse, mockAssetResponse }) => {
const IMAGE_URL = 'https://localhost:3000/my-image.png';
await mockEnvs([
// eslint-disable-next-line max-len
[ 'NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG', `{"background":["lightpink","no-repeat center/cover url(${ IMAGE_URL })"],"text_color":["deepskyblue","white"],"border":["3px solid green","3px dashed yellow"],"button":{"_default":{"background":["deeppink"],"text_color":["white"]},"_selected":{"background":["lime"]}}}` ],
]);
await page.route(IMAGE_URL, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_long.jpg',
});
});
await mockApiResponse('user_info', profileMock.base);
await mockAssetResponse(profileMock.base.avatar, './playwright/mocks/image_s.jpg');
const component = await render(<HeroBanner/>);
await expect(component).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
maskColor: pwConfig.maskColor,
});
});
import { Box, Flex, Heading, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import AdBanner from 'ui/shared/ad/AdBanner';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import SearchBar from 'ui/snippets/searchBar/SearchBar';
import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop';
const BACKGROUND_DEFAULT = 'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%), var(--chakra-colors-blue-400)';
const TEXT_COLOR_DEFAULT = 'white';
const BORDER_DEFAULT = 'none';
const HeroBanner = () => {
const background = useColorModeValue(
config.UI.homepage.heroBanner?.background?.[0] || config.UI.homepage.plate.background || BACKGROUND_DEFAULT,
config.UI.homepage.heroBanner?.background?.[1] || config.UI.homepage.plate.background || BACKGROUND_DEFAULT,
);
const textColor = useColorModeValue(
config.UI.homepage.heroBanner?.text_color?.[0] || config.UI.homepage.plate.textColor || TEXT_COLOR_DEFAULT,
config.UI.homepage.heroBanner?.text_color?.[1] || config.UI.homepage.plate.textColor || TEXT_COLOR_DEFAULT,
);
const border = useColorModeValue(
config.UI.homepage.heroBanner?.border?.[0] || BORDER_DEFAULT,
config.UI.homepage.heroBanner?.border?.[1] || BORDER_DEFAULT,
);
return (
<Flex
w="100%"
background={ background }
border={ border }
borderRadius="md"
p={{ base: 4, lg: 8 }}
columnGap={ 8 }
alignItems="center"
>
<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={ 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>
<AdBanner platform="mobile" w="fit-content" flexShrink={ 0 } borderRadius="md" overflow="hidden" display={{ base: 'none', lg: 'block ' }}/>
</Flex>
);
};
export default React.memo(HeroBanner);
......@@ -49,33 +49,6 @@ test.describe('default view', () => {
});
});
test.describe('custom hero plate background', () => {
const IMAGE_URL = 'https://localhost:3000/my-image.png';
test.beforeEach(async({ mockEnvs }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND', `no-repeat center/cover url(${ IMAGE_URL })` ],
]);
});
test('default view', async({ render, page }) => {
await page.route(IMAGE_URL, (route) => {
return route.fulfill({
status: 200,
path: './playwright/mocks/image_long.jpg',
});
});
const component = await render(<Home/>);
const heroPlate = component.locator('div[data-label="hero plate"]');
await expect(heroPlate).toHaveScreenshot({
mask: [ page.locator(pwConfig.adsBannerSelector) ],
maskColor: pwConfig.maskColor,
});
});
});
// had to separate mobile test, otherwise all the tests fell on CI
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
......
import { Box, Flex, Heading } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import HeroBanner from 'ui/home/HeroBanner';
import ChainIndicators from 'ui/home/indicators/ChainIndicators';
import LatestArbitrumL2Batches from 'ui/home/latestBatches/LatestArbitrumL2Batches';
import LatestZkEvmL2Batches from 'ui/home/latestBatches/LatestZkEvmL2Batches';
......@@ -9,50 +10,13 @@ import LatestBlocks from 'ui/home/LatestBlocks';
import Stats from 'ui/home/Stats';
import Transactions from 'ui/home/Transactions';
import AdBanner from 'ui/shared/ad/AdBanner';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import SearchBar from 'ui/snippets/searchBar/SearchBar';
import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop';
const rollupFeature = config.features.rollup;
const Home = () => {
return (
<Box as="main">
<Flex
w="100%"
background={ config.UI.homepage.plate.background }
borderRadius="md"
p={{ base: 4, lg: 8 }}
columnGap={ 8 }
alignItems="center"
data-label="hero plate"
>
<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>
<AdBanner platform="mobile" w="fit-content" flexShrink={ 0 } borderRadius="md" overflow="hidden" display={{ base: 'none', lg: 'block ' }}/>
</Flex>
<HeroBanner/>
<Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 2 } rowGap={ 1 } mt={ 3 } _empty={{ mt: 0 }}>
<Stats/>
<ChainIndicators/>
......
......@@ -9,8 +9,6 @@ import Popover from 'ui/shared/chakra/Popover';
import UserAvatar from 'ui/shared/UserAvatar';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
import useMenuButtonColors from '../useMenuButtonColors';
type Props = {
isHomePage?: boolean;
className?: string;
......@@ -21,7 +19,6 @@ type Props = {
const ProfileMenuDesktop = ({ isHomePage, className, fallbackIconSize, buttonBoxSize }: Props) => {
const { data, error, isPending } = useFetchProfileInfo();
const loginUrl = useLoginUrl();
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors();
const [ hasMenu, setHasMenu ] = React.useState(false);
React.useEffect(() => {
......@@ -50,29 +47,6 @@ const ProfileMenuDesktop = ({ isHomePage, className, fallbackIconSize, buttonBox
};
})();
const variant = React.useMemo(() => {
if (hasMenu) {
return 'subtle';
}
return isHomePage ? 'solid' : 'outline';
}, [ hasMenu, isHomePage ]);
let iconButtonStyles: Partial<IconButtonProps> = {};
if (hasMenu) {
iconButtonStyles = {
bg: isHomePage ? 'blue.50' : themedBackground,
};
} else if (isHomePage) {
iconButtonStyles = {
color: 'white',
};
} else {
iconButtonStyles = {
borderColor: themedBorderColor,
color: themedColor,
};
}
return (
<Popover openDelay={ 300 } placement="bottom-end" gutter={ 10 } isLazy>
<Tooltip
......@@ -88,12 +62,11 @@ const ProfileMenuDesktop = ({ isHomePage, className, fallbackIconSize, buttonBox
className={ className }
aria-label="profile menu"
icon={ <UserAvatar size={ 20 } fallbackIconSize={ fallbackIconSize }/> }
variant={ variant }
colorScheme="blue"
variant={ isHomePage ? 'hero' : 'header' }
data-selected={ hasMenu }
boxSize={ buttonBoxSize ?? '40px' }
flexShrink={ 0 }
{ ...iconButtonProps }
{ ...iconButtonStyles }
/>
</PopoverTrigger>
</Box>
......
......@@ -8,13 +8,10 @@ import * as mixpanel from 'lib/mixpanel/index';
import UserAvatar from 'ui/shared/UserAvatar';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
import useMenuButtonColors from '../useMenuButtonColors';
const ProfileMenuMobile = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { data, error, isPending } = useFetchProfileInfo();
const loginUrl = useLoginUrl();
const { themedBackground, themedBorderColor, themedColor } = useMenuButtonColors();
const [ hasMenu, setHasMenu ] = React.useState(false);
const handleSignInClick = React.useCallback(() => {
......@@ -48,13 +45,10 @@ const ProfileMenuMobile = () => {
<IconButton
aria-label="profile menu"
icon={ <UserAvatar size={ 20 }/> }
variant={ data?.avatar ? 'subtle' : 'outline' }
colorScheme="gray"
variant="header"
data-selected={ hasMenu }
boxSize="40px"
flexShrink={ 0 }
bg={ data?.avatar ? themedBackground : undefined }
color={ themedColor }
borderColor={ !data?.avatar ? themedBorderColor : undefined }
onClick={ hasMenu ? onOpen : undefined }
{ ...iconButtonProps }
/>
......
import { useColorModeValue } from '@chakra-ui/react';
export default function useMenuColors() {
const themedBackground = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const themedBackgroundOrange = useColorModeValue('orange.100', 'orange.900');
const themedBorderColor = useColorModeValue('gray.300', 'gray.700');
const themedColor = useColorModeValue('blackAlpha.800', 'gray.400');
return { themedBackground, themedBackgroundOrange, themedBorderColor, themedColor };
}
import { Box, Flex, chakra } from '@chakra-ui/react';
import { Box, Flex, chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressIdenticon from 'ui/shared/entities/address/AddressIdenticon';
import IconSvg from 'ui/shared/IconSvg';
import useMenuButtonColors from '../useMenuButtonColors';
type Props = {
address: string;
isAutoConnectDisabled?: boolean;
......@@ -14,8 +12,8 @@ type Props = {
};
const WalletIdenticon = ({ address, isAutoConnectDisabled, className }: Props) => {
const { themedBackgroundOrange } = useMenuButtonColors();
const isMobile = useIsMobile();
const borderColor = useColorModeValue('orange.100', 'orange.900');
return (
<Box className={ className } position="relative">
......@@ -31,7 +29,7 @@ const WalletIdenticon = ({ address, isAutoConnectDisabled, className }: Props) =
backgroundColor="rgba(16, 17, 18, 0.80)"
borderRadius="full"
border="1px solid"
borderColor={ themedBackgroundOrange }
borderColor={ borderColor }
>
<IconSvg
name="integration/partial"
......
import { Box, Button, Text, Flex, IconButton } from '@chakra-ui/react';
import { Box, Button, Text, Flex, IconButton, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import * as mixpanel from 'lib/mixpanel/index';
......@@ -6,8 +6,6 @@ import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import IconSvg from 'ui/shared/IconSvg';
import useMenuButtonColors from '../useMenuButtonColors';
type Props = {
address?: string;
ensDomainName?: string | null;
......@@ -18,7 +16,7 @@ type Props = {
};
const WalletMenuContent = ({ address, ensDomainName, disconnect, isAutoConnectDisabled, openWeb3Modal, closeWalletMenu }: Props) => {
const { themedBackgroundOrange } = useMenuButtonColors();
const bgColor = useColorModeValue('orange.100', 'orange.900');
const [ isModalOpening, setIsModalOpening ] = React.useState(false);
const onAddressClick = React.useCallback(() => {
......@@ -39,7 +37,7 @@ const WalletMenuContent = ({ address, ensDomainName, disconnect, isAutoConnectDi
p={ 3 }
mb={ 3 }
alignItems="center"
backgroundColor={ themedBackgroundOrange }
backgroundColor={ bgColor }
>
<IconSvg
name="integration/partial"
......
import type { ButtonProps } from '@chakra-ui/react';
import { PopoverContent, PopoverBody, PopoverTrigger, Button, Box, useBoolean, chakra, useColorModeValue } from '@chakra-ui/react';
import { PopoverContent, PopoverBody, PopoverTrigger, Button, Box, useBoolean, chakra } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
......@@ -13,7 +12,6 @@ import IconSvg from 'ui/shared/IconSvg';
import useWallet from 'ui/snippets/walletMenu/useWallet';
import WalletMenuContent from 'ui/snippets/walletMenu/WalletMenuContent';
import useMenuButtonColors from '../useMenuButtonColors';
import WalletIdenticon from './WalletIdenticon';
import WalletTooltip from './WalletTooltip';
......@@ -37,7 +35,6 @@ export const WalletMenuDesktop = ({
isHomePage, className, size = 'md', isWalletConnected, address, connect,
disconnect, isModalOpening, isModalOpen, openModal,
}: ComponentProps) => {
const { themedBackground, themedBackgroundOrange, themedBorderColor, themedColor } = useMenuButtonColors();
const [ isPopoverOpen, setIsPopoverOpen ] = useBoolean(false);
const isMobile = useIsMobile();
const { isAutoConnectDisabled } = useMarketplaceContext();
......@@ -51,36 +48,6 @@ export const WalletMenuDesktop = ({
},
});
const variant = React.useMemo(() => {
if (isWalletConnected) {
return 'subtle';
}
return isHomePage ? 'solid' : 'outline';
}, [ isWalletConnected, isHomePage ]);
const themedColorForOrangeBg = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
let buttonStyles: Partial<ButtonProps> = {};
if (isWalletConnected) {
const backgroundColor = isAutoConnectDisabled ? themedBackgroundOrange : themedBackground;
const color = isAutoConnectDisabled ? themedColorForOrangeBg : themedColor;
buttonStyles = {
bg: isHomePage ? 'blue.50' : backgroundColor,
color: isHomePage ? 'blackAlpha.800' : color,
_hover: {
color: isHomePage ? 'blackAlpha.800' : color,
},
};
} else if (isHomePage) {
buttonStyles = {
color: 'white',
};
} else {
buttonStyles = {
borderColor: themedBorderColor,
color: themedColor,
};
}
const openPopover = React.useCallback(() => {
mixpanel.logEvent(mixpanel.EventTypes.WALLET_ACTION, { Action: 'Open' });
setIsPopoverOpen.toggle();
......@@ -103,8 +70,9 @@ export const WalletMenuDesktop = ({
>
<Button
className={ className }
variant={ variant }
colorScheme="blue"
variant={ isHomePage ? 'hero' : 'header' }
data-selected={ isWalletConnected }
data-warning={ isAutoConnectDisabled }
flexShrink={ 0 }
isLoading={
((isModalOpening || isModalOpen) && !isWalletConnected) ||
......@@ -115,7 +83,6 @@ export const WalletMenuDesktop = ({
fontSize="sm"
size={ size }
px={{ lg: isHomePage ? 2 : 4, xl: 4 }}
{ ...buttonStyles }
>
{ isWalletConnected ? (
<>
......
......@@ -10,7 +10,6 @@ import IconSvg from 'ui/shared/IconSvg';
import useWallet from 'ui/snippets/walletMenu/useWallet';
import WalletMenuContent from 'ui/snippets/walletMenu/WalletMenuContent';
import useMenuButtonColors from '../useMenuButtonColors';
import WalletIdenticon from './WalletIdenticon';
import WalletTooltip from './WalletTooltip';
......@@ -28,7 +27,6 @@ export const WalletMenuMobile = (
{ isWalletConnected, address, connect, disconnect, isModalOpening, isModalOpen, openModal }: ComponentProps,
) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { themedBackground, themedBackgroundOrange, themedBorderColor, themedColor } = useMenuButtonColors();
const isMobile = useIsMobile();
const { isAutoConnectDisabled } = useMarketplaceContext();
const addressDomainQuery = useApiQuery('address_domain', {
......@@ -46,8 +44,6 @@ export const WalletMenuMobile = (
onOpen();
}, [ onOpen ]);
const themedBg = isAutoConnectDisabled ? themedBackgroundOrange : themedBackground;
return (
<>
<WalletTooltip
......@@ -62,13 +58,11 @@ export const WalletMenuMobile = (
<WalletIdenticon address={ address } isAutoConnectDisabled={ isAutoConnectDisabled }/> :
<IconSvg name="wallet" boxSize={ 6 } p={ 0.5 }/>
}
variant={ isWalletConnected ? 'subtle' : 'outline' }
colorScheme="gray"
variant="header"
data-selected={ isWalletConnected }
data-warning={ isAutoConnectDisabled }
boxSize="40px"
flexShrink={ 0 }
bg={ isWalletConnected ? themedBg : undefined }
color={ themedColor }
borderColor={ !isWalletConnected ? themedBorderColor : undefined }
onClick={ isWalletConnected ? openPopover : connect }
isLoading={
((isModalOpening || isModalOpen) && !isWalletConnected) ||
......
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