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;
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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