diff --git a/configs/app/features/hotContracts.ts b/configs/app/features/hotContracts.ts new file mode 100644 index 0000000000..7141530fc1 --- /dev/null +++ b/configs/app/features/hotContracts.ts @@ -0,0 +1,21 @@ +import type { Feature } from './types'; + +import { getEnvValue } from '../utils'; + +const title = 'Hot contracts'; + +const config: Feature<{ isEnabled: true }> = (() => { + if (getEnvValue('NEXT_PUBLIC_HOT_CONTRACTS_ENABLED') === 'true') { + return Object.freeze({ + title, + isEnabled: true, + }); + } + + return Object.freeze({ + title, + isEnabled: false, + }); +})(); + +export default config; diff --git a/configs/app/features/index.ts b/configs/app/features/index.ts index 8112e7fe94..498acd9368 100644 --- a/configs/app/features/index.ts +++ b/configs/app/features/index.ts @@ -22,6 +22,7 @@ export { default as gasTracker } from './gasTracker'; export { default as getGasButton } from './getGasButton'; export { default as googleAnalytics } from './googleAnalytics'; export { default as growthBook } from './growthBook'; +export { default as hotContracts } from './hotContracts'; export { default as marketplace } from './marketplace'; export { default as megaEth } from './megaEth'; export { default as metasuites } from './metasuites'; diff --git a/configs/envs/.env.main b/configs/envs/.env.main index c596a5ed9c..68afa5df51 100644 --- a/configs/envs/.env.main +++ b/configs/envs/.env.main @@ -37,6 +37,7 @@ NEXT_PUBLIC_HAS_USER_OPS=true NEXT_PUBLIC_HELIA_VERIFIED_FETCH_ENABLED=false NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['rgba(51, 53, 67, 1)'],'text_color':['rgba(165, 252, 122, 1)']} +NEXT_PUBLIC_HOT_CONTRACTS_ENABLED=true NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=true NEXT_PUBLIC_IS_TESTNET=true NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-categories/default.json diff --git a/deploy/tools/envs-validator/schema.ts b/deploy/tools/envs-validator/schema.ts index 51a544607d..30a5f2287b 100644 --- a/deploy/tools/envs-validator/schema.ts +++ b/deploy/tools/envs-validator/schema.ts @@ -146,6 +146,7 @@ const schema = yup return isUndefined || valueSchema.isValidSync(data); }), NEXT_PUBLIC_FLASHBLOCKS_SOCKET_URL: yup.string().test(urlTest), + NEXT_PUBLIC_HOT_CONTRACTS_ENABLED: yup.boolean(), // Misc NEXT_PUBLIC_USE_NEXT_JS_PROXY: yup.boolean(), diff --git a/deploy/tools/envs-validator/test/.env.base b/deploy/tools/envs-validator/test/.env.base index 5537f1d82e..b5dd8c604a 100644 --- a/deploy/tools/envs-validator/test/.env.base +++ b/deploy/tools/envs-validator/test/.env.base @@ -38,6 +38,7 @@ NEXT_PUBLIC_MAX_CONTENT_WIDTH_ENABLED=false NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs'] NEXT_PUBLIC_HOMEPAGE_STATS=['total_blocks','average_block_time','total_txs','wallet_addresses','gas_tracker','current_epoch'] NEXT_PUBLIC_HOMEPAGE_HERO_BANNER_CONFIG={'background':['lightpink'],'text_color':['deepskyblue','white'],'border':['3px solid black']} +NEXT_PUBLIC_HOT_CONTRACTS_ENABLED=true NEXT_PUBLIC_GAS_TRACKER_ENABLED=true NEXT_PUBLIC_GAS_TRACKER_UNITS=['gwei'] NEXT_PUBLIC_IS_TESTNET=true diff --git a/docs/ENVS.md b/docs/ENVS.md index 7683668e1d..4b0bc5793a 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -988,6 +988,16 @@ This feature enables Blockscout Merits program. It requires that the [My account   +### Hot contract + +Show the page with aggregate metrics for the most popular contracts. + +| Variable | Type| Description | Compulsoriness | Default value | Example value | Version | +| --- | --- | --- | --- | --- | --- | --- | +| NEXT_PUBLIC_HOT_CONTRACTS_ENABLED | `boolean` | Set to true to enable the feature | Required | - | `true` | upcoming | + +  + ### Flashblocks This feature allows users to view [Flashblocks](https://docs.base.org/base-chain/flashblocks/apps)-related content in the explorer, including the Flashblocks real-time feed. It currently supports only Base chains. diff --git a/icons/gas.svg b/icons/gas.svg index 459b11b242..ad71b60e84 100644 --- a/icons/gas.svg +++ b/icons/gas.svg @@ -1,10 +1,3 @@ - - - - - - - - - + + diff --git a/icons/gas_slim.svg b/icons/gas_slim.svg new file mode 100644 index 0000000000..1d4d0f720e --- /dev/null +++ b/icons/gas_slim.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/icons/hot-contracts.svg b/icons/hot-contracts.svg new file mode 100644 index 0000000000..eec8214e14 --- /dev/null +++ b/icons/hot-contracts.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/api/services/general/misc.ts b/lib/api/services/general/misc.ts index d518a2ab34..ba3005f340 100644 --- a/lib/api/services/general/misc.ts +++ b/lib/api/services/general/misc.ts @@ -8,6 +8,7 @@ import type { Blob } from 'types/api/blobs'; import type { Block } from 'types/api/block'; import type { ChartMarketResponse, ChartSecondaryCoinPriceResponse, ChartTransactionResponse } from 'types/api/charts'; import type { BackendVersionConfig, CeloConfig, CsvExportConfig } from 'types/api/configs'; +import type { HotContractsFilters, HotContractsResponse, HotContractsSorting } from 'types/api/contracts'; import type { DepositsResponse, DepositsCounters } from 'types/api/deposits'; import type { CeloEpochDetails, CeloEpochElectionRewardDetailsResponse, CeloEpochListResponse } from 'types/api/epochs'; import type { IndexingStatus } from 'types/api/indexingStatus'; @@ -75,6 +76,11 @@ export const GENERAL_API_MISC_RESOURCES = { stats_charts_secondary_coin_price: { path: '/api/v2/stats/charts/secondary-coin-market', }, + stats_hot_contracts: { + path: '/api/v2/stats/hot-smart-contracts', + paginated: true, + filterFields: [ 'scale' as const ], + }, // HOMEPAGE homepage_blocks: { @@ -271,6 +277,7 @@ R extends 'general:stats' ? HomeStats : R extends 'general:stats_charts_txs' ? ChartTransactionResponse : R extends 'general:stats_charts_market' ? ChartMarketResponse : R extends 'general:stats_charts_secondary_coin_price' ? ChartSecondaryCoinPriceResponse : +R extends 'general:stats_hot_contracts' ? HotContractsResponse : R extends 'general:homepage_blocks' ? Array : R extends 'general:homepage_txs' ? Array : R extends 'general:homepage_txs_watchlist' ? Array : @@ -316,6 +323,7 @@ never; /* eslint-disable @stylistic/indent */ export type GeneralApiMiscPaginationFilters = +R extends 'general:stats_hot_contracts' ? HotContractsFilters : R extends 'general:search' ? SearchResultFilters : R extends 'general:user_ops' ? UserOpsFilters : R extends 'general:validators_stability' ? ValidatorsStabilityFilters : @@ -325,6 +333,7 @@ never; /* eslint-disable @stylistic/indent */ export type GeneralApiMiscPaginationSorting = +R extends 'general:stats_hot_contracts' ? HotContractsSorting : R extends 'general:validators_stability' ? ValidatorsStabilitySorting : R extends 'general:validators_blackfort' ? ValidatorsBlackfortSorting : never; diff --git a/lib/hooks/useNavItems.tsx b/lib/hooks/useNavItems.tsx index 96b3f58bbb..a76161643c 100644 --- a/lib/hooks/useNavItems.tsx +++ b/lib/hooks/useNavItems.tsx @@ -261,34 +261,34 @@ export default function useNavItems(): ReturnType { ].filter(Boolean); const statsNavItem = (() => { - const uptimeItem = { - text: 'Uptime', - nextRoute: { pathname: '/uptime' as const }, - icon: 'refresh_menu', - isActive: pathname.startsWith('/uptime'), - }; - - if (config.features.stats.isEnabled && config.features.megaEth.isEnabled) { - return { - text: 'Charts & stats', - icon: 'stats', - isActive: pathname.startsWith('/stats') || pathname.startsWith('/uptime'), - subItems: [ - { - text: `${ config.chain.name } stats`, - nextRoute: { pathname: '/stats' as const }, - icon: 'graph', - isActive: pathname.startsWith('/stats/'), - }, - uptimeItem, - ], - }; - } + const items = [ + config.features.stats.isEnabled && { + text: 'Chain stats', + nextRoute: { pathname: '/stats' as const }, + icon: 'graph', + isActive: pathname.startsWith('/stats'), + }, + config.features.megaEth.isEnabled && { + text: 'Uptime', + nextRoute: { pathname: '/uptime' as const }, + icon: 'refresh_menu', + isActive: pathname.startsWith('/uptime'), + }, + config.features.hotContracts.isEnabled && { + text: 'Hot contracts', + nextRoute: { pathname: '/hot-contracts' as const }, + icon: 'hot-contracts', + isActive: pathname.startsWith('/hot-contracts'), + }, + config.features.gasTracker.isEnabled && { + text: 'Gas tracker', + nextRoute: { pathname: '/gas-tracker' as const }, + icon: 'gas', + isActive: pathname.startsWith('/gas-tracker'), + }, + ].filter(Boolean); - if (!config.features.stats.isEnabled) { - if (config.features.megaEth.isEnabled) { - return uptimeItem; - } + if (items.length === 0) { return null; } @@ -296,7 +296,8 @@ export default function useNavItems(): ReturnType { text: 'Charts & stats', nextRoute: { pathname: '/stats' as const }, icon: 'stats', - isActive: pathname.startsWith('/stats'), + isActive: items.some(item => isInternalItem(item) && item.isActive), + subItems: items, }; })(); @@ -316,11 +317,6 @@ export default function useNavItems(): ReturnType { nextRoute: { pathname: '/contract-verification' as const }, isActive: pathname.startsWith('/contract-verification'), }, - config.features.gasTracker.isEnabled && { - text: 'Gas tracker', - nextRoute: { pathname: '/gas-tracker' as const }, - isActive: pathname.startsWith('/gas-tracker'), - }, config.features.publicTagsSubmission.isEnabled && { text: 'Submit public tag', nextRoute: { pathname: '/public-tags/submit' as const }, diff --git a/lib/metadata/getPageOgType.ts b/lib/metadata/getPageOgType.ts index 30495eb4d9..1301a88e5e 100644 --- a/lib/metadata/getPageOgType.ts +++ b/lib/metadata/getPageOgType.ts @@ -27,6 +27,7 @@ const OG_TYPE_DICT: Record = { '/stats': 'Root page', '/stats/[id]': 'Regular page', '/uptime': 'Root page', + '/hot-contracts': 'Root page', '/api-docs': 'Regular page', '/search-results': 'Regular page', '/auth/profile': 'Root page', diff --git a/lib/metadata/templates/description.ts b/lib/metadata/templates/description.ts index 164741a2e1..2098931b15 100644 --- a/lib/metadata/templates/description.ts +++ b/lib/metadata/templates/description.ts @@ -30,6 +30,7 @@ const TEMPLATE_MAP: Record = { '/stats': DEFAULT_TEMPLATE, '/stats/[id]': DEFAULT_TEMPLATE, '/uptime': DEFAULT_TEMPLATE, + '/hot-contracts': DEFAULT_TEMPLATE, '/api-docs': DEFAULT_TEMPLATE, '/search-results': DEFAULT_TEMPLATE, '/auth/profile': DEFAULT_TEMPLATE, diff --git a/lib/metadata/templates/title.ts b/lib/metadata/templates/title.ts index 65abde5d5e..fd88c3771f 100644 --- a/lib/metadata/templates/title.ts +++ b/lib/metadata/templates/title.ts @@ -31,6 +31,7 @@ const TEMPLATE_MAP: Record = { '/stats': '%network_name% stats - %network_name% network insights', '/stats/[id]': '%network_name% stats - %id% chart', '/uptime': '%network_name% uptime', + '/hot-contracts': '%network_name% hot contracts', '/api-docs': '%network_name% API docs - %network_name% developer tools', '/search-results': '%network_name% search result for %q%', '/auth/profile': '%network_name% - my profile', diff --git a/lib/mixpanel/getPageType.ts b/lib/mixpanel/getPageType.ts index c2e1491edd..d0f70b7212 100644 --- a/lib/mixpanel/getPageType.ts +++ b/lib/mixpanel/getPageType.ts @@ -25,6 +25,7 @@ export const PAGE_TYPE_DICT: Record = { '/stats': 'Stats', '/stats/[id]': 'Stats chart', '/uptime': 'Uptime', + '/hot-contracts': 'Hot contracts', '/api-docs': 'REST API', '/search-results': 'Search results', '/auth/profile': 'Profile', diff --git a/mocks/contracts/index.ts b/mocks/contracts/index.ts index 32441a5981..4c319baf71 100644 --- a/mocks/contracts/index.ts +++ b/mocks/contracts/index.ts @@ -1,4 +1,4 @@ -import type { VerifiedContract, VerifiedContractsResponse } from 'types/api/contracts'; +import type { HotContractsResponse, VerifiedContract, VerifiedContractsResponse } from 'types/api/contracts'; export const contract1: VerifiedContract = { address: { @@ -83,3 +83,34 @@ export const baseResponse: VerifiedContractsResponse = { smart_contract_id: '172', }, }; + +export const hotContractsResponse: HotContractsResponse = { + items: [ + { + contract_address: { ...contract1.address, name: null, reputation: 'scam' }, + balance: '1000000000000000000', + transactions_count: '1000', + total_gas_used: '100000000', + }, + { + contract_address: { + ...contract2.address, + metadata: { + reputation: null, + tags: [ + { tagType: 'protocol', name: 'Goose', slug: 'goose', ordinal: 1, meta: null }, + ], + }, + }, + balance: '420', + transactions_count: '42', + total_gas_used: '12343566', + }, + ], + next_page_params: { + items_count: '50', + transactions_count: '50', + total_gas_used: '50', + contract_address_hash: '50', + }, +}; diff --git a/nextjs/getServerSideProps/guards.ts b/nextjs/getServerSideProps/guards.ts index e5ff5e1b23..c2b6d6d53c 100644 --- a/nextjs/getServerSideProps/guards.ts +++ b/nextjs/getServerSideProps/guards.ts @@ -142,6 +142,14 @@ export const gasTracker: Guard = (chainConfig: typeof config) => async() => { } }; +export const hotContracts: Guard = (chainConfig: typeof config) => async() => { + if (!chainConfig.features.hotContracts.isEnabled) { + return { + notFound: true, + }; + } +}; + export const advancedFilter: Guard = (chainConfig: typeof config) => async() => { if (!chainConfig.features.advancedFilter.isEnabled) { return { diff --git a/nextjs/getServerSideProps/main.ts b/nextjs/getServerSideProps/main.ts index d0e16c6e2d..9a8c65c45a 100644 --- a/nextjs/getServerSideProps/main.ts +++ b/nextjs/getServerSideProps/main.ts @@ -22,6 +22,7 @@ export const accountsLabelSearch = factory([ guards.accountsLabelSearch ]); export const validators = factory([ guards.validators ]); export const validatorDetails = factory([ guards.validatorDetails ]); export const gasTracker = factory([ guards.gasTracker ]); +export const hotContracts = factory([ guards.hotContracts ]); export const advancedFilter = factory([ guards.advancedFilter ]); export const dataAvailability = factory([ guards.dataAvailability ]); export const login = factory([ guards.login ]); diff --git a/nextjs/nextjs-routes.d.ts b/nextjs/nextjs-routes.d.ts index cd7d01380d..270f5259d1 100644 --- a/nextjs/nextjs-routes.d.ts +++ b/nextjs/nextjs-routes.d.ts @@ -59,6 +59,7 @@ declare module "nextjs-routes" { | StaticRoute<"/epochs"> | DynamicRoute<"/essential-dapps/[id]", { "id": string }> | StaticRoute<"/gas-tracker"> + | StaticRoute<"/hot-contracts"> | StaticRoute<"/"> | StaticRoute<"/internal-txs"> | StaticRoute<"/interop-messages"> diff --git a/pages/hot-contracts.tsx b/pages/hot-contracts.tsx new file mode 100644 index 0000000000..2a68cc0d40 --- /dev/null +++ b/pages/hot-contracts.tsx @@ -0,0 +1,19 @@ +import type { NextPage } from 'next'; +import dynamic from 'next/dynamic'; +import React from 'react'; + +import PageNextJs from 'nextjs/PageNextJs'; + +const HotContracts = dynamic(() => import('ui/pages/HotContracts'), { ssr: false }); + +const Page: NextPage = () => { + return ( + + + + ); +}; + +export default Page; + +export { hotContracts as getServerSideProps } from 'nextjs/getServerSideProps/main'; diff --git a/public/icons/name.d.ts b/public/icons/name.d.ts index 224a07511c..1bed8a7eae 100644 --- a/public/icons/name.d.ts +++ b/public/icons/name.d.ts @@ -76,6 +76,7 @@ | "flame" | "flashblock" | "games" + | "gas_slim" | "gas_xl" | "gas" | "gear_slim" @@ -86,6 +87,7 @@ | "heart_filled" | "heart_outline" | "hexagon" + | "hot-contracts" | "hourglass_slim" | "hourglass" | "info_filled" diff --git a/stubs/contract.ts b/stubs/contract.ts index 271b0f1efd..d309160a96 100644 --- a/stubs/contract.ts +++ b/stubs/contract.ts @@ -1,6 +1,6 @@ import type * as stats from '@blockscout/stats-types'; import type { SmartContract, SmartContractMudSystemsResponse } from 'types/api/contract'; -import type { VerifiedContract, VerifiedContractsCounters } from 'types/api/contracts'; +import type { HotContract, VerifiedContract, VerifiedContractsCounters } from 'types/api/contracts'; import type { SolidityScanReport } from 'lib/solidityScan/schema'; @@ -117,3 +117,10 @@ export const MUD_SYSTEMS: SmartContractMudSystemsResponse = { }, ], }; + +export const HOT_CONTRACTS: HotContract = { + contract_address: VERIFIED_CONTRACT_INFO.address, + balance: '1000000000000000000', + transactions_count: '1000', + total_gas_used: '100000000', +}; diff --git a/toolkit/chakra/tag.tsx b/toolkit/chakra/tag.tsx index 2332dae503..d172ac23c1 100644 --- a/toolkit/chakra/tag.tsx +++ b/toolkit/chakra/tag.tsx @@ -16,6 +16,7 @@ export interface TagProps extends ChakraTag.RootProps { truncated?: boolean; loading?: boolean; selected?: boolean; + disabled?: boolean; } export const Tag = React.forwardRef( @@ -31,6 +32,7 @@ export const Tag = React.forwardRef( truncated = false, loading, selected, + disabled, ...rest } = props; @@ -48,7 +50,8 @@ export const Tag = React.forwardRef( { startElement && ( diff --git a/toolkit/theme/recipes/tag.recipe.ts b/toolkit/theme/recipes/tag.recipe.ts index 49179eb254..364f97c264 100644 --- a/toolkit/theme/recipes/tag.recipe.ts +++ b/toolkit/theme/recipes/tag.recipe.ts @@ -14,6 +14,11 @@ export const recipe = defineSlotRecipe({ _loading: { borderRadius: 'sm', }, + _disabled: { + opacity: 'control.disabled', + pointerEvents: 'none', + cursor: 'not-allowed', + }, }, label: { lineClamp: '1', diff --git a/types/api/addressParams.ts b/types/api/addressParams.ts index 208181699d..20608b200c 100644 --- a/types/api/addressParams.ts +++ b/types/api/addressParams.ts @@ -1,5 +1,6 @@ import type { AddressMetadataTagApi } from './addressMetadata'; import type { SmartContractProxyType } from './contract'; +import type { TokenReputation } from './token'; export interface AddressImplementation { address_hash: string; @@ -61,6 +62,7 @@ export type AddressParamBasic = { } | null; filecoin?: AddressFilecoinParams; proxy_type?: SmartContractProxyType | null; + reputation?: TokenReputation; }; export type AddressParam = UserTags & AddressParamBasic; diff --git a/types/api/contracts.ts b/types/api/contracts.ts index 33acde1365..ea6aeda8ac 100644 --- a/types/api/contracts.ts +++ b/types/api/contracts.ts @@ -39,3 +39,35 @@ export type VerifiedContractsCounters = { smart_contracts: string; verified_smart_contracts: string; }; + +export interface HotContract { + contract_address: AddressParam ; + balance: string; + transactions_count: string; + total_gas_used: string; +} + +export interface HotContractsResponse { + items: Array; + next_page_params: { + items_count: string; + transactions_count: string; + total_gas_used: string; + contract_address_hash: string; + } | null; +} + +export interface HotContractsFilters { + scale?: HotContractsInterval; +} + +export interface HotContractsSorting { + sort: 'transactions_count' | 'total_gas_used'; + order: 'asc' | 'desc'; +} + +export type HotContractsSortingField = HotContractsSorting['sort']; + +export type HotContractsSortingValue = `${ HotContractsSortingField }-${ HotContractsSorting['order'] }` | 'default'; + +export type HotContractsInterval = '5m' | '1h' | '3h' | '1d' | '7d' | '30d'; diff --git a/ui/home/Stats.tsx b/ui/home/Stats.tsx index 0e16f4bdfc..c28d263a3b 100644 --- a/ui/home/Stats.tsx +++ b/ui/home/Stats.tsx @@ -181,7 +181,7 @@ const Stats = () => { }, hasGasTracker && apiData?.gas_prices && { id: 'gas_tracker' as const, - icon: 'gas' as const, + icon: 'gas_slim' as const, label: 'Gas tracker', value: apiData.gas_prices.average ? : 'N/A', hint: gasInfoTooltip, diff --git a/ui/hotContracts/HotContractsIntervalSelect.tsx b/ui/hotContracts/HotContractsIntervalSelect.tsx new file mode 100644 index 0000000000..864389b00c --- /dev/null +++ b/ui/hotContracts/HotContractsIntervalSelect.tsx @@ -0,0 +1,66 @@ +import { createListCollection } from '@chakra-ui/react'; +import React from 'react'; + +import type { HotContractsInterval } from 'types/api/contracts'; + +import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; +import type { SelectOption } from 'toolkit/chakra/select'; +import { Select } from 'toolkit/chakra/select'; +import type { TagProps } from 'toolkit/chakra/tag'; +import TagGroupSelect from 'ui/shared/tagGroupSelect/TagGroupSelect'; + +import { INTERVAL_ITEMS } from './utils'; + +const intervalCollection = createListCollection>({ + items: INTERVAL_ITEMS.map((item) => ({ + value: item.id, + label: item.labelFull, + })), +}); + +const intervalItems = INTERVAL_ITEMS.map((item) => ({ + id: item.id, + title: item.labelShort, +})); + +interface Props { + interval: HotContractsInterval; + onIntervalChange: (newInterval: HotContractsInterval) => void; + isLoading?: boolean; + selectTagSize?: TagProps['size']; +}; + +const HotContractsIntervalSelect = ({ interval, onIntervalChange, isLoading, selectTagSize }: Props) => { + + const isInitialLoading = useIsInitialLoading(isLoading); + + const handleItemSelect = React.useCallback(({ value }: { value: Array }) => { + onIntervalChange(value[0] as HotContractsInterval); + }, [ onIntervalChange ]); + + return ( + <> + + items={ intervalItems } + onChange={ onIntervalChange } + value={ interval } + tagSize={ selectTagSize } + loading={ isInitialLoading } + disabled={ isLoading } + hideBelow="lg" + /> + + + ); +}; + +export default React.memo(HotContractsIntervalSelect); diff --git a/ui/hotContracts/HotContractsListItem.tsx b/ui/hotContracts/HotContractsListItem.tsx new file mode 100644 index 0000000000..4f6f256fe8 --- /dev/null +++ b/ui/hotContracts/HotContractsListItem.tsx @@ -0,0 +1,63 @@ +import { HStack } from '@chakra-ui/react'; +import BigNumber from 'bignumber.js'; +import React from 'react'; + +import type { HotContract } from 'types/api/contracts'; + +import { Skeleton } from 'toolkit/chakra/skeleton'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import { Reputation } from 'ui/shared/entities/token/TokenEntity'; +import EntityTags from 'ui/shared/EntityTags/EntityTags'; +import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile'; +import NativeCoinValue from 'ui/shared/value/NativeCoinValue'; + +interface Props { + data: HotContract; + isLoading?: boolean; + exchangeRate: string | null; +} + +const HotContractsListItem = ({ data, isLoading, exchangeRate }: Props) => { + const protocolTags = data?.contract_address?.metadata?.tags?.filter(tag => tag.tagType === 'protocol'); + + return ( + + + + + + { protocolTags && protocolTags.length > 0 && ( + + ) } + + Txn count + + { Number(data.transactions_count).toLocaleString() } + + + + Gas used + + { BigNumber(data.total_gas_used || 0).toFormat() } + + + + Balance + + + + ); +}; + +export default React.memo(HotContractsListItem); diff --git a/ui/hotContracts/HotContractsTable.tsx b/ui/hotContracts/HotContractsTable.tsx new file mode 100644 index 0000000000..9450720fe3 --- /dev/null +++ b/ui/hotContracts/HotContractsTable.tsx @@ -0,0 +1,65 @@ +import React from 'react'; + +import type { HotContract, HotContractsSortingField, HotContractsSortingValue } from 'types/api/contracts'; + +import { currencyUnits } from 'lib/units'; +import { TableBody, TableColumnHeader, TableColumnHeaderSortable, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table'; +import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; +import getNextSortValue from 'ui/shared/sort/getNextSortValue'; + +import HotContractsTableItem from './HotContractsTableItem'; +import { SORT_SEQUENCE } from './utils'; + +interface Props { + items: Array | undefined; + isLoading?: boolean; + sort: HotContractsSortingValue; + setSorting: ({ value }: { value: Array }) => void; + exchangeRate: string | null; +}; + +const HotContractsTable = ({ items, isLoading, sort, setSorting, exchangeRate }: Props) => { + + const onSortToggle = React.useCallback((field: HotContractsSortingField) => { + const value = getNextSortValue(SORT_SEQUENCE, field)(sort); + setSorting({ value: [ value ] }); + }, [ sort, setSorting ]); + + return ( + + + + Contract + + Txn count + + + Gas used + + Balance { currencyUnits.ether } + + + + { items?.map((item, index) => ( + + )) } + + + ); +}; + +export default HotContractsTable; diff --git a/ui/hotContracts/HotContractsTableItem.tsx b/ui/hotContracts/HotContractsTableItem.tsx new file mode 100644 index 0000000000..2fb02fb08f --- /dev/null +++ b/ui/hotContracts/HotContractsTableItem.tsx @@ -0,0 +1,64 @@ +import { HStack } from '@chakra-ui/react'; +import { BigNumber } from 'bignumber.js'; +import React from 'react'; + +import type { HotContract } from 'types/api/contracts'; + +import { TableCell, TableRow } from 'toolkit/chakra/table'; +import { TruncatedText } from 'toolkit/components/truncation/TruncatedText'; +import AddressEntity from 'ui/shared/entities/address/AddressEntity'; +import { Reputation } from 'ui/shared/entities/token/TokenEntity'; +import EntityTags from 'ui/shared/EntityTags/EntityTags'; +import NativeCoinValue from 'ui/shared/value/NativeCoinValue'; + +interface Props { + isLoading?: boolean; + data: HotContract; + exchangeRate: string | null; +}; + +const HotContractsTableItem = ({ + isLoading, + data, + exchangeRate, +}: Props) => { + const protocolTags = data?.contract_address?.metadata?.tags?.filter(tag => tag.tagType === 'protocol'); + + return ( + + + + + + + { protocolTags && protocolTags.length > 0 && ( + + ) } + + + + + + + + + + + + ); +}; + +export default HotContractsTableItem; diff --git a/ui/hotContracts/utils.ts b/ui/hotContracts/utils.ts new file mode 100644 index 0000000000..256c4e5d0c --- /dev/null +++ b/ui/hotContracts/utils.ts @@ -0,0 +1,38 @@ +import type { HotContractsSortingValue, HotContractsSortingField, HotContractsInterval } from 'types/api/contracts'; + +import getQueryParamString from 'lib/router/getQueryParamString'; +import type { SelectOption } from 'toolkit/chakra/select'; + +export const SORT_OPTIONS: Array> = [ + { label: 'Default', value: 'default' }, + { label: 'Txs count descending', value: 'transactions_count-desc' }, + { label: 'Txs count ascending', value: 'transactions_count-asc' }, + { label: 'Gas used descending', value: 'total_gas_used-desc' }, + { label: 'Gas used ascending', value: 'total_gas_used-asc' }, +]; + +export const SORT_SEQUENCE: Record> = { + transactions_count: [ 'transactions_count-desc', 'transactions_count-asc', 'default' ], + total_gas_used: [ 'total_gas_used-desc', 'total_gas_used-asc', 'default' ], +}; + +export const INTERVAL_ITEMS: Array<{ id: HotContractsInterval; labelShort: string; labelFull: string }> = [ + { id: '5m', labelShort: '5m', labelFull: '5 minutes' }, + { id: '1h', labelShort: '1h', labelFull: '1 hour' }, + { id: '3h', labelShort: '3h', labelFull: '3 hours' }, + { id: '1d', labelShort: '1D', labelFull: '1 day' }, + { id: '7d', labelShort: '1W', labelFull: '1 week' }, + { id: '30d', labelShort: '1M', labelFull: '1 month' }, +]; + +export const getIntervalValueFromQuery = (query: string | Array | undefined): HotContractsInterval => { + const queryString = getQueryParamString(query); + if (queryString) { + const interval = INTERVAL_ITEMS.find(item => item.id === queryString); + if (interval) { + return interval.id; + } + } + + return INTERVAL_ITEMS[0].id; +}; diff --git a/ui/optimismSuperchain/searchResults/__screenshots__/SearchResults.pw.tsx_default_desktop-base-view-1.png b/ui/optimismSuperchain/searchResults/__screenshots__/SearchResults.pw.tsx_default_desktop-base-view-1.png index 391fc4777e..b79cceac74 100644 Binary files a/ui/optimismSuperchain/searchResults/__screenshots__/SearchResults.pw.tsx_default_desktop-base-view-1.png and b/ui/optimismSuperchain/searchResults/__screenshots__/SearchResults.pw.tsx_default_desktop-base-view-1.png differ diff --git a/ui/pages/HotContracts.pw.tsx b/ui/pages/HotContracts.pw.tsx new file mode 100644 index 0000000000..3291e94085 --- /dev/null +++ b/ui/pages/HotContracts.pw.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import * as contractsMock from 'mocks/contracts/index'; +import * as statsMock from 'mocks/stats/index'; +import { test, expect } from 'playwright/lib'; +import { getIntervalValueFromQuery } from 'ui/hotContracts/utils'; + +import HotContracts from './HotContracts'; + +test('base view +@mobile', async({ render, mockTextAd, mockApiResponse, mockEnvs }) => { + test.slow(); + await mockEnvs([ [ 'NEXT_PUBLIC_VIEWS_TOKEN_SCAM_TOGGLE_ENABLED', 'true' ] ]); + await mockTextAd(); + await mockApiResponse( + 'general:stats_hot_contracts', + contractsMock.hotContractsResponse, + { queryParams: { scale: getIntervalValueFromQuery(undefined) } }, + ); + await mockApiResponse('general:stats', { ...statsMock.base, coin_price: '3214.42' }); + + const component = await render(); + await expect(component).toHaveScreenshot({ timeout: 10_000 }); +}); diff --git a/ui/pages/HotContracts.tsx b/ui/pages/HotContracts.tsx new file mode 100644 index 0000000000..63c37f5483 --- /dev/null +++ b/ui/pages/HotContracts.tsx @@ -0,0 +1,137 @@ +import { Box, createListCollection, Flex } from '@chakra-ui/react'; +import { useRouter } from 'next/router'; +import React from 'react'; + +import type { HotContractsInterval, HotContractsSorting, HotContractsSortingField, HotContractsSortingValue } from 'types/api/contracts'; + +import useApiQuery from 'lib/api/useApiQuery'; +import { HOT_CONTRACTS } from 'stubs/contract'; +import { HOMEPAGE_STATS } from 'stubs/stats'; +import { Skeleton } from 'toolkit/chakra/skeleton'; +import { apos } from 'toolkit/utils/htmlEntities'; +import HotContractsIntervalSelect from 'ui/hotContracts/HotContractsIntervalSelect'; +import HotContractsListItem from 'ui/hotContracts/HotContractsListItem'; +import HotContractsTable from 'ui/hotContracts/HotContractsTable'; +import { getIntervalValueFromQuery, SORT_OPTIONS } from 'ui/hotContracts/utils'; +import ActionBar from 'ui/shared/ActionBar'; +import DataListDisplay from 'ui/shared/DataListDisplay'; +import PageTitle from 'ui/shared/Page/PageTitle'; +import Pagination from 'ui/shared/pagination/Pagination'; +import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; +import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue'; +import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery'; +import Sort from 'ui/shared/sort/Sort'; + +const sortCollection = createListCollection({ + items: SORT_OPTIONS, +}); + +const HotContracts = () => { + const router = useRouter(); + const [ interval, setInterval ] = React.useState(getIntervalValueFromQuery(router.query.scale)); + const [ sort, setSort ] = + React.useState(getSortValueFromQuery(router.query, SORT_OPTIONS) ?? 'default'); + + const { data, isError, isPlaceholderData, pagination, onSortingChange, onFilterChange } = useQueryWithPages({ + resourceName: 'general:stats_hot_contracts', + filters: { scale: interval }, + sorting: getSortParamsFromValue(sort), + options: { + placeholderData: { + items: Array(50).fill(HOT_CONTRACTS), + next_page_params: { items_count: '50', transactions_count: '50', total_gas_used: '50', contract_address_hash: '50' }, + }, + }, + }); + + const statsQuery = useApiQuery('general:stats', { + queryOptions: { + placeholderData: HOMEPAGE_STATS, + refetchOnMount: false, + }, + }); + + const handleSortChange = React.useCallback(({ value }: { value: Array }) => { + setSort(value[0] as HotContractsSortingValue); + onSortingChange(value[0] === 'default' ? undefined : getSortParamsFromValue(value[0] as HotContractsSortingValue)); + }, [ onSortingChange ]); + + const handleIntervalChange = React.useCallback((newInterval: HotContractsInterval) => { + setInterval(newInterval); + onFilterChange({ scale: newInterval }); + }, [ onFilterChange ]); + + const content = ( + <> + + { data?.items.map((item, index) => ( + + )) } + + + + + + ); + + const actionBar = ( + + + + { [ '1d', '7d', '30d' ].includes(interval) && ( + + The data is updated once a day. + + ) } + + + + + ); + + return ( + <> + + + { content } + + + ); +}; + +export default HotContracts; diff --git a/ui/pages/__screenshots__/HotContracts.pw.tsx_default_base-view-mobile-1.png b/ui/pages/__screenshots__/HotContracts.pw.tsx_default_base-view-mobile-1.png new file mode 100644 index 0000000000..336f1ff113 Binary files /dev/null and b/ui/pages/__screenshots__/HotContracts.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/pages/__screenshots__/HotContracts.pw.tsx_mobile_base-view-mobile-1.png b/ui/pages/__screenshots__/HotContracts.pw.tsx_mobile_base-view-mobile-1.png new file mode 100644 index 0000000000..27060ab4c9 Binary files /dev/null and b/ui/pages/__screenshots__/HotContracts.pw.tsx_mobile_base-view-mobile-1.png differ diff --git a/ui/shared/sort/Sort.tsx b/ui/shared/sort/Sort.tsx index 413acb68a3..e2a592a09a 100644 --- a/ui/shared/sort/Sort.tsx +++ b/ui/shared/sort/Sort.tsx @@ -1,6 +1,7 @@ import { chakra } from '@chakra-ui/react'; import React from 'react'; +import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; import useIsMobile from 'lib/hooks/useIsMobile'; import { IconButton } from 'toolkit/chakra/icon-button'; import type { SelectRootProps } from 'toolkit/chakra/select'; @@ -14,16 +15,18 @@ export interface Props extends SelectRootProps { const Sort = (props: Props) => { const { collection, isLoading, ...rest } = props; const isMobile = useIsMobile(false); + const isInitialLoading = useIsInitialLoading(isLoading); const trigger = (() => { if (isMobile) { return ( @@ -33,7 +36,7 @@ const Sort = (props: Props) => { return ( @@ -56,7 +59,7 @@ const Sort = (props: Props) => { })(); return ( - + { trigger } { collection.items.map((item) => ( diff --git a/ui/shared/tagGroupSelect/TagGroupSelect.tsx b/ui/shared/tagGroupSelect/TagGroupSelect.tsx index 8feeff323d..ed1a04a920 100644 --- a/ui/shared/tagGroupSelect/TagGroupSelect.tsx +++ b/ui/shared/tagGroupSelect/TagGroupSelect.tsx @@ -1,3 +1,4 @@ +import type { StackProps } from '@chakra-ui/react'; import { HStack } from '@chakra-ui/react'; import React from 'react'; @@ -7,6 +8,8 @@ import { Tag } from 'toolkit/chakra/tag'; type Props = { items: Array<{ id: T; title: string }>; tagSize?: TagProps['size']; + loading?: boolean; + disabled?: boolean; } & ( { value?: T; @@ -17,9 +20,9 @@ type Props = { onChange: (value: Array) => void; isMulti: true; } -); +) & Omit; -const TagGroupSelect = ({ items, value, isMulti, onChange, tagSize, ...rest }: Props) => { +const TagGroupSelect = ({ items, value, isMulti, onChange, tagSize, loading, disabled, ...rest }: Props) => { const onItemClick = React.useCallback((event: React.SyntheticEvent) => { const itemValue = (event.currentTarget as HTMLDivElement).getAttribute('data-id') as T; if (isMulti) { @@ -46,10 +49,12 @@ const TagGroupSelect = ({ items, value, isMulti, onChange, tag data-id={ item.id } selected={ isSelected } fontWeight={ 500 } - onClick={ onItemClick } + onClick={ disabled ? undefined : onItemClick } size={ tagSize } display="inline-flex" justifyContent="center" + loading={ loading } + disabled={ disabled } > { item.title } diff --git a/ui/showcases/Tag.tsx b/ui/showcases/Tag.tsx index cb45f21f39..3b4a74bf39 100644 --- a/ui/showcases/Tag.tsx +++ b/ui/showcases/Tag.tsx @@ -65,6 +65,10 @@ const TagShowcase = () => { My tag Very very very very very looooooonggggg text + + Default + Selected + My tag Very very very very very looooooonggggg text diff --git a/ui/showcases/__screenshots__/Tag.pw.tsx_dark-color-mode_default-dark-mode-1.png b/ui/showcases/__screenshots__/Tag.pw.tsx_dark-color-mode_default-dark-mode-1.png index e027205213..95dbf2bd73 100644 Binary files a/ui/showcases/__screenshots__/Tag.pw.tsx_dark-color-mode_default-dark-mode-1.png and b/ui/showcases/__screenshots__/Tag.pw.tsx_dark-color-mode_default-dark-mode-1.png differ diff --git a/ui/showcases/__screenshots__/Tag.pw.tsx_default_default-dark-mode-1.png b/ui/showcases/__screenshots__/Tag.pw.tsx_default_default-dark-mode-1.png index 8c1bf31a1e..5dc9baa32e 100644 Binary files a/ui/showcases/__screenshots__/Tag.pw.tsx_default_default-dark-mode-1.png and b/ui/showcases/__screenshots__/Tag.pw.tsx_default_default-dark-mode-1.png differ diff --git a/ui/snippets/header/__screenshots__/Burger.pw.tsx_dark-color-mode_with-promo-banner-text-dark-mode-1.png b/ui/snippets/header/__screenshots__/Burger.pw.tsx_dark-color-mode_with-promo-banner-text-dark-mode-1.png index 4c2fca0294..6cf3bbf976 100644 Binary files a/ui/snippets/header/__screenshots__/Burger.pw.tsx_dark-color-mode_with-promo-banner-text-dark-mode-1.png and b/ui/snippets/header/__screenshots__/Burger.pw.tsx_dark-color-mode_with-promo-banner-text-dark-mode-1.png differ diff --git a/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_auth-base-view-1.png b/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_auth-base-view-1.png index 6689c53fde..a0be06b94c 100644 Binary files a/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_auth-base-view-1.png and b/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_auth-base-view-1.png differ diff --git a/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_base-view-1.png b/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_base-view-1.png index fc0f9c1952..151a94dc3f 100644 Binary files a/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_base-view-1.png and b/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_base-view-1.png differ diff --git a/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_dark-mode-base-view-1.png b/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_dark-mode-base-view-1.png index 7d66d4ae46..640e165f13 100644 Binary files a/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_dark-mode-base-view-1.png and b/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_dark-mode-base-view-1.png differ diff --git a/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_with-promo-banner-image-1.png b/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_with-promo-banner-image-1.png index eced0ac997..6abc057743 100644 Binary files a/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_with-promo-banner-image-1.png and b/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_with-promo-banner-image-1.png differ diff --git a/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_with-promo-banner-text-dark-mode-1.png b/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_with-promo-banner-text-dark-mode-1.png index dadd97c33d..a1e1409cb4 100644 Binary files a/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_with-promo-banner-text-dark-mode-1.png and b/ui/snippets/header/__screenshots__/Burger.pw.tsx_default_with-promo-banner-text-dark-mode-1.png differ diff --git a/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_base-view-dark-mode-1.png b/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_base-view-dark-mode-1.png index ec4577960a..efad4d897a 100644 Binary files a/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_base-view-dark-mode-1.png and b/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_base-view-dark-mode-1.png differ diff --git a/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_with-promo-banner-text-dark-mode-1.png b/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_with-promo-banner-text-dark-mode-1.png index 1973e89275..15c5ac0361 100644 Binary files a/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_with-promo-banner-text-dark-mode-1.png and b/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_with-promo-banner-text-dark-mode-1.png differ diff --git a/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_base-view-dark-mode-1.png b/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_base-view-dark-mode-1.png index 8a021dbb55..4df77aa892 100644 Binary files a/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_base-view-dark-mode-1.png and b/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_base-view-dark-mode-1.png differ diff --git a/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-groped-items-1.png b/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-groped-items-1.png index 10e16e45e0..a8fb3e4bba 100644 Binary files a/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-groped-items-1.png and b/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-groped-items-1.png differ diff --git a/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-image-1.png b/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-image-1.png index d7d25fecb0..6bd4c7078d 100644 Binary files a/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-image-1.png and b/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-image-1.png differ diff --git a/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-image-with-tooltip-1.png b/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-image-with-tooltip-1.png index 9b4b1d564a..b753d32b9d 100644 Binary files a/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-image-with-tooltip-1.png and b/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-image-with-tooltip-1.png differ diff --git a/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-text-dark-mode-1.png b/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-text-dark-mode-1.png index 7fff1becb3..b9872e5a95 100644 Binary files a/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-text-dark-mode-1.png and b/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-text-dark-mode-1.png differ diff --git a/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-text-with-tooltip-1.png b/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-text-with-tooltip-1.png index 4c799123c8..e4fbd19567 100644 Binary files a/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-text-with-tooltip-1.png and b/ui/snippets/navigation/horizontal/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-text-with-tooltip-1.png differ diff --git a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_hover-xl-screen-dark-mode-1.png b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_hover-xl-screen-dark-mode-1.png index e70fb7570a..1c3c77ef0b 100644 Binary files a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_hover-xl-screen-dark-mode-1.png and b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_hover-xl-screen-dark-mode-1.png differ diff --git a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_no-auth-xl-screen-dark-mode-1.png b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_no-auth-xl-screen-dark-mode-1.png index e70fb7570a..1c3c77ef0b 100644 Binary files a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_no-auth-xl-screen-dark-mode-1.png and b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_no-auth-xl-screen-dark-mode-1.png differ diff --git a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_with-highlighted-routes-xl-screen-dark-mode-1.png b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_with-highlighted-routes-xl-screen-dark-mode-1.png index 8c14830e70..053cde861b 100644 Binary files a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_with-highlighted-routes-xl-screen-dark-mode-1.png and b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_with-highlighted-routes-xl-screen-dark-mode-1.png differ diff --git a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_with-promo-banner-text-xl-screen-dark-mode-1.png b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_with-promo-banner-text-xl-screen-dark-mode-1.png index b28f396de7..6c01cc51e5 100644 Binary files a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_with-promo-banner-text-xl-screen-dark-mode-1.png and b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_dark-color-mode_with-promo-banner-text-xl-screen-dark-mode-1.png differ diff --git a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_hover-xl-screen-dark-mode-1.png b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_hover-xl-screen-dark-mode-1.png index dfe6772902..370a6b46e1 100644 Binary files a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_hover-xl-screen-dark-mode-1.png and b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_hover-xl-screen-dark-mode-1.png differ diff --git a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_no-auth-xl-screen-dark-mode-1.png b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_no-auth-xl-screen-dark-mode-1.png index dfe6772902..370a6b46e1 100644 Binary files a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_no-auth-xl-screen-dark-mode-1.png and b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_no-auth-xl-screen-dark-mode-1.png differ diff --git a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_with-highlighted-routes-xl-screen-dark-mode-1.png b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_with-highlighted-routes-xl-screen-dark-mode-1.png index 5f16250e40..eaac6d4724 100644 Binary files a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_with-highlighted-routes-xl-screen-dark-mode-1.png and b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_with-highlighted-routes-xl-screen-dark-mode-1.png differ diff --git a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-image-xl-screen-1.png b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-image-xl-screen-1.png index 762e42c42d..5e6f3f4919 100644 Binary files a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-image-xl-screen-1.png and b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-image-xl-screen-1.png differ diff --git a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-text-xl-screen-dark-mode-1.png b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-text-xl-screen-dark-mode-1.png index 0388010733..96977efda9 100644 Binary files a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-text-xl-screen-dark-mode-1.png and b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_with-promo-banner-text-xl-screen-dark-mode-1.png differ diff --git a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_with-submenu-xl-screen-base-view-1.png b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_with-submenu-xl-screen-base-view-1.png index 19d9f8bde7..d8a705161b 100644 Binary files a/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_with-submenu-xl-screen-base-view-1.png and b/ui/snippets/navigation/vertical/__screenshots__/NavigationDesktop.pw.tsx_default_with-submenu-xl-screen-base-view-1.png differ