Commit 2ac9ac87 authored by Max Alekseenko's avatar Max Alekseenko

Merge remote-tracking branch 'origin/tom2drum/issue-2029' into rewards

parents 379777bc 5552cd6d
...@@ -16,6 +16,7 @@ const bannerContentUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_BANNE ...@@ -16,6 +16,7 @@ const bannerContentUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_BANNE
const bannerLinkUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL'); const bannerLinkUrl = getEnvValue('NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL');
const ratingAirtableApiKey = getEnvValue('NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY'); const ratingAirtableApiKey = getEnvValue('NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY');
const ratingAirtableBaseId = getEnvValue('NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID'); const ratingAirtableBaseId = getEnvValue('NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID');
const graphLinksUrl = getExternalAssetFilePath('NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL');
const title = 'Marketplace'; const title = 'Marketplace';
...@@ -30,6 +31,7 @@ const config: Feature<( ...@@ -30,6 +31,7 @@ const config: Feature<(
featuredApp: string | undefined; featuredApp: string | undefined;
banner: { contentUrl: string; linkUrl: string } | undefined; banner: { contentUrl: string; linkUrl: string } | undefined;
rating: { airtableApiKey: string; airtableBaseId: string } | undefined; rating: { airtableApiKey: string; airtableBaseId: string } | undefined;
graphLinksUrl: string | undefined;
}> = (() => { }> = (() => {
if (enabled === 'true' && chain.rpcUrl && submitFormUrl) { if (enabled === 'true' && chain.rpcUrl && submitFormUrl) {
const props = { const props = {
...@@ -46,6 +48,7 @@ const config: Feature<( ...@@ -46,6 +48,7 @@ const config: Feature<(
airtableApiKey: ratingAirtableApiKey, airtableApiKey: ratingAirtableApiKey,
airtableBaseId: ratingAirtableBaseId, airtableBaseId: ratingAirtableBaseId,
} : undefined, } : undefined,
graphLinksUrl,
}; };
if (configUrl) { if (configUrl) {
......
...@@ -16,7 +16,7 @@ const L2WithdrawalUrl = getEnvValue('NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL'); ...@@ -16,7 +16,7 @@ const L2WithdrawalUrl = getEnvValue('NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL');
const title = 'Rollup (L2) chain'; const title = 'Rollup (L2) chain';
const config: Feature<{ type: RollupType; L1BaseUrl: string; L2WithdrawalUrl?: string }> = (() => { const config: Feature<{ type: RollupType; L1BaseUrl: string; L2WithdrawalUrl?: string; homepage: { showLatestBlocks: boolean } }> = (() => {
if (type && L1BaseUrl) { if (type && L1BaseUrl) {
return Object.freeze({ return Object.freeze({
...@@ -25,6 +25,9 @@ const config: Feature<{ type: RollupType; L1BaseUrl: string; L2WithdrawalUrl?: s ...@@ -25,6 +25,9 @@ const config: Feature<{ type: RollupType; L1BaseUrl: string; L2WithdrawalUrl?: s
type, type,
L1BaseUrl: stripTrailingSlash(L1BaseUrl), L1BaseUrl: stripTrailingSlash(L1BaseUrl),
L2WithdrawalUrl, L2WithdrawalUrl,
homepage: {
showLatestBlocks: getEnvValue('NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS') === 'true',
},
}); });
} }
......
...@@ -42,6 +42,7 @@ NEXT_PUBLIC_MARKETPLACE_ENABLED=true ...@@ -42,6 +42,7 @@ NEXT_PUBLIC_MARKETPLACE_ENABLED=true
NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-security-reports/default.json
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/appiy5yijZpMMSKjT/shr6uMGPKjj1DK7NL
NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form NEXT_PUBLIC_MARKETPLACE_SUGGEST_IDEAS_FORM=https://airtable.com/appiy5yijZpMMSKjT/pag3t82DUCyhGRZZO/form
NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL=https://raw.githubusercontent.com/blockscout/frontend-configs/refs/heads/marketplace-graph-test/test-configs/marketplace-graph-links.json
NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com NEXT_PUBLIC_METADATA_SERVICE_API_HOST=https://metadata.services.blockscout.com
NEXT_PUBLIC_METASUITES_ENABLED=true NEXT_PUBLIC_METASUITES_ENABLED=true
NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'} NEXT_PUBLIC_MULTICHAIN_BALANCE_PROVIDER_CONFIG={'name': 'zerion', 'dapp_id': 'zerion', 'url_template': 'https://app.zerion.io/{address}/overview?utm_source=blockscout&utm_medium=address', 'logo': 'https://raw.githubusercontent.com/blockscout/frontend-configs/main/configs/marketplace-logos/zerion.svg'}
......
...@@ -18,6 +18,7 @@ ASSETS_ENVS=( ...@@ -18,6 +18,7 @@ ASSETS_ENVS=(
"NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL" "NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL"
"NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL" "NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL"
"NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL" "NEXT_PUBLIC_MARKETPLACE_BANNER_CONTENT_URL"
"NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL"
"NEXT_PUBLIC_FEATURED_NETWORKS" "NEXT_PUBLIC_FEATURED_NETWORKS"
"NEXT_PUBLIC_FOOTER_LINKS" "NEXT_PUBLIC_FOOTER_LINKS"
"NEXT_PUBLIC_NETWORK_LOGO" "NEXT_PUBLIC_NETWORK_LOGO"
......
...@@ -39,6 +39,7 @@ async function validateEnvs(appEnvs: Record<string, string>) { ...@@ -39,6 +39,7 @@ async function validateEnvs(appEnvs: Record<string, string>) {
'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL', 'NEXT_PUBLIC_MARKETPLACE_CONFIG_URL',
'NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL', 'NEXT_PUBLIC_MARKETPLACE_CATEGORIES_URL',
'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL', 'NEXT_PUBLIC_MARKETPLACE_SECURITY_REPORTS_URL',
'NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL',
'NEXT_PUBLIC_FOOTER_LINKS', 'NEXT_PUBLIC_FOOTER_LINKS',
]; ];
......
...@@ -243,6 +243,14 @@ const marketplaceSchema = yup ...@@ -243,6 +243,14 @@ const marketplaceSchema = yup
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'), otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
}), }),
NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL: yup
.string()
.when('NEXT_PUBLIC_MARKETPLACE_ENABLED', {
is: true,
then: (schema) => schema,
// eslint-disable-next-line max-len
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL cannot not be used without NEXT_PUBLIC_MARKETPLACE_ENABLED'),
}),
}); });
const beaconChainSchema = yup const beaconChainSchema = yup
...@@ -279,6 +287,17 @@ const rollupSchema = yup ...@@ -279,6 +287,17 @@ const rollupSchema = yup
then: (schema) => schema.test(urlTest).required(), then: (schema) => schema.test(urlTest).required(),
otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL can be used only if NEXT_PUBLIC_ROLLUP_TYPE is set to \'optimistic\' '), otherwise: (schema) => schema.max(-1, 'NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL can be used only if NEXT_PUBLIC_ROLLUP_TYPE is set to \'optimistic\' '),
}), }),
NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS: yup
.boolean()
.when('NEXT_PUBLIC_ROLLUP_TYPE', {
is: (value: string) => value,
then: (schema) => schema,
otherwise: (schema) => schema.test(
'not-exist',
'NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS cannot not be used if NEXT_PUBLIC_ROLLUP_TYPE is not defined',
value => value === undefined,
),
}),
}); });
const adButlerConfigSchema = yup const adButlerConfigSchema = yup
......
NEXT_PUBLIC_ROLLUP_TYPE=optimistic NEXT_PUBLIC_ROLLUP_TYPE=optimistic
NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://example.com NEXT_PUBLIC_ROLLUP_L1_BASE_URL=https://example.com
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://example.com NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL=https://example.com
NEXT_PUBLIC_FAULT_PROOF_ENABLED=true NEXT_PUBLIC_FAULT_PROOF_ENABLED=true
\ No newline at end of file NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS=true
\ No newline at end of file
...@@ -435,6 +435,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi ...@@ -435,6 +435,7 @@ This feature is **enabled by default** with the `coinzilla` ads provider. To swi
| NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals (Optimistic stack only) | Required for `optimistic` rollups | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0+ | | NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL | `string` | URL for L2 -> L1 withdrawals (Optimistic stack only) | Required for `optimistic` rollups | - | `https://app.optimism.io/bridge/withdraw` | v1.24.0+ |
| NEXT_PUBLIC_FAULT_PROOF_ENABLED | `boolean` | Set to `true` for chains with fault proof system enabled (Optimistic stack only) | - | - | `true` | v1.31.0+ | | NEXT_PUBLIC_FAULT_PROOF_ENABLED | `boolean` | Set to `true` for chains with fault proof system enabled (Optimistic stack only) | - | - | `true` | v1.31.0+ |
| NEXT_PUBLIC_HAS_MUD_FRAMEWORK | `boolean` | Set to `true` for instances that use MUD framework (Optimistic stack only) | - | - | `true` | v1.33.0+ | | NEXT_PUBLIC_HAS_MUD_FRAMEWORK | `boolean` | Set to `true` for instances that use MUD framework (Optimistic stack only) | - | - | `true` | v1.33.0+ |
| NEXT_PUBLIC_ROLLUP_HOMEPAGE_SHOW_LATEST_BLOCKS | `boolean` | Set to `true` to display "Latest blocks" widget instead of "Latest batches" on the home page | - | - | `true` | v1.36.0+ |
&nbsp; &nbsp;
...@@ -507,6 +508,7 @@ This feature is **always enabled**, but you can disable it by passing `none` val ...@@ -507,6 +508,7 @@ This feature is **always enabled**, but you can disable it by passing `none` val
| NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL | `string` | URL of the page the banner leads to | - | - | `https://example.com` | v1.29.0+ | | NEXT_PUBLIC_MARKETPLACE_BANNER_LINK_URL | `string` | URL of the page the banner leads to | - | - | `https://example.com` | v1.29.0+ |
| NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY | `string` | Airtable API key | - | - | - | v1.33.0+ | | NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_API_KEY | `string` | Airtable API key | - | - | - | v1.33.0+ |
| NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID | `string` | Airtable base ID with dapp ratings | - | - | - | v1.33.0+ | | NEXT_PUBLIC_MARKETPLACE_RATING_AIRTABLE_BASE_ID | `string` | Airtable base ID with dapp ratings | - | - | - | v1.33.0+ |
| NEXT_PUBLIC_MARKETPLACE_GRAPH_LINKS_URL | `string` | URL of the file (`.json` format only) which contains the list of The Graph links to be displayed on the Marketplace page | - | - | `https://example.com/graph_links.json` | v1.36.0+ |
#### Marketplace app configuration properties #### Marketplace app configuration properties
......
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path fill="#6747ED" d="M10 20c5.523 0 10-4.477 10-10S15.523 0 10 0 0 4.477 0 10s4.477 10 10 10"/>
<path fill="#fff" fill-rule="evenodd" d="M9.854 11.292a2.66 2.66 0 0 1-2.666-2.667 2.66 2.66 0 0 1 2.666-2.667 2.66 2.66 0 0 1 2.667 2.667 2.66 2.66 0 0 1-2.667 2.667m0-6.667a4.001 4.001 0 0 1 0 8 4.001 4.001 0 0 1 0-8m3.813 8.208c.27.271.27.688 0 .938L11 16.437c-.27.271-.687.271-.937 0-.271-.27-.271-.687 0-.937l2.666-2.667c.25-.27.688-.27.938 0m1.541-7.541a.66.66 0 0 1-.666.666.66.66 0 0 1-.667-.666c0-.375.292-.667.667-.667.354 0 .666.292.666.667" clip-rule="evenodd"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 30 30">
<path fill="currentColor" fill-rule="evenodd" d="m6.134 10.067 3.722-3.828a.97.97 0 0 1 1.337.06c.177.182.283.428.292.69a1.05 1.05 0 0 1-.234.704L9.243 9.765h14.371c.261 0 .513.107.7.299s.294.454.294.73c0 .274-.107.537-.294.729a.98.98 0 0 1-.7.299H6.831a.97.97 0 0 1-.55-.17 1 1 0 0 1-.368-.46 1.06 1.06 0 0 1 .22-1.126m18.723 6a.965.965 0 0 1 .55.17c.163.111.291.27.368.459a1.06 1.06 0 0 1-.22 1.125l-3.737 3.842-.006.008a1 1 0 0 1-.323.255.97.97 0 0 1-1.13-.197q-.146-.152-.224-.353a1.06 1.06 0 0 1 .281-1.16l.007-.007 2.022-2.086h-5.882c.104-.685.184-1.404 0-2.073z" clip-rule="evenodd"/>
<path fill="currentColor" d="M10 19.513c-1.272 0-2.48-.276-3.395-.778C5.57 18.169 5 17.374 5 16.497c0-.878.57-1.672 1.605-2.239.917-.502 2.123-.778 3.395-.778s2.48.276 3.395.778C14.43 14.825 15 15.622 15 16.497s-.57 1.671-1.605 2.238c-.917.502-2.123.778-3.395.778m0-4.793c-1.052 0-2.073.228-2.8.626-.61.334-.96.753-.96 1.15 0 .398.35.818.96 1.151.727.397 1.746.626 2.8.626s2.073-.228 2.8-.626c.61-.333.96-.753.96-1.15 0-.398-.35-.817-.96-1.151-.727-.398-1.746-.626-2.8-.626"/>
<path stroke="currentColor" stroke-width=".3" d="M10 19.513c-1.272 0-2.48-.276-3.395-.778C5.57 18.169 5 17.374 5 16.497c0-.878.57-1.672 1.605-2.239.917-.502 2.123-.778 3.395-.778s2.48.276 3.395.778C14.43 14.825 15 15.622 15 16.497s-.57 1.671-1.605 2.238c-.917.502-2.123.778-3.395.778Zm0-4.793c-1.052 0-2.073.228-2.8.626-.61.334-.96.753-.96 1.15 0 .398.35.818.96 1.151.727.397 1.746.626 2.8.626s2.073-.228 2.8-.626c.61-.333.96-.753.96-1.15 0-.398-.35-.817-.96-1.151-.727-.398-1.746-.626-2.8-.626Z"/>
<path fill="currentColor" d="M10 23.962c-1.272 0-2.48-.276-3.395-.778C5.57 22.618 5 21.823 5 20.946v-4.45a.62.62 0 0 1 1.24 0v4.45c0 .397.35.817.96 1.151.727.397 1.748.626 2.8.626 1.053 0 2.073-.229 2.8-.626.61-.334.96-.754.96-1.151v-4.45a.62.62 0 0 1 1.24 0v4.45c0 .877-.57 1.672-1.605 2.238-.917.502-2.123.778-3.395.778"/>
<path stroke="currentColor" stroke-width=".3" d="M10 23.962c-1.272 0-2.48-.276-3.395-.778C5.57 22.618 5 21.823 5 20.946v-4.45a.62.62 0 0 1 1.24 0v4.45c0 .397.35.817.96 1.151.727.397 1.748.626 2.8.626 1.053 0 2.073-.229 2.8-.626.61-.334.96-.754.96-1.151v-4.45a.62.62 0 0 1 1.24 0v4.45c0 .877-.57 1.672-1.605 2.238-.917.502-2.123.778-3.395.778Z"/>
<path fill="currentColor" d="M10 21.738c-1.272 0-2.48-.277-3.395-.778C5.57 20.393 5 19.598 5 18.72a.62.62 0 1 1 1.24 0c0 .397.35.817.96 1.151.727.398 1.748.626 2.8.626 1.053 0 2.073-.228 2.8-.626.61-.334.96-.754.96-1.15a.62.62 0 1 1 1.24 0c0 .876-.57 1.671-1.605 2.238-.917.501-2.123.778-3.395.778"/>
<path stroke="currentColor" stroke-width=".3" d="M10 21.738c-1.272 0-2.48-.277-3.395-.778C5.57 20.393 5 19.598 5 18.72a.62.62 0 1 1 1.24 0c0 .397.35.817.96 1.151.727.398 1.748.626 2.8.626 1.053 0 2.073-.228 2.8-.626.61-.334.96-.754.96-1.15a.62.62 0 1 1 1.24 0c0 .876-.57 1.671-1.605 2.238-.917.501-2.123.778-3.395.778Z"/>
</svg>
...@@ -37,6 +37,7 @@ import type { ...@@ -37,6 +37,7 @@ import type {
AddressMudRecordsFilter, AddressMudRecordsFilter,
AddressMudRecordsSorting, AddressMudRecordsSorting,
AddressMudRecord, AddressMudRecord,
AddressEpochRewardsResponse,
} from 'types/api/address'; } from 'types/api/address';
import type { AddressesResponse, AddressesMetadataSearchResult, AddressesMetadataSearchFilters } from 'types/api/addresses'; import type { AddressesResponse, AddressesMetadataSearchResult, AddressesMetadataSearchFilters } from 'types/api/addresses';
import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata'; import type { AddressMetadataInfo, PublicTagTypesResponse } from 'types/api/addressMetadata';
...@@ -587,6 +588,11 @@ export const RESOURCES = { ...@@ -587,6 +588,11 @@ export const RESOURCES = {
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
filterFields: [], filterFields: [],
}, },
address_epoch_rewards: {
path: '/api/v2/addresses/:hash/election-rewards',
pathParams: [ 'hash' as const ],
filterFields: [],
},
// CONTRACT // CONTRACT
contract: { contract: {
...@@ -681,6 +687,12 @@ export const RESOURCES = { ...@@ -681,6 +687,12 @@ export const RESOURCES = {
filterFields: [], filterFields: [],
}, },
// TOKEN TRANSFERS
token_transfers_all: {
path: '/api/v2/token-transfers',
filterFields: [ 'type' as const ],
},
// APP STATS // APP STATS
stats: { stats: {
path: '/api/v2/stats', path: '/api/v2/stats',
...@@ -1084,7 +1096,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward ...@@ -1084,7 +1096,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward
'addresses' | 'addresses_metadata_search' | 'addresses' | 'addresses_metadata_search' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' | 'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' | 'search' |
'address_logs' | 'address_tokens' | 'address_nfts' | 'address_collections' | 'address_logs' | 'address_tokens' | 'address_nfts' | 'address_collections' | 'address_epoch_rewards' |
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' | 'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' |
'token_instance_transfers' | 'token_instance_holders' | 'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' | 'verified_contracts' |
...@@ -1097,7 +1109,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward ...@@ -1097,7 +1109,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward
'zksync_l2_txn_batches' | 'zksync_l2_txn_batch_txs' | 'zksync_l2_txn_batches' | 'zksync_l2_txn_batch_txs' |
'withdrawals' | 'address_withdrawals' | 'block_withdrawals' | 'withdrawals' | 'address_withdrawals' | 'block_withdrawals' |
'watchlist' | 'private_tags_address' | 'private_tags_tx' | 'watchlist' | 'private_tags_address' | 'private_tags_tx' |
'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators_stability' | 'validators_blackfort' | 'noves_address_history'; 'domains_lookup' | 'addresses_lookup' | 'user_ops' | 'validators_stability' | 'validators_blackfort' | 'noves_address_history' |
'token_transfers_all';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -1264,6 +1277,7 @@ Q extends 'address_mud_tables' ? AddressMudTables : ...@@ -1264,6 +1277,7 @@ Q extends 'address_mud_tables' ? AddressMudTables :
Q extends 'address_mud_tables_count' ? number : Q extends 'address_mud_tables_count' ? number :
Q extends 'address_mud_records' ? AddressMudRecords : Q extends 'address_mud_records' ? AddressMudRecords :
Q extends 'address_mud_record' ? AddressMudRecord : Q extends 'address_mud_record' ? AddressMudRecord :
Q extends 'address_epoch_rewards' ? AddressEpochRewardsResponse :
Q extends 'withdrawals' ? WithdrawalsResponse : Q extends 'withdrawals' ? WithdrawalsResponse :
Q extends 'withdrawals_counters' ? WithdrawalsCounters : Q extends 'withdrawals_counters' ? WithdrawalsCounters :
Q extends 'rewards_config' ? RewardsConfigResponse : Q extends 'rewards_config' ? RewardsConfigResponse :
...@@ -1275,6 +1289,7 @@ Q extends 'rewards_user_balances' ? RewardsUserBalancesResponse : ...@@ -1275,6 +1289,7 @@ Q extends 'rewards_user_balances' ? RewardsUserBalancesResponse :
Q extends 'rewards_user_daily_check' ? RewardsUserDailyCheckResponse : Q extends 'rewards_user_daily_check' ? RewardsUserDailyCheckResponse :
Q extends 'rewards_user_daily_claim' ? RewardsUserDailyClaimResponse : Q extends 'rewards_user_daily_claim' ? RewardsUserDailyClaimResponse :
Q extends 'rewards_user_referrals' ? RewardsUserReferralsResponse : Q extends 'rewards_user_referrals' ? RewardsUserReferralsResponse :
Q extends 'token_transfers_all' ? TokenTransferResponse :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -1309,6 +1324,7 @@ Q extends 'user_ops' ? UserOpsFilters : ...@@ -1309,6 +1324,7 @@ Q extends 'user_ops' ? UserOpsFilters :
Q extends 'validators_stability' ? ValidatorsStabilityFilters : Q extends 'validators_stability' ? ValidatorsStabilityFilters :
Q extends 'address_mud_tables' ? AddressMudTablesFilter : Q extends 'address_mud_tables' ? AddressMudTablesFilter :
Q extends 'address_mud_records' ? AddressMudRecordsFilter : Q extends 'address_mud_records' ? AddressMudRecordsFilter :
Q extends 'token_transfers_all' ? TokenTransferFilters :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
......
import { useQuery } from '@tanstack/react-query';
import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources';
import useFetch from 'lib/hooks/useFetch';
const feature = config.features.marketplace;
export default function useGraphLinks() {
const fetch = useFetch();
return useQuery<unknown, ResourceError<unknown>, Record<string, Array<{text: string; url: string}>>>({
queryKey: [ 'graph-links' ],
queryFn: async() => fetch((feature.isEnabled && feature.graphLinksUrl) ? feature.graphLinksUrl : '', undefined, { resource: 'graph-links' }),
enabled: feature.isEnabled && Boolean(feature.graphLinksUrl),
staleTime: Infinity,
placeholderData: {},
});
}
...@@ -187,6 +187,21 @@ export default function useNavItems(): ReturnType { ...@@ -187,6 +187,21 @@ export default function useNavItems(): ReturnType {
].filter(Boolean); ].filter(Boolean);
} }
const tokensNavItems = [
{
text: 'Tokens',
nextRoute: { pathname: '/tokens' as const },
icon: 'token',
isActive: pathname.startsWith('/token'),
},
{
text: 'Token transfers',
nextRoute: { pathname: '/token-transfers' as const },
icon: 'token-transfers',
isActive: pathname === '/token-transfers',
},
];
const apiNavItems: Array<NavItem> = [ const apiNavItems: Array<NavItem> = [
config.features.restApiDocs.isEnabled ? { config.features.restApiDocs.isEnabled ? {
text: 'REST API', text: 'REST API',
...@@ -240,9 +255,9 @@ export default function useNavItems(): ReturnType { ...@@ -240,9 +255,9 @@ export default function useNavItems(): ReturnType {
}, },
{ {
text: 'Tokens', text: 'Tokens',
nextRoute: { pathname: '/tokens' as const },
icon: 'token', icon: 'token',
isActive: pathname.startsWith('/token'), isActive: tokensNavItems.flat().some(item => isInternalItem(item) && item.isActive),
subItems: tokensNavItems,
}, },
config.features.marketplace.isEnabled ? { config.features.marketplace.isEnabled ? {
text: 'DApps', text: 'DApps',
......
...@@ -52,6 +52,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -52,6 +52,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = {
'/validators': 'Root page', '/validators': 'Root page',
'/gas-tracker': 'Root page', '/gas-tracker': 'Root page',
'/mud-worlds': 'Root page', '/mud-worlds': 'Root page',
'/token-transfers': 'Root page',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': 'Regular page', '/login': 'Regular page',
......
...@@ -56,6 +56,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -56,6 +56,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/validators': DEFAULT_TEMPLATE, '/validators': DEFAULT_TEMPLATE,
'/gas-tracker': DEFAULT_TEMPLATE, '/gas-tracker': DEFAULT_TEMPLATE,
'/mud-worlds': DEFAULT_TEMPLATE, '/mud-worlds': DEFAULT_TEMPLATE,
'/token-transfers': DEFAULT_TEMPLATE,
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': DEFAULT_TEMPLATE, '/login': DEFAULT_TEMPLATE,
......
...@@ -41,8 +41,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -41,8 +41,8 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/deposits': '%network_name% deposits (L1 > L2)', '/deposits': '%network_name% deposits (L1 > L2)',
'/output-roots': '%network_name% output roots', '/output-roots': '%network_name% output roots',
'/dispute-games': '%network_name% dispute games', '/dispute-games': '%network_name% dispute games',
'/batches': '%network_name% tx batches (L2 blocks)', '/batches': '%network_name% txn batches',
'/batches/[number]': '%network_name% L2 tx batch %number%', '/batches/[number]': '%network_name% L2 txn batch %number%',
'/blobs/[hash]': '%network_name% blob %hash% details', '/blobs/[hash]': '%network_name% blob %hash% details',
'/ops': 'User operations on %network_name% - %network_name% explorer', '/ops': 'User operations on %network_name% - %network_name% explorer',
'/op/[hash]': '%network_name% user operation %hash%', '/op/[hash]': '%network_name% user operation %hash%',
...@@ -52,6 +52,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -52,6 +52,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = {
'/validators': '%network_name% validators list', '/validators': '%network_name% validators list',
'/gas-tracker': '%network_name% gas tracker - Current gas fees', '/gas-tracker': '%network_name% gas tracker - Current gas fees',
'/mud-worlds': '%network_name% MUD worlds list', '/mud-worlds': '%network_name% MUD worlds list',
'/token-transfers': '%network_name% token transfers',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': '%network_name% login', '/login': '%network_name% login',
......
...@@ -39,8 +39,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -39,8 +39,8 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/deposits': 'Deposits (L1 > L2)', '/deposits': 'Deposits (L1 > L2)',
'/output-roots': 'Output roots', '/output-roots': 'Output roots',
'/dispute-games': 'Dispute games', '/dispute-games': 'Dispute games',
'/batches': 'Tx batches (L2 blocks)', '/batches': 'Txn batches',
'/batches/[number]': 'L2 tx batch details', '/batches/[number]': 'L2 txn batch details',
'/blobs/[hash]': 'Blob details', '/blobs/[hash]': 'Blob details',
'/ops': 'User operations', '/ops': 'User operations',
'/op/[hash]': 'User operation details', '/op/[hash]': 'User operation details',
...@@ -50,6 +50,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -50,6 +50,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = {
'/validators': 'Validators list', '/validators': 'Validators list',
'/gas-tracker': 'Gas tracker', '/gas-tracker': 'Gas tracker',
'/mud-worlds': 'MUD worlds', '/mud-worlds': 'MUD worlds',
'/token-transfers': 'Token transfers',
// service routes, added only to make typescript happy // service routes, added only to make typescript happy
'/login': 'Login', '/login': 'Login',
......
import _get from 'lodash/get';
import React from 'react'; import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
...@@ -24,9 +25,10 @@ export default function useAddOrSwitchChain() { ...@@ -24,9 +25,10 @@ export default function useAddOrSwitchChain() {
const errorObj = getErrorObj(error); const errorObj = getErrorObj(error);
const code = errorObj && 'code' in errorObj ? errorObj.code : undefined; const code = errorObj && 'code' in errorObj ? errorObj.code : undefined;
const originalErrorCode = _get(errorObj, 'data.originalError.code');
// This error code indicates that the chain has not been added to Wallet. // This error code indicates that the chain has not been added to Wallet.
if (code === 4902) { if (code === 4902 || originalErrorCode === 4902) {
const params = [ { const params = [ {
chainId: hexadecimalChainId, chainId: hexadecimalChainId,
chainName: config.chain.name, chainName: config.chain.name,
......
...@@ -48,12 +48,12 @@ export default function useWeb3Wallet({ source }: Params) { ...@@ -48,12 +48,12 @@ export default function useWeb3Wallet({ source }: Params) {
const isConnected = isClientLoaded && !isDisconnected && address !== undefined; const isConnected = isClientLoaded && !isDisconnected && address !== undefined;
return { return React.useMemo(() => ({
connect: handleConnect, connect: handleConnect,
disconnect: handleDisconnect, disconnect: handleDisconnect,
isOpen: isOpening || isOpen, isOpen: isOpening || isOpen,
isConnected, isConnected,
address, address,
openModal, openModal,
}; }), [ handleConnect, handleDisconnect, isOpen, isOpening, isConnected, address, openModal ]);
} }
import type { AddressEpochRewardsResponse } from 'types/api/address';
import { tokenInfo } from 'mocks/tokens/tokenInfo';
import { withEns, withName, withoutName } from './address';
export const epochRewards: AddressEpochRewardsResponse = {
items: [
{
type: 'delegated_payment',
amount: '136609473658452408568',
account: withName,
associated_account: withName,
block_hash: '0x',
block_number: 26369280,
epoch_number: 1526,
token: tokenInfo,
},
{
type: 'group',
amount: '117205842355246195095',
account: withoutName,
associated_account: withoutName,
block_hash: '0x',
block_number: 26352000,
epoch_number: 1525,
token: tokenInfo,
},
{
type: 'validator',
amount: '125659647325556554060',
account: withEns,
associated_account: withEns,
block_hash: '0x',
block_number: 26300160,
epoch_number: 1524,
token: tokenInfo,
},
],
next_page_params: null,
};
...@@ -102,3 +102,14 @@ export const noteTag: AddressMetadataTagApi = { ...@@ -102,3 +102,14 @@ export const noteTag: AddressMetadataTagApi = {
data: '<b>Warning!</b> This is scam! See the <a href="https://example.com" target="_blank">report</a>', data: '<b>Warning!</b> This is scam! See the <a href="https://example.com" target="_blank">report</a>',
}, },
}; };
export const noteTag2: AddressMetadataTagApi = {
slug: 'note0',
name: 'note_0',
tagType: 'note',
ordinal: 0,
meta: {
alertStatus: 'info',
data: 'The token MILF was launched on May 13, 2021. The maximum total supply of the token is 100 billion.',
},
};
...@@ -42,6 +42,7 @@ export const erc20: TokenTransfer = { ...@@ -42,6 +42,7 @@ export const erc20: TokenTransfer = {
tx_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', tx_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
type: 'token_transfer', type: 'token_transfer',
timestamp: '2022-10-10T14:34:30.000000Z', timestamp: '2022-10-10T14:34:30.000000Z',
block_number: '12345',
block_hash: '1', block_hash: '1',
log_index: '1', log_index: '1',
method: 'updateSmartAsset', method: 'updateSmartAsset',
...@@ -88,6 +89,7 @@ export const erc721: TokenTransfer = { ...@@ -88,6 +89,7 @@ export const erc721: TokenTransfer = {
tx_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc', tx_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc',
type: 'token_transfer', type: 'token_transfer',
timestamp: '2022-10-10T14:34:30.000000Z', timestamp: '2022-10-10T14:34:30.000000Z',
block_number: '12345',
block_hash: '1', block_hash: '1',
log_index: '1', log_index: '1',
method: 'updateSmartAsset', method: 'updateSmartAsset',
...@@ -136,6 +138,7 @@ export const erc1155A: TokenTransfer = { ...@@ -136,6 +138,7 @@ export const erc1155A: TokenTransfer = {
tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746', tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746',
type: 'token_minting', type: 'token_minting',
timestamp: '2022-10-10T14:34:30.000000Z', timestamp: '2022-10-10T14:34:30.000000Z',
block_number: '12345',
block_hash: '1', block_hash: '1',
log_index: '1', log_index: '1',
}; };
...@@ -214,6 +217,7 @@ export const erc404A: TokenTransfer = { ...@@ -214,6 +217,7 @@ export const erc404A: TokenTransfer = {
type: 'token_transfer', type: 'token_transfer',
method: 'swap', method: 'swap',
timestamp: '2022-10-10T14:34:30.000000Z', timestamp: '2022-10-10T14:34:30.000000Z',
block_number: '12345',
block_hash: '1', block_hash: '1',
log_index: '1', log_index: '1',
}; };
......
...@@ -58,6 +58,7 @@ declare module "nextjs-routes" { ...@@ -58,6 +58,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/stats"> | StaticRoute<"/stats">
| DynamicRoute<"/token/[hash]", { "hash": string }> | DynamicRoute<"/token/[hash]", { "hash": string }>
| DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }> | DynamicRoute<"/token/[hash]/instance/[id]", { "hash": string; "id": string }>
| StaticRoute<"/token-transfers">
| StaticRoute<"/tokens"> | StaticRoute<"/tokens">
| DynamicRoute<"/tx/[hash]", { "hash": string }> | DynamicRoute<"/tx/[hash]", { "hash": string }>
| StaticRoute<"/txs"> | StaticRoute<"/txs">
......
...@@ -29,6 +29,7 @@ const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => { ...@@ -29,6 +29,7 @@ const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => {
setCookie?.forEach((value) => { setCookie?.forEach((value) => {
nextRes.appendHeader('set-cookie', value); nextRes.appendHeader('set-cookie', value);
}); });
nextRes.setHeader('content-type', apiRes.headers.get('content-type') || '');
nextRes.status(apiRes.status).send(apiRes.body); nextRes.status(apiRes.status).send(apiRes.body);
}; };
......
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import React from 'react';
import PageNextJs from 'nextjs/PageNextJs';
const TokenTransfers = dynamic(() => import('ui/pages/TokenTransfers'), { ssr: false });
const Page: NextPage = () => {
return (
<PageNextJs pathname="/token-transfers">
<TokenTransfers/>
</PageNextJs>
);
};
export default Page;
export { base as getServerSideProps } from 'nextjs/getServerSideProps';
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
| "block" | "block"
| "brands/blockscout" | "brands/blockscout"
| "brands/celenium" | "brands/celenium"
| "brands/graph"
| "brands/safe" | "brands/safe"
| "brands/solidity_scan" | "brands/solidity_scan"
| "burger" | "burger"
...@@ -154,6 +155,7 @@ ...@@ -154,6 +155,7 @@
| "swap" | "swap"
| "testnet" | "testnet"
| "token-placeholder" | "token-placeholder"
| "token-transfers"
| "token" | "token"
| "tokens" | "tokens"
| "tokens/xdai" | "tokens/xdai"
......
...@@ -3,6 +3,7 @@ import type { ...@@ -3,6 +3,7 @@ import type {
AddressCoinBalanceHistoryItem, AddressCoinBalanceHistoryItem,
AddressCollection, AddressCollection,
AddressCounters, AddressCounters,
AddressEpochRewardsItem,
AddressMudTableItem, AddressMudTableItem,
AddressNFT, AddressNFT,
AddressTabsCounters, AddressTabsCounters,
...@@ -10,7 +11,7 @@ import type { ...@@ -10,7 +11,7 @@ import type {
} from 'types/api/address'; } from 'types/api/address';
import type { AddressesItem } from 'types/api/addresses'; import type { AddressesItem } from 'types/api/addresses';
import { ADDRESS_HASH } from './addressParams'; import { ADDRESS_HASH, ADDRESS_PARAMS } from './addressParams';
import { MUD_SCHEMA, MUD_TABLE } from './mud'; import { MUD_SCHEMA, MUD_TABLE } from './mud';
import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_721, TOKEN_INFO_ERC_404, TOKEN_INSTANCE } from './token'; import { TOKEN_INFO_ERC_1155, TOKEN_INFO_ERC_20, TOKEN_INFO_ERC_721, TOKEN_INFO_ERC_404, TOKEN_INSTANCE } from './token';
import { TX_HASH } from './tx'; import { TX_HASH } from './tx';
...@@ -116,3 +117,14 @@ export const ADDRESS_MUD_TABLE_ITEM: AddressMudTableItem = { ...@@ -116,3 +117,14 @@ export const ADDRESS_MUD_TABLE_ITEM: AddressMudTableItem = {
schema: MUD_SCHEMA, schema: MUD_SCHEMA,
table: MUD_TABLE, table: MUD_TABLE,
}; };
export const EPOCH_REWARD_ITEM: AddressEpochRewardsItem = {
amount: '136609473658452408568',
block_number: 10355938,
type: 'voter',
token: TOKEN_INFO_ERC_20,
block_hash: '0x5956a847d8089e254e02e5111cad6992b99ceb9e5c2dc4343fd53002834c4dc6',
account: ADDRESS_PARAMS,
epoch_number: 1526,
associated_account: ADDRESS_PARAMS,
};
...@@ -91,6 +91,7 @@ export const getTokenInstanceHoldersStub = (type?: TokenType, pagination: TokenH ...@@ -91,6 +91,7 @@ export const getTokenInstanceHoldersStub = (type?: TokenType, pagination: TokenH
export const TOKEN_TRANSFER_ERC_20: TokenTransfer = { export const TOKEN_TRANSFER_ERC_20: TokenTransfer = {
block_hash: BLOCK_HASH, block_hash: BLOCK_HASH,
block_number: '123456',
from: ADDRESS_PARAMS, from: ADDRESS_PARAMS,
log_index: '4', log_index: '4',
method: 'addLiquidity', method: 'addLiquidity',
......
...@@ -154,33 +154,34 @@ const variantSubtle = defineStyle((props) => { ...@@ -154,33 +154,34 @@ const variantSubtle = defineStyle((props) => {
// for buttons in the hero banner // for buttons in the hero banner
const variantHero = defineStyle((props) => { const variantHero = defineStyle((props) => {
const buttonConfig = config.UI.homepage.heroBanner?.button;
return { return {
bg: mode( bg: mode(
config.UI.homepage.heroBanner?.button?._default?.background?.[0] || 'blue.600', buttonConfig?._default?.background?.[0] || 'blue.600',
config.UI.homepage.heroBanner?.button?._default?.background?.[1] || 'blue.600', buttonConfig?._default?.background?.[1] || buttonConfig?._default?.background?.[0] || 'blue.600',
)(props), )(props),
color: mode( color: mode(
config.UI.homepage.heroBanner?.button?._default?.text_color?.[0] || 'white', buttonConfig?._default?.text_color?.[0] || 'white',
config.UI.homepage.heroBanner?.button?._default?.text_color?.[1] || 'white', buttonConfig?._default?.text_color?.[1] || buttonConfig?._default?.text_color?.[0] || 'white',
)(props), )(props),
_hover: { _hover: {
bg: mode( bg: mode(
config.UI.homepage.heroBanner?.button?._hover?.background?.[0] || 'blue.400', buttonConfig?._hover?.background?.[0] || 'blue.400',
config.UI.homepage.heroBanner?.button?._hover?.background?.[1] || 'blue.400', buttonConfig?._hover?.background?.[1] || buttonConfig?._hover?.background?.[0] || 'blue.400',
)(props), )(props),
color: mode( color: mode(
config.UI.homepage.heroBanner?.button?._hover?.text_color?.[0] || 'white', buttonConfig?._hover?.text_color?.[0] || 'white',
config.UI.homepage.heroBanner?.button?._hover?.text_color?.[1] || 'white', buttonConfig?._hover?.text_color?.[1] || buttonConfig?._hover?.text_color?.[0] || 'white',
)(props), )(props),
}, },
'&[data-selected=true]': { '&[data-selected=true]': {
bg: mode( bg: mode(
config.UI.homepage.heroBanner?.button?._selected?.background?.[0] || 'blue.50', buttonConfig?._selected?.background?.[0] || 'blue.50',
config.UI.homepage.heroBanner?.button?._selected?.background?.[1] || 'blue.50', buttonConfig?._selected?.background?.[1] || buttonConfig?._selected?.background?.[0] || 'blue.50',
)(props), )(props),
color: mode( color: mode(
config.UI.homepage.heroBanner?.button?._selected?.text_color?.[0] || 'blackAlpha.800', buttonConfig?._selected?.text_color?.[0] || 'blackAlpha.800',
config.UI.homepage.heroBanner?.button?._selected?.text_color?.[1] || 'blackAlpha.800', buttonConfig?._selected?.text_color?.[1] || buttonConfig?._selected?.text_color?.[0] || 'blackAlpha.800',
)(props), )(props),
}, },
}; };
......
...@@ -30,39 +30,30 @@ const variantSimple = definePartsStyle((props) => { ...@@ -30,39 +30,30 @@ const variantSimple = definePartsStyle((props) => {
}); });
const sizes = { const sizes = {
md: definePartsStyle({
th: {
px: 4,
fontSize: 'sm',
},
td: {
p: 4,
},
}),
sm: definePartsStyle({ sm: definePartsStyle({
th: {
px: '10px',
py: '10px',
fontSize: 'sm',
},
td: {
px: '10px',
py: 4,
fontSize: 'sm',
fontWeight: 500,
},
}),
xs: definePartsStyle({
th: { th: {
px: '6px', px: '6px',
py: '10px', py: '10px',
fontSize: 'sm', fontSize: 'sm',
_first: {
pl: 3,
},
_last: {
pr: 3,
},
}, },
td: { td: {
px: '6px', px: '6px',
py: 4, py: 4,
fontSize: 'sm', fontSize: 'sm',
fontWeight: 500, fontWeight: 500,
lineHeight: 5,
_first: {
pl: 3,
},
_last: {
pr: 3,
},
}, },
}), }),
}; };
...@@ -104,6 +95,10 @@ const Table = defineMultiStyleConfig({ ...@@ -104,6 +95,10 @@ const Table = defineMultiStyleConfig({
baseStyle, baseStyle,
sizes, sizes,
variants, variants,
defaultProps: {
size: 'sm',
variant: 'simple',
},
}); });
export default Table; export default Table;
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
import type { UserTags, AddressImplementation } from './addressParams'; import type { UserTags, AddressImplementation, AddressParam } from './addressParams';
import type { Block } from './block'; import type { Block, EpochRewardsType } from './block';
import type { InternalTransaction } from './internalTransaction'; import type { InternalTransaction } from './internalTransaction';
import type { MudWorldSchema, MudWorldTable } from './mudWorlds'; import type { MudWorldSchema, MudWorldTable } from './mudWorlds';
import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token'; import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token';
...@@ -191,6 +191,7 @@ export type AddressTabsCounters = { ...@@ -191,6 +191,7 @@ export type AddressTabsCounters = {
transactions_count: number | null; transactions_count: number | null;
validations_count: number | null; validations_count: number | null;
withdrawals_count: number | null; withdrawals_count: number | null;
celo_election_rewards_count?: number | null;
} }
// MUD framework // MUD framework
...@@ -245,3 +246,25 @@ export type AddressMudRecord = { ...@@ -245,3 +246,25 @@ export type AddressMudRecord = {
schema: MudWorldSchema; schema: MudWorldSchema;
table: MudWorldTable; table: MudWorldTable;
} }
export type AddressEpochRewardsResponse = {
items: Array<AddressEpochRewardsItem>;
next_page_params: {
amount: string;
associated_account_address_hash: string;
block_number: number;
items_count: number;
type: EpochRewardsType;
} | null;
}
export type AddressEpochRewardsItem = {
type: EpochRewardsType;
token: TokenInfo;
amount: string;
block_number: number;
block_hash: string;
account: AddressParam;
epoch_number: number;
associated_account: AddressParam;
}
...@@ -144,6 +144,8 @@ export interface BlockEpochElectionReward { ...@@ -144,6 +144,8 @@ export interface BlockEpochElectionReward {
total: string; total: string;
} }
export type EpochRewardsType = 'group' | 'validator' | 'delegated_payment' | 'voter';
export interface BlockEpoch { export interface BlockEpoch {
number: number; number: number;
distribution: { distribution: {
...@@ -151,12 +153,7 @@ export interface BlockEpoch { ...@@ -151,12 +153,7 @@ export interface BlockEpoch {
community_transfer: TokenTransfer | null; community_transfer: TokenTransfer | null;
reserve_bolster_transfer: TokenTransfer | null; reserve_bolster_transfer: TokenTransfer | null;
}; };
aggregated_election_rewards: { aggregated_election_rewards: Record<EpochRewardsType, BlockEpochElectionReward | null>;
delegated_payment: BlockEpochElectionReward | null;
group: BlockEpochElectionReward | null;
validator: BlockEpochElectionReward | null;
voter: BlockEpochElectionReward | null;
};
} }
export interface BlockEpochElectionRewardDetails { export interface BlockEpochElectionRewardDetails {
......
...@@ -51,6 +51,7 @@ interface TokenTransferBase { ...@@ -51,6 +51,7 @@ interface TokenTransferBase {
from: AddressParam; from: AddressParam;
to: AddressParam; to: AddressParam;
timestamp: string; timestamp: string;
block_number: string;
block_hash: string; block_hash: string;
log_index: string; log_index: string;
method?: string; method?: string;
......
...@@ -87,7 +87,7 @@ const AddressAccountHistory = ({ scrollRef, shouldRender = true, isQueryEnabled ...@@ -87,7 +87,7 @@ const AddressAccountHistory = ({ scrollRef, shouldRender = true, isQueryEnabled
</Hide> </Hide>
<Show above="lg" ssr={ false }> <Show above="lg" ssr={ false }>
<Table variant="simple" > <Table>
<TheadSticky top={ 75 }> <TheadSticky top={ 75 }>
<Tr> <Tr>
<Th width="120px"> <Th width="120px">
......
...@@ -105,7 +105,7 @@ const AddressBlocksValidated = ({ scrollRef, shouldRender = true, isQueryEnabled ...@@ -105,7 +105,7 @@ const AddressBlocksValidated = ({ scrollRef, shouldRender = true, isQueryEnabled
const content = query.data?.items ? ( const content = query.data?.items ? (
<> <>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }}> <Table style={{ tableLayout: 'auto' }}>
<Thead top={ query.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }> <Thead top={ query.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }>
<Tr> <Tr>
<Th>Block</Th> <Th>Block</Th>
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import { epochRewards } from 'mocks/address/epochRewards';
import { test, expect } from 'playwright/lib';
import AddressEpochRewards from './AddressEpochRewards';
const ADDRESS_HASH = '0x1234';
const hooksConfig = {
router: {
query: { hash: ADDRESS_HASH },
},
};
test('base view +@mobile', async({ render, mockApiResponse }) => {
await mockApiResponse('address_epoch_rewards', epochRewards, { pathParams: { hash: ADDRESS_HASH } });
const component = await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressEpochRewards/>
</Box>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
import { Hide, Show } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString';
import { EPOCH_REWARD_ITEM } from 'stubs/address';
import { generateListStub } from 'stubs/utils';
import AddressEpochRewardsTable from 'ui/address/epochRewards/AddressEpochRewardsTable';
import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import AddressEpochRewardsListItem from './epochRewards/AddressEpochRewardsListItem';
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
shouldRender?: boolean;
isQueryEnabled?: boolean;
}
const AddressEpochRewards = ({ scrollRef, shouldRender = true, isQueryEnabled = true }: Props) => {
const router = useRouter();
const isMounted = useIsMounted();
const hash = getQueryParamString(router.query.hash);
const rewardsQuery = useQueryWithPages({
resourceName: 'address_epoch_rewards',
pathParams: {
hash,
},
scrollRef,
options: {
enabled: isQueryEnabled && Boolean(hash),
placeholderData: generateListStub<'address_epoch_rewards'>(EPOCH_REWARD_ITEM, 50, { next_page_params: {
amount: '1',
items_count: 50,
type: 'voter',
associated_account_address_hash: '1',
block_number: 10355938,
} }),
},
});
if (!isMounted || !shouldRender) {
return null;
}
const content = rewardsQuery.data?.items ? (
<>
<Hide below="lg" ssr={ false }>
<AddressEpochRewardsTable
items={ rewardsQuery.data.items }
top={ rewardsQuery.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }
isLoading={ rewardsQuery.isPlaceholderData }
/>
</Hide>
<Show below="lg" ssr={ false }>
{ rewardsQuery.data.items.map((item, index) => (
<AddressEpochRewardsListItem
key={ item.block_hash + item.type + item.account.hash + item.associated_account.hash + (rewardsQuery.isPlaceholderData ? String(index) : '') }
item={ item }
isLoading={ rewardsQuery.isPlaceholderData }
/>
)) }
</Show>
</>
) : null;
const actionBar = rewardsQuery.pagination.isVisible ? (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...rewardsQuery.pagination }/>
</ActionBar>
) : null;
return (
<DataListDisplay
isError={ rewardsQuery.isError }
items={ rewardsQuery.data?.items }
emptyText="There are no epoch rewards for this address."
content={ content }
actionBar={ actionBar }
/>
);
};
export default AddressEpochRewards;
import { Flex, Skeleton } from '@chakra-ui/react'; import { Text, Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -45,7 +45,9 @@ const AddressBlocksValidatedListItem = (props: Props) => { ...@@ -45,7 +45,9 @@ const AddressBlocksValidatedListItem = (props: Props) => {
</Flex> </Flex>
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Gas used</Skeleton> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Gas used</Skeleton>
<Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Skeleton> <Skeleton isLoaded={ !props.isLoading }>
<Text color="text_secondary">{ BigNumber(props.gas_used || 0).toFormat() }</Text>
</Skeleton>
<BlockGasUsed <BlockGasUsed
gasUsed={ props.gas_used } gasUsed={ props.gas_used }
gasLimit={ props.gas_limit } gasLimit={ props.gas_limit }
...@@ -55,7 +57,9 @@ const AddressBlocksValidatedListItem = (props: Props) => { ...@@ -55,7 +57,9 @@ const AddressBlocksValidatedListItem = (props: Props) => {
{ !config.UI.views.block.hiddenFields?.total_reward && !config.features.rollup.isEnabled && ( { !config.UI.views.block.hiddenFields?.total_reward && !config.features.rollup.isEnabled && (
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Reward { currencyUnits.ether }</Skeleton> <Skeleton isLoaded={ !props.isLoading } fontWeight={ 500 } flexShrink={ 0 }>Reward { currencyUnits.ether }</Skeleton>
<Skeleton isLoaded={ !props.isLoading } color="text_secondary">{ totalReward.toFixed() }</Skeleton> <Skeleton isLoaded={ !props.isLoading }>
<Text color="text_secondary">{ totalReward.toFixed() }</Text>
</Skeleton>
</Flex> </Flex>
) } ) }
</ListItemMobile> </ListItemMobile>
......
...@@ -57,7 +57,7 @@ const AddressBlocksValidatedTableItem = (props: Props) => { ...@@ -57,7 +57,7 @@ const AddressBlocksValidatedTableItem = (props: Props) => {
</Flex> </Flex>
</Td> </Td>
{ !config.UI.views.block.hiddenFields?.total_reward && !config.features.rollup.isEnabled && ( { !config.UI.views.block.hiddenFields?.total_reward && !config.features.rollup.isEnabled && (
<Td isNumeric display="flex" justifyContent="end"> <Td isNumeric>
<Skeleton isLoaded={ !props.isLoading } display="inline-block"> <Skeleton isLoaded={ !props.isLoading } display="inline-block">
<span>{ totalReward.toFixed() }</span> <span>{ totalReward.toFixed() }</span>
</Skeleton> </Skeleton>
......
...@@ -26,7 +26,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => { ...@@ -26,7 +26,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
const content = query.data?.items ? ( const content = query.data?.items ? (
<> <>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<Table variant="simple" size="sm"> <Table>
<Thead top={ query.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }> <Thead top={ query.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }>
<Tr> <Tr>
<Th width="20%">Block</Th> <Th width="20%">Block</Th>
......
...@@ -69,6 +69,17 @@ const abiItem: AbiFunction = { ...@@ -69,6 +69,17 @@ const abiItem: AbiFunction = {
name: 'internalProposals', name: 'internalProposals',
type: 'tuple[]', type: 'tuple[]',
}, },
// ARRAY OF TUPLES WITHOUT NAMES
{
components: [
{ type: 'address' },
{ type: 'uint256' },
],
internalType: 'struct SharingPercentage[]',
name: '_sharingPercentages',
type: 'tuple[]',
},
], ],
}; };
...@@ -115,6 +126,10 @@ const result = [ ...@@ -115,6 +126,10 @@ const result = [
}, },
}, },
], ],
[
[ '0xfD36176C63dA52E783a347DE3544B0b44C7054a6', 0 ],
[ '0xC9534cB913150aD3e98D792857689B55e2404212', 3500 ],
],
]; ];
const onSettle = () => {}; const onSettle = () => {};
......
...@@ -23,8 +23,20 @@ const ItemTuple = ({ abiParameter, data, mode, level }: Props) => { ...@@ -23,8 +23,20 @@ const ItemTuple = ({ abiParameter, data, mode, level }: Props) => {
<span> { '{' }</span> <span> { '{' }</span>
</p> </p>
{ 'components' in abiParameter && abiParameter.components.map((component, index) => { { 'components' in abiParameter && abiParameter.components.map((component, index) => {
const dataObj = typeof data === 'object' && data !== null ? data : undefined; const itemData = (() => {
const itemData = dataObj && component.name && component.name in dataObj ? dataObj[component.name as keyof typeof dataObj] : undefined; if (typeof data !== 'object' || data === null) {
return;
}
if (Array.isArray(data)) {
return data[index];
}
if (component.name && component.name in data) {
return data[component.name as keyof typeof data];
}
})();
return ( return (
<Item <Item
key={ index } key={ index }
......
...@@ -6,7 +6,7 @@ import { test, expect } from 'playwright/lib'; ...@@ -6,7 +6,7 @@ import { test, expect } from 'playwright/lib';
import AddressMetadataAlert from './AddressMetadataAlert'; import AddressMetadataAlert from './AddressMetadataAlert';
test('base view', async({ render }) => { test('base view', async({ render }) => {
const component = await render(<AddressMetadataAlert tags={ [ metadataMock.noteTag ] }/>); const component = await render(<AddressMetadataAlert tags={ [ metadataMock.noteTag, metadataMock.noteTag2 ] }/>);
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
import { Alert, chakra } from '@chakra-ui/react'; import { Alert, Flex, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata'; import type { AddressMetadataTagFormatted } from 'types/client/addressMetadata';
...@@ -9,35 +9,34 @@ interface Props { ...@@ -9,35 +9,34 @@ interface Props {
} }
const AddressMetadataAlert = ({ tags, className }: Props) => { const AddressMetadataAlert = ({ tags, className }: Props) => {
const noteTag = tags?.find(({ tagType }) => tagType === 'note'); const noteTags = tags?.filter(({ tagType }) => tagType === 'note').filter(({ meta }) => meta?.data);
if (!noteTag) {
return null;
}
const content = noteTag.meta?.data;
if (!content) { if (!noteTags?.length) {
return null; return null;
} }
return ( return (
<Alert <Flex flexDir="column" gap={ 3 } className={ className }>
className={ className } { noteTags.map((noteTag) => (
status={ noteTag.meta?.alertStatus ?? 'error' } <Alert
bgColor={ noteTag.meta?.alertBgColor } key={ noteTag.name }
color={ noteTag.meta?.alertTextColor } status={ noteTag.meta?.alertStatus ?? 'error' }
whiteSpace="pre-wrap" bgColor={ noteTag.meta?.alertBgColor }
display="inline-block" color={ noteTag.meta?.alertTextColor }
sx={{ whiteSpace="pre-wrap"
'& a': { display="inline-block"
color: 'link', sx={{
_hover: { '& a': {
color: 'link_hovered', color: 'link',
}, _hover: {
}, color: 'link_hovered',
}} },
dangerouslySetInnerHTML={{ __html: content }} },
/> }}
dangerouslySetInnerHTML={{ __html: noteTag.meta?.data ?? '' }}
/>
)) }
</Flex>
); );
}; };
......
import { Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { AddressEpochRewardsItem } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
type Props = {
item: AddressEpochRewardsItem;
isLoading?: boolean;
};
const AddressEpochRewardsListItem = ({ item, isLoading }: Props) => {
const { valueStr } = getCurrencyValue({ value: item.amount, accuracy: 2, decimals: item.token.decimals });
return (
<ListItemMobileGrid.Container gridTemplateColumns="100px auto">
<ListItemMobileGrid.Label isLoading={ isLoading }>Block</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<BlockEntity
number={ Number(item.block_number) }
isLoading={ isLoading }
noIcon
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Epoch #</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
{ item.epoch_number }
</ListItemMobileGrid.Value>
{ /* <ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TimeAgoWithTooltip
timestamp={ item.timestamp }
isLoading={ isLoading }
color="text_secondary"
display="inline-block"
/>
</ListItemMobileGrid.Value> */ }
<ListItemMobileGrid.Label isLoading={ isLoading }>Reward type</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<EpochRewardTypeTag type={ item.type } isLoading={ isLoading }/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Associated address</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<AddressEntity
address={ item.associated_account }
isLoading={ isLoading }
/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Value</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Skeleton isLoaded={ !isLoading } display="flex" alignItems="center" gap={ 2 }>
{ valueStr }
<TokenEntity token={ item.token } isLoading={ isLoading } onlySymbol width="auto" noCopy/>
</Skeleton>
</ListItemMobileGrid.Value>
</ListItemMobileGrid.Container>
);
};
export default AddressEpochRewardsListItem;
import { Table, Tbody, Th, Tr } from '@chakra-ui/react';
import React from 'react';
import type { AddressEpochRewardsItem } from 'types/api/address';
import { default as Thead } from 'ui/shared/TheadSticky';
import AddressEpochRewardsTableItem from './AddressEpochRewardsTableItem';
type Props = {
items: Array<AddressEpochRewardsItem>;
isLoading?: boolean;
top: number;
};
const AddressEpochRewardsTable = ({ items, isLoading, top }: Props) => {
return (
<Table minW="1000px" style={{ tableLayout: 'auto' }}>
<Thead top={ top }>
<Tr>
<Th>Block</Th>
<Th>Reward type</Th>
<Th>Associated address</Th>
<Th isNumeric>Value</Th>
</Tr>
</Thead>
<Tbody>
{ items.map((item, index) => {
return (
<AddressEpochRewardsTableItem
key={ item.block_hash + item.type + item.account.hash + item.associated_account.hash + (isLoading ? String(index) : '') }
item={ item }
isLoading={ isLoading }
/>
);
}) }
</Tbody>
</Table>
);
};
export default AddressEpochRewardsTable;
import { Flex, Td, Tr, Text, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { AddressEpochRewardsItem } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
type Props = {
item: AddressEpochRewardsItem;
isLoading?: boolean;
};
const AddressEpochRewardsTableItem = ({ item, isLoading }: Props) => {
const { valueStr } = getCurrencyValue({ value: item.amount, decimals: item.token.decimals });
return (
<Tr>
<Td verticalAlign="middle">
<Flex alignItems="center" gap={ 3 }>
<BlockEntity number={ item.block_number } isLoading={ isLoading } noIcon/>
<Text color="text_secondary" fontWeight={ 600 }>{ `Epoch # ${ item.epoch_number }` }</Text>
{ /* no timestamp from API, will be added later */ }
{ /* <TimeAgoWithTooltip timestamp={ item } isLoading={ isLoading }/> */ }
</Flex>
</Td>
<Td verticalAlign="middle">
<EpochRewardTypeTag type={ item.type } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle">
<AddressEntity address={ item.associated_account } isLoading={ isLoading }/>
</Td>
<Td verticalAlign="middle" isNumeric>
<Skeleton isLoaded={ !isLoading } display="flex" alignItems="center" gap={ 2 } justifyContent="flex-end">
{ valueStr }
<TokenEntity token={ item.token } isLoading={ isLoading } onlySymbol width="auto" noCopy/>
</Skeleton>
</Td>
</Tr>
);
};
export default AddressEpochRewardsTableItem;
...@@ -18,7 +18,7 @@ interface Props { ...@@ -18,7 +18,7 @@ interface Props {
const AddressIntTxsTable = ({ data, currentAddress, isLoading }: Props) => { const AddressIntTxsTable = ({ data, currentAddress, isLoading }: Props) => {
return ( return (
<AddressHighlightProvider> <AddressHighlightProvider>
<Table variant="simple" size="sm"> <Table>
<Thead top={ 68 }> <Thead top={ 68 }>
<Tr> <Tr>
<Th width="15%">Parent txn hash</Th> <Th width="15%">Parent txn hash</Th>
......
...@@ -26,9 +26,9 @@ const AddressMudRecordValues = ({ data }: Props) => { ...@@ -26,9 +26,9 @@ const AddressMudRecordValues = ({ data }: Props) => {
{ {
data?.schema.value_names.map((valName, index) => ( data?.schema.value_names.map((valName, index) => (
<Tr key={ valName } backgroundColor={ valuesBgColor } borderBottomStyle="hidden"> <Tr key={ valName } backgroundColor={ valuesBgColor } borderBottomStyle="hidden">
<Td fontSize="sm" w="100px" py={ 0 } pb={ 4 } pr={ 0 }wordBreak="break-all">{ valName }</Td> <Td fontWeight={ 400 } w="100px" py={ 0 } pb={ 4 } pr={ 0 }wordBreak="break-all">{ valName }</Td>
<Td fontSize="sm" w="90px" py={ 0 } pb={ 4 } wordBreak="break-all">{ data.schema.value_types[index] }</Td> <Td fontWeight={ 400 } w="90px" py={ 0 } pb={ 4 } wordBreak="break-all">{ data.schema.value_types[index] }</Td>
<Td fontSize="sm" wordBreak="break-word" py={ 0 } pb={ 4 }> <Td fontWeight={ 400 } wordBreak="break-word" py={ 0 } pb={ 4 }>
<Box> <Box>
{ getValueString(data.record.decoded[valName]) } { getValueString(data.record.decoded[valName]) }
</Box> </Box>
......
...@@ -140,7 +140,7 @@ const AddressMudRecordsTable = ({ ...@@ -140,7 +140,7 @@ const AddressMudRecordsTable = ({
return ( return (
// can't implement both horizontal table scroll and sticky header // can't implement both horizontal table scroll and sticky header
<Box maxW="100%" overflowX={ hasHorizontalScroll ? 'scroll' : 'unset' } whiteSpace="nowrap" ref={ tableRef }> <Box maxW="100%" overflowX={ hasHorizontalScroll ? 'scroll' : 'unset' } whiteSpace="nowrap" ref={ tableRef }>
<Table variant="simple" size="sm" style={{ tableLayout: 'fixed' }}> <Table style={{ tableLayout: 'fixed' }}>
<Thead top={ hasHorizontalScroll ? 0 : top } display={ hasHorizontalScroll ? 'table' : 'table-header-group' } w="100%"> <Thead top={ hasHorizontalScroll ? 0 : top } display={ hasHorizontalScroll ? 'table' : 'table-header-group' } w="100%">
<Tr > <Tr >
{ keys.map((keyName, index) => { { keys.map((keyName, index) => {
......
...@@ -18,7 +18,7 @@ type Props = { ...@@ -18,7 +18,7 @@ type Props = {
//sorry for the naming //sorry for the naming
const AddressMudTablesTable = ({ items, isLoading, top, scrollRef, hash }: Props) => { const AddressMudTablesTable = ({ items, isLoading, top, scrollRef, hash }: Props) => {
return ( return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }}> <Table style={{ tableLayout: 'auto' }}>
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th width="24px"></Th> <Th width="24px"></Th>
......
...@@ -15,7 +15,7 @@ interface Props { ...@@ -15,7 +15,7 @@ interface Props {
const ERC20TokensTable = ({ data, top, isLoading }: Props) => { const ERC20TokensTable = ({ data, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm"> <Table>
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th width="30%">Asset</Th> <Th width="30%">Asset</Th>
......
...@@ -21,7 +21,7 @@ interface Props { ...@@ -21,7 +21,7 @@ interface Props {
const AddressesTable = ({ items, totalSupply, pageStartIndex, top, isLoading }: Props) => { const AddressesTable = ({ items, totalSupply, pageStartIndex, top, isLoading }: Props) => {
const hasPercentage = !totalSupply.eq(ZERO); const hasPercentage = !totalSupply.eq(ZERO);
return ( return (
<Table variant="simple" size="sm"> <Table>
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th width="64px">Rank</Th> <Th width="64px">Rank</Th>
......
...@@ -16,7 +16,7 @@ interface Props { ...@@ -16,7 +16,7 @@ interface Props {
const AddressesLabelSearchTable = ({ items, top, isLoading }: Props) => { const AddressesLabelSearchTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm"> <Table>
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th width="70%">Address</Th> <Th width="70%">Address</Th>
......
...@@ -21,7 +21,7 @@ interface Props { ...@@ -21,7 +21,7 @@ interface Props {
const ApiKeyTable = ({ data, isLoading, onDeleteClick, onEditClick, limit }: Props) => { const ApiKeyTable = ({ data, isLoading, onDeleteClick, onEditClick, limit }: Props) => {
return ( return (
<Table variant="simple" minWidth="600px"> <Table minWidth="600px">
<Thead> <Thead>
<Tr> <Tr>
<Th>{ `API key token (limit ${ limit } keys)` }</Th> <Th>{ `API key token (limit ${ limit } keys)` }</Th>
......
import React from 'react';
import type { BlockEpoch } from 'types/api/block';
import Tag from 'ui/shared/chakra/Tag';
interface Props {
type: keyof BlockEpoch['aggregated_election_rewards'];
isLoading?: boolean;
}
const BlockEpochElectionRewardType = ({ type, isLoading }: Props) => {
switch (type) {
case 'delegated_payment':
return <Tag colorScheme="blue" isLoading={ isLoading }>Delegated payments</Tag>;
case 'group':
return <Tag colorScheme="teal" isLoading={ isLoading }>Validator group rewards</Tag>;
case 'validator':
return <Tag colorScheme="purple" isLoading={ isLoading }>Validator rewards</Tag>;
case 'voter':
return <Tag colorScheme="yellow" isLoading={ isLoading }>Voting rewards</Tag>;
}
};
export default React.memo(BlockEpochElectionRewardType);
...@@ -16,7 +16,7 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => { ...@@ -16,7 +16,7 @@ const BlockEpochElectionRewards = ({ data, isLoading }: Props) => {
<Box mt={ 8 }> <Box mt={ 8 }>
<Heading as="h4" size="sm" mb={ 3 }>Election rewards</Heading> <Heading as="h4" size="sm" mb={ 3 }>Election rewards</Heading>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }}> <Table style={{ tableLayout: 'auto' }}>
<Thead> <Thead>
<Tr> <Tr>
<Th width="24px"/> <Th width="24px"/>
......
...@@ -5,10 +5,10 @@ import type { BlockEpoch, BlockEpochElectionReward } from 'types/api/block'; ...@@ -5,10 +5,10 @@ import type { BlockEpoch, BlockEpochElectionReward } from 'types/api/block';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import BlockEpochElectionRewardDetailsMobile from './BlockEpochElectionRewardDetailsMobile'; import BlockEpochElectionRewardDetailsMobile from './BlockEpochElectionRewardDetailsMobile';
import BlockEpochElectionRewardType from './BlockEpochElectionRewardType';
interface Props { interface Props {
data: BlockEpochElectionReward; data: BlockEpochElectionReward;
...@@ -53,7 +53,7 @@ const BlockEpochElectionRewardsListItem = ({ data, isLoading, type }: Props) => ...@@ -53,7 +53,7 @@ const BlockEpochElectionRewardsListItem = ({ data, isLoading, type }: Props) =>
/> />
</Skeleton> </Skeleton>
) : <Box boxSize={ 6 }/> } ) : <Box boxSize={ 6 }/> }
<BlockEpochElectionRewardType type={ type } isLoading={ isLoading }/> <EpochRewardTypeTag type={ type } isLoading={ isLoading }/>
<Skeleton isLoaded={ !isLoading }>{ data.count }</Skeleton> <Skeleton isLoaded={ !isLoading }>{ data.count }</Skeleton>
<Flex columnGap={ 2 } alignItems="center" ml="auto" fontWeight={ 500 }> <Flex columnGap={ 2 } alignItems="center" ml="auto" fontWeight={ 500 }>
<Skeleton isLoaded={ !isLoading }>{ valueStr }</Skeleton> <Skeleton isLoaded={ !isLoading }>{ valueStr }</Skeleton>
......
...@@ -5,10 +5,10 @@ import type { BlockEpoch, BlockEpochElectionReward } from 'types/api/block'; ...@@ -5,10 +5,10 @@ import type { BlockEpoch, BlockEpochElectionReward } from 'types/api/block';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import TokenEntity from 'ui/shared/entities/token/TokenEntity'; import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import EpochRewardTypeTag from 'ui/shared/EpochRewardTypeTag';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import BlockEpochElectionRewardDetailsDesktop from './BlockEpochElectionRewardDetailsDesktop'; import BlockEpochElectionRewardDetailsDesktop from './BlockEpochElectionRewardDetailsDesktop';
import BlockEpochElectionRewardType from './BlockEpochElectionRewardType';
import { getRewardNumText } from './utils'; import { getRewardNumText } from './utils';
interface Props { interface Props {
...@@ -54,7 +54,7 @@ const BlockEpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) => ...@@ -54,7 +54,7 @@ const BlockEpochElectionRewardsTableItem = ({ isLoading, data, type }: Props) =>
) } ) }
</Td> </Td>
<Td borderColor={ mainRowBorderColor }> <Td borderColor={ mainRowBorderColor }>
<BlockEpochElectionRewardType type={ type } isLoading={ isLoading }/> <EpochRewardTypeTag type={ type } isLoading={ isLoading }/>
</Td> </Td>
<Td borderColor={ mainRowBorderColor }> <Td borderColor={ mainRowBorderColor }>
<Skeleton isLoaded={ !isLoading } fontWeight={ 400 } my={ 1 }> <Skeleton isLoaded={ !isLoading } fontWeight={ 400 } my={ 1 }>
......
...@@ -40,7 +40,7 @@ const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum ...@@ -40,7 +40,7 @@ const BlocksTable = ({ data, isLoading, top, page, showSocketInfo, socketInfoNum
return ( return (
<AddressHighlightProvider> <AddressHighlightProvider>
<Table variant="simple" minWidth="1040px" size="md" fontWeight={ 500 }> <Table minWidth="1040px" fontWeight={ 500 }>
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th width="150px">Block</Th> <Th width="150px">Block</Th>
......
...@@ -20,7 +20,7 @@ interface Props { ...@@ -20,7 +20,7 @@ interface Props {
const CustomAbiTable = ({ data, isLoading, onDeleteClick, onEditClick }: Props) => { const CustomAbiTable = ({ data, isLoading, onDeleteClick, onEditClick }: Props) => {
return ( return (
<Table variant="simple" minWidth="600px"> <Table minWidth="600px">
<Thead> <Thead>
<Tr> <Tr>
<Th>ABI for Smart contract address (0x...)</Th> <Th>ABI for Smart contract address (0x...)</Th>
......
...@@ -15,7 +15,7 @@ import OptimisticDepositsTableItem from './OptimisticDepositsTableItem'; ...@@ -15,7 +15,7 @@ import OptimisticDepositsTableItem from './OptimisticDepositsTableItem';
const OptimisticDepositsTable = ({ items, top, isLoading }: Props) => { const OptimisticDepositsTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px"> <Table style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th>L1 block No</Th> <Th>L1 block No</Th>
......
...@@ -15,7 +15,7 @@ import DepositsTableItem from './DepositsTableItem'; ...@@ -15,7 +15,7 @@ import DepositsTableItem from './DepositsTableItem';
const DepositsTable = ({ items, top, isLoading }: Props) => { const DepositsTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px"> <Table style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th>L1 block No</Th> <Th>L1 block No</Th>
......
...@@ -15,7 +15,7 @@ import ZkEvmL2DepositsTableItem from './ZkEvmL2DepositsTableItem'; ...@@ -15,7 +15,7 @@ import ZkEvmL2DepositsTableItem from './ZkEvmL2DepositsTableItem';
const ZkEvmL2DepositsTable = ({ items, top, isLoading }: Props) => { const ZkEvmL2DepositsTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px"> <Table style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th>L1 block</Th> <Th>L1 block</Th>
......
...@@ -15,7 +15,7 @@ type Props = { ...@@ -15,7 +15,7 @@ type Props = {
const OptimisticL2DisputeGamesTable = ({ items, top, isLoading }: Props) => { const OptimisticL2DisputeGamesTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px"> <Table style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th>Index</Th> <Th>Index</Th>
......
...@@ -14,18 +14,32 @@ const BORDER_DEFAULT = 'none'; ...@@ -14,18 +14,32 @@ const BORDER_DEFAULT = 'none';
const HeroBanner = () => { const HeroBanner = () => {
const background = useColorModeValue( const background = useColorModeValue(
config.UI.homepage.heroBanner?.background?.[0] || config.UI.homepage.plate.background || BACKGROUND_DEFAULT, // light mode
config.UI.homepage.heroBanner?.background?.[1] || config.UI.homepage.plate.background || BACKGROUND_DEFAULT, config.UI.homepage.heroBanner?.background?.[0] ||
config.UI.homepage.plate.background ||
BACKGROUND_DEFAULT,
// dark mode
config.UI.homepage.heroBanner?.background?.[1] ||
config.UI.homepage.heroBanner?.background?.[0] ||
config.UI.homepage.plate.background ||
BACKGROUND_DEFAULT,
); );
const textColor = useColorModeValue( const textColor = useColorModeValue(
config.UI.homepage.heroBanner?.text_color?.[0] || config.UI.homepage.plate.textColor || TEXT_COLOR_DEFAULT, // light mode
config.UI.homepage.heroBanner?.text_color?.[1] || config.UI.homepage.plate.textColor || TEXT_COLOR_DEFAULT, config.UI.homepage.heroBanner?.text_color?.[0] ||
config.UI.homepage.plate.textColor ||
TEXT_COLOR_DEFAULT,
// dark mode
config.UI.homepage.heroBanner?.text_color?.[1] ||
config.UI.homepage.heroBanner?.text_color?.[0] ||
config.UI.homepage.plate.textColor ||
TEXT_COLOR_DEFAULT,
); );
const border = useColorModeValue( const border = useColorModeValue(
config.UI.homepage.heroBanner?.border?.[0] || BORDER_DEFAULT, config.UI.homepage.heroBanner?.border?.[0] || BORDER_DEFAULT,
config.UI.homepage.heroBanner?.border?.[1] || BORDER_DEFAULT, config.UI.homepage.heroBanner?.border?.[1] || config.UI.homepage.heroBanner?.border?.[0] || BORDER_DEFAULT,
); );
return ( return (
......
...@@ -11,6 +11,7 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard'; ...@@ -11,6 +11,7 @@ import CopyToClipboard from 'ui/shared/CopyToClipboard';
import AppSecurityReport from './AppSecurityReport'; import AppSecurityReport from './AppSecurityReport';
import FavoriteIcon from './FavoriteIcon'; import FavoriteIcon from './FavoriteIcon';
import MarketplaceAppCardLink from './MarketplaceAppCardLink'; import MarketplaceAppCardLink from './MarketplaceAppCardLink';
import MarketplaceAppGraphLinks from './MarketplaceAppGraphLinks';
import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon'; import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon';
import Rating from './Rating/Rating'; import Rating from './Rating/Rating';
import type { RateFunction } from './Rating/useRatings'; import type { RateFunction } from './Rating/useRatings';
...@@ -28,6 +29,7 @@ interface Props extends MarketplaceAppWithSecurityReport { ...@@ -28,6 +29,7 @@ interface Props extends MarketplaceAppWithSecurityReport {
isRatingSending: boolean; isRatingSending: boolean;
isRatingLoading: boolean; isRatingLoading: boolean;
canRate: boolean | undefined; canRate: boolean | undefined;
graphLinks: Array<{text: string; url: string}>;
} }
const MarketplaceAppCard = ({ const MarketplaceAppCard = ({
...@@ -54,6 +56,7 @@ const MarketplaceAppCard = ({ ...@@ -54,6 +56,7 @@ const MarketplaceAppCard = ({
isRatingSending, isRatingSending,
isRatingLoading, isRatingLoading,
canRate, canRate,
graphLinks,
}: Props) => { }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const categoriesLabel = categories.join(', '); const categoriesLabel = categories.join(', ');
...@@ -118,11 +121,7 @@ const MarketplaceAppCard = ({ ...@@ -118,11 +121,7 @@ const MarketplaceAppCard = ({
> >
<Skeleton <Skeleton
isLoaded={ !isLoading } isLoaded={ !isLoading }
fontSize={{ base: 'sm', md: 'lg' }}
lineHeight={{ base: '20px', md: '28px' }}
paddingRight={{ base: '40px', md: 0 }} paddingRight={{ base: '40px', md: 0 }}
fontWeight="semibold"
fontFamily="heading"
display="inline-block" display="inline-block"
> >
<MarketplaceAppCardLink <MarketplaceAppCardLink
...@@ -131,8 +130,18 @@ const MarketplaceAppCard = ({ ...@@ -131,8 +130,18 @@ const MarketplaceAppCard = ({
external={ external } external={ external }
title={ title } title={ title }
onClick={ onAppClick } onClick={ onAppClick }
fontWeight="semibold"
fontFamily="heading"
fontSize={{ base: 'sm', md: 'lg' }}
lineHeight={{ base: '20px', md: '28px' }}
/> />
<MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/> <MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/>
<MarketplaceAppGraphLinks
links={ graphLinks }
ml={ 2 }
verticalAlign="middle"
mb={{ base: 0, md: 1 }}
/>
</Skeleton> </Skeleton>
<Skeleton <Skeleton
......
import { LinkOverlay } from '@chakra-ui/react'; import { LinkOverlay, chakra } from '@chakra-ui/react';
import NextLink from 'next/link'; import NextLink from 'next/link';
import React from 'react'; import React from 'react';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
...@@ -9,24 +9,25 @@ type Props = { ...@@ -9,24 +9,25 @@ type Props = {
external?: boolean; external?: boolean;
title: string; title: string;
onClick?: (event: MouseEvent, id: string) => void; onClick?: (event: MouseEvent, id: string) => void;
className?: string;
} }
const MarketplaceAppCardLink = ({ url, external, id, title, onClick }: Props) => { const MarketplaceAppCardLink = ({ url, external, id, title, onClick, className }: Props) => {
const handleClick = React.useCallback((event: MouseEvent) => { const handleClick = React.useCallback((event: MouseEvent) => {
onClick?.(event, id); onClick?.(event, id);
}, [ onClick, id ]); }, [ onClick, id ]);
return external ? ( return external ? (
<LinkOverlay href={ url } isExternal={ true } marginRight={ 2 }> <LinkOverlay href={ url } isExternal={ true } marginRight={ 2 } className={ className }>
{ title } { title }
</LinkOverlay> </LinkOverlay>
) : ( ) : (
<NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref legacyBehavior> <NextLink href={{ pathname: '/apps/[id]', query: { id } }} passHref legacyBehavior>
<LinkOverlay onClick={ handleClick } marginRight={ 2 }> <LinkOverlay onClick={ handleClick } marginRight={ 2 } className={ className }>
{ title } { title }
</LinkOverlay> </LinkOverlay>
</NextLink> </NextLink>
); );
}; };
export default MarketplaceAppCardLink; export default chakra(MarketplaceAppCardLink);
import {
Text,
PopoverTrigger,
PopoverBody,
PopoverContent,
chakra,
Box,
VStack,
} from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import Popover from 'ui/shared/chakra/Popover';
import IconSvg from 'ui/shared/IconSvg';
import LinkExternal from 'ui/shared/links/LinkExternal';
interface Props {
className?: string;
links?: Array<{ title: string; url: string }>;
}
const MarketplaceAppGraphLinks = ({ className, links }: Props) => {
const isMobile = useIsMobile();
const handleButtonClick = React.useCallback((e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
}, []);
if (!links || links.length === 0) {
return null;
}
return (
<Box position="relative" className={ className } display="inline-flex" alignItems="center" height={ 7 } onClick={ handleButtonClick }>
<Popover
placement={ isMobile ? 'bottom-end' : 'bottom' }
isLazy
trigger="hover"
>
<PopoverTrigger>
<IconSvg name="brands/graph" boxSize={ 5 } onClick={ handleButtonClick }/>
</PopoverTrigger>
<PopoverContent w="260px">
<PopoverBody fontSize="sm">
<VStack gap={ 4 } align="start">
<Text>{ `This dapp uses ${ links.length > 1 ? 'several subgraphs' : 'a subgraph' } powered by The Graph` }</Text>
{ links.map(link => (
<LinkExternal key={ link.url } href={ link.url }>{ link.title }</LinkExternal>
)) }
</VStack>
</PopoverBody>
</PopoverContent>
</Popover>
</Box>
);
};
export default React.memo(chakra(MarketplaceAppGraphLinks));
...@@ -18,6 +18,8 @@ import IconSvg from 'ui/shared/IconSvg'; ...@@ -18,6 +18,8 @@ import IconSvg from 'ui/shared/IconSvg';
import AppSecurityReport from './AppSecurityReport'; import AppSecurityReport from './AppSecurityReport';
import FavoriteIcon from './FavoriteIcon'; import FavoriteIcon from './FavoriteIcon';
import MarketplaceAppGraphLinks from './MarketplaceAppGraphLinks';
import MarketplaceAppIntegrationIcon from './MarketplaceAppIntegrationIcon';
import MarketplaceAppModalLink from './MarketplaceAppModalLink'; import MarketplaceAppModalLink from './MarketplaceAppModalLink';
import Rating from './Rating/Rating'; import Rating from './Rating/Rating';
import type { RateFunction } from './Rating/useRatings'; import type { RateFunction } from './Rating/useRatings';
...@@ -36,6 +38,7 @@ type Props = { ...@@ -36,6 +38,7 @@ type Props = {
isRatingSending: boolean; isRatingSending: boolean;
isRatingLoading: boolean; isRatingLoading: boolean;
canRate: boolean | undefined; canRate: boolean | undefined;
graphLinks?: Array<{text: string; url: string}>;
} }
const MarketplaceAppModal = ({ const MarketplaceAppModal = ({
...@@ -49,6 +52,7 @@ const MarketplaceAppModal = ({ ...@@ -49,6 +52,7 @@ const MarketplaceAppModal = ({
isRatingSending, isRatingSending,
isRatingLoading, isRatingLoading,
canRate, canRate,
graphLinks,
}: Props) => { }: Props) => {
const { const {
id, id,
...@@ -67,6 +71,7 @@ const MarketplaceAppModal = ({ ...@@ -67,6 +71,7 @@ const MarketplaceAppModal = ({
categories, categories,
securityReport, securityReport,
rating, rating,
internalWallet,
} = data; } = data;
const socialLinks = [ const socialLinks = [
...@@ -148,16 +153,19 @@ const MarketplaceAppModal = ({ ...@@ -148,16 +153,19 @@ const MarketplaceAppModal = ({
/> />
</Flex> </Flex>
<Heading <Flex alignItems="center" mb={{ md: 2 }} gridColumn={ 2 }>
as="h2" <Heading
gridColumn={ 2 } as="h2"
fontSize={{ base: '2xl', md: '32px' }} fontSize={{ base: '2xl', md: '32px' }}
fontWeight="medium" fontWeight="medium"
lineHeight={{ md: 10 }} lineHeight={{ md: 10 }}
mb={{ md: 2 }} mr={ 2 }
> >
{ title } { title }
</Heading> </Heading>
<MarketplaceAppIntegrationIcon external={ external } internalWallet={ internalWallet }/>
<MarketplaceAppGraphLinks links={ graphLinks } ml={ 2 }/>
</Flex>
<Text <Text
variant="secondary" variant="secondary"
......
import { Grid, Box } from '@chakra-ui/react'; import { Grid, Box } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
...@@ -25,11 +26,13 @@ type Props = { ...@@ -25,11 +26,13 @@ type Props = {
isRatingSending: boolean; isRatingSending: boolean;
isRatingLoading: boolean; isRatingLoading: boolean;
canRate: boolean | undefined; canRate: boolean | undefined;
graphLinksQuery: UseQueryResult<Record<string, Array<{text: string; url: string}>>, unknown>;
} }
const MarketplaceList = ({ const MarketplaceList = ({
apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId, apps, showAppInfo, favoriteApps, onFavoriteClick, isLoading, selectedCategoryId,
onAppClick, showContractList, userRatings, rateApp, isRatingSending, isRatingLoading, canRate, onAppClick, showContractList, userRatings, rateApp, isRatingSending, isRatingLoading, canRate,
graphLinksQuery,
}: Props) => { }: Props) => {
const { cutRef, renderedItemsNum } = useLazyRenderedList(apps, !isLoading, 16); const { cutRef, renderedItemsNum } = useLazyRenderedList(apps, !isLoading, 16);
...@@ -75,6 +78,7 @@ const MarketplaceList = ({ ...@@ -75,6 +78,7 @@ const MarketplaceList = ({
isRatingSending={ isRatingSending } isRatingSending={ isRatingSending }
isRatingLoading={ isRatingLoading } isRatingLoading={ isRatingLoading }
canRate={ canRate } canRate={ canRate }
graphLinks={ graphLinksQuery.data?.[app.id] }
/> />
)) } )) }
</Grid> </Grid>
......
...@@ -17,7 +17,7 @@ import ArbitrumL2MessagesTableItem from './ArbitrumL2MessagesTableItem'; ...@@ -17,7 +17,7 @@ import ArbitrumL2MessagesTableItem from './ArbitrumL2MessagesTableItem';
const ArbitrumL2MessagesTable = ({ items, direction, top, isLoading }: Props) => { const ArbitrumL2MessagesTable = ({ items, direction, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px"> <Table style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
{ direction === 'to-rollup' && <Th>L1 block</Th> } { direction === 'to-rollup' && <Th>L1 block</Th> }
......
...@@ -16,7 +16,7 @@ type Props = { ...@@ -16,7 +16,7 @@ type Props = {
const MudWorldsTable = ({ items, top, isLoading }: Props) => { const MudWorldsTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }}> <Table style={{ tableLayout: 'auto' }}>
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th>Address</Th> <Th>Address</Th>
......
...@@ -81,13 +81,16 @@ const MyProfileEmail = ({ profileQuery }: Props) => { ...@@ -81,13 +81,16 @@ const MyProfileEmail = ({ profileQuery }: Props) => {
onSubmit={ formApi.handleSubmit(onFormSubmit) } onSubmit={ formApi.handleSubmit(onFormSubmit) }
> >
<MyProfileFieldsName/> <MyProfileFieldsName/>
<MyProfileFieldsEmail isReadOnly={ !config.services.reCaptchaV3.siteKey }/> <MyProfileFieldsEmail
{ config.services.reCaptchaV3.siteKey && ( isReadOnly={ !config.services.reCaptchaV3.siteKey || Boolean(profileQuery.data?.email) }
defaultValue={ profileQuery.data?.email || undefined }
/>
{ config.services.reCaptchaV3.siteKey && !profileQuery.data?.email && (
<GoogleReCaptchaProvider reCaptchaKey={ config.services.reCaptchaV3.siteKey }> <GoogleReCaptchaProvider reCaptchaKey={ config.services.reCaptchaV3.siteKey }>
<FormFieldReCaptcha/> <FormFieldReCaptcha/>
</GoogleReCaptchaProvider> </GoogleReCaptchaProvider>
) } ) }
{ config.services.reCaptchaV3.siteKey && ( { config.services.reCaptchaV3.siteKey && !profileQuery.data?.email && (
<Button <Button
mt={ 6 } mt={ 6 }
size="sm" size="sm"
......
...@@ -19,7 +19,7 @@ const MyProfileWallet = ({ profileQuery, onAddWallet }: Props) => { ...@@ -19,7 +19,7 @@ const MyProfileWallet = ({ profileQuery, onAddWallet }: Props) => {
<section> <section>
<Heading as="h2" size="sm" mb={ 3 }>My linked wallet</Heading> <Heading as="h2" size="sm" mb={ 3 }>My linked wallet</Heading>
<Text mb={ 3 } > <Text mb={ 3 } >
This wallet address can be used to login { config.features.rewards.isEnabled ? 'and is used for merit program' : '' } This wallet address is used for login { config.features.rewards.isEnabled ? 'and participation in the merit program' : '' }
</Text> </Text>
{ profileQuery.data?.address_hash ? ( { profileQuery.data?.address_hash ? (
<Box px={ 3 } py="18px" bgColor={ bgColor } borderRadius="base"> <Box px={ 3 } py="18px" bgColor={ bgColor } borderRadius="base">
......
...@@ -10,9 +10,10 @@ import InputPlaceholder from 'ui/shared/InputPlaceholder'; ...@@ -10,9 +10,10 @@ import InputPlaceholder from 'ui/shared/InputPlaceholder';
interface Props { interface Props {
isReadOnly?: boolean; isReadOnly?: boolean;
defaultValue: string | undefined;
} }
const MyProfileFieldsEmail = ({ isReadOnly }: Props) => { const MyProfileFieldsEmail = ({ isReadOnly, defaultValue }: Props) => {
const { control } = useFormContext<FormFields>(); const { control } = useFormContext<FormFields>();
const { field, fieldState, formState } = useController<FormFields, 'email'>({ const { field, fieldState, formState } = useController<FormFields, 'email'>({
control, control,
...@@ -21,6 +22,7 @@ const MyProfileFieldsEmail = ({ isReadOnly }: Props) => { ...@@ -21,6 +22,7 @@ const MyProfileFieldsEmail = ({ isReadOnly }: Props) => {
}); });
const isDisabled = formState.isSubmitting; const isDisabled = formState.isSubmitting;
const isVerified = defaultValue && field.value === defaultValue;
return ( return (
<FormControl variant="floating" isDisabled={ isDisabled } isRequired size="md"> <FormControl variant="floating" isDisabled={ isDisabled } isRequired size="md">
...@@ -34,7 +36,7 @@ const MyProfileFieldsEmail = ({ isReadOnly }: Props) => { ...@@ -34,7 +36,7 @@ const MyProfileFieldsEmail = ({ isReadOnly }: Props) => {
autoComplete="off" autoComplete="off"
/> />
<InputPlaceholder text="Email" error={ fieldState.error }/> <InputPlaceholder text="Email" error={ fieldState.error }/>
{ !formState.dirtyFields.email && ( { isVerified && (
<InputRightElement h="100%"> <InputRightElement h="100%">
<IconSvg name="certified" boxSize={ 5 } color="green.500"/> <IconSvg name="certified" boxSize={ 5 } color="green.500"/>
</InputRightElement> </InputRightElement>
......
...@@ -16,7 +16,7 @@ const MyProfileFieldsName = () => { ...@@ -16,7 +16,7 @@ const MyProfileFieldsName = () => {
const isDisabled = formState.isSubmitting; const isDisabled = formState.isSubmitting;
return ( return (
<FormControl variant="floating" isDisabled={ isDisabled } size="md" cursor="not-allowed" mb={ 3 }> <FormControl variant="floating" isDisabled={ isDisabled } size="md" mb={ 3 }>
<Input <Input
{ ...field } { ...field }
isInvalid={ Boolean(fieldState.error) } isInvalid={ Boolean(fieldState.error) }
......
...@@ -22,7 +22,7 @@ const NameDomainHistoryTable = ({ history, domain, isLoading, sort, onSortToggle ...@@ -22,7 +22,7 @@ const NameDomainHistoryTable = ({ history, domain, isLoading, sort, onSortToggle
const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)'; const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)';
return ( return (
<Table variant="simple" size="sm"> <Table>
<Thead top={ 0 }> <Thead top={ 0 }>
<Tr> <Tr>
<Th width="25%">Txn hash</Th> <Th width="25%">Txn hash</Th>
......
...@@ -21,7 +21,7 @@ const NameDomainsTable = ({ data, isLoading, sort, onSortToggle }: Props) => { ...@@ -21,7 +21,7 @@ const NameDomainsTable = ({ data, isLoading, sort, onSortToggle }: Props) => {
const sortIconTransform = sort?.toLowerCase().includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)'; const sortIconTransform = sort?.toLowerCase().includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)';
return ( return (
<Table variant="simple" size="sm"> <Table>
<Thead top={ ACTION_BAR_HEIGHT_DESKTOP }> <Thead top={ ACTION_BAR_HEIGHT_DESKTOP }>
<Tr> <Tr>
<Th width="25%">Domain</Th> <Th width="25%">Domain</Th>
......
...@@ -15,7 +15,7 @@ type Props = { ...@@ -15,7 +15,7 @@ type Props = {
const OptimisticL2OutputRootsTable = ({ items, top, isLoading }: Props) => { const OptimisticL2OutputRootsTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" minW="900px"> <Table minW="900px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th width="160px">L2 output index</Th> <Th width="160px">L2 output index</Th>
......
...@@ -24,6 +24,7 @@ import AddressBlocksValidated from 'ui/address/AddressBlocksValidated'; ...@@ -24,6 +24,7 @@ import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance'; import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressContract from 'ui/address/AddressContract'; import AddressContract from 'ui/address/AddressContract';
import AddressDetails from 'ui/address/AddressDetails'; import AddressDetails from 'ui/address/AddressDetails';
import AddressEpochRewards from 'ui/address/AddressEpochRewards';
import AddressInternalTxs from 'ui/address/AddressInternalTxs'; import AddressInternalTxs from 'ui/address/AddressInternalTxs';
import AddressLogs from 'ui/address/AddressLogs'; import AddressLogs from 'ui/address/AddressLogs';
import AddressMud from 'ui/address/AddressMud'; import AddressMud from 'ui/address/AddressMud';
...@@ -195,6 +196,12 @@ const AddressPageContent = () => { ...@@ -195,6 +196,12 @@ const AddressPageContent = () => {
count: addressTabsCountersQuery.data?.internal_txs_count, count: addressTabsCountersQuery.data?.internal_txs_count,
component: <AddressInternalTxs scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>, component: <AddressInternalTxs scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
}, },
addressTabsCountersQuery.data?.celo_election_rewards_count ? {
id: 'epoch_rewards',
title: 'Epoch rewards',
count: addressTabsCountersQuery.data?.celo_election_rewards_count,
component: <AddressEpochRewards scrollRef={ tabsScrollRef } shouldRender={ !isTabsLoading } isQueryEnabled={ areQueriesEnabled }/>,
} : undefined,
{ {
id: 'coin_balance_history', id: 'coin_balance_history',
title: 'Coin balance history', title: 'Coin balance history',
...@@ -282,7 +289,7 @@ const AddressPageContent = () => { ...@@ -282,7 +289,7 @@ const AddressPageContent = () => {
{ slug: 'mud', name: 'MUD World', tagType: 'custom' as const, ordinal: -10 } : { slug: 'mud', name: 'MUD World', tagType: 'custom' as const, ordinal: -10 } :
undefined, undefined,
...formatUserTags(addressQuery.data), ...formatUserTags(addressQuery.data),
...(addressMetadataQuery.data?.addresses?.[hash.toLowerCase()]?.tags || []), ...(addressMetadataQuery.data?.addresses?.[hash.toLowerCase()]?.tags.filter(tag => tag.tagType !== 'note') || []),
].filter(Boolean).sort(sortEntityTags); ].filter(Boolean).sort(sortEntityTags);
}, [ addressMetadataQuery.data, addressQuery.data, hash, isSafeAddress, userOpsAccountQuery.data, mudTablesCountQuery.data, usernameApiTag ]); }, [ addressMetadataQuery.data, addressQuery.data, hash, isSafeAddress, userOpsAccountQuery.data, mudTablesCountQuery.data, usernameApiTag ]);
......
...@@ -108,7 +108,7 @@ const ArbitrumL2TxnBatch = () => { ...@@ -108,7 +108,7 @@ const ArbitrumL2TxnBatch = () => {
} }
return { return {
label: 'Back to tx batches list', label: 'Back to txn batches list',
url: appProps.referrer, url: appProps.referrer,
}; };
}, [ appProps.referrer ]); }, [ appProps.referrer ]);
...@@ -117,7 +117,7 @@ const ArbitrumL2TxnBatch = () => { ...@@ -117,7 +117,7 @@ const ArbitrumL2TxnBatch = () => {
<> <>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
<PageTitle <PageTitle
title={ `Tx batch #${ number }` } title={ `Txn batch #${ number }` }
backLink={ backLink } backLink={ backLink }
/> />
{ batchQuery.isPlaceholderData ? { batchQuery.isPlaceholderData ?
......
...@@ -2,7 +2,6 @@ import { Hide, Show, Skeleton, Text } from '@chakra-ui/react'; ...@@ -2,7 +2,6 @@ import { Hide, Show, Skeleton, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { nbsp } from 'lib/html-entities';
import { ARBITRUM_L2_TXN_BATCHES_ITEM } from 'stubs/arbitrumL2'; import { ARBITRUM_L2_TXN_BATCHES_ITEM } from 'stubs/arbitrumL2';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
...@@ -60,7 +59,7 @@ const ArbitrumL2TxnBatches = () => { ...@@ -60,7 +59,7 @@ const ArbitrumL2TxnBatches = () => {
return ( return (
<Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap"> <Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap">
Tx batch (L2 block) Txn batch
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].number } </Text>to <Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].number } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].number } </Text> <Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].number } </Text>
(total of { countersQuery.data?.toLocaleString() } batches) (total of { countersQuery.data?.toLocaleString() } batches)
...@@ -72,11 +71,11 @@ const ArbitrumL2TxnBatches = () => { ...@@ -72,11 +71,11 @@ const ArbitrumL2TxnBatches = () => {
return ( return (
<> <>
<PageTitle title={ `Tx batches (L2${ nbsp }blocks)` } withTextAd/> <PageTitle title="Txn batches" withTextAd/>
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
items={ data?.items } items={ data?.items }
emptyText="There are no tx batches." emptyText="There are no txn batches."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
/> />
......
...@@ -110,7 +110,10 @@ const Chart = () => { ...@@ -110,7 +110,10 @@ const Chart = () => {
router.push({ router.push({
pathname: router.pathname, pathname: router.pathname,
query: { ...router.query, resolution }, query: { ...router.query, resolution },
}); },
undefined,
{ shallow: true },
);
}, [ setResolution, router ]); }, [ setResolution, router ]);
const handleReset = React.useCallback(() => { const handleReset = React.useCallback(() => {
......
...@@ -14,6 +14,20 @@ import AdBanner from 'ui/shared/ad/AdBanner'; ...@@ -14,6 +14,20 @@ import AdBanner from 'ui/shared/ad/AdBanner';
const rollupFeature = config.features.rollup; const rollupFeature = config.features.rollup;
const Home = () => { const Home = () => {
const leftWidget = (() => {
if (rollupFeature.isEnabled && !rollupFeature.homepage.showLatestBlocks) {
switch (rollupFeature.type) {
case 'zkEvm':
return <LatestZkEvmL2Batches/>;
case 'arbitrum':
return <LatestArbitrumL2Batches/>;
}
}
return <LatestBlocks/>;
})();
return ( return (
<Box as="main"> <Box as="main">
<HeroBanner/> <HeroBanner/>
...@@ -23,9 +37,7 @@ const Home = () => { ...@@ -23,9 +37,7 @@ const Home = () => {
</Flex> </Flex>
<AdBanner mt={ 6 } mx="auto" display={{ base: 'flex', lg: 'none' }} justifyContent="center"/> <AdBanner mt={ 6 } mx="auto" display={{ base: 'flex', lg: 'none' }} justifyContent="center"/>
<Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 6 }> <Flex mt={ 8 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 6 }>
{ rollupFeature.isEnabled && rollupFeature.type === 'zkEvm' && <LatestZkEvmL2Batches/> } { leftWidget }
{ rollupFeature.isEnabled && rollupFeature.type === 'arbitrum' && <LatestArbitrumL2Batches/> }
{ !(rollupFeature.isEnabled && (rollupFeature.type === 'arbitrum' || rollupFeature.type === 'zkEvm')) && <LatestBlocks/> }
<Box flexGrow={ 1 }> <Box flexGrow={ 1 }>
<Transactions/> <Transactions/>
</Box> </Box>
......
...@@ -7,6 +7,7 @@ import type { TabItem } from 'ui/shared/Tabs/types'; ...@@ -7,6 +7,7 @@ import type { TabItem } from 'ui/shared/Tabs/types';
import config from 'configs/app'; import config from 'configs/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError'; import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useGraphLinks from 'lib/hooks/useGraphLinks';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import Banner from 'ui/marketplace/Banner'; import Banner from 'ui/marketplace/Banner';
import ContractListModal from 'ui/marketplace/ContractListModal'; import ContractListModal from 'ui/marketplace/ContractListModal';
...@@ -80,6 +81,8 @@ const Marketplace = () => { ...@@ -80,6 +81,8 @@ const Marketplace = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const graphLinksQuery = useGraphLinks();
const categoryTabs = React.useMemo(() => { const categoryTabs = React.useMemo(() => {
const tabs: Array<TabItem> = categories.map(category => ({ const tabs: Array<TabItem> = categories.map(category => ({
id: category.name, id: category.name,
...@@ -236,6 +239,7 @@ const Marketplace = () => { ...@@ -236,6 +239,7 @@ const Marketplace = () => {
isRatingSending={ isRatingSending } isRatingSending={ isRatingSending }
isRatingLoading={ isRatingLoading } isRatingLoading={ isRatingLoading }
canRate={ canRate } canRate={ canRate }
graphLinksQuery={ graphLinksQuery }
/> />
{ (selectedApp && isAppInfoModalOpen) && ( { (selectedApp && isAppInfoModalOpen) && (
...@@ -250,6 +254,7 @@ const Marketplace = () => { ...@@ -250,6 +254,7 @@ const Marketplace = () => {
isRatingSending={ isRatingSending } isRatingSending={ isRatingSending }
isRatingLoading={ isRatingLoading } isRatingLoading={ isRatingLoading }
canRate={ canRate } canRate={ canRate }
graphLinks={ graphLinksQuery.data?.[selectedApp.id] }
/> />
) } ) }
......
...@@ -106,7 +106,7 @@ const OptimisticL2TxnBatch = () => { ...@@ -106,7 +106,7 @@ const OptimisticL2TxnBatch = () => {
} }
return { return {
label: 'Back to tx batches list', label: 'Back to txn batches list',
url: appProps.referrer, url: appProps.referrer,
}; };
}, [ appProps.referrer ]); }, [ appProps.referrer ]);
......
...@@ -2,7 +2,6 @@ import { Hide, Show, Skeleton, Text } from '@chakra-ui/react'; ...@@ -2,7 +2,6 @@ import { Hide, Show, Skeleton, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { nbsp } from 'lib/html-entities';
import { L2_TXN_BATCHES_ITEM } from 'stubs/L2'; import { L2_TXN_BATCHES_ITEM } from 'stubs/L2';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; import { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
...@@ -60,7 +59,7 @@ const OptimisticL2TxnBatches = () => { ...@@ -60,7 +59,7 @@ const OptimisticL2TxnBatches = () => {
return ( return (
<Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap"> <Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap">
Tx batch (L2 block) Txn batch
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].internal_id } </Text>to <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> <Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].internal_id } </Text>
(total of { countersQuery.data?.toLocaleString() } batches) (total of { countersQuery.data?.toLocaleString() } batches)
...@@ -72,11 +71,11 @@ const OptimisticL2TxnBatches = () => { ...@@ -72,11 +71,11 @@ const OptimisticL2TxnBatches = () => {
return ( return (
<> <>
<PageTitle title={ `Tx batches (L2${ nbsp }blocks)` } withTextAd/> <PageTitle title="Txn batches" withTextAd/>
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
items={ data?.items } items={ data?.items }
emptyText="There are no tx batches." emptyText="There are no txn batches."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
/> />
......
...@@ -148,7 +148,7 @@ const SearchResultsPageContent = () => { ...@@ -148,7 +148,7 @@ const SearchResultsPageContent = () => {
)) } )) }
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<Table variant="simple" size="md" fontWeight={ 500 }> <Table fontWeight={ 500 }>
<Thead top={ pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }> <Thead top={ pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }>
<Tr> <Tr>
<Th width="30%">Search result</Th> <Th width="30%">Search result</Th>
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import { mixTokens } from 'mocks/tokens/tokenTransfer';
import { test, expect } from 'playwright/lib';
import TokenTransfers from './TokenTransfers';
test('base view +@mobile', async({ render, mockTextAd, mockApiResponse }) => {
await mockTextAd();
await mockApiResponse('token_transfers_all', mixTokens, { queryParams: { type: [] } });
const component = await render(<Box pt={{ base: '106px', lg: 0 }}> <TokenTransfers/> </Box>);
await expect(component).toHaveScreenshot();
});
import { Show, Hide } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { TokenType } from 'types/api/token';
import { getTokenTransfersStub } from 'stubs/token';
import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import PopoverFilter from 'ui/shared/filters/PopoverFilter';
import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import { getTokenFilterValue } from 'ui/tokens/utils';
import TokenTransfersListItem from 'ui/tokenTransfers/TokenTransfersListItem';
import TokenTransfersTable from 'ui/tokenTransfers/TokenTransfersTable';
const TokenTransfers = () => {
const router = useRouter();
const [ typeFilter, setTypeFilter ] = React.useState<Array<TokenType>>(getTokenFilterValue(router.query.type) || []);
const tokenTransfersQuery = useQueryWithPages({
resourceName: 'token_transfers_all',
filters: { type: typeFilter },
options: {
placeholderData: getTokenTransfersStub(),
},
});
const handleTokenTypesChange = React.useCallback((value: Array<TokenType>) => {
tokenTransfersQuery.onFilterChange({ type: value });
setTypeFilter(value);
}, [ tokenTransfersQuery ]);
const content = (
<>
<Show below="lg" ssr={ false }>
{ tokenTransfersQuery.data?.items.map((item, index) => (
<TokenTransfersListItem
key={ item.block_number + item.log_index + (tokenTransfersQuery.isPlaceholderData ? index : '') }
isLoading={ tokenTransfersQuery.isPlaceholderData }
item={ item }
/>
)) }
</Show>
<Hide below="lg" ssr={ false }>
<TokenTransfersTable
items={ tokenTransfersQuery.data?.items }
top={ tokenTransfersQuery.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }
isLoading={ tokenTransfersQuery.isPlaceholderData }
/>
</Hide>
</>
);
const filter = (
<PopoverFilter contentProps={{ w: '200px' }} appliedFiltersNum={ typeFilter.length }>
<TokenTypeFilter<TokenType> onChange={ handleTokenTypesChange } defaultValue={ typeFilter } nftOnly={ false }/>
</PopoverFilter>
);
const actionBar = (
<ActionBar mt={ -6 }>
{ filter }
<Pagination { ...tokenTransfersQuery.pagination }/>
</ActionBar>
);
return (
<>
<PageTitle
title="Token Transfers"
withTextAd
/>
<DataListDisplay
isError={ tokenTransfersQuery.isError }
items={ tokenTransfersQuery.data?.items }
emptyText="There are no token transfers."
content={ content }
actionBar={ actionBar }
/>
</>
);
};
export default TokenTransfers;
...@@ -13,6 +13,7 @@ import DataListDisplay from 'ui/shared/DataListDisplay'; ...@@ -13,6 +13,7 @@ import DataListDisplay from 'ui/shared/DataListDisplay';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages'; import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import useProfileQuery from 'ui/snippets/auth/useProfileQuery';
import useRedirectForInvalidAuthToken from 'ui/snippets/auth/useRedirectForInvalidAuthToken'; import useRedirectForInvalidAuthToken from 'ui/snippets/auth/useRedirectForInvalidAuthToken';
import AddressModal from 'ui/watchlist/AddressModal/AddressModal'; import AddressModal from 'ui/watchlist/AddressModal/AddressModal';
import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal'; import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
...@@ -28,6 +29,7 @@ const WatchList: React.FC = () => { ...@@ -28,6 +29,7 @@ const WatchList: React.FC = () => {
}, },
}); });
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const profileQuery = useProfileQuery();
const addressModalProps = useDisclosure(); const addressModalProps = useDisclosure();
const deleteModalProps = useDisclosure(); const deleteModalProps = useDisclosure();
...@@ -93,6 +95,7 @@ const WatchList: React.FC = () => { ...@@ -93,6 +95,7 @@ const WatchList: React.FC = () => {
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
hasEmail={ Boolean(profileQuery.data?.email) }
/> />
)) } )) }
</Box> </Box>
...@@ -103,6 +106,7 @@ const WatchList: React.FC = () => { ...@@ -103,6 +106,7 @@ const WatchList: React.FC = () => {
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
top={ pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 } top={ pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0 }
hasEmail={ Boolean(profileQuery.data?.email) }
/> />
</Box> </Box>
</> </>
......
...@@ -59,7 +59,7 @@ const ZkEvmL2TxnBatch = () => { ...@@ -59,7 +59,7 @@ const ZkEvmL2TxnBatch = () => {
} }
return { return {
label: 'Back to tx batches list', label: 'Back to txn batches list',
url: appProps.referrer, url: appProps.referrer,
}; };
}, [ appProps.referrer ]); }, [ appProps.referrer ]);
...@@ -68,7 +68,7 @@ const ZkEvmL2TxnBatch = () => { ...@@ -68,7 +68,7 @@ const ZkEvmL2TxnBatch = () => {
<> <>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
<PageTitle <PageTitle
title={ `Tx batch #${ number }` } title={ `Txn batch #${ number }` }
backLink={ backLink } backLink={ backLink }
/> />
{ batchQuery.isPlaceholderData ? <TabsSkeleton tabs={ tabs }/> : ( { batchQuery.isPlaceholderData ? <TabsSkeleton tabs={ tabs }/> : (
......
...@@ -59,7 +59,7 @@ const ZkEvmL2TxnBatches = () => { ...@@ -59,7 +59,7 @@ const ZkEvmL2TxnBatches = () => {
return ( return (
<Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap"> <Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap">
Tx batch Txn batch
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].number } </Text>to <Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].number } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].number } </Text> <Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].number } </Text>
(total of { countersQuery.data?.toLocaleString() } batches) (total of { countersQuery.data?.toLocaleString() } batches)
...@@ -71,11 +71,11 @@ const ZkEvmL2TxnBatches = () => { ...@@ -71,11 +71,11 @@ const ZkEvmL2TxnBatches = () => {
return ( return (
<> <>
<PageTitle title="Tx batches" withTextAd/> <PageTitle title="Txn batches" withTextAd/>
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
items={ data?.items } items={ data?.items }
emptyText="There are no tx batches." emptyText="There are no txn batches."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
/> />
......
...@@ -80,7 +80,7 @@ const ZkSyncL2TxnBatch = () => { ...@@ -80,7 +80,7 @@ const ZkSyncL2TxnBatch = () => {
} }
return { return {
label: 'Back to tx batches list', label: 'Back to txn batches list',
url: appProps.referrer, url: appProps.referrer,
}; };
}, [ appProps.referrer ]); }, [ appProps.referrer ]);
...@@ -89,7 +89,7 @@ const ZkSyncL2TxnBatch = () => { ...@@ -89,7 +89,7 @@ const ZkSyncL2TxnBatch = () => {
<> <>
<TextAd mb={ 6 }/> <TextAd mb={ 6 }/>
<PageTitle <PageTitle
title={ `Tx batch #${ number }` } title={ `Txn batch #${ number }` }
backLink={ backLink } backLink={ backLink }
/> />
{ batchQuery.isPlaceholderData ? { batchQuery.isPlaceholderData ?
......
...@@ -59,7 +59,7 @@ const ZkSyncL2TxnBatches = () => { ...@@ -59,7 +59,7 @@ const ZkSyncL2TxnBatches = () => {
return ( return (
<Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap"> <Skeleton isLoaded={ !countersQuery.isPlaceholderData && !isPlaceholderData } display="flex" flexWrap="wrap">
Tx batch Txn batch
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].number } </Text>to <Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[0].number } </Text>to
<Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].number } </Text> <Text fontWeight={ 600 } whiteSpace="pre"> #{ data.items[data.items.length - 1].number } </Text>
(total of { countersQuery.data?.toLocaleString() } batches) (total of { countersQuery.data?.toLocaleString() } batches)
...@@ -71,11 +71,11 @@ const ZkSyncL2TxnBatches = () => { ...@@ -71,11 +71,11 @@ const ZkSyncL2TxnBatches = () => {
return ( return (
<> <>
<PageTitle title="Tx batches" withTextAd/> <PageTitle title="Txn batches" withTextAd/>
<DataListDisplay <DataListDisplay
isError={ isError } isError={ isError }
items={ data?.items } items={ data?.items }
emptyText="There are no tx batches." emptyText="There are no txn batches."
content={ content } content={ content }
actionBar={ actionBar } actionBar={ actionBar }
/> />
......
...@@ -22,7 +22,7 @@ interface Props { ...@@ -22,7 +22,7 @@ interface Props {
const AddressTagTable = ({ data, onDeleteClick, onEditClick, isLoading, top }: Props) => { const AddressTagTable = ({ data, onDeleteClick, onEditClick, isLoading, top }: Props) => {
return ( return (
<Table variant="simple" minWidth="600px"> <Table minWidth="600px">
<TheadSticky top={ top }> <TheadSticky top={ top }>
<Tr> <Tr>
<Th width="60%">Address</Th> <Th width="60%">Address</Th>
......
...@@ -22,7 +22,7 @@ interface Props { ...@@ -22,7 +22,7 @@ interface Props {
const AddressTagTable = ({ data, isLoading, onDeleteClick, onEditClick, top }: Props) => { const AddressTagTable = ({ data, isLoading, onDeleteClick, onEditClick, top }: Props) => {
return ( return (
<Table variant="simple" minWidth="600px"> <Table minWidth="600px">
<TheadSticky top={ top }> <TheadSticky top={ top }>
<Tr> <Tr>
<Th width="75%">Transaction</Th> <Th width="75%">Transaction</Th>
......
...@@ -45,7 +45,7 @@ const EntityTags = ({ tags, className, isLoading }: Props) => { ...@@ -45,7 +45,7 @@ const EntityTags = ({ tags, className, isLoading }: Props) => {
+{ tags.length - visibleNum } +{ tags.length - visibleNum }
</Tag> </Tag>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent w="300px"> <PopoverContent maxW="300px" w="auto">
<PopoverBody > <PopoverBody >
<Flex columnGap={ 2 } rowGap={ 2 } flexWrap="wrap"> <Flex columnGap={ 2 } rowGap={ 2 } flexWrap="wrap">
{ tags.slice(visibleNum).map((tag) => <EntityTag key={ tag.slug } data={ tag }/>) } { tags.slice(visibleNum).map((tag) => <EntityTag key={ tag.slug } data={ tag }/>) }
......
import type { EntityTag } from './types'; import type { EntityTag } from './types';
import { route } from 'nextjs-routes'; // import { route } from 'nextjs-routes';
export function getTagLinkParams(data: EntityTag): { type: 'external' | 'internal'; href: string } | undefined { export function getTagLinkParams(data: EntityTag): { type: 'external' | 'internal'; href: string } | undefined {
if (data.meta?.warpcastHandle) { if (data.meta?.warpcastHandle) {
...@@ -17,10 +17,10 @@ export function getTagLinkParams(data: EntityTag): { type: 'external' | 'interna ...@@ -17,10 +17,10 @@ export function getTagLinkParams(data: EntityTag): { type: 'external' | 'interna
}; };
} }
if (data.tagType === 'generic' || data.tagType === 'protocol') { // if (data.tagType === 'generic' || data.tagType === 'protocol') {
return { // return {
type: 'internal', // type: 'internal',
href: route({ pathname: '/accounts/label/[slug]', query: { slug: data.slug, tagType: data.tagType, tagName: data.name } }), // href: route({ pathname: '/accounts/label/[slug]', query: { slug: data.slug, tagType: data.tagType, tagName: data.name } }),
}; // };
} // }
} }
import { Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { EpochRewardsType } from 'types/api/block';
import Tag from 'ui/shared/chakra/Tag';
type Props = {
type: EpochRewardsType;
isLoading?: boolean;
};
const TYPE_TAGS: Record<EpochRewardsType, { text: string; label: string; color: string }> = {
group: {
text: 'Validator group rewards',
// eslint-disable-next-line max-len
label: 'Reward given to a validator group. The address being viewed is the group\'s address; the associated address is the validator\'s address on whose behalf the reward was paid.',
color: 'teal',
},
validator: {
text: 'Validator rewards',
label: 'Reward given to a validator. The address being viewed is the validator\'s address; the associated address is the validator group\'s address.',
color: 'purple',
},
delegated_payment: {
text: 'Delegated payments',
// eslint-disable-next-line max-len
label: 'Reward portion delegated by a validator to another address. The address being viewed is the beneficiary receiving the reward; the associated address is the validator who set the delegation.',
color: 'blue',
},
voter: {
text: 'Voting rewards',
label: 'Reward given to a voter. The address being viewed is the voter\'s address; the associated address is the group address.',
color: 'yellow',
},
};
const EpochRewardTypeTag = ({ type, isLoading }: Props) => {
const { text, label, color } = TYPE_TAGS[type];
return (
<Tooltip label={ label } maxW="322px" textAlign="center">
<Tag colorScheme={ color } isLoading={ isLoading }>
{ text }
</Tag>
</Tooltip>
);
};
export default React.memo(EpochRewardTypeTag);
...@@ -88,7 +88,7 @@ export const Desktop = ({ ...props }: Props) => { ...@@ -88,7 +88,7 @@ export const Desktop = ({ ...props }: Props) => {
my={ props.isLoading ? '6px' : 0 } my={ props.isLoading ? '6px' : 0 }
{ ...props } { ...props }
> >
{ ({ content }) => <Tr><Td colSpan={ 100 } p={ 0 }>{ content }</Td></Tr> } { ({ content }) => <Tr><Td colSpan={ 100 } p={ 0 } _first={{ p: 0 }} _last={{ p: 0 }}>{ content }</Td></Tr> }
</SocketNewItemsNotice> </SocketNewItemsNotice>
); );
}; };
......
...@@ -34,7 +34,7 @@ const TokenTransferTable = ({ ...@@ -34,7 +34,7 @@ const TokenTransferTable = ({
return ( return (
<AddressHighlightProvider> <AddressHighlightProvider>
<Table variant="simple" size="sm" minW="950px"> <Table minW="950px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
{ showTxInfo && <Th width="44px"></Th> } { showTxInfo && <Th width="44px"></Th> }
......
import { useWeb3Modal } from '@web3modal/wagmi/react';
import React from 'react'; import React from 'react';
import { useSignMessage } from 'wagmi'; import { useSignMessage } from 'wagmi';
...@@ -8,9 +7,10 @@ import config from 'configs/app'; ...@@ -8,9 +7,10 @@ import config from 'configs/app';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import getErrorMessage from 'lib/errors/getErrorMessage'; import getErrorMessage from 'lib/errors/getErrorMessage';
import getErrorObj from 'lib/errors/getErrorObj'; import getErrorObj from 'lib/errors/getErrorObj';
import getErrorObjPayload from 'lib/errors/getErrorObjPayload';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import * as mixpanel from 'lib/mixpanel'; import type * as mixpanel from 'lib/mixpanel';
import useAccount from 'lib/web3/useAccount'; import useWeb3Wallet from 'lib/web3/useWallet';
interface Props { interface Props {
onSuccess?: ({ address, profile }: { address: string; profile: UserInfo }) => void; onSuccess?: ({ address, profile }: { address: string; profile: UserInfo }) => void;
...@@ -25,8 +25,7 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth }: P ...@@ -25,8 +25,7 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth }: P
const apiFetch = useApiFetch(); const apiFetch = useApiFetch();
const toast = useToast(); const toast = useToast();
const web3Modal = useWeb3Modal(); const web3Wallet = useWeb3Wallet({ source });
const { isConnected, address } = useAccount();
const { signMessageAsync } = useSignMessage(); const { signMessageAsync } = useSignMessage();
const proceedToAuth = React.useCallback(async(address: string) => { const proceedToAuth = React.useCallback(async(address: string) => {
...@@ -46,12 +45,13 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth }: P ...@@ -46,12 +45,13 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth }: P
onSuccess?.({ address, profile: response }); onSuccess?.({ address, profile: response });
} catch (error) { } catch (error) {
const errorObj = getErrorObj(error); const errorObj = getErrorObj(error);
const apiErrorMessage = getErrorObjPayload<{ message: string }>(error)?.message;
const shortMessage = errorObj && 'shortMessage' in errorObj && typeof errorObj.shortMessage === 'string' ? errorObj.shortMessage : undefined; const shortMessage = errorObj && 'shortMessage' in errorObj && typeof errorObj.shortMessage === 'string' ? errorObj.shortMessage : undefined;
onError?.(); onError?.();
toast({ toast({
status: 'error', status: 'error',
title: 'Error', title: 'Error',
description: shortMessage || getErrorMessage(error) || 'Something went wrong', description: apiErrorMessage || shortMessage || getErrorMessage(error) || 'Something went wrong',
}); });
} finally { } finally {
setIsPending(false); setIsPending(false);
...@@ -60,22 +60,20 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth }: P ...@@ -60,22 +60,20 @@ function useSignInWithWallet({ onSuccess, onError, source = 'Login', isAuth }: P
const start = React.useCallback(() => { const start = React.useCallback(() => {
setIsPending(true); setIsPending(true);
if (address) { if (web3Wallet.address) {
proceedToAuth(address); proceedToAuth(web3Wallet.address);
} else { } else {
isConnectingWalletRef.current = true; isConnectingWalletRef.current = true;
web3Modal.open(); web3Wallet.openModal();
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Started' });
} }
}, [ address, proceedToAuth, source, web3Modal ]); }, [ proceedToAuth, web3Wallet ]);
React.useEffect(() => { React.useEffect(() => {
if (address && isConnectingWalletRef.current) { if (web3Wallet.address && isConnectingWalletRef.current) {
isConnectingWalletRef.current = false; isConnectingWalletRef.current = false;
proceedToAuth(address); proceedToAuth(web3Wallet.address);
mixpanel.logEvent(mixpanel.EventTypes.WALLET_CONNECT, { Source: source, Status: 'Connected' });
} }
}, [ address, isConnected, proceedToAuth, source ]); }, [ proceedToAuth, web3Wallet.address ]);
return React.useMemo(() => ({ start, isPending }), [ start, isPending ]); return React.useMemo(() => ({ start, isPending }), [ start, isPending ]);
} }
......
...@@ -99,7 +99,7 @@ test.describe('with tooltips', () => { ...@@ -99,7 +99,7 @@ test.describe('with tooltips', () => {
await component.locator('header').hover(); await component.locator('header').hover();
await page.locator('div[aria-label="Expand/Collapse menu"]').click(); await page.locator('div[aria-label="Expand/Collapse menu"]').click();
await page.locator('a[aria-label="Tokens link"]').hover(); await page.locator('a[aria-label="DApps link"]').hover();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
......
...@@ -106,7 +106,7 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -106,7 +106,7 @@ const SearchBar = ({ isHomepage }: Props) => {
React.useEffect(() => { React.useEffect(() => {
handleSearchTermChange(''); handleSearchTermChange('');
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ router.pathname ]); }, [ router.asPath?.split('?')?.[0] ]);
React.useEffect(() => { React.useEffect(() => {
const inputEl = inputRef.current; const inputEl = inputRef.current;
...@@ -151,6 +151,7 @@ const SearchBar = ({ isHomepage }: Props) => { ...@@ -151,6 +151,7 @@ const SearchBar = ({ isHomepage }: Props) => {
<PopoverContent <PopoverContent
w={ `${ menuWidth.current }px` } w={ `${ menuWidth.current }px` }
ref={ menuRef } ref={ menuRef }
overflow="hidden"
> >
<PopoverBody <PopoverBody
p={ 0 } p={ 0 }
......
import { InputGroup, Input, InputLeftElement, chakra, useColorModeValue, forwardRef, InputRightElement } from '@chakra-ui/react'; import { InputGroup, Input, InputLeftElement, chakra, useColorModeValue, forwardRef, InputRightElement, Center } from '@chakra-ui/react';
import throttle from 'lodash/throttle'; import throttle from 'lodash/throttle';
import React from 'react'; import React from 'react';
import type { ChangeEvent, FormEvent, FocusEvent } from 'react'; import type { ChangeEvent, FormEvent, FocusEvent } from 'react';
...@@ -62,9 +62,69 @@ const SearchBarInput = ( ...@@ -62,9 +62,69 @@ const SearchBarInput = (
}; };
}, [ isMobile, handleScroll ]); }, [ isMobile, handleScroll ]);
const handleKeyPress = React.useCallback((event: KeyboardEvent) => {
if (isMobile) {
return;
}
switch (event.key) {
case '/': {
if ([ 'INPUT', 'TEXTAREA' ].includes((event.target as HTMLElement).tagName)) {
break;
}
if (!isSuggestOpen) {
event.preventDefault();
innerRef.current?.querySelector('input')?.focus();
onFocus?.();
}
break;
}
case 'Escape': {
if (isSuggestOpen) {
innerRef.current?.querySelector('input')?.blur();
onHide?.();
}
break;
}
}
}, [ isMobile, isSuggestOpen, onFocus, onHide ]);
React.useEffect(() => {
window.addEventListener('keydown', handleKeyPress);
return () => {
window.removeEventListener('keydown', handleKeyPress);
};
}, [ handleKeyPress ]);
const bgColor = useColorModeValue('white', 'black'); const bgColor = useColorModeValue('white', 'black');
const transformMobile = scrollDirection !== 'down' ? 'translateY(0)' : 'translateY(-100%)'; const transformMobile = scrollDirection !== 'down' ? 'translateY(0)' : 'translateY(-100%)';
const rightElement = (() => {
if (value) {
return <ClearButton onClick={ onClear }/>;
}
if (isMobile) {
return null;
}
return (
<Center
boxSize="20px"
my="2px"
mr={{ base: 1, lg: isHomepage ? 2 : 1 }}
borderRadius="sm"
borderWidth="1px"
borderColor="gray.400"
color="gray.400"
display={{ base: 'none', lg: 'flex' }}
>
/
</Center>
);
})();
return ( return (
<chakra.form <chakra.form
ref={ innerRef } ref={ innerRef }
...@@ -111,11 +171,9 @@ const SearchBarInput = ( ...@@ -111,11 +171,9 @@ const SearchBarInput = (
color={ useColorModeValue('black', 'white') } color={ useColorModeValue('black', 'white') }
value={ value } value={ value }
/> />
{ value && ( <InputRightElement top={{ base: 2, lg: isHomepage ? 3 : 2 }} right={ 2 }>
<InputRightElement top={{ base: 2, lg: isHomepage ? 3 : 2 }} right={ 2 }> { rightElement }
<ClearButton onClick={ onClear }/> </InputRightElement>
</InputRightElement>
) }
</InputGroup> </InputGroup>
</chakra.form> </chakra.form>
); );
......
import { Box, Button, Divider, Flex, Link, VStack } from '@chakra-ui/react'; import { Box, Button, Divider, Flex, Link, VStack, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { NavLink } from './types'; import type { NavLink } from './types';
...@@ -18,6 +18,11 @@ import UserProfileContentNavLink from './UserProfileContentNavLink'; ...@@ -18,6 +18,11 @@ import UserProfileContentNavLink from './UserProfileContentNavLink';
import UserProfileContentWallet from './UserProfileContentWallet'; import UserProfileContentWallet from './UserProfileContentWallet';
const navLinks: Array<NavLink> = [ const navLinks: Array<NavLink> = [
{
text: 'My profile',
href: route({ pathname: '/auth/profile' }),
icon: 'profile' as const,
},
{ {
text: 'Watch list', text: 'Watch list',
href: route({ pathname: '/account/watchlist' }), href: route({ pathname: '/account/watchlist' }),
...@@ -57,6 +62,8 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress } ...@@ -57,6 +62,8 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress }
const { isAutoConnectDisabled } = useMarketplaceContext(); const { isAutoConnectDisabled } = useMarketplaceContext();
const logout = useLogout(); const logout = useLogout();
const accountTextColor = useColorModeValue('gray.500', 'gray.300');
if (!data) { if (!data) {
return ( return (
<Box> <Box>
...@@ -71,15 +78,16 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress } ...@@ -71,15 +78,16 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress }
<Box> <Box>
{ isAutoConnectDisabled && <UserWalletAutoConnectAlert/> } { isAutoConnectDisabled && <UserWalletAutoConnectAlert/> }
<UserProfileContentNavLink <Box fontSize="xs" lineHeight={ 6 } fontWeight="500" px={ 1 } mb="2px">Account</Box>
text="My profile" <Box
href={ route({ pathname: '/auth/profile' }) } fontSize="xs"
icon="profile" lineHeight={ 4 }
onClick={ onClose } fontWeight="500"
py="8px" borderColor="divider"
/> borderWidth="1px"
borderRadius="base"
<Box fontSize="xs" lineHeight={ 4 } fontWeight="500" borderColor="divider" borderWidth="1px" borderRadius="base"> color={ accountTextColor }
>
{ config.features.blockchainInteraction.isEnabled && ( { config.features.blockchainInteraction.isEnabled && (
<Flex p={ 2 } borderColor="divider" borderBottomWidth="1px"> <Flex p={ 2 } borderColor="divider" borderBottomWidth="1px">
<Box>Address</Box> <Box>Address</Box>
...@@ -91,7 +99,7 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress } ...@@ -91,7 +99,7 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress }
/> />
{ data?.address_hash ? { data?.address_hash ?
<Box>{ shortenString(data?.address_hash) }</Box> : <Box>{ shortenString(data?.address_hash) }</Box> :
<Link onClick={ onAddAddress } color="text_secondary" _hover={{ color: 'link_hovered', textDecoration: 'none' }}>Add address</Link> <Link onClick={ onAddAddress } color="icon_info" _hover={{ color: 'link_hovered', textDecoration: 'none' }}>Add address</Link>
} }
</Flex> </Flex>
) } ) }
...@@ -99,14 +107,14 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress } ...@@ -99,14 +107,14 @@ const UserProfileContent = ({ data, onClose, onLogin, onAddEmail, onAddAddress }
<Box mr="auto">Email</Box> <Box mr="auto">Email</Box>
{ data?.email ? { data?.email ?
<TruncatedValue value={ data.email }/> : <TruncatedValue value={ data.email }/> :
<Link onClick={ onAddEmail } color="text_secondary" _hover={{ color: 'link_hovered', textDecoration: 'none' }}>Add email</Link> <Link onClick={ onAddEmail } color="icon_info" _hover={{ color: 'link_hovered', textDecoration: 'none' }}>Add email</Link>
} }
</Flex> </Flex>
</Box> </Box>
{ config.features.blockchainInteraction.isEnabled && <UserProfileContentWallet onClose={ onClose } mt={ 3 }/> } { config.features.blockchainInteraction.isEnabled && <UserProfileContentWallet onClose={ onClose } mt={ 3 }/> }
<VStack as="ul" spacing="0" alignItems="flex-start" overflow="hidden" mt={ 3 }> <VStack as="ul" spacing="0" alignItems="flex-start" overflow="hidden" mt={ 4 }>
{ navLinks.map((item) => ( { navLinks.map((item) => (
<UserProfileContentNavLink <UserProfileContentNavLink
key={ item.text } key={ item.text }
......
import type { SpaceProps } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -7,11 +6,7 @@ import type { NavLink } from './types'; ...@@ -7,11 +6,7 @@ import type { NavLink } from './types';
import IconSvg from 'ui/shared/IconSvg'; import IconSvg from 'ui/shared/IconSvg';
import LinkInternal from 'ui/shared/links/LinkInternal'; import LinkInternal from 'ui/shared/links/LinkInternal';
type Props = NavLink & { const UserProfileContentNavLink = ({ href, icon, text, onClick }: NavLink) => {
py?: SpaceProps['py'];
}
const UserProfileContentNavLink = ({ href, icon, text, onClick, py }: Props) => {
return ( return (
<LinkInternal <LinkInternal
...@@ -19,8 +14,8 @@ const UserProfileContentNavLink = ({ href, icon, text, onClick, py }: Props) => ...@@ -19,8 +14,8 @@ const UserProfileContentNavLink = ({ href, icon, text, onClick, py }: Props) =>
display="flex" display="flex"
alignItems="center" alignItems="center"
columnGap={ 3 } columnGap={ 3 }
py={ py ?? '14px' } py="14px"
color="initial" color="inherit"
_hover={{ textDecoration: 'none', color: 'link_hovered' }} _hover={{ textDecoration: 'none', color: 'link_hovered' }}
onClick={ onClick } onClick={ onClick }
> >
......
...@@ -73,7 +73,7 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => { ...@@ -73,7 +73,7 @@ const UserProfileContentWallet = ({ onClose, className }: Props) => {
return ( return (
<Box className={ className }> <Box className={ className }>
<Flex px={ 2 } py={ 1 } mb={ 1 } fontSize="xs" lineHeight={ 4 } fontWeight="500"> <Flex px={ 1 } mb="2px" fontSize="xs" alignItems="center" lineHeight={ 6 } fontWeight="500">
<span>Connected wallet</span> <span>Connected wallet</span>
<Hint <Hint
label={ label={
......
...@@ -15,7 +15,7 @@ interface Props { ...@@ -15,7 +15,7 @@ interface Props {
const TokenHoldersTable = ({ data, token, top, isLoading }: Props) => { const TokenHoldersTable = ({ data, token, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" layout="auto"> <Table layout="auto">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th>Holder</Th> <Th>Holder</Th>
......
...@@ -27,7 +27,7 @@ const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socket ...@@ -27,7 +27,7 @@ const TokenTransferTable = ({ data, top, showSocketInfo, socketInfoAlert, socket
return ( return (
<AddressHighlightProvider> <AddressHighlightProvider>
<Table variant="simple" size="sm" minW="950px"> <Table minW="950px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th width="280px">Txn hash</Th> <Th width="280px">Txn hash</Th>
......
import { Flex, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import getCurrencyValue from 'lib/getCurrencyValue';
import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import Tag from 'ui/shared/chakra/Tag';
import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import NftEntity from 'ui/shared/entities/nft/NftEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import ListItemMobileGrid from 'ui/shared/ListItemMobile/ListItemMobileGrid';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
type Props = {
item: TokenTransfer;
isLoading: boolean;
}
const TokenTransfersListItem = ({ item, isLoading }: Props) => {
const { valueStr } = 'value' in item.total && item.total.value !== null ? getCurrencyValue({
value: item.total.value,
exchangeRate: item.token.exchange_rate,
accuracy: 8,
accuracyUsd: 2,
decimals: item.total.decimals || '0',
}) : { valueStr: null };
return (
<ListItemMobileGrid.Container>
<ListItemMobileGrid.Label isLoading={ isLoading }>Txn hash</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TxEntity hash={ item.tx_hash } isLoading={ isLoading } truncation="constant_long" noIcon/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>Age</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<TimeAgoWithTooltip
timestamp={ item.timestamp }
enableIncrement
isLoading={ isLoading }
/>
</ListItemMobileGrid.Value>
{ item.method && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>Method</ListItemMobileGrid.Label><ListItemMobileGrid.Value>
<Tag isLoading={ isLoading }>{ item.method }</Tag>
</ListItemMobileGrid.Value>
</>
) }
<ListItemMobileGrid.Label isLoading={ isLoading }>Block</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<BlockEntity number={ item.block_number } isLoading={ isLoading } noIcon/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>From</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<AddressEntity address={ item.from } isLoading={ isLoading } truncation="constant"/>
</ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>To</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<AddressEntity address={ item.to } isLoading={ isLoading } truncation="constant"/>
</ListItemMobileGrid.Value>
{ 'token_id' in item.total && (NFT_TOKEN_TYPE_IDS.includes(item.token.type)) && item.total.token_id !== null && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>Token ID</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value overflow="hidden">
<NftEntity
hash={ item.token.address }
id={ item.total.token_id }
isLoading={ isLoading }
noIcon
/>
</ListItemMobileGrid.Value>
</>
) }
{ valueStr && (item.token.type === 'ERC-20' || item.token.type === 'ERC-1155') && (
<>
<ListItemMobileGrid.Label isLoading={ isLoading }>Amount</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value>
<Flex gap={ 2 } overflow="hidden">
<Skeleton isLoaded={ !isLoading } wordBreak="break-all">
{ valueStr }
</Skeleton>
<TokenEntity
token={ item.token }
isLoading={ isLoading }
onlySymbol
noCopy
width="auto"
minW="auto"
maxW="100px"
/>
</Flex>
</ListItemMobileGrid.Value>
</>
) }
</ListItemMobileGrid.Container>
);
};
export default TokenTransfersListItem;
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
import { default as Thead } from 'ui/shared/TheadSticky';
import TokenTransferTableItem from 'ui/tokenTransfers/TokenTransfersTableItem';
interface Props {
items?: Array<TokenTransfer>;
top: number;
isLoading?: boolean;
}
const TokenTransferTable = ({ items, top, isLoading }: Props) => {
return (
<AddressHighlightProvider>
<Table variant="simple" size="sm" minW="950px" style={{ tableLayout: 'auto' }}>
<Thead top={ top }>
<Tr>
<Th>Txn hash</Th>
<Th>Method</Th>
<Th>Block</Th>
<Th>From/To</Th>
<Th>Token ID</Th>
<Th isNumeric>Amount</Th>
</Tr>
</Thead>
<Tbody>
{ items?.map((item, index) => (
<TokenTransferTableItem
key={ item.block_number + item.log_index + (isLoading ? index : '') }
item={ item }
isLoading={ isLoading }
/>
)) }
</Tbody>
</Table>
</AddressHighlightProvider>
);
};
export default React.memo(TokenTransferTable);
import { Tr, Td, Flex, Skeleton } from '@chakra-ui/react';
import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import getCurrencyValue from 'lib/getCurrencyValue';
import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
import AddressFromTo from 'ui/shared/address/AddressFromTo';
import Tag from 'ui/shared/chakra/Tag';
import BlockEntity from 'ui/shared/entities/block/BlockEntity';
import NftEntity from 'ui/shared/entities/nft/NftEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import TxEntity from 'ui/shared/entities/tx/TxEntity';
import TimeAgoWithTooltip from 'ui/shared/TimeAgoWithTooltip';
type Props = {
item: TokenTransfer;
isLoading?: boolean;
}
const TokenTransferTableItem = ({ item, isLoading }: Props) => {
const { valueStr } = 'value' in item.total && item.total.value !== null ? getCurrencyValue({
value: item.total.value,
exchangeRate: item.token.exchange_rate,
accuracy: 8,
accuracyUsd: 2,
decimals: item.total.decimals || '0',
}) : { valueStr: null };
return (
<Tr>
<Td>
<TxEntity
hash={ item.tx_hash }
isLoading={ isLoading }
fontWeight={ 600 }
noIcon
truncation="constant_long"
/>
<TimeAgoWithTooltip
timestamp={ item.timestamp }
enableIncrement
isLoading={ isLoading }
color="text_secondary"
fontWeight="400"
display="inline-block"
/>
</Td>
<Td maxW="120px">
{ item.method && <Tag isLoading={ isLoading }>{ item.method }</Tag> }
</Td>
<Td>
<BlockEntity number={ item.block_number } isLoading={ isLoading } noIcon/>
</Td>
<Td>
<AddressFromTo
maxW={{ lg: '220px', xl: '320px' }}
from={ item.from }
to={ item.to }
isLoading={ isLoading }
mode={{ lg: 'compact', xl: 'long' }}
/>
</Td>
<Td>
{ 'token_id' in item.total && (NFT_TOKEN_TYPE_IDS.includes(item.token.type)) && item.total.token_id !== null ? (
<NftEntity
hash={ item.token.address }
id={ item.total.token_id }
isLoading={ isLoading }
maxW="140px"
/>
) : '-' }
</Td>
<Td isNumeric verticalAlign="top">
{ valueStr ? (
<Flex gap={ 2 } overflow="hidden" justifyContent="flex-end">
<Skeleton isLoaded={ !isLoading } wordBreak="break-all">
{ valueStr }
</Skeleton>
<TokenEntity
token={ item.token }
isLoading={ isLoading }
onlySymbol
noCopy
width="auto"
minW="auto"
maxW="100px"
/>
</Flex>
) : '-'
}
</Td>
</Tr>
);
};
export default React.memo(TokenTransferTableItem);
...@@ -82,7 +82,7 @@ export default function TxAssetFlows(props: FlowViewProps) { ...@@ -82,7 +82,7 @@ export default function TxAssetFlows(props: FlowViewProps) {
</Hide> </Hide>
<Show above="lg"> <Show above="lg">
<Table variant="simple" size="sm"> <Table>
<TheadSticky top={ 75 }> <TheadSticky top={ 75 }>
<Tr> <Tr>
<Th> <Th>
......
...@@ -16,7 +16,7 @@ interface Props { ...@@ -16,7 +16,7 @@ interface Props {
const TxInternalsTable = ({ data, top, isLoading }: Props) => { const TxInternalsTable = ({ data, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm"> <Table>
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th width="60%">Blob hash</Th> <Th width="60%">Blob hash</Th>
......
...@@ -306,7 +306,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => { ...@@ -306,7 +306,7 @@ const TxInfo = ({ data, isLoading, socketStatus }: Props) => {
hint="Batch index for this transaction" hint="Batch index for this transaction"
isLoading={ isLoading } isLoading={ isLoading }
> >
Tx batch Txn batch
</DetailsInfoItem.Label> </DetailsInfoItem.Label>
<DetailsInfoItem.Value> <DetailsInfoItem.Value>
<BatchEntityL2 <BatchEntityL2
......
...@@ -23,7 +23,7 @@ const TxInternalsTable = ({ data, sort, onSortToggle, top, isLoading }: Props) = ...@@ -23,7 +23,7 @@ const TxInternalsTable = ({ data, sort, onSortToggle, top, isLoading }: Props) =
return ( return (
<AddressHighlightProvider> <AddressHighlightProvider>
<Table variant="simple" size="sm"> <Table>
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th width="28%">Type</Th> <Th width="28%">Type</Th>
......
...@@ -21,7 +21,7 @@ interface Props { ...@@ -21,7 +21,7 @@ interface Props {
const TxStateTable = ({ data, isLoading, top }: Props) => { const TxStateTable = ({ data, isLoading, top }: Props) => {
return ( return (
<AddressHighlightProvider> <AddressHighlightProvider>
<Table variant="simple" minWidth="1000px" size="sm" w="100%"> <Table minWidth="1000px" w="100%">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th width="140px">Type</Th> <Th width="140px">Type</Th>
......
...@@ -77,7 +77,7 @@ const ArbitrumL2TxnBatchDetails = ({ query }: Props) => { ...@@ -77,7 +77,7 @@ const ArbitrumL2TxnBatchDetails = ({ query }: Props) => {
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
hint="Batch number indicates the length of batches produced by grouping L2 blocks to be proven on L1" hint="Batch number indicates the length of batches produced by grouping L2 blocks to be proven on L1"
> >
Tx batch number Txn batch number
</DetailsInfoItem.Label> </DetailsInfoItem.Label>
<DetailsInfoItem.Value> <DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }> <Skeleton isLoaded={ !isPlaceholderData }>
...@@ -86,8 +86,8 @@ const ArbitrumL2TxnBatchDetails = ({ query }: Props) => { ...@@ -86,8 +86,8 @@ const ArbitrumL2TxnBatchDetails = ({ query }: Props) => {
<PrevNext <PrevNext
ml={ 6 } ml={ 6 }
onClick={ handlePrevNextClick } onClick={ handlePrevNextClick }
prevLabel="View previous tx batch" prevLabel="View previous txn batch"
nextLabel="View next tx batch" nextLabel="View next txn batch"
isPrevDisabled={ data.number === 0 } isPrevDisabled={ data.number === 0 }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
/> />
......
...@@ -15,7 +15,7 @@ type Props = { ...@@ -15,7 +15,7 @@ type Props = {
const ArbitrumL2TxnBatchesTable = ({ items, top, isLoading }: Props) => { const ArbitrumL2TxnBatchesTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" minW="1000px" style={{ tableLayout: 'auto' }}> <Table minW="1000px" style={{ tableLayout: 'auto' }}>
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th>Batch #</Th> <Th>Batch #</Th>
......
...@@ -47,7 +47,7 @@ const OptimisticL2TxnBatchBlobCelestia = ({ blobs, isLoading }: Props) => { ...@@ -47,7 +47,7 @@ const OptimisticL2TxnBatchBlobCelestia = ({ blobs, isLoading }: Props) => {
<Icon as={ celeniumIcon } boxSize={ 5 }/> <Icon as={ celeniumIcon } boxSize={ 5 }/>
<LinkExternal href={ getCeleniumUrl(blob) }>Blob page</LinkExternal> <LinkExternal href={ getCeleniumUrl(blob) }>Blob page</LinkExternal>
</GridItem> </GridItem>
<GridItem fontWeight={ 600 }>Hight</GridItem> <GridItem fontWeight={ 600 }>Height</GridItem>
<GridItem colSpan={ 2 }> <GridItem colSpan={ 2 }>
{ blob.height } { blob.height }
</GridItem> </GridItem>
......
...@@ -75,8 +75,8 @@ const OptimisticL2TxnBatchDetails = ({ query }: Props) => { ...@@ -75,8 +75,8 @@ const OptimisticL2TxnBatchDetails = ({ query }: Props) => {
<PrevNext <PrevNext
ml={ 6 } ml={ 6 }
onClick={ handlePrevNextClick } onClick={ handlePrevNextClick }
prevLabel="View previous tx batch" prevLabel="View previous txn batch"
nextLabel="View next tx batch" nextLabel="View next txn batch"
isPrevDisabled={ data.internal_id === 0 } isPrevDisabled={ data.internal_id === 0 }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
/> />
......
...@@ -68,7 +68,7 @@ const OptimisticL2TxnBatchesListItem = ({ item, isLoading }: Props) => { ...@@ -68,7 +68,7 @@ const OptimisticL2TxnBatchesListItem = ({ item, isLoading }: Props) => {
</LinkInternal> </LinkInternal>
</ListItemMobileGrid.Value> </ListItemMobileGrid.Value>
<ListItemMobileGrid.Label isLoading={ isLoading }>L2 blocks</ListItemMobileGrid.Label> <ListItemMobileGrid.Label isLoading={ isLoading }>Txn</ListItemMobileGrid.Label>
<ListItemMobileGrid.Value> <ListItemMobileGrid.Value>
<LinkInternal <LinkInternal
href={ route({ pathname: '/batches/[number]', query: { number: item.internal_id.toString(), tab: 'txs' } }) } href={ route({ pathname: '/batches/[number]', query: { number: item.internal_id.toString(), tab: 'txs' } }) }
......
...@@ -15,7 +15,7 @@ type Props = { ...@@ -15,7 +15,7 @@ type Props = {
const OptimisticL2TxnBatchesTable = ({ items, top, isLoading }: Props) => { const OptimisticL2TxnBatchesTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" minW="850px" layout="auto"> <Table minW="850px" layout="auto">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th>Batch ID</Th> <Th>Batch ID</Th>
......
...@@ -64,7 +64,7 @@ const ZkEvmL2TxnBatchDetails = ({ query }: Props) => { ...@@ -64,7 +64,7 @@ const ZkEvmL2TxnBatchDetails = ({ query }: Props) => {
<DetailsInfoItem.Label <DetailsInfoItem.Label
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
> >
Tx batch number Txn batch number
</DetailsInfoItem.Label> </DetailsInfoItem.Label>
<DetailsInfoItem.Value> <DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }> <Skeleton isLoaded={ !isPlaceholderData }>
...@@ -73,8 +73,8 @@ const ZkEvmL2TxnBatchDetails = ({ query }: Props) => { ...@@ -73,8 +73,8 @@ const ZkEvmL2TxnBatchDetails = ({ query }: Props) => {
<PrevNext <PrevNext
ml={ 6 } ml={ 6 }
onClick={ handlePrevNextClick } onClick={ handlePrevNextClick }
prevLabel="View previous tx batch" prevLabel="View previous txn batch"
nextLabel="View next tx batch" nextLabel="View next txn batch"
isPrevDisabled={ data.number === 0 } isPrevDisabled={ data.number === 0 }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
/> />
......
...@@ -15,7 +15,7 @@ type Props = { ...@@ -15,7 +15,7 @@ type Props = {
const TxnBatchesTable = ({ items, top, isLoading }: Props) => { const TxnBatchesTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" minW="1000px"> <Table minW="1000px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th width="33%">Batch #</Th> <Th width="33%">Batch #</Th>
......
...@@ -80,7 +80,7 @@ const ZkSyncL2TxnBatchDetails = ({ query }: Props) => { ...@@ -80,7 +80,7 @@ const ZkSyncL2TxnBatchDetails = ({ query }: Props) => {
hint="Batch number indicates the length of batches produced by grouping L2 blocks to be proven on Ethereum." hint="Batch number indicates the length of batches produced by grouping L2 blocks to be proven on Ethereum."
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
> >
Tx batch number Txn batch number
</DetailsInfoItem.Label> </DetailsInfoItem.Label>
<DetailsInfoItem.Value> <DetailsInfoItem.Value>
<Skeleton isLoaded={ !isPlaceholderData }> <Skeleton isLoaded={ !isPlaceholderData }>
...@@ -89,8 +89,8 @@ const ZkSyncL2TxnBatchDetails = ({ query }: Props) => { ...@@ -89,8 +89,8 @@ const ZkSyncL2TxnBatchDetails = ({ query }: Props) => {
<PrevNext <PrevNext
ml={ 6 } ml={ 6 }
onClick={ handlePrevNextClick } onClick={ handlePrevNextClick }
prevLabel="View previous tx batch" prevLabel="View previous txn batch"
nextLabel="View next tx batch" nextLabel="View next txn batch"
isPrevDisabled={ data.number === 0 } isPrevDisabled={ data.number === 0 }
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
/> />
......
...@@ -15,7 +15,7 @@ type Props = { ...@@ -15,7 +15,7 @@ type Props = {
const ZkSyncTxnBatchesTable = ({ items, top, isLoading }: Props) => { const ZkSyncTxnBatchesTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" minW="1000px"> <Table minW="1000px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th width="40%">Batch #</Th> <Th width="40%">Batch #</Th>
......
...@@ -49,7 +49,7 @@ const TxsTable = ({ ...@@ -49,7 +49,7 @@ const TxsTable = ({
return ( return (
<AddressHighlightProvider> <AddressHighlightProvider>
<Table variant="simple" minWidth="950px" size="xs"> <Table minWidth="950px">
<TheadSticky top={ top }> <TheadSticky top={ top }>
<Tr> <Tr>
<Th width="54px"></Th> <Th width="54px"></Th>
......
...@@ -112,7 +112,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement, ...@@ -112,7 +112,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement,
</Td> </Td>
) } ) }
{ !config.UI.views.tx.hiddenFields?.tx_fee && ( { !config.UI.views.tx.hiddenFields?.tx_fee && (
<Td isNumeric> <Td isNumeric maxW="220px">
<TxFee <TxFee
tx={ tx } tx={ tx }
accuracy={ 8 } accuracy={ 8 }
......
...@@ -18,7 +18,7 @@ import UserOpsTableItem from './UserOpsTableItem'; ...@@ -18,7 +18,7 @@ import UserOpsTableItem from './UserOpsTableItem';
const UserOpsTable = ({ items, isLoading, top, showTx, showSender }: Props) => { const UserOpsTable = ({ items, isLoading, top, showTx, showSender }: Props) => {
return ( return (
<Table variant="simple" size="sm" minW="1000px"> <Table minW="1000px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th w="60%">User op hash</Th> <Th w="60%">User op hash</Th>
......
...@@ -33,7 +33,7 @@ const ValidatorsTable = ({ data, sort, setSorting, isLoading, top }: Props) => { ...@@ -33,7 +33,7 @@ const ValidatorsTable = ({ data, sort, setSorting, isLoading, top }: Props) => {
}, [ sort, setSorting ]); }, [ sort, setSorting ]);
return ( return (
<Table variant="simple" size="sm"> <Table>
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th> <Th>
......
...@@ -32,7 +32,7 @@ const ValidatorsTable = ({ data, sort, setSorting, isLoading }: Props) => { ...@@ -32,7 +32,7 @@ const ValidatorsTable = ({ data, sort, setSorting, isLoading }: Props) => {
}, [ sort, setSorting ]); }, [ sort, setSorting ]);
return ( return (
<Table variant="simple" size="sm"> <Table>
<Thead top={ ACTION_BAR_HEIGHT_DESKTOP }> <Thead top={ ACTION_BAR_HEIGHT_DESKTOP }>
<Tr> <Tr>
<Th width="50%">Validator’s address</Th> <Th width="50%">Validator’s address</Th>
......
...@@ -15,7 +15,7 @@ interface Props { ...@@ -15,7 +15,7 @@ interface Props {
const VerifiedAddressesTable = ({ data, applications, onItemEdit, onItemAdd, isLoading }: Props) => { const VerifiedAddressesTable = ({ data, applications, onItemEdit, onItemAdd, isLoading }: Props) => {
return ( return (
<Table variant="simple"> <Table>
<Thead> <Thead>
<Tr> <Tr>
<Th>Address</Th> <Th>Address</Th>
......
...@@ -29,7 +29,7 @@ const VerifiedContractsTable = ({ data, sort, setSorting, isLoading }: Props) => ...@@ -29,7 +29,7 @@ const VerifiedContractsTable = ({ data, sort, setSorting, isLoading }: Props) =>
}, [ sort, setSorting ]); }, [ sort, setSorting ]);
return ( return (
<Table variant="simple" size="sm" minW="915px"> <Table minW="915px">
<Thead top={ ACTION_BAR_HEIGHT_DESKTOP }> <Thead top={ ACTION_BAR_HEIGHT_DESKTOP }>
<Tr> <Tr>
<Th width="50%">Contract</Th> <Th width="50%">Contract</Th>
......
...@@ -17,9 +17,10 @@ interface Props { ...@@ -17,9 +17,10 @@ interface Props {
isLoading?: boolean; isLoading?: boolean;
onEditClick: (data: WatchlistAddress) => void; onEditClick: (data: WatchlistAddress) => void;
onDeleteClick: (data: WatchlistAddress) => void; onDeleteClick: (data: WatchlistAddress) => void;
hasEmail: boolean;
} }
const WatchListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) => { const WatchListItem = ({ item, isLoading, onEditClick, onDeleteClick, hasEmail }: Props) => {
const [ notificationEnabled, setNotificationEnabled ] = useState(item.notification_methods.email); const [ notificationEnabled, setNotificationEnabled ] = useState(item.notification_methods.email);
const [ switchDisabled, setSwitchDisabled ] = useState(false); const [ switchDisabled, setSwitchDisabled ] = useState(false);
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
...@@ -49,7 +50,7 @@ const WatchListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) = ...@@ -49,7 +50,7 @@ const WatchListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) =
const showNotificationToast = useCallback((isOn: boolean) => { const showNotificationToast = useCallback((isOn: boolean) => {
notificationToast({ notificationToast({
position: 'top-right', position: 'top-right',
description: isOn ? 'Email notification is ON' : 'Email notification is OFF', description: !isOn ? 'Email notification is ON' : 'Email notification is OFF',
colorScheme: 'green', colorScheme: 'green',
status: 'success', status: 'success',
variant: 'subtle', variant: 'subtle',
...@@ -103,7 +104,7 @@ const WatchListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) = ...@@ -103,7 +104,7 @@ const WatchListItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) =
isChecked={ notificationEnabled } isChecked={ notificationEnabled }
onChange={ onSwitch } onChange={ onSwitch }
aria-label="Email notification" aria-label="Email notification"
isDisabled={ switchDisabled } isDisabled={ !hasEmail || switchDisabled }
/> />
</Skeleton> </Skeleton>
</HStack> </HStack>
......
...@@ -21,9 +21,10 @@ interface Props { ...@@ -21,9 +21,10 @@ interface Props {
isLoading?: boolean; isLoading?: boolean;
onEditClick: (data: WatchlistAddress) => void; onEditClick: (data: WatchlistAddress) => void;
onDeleteClick: (data: WatchlistAddress) => void; onDeleteClick: (data: WatchlistAddress) => void;
hasEmail: boolean;
} }
const WatchlistTableItem = ({ item, isLoading, onEditClick, onDeleteClick }: Props) => { const WatchlistTableItem = ({ item, isLoading, onEditClick, onDeleteClick, hasEmail }: Props) => {
const [ notificationEnabled, setNotificationEnabled ] = useState(item.notification_methods.email); const [ notificationEnabled, setNotificationEnabled ] = useState(item.notification_methods.email);
const [ switchDisabled, setSwitchDisabled ] = useState(false); const [ switchDisabled, setSwitchDisabled ] = useState(false);
const onItemEditClick = useCallback(() => { const onItemEditClick = useCallback(() => {
...@@ -53,7 +54,7 @@ const WatchlistTableItem = ({ item, isLoading, onEditClick, onDeleteClick }: Pro ...@@ -53,7 +54,7 @@ const WatchlistTableItem = ({ item, isLoading, onEditClick, onDeleteClick }: Pro
const showNotificationToast = useCallback((isOn: boolean) => { const showNotificationToast = useCallback((isOn: boolean) => {
notificationToast({ notificationToast({
position: 'top-right', position: 'top-right',
description: isOn ? 'Email notification is ON' : 'Email notification is OFF', description: !isOn ? 'Email notification is ON' : 'Email notification is OFF',
colorScheme: 'green', colorScheme: 'green',
status: 'success', status: 'success',
variant: 'subtle', variant: 'subtle',
...@@ -101,7 +102,7 @@ const WatchlistTableItem = ({ item, isLoading, onEditClick, onDeleteClick }: Pro ...@@ -101,7 +102,7 @@ const WatchlistTableItem = ({ item, isLoading, onEditClick, onDeleteClick }: Pro
size="md" size="md"
isChecked={ notificationEnabled } isChecked={ notificationEnabled }
onChange={ onSwitch } onChange={ onSwitch }
isDisabled={ switchDisabled } isDisabled={ !hasEmail || switchDisabled }
aria-label="Email notification" aria-label="Email notification"
/> />
</Skeleton> </Skeleton>
......
...@@ -18,11 +18,12 @@ interface Props { ...@@ -18,11 +18,12 @@ interface Props {
onEditClick: (data: WatchlistAddress) => void; onEditClick: (data: WatchlistAddress) => void;
onDeleteClick: (data: WatchlistAddress) => void; onDeleteClick: (data: WatchlistAddress) => void;
top: number; top: number;
hasEmail: boolean;
} }
const WatchlistTable = ({ data, isLoading, onDeleteClick, onEditClick, top }: Props) => { const WatchlistTable = ({ data, isLoading, onDeleteClick, onEditClick, top, hasEmail }: Props) => {
return ( return (
<Table variant="simple" minWidth="600px"> <Table minWidth="600px">
<TheadSticky top={ top }> <TheadSticky top={ top }>
<Tr> <Tr>
<Th width="70%">Address</Th> <Th width="70%">Address</Th>
...@@ -39,6 +40,7 @@ const WatchlistTable = ({ data, isLoading, onDeleteClick, onEditClick, top }: Pr ...@@ -39,6 +40,7 @@ const WatchlistTable = ({ data, isLoading, onDeleteClick, onEditClick, top }: Pr
isLoading={ isLoading } isLoading={ isLoading }
onDeleteClick={ onDeleteClick } onDeleteClick={ onDeleteClick }
onEditClick={ onEditClick } onEditClick={ onEditClick }
hasEmail={ hasEmail }
/> />
)) } )) }
</Tbody> </Tbody>
......
...@@ -35,7 +35,7 @@ const BeaconChainWithdrawalsTable = ({ items, isLoading, top, view }: Props) => ...@@ -35,7 +35,7 @@ const BeaconChainWithdrawalsTable = ({ items, isLoading, top, view }: Props) =>
} }
return ( return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px"> <Table style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th minW="140px">Index</Th> <Th minW="140px">Index</Th>
......
...@@ -15,7 +15,7 @@ import OptimisticL2WithdrawalsTableItem from './OptimisticL2WithdrawalsTableItem ...@@ -15,7 +15,7 @@ import OptimisticL2WithdrawalsTableItem from './OptimisticL2WithdrawalsTableItem
const OptimisticL2WithdrawalsTable = ({ items, top, isLoading }: Props) => { const OptimisticL2WithdrawalsTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px"> <Table style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th>Msg nonce</Th> <Th>Msg nonce</Th>
......
...@@ -15,7 +15,7 @@ import WithdrawalsTableItem from './WithdrawalsTableItem'; ...@@ -15,7 +15,7 @@ import WithdrawalsTableItem from './WithdrawalsTableItem';
const WithdrawalsTable = ({ items, top, isLoading }: Props) => { const WithdrawalsTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px"> <Table style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th>L2 block No</Th> <Th>L2 block No</Th>
......
...@@ -15,7 +15,7 @@ import ZkEvmL2WithdrawalsTableItem from './ZkEvmL2WithdrawalsTableItem'; ...@@ -15,7 +15,7 @@ import ZkEvmL2WithdrawalsTableItem from './ZkEvmL2WithdrawalsTableItem';
const ZkEvmL2DepositsTable = ({ items, top, isLoading }: Props) => { const ZkEvmL2DepositsTable = ({ items, top, isLoading }: Props) => {
return ( return (
<Table variant="simple" size="sm" style={{ tableLayout: 'auto' }} minW="950px"> <Table style={{ tableLayout: 'auto' }} minW="950px">
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th>Block</Th> <Th>Block</Th>
......
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