Commit bcc3cb75 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into tom2drum/issue-2029

parents f9e590b8 49bfadeb
...@@ -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) {
......
...@@ -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
......
...@@ -506,6 +506,7 @@ This feature is **always enabled**, but you can disable it by passing `none` val ...@@ -506,6 +506,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>
...@@ -622,6 +622,12 @@ export const RESOURCES = { ...@@ -622,6 +622,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',
...@@ -1038,7 +1044,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' | 'block_election_reward ...@@ -1038,7 +1044,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>;
...@@ -1208,6 +1215,7 @@ Q extends 'address_mud_record' ? AddressMudRecord : ...@@ -1208,6 +1215,7 @@ Q extends 'address_mud_record' ? AddressMudRecord :
Q extends 'address_epoch_rewards' ? AddressEpochRewardsResponse : 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 'token_transfers_all' ? TokenTransferResponse :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -1242,6 +1250,7 @@ Q extends 'user_ops' ? UserOpsFilters : ...@@ -1242,6 +1250,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: {},
});
}
...@@ -179,6 +179,21 @@ export default function useNavItems(): ReturnType { ...@@ -179,6 +179,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',
...@@ -232,9 +247,9 @@ export default function useNavItems(): ReturnType { ...@@ -232,9 +247,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',
......
...@@ -51,6 +51,7 @@ const OG_TYPE_DICT: Record<Route['pathname'], OGPageType> = { ...@@ -51,6 +51,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',
......
...@@ -55,6 +55,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -55,6 +55,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,
......
...@@ -51,6 +51,7 @@ const TEMPLATE_MAP: Record<Route['pathname'], string> = { ...@@ -51,6 +51,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',
......
...@@ -49,6 +49,7 @@ export const PAGE_TYPE_DICT: Record<Route['pathname'], string> = { ...@@ -49,6 +49,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',
......
...@@ -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',
}; };
......
...@@ -57,6 +57,7 @@ declare module "nextjs-routes" { ...@@ -57,6 +57,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"
...@@ -151,6 +152,7 @@ ...@@ -151,6 +152,7 @@
| "swap" | "swap"
| "testnet" | "testnet"
| "token-placeholder" | "token-placeholder"
| "token-transfers"
| "token" | "token"
| "tokens" | "tokens"
| "tokens/xdai" | "tokens/xdai"
......
...@@ -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',
......
...@@ -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: { th: {
px: '10px', px: '6px',
py: '10px', py: '10px',
fontSize: 'sm', fontSize: 'sm',
_first: {
pl: 3,
}, },
td: { _last: {
px: '10px', pr: 3,
py: 4,
fontSize: 'sm',
fontWeight: 500,
}, },
}),
xs: definePartsStyle({
th: {
px: '6px',
py: '10px',
fontSize: 'sm',
}, },
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;
...@@ -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>
......
...@@ -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>
......
...@@ -15,7 +15,7 @@ import AddressEpochRewardsTableItem from './AddressEpochRewardsTableItem'; ...@@ -15,7 +15,7 @@ import AddressEpochRewardsTableItem from './AddressEpochRewardsTableItem';
const AddressEpochRewardsTable = ({ items, isLoading, top }: Props) => { const AddressEpochRewardsTable = ({ items, isLoading, top }: 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>Block</Th> <Th>Block</Th>
......
...@@ -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>
......
...@@ -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"/>
......
...@@ -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>
......
...@@ -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>
<Flex alignItems="center" mb={{ md: 2 }} gridColumn={ 2 }>
<Heading <Heading
as="h2" as="h2"
gridColumn={ 2 }
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>
......
...@@ -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>
......
...@@ -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] }
/> />
) } ) }
......
...@@ -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;
...@@ -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>
......
...@@ -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> }
......
...@@ -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();
}); });
......
...@@ -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>
......
...@@ -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>
......
...@@ -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>
......
...@@ -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>
......
...@@ -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>
......
...@@ -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>
......
...@@ -23,7 +23,7 @@ interface Props { ...@@ -23,7 +23,7 @@ interface Props {
const WatchlistTable = ({ data, isLoading, onDeleteClick, onEditClick, top, hasEmail }: 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>
......
...@@ -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