Commit 4571aa65 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into tom2drum/sx-and-more

parents c39d3e98 eb86be62
......@@ -20,6 +20,7 @@ on:
- eth_sepolia
- eth_goerli
- optimism
- optimism_celestia
- optimism_sepolia
- polygon
- rootstock
......
......@@ -20,6 +20,7 @@ on:
- eth_sepolia
- eth_goerli
- optimism
- optimism_celestia
- optimism_sepolia
- polygon
- rootstock
......
......@@ -368,6 +368,7 @@
"eth_goerli",
"eth_sepolia",
"optimism",
"optimism_celestia",
"optimism_sepolia",
"polygon",
"rootstock_testnet",
......
......@@ -2,6 +2,8 @@ import type { Feature } from './types';
import type { RollupType } from 'types/client/rollup';
import { ROLLUP_TYPES } from 'types/client/rollup';
import stripTrailingSlash from 'lib/stripTrailingSlash';
import { getEnvValue } from '../utils';
const type = (() => {
......@@ -21,7 +23,7 @@ const config: Feature<{ type: RollupType; L1BaseUrl: string; L2WithdrawalUrl?: s
title,
isEnabled: true,
type,
L1BaseUrl,
L1BaseUrl: stripTrailingSlash(L1BaseUrl),
L2WithdrawalUrl,
});
}
......
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;
# Set of ENVs for OP Celestia Raspberry network explorer
# https://opcelestia-raspberry.gelatoscout.com
# This is an auto-generated file. To update all values, run "yarn preset:sync --name=optimism_celestia"
# Local ENVs
NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_ENV=development
NEXT_PUBLIC_API_WEBSOCKET_PROTOCOL=ws
# Instance ENVs
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_DESKTOP={'id':'721628','width':'728','height':'90'}
NEXT_PUBLIC_AD_ADBUTLER_CONFIG_MOBILE={'id':'721627','width':'300','height':'100'}
NEXT_PUBLIC_AD_BANNER_PROVIDER=adbutler
NEXT_PUBLIC_AD_TEXT_PROVIDER=none
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_API_HOST=opcelestia-raspberry.gelatoscout.com
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_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/featured-networks/opcelestia-raspberry.json
NEXT_PUBLIC_GRAPHIQL_TRANSACTION=0x0f5b54de81848d8d8baa02c69030037218a2b4df622d64a2a429e11721606656
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs']
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=rgba(255, 0, 0, 1)
NEXT_PUBLIC_IS_TESTNET=true
NEXT_PUBLIC_MARKETPLACE_ENABLED=false
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_ICON=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg
NEXT_PUBLIC_NETWORK_ICON_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-icons/optimism.svg
NEXT_PUBLIC_NETWORK_ID=123420111
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg
NEXT_PUBLIC_NETWORK_LOGO_DARK=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/network-logos/optimism.svg
NEXT_PUBLIC_NETWORK_NAME=OP Celestia Raspberry
NEXT_PUBLIC_NETWORK_RPC_URL=https://rpc.opcelestia-raspberry.gelato.digital
NEXT_PUBLIC_NETWORK_SHORT_NAME=opcelestia-raspberry
NEXT_PUBLIC_OG_ENHANCED_DATA_ENABLED=true
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://eth-sepolia.blockscout.com/
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://bridge.gelato.network/bridge/opcelestia-raspberry
NEXT_PUBLIC_ROLLUP_TYPE=optimistic
NEXT_PUBLIC_STATS_API_HOST=https://stats-opcelestia-raspberry.k8s.blockscout.com
NEXT_PUBLIC_TRANSACTION_INTERPRETATION_PROVIDER=blockscout
NEXT_PUBLIC_VISUALIZE_API_HOST=https://visualizer.services.blockscout.com
NEXT_PUBLIC_WEB3_WALLETS=none
\ No newline at end of file
......@@ -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
......
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="10" fill="url(#a)"/>
<g clip-path="url(#b)" fill="#fff" fill-opacity=".95">
<path d="M15.763 12.826c.119-.243.178-.365.158-.462a.292.292 0 0 0-.148-.199c-.088-.047-.244-.02-.554.033a6.409 6.409 0 0 1-5.632-1.786 6.409 6.409 0 0 1-1.785-5.631c.053-.31.08-.466.033-.554a.292.292 0 0 0-.199-.149c-.098-.02-.219.04-.462.159a6.417 6.417 0 1 0 8.589 8.589Z"/>
<path d="M15.9 10.817c.152-.054.229-.082.31-.152a.686.686 0 0 0 .163-.228c.04-.1.04-.183.043-.35a6.398 6.398 0 0 0-1.879-4.624 6.398 6.398 0 0 0-4.624-1.88c-.167.003-.25.004-.35.044a.685.685 0 0 0-.229.163c-.07.081-.097.158-.151.31a5.25 5.25 0 0 0 6.717 6.717Z"/>
</g>
<defs>
<linearGradient id="a" x1="17.5" y1="2" x2="0" y2="20" gradientUnits="userSpaceOnUse">
<stop stop-color="#196E41"/>
<stop offset="1" stop-color="#092E1B"/>
</linearGradient>
<clipPath id="b">
<rect x="3" y="3" width="14" height="14" rx="7" fill="#fff"/>
</clipPath>
</defs>
</svg>
......@@ -85,6 +85,9 @@ import type {
OptimisticL2TxnBatchesResponse,
OptimisticL2WithdrawalsResponse,
OptimisticL2DisputeGamesResponse,
OptimismL2TxnBatch,
OptimismL2BatchTxs,
OptimismL2BatchBlocks,
} from 'types/api/optimisticL2';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchRedirectResult, SearchResult, SearchResultFilters, SearchResultItem } from 'types/api/search';
......@@ -678,12 +681,29 @@ export const RESOURCES = {
},
optimistic_l2_txn_batches: {
path: '/api/v2/optimism/txn-batches',
path: '/api/v2/optimism/batches',
filterFields: [],
},
optimistic_l2_txn_batches_count: {
path: '/api/v2/optimism/txn-batches/count',
path: '/api/v2/optimism/batches/count',
},
optimistic_l2_txn_batch: {
path: '/api/v2/optimism/batches/:number',
pathParams: [ 'number' as const ],
},
optimistic_l2_txn_batch_txs: {
path: '/api/v2/transactions/optimism-batch/:number',
pathParams: [ 'number' as const ],
filterFields: [],
},
optimistic_l2_txn_batch_blocks: {
path: '/api/v2/blocks/optimism-batch/:number',
pathParams: [ 'number' as const ],
filterFields: [],
},
optimistic_l2_dispute_games: {
......@@ -967,7 +987,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward
'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' |
'optimistic_l2_output_roots' | 'optimistic_l2_withdrawals' | 'optimistic_l2_txn_batches' | 'optimistic_l2_deposits' |
'optimistic_l2_dispute_games' |
'optimistic_l2_dispute_games' | 'optimistic_l2_txn_batch_txs' | 'optimistic_l2_txn_batch_blocks' |
'mud_worlds'| 'address_mud_tables' | 'address_mud_records' |
'shibarium_deposits' | 'shibarium_withdrawals' |
'arbitrum_l2_messages' | 'arbitrum_l2_txn_batches' | 'arbitrum_l2_txn_batch_txs' | 'arbitrum_l2_txn_batch_blocks' |
......@@ -1072,11 +1092,14 @@ Q extends 'optimistic_l2_output_roots' ? OptimisticL2OutputRootsResponse :
Q extends 'optimistic_l2_withdrawals' ? OptimisticL2WithdrawalsResponse :
Q extends 'optimistic_l2_deposits' ? OptimisticL2DepositsResponse :
Q extends 'optimistic_l2_txn_batches' ? OptimisticL2TxnBatchesResponse :
Q extends 'optimistic_l2_txn_batches_count' ? number :
Q extends 'optimistic_l2_txn_batch' ? OptimismL2TxnBatch :
Q extends 'optimistic_l2_txn_batch_txs' ? OptimismL2BatchTxs :
Q extends 'optimistic_l2_txn_batch_blocks' ? OptimismL2BatchBlocks :
Q extends 'optimistic_l2_dispute_games' ? OptimisticL2DisputeGamesResponse :
Q extends 'optimistic_l2_output_roots_count' ? number :
Q extends 'optimistic_l2_withdrawals_count' ? number :
Q extends 'optimistic_l2_deposits_count' ? number :
Q extends 'optimistic_l2_txn_batches_count' ? number :
Q extends 'optimistic_l2_dispute_games_count' ? number :
never;
// !!! IMPORTANT !!!
......
export const txnBatchesData = {
import type {
OptimismL2TxnBatchTypeCallData,
OptimismL2TxnBatchTypeCelestia,
OptimismL2TxnBatchTypeEip4844,
OptimisticL2TxnBatchesResponse,
} from 'types/api/optimisticL2';
export const txnBatchesData: OptimisticL2TxnBatchesResponse = {
items: [
{
batch_data_container: 'in_blob4844',
internal_id: 260998,
l1_timestamp: '2022-11-10T11:29:11.000000Z',
l1_tx_hashes: [
'0x5bc94d02b65743dfaa9e10a2d6e175aff2a05cce2128c8eaf848bd84ab9325c5',
'0x92a51bc623111dbb91f243e3452e60fab6f090710357f9d9b75ac8a0f67dfd9d',
'0x9553351f6bd1577f4e782738c087be08697fb11f3b91745138d71ba166d62c3b',
],
l1_timestamp: '2023-02-24T10:16:12.000000Z',
l2_block_number: 5902836,
tx_count: 0,
l2_block_end: 124882074,
l2_block_start: 124881833,
tx_count: 4011,
},
{
batch_data_container: 'in_calldata',
internal_id: 260997,
l1_timestamp: '2022-11-03T11:20:59.000000Z',
l1_tx_hashes: [
'0xc45f846ee28ce9ba116ce2d378d3dd00b55d324b833b3ecd4241c919c572c4aa',
'0x80f5fba70d5685bc2b70df836942e892b24afa7bba289a2fac0ca8f4d554cc72',
],
l1_timestamp: '2023-02-24T10:16:00.000000Z',
l2_block_number: 5902835,
tx_count: 0,
l2_block_end: 124881832,
l2_block_start: 124881613,
tx_count: 4206,
},
{
internal_id: 260996,
l1_timestamp: '2024-09-03T11:14:23.000000Z',
l1_tx_hashes: [
'0x48139721f792d3a68c3781b4cf50e66e8fc7dbb38adff778e09066ea5be9adb8',
'0x39f4c46cae57bae936acb9159e367794f41f021ed3788adb80ad93830edb5f22',
],
l1_timestamp: '2023-02-24T10:16:00.000000Z',
l2_block_number: 5902834,
tx_count: 0,
l2_block_end: 124881612,
l2_block_start: 124881380,
tx_count: 4490,
},
],
next_page_params: {
block_number: 5902834,
id: 5902834,
items_count: 50,
},
};
export const txnBatchTypeCallData: OptimismL2TxnBatchTypeCallData = {
batch_data_container: 'in_calldata',
internal_id: 309123,
l1_timestamp: '2022-08-10T10:30:24.000000Z',
l1_tx_hashes: [
'0x478c45f182631ae6f7249d40f31fdac36f41d88caa2e373fba35340a7345ca67',
],
l2_block_end: 10146784,
l2_block_start: 10145379,
tx_count: 1608,
};
export const txnBatchTypeCelestia: OptimismL2TxnBatchTypeCelestia = {
batch_data_container: 'in_celestia',
blobs: [
{
commitment: '0x39c18c21c6b127d58809b8d3b5931472421f9b51532959442f53038f10b78f2a',
height: 2584868,
l1_timestamp: '2024-08-28T16:51:12.000000Z',
l1_transaction_hash: '0x2bb0b96a8ba0f063a243ac3dee0b2f2d87edb2ba9ef44bfcbc8ed191af1c4c24',
namespace: '0x00000000000000000000000000000000000000000008e5f679bf7116cb',
},
],
internal_id: 309667,
l1_timestamp: '2022-08-28T16:51:12.000000Z',
l1_tx_hashes: [
'0x2bb0b96a8ba0f063a243ac3dee0b2f2d87edb2ba9ef44bfcbc8ed191af1c4c24',
],
l2_block_end: 10935879,
l2_block_start: 10934514,
tx_count: 1574,
};
export const txnBatchTypeEip4844: OptimismL2TxnBatchTypeEip4844 = {
batch_data_container: 'in_blob4844',
blobs: [
{
hash: '0x012a4f0c6db6bce9d3d357b2bf847764320bcb0107ab318f3a532f637bc60dfe',
l1_timestamp: '2022-08-23T03:59:12.000000Z',
l1_transaction_hash: '0x3870f136497e5501dc20d0974daf379c8636c958794d59a9c90d4f8a9f0ed20a',
},
{
hash: '0x01d1097cce23229931afbc2fd1cf0d707da26df7b39cef1c542276ae718de4f6',
l1_timestamp: '2022-08-23T03:59:12.000000Z',
l1_transaction_hash: '0x3870f136497e5501dc20d0974daf379c8636c958794d59a9c90d4f8a9f0ed20a',
},
],
internal_id: 2538459,
l1_timestamp: '2022-08-23T03:59:12.000000Z',
l1_tx_hashes: [
'0x3870f136497e5501dc20d0974daf379c8636c958794d59a9c90d4f8a9f0ed20a',
],
l2_block_end: 16291502,
l2_block_start: 16291373,
tx_count: 704,
};
......@@ -72,6 +72,7 @@ export const withoutGasInfo: HomeStats = {
export const withSecondaryCoin: HomeStats = {
...base,
secondary_coin_price: '3.398',
secondary_coin_image: 'http://localhost:3100/secondary_utia.jpg',
};
export const noChartData: HomeStats = {
......
......@@ -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': [
......
......@@ -112,7 +112,7 @@ export const optimisticRollup: GetServerSideProps<Props> = async(context) => {
return base(context);
};
const BATCH_ROLLUP_TYPES: Array<RollupType> = [ 'zkEvm', 'zkSync', 'arbitrum' ];
const BATCH_ROLLUP_TYPES: Array<RollupType> = [ 'zkEvm', 'zkSync', 'arbitrum', 'optimistic' ];
export const batch: GetServerSideProps<Props> = async(context) => {
if (!(rollupFeature.isEnabled && BATCH_ROLLUP_TYPES.includes(rollupFeature.type))) {
return {
......
......@@ -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"
/>
......
......@@ -17,6 +17,8 @@ const Batch = dynamic(() => {
switch (rollupFeature.type) {
case 'arbitrum':
return import('ui/pages/ArbitrumL2TxnBatch');
case 'optimistic':
return import('ui/pages/OptimisticL2TxnBatch');
case 'zkEvm':
return import('ui/pages/ZkEvmL2TxnBatch');
case 'zkSync':
......
......@@ -24,6 +24,7 @@
| "block_slim"
| "block"
| "brands/blockscout"
| "brands/celenium"
| "brands/safe"
| "brands/solidity_scan"
| "burger"
......
import type {
OptimismL2TxnBatch,
OptimisticL2DepositsItem,
OptimisticL2DisputeGamesItem,
OptimisticL2OutputRootsItem,
......@@ -30,14 +31,29 @@ export const L2_WITHDRAWAL_ITEM: OptimisticL2WithdrawalsItem = {
};
export const L2_TXN_BATCHES_ITEM: OptimisticL2TxnBatchesItem = {
internal_id: 260991,
batch_data_container: 'in_blob4844',
l1_timestamp: '2023-06-01T14:46:48.000000Z',
l1_tx_hashes: [
TX_HASH,
],
l2_block_number: 5218590,
l2_block_start: 5218590,
l2_block_end: 5218777,
tx_count: 9,
};
export const L2_TXN_BATCH: OptimismL2TxnBatch = {
...L2_TXN_BATCHES_ITEM,
batch_data_container: 'in_blob4844',
blobs: [
{
hash: '0x01fb41e1ae9f827e13abb0ee94be2ee574a23ac31426cea630ddd18af854bc85',
l1_timestamp: '2024-09-03T13:26:23.000000Z',
l1_transaction_hash: '0xd25ee571f1701690615099b208a9431d8611d0130dc342bead6d9edc291f04b9',
},
],
};
export const L2_OUTPUT_ROOTS_ITEM: OptimisticL2OutputRootsItem = {
l1_block_number: 9103684,
l1_timestamp: '2023-06-01T15:26:12.000000Z',
......
......@@ -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: {
......
......@@ -11,6 +11,7 @@ const PRESETS = {
eth_sepolia: 'https://eth-sepolia.blockscout.com',
gnosis: 'https://gnosis.blockscout.com',
optimism: 'https://optimism.blockscout.com',
optimism_celestia: 'https://opcelestia-raspberry.gelatoscout.com',
optimism_sepolia: 'https://optimism-sepolia.blockscout.com',
polygon: 'https://polygon.blockscout.com',
rootstock_testnet: 'https://rootstock-testnet.blockscout.com',
......
......@@ -3,6 +3,7 @@ import type { Reward } from 'types/api/reward';
import type { Transaction } from 'types/api/transaction';
import type { ArbitrumBatchStatus, ArbitrumL2TxData } from './arbitrumL2';
import type { OptimisticL2BatchDataContainer, OptimisticL2BlobTypeEip4844, OptimisticL2BlobTypeCelestia } from './optimisticL2';
import type { TokenInfo } from './token';
import type { TokenTransfer } from './tokenTransfer';
import type { ZkSyncBatchesItem } from './zkSyncL2';
......@@ -59,6 +60,7 @@ export interface Block {
'batch_number': number | null;
};
arbitrum?: ArbitrumBlockData;
optimism?: OptimismBlockData;
// CELO FIELDS
celo?: {
epoch_number: number;
......@@ -78,6 +80,14 @@ type ArbitrumBlockData = {
'status': ArbitrumBatchStatus;
}
export interface OptimismBlockData {
batch_data_container: OptimisticL2BatchDataContainer;
internal_id: number;
blobs: Array<OptimisticL2BlobTypeEip4844> | Array<OptimisticL2BlobTypeCelestia> | null;
l1_timestamp: string;
l1_tx_hashes: Array<string>;
}
export interface BlocksResponse {
items: Array<Block>;
next_page_params: {
......
import type { AddressParam } from './addressParams';
import type { Block } from './block';
import type { Transaction } from './transaction';
export type OptimisticL2DepositsItem = {
l1_block_number: number;
......@@ -35,21 +37,82 @@ export type OptimisticL2OutputRootsResponse = {
};
}
export type OptimisticL2BatchDataContainer = 'in_blob4844' | 'in_celestia' | 'in_calldata';
export type OptimisticL2TxnBatchesItem = {
l1_tx_hashes: Array<string>;
internal_id: number;
batch_data_container?: OptimisticL2BatchDataContainer;
l1_timestamp: string;
l2_block_number: number;
l1_tx_hashes: Array<string>;
l2_block_start: number;
l2_block_end: number;
tx_count: number;
}
export type OptimisticL2TxnBatchesResponse = {
items: Array<OptimisticL2TxnBatchesItem>;
next_page_params: {
block_number: number;
id: number;
items_count: number;
};
}
export interface OptimisticL2BlobTypeEip4844 {
hash: string;
l1_timestamp: string;
l1_transaction_hash: string;
}
export interface OptimisticL2BlobTypeCelestia {
commitment: string;
height: number;
l1_timestamp: string;
l1_transaction_hash: string;
namespace: string;
}
interface OptimismL2TxnBatchBase {
internal_id: number;
l1_timestamp: string;
l1_tx_hashes: Array<string>;
l2_block_start: number;
l2_block_end: number;
tx_count: number;
}
export interface OptimismL2TxnBatchTypeCallData extends OptimismL2TxnBatchBase {
batch_data_container: 'in_calldata';
}
export interface OptimismL2TxnBatchTypeEip4844 extends OptimismL2TxnBatchBase {
batch_data_container: 'in_blob4844';
blobs: Array<OptimisticL2BlobTypeEip4844> | null;
}
export interface OptimismL2TxnBatchTypeCelestia extends OptimismL2TxnBatchBase {
batch_data_container: 'in_celestia';
blobs: Array<OptimisticL2BlobTypeCelestia> | null;
}
export type OptimismL2TxnBatch = OptimismL2TxnBatchTypeCallData | OptimismL2TxnBatchTypeEip4844 | OptimismL2TxnBatchTypeCelestia;
export type OptimismL2BatchTxs = {
items: Array<Transaction>;
next_page_params: {
block_number: number;
index: number;
items_count: number;
} | null;
}
export type OptimismL2BatchBlocks = {
items: Array<Block>;
next_page_params: {
batch_number: number;
items_count: number;
} | null;
}
export type OptimisticL2WithdrawalsItem = {
'challenge_period_end': string | null;
'from': AddressParam | null;
......
......@@ -19,6 +19,7 @@ export type HomeStats = {
rootstock_locked_btc?: string | null;
last_output_root_size?: string | null;
secondary_coin_price?: string | null;
secondary_coin_image?: string | null;
celo?: {
epoch_number: number;
};
......
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;
}
......@@ -19,6 +19,7 @@ import getNetworkValidationActionText from 'lib/networks/getNetworkValidationAct
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import getQueryParamString from 'lib/router/getQueryParamString';
import { currencyUnits } from 'lib/units';
import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
import BlockGasUsed from 'ui/shared/block/BlockGasUsed';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
......@@ -202,7 +203,7 @@ const BlockDetails = ({ query }: Props) => {
hint="Batch number"
isLoading={ isPlaceholderData }
>
Batch
Batch
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.arbitrum.batch_number ?
......@@ -212,6 +213,28 @@ const BlockDetails = ({ query }: Props) => {
</>
) }
{ rollupFeature.isEnabled && rollupFeature.type === 'optimistic' && data.optimism && !config.UI.views.block.hiddenFields?.batch && (
<>
<DetailsInfoItem.Label
hint="Batch number"
isLoading={ isPlaceholderData }
>
Batch
</DetailsInfoItem.Label>
<DetailsInfoItem.Value columnGap={ 3 }>
{ data.optimism.internal_id ?
<BatchEntityL2 isLoading={ isPlaceholderData } number={ data.optimism.internal_id }/> :
<Skeleton isLoaded={ !isPlaceholderData }>Pending</Skeleton> }
{ data.optimism.batch_data_container && (
<OptimisticL2TxnBatchDA
container={ data.optimism.batch_data_container }
isLoading={ isPlaceholderData }
/>
) }
</DetailsInfoItem.Value>
</>
) }
<DetailsInfoItem.Label
hint="Size of the block in bytes"
isLoading={ isPlaceholderData }
......
......@@ -26,7 +26,7 @@ const TABS_HEIGHT = 88;
interface Props {
type?: BlockType;
query: QueryWithPagesResult<'blocks'>;
query: QueryWithPagesResult<'blocks'> | QueryWithPagesResult<'optimistic_l2_txn_batch_blocks'>;
enableSocket?: boolean;
top?: number;
}
......
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);
......@@ -21,6 +21,7 @@ test.describe('daily txs chart', () => {
await mockApiResponse('stats', statsMock.withSecondaryCoin);
await mockApiResponse('stats_charts_txs', dailyTxsMock.base);
await mockAssetResponse(statsMock.withSecondaryCoin.coin_image as string, './playwright/mocks/image_svg.svg');
await mockAssetResponse(statsMock.withSecondaryCoin.secondary_coin_image as string, './playwright/mocks/image_s.jpg');
component = await render(<ChainIndicators/>);
await page.waitForFunction(() => {
return document.querySelector('path[data-name="gradient-chart-area"]')?.getAttribute('opacity') === '1';
......
......@@ -7,7 +7,6 @@ import config from 'configs/app';
import { sortByDateDesc } from 'ui/shared/chart/utils/sorts';
import IconSvg from 'ui/shared/IconSvg';
import NativeTokenIcon from 'ui/shared/NativeTokenIcon';
import TokenLogoPlaceholder from 'ui/shared/TokenLogoPlaceholder';
const nonNullTailReducer = (result: Array<TimeChartItemRaw>, item: TimeChartItemRaw) => {
if (item.value === null && result.length === 0) {
......@@ -71,7 +70,7 @@ const secondaryCoinPriceIndicator: TChainIndicator<'stats_charts_secondary_coin_
'$N/A' :
'$' + Number(stats.secondary_coin_price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 }),
valueDiff: () => null,
icon: <TokenLogoPlaceholder boxSize={ 6 }/>,
icon: <NativeTokenIcon boxSize={ 6 } type="secondary"/>,
hint: `${ config.chain.secondaryCoin.symbol } token daily price in USD.`,
api: {
resourceName: 'stats_charts_secondary_coin_price',
......
......@@ -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/>
......
import { useRouter } from 'next/router';
import React from 'react';
import type { RoutedTab } from 'ui/shared/Tabs/types';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app';
import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString';
import { BLOCK } from 'stubs/block';
import { L2_TXN_BATCH } from 'stubs/L2';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import BlocksContent from 'ui/blocks/BlocksContent';
import TextAd from 'ui/shared/ad/TextAd';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
import OptimisticL2TxnBatchDetails from 'ui/txnBatches/optimisticL2/OptimisticL2TxnBatchDetails';
import TxsWithFrontendSorting from 'ui/txs/TxsWithFrontendSorting';
const TAB_LIST_PROPS = {
marginBottom: 0,
py: 5,
marginTop: -5,
};
const TABS_HEIGHT = 80;
const OptimisticL2TxnBatch = () => {
const router = useRouter();
const appProps = useAppContext();
const number = getQueryParamString(router.query.number);
const tab = getQueryParamString(router.query.tab);
const isMobile = useIsMobile();
const batchQuery = useApiQuery('optimistic_l2_txn_batch', {
pathParams: { number },
queryOptions: {
enabled: Boolean(number),
placeholderData: L2_TXN_BATCH,
},
});
const batchTxsQuery = useQueryWithPages({
resourceName: 'optimistic_l2_txn_batch_txs',
pathParams: { number },
options: {
enabled: Boolean(!batchQuery.isPlaceholderData && batchQuery.data?.internal_id && tab === 'txs'),
placeholderData: generateListStub<'optimistic_l2_txn_batch_txs'>(TX, 50, { next_page_params: {
block_number: 1338932,
index: 1,
items_count: 50,
} }),
},
});
const batchBlocksQuery = useQueryWithPages({
resourceName: 'optimistic_l2_txn_batch_blocks',
pathParams: { number },
options: {
enabled: Boolean(!batchQuery.isPlaceholderData && batchQuery.data?.internal_id && tab === 'blocks'),
placeholderData: generateListStub<'optimistic_l2_txn_batch_blocks'>(BLOCK, 50, { next_page_params: {
batch_number: 1338932,
items_count: 50,
} }),
},
});
throwOnAbsentParamError(number);
throwOnResourceLoadError(batchQuery);
let pagination;
if (tab === 'txs') {
pagination = batchTxsQuery.pagination;
}
if (tab === 'blocks') {
pagination = batchBlocksQuery.pagination;
}
const hasPagination = !isMobile && pagination?.isVisible;
const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <OptimisticL2TxnBatchDetails query={ batchQuery }/> },
{
id: 'txs',
title: 'Transactions',
component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
},
{
id: 'blocks',
title: 'Blocks',
component: <BlocksContent type="block" query={ batchBlocksQuery } enableSocket={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
},
].filter(Boolean)), [ batchQuery, batchTxsQuery, batchBlocksQuery, hasPagination ]);
const backLink = React.useMemo(() => {
const hasGoBackLink = appProps.referrer && appProps.referrer.endsWith('/batches');
if (!hasGoBackLink) {
return;
}
return {
label: 'Back to tx batches list',
url: appProps.referrer,
};
}, [ appProps.referrer ]);
return (
<>
<TextAd mb={ 6 }/>
<PageTitle
title={ `Batch #${ number }` }
backLink={ backLink }
/>
{ batchQuery.isPlaceholderData ?
<TabsSkeleton tabs={ tabs }/> : (
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ hasPagination && pagination ? <Pagination { ...(pagination) }/> : null }
stickyEnabled={ hasPagination }
/>
) }
</>
);
};
export default OptimisticL2TxnBatch;
......@@ -23,7 +23,7 @@ const OptimisticL2TxnBatches = () => {
{
next_page_params: {
items_count: 50,
block_number: 9045200,
id: 9045200,
},
},
),
......@@ -41,7 +41,7 @@ const OptimisticL2TxnBatches = () => {
<Show below="lg" ssr={ false }>
{ data.items.map(((item, index) => (
<OptimisticL2TxnBatchesListItem
key={ item.l2_block_number + (isPlaceholderData ? String(index) : '') }
key={ item.internal_id + (isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ isPlaceholderData }
/>
......@@ -61,8 +61,8 @@ const OptimisticL2TxnBatches = () => {
return (
<Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap">
Tx batch (L2 block)
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].l2_block_number } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].l2_block_number } </Text>
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].internal_id } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].internal_id } </Text>
(total of { countersQuery.data?.toLocaleString() } batches)
</Skeleton>
);
......
......@@ -10,9 +10,10 @@ import TokenLogoPlaceholder from './TokenLogoPlaceholder';
type Props = {
isLoading?: boolean;
className?: string;
type?: 'primary' | 'secondary';
}
const NativeTokenIcon = (props: Props) => {
const NativeTokenIcon = ({ isLoading, className, type }: Props) => {
const statsQueryResult = useApiQuery('stats', {
queryOptions: {
refetchOnMount: false,
......@@ -20,18 +21,20 @@ const NativeTokenIcon = (props: Props) => {
},
});
if (props.isLoading || statsQueryResult.isPlaceholderData) {
return <Skeleton borderRadius="base" className={ props.className }/>;
if (isLoading || statsQueryResult.isPlaceholderData) {
return <Skeleton borderRadius="base" className={ className }/>;
}
const src = type === 'secondary' ? statsQueryResult.data?.secondary_coin_image : statsQueryResult.data?.coin_image;
return (
<Image
borderRadius="base"
className={ props.className }
src={ statsQueryResult.data?.coin_image || '' }
className={ className }
src={ src || '' }
alt={ `${ config.chain.currency.symbol } logo` }
fallback={ <TokenLogoPlaceholder borderRadius="base" className={ props.className }/> }
fallbackStrategy={ statsQueryResult.data?.coin_image ? 'onError' : 'beforeLoadOrError' }
fallback={ <TokenLogoPlaceholder borderRadius="base" className={ className }/> }
fallbackStrategy={ src ? 'onError' : 'beforeLoadOrError' }
/>
);
};
......
import React from 'react';
import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
import type { ExcludeUndefined } from 'types/utils';
import Tag from 'ui/shared/chakra/Tag';
export interface Props {
container: ExcludeUndefined<OptimisticL2TxnBatchesItem['batch_data_container']>;
isLoading?: boolean;
}
const OptimisticL2TxnBatchDA = ({ container, isLoading }: Props) => {
const text = (() => {
switch (container) {
case 'in_blob4844':
return 'EIP-4844 blob';
case 'in_calldata':
return 'Calldata';
case 'in_celestia':
return 'Celestia blob';
}
})();
return (
<Tag colorScheme="yellow" isLoading={ isLoading }>
{ text }
</Tag>
);
};
export default React.memo(OptimisticL2TxnBatchDA);
import { chakra } from '@chakra-ui/react';
import _omit from 'lodash/omit';
import React from 'react';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import * as BlobEntity from './BlobEntity';
const rollupFeature = config.features.rollup;
const BlobEntityL1 = (props: BlobEntity.EntityProps) => {
const partsProps = _omit(props, [ 'className', 'onClick' ]);
const linkProps = _omit(props, [ 'className' ]);
if (!rollupFeature.isEnabled) {
return null;
}
return (
<BlobEntity.Container className={ props.className }>
<BlobEntity.Icon { ...partsProps }/>
<BlobEntity.Link
{ ...linkProps }
isExternal
href={ rollupFeature.L1BaseUrl + route({ pathname: '/blobs/[hash]', query: { hash: props.hash } }) }
>
<BlobEntity.Content { ...partsProps }/>
</BlobEntity.Link>
<BlobEntity.Copy { ...partsProps }/>
</BlobEntity.Container>
);
};
export default chakra(BlobEntityL1);
......@@ -72,7 +72,7 @@ const NftMedia = ({ imageUrl, animationUrl, className, isLoading, withFullscreen
switch (type) {
case 'video':
return <NftVideo { ...props } autoPlay={ autoplayVideo }/>;
return <NftVideo { ...props } autoPlay={ autoplayVideo } poster={ imageUrl || undefined }/>;
case 'html':
return <NftHtml { ...props }/>;
case 'image':
......
......@@ -5,22 +5,37 @@ import { mediaStyleProps, videoPlayProps } from './utils';
interface Props {
src: string;
poster?: string;
autoPlay?: boolean;
onLoad: () => void;
onError: () => void;
onClick?: () => void;
}
const NftVideo = ({ src, autoPlay = true, onLoad, onError, onClick }: Props) => {
const NftVideo = ({ src, poster, autoPlay = true, onLoad, onError, onClick }: Props) => {
const ref = React.useRef<HTMLVideoElement>(null);
const handleMouseEnter = React.useCallback(() => {
!autoPlay && ref.current?.play();
}, [ autoPlay ]);
const handleMouseLeave = React.useCallback(() => {
!autoPlay && ref.current?.pause();
}, [ autoPlay ]);
return (
<chakra.video
ref={ ref }
{ ...videoPlayProps }
autoPlay={ autoPlay }
poster={ poster }
src={ src }
onCanPlayThrough={ onLoad }
onError={ onError }
borderRadius="md"
onClick={ onClick }
onMouseEnter={ handleMouseEnter }
onMouseLeave={ handleMouseLeave }
{ ...mediaStyleProps }
/>
);
......
......@@ -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 }
/>
......
......@@ -46,7 +46,7 @@ const SearchBar = ({ isHomepage }: Props) => {
const recentSearchKeywords = getRecentSearchKeywords();
const { searchTerm, debouncedSearchTerm, handleSearchTermChange, query, pathname } = useQuickSearchQuery();
const { searchTerm, debouncedSearchTerm, handleSearchTermChange, query } = useQuickSearchQuery();
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
......@@ -55,13 +55,13 @@ const SearchBar = ({ isHomepage }: Props) => {
const url = route(resultRoute);
mixpanel.logEvent(mixpanel.EventTypes.SEARCH_QUERY, {
'Search query': searchTerm,
'Source page type': mixpanel.getPageType(pathname),
'Source page type': mixpanel.getPageType(router.pathname),
'Result URL': url,
});
saveToRecentKeywords(searchTerm);
router.push(resultRoute, undefined, { shallow: true });
}
}, [ searchTerm, pathname, router ]);
}, [ searchTerm, router ]);
const handleFocus = React.useCallback(() => {
onOpen();
......@@ -90,18 +90,24 @@ const SearchBar = ({ isHomepage }: Props) => {
const handleItemClick = React.useCallback((event: React.MouseEvent<HTMLAnchorElement>) => {
mixpanel.logEvent(mixpanel.EventTypes.SEARCH_QUERY, {
'Search query': searchTerm,
'Source page type': mixpanel.getPageType(pathname),
'Source page type': mixpanel.getPageType(router.pathname),
'Result URL': event.currentTarget.href,
});
saveToRecentKeywords(searchTerm);
onClose();
}, [ pathname, searchTerm, onClose ]);
}, [ router.pathname, searchTerm, onClose ]);
const menuPaddingX = isMobile && !isHomepage ? 24 : 0;
const calculateMenuWidth = React.useCallback(() => {
menuWidth.current = (inputRef.current?.getBoundingClientRect().width || 0) - menuPaddingX;
}, [ menuPaddingX ]);
// clear input on page change
React.useEffect(() => {
handleSearchTermChange('');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ router.pathname ]);
React.useEffect(() => {
const inputEl = inputRef.current;
if (!inputEl) {
......
import { useRouter } from 'next/router';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import useDebounce from 'lib/hooks/useDebounce';
export default function useQuickSearchQuery() {
const router = useRouter();
const [ searchTerm, setSearchTerm ] = React.useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
const pathname = router.pathname;
const query = useApiQuery('quick_search', {
queryParams: { q: debouncedSearchTerm },
......@@ -30,6 +26,5 @@ export default function useQuickSearchQuery() {
handleSearchTermChange: setSearchTerm,
query,
redirectCheckQuery,
pathname,
}), [ debouncedSearchTerm, pathname, query, redirectCheckQuery, searchTerm ]);
}), [ debouncedSearchTerm, query, redirectCheckQuery, searchTerm ]);
}
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) ||
......
import { GridItem } from '@chakra-ui/react';
import React from 'react';
import dayjs from 'lib/date/dayjs';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import OptimisticL2TxnBatchBlobWrapper from './OptimisticL2TxnBatchBlobWrapper';
interface Props {
l1TxHashes: Array<string>;
l1Timestamp: string;
isLoading: boolean;
}
const OptimisticL2TxnBatchBlobCallData = ({ l1TxHashes, l1Timestamp, isLoading }: Props) => {
return (
<OptimisticL2TxnBatchBlobWrapper isLoading={ isLoading }>
<GridItem fontWeight={ 600 }>Timestamp</GridItem>
<GridItem whiteSpace="normal">
{ dayjs(l1Timestamp).fromNow() } | { dayjs(l1Timestamp).format('llll') }
</GridItem>
<GridItem fontWeight={ 600 }>L1 txn hash{ l1TxHashes.length > 1 ? 'es' : '' }</GridItem>
<GridItem overflow="hidden" display="flex" flexDir="column" rowGap={ 2 }>
{ l1TxHashes.map((hash) => <TxEntityL1 key={ hash } hash={ hash } noIcon noCopy={ false }/>) }
</GridItem>
</OptimisticL2TxnBatchBlobWrapper>
);
};
export default React.memo(OptimisticL2TxnBatchBlobCallData);
import { Flex, GridItem, Icon, VStack } from '@chakra-ui/react';
import React from 'react';
import type { OptimisticL2BlobTypeCelestia } from 'types/api/optimisticL2';
// This icon doesn't work properly when it is in the sprite
// Probably because of the gradient
// eslint-disable-next-line no-restricted-imports
import celeniumIcon from 'icons/brands/celenium.svg';
import dayjs from 'lib/date/dayjs';
import hexToBase64 from 'lib/hexToBase64';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import LinkExternal from 'ui/shared/links/LinkExternal';
import OptimisticL2TxnBatchBlobWrapper from './OptimisticL2TxnBatchBlobWrapper';
function getCeleniumUrl(blob: OptimisticL2BlobTypeCelestia) {
const url = new URL('https://mocha.celenium.io/blob');
url.searchParams.set('commitment', hexToBase64(blob.commitment));
url.searchParams.set('hash', hexToBase64(blob.namespace));
url.searchParams.set('height', String(blob.height));
return url.toString();
}
interface Props {
blobs: Array<OptimisticL2BlobTypeCelestia>;
isLoading: boolean;
}
const OptimisticL2TxnBatchBlobCelestia = ({ blobs, isLoading }: Props) => {
return (
<VStack rowGap={ 2 } w="100%">
{ blobs.map((blob) => {
return (
<OptimisticL2TxnBatchBlobWrapper key={ blob.commitment } isLoading={ isLoading } gridTemplateColumns="auto 1fr auto">
<GridItem fontWeight={ 600 }>Commitment</GridItem>
<GridItem overflow="hidden">
<Flex minW="0" w="calc(100% - 20px)">
<HashStringShortenDynamic hash={ blob.commitment }/>
<CopyToClipboard text={ blob.commitment }/>
</Flex>
</GridItem>
<GridItem display="flex" columnGap={ 2 }>
<Icon as={ celeniumIcon } boxSize={ 5 }/>
<LinkExternal href={ getCeleniumUrl(blob) }>Blob page</LinkExternal>
</GridItem>
<GridItem fontWeight={ 600 }>Hight</GridItem>
<GridItem colSpan={ 2 }>
{ blob.height }
</GridItem>
<GridItem fontWeight={ 600 }>Timestamp</GridItem>
<GridItem whiteSpace="normal" colSpan={ 2 }>
{ dayjs(blob.l1_timestamp).fromNow() } | { dayjs(blob.l1_timestamp).format('llll') }
</GridItem>
<GridItem fontWeight={ 600 }>L1 txn hash</GridItem>
<GridItem overflow="hidden" colSpan={ 2 }>
<TxEntityL1 hash={ blob.l1_transaction_hash } noIcon noCopy={ false }/>
</GridItem>
</OptimisticL2TxnBatchBlobWrapper>
);
}) }
</VStack>
);
};
export default React.memo(OptimisticL2TxnBatchBlobCelestia);
import { GridItem, VStack } from '@chakra-ui/react';
import React from 'react';
import type { OptimisticL2BlobTypeEip4844 } from 'types/api/optimisticL2';
import dayjs from 'lib/date/dayjs';
import BlobEntityL1 from 'ui/shared/entities/blob/BlobEntityL1';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import OptimisticL2TxnBatchBlobWrapper from './OptimisticL2TxnBatchBlobWrapper';
interface Props {
blobs: Array<OptimisticL2BlobTypeEip4844>;
isLoading: boolean;
}
const OptimisticL2TxnBatchBlobEip4844 = ({ blobs, isLoading }: Props) => {
return (
<VStack rowGap={ 2 } w="100%">
{ blobs.map((blob) => {
return (
<OptimisticL2TxnBatchBlobWrapper key={ blob.hash } isLoading={ isLoading }>
<GridItem fontWeight={ 600 }>Versioned hash</GridItem>
<GridItem overflow="hidden">
<BlobEntityL1 hash={ blob.hash }/>
</GridItem>
<GridItem fontWeight={ 600 }>Timestamp</GridItem>
<GridItem whiteSpace="normal">
{ dayjs(blob.l1_timestamp).fromNow() } | { dayjs(blob.l1_timestamp).format('llll') }
</GridItem>
<GridItem fontWeight={ 600 }>L1 txn hash</GridItem>
<GridItem overflow="hidden">
<TxEntityL1 hash={ blob.l1_transaction_hash } noIcon noCopy={ false }/>
</GridItem>
</OptimisticL2TxnBatchBlobWrapper>
);
}) }
</VStack>
);
};
export default React.memo(OptimisticL2TxnBatchBlobEip4844);
import { chakra, Grid, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
interface Props {
children: React.ReactNode;
className?: string;
isLoading: boolean;
}
const OptimisticL2TxnBatchBlobWrapper = ({ children, className, isLoading }: Props) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.100');
return (
<Grid
className={ className }
columnGap={ 3 }
rowGap="10px"
p={ 4 }
bgColor={ bgColor }
gridTemplateColumns="auto 1fr"
borderRadius="base"
w="100%"
h={ isLoading ? '140px' : undefined }
fontSize="sm"
lineHeight={ 5 }
>
{ isLoading ? null : children }
</Grid>
);
};
export default React.memo(chakra(OptimisticL2TxnBatchBlobWrapper));
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { OptimismL2TxnBatch } from 'types/api/optimisticL2';
import type { ResourceError } from 'lib/api/resources';
import * as txnBatchesMock from 'mocks/l2txnBatches/txnBatches';
import { ENVS_MAP } from 'playwright/fixtures/mockEnvs';
import { test, expect } from 'playwright/lib';
import OptimisticL2TxnBatchDetails from './OptimisticL2TxnBatchDetails';
const hooksConfig = {
router: {
query: { number: '1' },
},
};
test.beforeEach(async({ mockEnvs }) => {
await mockEnvs(ENVS_MAP.optimisticRollup);
});
test('call data blob container +@mobile', async({ render }) => {
const query = {
data: txnBatchesMock.txnBatchTypeCallData,
} as UseQueryResult<OptimismL2TxnBatch, ResourceError>;
const component = await render(<OptimisticL2TxnBatchDetails query={ query }/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
test('celestia blob container +@mobile', async({ render }) => {
const query = {
data: txnBatchesMock.txnBatchTypeCelestia,
} as UseQueryResult<OptimismL2TxnBatch, ResourceError>;
const component = await render(<OptimisticL2TxnBatchDetails query={ query }/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
test('EIP-4844 blob container +@mobile', async({ render }) => {
const query = {
data: txnBatchesMock.txnBatchTypeEip4844,
} as UseQueryResult<OptimismL2TxnBatch, ResourceError>;
const component = await render(<OptimisticL2TxnBatchDetails query={ query }/>, { hooksConfig });
await expect(component).toHaveScreenshot();
});
import { Grid, Skeleton } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { OptimismL2TxnBatch } from 'types/api/optimisticL2';
import { route } from 'nextjs-routes';
import type { ResourceError } from 'lib/api/resources';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import isCustomAppError from 'ui/shared/AppError/isCustomAppError';
import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import * as DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import DetailsTimestamp from 'ui/shared/DetailsTimestamp';
import LinkInternal from 'ui/shared/links/LinkInternal';
import PrevNext from 'ui/shared/PrevNext';
import OptimisticL2TxnBatchBlobCallData from './OptimisticL2TxnBatchBlobCallData';
import OptimisticL2TxnBatchBlobCelestia from './OptimisticL2TxnBatchBlobCelestia';
import OptimisticL2TxnBatchBlobEip4844 from './OptimisticL2TxnBatchBlobEip4844';
interface Props {
query: UseQueryResult<OptimismL2TxnBatch, ResourceError>;
}
const OptimisticL2TxnBatchDetails = ({ query }: Props) => {
const router = useRouter();
const { data, isError, error, isPlaceholderData } = query;
const handlePrevNextClick = React.useCallback((direction: 'prev' | 'next') => {
if (!data) {
return;
}
const increment = direction === 'next' ? +1 : -1;
const nextId = String(data.internal_id + increment);
router.push({ pathname: '/batches/[number]', query: { number: nextId } }, undefined);
}, [ data, router ]);
if (isError) {
if (isCustomAppError(error)) {
throwOnResourceLoadError({ isError, error });
}
return <DataFetchAlert/>;
}
if (!data) {
return null;
}
const blocksCount = data.l2_block_end - data.l2_block_start + 1;
return (
<Grid
columnGap={ 8 }
rowGap={{ base: 3, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'minmax(min-content, 200px) minmax(0, 1fr)' }}
overflow="hidden"
>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Batch ID indicates the length of batches produced by grouping L2 blocks to be proven on L1"
>
Batch ID
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
{ data.internal_id }
</Skeleton>
<PrevNext
ml={ 6 }
onClick={ handlePrevNextClick }
prevLabel="View previous tx batch"
nextLabel="View next tx batch"
isPrevDisabled={ data.internal_id === 0 }
isLoading={ isPlaceholderData }
/>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Date and time at which batch is submitted to L1"
>
Timestamp
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
{ data.l1_timestamp ?
<DetailsTimestamp timestamp={ data.l1_timestamp }isLoading={ isPlaceholderData }/> :
'Undefined'
}
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Number of transactions in this batch"
>
Transactions
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
<LinkInternal href={ route({ pathname: '/batches/[number]', query: { number: data.internal_id.toString(), tab: 'txs' } }) }>
{ data.tx_count.toLocaleString() } transaction{ data.tx_count === 1 ? '' : 's' }
</LinkInternal>
{ ' ' }in this batch
</Skeleton>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Number of L2 blocks in this batch"
>
Blocks
</DetailsInfoItem.Label>
<DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }>
<LinkInternal href={ route({ pathname: '/batches/[number]', query: { number: data.internal_id.toString(), tab: 'blocks' } }) }>
{ blocksCount.toLocaleString() } block{ blocksCount === 1 ? '' : 's' }
</LinkInternal>
{ ' ' }in this batch
</Skeleton>
</DetailsInfoItem.Value>
<DetailsInfoItem.Label
isLoading={ isPlaceholderData }
hint="Where the batch data is stored"
>
Batch data container
</DetailsInfoItem.Label>
<DetailsInfoItem.Value flexDir="column" alignItems="flex-start" rowGap={ 2 }>
<OptimisticL2TxnBatchDA container={ data.batch_data_container } isLoading={ isPlaceholderData }/>
{ data.batch_data_container === 'in_blob4844' && data.blobs &&
<OptimisticL2TxnBatchBlobEip4844 blobs={ data.blobs } isLoading={ isPlaceholderData }/> }
{ data.batch_data_container === 'in_calldata' && (
<OptimisticL2TxnBatchBlobCallData
l1TxHashes={ data.l1_tx_hashes }
l1Timestamp={ data.l1_timestamp }
isLoading={ isPlaceholderData }
/>
) }
{ data.batch_data_container === 'in_celestia' && data.blobs &&
<OptimisticL2TxnBatchBlobCelestia blobs={ data.blobs } isLoading={ isPlaceholderData }/> }
</DetailsInfoItem.Value>
</Grid>
);
};
export default OptimisticL2TxnBatchDetails;
import { Skeleton, VStack } from '@chakra-ui/react';
import { Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
......@@ -6,8 +6,8 @@ import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import LinkInternal from 'ui/shared/links/LinkInternal';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
......@@ -24,52 +24,60 @@ const OptimisticL2TxnBatchesListItem = ({ item, isLoading }: Props) => {
return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto">
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 block #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px">
<BlockEntityL2
<ListItemMobileGrid.Label isLoading={ isLoading }>Batch ID</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<BatchEntityL2 number={ item.internal_id } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
{ item.batch_data_container && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>
Storage
</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<OptimisticL2TxnBatchDA container={ item.batch_data_container } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
</>
) }
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TimeAgoWithTooltip
timestamp={ item.l1_timestamp }
isLoading={ isLoading }
number={ item.l2_block_number }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
display="inline-block"
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 block txn count</ListItemMobileGrid.Label>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn count</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } minW="40px">
{ item.l1_tx_hashes.length }
</Skeleton>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 blocks</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<LinkInternal
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString(), tab: 'txs' } }) }
href={ route({ pathname: '/batches/[number]', query: { number: item.internal_id.toString(), tab: 'blocks' } }) }
isLoading={ isLoading }
>
<Skeleton isLoaded={ !isLoading } minW="40px">
{ item.tx_count }
{ item.l2_block_end - item.l2_block_start + 1 }
</Skeleton>
</LinkInternal>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L1 txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value py="3px">
<VStack spacing={ 3 } w="100%" overflow="hidden" alignItems="flex-start">
{ item.l1_tx_hashes.map(hash => (
<TxEntityL1
key={ hash }
isLoading={ isLoading }
hash={ hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
/>
)) }
</VStack>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 blocks</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TimeAgoWithTooltip
timestamp={ item.l1_timestamp }
<LinkInternal
href={ route({ pathname: '/batches/[number]', query: { number: item.internal_id.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
display="inline-block"
/>
>
<Skeleton isLoaded={ !isLoading } minW="40px">
{ item.tx_count }
</Skeleton>
</LinkInternal>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
......
......@@ -15,19 +15,21 @@ type Props = {
const OptimisticL2TxnBatchesTable = ({ items, top, isLoading }: Props) => {
return (
<Table variant="simple" size="sm" minW="850px">
<Table variant="simple" size="sm" minW="850px" layout="auto">
<Thead top={ top }>
<Tr>
<Th width="170px">L2 block #</Th>
<Th width="170px">L2 block txn count</Th>
<Th width="100%">L1 txn hash</Th>
<Th width="150px">Age</Th>
<Th>Batch ID</Th>
<Th >Storage</Th>
<Th >Age</Th>
<Th isNumeric>L1 txn count</Th>
<Th isNumeric>L2 blocks</Th>
<Th isNumeric>Txn</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => (
<OptimisticL2TxnBatchesTableItem
key={ item.l2_block_number + (isLoading ? String(index) : '') }
key={ item.internal_id + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
/>
......
import { Td, Tr, VStack, Skeleton } from '@chakra-ui/react';
import { Td, Tr, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
......@@ -6,8 +6,8 @@ import type { OptimisticL2TxnBatchesItem } from 'types/api/optimisticL2';
import { route } from 'nextjs-routes';
import config from 'configs/app';
import BlockEntityL2 from 'ui/shared/entities/block/BlockEntityL2';
import TxEntityL1 from 'ui/shared/entities/tx/TxEntityL1';
import OptimisticL2TxnBatchDA from 'ui/shared/batch/OptimisticL2TxnBatchDA';
import BatchEntityL2 from 'ui/shared/entities/block/BatchEntityL2';
import LinkInternal from 'ui/shared/links/LinkInternal';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
......@@ -22,49 +22,47 @@ const OptimisticL2TxnBatchesTableItem = ({ item, isLoading }: Props) => {
return (
<Tr>
<Td>
<BlockEntityL2
<Td verticalAlign="middle">
<BatchEntityL2 number={ item.internal_id } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle">
{ item.batch_data_container ? <OptimisticL2TxnBatchDA container={ item.batch_data_container } isLoading={ isLoading }/> : '-' }
</Td>
<Td verticalAlign="middle">
<TimeAgoWithTooltip
timestamp={ item.l1_timestamp }
isLoading={ isLoading }
number={ item.l2_block_number }
fontSize="sm"
lineHeight={ 5 }
fontWeight={ 600 }
noIcon
display="inline-block"
color="text_secondary"
my={ 1 }
/>
</Td>
<Td>
<Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading } minW="40px" display="inline-block">
{ item.l1_tx_hashes.length }
</Skeleton>
</Td>
<Td verticalAlign="middle" isNumeric>
<LinkInternal
href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: item.l2_block_number.toString(), tab: 'txs' } }) }
href={ route({ pathname: '/batches/[number]', query: { number: item.internal_id.toString(), tab: 'blocks' } }) }
isLoading={ isLoading }
justifyContent="flex-end"
>
<Skeleton isLoaded={ !isLoading } minW="40px" my={ 1 }>
{ item.tx_count }
<Skeleton isLoaded={ !isLoading } minW="40px" display="inline-block">
{ item.l2_block_end - item.l2_block_start + 1 }
</Skeleton>
</LinkInternal>
</Td>
<Td pr={ 12 }>
<VStack spacing={ 3 } alignItems="flex-start">
{ item.l1_tx_hashes.map(hash => (
<TxEntityL1
key={ hash }
isLoading={ isLoading }
hash={ hash }
fontSize="sm"
lineHeight={ 5 }
truncation="constant_long"
noIcon
/>
)) }
</VStack>
</Td>
<Td>
<TimeAgoWithTooltip
timestamp={ item.l1_timestamp }
<Td verticalAlign="middle" isNumeric>
<LinkInternal
href={ route({ pathname: '/batches/[number]', query: { number: item.internal_id.toString(), tab: 'txs' } }) }
isLoading={ isLoading }
display="inline-block"
color="text_secondary"
my={ 1 }
/>
justifyContent="flex-end"
>
<Skeleton isLoaded={ !isLoading } minW="40px" display="inline-block">
{ item.tx_count }
</Skeleton>
</LinkInternal>
</Td>
</Tr>
);
......
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