Chain menu updates (#3127)

* UI/UX: Dropdown DeFi button fixes

Resolves #3104

* move chain menu to the right of the topbar

* add list mode to chains menu

* rollback price diff on mobile
This commit is contained in:
tom goriunov
2025-11-14 11:39:15 +01:00
committed by GitHub
parent 2aecde7104
commit 770980044d
32 changed files with 182 additions and 170 deletions
+1
View File
@@ -60,6 +60,7 @@ const UI = Object.freeze({
featuredNetworks: {
items: getExternalAssetFilePath('NEXT_PUBLIC_FEATURED_NETWORKS'),
allLink: getEnvValue('NEXT_PUBLIC_FEATURED_NETWORKS_ALL_LINK'),
mode: (getEnvValue('NEXT_PUBLIC_FEATURED_NETWORKS_MODE') || 'tabs') as 'tabs' | 'list',
},
footer: {
links: getExternalAssetFilePath('NEXT_PUBLIC_FOOTER_LINKS'),
+25 -16
View File
@@ -9,45 +9,53 @@ NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
NEXT_PUBLIC_ROLLUP_STAGE_INDEX=1
NEXT_PUBLIC_IS_TESTNET=false
NEXT_PUBLIC_FEATURED_NETWORKS_MODE=list
NEXT_PUBLIC_FEATURED_NETWORKS_ALL_LINK=https://chains.blockscout.com/
# Instance ENVs
NEXT_PUBLIC_AD_BANNER_ENABLE_SPECIFY=true
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS=['talentprotocol','drops','blockscoutbadges','gitpoap','efp','etherscore','webacy','humanpassport','trustblock','smartmuv','humanode','deepdao']
NEXT_PUBLIC_ADDRESS_3RD_PARTY_WIDGETS_CONFIG_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/widgets/config.json
NEXT_PUBLIC_ADMIN_SERVICE_API_HOST=https://admin-rs.services.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=optimism.blockscout.com
NEXT_PUBLIC_API_HOST=explorer.optimism.io
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
NEXT_PUBLIC_CONTRACT_CODE_IDES=[{'title':'Remix IDE','url':'https://remix.ethereum.org/?address={hash}&blockscout={domain}','icon_url':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/ide-icons/remix.png'}]
NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://contracts-info.services.blockscout.com
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swapscout','icon':'swap','dappId':'swapscout'},{'text':'Revokescout','icon':'integration/partial','dappId':'revokescout'},{'text':'Payment link','icon':'payment_link','dappId':'peanut-protocol'}]
NEXT_PUBLIC_DEFI_DROPDOWN_ITEMS=[{'text':'Swap','icon':'swap','url':'https://explorer.optimism.io/essential-dapps/swap?utm_source=defi-dropdown&utm_medium=swap'},{'text':'Revoke','icon':'integration/partial','dappId':'revokescout'}, {'text': 'Multisend', 'icon': 'token', 'url': 'https://explorer.optimism.io/essential-dapps/multisend?utm_source=defi-dropdown-multisend&utm_medium=multisend'}]
NEXT_PUBLIC_DEX_POOLS_ENABLED=true
NEXT_PUBLIC_FAULT_PROOF_ENABLED=true
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/optimism-mainnet.json
NEXT_PUBLIC_FLASHBLOCKS_SOCKET_URL=wss://explorer.optimism.io/api/v2/proxy/3rdparty/ws_opm
NEXT_PUBLIC_FOOTER_LINKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/footer-links/optimism.json
NEXT_PUBLIC_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/sherblockHolmesBadge
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'url_template': 'https://smolrefuel.com/?outboundChain={chainId}&partner=blockscout&utm_source=blockscout&disableBridges=true', 'dapp_id': 'smol-refuel', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'}
NEXT_PUBLIC_GAS_REFUEL_PROVIDER_CONFIG={'name': 'Need gas?', 'url_template': 'https://www.gas.zip?utm_source=blockscout', 'logo': 'https://blockscout-content.s3.amazonaws.com/smolrefuel-logo-action-button.png'}
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x97f34a4cf685e365460dd38dbe16e092d8e4cc4b6ac779e3abcf4c18df6b1329
NEXT_PUBLIC_HAS_CONTRACT_AUDIT_REPORTS=true
NEXT_PUBLIC_HAS_USER_OPS=true
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs', 'coin_price', 'market_cap', 'secondary_coin_price']
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_operational_txs','coin_price','market_cap','secondary_coin_price']
NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['linear-gradient(90deg, rgb(232, 52, 53) 0%, rgb(139, 28, 232) 100%)'],'text_color':['rgb(255, 255, 255)']}
NEXT_PUBLIC_HOMEPAGE_STATS=['total_blocks','average_block_time','total_operational_txs','total_txs','wallet_addresses','gas_tracker']
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=<p>Joined recent campaigns? Mint your Merit Badge <a href="https://badges.blockscout.com?utm_source=instance&utm_medium=optimism">here</a></p>
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/974c47f86a3158c1a86b092ae2f044b3/raw/abcc7e02150cd85d4974503a0357162c0a2c35a9/merits-banner.html
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://optimism.blockscout.com/apps/swapscout
NEXT_PUBLIC_MAINTENANCE_ALERT_MESSAGE=<p>Launch your own fully functioning blockchain explorer in minutes. <a href='https://deploy.blockscout.com/?utm_source=blockscout_ad'>Deploy now</a></p>
NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL=https://gist.githubusercontent.com/0xdeval/1a7395339d20b603b9c1d0ee929f23dc/raw/55e2eebef3ba3ef064842d15555b59d5ba8cbcc7/badges-banner.html
NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL=https://badges.blockscout.com/badges-list?utm_source=blockscout-explorer&utm_medium=marketplace-banner
NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json
NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_ESSENTIAL_DAPPS_CONFIG={'swap': {'chains': ['1', '10', '30', '100', '122', '130', '137', '324', '480', '1135', '1514', '1868', '8453', '13371', '42161', '42220', '57073', '534352', '11155111', '1313161554', '1923', '42793', '59144'], 'fee': '0.004', 'integrator': 'blockscout'}, 'multisend': {'chains': ['1', '10', '30', '100', '122', '130', '137', '324', '480', '1135', '1514', '1868', '8453', '13371', '42161', '42220', '57073', '534352', '11155111', '1313161554', '59144', '7000'], 'posthogKey': 'phc_7O4WGsecqqDO1PeaKayHAxUWN1PjheOmQCiDxEMcmkx', 'posthogHost': 'https://us.i.posthog.com'}}
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_METASUITES_ENABLED=true
NEXT_PUBLIC_MIXPANEL_CONFIG_OVERRIDES={"record_sessions_percent": 0.5,"record_heatmap_data": true}
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG=[{'name': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}]
NEXT_PUBLIC_NAME_SERVICE_API_HOST=https://bens.services.blockscout.com
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/pools']
NEXT_PUBLIC_NAVIGATION_HIGHLIGHTED_ROUTES=['/apps']
NEXT_PUBLIC_NAVIGATION_PROMO_BANNER_CONFIG={"img_url": {"small": "https://blockscout-merits-images.s3.us-east-1.amazonaws.com/banners/sidemenu-banner-small.png", "large": "https://blockscout-merits-images.s3.us-east-1.amazonaws.com/banners/sidemenu-banner-big.png"}, "link_url": "https://www.blockscout.com/?utm_source=blockscout&utm_medium=side-menu-banner"}
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Moralis','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/moralis.png','baseUrl':'https://moralis.com/','paths':{'token':'/chain/optimism/token/price'}},{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/optimism/pools'}}, {'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/optimistic'}},{'title':'3xpl','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/3xpl.png','baseUrl':'https://3xpl.com/','paths':{'tx':'/optimism/transaction','address':'/optimism/address'}}]
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Moralis','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/moralis.png','baseUrl':'https://explorer.moralis.com/','paths':{'token':'/chain/optimism/token/price'}},{'title':'GeckoTerminal','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/geckoterminal.png','baseUrl':'https://www.geckoterminal.com/','paths':{'token':'/optimism/pools'}}, {'title':'Tenderly','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/tenderly.png','baseUrl':'https://dashboard.tenderly.co','paths':{'tx':'/tx/optimistic'}},{'title':'3xpl','logo':'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/explorer-logos/3xpl.png','baseUrl':'https://3xpl.com/','paths':{'tx':'/optimism/transaction','address':'/optimism/address'}}]
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism-mainnet-light.svg
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism-mainnet-dark.svg
NEXT_PUBLIC_NETWORK_ID=10
@@ -56,21 +64,22 @@ NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/front
NEXT_PUBLIC_NETWORK_NAME=OP Mainnet
NEXT_PUBLIC_NETWORK_RPC_URL=https://mainnet.optimism.io
NEXT_PUBLIC_NETWORK_SECONDARY_COIN_SYMBOL=OP
NEXT_PUBLIC_NETWORK_SHORT_NAME=OP Mainnet
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_OG_IMAGE_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/og-images/optimism-mainnet.png
NEXT_PUBLIC_OTHER_LINKS=[{'url':'https://optimism.drpc.org?ref=559183','text':'Public RPC'}]
NEXT_PUBLIC_PUZZLE_GAME_BADGE_CLAIM_LINK=https://badges.blockscout.com/mint/capyPuzzleBadge
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth.blockscout.com/
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_ROLLUP_STAGE_INDEX=1
NEXT_PUBLIC_ROLLUP_TYPE=optimistic
NEXT_PUBLIC_SAFE_TX_SERVICE_URL=https://safe-transaction-optimism.safe.global
NEXT_PUBLIC_STATS_API_BASE_PATH=/stats-service
NEXT_PUBLIC_STATS_API_HOST=https://optimism.blockscout.com
NEXT_PUBLIC_STATS_API_HOST=https://explorer.optimism.io
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_USER_OPS_INDEXER_API_HOST=https://user-ops-indexer-optimism-mainnet.k8s-prod-3.blockscout.com
NEXT_PUBLIC_VIEWS_CONTRACT_LANGUAGE_FILTERS=['solidity','vyper','yul','geas']
NEXT_PUBLIC_VIEWS_CONTRACT_SOLIDITYSCAN_ENABLED=true
NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED=true
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_WEB3_WALLETS=['token_pocket', 'metamask']
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_operational_txs']
NEXT_PUBLIC_HOMEPAGE_STATS=['latest_batch','average_block_time','total_operational_txs','wallet_addresses','gas_tracker']
NEXT_PUBLIC_XSTAR_SCORE_URL=https://docs.xname.app/the-solution-adaptive-proof-of-humanity-on-blockchain/xhs-scoring-algorithm?utm_source=blockscout&utm_medium=address
+7
View File
@@ -899,6 +899,13 @@ const schema = yup
then: (schema) => schema.test(urlTest),
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_FEATURED_NETWORKS_ALL_LINK can only be set when NEXT_PUBLIC_FEATURED_NETWORKS is configured'),
}),
NEXT_PUBLIC_FEATURED_NETWORKS_MODE: yup
.string()
.when('NEXT_PUBLIC_FEATURED_NETWORKS', {
is: (value: Array<unknown> | undefined) => value && value.length > 0,
then: (schema) => schema.oneOf([ 'tabs', 'list' ]),
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_FEATURED_NETWORKS_MODE can only be set when NEXT_PUBLIC_FEATURED_NETWORKS is configured'),
}),
NEXT_PUBLIC_OTHER_LINKS: yup
.array()
.transform(replaceQuotes)
@@ -23,6 +23,7 @@ NEXT_PUBLIC_CONTRACT_INFO_API_HOST=https://example.com
NEXT_PUBLIC_DATA_AVAILABILITY_ENABLED=true
NEXT_PUBLIC_FEATURED_NETWORKS=https://example.com
NEXT_PUBLIC_FEATURED_NETWORKS_ALL_LINK=https://example.com
NEXT_PUBLIC_FEATURED_NETWORKS_MODE=list
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'}
+1
View File
@@ -174,6 +174,7 @@ _Note_ Here, all values are arrays of up to two strings. The first string repres
| --- | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_FEATURED_NETWORKS | `string` | URL of configuration file (`.json` format only) or file content string representation. It contains list of featured networks that will be shown in the network menu. See [below](#featured-network-configuration-properties) list of available properties for particular network | - | - | `https://example.com/featured_networks_config.json` \| `[{'title':'Astar(EVM)','url':'https://astar.blockscout.com/','group':'Mainnets','icon':'https://example.com/astar.svg'}]` | v1.0.x+ |
| NEXT_PUBLIC_FEATURED_NETWORKS_ALL_LINK | `string` | Link to the all chains resource. Will be displayed at the bottom of featured networks list. | Works only if NEXT_PUBLIC_FEATURED_NETWORKS is set | - | `https://example.com` | v2.3.0+ |
| NEXT_PUBLIC_FEATURED_NETWORKS_MODE | `tabs \| list` | Indicates how the networks are presented: in one list or in separate tabs. | Works only if NEXT_PUBLIC_FEATURED_NETWORKS is set | `tabs` | `list` | upcoming |
#### Featured network configuration properties
-4
View File
@@ -3,13 +3,9 @@ import type { FeaturedNetwork } from 'types/networks';
export const FEATURED_NETWORKS: Array<FeaturedNetwork> = [
{ title: 'Gnosis Chain', url: 'https://blockscout.com/xdai/mainnet', group: 'Mainnets', isActive: true },
{ title: 'Arbitrum on xDai', url: 'https://blockscout.com/xdai/aox', group: 'Mainnets' },
{ title: 'Ethereum', url: 'https://blockscout.com/eth/mainnet', group: 'Mainnets' },
{ title: 'Ethereum Classic', url: 'https://blockscout.com/etx/mainnet', group: 'Mainnets', icon: 'https://localhost:3000/my-logo.png' },
{ title: 'POA', url: 'https://blockscout.com/poa/core', group: 'Mainnets' },
{ title: 'RSK', url: 'https://blockscout.com/rsk/mainnet', group: 'Mainnets' },
{ title: 'Gnosis Chain Testnet', url: 'https://blockscout.com/xdai/testnet', group: 'Testnets' },
{ title: 'POA Sokol', url: 'https://blockscout.com/poa/sokol', group: 'Testnets' },
{ title: 'ARTIS Σ1', url: 'https://blockscout.com/artis/sigma1', group: 'Other' },
{ title: 'LUKSO L14', url: 'https://blockscout.com/lukso/l14', group: 'Other' },
{ title: 'Astar', url: 'https://blockscout.com/astar', group: 'Other' },
];
-12
View File
@@ -1,16 +1,12 @@
import type { BrowserContext } from '@playwright/test';
import React from 'react';
import { FEATURED_NETWORKS } from 'mocks/config/network';
import { contextWithAuth } from 'playwright/fixtures/auth';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect, devices } from 'playwright/lib';
import Burger from './Burger';
const FEATURED_NETWORKS_URL = 'https://localhost:3000/featured-networks.json';
const LOGO_URL = 'https://localhost:3000/my-logo.png';
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
const hooksConfig = {
@@ -21,14 +17,6 @@ const hooksConfig = {
},
};
test.beforeEach(async({ mockEnvs, mockConfigResponse, mockAssetResponse }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL ],
]);
await mockConfigResponse('NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL, FEATURED_NETWORKS);
await mockAssetResponse(LOGO_URL, './playwright/mocks/image_s.jpg');
});
test('base view', async({ render, page }) => {
const component = await render(<Burger/>, { hooksConfig });
@@ -4,7 +4,6 @@ import React from 'react';
import config from 'configs/app';
import * as cookies from 'lib/cookies';
import { FEATURED_NETWORKS } from 'mocks/config/network';
import { contextWithAuth } from 'playwright/fixtures/auth';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
@@ -20,16 +19,6 @@ const hooksConfig = {
},
};
const FEATURED_NETWORKS_URL = 'https://localhost:3000/featured-networks.json';
test.beforeEach(async({ mockEnvs, mockConfigResponse }) => {
await mockEnvs([
...ENVS_MAP.rewardsService,
[ 'NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL ],
]);
await mockConfigResponse('NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL, FEATURED_NETWORKS);
});
test.describe('no auth', () => {
let component: Locator;
+42 -27
View File
@@ -1,4 +1,4 @@
import { VStack, Flex, Box } from '@chakra-ui/react';
import { VStack, Flex, Box, Text } from '@chakra-ui/react';
import React from 'react';
import type { FeaturedNetwork, NetworkGroup } from 'types/networks';
@@ -55,6 +55,20 @@ const NetworkMenuContent = ({ items, tabs }: Props) => {
);
}
const viewAllLink = config.UI.featuredNetworks.allLink && (
<Link
href={ config.UI.featuredNetworks.allLink }
external
noIcon
variant="secondary"
my={ 2 }
px={ 2 }
fontSize="xs"
>
View all chains
</Link>
);
if (tabs.length === 1) {
return (
<VStack as="ul" gap={ 1 } alignItems="stretch" overflowY="scroll" maxH="516px">
@@ -66,19 +80,32 @@ const NetworkMenuContent = ({ items, tabs }: Props) => {
{ ...network }
/>
)) }
{ config.UI.featuredNetworks.allLink && (
<Link
href={ config.UI.featuredNetworks.allLink }
external
noIcon
variant="secondary"
my={ 2 }
px={ 2 }
fontSize="xs"
>
View all chains
</Link>
) }
{ viewAllLink }
</VStack>
);
}
if (config.UI.featuredNetworks.mode === 'list') {
return (
<VStack overflowY="scroll" maxH="516px" alignItems="stretch" gap={ 3 }>
{ tabs.map((tab, index) => {
return (
<Box key={ tab }>
<Text fontSize="sm" fontWeight={ 600 } mb={ 2 }>{ tab }</Text>
<VStack key={ tab } as="ul" gap={ 1 } alignItems="stretch">
{ items
.filter((network) => network.group === tab)
.map((network) => (
<NetworkMenuLink
key={ network.title }
{ ...network }
/>
)) }
{ index === tabs.length - 1 && viewAllLink }
</VStack>
</Box>
);
}) }
</VStack>
);
}
@@ -118,19 +145,7 @@ const NetworkMenuContent = ({ items, tabs }: Props) => {
{ ...network }
/>
)) }
{ config.UI.featuredNetworks.allLink && (
<Link
href={ config.UI.featuredNetworks.allLink }
external
noIcon
variant="secondary"
my={ 2 }
px={ 2 }
fontSize="xs"
>
View all chains
</Link>
) }
{ viewAllLink }
</VStack>
</TabsContent>
)) }
+40
View File
@@ -0,0 +1,40 @@
import React from 'react';
import { FEATURED_NETWORKS } from 'mocks/config/network';
import { test, expect } from 'playwright/lib';
import NetworkMenu from './NetworkMenu';
test('tabs +@dark-mode', async({ render, mockEnvs, mockConfigResponse, mockAssetResponse, page }) => {
const FEATURED_NETWORKS_URL = 'https://localhost:3000/featured-networks.json';
await mockEnvs([
[ 'NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL ],
[ 'NEXT_PUBLIC_FEATURED_NETWORKS_ALL_LINK', 'https://example.com' ],
]);
await mockConfigResponse('NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL, FEATURED_NETWORKS);
await mockAssetResponse('https://localhost:3000/my-logo.png', './playwright/mocks/image_s.jpg');
const component = await render(<NetworkMenu/>);
await component.getByLabel('Network menu').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 300, height: 500 } });
await page.getByRole('link', { name: 'Arbitrum' }).hover();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 300, height: 500 } });
});
test('list +@dark-mode', async({ render, mockEnvs, mockConfigResponse, mockAssetResponse, page }) => {
const FEATURED_NETWORKS_URL = 'https://localhost:3000/featured-networks.json';
await mockEnvs([
[ 'NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL ],
[ 'NEXT_PUBLIC_FEATURED_NETWORKS_ALL_LINK', 'https://example.com' ],
[ 'NEXT_PUBLIC_FEATURED_NETWORKS_MODE', 'list' ],
]);
await mockConfigResponse('NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL, FEATURED_NETWORKS);
await mockAssetResponse('https://localhost:3000/my-logo.png', './playwright/mocks/image_s.jpg');
const component = await render(<NetworkMenu/>);
await component.getByLabel('Network menu').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 300, height: 500 } });
});
+15 -29
View File
@@ -1,28 +1,17 @@
import { Flex } from '@chakra-ui/react';
import React from 'react';
import { IconButton } from 'toolkit/chakra/icon-button';
import { Button } from 'toolkit/chakra/button';
import { PopoverRoot, PopoverTrigger } from 'toolkit/chakra/popover';
import { Tooltip } from 'toolkit/chakra/tooltip';
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
import IconSvg from 'ui/shared/IconSvg';
import NetworkMenuContent from 'ui/snippets/networkMenu/NetworkMenuContent';
import useNetworkMenu from 'ui/snippets/networkMenu/useNetworkMenu';
const NetworkMenu = () => {
const menu = useNetworkMenu();
const tooltip = useDisclosure();
const handlePopoverOpenChange = React.useCallback(({ open }: { open: boolean }) => {
open && tooltip.onClose();
menu.onOpenChange({ open });
}, [ menu, tooltip ]);
const handleTooltipOpenChange = React.useCallback(({ open }: { open: boolean }) => {
if (!menu.open) {
tooltip.onOpenChange({ open });
}
}, [ menu, tooltip ]);
}, [ menu ]);
return (
<PopoverRoot
@@ -31,22 +20,19 @@ const NetworkMenu = () => {
open={ menu.open }
onOpenChange={ handlePopoverOpenChange }
>
<Tooltip content="Chains list" disableOnMobile open={ tooltip.open } onOpenChange={ handleTooltipOpenChange }>
<Flex alignItems="center">
<PopoverTrigger>
<IconButton
variant="link"
boxSize={ 5 }
aria-label="Network menu"
borderRadius="sm"
onClick={ menu.onToggle }
p={ 0.5 }
>
<IconSvg name="networks" boxSize="full"/>
</IconButton>
</PopoverTrigger>
</Flex>
</Tooltip>
<PopoverTrigger>
<Button
variant="link"
aria-label="Network menu"
onClick={ menu.onToggle }
size="2xs"
fontWeight={ 500 }
>
<IconSvg name="networks" boxSize="14px"/>
<span>Chains</span>
<IconSvg name="arrows/east-mini" boxSize={ 4 } transform="rotate(-90deg)"/>
</Button>
</PopoverTrigger>
<NetworkMenuContent items={ menu.data } tabs={ menu.availableTabs }/>
</PopoverRoot>
);
-20
View File
@@ -1,6 +1,5 @@
import React from 'react';
import { FEATURED_NETWORKS } from 'mocks/config/network';
import * as statsMock from 'mocks/stats/index';
import { test, expect } from 'playwright/lib';
@@ -40,25 +39,6 @@ test('with secondary coin price +@mobile', async({ render, mockApiResponse }) =>
await expect(component).toHaveScreenshot();
});
test('with network menu +@dark-mode +@mobile', async({ render, mockApiResponse, mockEnvs, mockConfigResponse, mockAssetResponse, page }) => {
const FEATURED_NETWORKS_URL = 'https://localhost:3000/featured-networks.json';
await mockApiResponse('general:stats', statsMock.base);
await mockEnvs([
[ 'NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL ],
[ 'NEXT_PUBLIC_FEATURED_NETWORKS_ALL_LINK', 'https://example.com' ],
]);
await mockConfigResponse('NEXT_PUBLIC_FEATURED_NETWORKS', FEATURED_NETWORKS_URL, FEATURED_NETWORKS);
await mockAssetResponse('https://localhost:3000/my-logo.png', './playwright/mocks/image_s.jpg');
const component = await render(<TopBar/>);
await component.getByLabel('Network menu').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 500 } });
await page.getByRole('link', { name: 'POA' }).hover();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 500 } });
});
test('with DeFi dropdown +@dark-mode +@mobile', async({ render, page, mockApiResponse, mockEnvs }) => {
await mockEnvs([
[
+6 -9
View File
@@ -1,4 +1,4 @@
import { Flex, Separator, Box } from '@chakra-ui/react';
import { Flex, Separator, Box, HStack } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
@@ -21,21 +21,18 @@ const TopBar = () => {
alignItems="center"
maxW={ `${ CONTENT_MAX_WIDTH }px` }
>
{ !config.features.opSuperchain.isEnabled ? <TopBarStats/> : <div/> }
<HStack gap={ 0 } fontSize="xs">
{ Boolean(config.UI.featuredNetworks.items) && <NetworkMenu/> }
{ !config.features.opSuperchain.isEnabled ? <TopBarStats/> : <div/> }
</HStack>
<Flex alignItems="center">
{ config.features.deFiDropdown.isEnabled && (
<>
<DeFiDropdown/>
<Separator mr={ 3 } ml={{ base: 2, sm: 3 }} height={ 4 } orientation="vertical"/>
<Separator mr={ 3 } ml={{ base: 2, sm: 3 }} height={ 4 } orientation="vertical" borderColor="border.divider"/>
</>
) }
<Settings/>
{ Boolean(config.UI.featuredNetworks.items) && (
<>
<Separator mx={ 3 } height={ 4 } orientation="vertical"/>
<NetworkMenu/>
</>
) }
</Flex>
</Flex>
</Box>
+44 -42
View File
@@ -51,52 +51,54 @@ const TopBarStats = () => {
const hasNativeCoinPrice = data?.coin_price && !config.UI.nativeCoinPrice.isHidden;
const hasSecondaryCoinPrice = data?.secondary_coin_price && config.chain.secondaryCoin.symbol && (hasNativeCoinPrice ? !isMobile : true);
const hasGasInfo = data?.gas_prices && data.gas_prices.average !== null && config.features.gasTracker.isEnabled;
const hasGasInfo = data?.gas_prices && data.gas_prices.average !== null && config.features.gasTracker.isEnabled && !isMobile;
return (
<Flex
alignItems="center"
fontSize="xs"
fontWeight={ 500 }
>
{ hasNativeCoinPrice && (
<Flex columnGap={ 1 }>
<Skeleton loading={ isPlaceholderData }>
<chakra.span color="text.secondary">{ config.chain.currency.symbol } </chakra.span>
<span>${ Number(data.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }) }</span>
</Skeleton>
{ data.coin_price_change_percentage && (
<>
{ Boolean(config.UI.featuredNetworks.items) && <TextSeparator/> }
<Flex
alignItems="center"
fontWeight={ 500 }
>
{ hasNativeCoinPrice && (
<Flex columnGap={ 1 }>
<Skeleton loading={ isPlaceholderData }>
<chakra.span color={ Number(data.coin_price_change_percentage) >= 0 ? 'green.500' : 'red.500' }>
{ Number(data.coin_price_change_percentage).toFixed(2) }%
</chakra.span>
<chakra.span color="text.secondary">{ config.chain.currency.symbol } </chakra.span>
<span>${ Number(data.coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }) }</span>
</Skeleton>
) }
</Flex>
) }
{ hasSecondaryCoinPrice && (
<Flex columnGap={ 1 } ml={ data?.coin_price ? 3 : 0 }>
<Skeleton loading={ isPlaceholderData }>
<chakra.span color="text.secondary">{ config.chain.secondaryCoin.symbol } </chakra.span>
<span>${ Number(data.secondary_coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }) }</span>
</Skeleton>
</Flex>
) }
{ (hasNativeCoinPrice || hasSecondaryCoinPrice) && hasGasInfo && <TextSeparator/> }
{ hasGasInfo && (
<>
<Skeleton loading={ isPlaceholderData } display={{ base: 'none', lg: 'inline-flex' }} whiteSpace="pre-wrap">
<chakra.span color="text.secondary">Gas </chakra.span>
<GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt } placement={ !data?.coin_price ? 'bottom-start' : undefined }>
<Link>
<GasPrice data={ data.gas_prices?.average ?? null }/>
</Link>
</GasInfoTooltip>
</Skeleton>
{ !isPlaceholderData && <GetGasButton/> }
</>
) }
</Flex>
{ data.coin_price_change_percentage && !(isMobile && Boolean(config.UI.featuredNetworks.items)) && (
<Skeleton loading={ isPlaceholderData }>
<chakra.span color={ Number(data.coin_price_change_percentage) >= 0 ? 'green.500' : 'red.500' }>
{ Number(data.coin_price_change_percentage).toFixed(2) }%
</chakra.span>
</Skeleton>
) }
</Flex>
) }
{ hasSecondaryCoinPrice && (
<Flex columnGap={ 1 } ml={ data?.coin_price ? 3 : 0 }>
<Skeleton loading={ isPlaceholderData }>
<chakra.span color="text.secondary">{ config.chain.secondaryCoin.symbol } </chakra.span>
<span>${ Number(data.secondary_coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }) }</span>
</Skeleton>
</Flex>
) }
{ (hasNativeCoinPrice || hasSecondaryCoinPrice) && hasGasInfo && <TextSeparator/> }
{ hasGasInfo && (
<>
<Skeleton loading={ isPlaceholderData } whiteSpace="pre-wrap">
<chakra.span color="text.secondary">Gas </chakra.span>
<GasInfoTooltip data={ data } dataUpdatedAt={ dataUpdatedAt } placement={ !data?.coin_price ? 'bottom-start' : undefined }>
<Link>
<GasPrice data={ data.gas_prices?.average ?? null }/>
</Link>
</GasInfoTooltip>
</Skeleton>
{ !isPlaceholderData && <GetGasButton/> }
</>
) }
</Flex>
</>
);
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB