Commit a8343da3 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into cors-refactoring

parents d6d7deb4 4750798b
...@@ -126,14 +126,16 @@ frontend: ...@@ -126,14 +126,16 @@ frontend:
_default: ENC[AES256_GCM,data:Hf4azYsGh2lysotK7afaHI85IaLBJeQmPlGE/lwokmX2eaQHVZJ/i5RsaoKoOCSiNyfAowr7P+6IBEC16BUTQMpbhYveBd7c/xjIsoomoHbTdoAdZA/QxNVpS4a2qVthruFudyT1BoZUHIGQ,iv:Mid+PfbslOyivrFSXopdeW96YvOcLP+g2RGcw1o7B98=,tag:IuPUsGdbZQodCMyi1DR04Q==,type:str] _default: ENC[AES256_GCM,data:Hf4azYsGh2lysotK7afaHI85IaLBJeQmPlGE/lwokmX2eaQHVZJ/i5RsaoKoOCSiNyfAowr7P+6IBEC16BUTQMpbhYveBd7c/xjIsoomoHbTdoAdZA/QxNVpS4a2qVthruFudyT1BoZUHIGQ,iv:Mid+PfbslOyivrFSXopdeW96YvOcLP+g2RGcw1o7B98=,tag:IuPUsGdbZQodCMyi1DR04Q==,type:str]
NEXT_PUBLIC_AUTH0_CLIENT_ID: NEXT_PUBLIC_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:cRcbMzOW2AFyDz/lqv4T9SpDCabXdBKLFN9dbq/rFg4=,iv:G9afggfvZ+BpuE5u31KDVfIxhlP38IE28Yt9pMQsd6E=,tag:t1kkjXPolc/58fuQVULLfA==,type:str] _default: ENC[AES256_GCM,data:cRcbMzOW2AFyDz/lqv4T9SpDCabXdBKLFN9dbq/rFg4=,iv:G9afggfvZ+BpuE5u31KDVfIxhlP38IE28Yt9pMQsd6E=,tag:t1kkjXPolc/58fuQVULLfA==,type:str]
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID:
_default: ENC[AES256_GCM,data:LhVaS9L3ujRwznCe6D+edYU5XT4GLXm4jJ4lKGsrzyA=,iv:ibEJAfSrm3ZWtVJ5Du9MagbC6/Tv5L3xQQRjeQ1BGDA=,tag:IqtgpmJPhmHnSgMuIfI/0A==,type:str]
sops: sops:
kms: [] kms: []
gcp_kms: [] gcp_kms: []
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2022-11-17T09:16:34Z" lastmodified: "2023-01-18T10:42:25Z"
mac: ENC[AES256_GCM,data:kMQ1Hxpvdg90p54JD4SFMdPlH3bSi2L8QOX2d80ZFUpli2FYYrqlr4AA+cLLEoar9Vfs9yy5t8Wo0s4pjEJXnMd6hHdp8zon3Y99EO/6/+8O3nP/uvvRONrHy8gJHL+afbWWkmzTDE1gBgB3x7/06mVv2XWgXZfvr323f3yggzU=,iv:5ux8DilPzqzoRAxowl2EXYteg4Pjd8E5d4kb36LSKBU=,tag:TcX7FtlHIhfHZPWKxfqsgA==,type:str] mac: ENC[AES256_GCM,data:QZixaOd5zjucSuwtyBcgACtNynt2X23B6Dxqxm2ZQtsvwaqz51i7TOe1BlmORT+71KDJhnaDmodc+xcNAta0K0e8uS0qFvDaE3aew77yfpn02kM5/2PwYc2xlh7nKg6dsfddxERx5UzaWLQWnU7ODN7hpsZ3Q5Hurf9fI5APleI=,iv:XufptrfeRp63XuwLHEmUFUEi5kwsYtNNaJ63fyaLqOQ=,tag:oFOiQSlhPiw3I9Pe0lPaGw==,type:str]
pgp: pgp:
- created_at: "2022-09-14T13:42:28Z" - created_at: "2022-09-14T13:42:28Z"
enc: | enc: |
......
This diff is collapsed.
...@@ -320,18 +320,20 @@ frontend: ...@@ -320,18 +320,20 @@ frontend:
- "/login" - "/login"
- "/address" - "/address"
- "/stats" - "/stats"
- "/search-results"
- "/token" - "/token"
resources: resources:
limits: limits:
memory: memory:
_default: "0.3Gi" _default: "0.1Gi"
cpu: cpu:
_default: "0.2" _default: "0.1"
requests: requests:
memory: memory:
_default: "0.3Gi" _default: "0.1Gi"
cpu: cpu:
_default: "0.2" _default: "0.1"
# node label # node label
nodeSelector: nodeSelector:
enabled: true enabled: true
...@@ -399,4 +401,3 @@ frontend: ...@@ -399,4 +401,3 @@ frontend:
_default: https://rpc.ankr.com/eth_goerli _default: https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_NETWORK_EXPLORERS: NEXT_PUBLIC_NETWORK_EXPLORERS:
_default: "[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction','address':'/ethereum/ethereum/goerli/address'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address'}}]" _default: "[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction','address':'/ethereum/ethereum/goerli/address'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address'}}]"
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.038 13.692h6.675l-4.147 4.148.832.832 5.568-5.568-5.568-5.568-.832.832 4.147 4.148H8.038A3.832 3.832 0 0 1 4.21 8.687V1.329H3.033v7.36a5.01 5.01 0 0 0 5.005 5.004Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 201 100" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 201 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M100.027 8.408c-2.187 0-4.238.41-6.152 1.23a15.973 15.973 0 0 0-4.922 3.35c-1.413 1.413-2.53 3.076-3.35 4.99-.82 1.869-1.23 3.897-1.23 6.084a3.372 3.372 0 0 1-3.913 3.328l-2.409-.392a2.974 2.974 0 0 1-2.496-2.936c0-3.326.615-6.448 1.845-9.365 1.276-2.916 3.008-5.468 5.196-7.656 2.187-2.187 4.74-3.896 7.656-5.127C93.169.638 96.29 0 99.617 0h4.17c3.327 0 6.449.592 9.365 1.777 2.963 1.185 5.537 2.894 7.725 5.127 2.187 2.233 3.896 4.922 5.127 8.067 1.276 3.099 1.914 6.608 1.914 10.527 0 3.874-.592 7.77-1.777 11.69a67.002 67.002 0 0 1-4.58 11.552 87.122 87.122 0 0 1-6.495 11.006 147.24 147.24 0 0 1-7.451 10.049c-5.97 7.337-12.737 14.401-20.302 21.191h32.88a8.409 8.409 0 0 1 8.409 8.408H73.436v-2.253c0-2.988 1.345-5.81 3.612-7.756 7.075-6.073 13.482-12.33 19.22-18.77a162.094 162.094 0 0 0 8.408-10.322 100.413 100.413 0 0 0 7.314-11.211c2.142-3.874 3.851-7.793 5.127-11.758 1.322-4.01 1.983-7.952 1.983-11.826 0-2.871-.433-5.378-1.299-7.52-.866-2.141-2.028-3.919-3.487-5.332a13.909 13.909 0 0 0-5.058-3.144 16.03 16.03 0 0 0-5.879-1.094h-3.35ZM48.193 82.441v16.953a8.818 8.818 0 0 1-8.818-8.818v-8.135H3.3a3.3 3.3 0 0 1-2.872-4.926L42.701 2.857a2.937 2.937 0 0 1 5.492 1.447v69.73h8.409a8.408 8.408 0 0 1-8.409 8.407Zm-8.818-56.943L12.373 74.033h27.002V25.498Zm126.5-15.86c1.914-.82 3.965-1.23 6.152-1.23h3.35c2.051 0 4.01.365 5.879 1.094 1.914.683 3.6 1.732 5.058 3.144 1.459 1.413 2.621 3.19 3.487 5.332.866 2.142 1.299 4.649 1.299 7.52 0 3.874-.661 7.816-1.983 11.826-1.276 3.965-2.985 7.884-5.127 11.758a100.413 100.413 0 0 1-7.314 11.211 162.124 162.124 0 0 1-8.408 10.322c-5.738 6.44-12.145 12.697-19.22 18.77-2.267 1.946-3.612 4.768-3.612 7.756v2.253h55.166a8.409 8.409 0 0 0-8.409-8.408h-32.881c7.566-6.79 14.333-13.854 20.303-21.191a147.24 147.24 0 0 0 7.451-10.049 87.122 87.122 0 0 0 6.495-11.006 67.002 67.002 0 0 0 4.58-11.553c1.185-3.919 1.777-7.815 1.777-11.689 0-3.92-.638-7.428-1.914-10.527-1.231-3.145-2.94-5.834-5.127-8.067-2.188-2.233-4.762-3.942-7.725-5.127C182.236.592 179.114 0 175.787 0h-4.17c-3.327 0-6.448.638-9.365 1.914-2.917 1.23-5.469 2.94-7.656 5.127-2.188 2.188-3.92 4.74-5.196 7.656-1.23 2.917-1.845 6.039-1.845 9.366a2.974 2.974 0 0 0 2.496 2.935l2.409.392a3.371 3.371 0 0 0 3.913-3.328c0-2.187.41-4.215 1.231-6.084.82-1.914 1.936-3.577 3.349-4.99a15.982 15.982 0 0 1 4.922-3.35Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M100.027 8.408c-2.187 0-4.238.41-6.152 1.23a15.973 15.973 0 0 0-4.922 3.35c-1.413 1.413-2.53 3.076-3.35 4.99-.82 1.869-1.23 3.897-1.23 6.084a3.372 3.372 0 0 1-3.913 3.328l-2.409-.392a2.974 2.974 0 0 1-2.496-2.936c0-3.326.615-6.448 1.845-9.365 1.276-2.916 3.008-5.468 5.196-7.656 2.187-2.187 4.74-3.896 7.656-5.127C93.169.638 96.29 0 99.617 0h4.17c3.327 0 6.449.592 9.365 1.777 2.963 1.185 5.537 2.894 7.725 5.127 2.187 2.233 3.896 4.922 5.127 8.067 1.276 3.099 1.914 6.608 1.914 10.527 0 3.874-.592 7.77-1.777 11.69a67.002 67.002 0 0 1-4.58 11.552 87.122 87.122 0 0 1-6.495 11.006 147.24 147.24 0 0 1-7.451 10.049c-5.97 7.337-12.737 14.401-20.302 21.191h32.88a8.409 8.409 0 0 1 8.409 8.408H73.436v-2.253c0-2.988 1.345-5.81 3.612-7.756 7.075-6.073 13.482-12.33 19.22-18.77a162.094 162.094 0 0 0 8.408-10.322 100.413 100.413 0 0 0 7.314-11.211c2.142-3.874 3.851-7.793 5.127-11.758 1.322-4.01 1.983-7.952 1.983-11.826 0-2.871-.433-5.378-1.299-7.52-.866-2.141-2.028-3.919-3.487-5.332a13.909 13.909 0 0 0-5.058-3.144 16.03 16.03 0 0 0-5.879-1.094h-3.35ZM48.193 82.441v16.953a8.818 8.818 0 0 1-8.818-8.818v-8.135H3.3a3.3 3.3 0 0 1-2.872-4.926L42.701 2.857a2.937 2.937 0 0 1 5.492 1.447v69.73h8.409a8.408 8.408 0 0 1-8.409 8.407Zm-8.818-56.943L12.373 74.033h27.002V25.498Zm126.5-15.86c1.914-.82 3.965-1.23 6.152-1.23h3.35c2.051 0 4.01.365 5.879 1.094a13.91 13.91 0 0 1 5.058 3.144c1.459 1.413 2.621 3.19 3.487 5.332.866 2.142 1.299 4.649 1.299 7.52 0 3.874-.661 7.816-1.983 11.826-1.276 3.965-2.985 7.884-5.127 11.758a100.413 100.413 0 0 1-7.314 11.211 162.124 162.124 0 0 1-8.408 10.322c-5.738 6.44-12.145 12.697-19.22 18.77-2.267 1.946-3.612 4.768-3.612 7.756v2.253h55.166a8.409 8.409 0 0 0-8.409-8.408h-32.881c7.566-6.79 14.333-13.854 20.303-21.191a147.24 147.24 0 0 0 7.451-10.049 87.122 87.122 0 0 0 6.495-11.006 67.002 67.002 0 0 0 4.58-11.553c1.185-3.919 1.777-7.815 1.777-11.689 0-3.92-.638-7.428-1.914-10.527-1.231-3.145-2.94-5.834-5.127-8.067-2.188-2.233-4.762-3.942-7.725-5.127C182.236.592 179.114 0 175.787 0h-4.17c-3.327 0-6.448.638-9.365 1.914-2.917 1.23-5.469 2.94-7.656 5.127-2.188 2.188-3.92 4.74-5.196 7.656-1.23 2.917-1.845 6.039-1.845 9.366a2.974 2.974 0 0 0 2.496 2.935l2.409.392a3.371 3.371 0 0 0 3.913-3.328c0-2.187.41-4.215 1.231-6.084.82-1.914 1.936-3.577 3.349-4.99a15.982 15.982 0 0 1 4.922-3.35Z" fill="currentColor"/>
</svg> </svg>
...@@ -11,15 +11,18 @@ import type { ...@@ -11,15 +11,18 @@ import type {
AddressInternalTxsResponse, AddressInternalTxsResponse,
AddressTxsFilters, AddressTxsFilters,
AddressTokenTransferFilters, AddressTokenTransferFilters,
AddressTokensFilter,
AddressTokensResponse,
} from 'types/api/address'; } from 'types/api/address';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts'; import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { SmartContract } from 'types/api/contract'; import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod } from 'types/api/contract';
import type { IndexingStatus } from 'types/api/indexingStatus'; import type { IndexingStatus } from 'types/api/indexingStatus';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log'; import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace'; import type { RawTracesResponse } from 'types/api/rawTrace';
import type { Stats, Charts, HomeStats } from 'types/api/stats'; import type { SearchResult, SearchResultFilters } from 'types/api/search';
import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo'; import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer'; import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction'; import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
...@@ -67,8 +70,13 @@ export const RESOURCES = { ...@@ -67,8 +70,13 @@ export const RESOURCES = {
endpoint: appConfig.statsApi.endpoint, endpoint: appConfig.statsApi.endpoint,
basePath: appConfig.statsApi.basePath, basePath: appConfig.statsApi.basePath,
}, },
stats_charts: { stats_lines: {
path: '/api/v1/charts/line', path: '/api/v1/lines',
endpoint: appConfig.statsApi.endpoint,
basePath: appConfig.statsApi.basePath,
},
stats_line: {
path: '/api/v1/lines/:id',
endpoint: appConfig.statsApi.endpoint, endpoint: appConfig.statsApi.endpoint,
basePath: appConfig.statsApi.basePath, basePath: appConfig.statsApi.basePath,
}, },
...@@ -162,11 +170,31 @@ export const RESOURCES = { ...@@ -162,11 +170,31 @@ export const RESOURCES = {
paginationFields: [ 'items_count' as const, 'transaction_index' as const, 'index' as const, 'block_number' as const ], paginationFields: [ 'items_count' as const, 'transaction_index' as const, 'index' as const, 'block_number' as const ],
filterFields: [ ], filterFields: [ ],
}, },
address_tokens: {
path: '/api/v2/addresses/:id/tokens',
paginationFields: [ 'items_count' as const, 'token_name' as const, 'token_type' as const, 'value' as const ],
filterFields: [ 'type' as const ],
},
// CONTRACT // CONTRACT
contract: { contract: {
path: '/api/v2/smart-contracts/:id', path: '/api/v2/smart-contracts/:id',
}, },
contract_methods_read: {
path: '/api/v2/smart-contracts/:id/methods-read',
},
contract_methods_read_proxy: {
path: '/api/v2/smart-contracts/:id/methods-read-proxy',
},
contract_method_query: {
path: '/api/v2/smart-contracts/:id/query-read-method',
},
contract_methods_write: {
path: '/api/v2/smart-contracts/:id/methods-write',
},
contract_methods_write_proxy: {
path: '/api/v2/smart-contracts/:id/methods-write-proxy',
},
// TOKEN // TOKEN
token: { token: {
...@@ -201,6 +229,23 @@ export const RESOURCES = { ...@@ -201,6 +229,23 @@ export const RESOURCES = {
path: '/api/v2/main-page/indexing-status', path: '/api/v2/main-page/indexing-status',
}, },
// SEARCH
search: {
path: '/api/v2/search',
paginationFields: [
'address_hash' as const,
'block_hash' as const,
'holder_count' as const,
'inserted_at' as const,
'item_type' as const,
'items_count' as const,
'name' as const,
'q' as const,
'tx_hash' as const,
],
filterFields: [ 'q' ],
},
// DEPRECATED // DEPRECATED
old_api: { old_api: {
path: '/api', path: '/api',
...@@ -231,7 +276,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' | ...@@ -231,7 +276,8 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'txs_validated' | 'txs_pending' | 'txs_validated' | 'txs_pending' |
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' | 'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' | 'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'address_logs' | 'search' |
'address_logs' | 'address_tokens' |
'token_holders'; 'token_holders';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>; export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
...@@ -251,8 +297,9 @@ Q extends 'homepage_chart_market' ? ChartMarketResponse : ...@@ -251,8 +297,9 @@ Q extends 'homepage_chart_market' ? ChartMarketResponse :
Q extends 'homepage_blocks' ? Array<Block> : Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> : Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_indexing_status' ? IndexingStatus : Q extends 'homepage_indexing_status' ? IndexingStatus :
Q extends 'stats_counters' ? Stats : Q extends 'stats_counters' ? Counters :
Q extends 'stats_charts' ? Charts : Q extends 'stats_lines' ? StatsCharts :
Q extends 'stats_line' ? StatsChart :
Q extends 'blocks' ? BlocksResponse : Q extends 'blocks' ? BlocksResponse :
Q extends 'block' ? Block : Q extends 'block' ? Block :
Q extends 'block_txs' ? BlockTransactionsResponse : Q extends 'block_txs' ? BlockTransactionsResponse :
...@@ -273,10 +320,16 @@ Q extends 'address_blocks_validated' ? AddressBlocksValidatedResponse : ...@@ -273,10 +320,16 @@ Q extends 'address_blocks_validated' ? AddressBlocksValidatedResponse :
Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse : Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse :
Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart : Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
Q extends 'address_logs' ? LogsResponseAddress : Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'address_tokens' ? AddressTokensResponse :
Q extends 'token' ? TokenInfo : Q extends 'token' ? TokenInfo :
Q extends 'token_counters' ? TokenCounters : Q extends 'token_counters' ? TokenCounters :
Q extends 'token_holders' ? TokenHolders : Q extends 'token_holders' ? TokenHolders :
Q extends 'search' ? SearchResult :
Q extends 'contract' ? SmartContract : Q extends 'contract' ? SmartContract :
Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> :
Q extends 'contract_methods_read_proxy' ? Array<SmartContractReadMethod> :
Q extends 'contract_methods_write' ? Array<SmartContractWriteMethod> :
Q extends 'contract_methods_write_proxy' ? Array<SmartContractWriteMethod> :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -287,5 +340,7 @@ Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters : ...@@ -287,5 +340,7 @@ Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters :
Q extends 'tx_token_transfers' ? TokenTransferFilters : Q extends 'tx_token_transfers' ? TokenTransferFilters :
Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters : Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters :
Q extends 'address_token_transfers' ? AddressTokenTransferFilters : Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
Q extends 'address_tokens' ? AddressTokensFilter :
Q extends 'search' ? SearchResultFilters :
never; never;
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
...@@ -6,18 +6,18 @@ import useFetch from 'lib/hooks/useFetch'; ...@@ -6,18 +6,18 @@ import useFetch from 'lib/hooks/useFetch';
import buildUrl from './buildUrl'; import buildUrl from './buildUrl';
import { RESOURCES } from './resources'; import { RESOURCES } from './resources';
import type { ResourceError, ApiResource } from './resources'; import type { ApiResource } from './resources';
export interface Params { export interface Params {
pathParams?: Record<string, string | undefined>; pathParams?: Record<string, string | undefined>;
queryParams?: Record<string, string | number | undefined>; queryParams?: Record<string, string | number | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method'>; fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal'>;
} }
export default function useApiFetch() { export default function useApiFetch() {
const fetch = useFetch(); const fetch = useFetch();
return React.useCallback(<R extends keyof typeof RESOURCES, SuccessType = unknown, ErrorType = ResourceError>( return React.useCallback(<R extends keyof typeof RESOURCES, SuccessType = unknown, ErrorType = unknown>(
resourceName: R, resourceName: R,
{ pathParams, queryParams, fetchParams }: Params = {}, { pathParams, queryParams, fetchParams }: Params = {},
) => { ) => {
......
export default function escapeRegExp(string: string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
import escapeRegExp from 'lib/escapeRegExp';
export default function highlightText(text: string, query: string) {
const regex = new RegExp('(' + escapeRegExp(query) + ')', 'gi');
return text.replace(regex, '<mark>$1</mark>');
}
import React from 'react';
export default function useDebounce(value: string, delay: number) {
const [ debouncedValue, setDebouncedValue ] = React.useState(value);
React.useEffect(
() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
},
[ value, delay ],
);
return debouncedValue;
}
...@@ -10,6 +10,7 @@ import { getResourceKey } from 'lib/api/useApiQuery'; ...@@ -10,6 +10,7 @@ import { getResourceKey } from 'lib/api/useApiQuery';
export interface Params { export interface Params {
method?: RequestInit['method']; method?: RequestInit['method'];
headers?: RequestInit['headers']; headers?: RequestInit['headers'];
signal?: RequestInit['signal'];
body?: Record<string, unknown>; body?: Record<string, unknown>;
credentials?: RequestCredentials; credentials?: RequestCredentials;
} }
...@@ -19,7 +20,7 @@ export default function useFetch() { ...@@ -19,7 +20,7 @@ export default function useFetch() {
const { token } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {}; const { token } = queryClient.getQueryData<CsrfData>(getResourceKey('csrf')) || {};
return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => { return React.useCallback(<Success, Error>(path: string, params?: Params): Promise<Success | ResourceError<Error>> => {
const hasBody = params?.method && params?.body && ![ 'GET', 'HEAD' ].includes(params.method); const hasBody = params?.method && ![ 'GET', 'HEAD' ].includes(params.method);
const reqParams = { const reqParams = {
...params, ...params,
......
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import difference from 'lodash/difference';
import mapValues from 'lodash/mapValues'; import mapValues from 'lodash/mapValues';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import pick from 'lodash/pick'; import pick from 'lodash/pick';
...@@ -43,7 +44,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -43,7 +44,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const isMounted = React.useRef(false); const isMounted = React.useRef(false);
const canGoBackwards = React.useRef(!router.query.page); const canGoBackwards = React.useRef(!router.query.page);
const queryParams = { ...filters, ...pageParams[page] }; const queryParams = { ...pageParams[page], ...filters };
const scrollToTop = useCallback(() => { const scrollToTop = useCallback(() => {
scrollRef?.current ? scrollRef.current.scrollIntoView(true) : animateScroll.scrollToTop({ duration: 0 }); scrollRef?.current ? scrollRef.current.scrollIntoView(true) : animateScroll.scrollToTop({ duration: 0 });
...@@ -73,7 +74,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -73,7 +74,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
setPage(prev => prev + 1); setPage(prev => prev + 1);
const nextPageQuery = { ...router.query }; const nextPageQuery = { ...router.query };
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString()); Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = String(val));
nextPageQuery.page = String(page + 1); nextPageQuery.page = String(page + 1);
setHasPagination(true); setHasPagination(true);
...@@ -87,7 +88,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -87,7 +88,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
// we dont have pagination params for the first page // we dont have pagination params for the first page
let nextPageQuery: typeof router.query = { ...router.query }; let nextPageQuery: typeof router.query = { ...router.query };
if (page === 2) { if (page === 2) {
nextPageQuery = omit(router.query, resource.paginationFields, 'page'); nextPageQuery = omit(router.query, difference(resource.paginationFields, resource.filterFields), 'page');
canGoBackwards.current = true; canGoBackwards.current = true;
} else { } else {
const nextPageParams = pageParams[page - 1]; const nextPageParams = pageParams[page - 1];
...@@ -102,13 +103,14 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -102,13 +103,14 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] }); page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] });
}); });
setHasPagination(true); setHasPagination(true);
}, [ router, page, resource.paginationFields, pageParams, scrollToTop, queryClient, resourceName ]); }, [ router, page, resource.paginationFields, resource.filterFields, pageParams, scrollToTop, queryClient, resourceName ]);
const resetPage = useCallback(() => { const resetPage = useCallback(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] }); queryClient.removeQueries({ queryKey: [ resourceName ] });
scrollToTop(); scrollToTop();
router.push({ pathname: router.pathname, query: omit(router.query, resource.paginationFields, 'page') }, undefined, { shallow: true }).then(() => { const nextRouterQuery = omit(router.query, difference(resource.paginationFields, resource.filterFields), 'page');
router.push({ pathname: router.pathname, query: nextRouterQuery }, undefined, { shallow: true }).then(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] }); queryClient.removeQueries({ queryKey: [ resourceName ] });
setPage(1); setPage(1);
setPageParams({}); setPageParams({});
...@@ -121,10 +123,10 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -121,10 +123,10 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
}); });
setHasPagination(true); setHasPagination(true);
}, [ queryClient, resourceName, router, resource.paginationFields, scrollToTop ]); }, [ queryClient, resourceName, router, resource.paginationFields, resource.filterFields, scrollToTop ]);
const onFilterChange = useCallback((newFilters: PaginationFilters<Resource> | undefined) => { const onFilterChange = useCallback((newFilters: PaginationFilters<Resource> | undefined) => {
const newQuery = omit(router.query, resource.paginationFields, 'page', resource.filterFields); const newQuery = omit<typeof router.query>(router.query, resource.paginationFields, 'page', resource.filterFields);
if (newFilters) { if (newFilters) {
Object.entries(newFilters).forEach(([ key, value ]) => { Object.entries(newFilters).forEach(([ key, value ]) => {
if (value && value.length) { if (value && value.length) {
...@@ -132,7 +134,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -132,7 +134,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
} }
}); });
} }
setHasPagination(false);
scrollToTop(); scrollToTop();
router.push( router.push(
{ {
...@@ -147,14 +149,12 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -147,14 +149,12 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
}); });
}, [ router, resource.paginationFields, resource.filterFields, scrollToTop ]); }, [ router, resource.paginationFields, resource.filterFields, scrollToTop ]);
const hasPaginationParams = Object.keys(currPageParams || {}).length > 0;
const nextPageParams = data?.next_page_params; const nextPageParams = data?.next_page_params;
const pagination = { const pagination = {
page, page,
onNextPageClick, onNextPageClick,
onPrevPageClick, onPrevPageClick,
hasPaginationParams,
resetPage, resetPage,
hasNextPage: nextPageParams ? Object.keys(nextPageParams).length > 0 : false, hasNextPage: nextPageParams ? Object.keys(nextPageParams).length > 0 : false,
canGoBackwards: canGoBackwards.current, canGoBackwards: canGoBackwards.current,
......
import React from 'react';
// run effect only if value is updated since initial mount
const useUpdateValueEffect = (effect: () => void, value: string) => {
const mountedRef = React.useRef(false);
const valueRef = React.useRef<string>();
const isChangedRef = React.useRef(false);
React.useEffect(() => {
mountedRef.current = true;
valueRef.current = value;
return () => {
mountedRef.current = false;
valueRef.current = undefined;
isChangedRef.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
if (mountedRef.current && (value !== valueRef.current || isChangedRef.current)) {
isChangedRef.current = true;
return effect();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ value ]);
};
export default useUpdateValueEffect;
import type { SearchResultToken, SearchResultBlock, SearchResultAddressOrContract, SearchResultTx, SearchResult } from 'types/api/search';
export const token1: SearchResultToken = {
address: '0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
address_url: '/address/0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
name: 'Toms NFT',
symbol: 'TNT',
token_url: '/token/0x377c5F2B300B25a534d4639177873b7fEAA56d4B',
type: 'token' as const,
};
export const token2: SearchResultToken = {
address: '0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
address_url: '/address/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
name: 'TomToken',
symbol: 'pdE1B',
token_url: '/token/0xC35Cc7223B0175245E9964f2E3119c261E8e21F9',
type: 'token' as const,
};
export const block1: SearchResultBlock = {
block_hash: '0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1',
block_number: 8198536,
type: 'block' as const,
url: '/block/0x1af31d7535dded06bab9a88eb40ee2f8d0529a60ab3b8a7be2ba69b008cacbd1',
};
export const address1: SearchResultAddressOrContract = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: null,
type: 'address' as const,
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
};
export const contract1: SearchResultAddressOrContract = {
address: '0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
name: 'Unknown contract in this network',
type: 'contract' as const,
url: '/address/0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a',
};
export const tx1: SearchResultTx = {
tx_hash: '0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd',
type: 'transaction' as const,
url: '/tx/0x349d4025d03c6faec117ee10ac0bce7c7a805dd2cbff7a9f101304d9a8a525dd',
};
export const baseResponse: SearchResult = {
items: [
token1,
token2,
block1,
address1,
contract1,
tx1,
],
next_page_params: null,
};
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
"d3": "^7.6.1", "d3": "^7.6.1",
"dayjs": "^1.11.5", "dayjs": "^1.11.5",
"dom-to-image": "^2.6.0", "dom-to-image": "^2.6.0",
"ethers": "^5.7.1", "ethers": "^5.7.2",
"framer-motion": "^6.5.1", "framer-motion": "^6.5.1",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"lodash": "^4.0.0", "lodash": "^4.0.0",
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import SearchResults from 'ui/pages/SearchResults';
const SearchResultsPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<SearchResults/>
</>
);
};
export default SearchResultsPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
...@@ -16,8 +16,8 @@ const size = { ...@@ -16,8 +16,8 @@ const size = {
xs: defineStyle({ xs: defineStyle({
fontSize: 'md', fontSize: 'md',
lineHeight: '24px', lineHeight: '24px',
px: '4px', px: '8px',
py: '12px', py: '4px',
h: '32px', h: '32px',
borderRadius: 'base', borderRadius: 'base',
}), }),
......
...@@ -14,7 +14,7 @@ const $arrowBg = cssVar('popper-arrow-bg'); ...@@ -14,7 +14,7 @@ const $arrowBg = cssVar('popper-arrow-bg');
const $arrowShadowColor = cssVar('popper-arrow-shadow-color'); const $arrowShadowColor = cssVar('popper-arrow-shadow-color');
const baseStylePopper = defineStyle({ const baseStylePopper = defineStyle({
zIndex: 20, zIndex: 'popover',
}); });
const baseStyleContent = defineStyle((props) => { const baseStyleContent = defineStyle((props) => {
......
...@@ -9,6 +9,10 @@ const global = (props: StyleFunctionProps) => ({ ...@@ -9,6 +9,10 @@ const global = (props: StyleFunctionProps) => ({
...getDefaultTransitionProps(), ...getDefaultTransitionProps(),
'-webkit-tap-highlight-color': 'transparent', '-webkit-tap-highlight-color': 'transparent',
}, },
mark: {
bgColor: 'yellow.200',
color: 'inherit',
},
'svg *::selection': { 'svg *::selection': {
color: 'none', color: 'none',
background: 'none', background: 'none',
......
...@@ -48,6 +48,16 @@ export interface AddressTokenBalance { ...@@ -48,6 +48,16 @@ export interface AddressTokenBalance {
value: string; value: string;
} }
export interface AddressTokensResponse {
items: Array<AddressTokenBalance>;
next_page_params: {
items_count: number;
token_name: 'string' | null;
token_type: TokenType;
value: number;
} | null;
}
export interface AddressTransactionsResponse { export interface AddressTransactionsResponse {
items: Array<Transaction>; items: Array<Transaction>;
next_page_params: { next_page_params: {
...@@ -75,6 +85,10 @@ export type AddressTokenTransferFilters = { ...@@ -75,6 +85,10 @@ export type AddressTokenTransferFilters = {
type: Array<TokenType>; type: Array<TokenType>;
} }
export type AddressTokensFilter = {
type: TokenType;
}
export interface AddressCoinBalanceHistoryItem { export interface AddressCoinBalanceHistoryItem {
block_number: number; block_number: number;
block_timestamp: string; block_timestamp: string;
......
...@@ -13,3 +13,37 @@ export interface SmartContract { ...@@ -13,3 +13,37 @@ export interface SmartContract {
source_code: string | null; source_code: string | null;
can_be_visualized_via_sol2uml: boolean | null; can_be_visualized_via_sol2uml: boolean | null;
} }
export interface SmartContractMethodBase {
inputs: Array<SmartContractMethodInput>;
outputs: Array<SmartContractMethodOutput>;
constant: boolean;
name: string;
stateMutability: string;
type: 'function';
payable: boolean;
}
export interface SmartContractReadMethod extends SmartContractMethodBase {
method_id: string;
}
export interface SmartContractWriteFallback {
payable: true;
stateMutability: 'payable';
type: 'fallback';
}
export type SmartContractWriteMethod = SmartContractMethodBase | SmartContractWriteFallback;
export type SmartContractMethod = SmartContractReadMethod | SmartContractWriteMethod;
export interface SmartContractMethodInput {
internalType: string;
name: string;
type: string;
}
export interface SmartContractMethodOutput extends SmartContractMethodInput {
value?: string;
}
export type SearchResultType = 'token' | 'address' | 'block' | 'transaction' | 'contract';
export interface SearchResultToken {
type: 'token';
name: string;
symbol: string;
address: string;
token_url: string;
address_url: string;
}
export interface SearchResultAddressOrContract {
type: 'address' | 'contract';
name: string | null;
address: string;
url: string;
}
export interface SearchResultBlock {
type: 'block';
block_number: number;
block_hash: string;
url: string;
}
export interface SearchResultTx {
type: 'transaction';
tx_hash: string;
url: string;
}
export type SearchResultItem = SearchResultToken | SearchResultAddressOrContract | SearchResultBlock | SearchResultTx;
export interface SearchResult {
items: Array<SearchResultItem>;
next_page_params: {
'address_hash': string | null;
'block_hash': string | null;
'holder_count': number | null;
'inserted_at': string | null;
'item_type': SearchResultType;
'items_count': number;
'name': string;
'q': string;
'tx_hash': string | null;
} | null;
}
export interface SearchResultFilters {
q: string;
}
...@@ -19,28 +19,36 @@ export type GasPrices = { ...@@ -19,28 +19,36 @@ export type GasPrices = {
slow: number; slow: number;
} }
export type Stats = { export type Counters = {
counters: { counters: Array<Counter>;
totalBlocks: string; }
averageBlockTime: string;
totalTransactions: string;
completedTransactions: string;
totalAccounts: string; type Counter = {
id: string;
value: string;
title: string;
units: string;
}
totalTokens: string; export type StatsCharts = {
sections: Array<StatsChartsSection>;
}
totalNativeCoinHolders: string; export type StatsChartsSection = {
totalNativeCoinTransfers: string; id: string;
}; title: string;
charts: Array<StatsChartInfo>;
} }
export type Charts = { export type StatsChartInfo = {
chart: Array<ChartsItem>; id: string;
title: string;
description: string;
} }
export type ChartsItem ={ export type StatsChart = { chart: Array<StatsChartItem> };
export type StatsChartItem = {
date: string; date: string;
value: string; value: string;
} }
export type StatsSection = { id: StatsSectionIds; title: string; charts: Array<StatsChart> }
export type StatsSectionIds = keyof typeof StatsSectionId;
export enum StatsSectionId {
'all',
'accounts',
'blocks',
'tokens',
'transactions',
'gas',
}
export type StatsInterval = { id: StatsIntervalIds; title: string } export type StatsInterval = { id: StatsIntervalIds; title: string }
export type StatsIntervalIds = keyof typeof StatsIntervalId; export type StatsIntervalIds = keyof typeof StatsIntervalId;
export enum StatsIntervalId { export enum StatsIntervalId {
...@@ -18,9 +7,3 @@ export enum StatsIntervalId { ...@@ -18,9 +7,3 @@ export enum StatsIntervalId {
'sixMonths', 'sixMonths',
'oneYear', 'oneYear',
} }
export type StatsChart = {
apiId: string;
title: string;
description: string;
}
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { withName } from 'mocks/address/address';
import * as tokenBalanceMock from 'mocks/address/tokenBalance';
import { baseList } from 'mocks/address/tokenBalance';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTokens from './AddressTokens';
const ADDRESS_HASH = withName.hash;
const API_URL_ADDRESS = buildApiUrl('address', { id: ADDRESS_HASH });
const API_URL_ADDRESS_TOKEN_BALANCES = buildApiUrl('address_token_balances', { id: ADDRESS_HASH });
const API_URL_TOKENS = buildApiUrl('address_tokens', { id: ADDRESS_HASH });
const nextPageParams = {
items_count: 50,
token_name: 'aaa',
token_type: '123',
value: 1,
};
test('erc20 +@mobile +@dark-mode', async({ mount, page }) => {
const hooksConfig = {
router: {
query: { id: ADDRESS_HASH, tab: 'tokens_erc20' },
isReady: true,
},
};
const response20 = {
items: [ tokenBalanceMock.erc20a, tokenBalanceMock.erc20b, tokenBalanceMock.erc20c, tokenBalanceMock.erc20d ],
next_page_params: nextPageParams,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(withName),
}));
await page.route(API_URL_ADDRESS_TOKEN_BALANCES, (route) => route.fulfill({
status: 200,
body: JSON.stringify(baseList),
}));
await page.route(API_URL_TOKENS + '?type=ERC-20', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response20),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokens/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('erc721 +@mobile +@dark-mode', async({ mount, page }) => {
const hooksConfig = {
router: {
query: { id: ADDRESS_HASH, tab: 'tokens_erc721' },
isReady: true,
},
};
const response20 = {
items: [ tokenBalanceMock.erc721a, tokenBalanceMock.erc721b, tokenBalanceMock.erc721c ],
next_page_params: nextPageParams,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(withName),
}));
await page.route(API_URL_ADDRESS_TOKEN_BALANCES, (route) => route.fulfill({
status: 200,
body: JSON.stringify(baseList),
}));
await page.route(API_URL_TOKENS + '?type=ERC-721', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response20),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokens/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('erc1155 +@mobile +@dark-mode', async({ mount, page }) => {
const hooksConfig = {
router: {
query: { id: ADDRESS_HASH, tab: 'tokens_erc1155' },
isReady: true,
},
};
const response20 = {
items: [ tokenBalanceMock.erc1155a, tokenBalanceMock.erc1155b ],
next_page_params: nextPageParams,
};
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(withName),
}));
await page.route(API_URL_ADDRESS_TOKEN_BALANCES, (route) => route.fulfill({
status: 200,
body: JSON.stringify(baseList),
}));
await page.route(API_URL_TOKENS + '?type=ERC-1155', (route) => route.fulfill({
status: 200,
body: JSON.stringify(response20),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokens/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
import { Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { tokenTabsByType } from 'ui/pages/Address';
import Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TokenBalances from './tokens/TokenBalances';
import TokensWithIds from './tokens/TokensWithIds';
import TokensWithoutIds from './tokens/TokensWithoutIds';
const TAB_LIST_PROPS = {
marginBottom: 0,
py: 5,
marginTop: 3,
columnGap: 3,
};
const TAB_LIST_PROPS_MOBILE = {
mt: 8,
columnGap: 3,
};
const AddressTokens = () => {
const router = useRouter();
const isMobile = useIsMobile();
const scrollRef = React.useRef<HTMLDivElement>(null);
const tokenType: TokenType = (Object.keys(tokenTabsByType) as Array<TokenType>).find(key => tokenTabsByType[key] === router.query.tab) || 'ERC-20';
const tokensQuery = useQueryWithPages({
resourceName: 'address_tokens',
pathParams: { id: router.query.id?.toString() },
filters: { type: tokenType },
scrollRef,
});
const tabs = [
{ id: tokenTabsByType['ERC-20'], title: 'ERC-20', component: <TokensWithoutIds tokensQuery={ tokensQuery }/> },
{ id: tokenTabsByType['ERC-721'], title: 'ERC-721', component: <TokensWithoutIds tokensQuery={ tokensQuery }/> },
{ id: tokenTabsByType['ERC-1155'], title: 'ERC-1155', component: <TokensWithIds tokensQuery={ tokensQuery }/> },
];
return (
<>
<TokenBalances/>
{ /* should stay before tabs to scroll up whith pagination */ }
<Box ref={ scrollRef }></Box>
<RoutedTabs
tabs={ tabs }
variant="outline"
colorScheme="gray"
size="sm"
tabListProps={ isMobile ? TAB_LIST_PROPS_MOBILE : TAB_LIST_PROPS }
rightSlot={ tokensQuery.isPaginationVisible && !isMobile ? <Pagination { ...tokensQuery.pagination }/> : null }
stickyEnabled={ !isMobile }
/>
</>
);
};
export default AddressTokens;
...@@ -4,7 +4,6 @@ import React from 'react'; ...@@ -4,7 +4,6 @@ import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import ChartWidget from 'ui/shared/chart/ChartWidget'; import ChartWidget from 'ui/shared/chart/ChartWidget';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
interface Props { interface Props {
addressHash: string; addressHash: string;
...@@ -20,16 +19,9 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => { ...@@ -20,16 +19,9 @@ const AddressCoinBalanceChart = ({ addressHash }: Props) => {
value: BigNumber(value).div(10 ** appConfig.network.currency.decimals).toNumber(), value: BigNumber(value).div(10 ** appConfig.network.currency.decimals).toNumber(),
})), [ data ]); })), [ data ]);
if (isError) {
return <DataFetchAlert/>;
}
if (!items?.length) {
return null;
}
return ( return (
<ChartWidget <ChartWidget
isError={ isError }
title="Balances" title="Balances"
items={ items } items={ items }
isLoading={ isLoading } isLoading={ isLoading }
......
import { Box, Button, chakra, Flex, Icon, Text, useColorModeValue } from '@chakra-ui/react';
import _fromPairs from 'lodash/fromPairs';
import React from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import type { MethodFormFields } from './types';
import type { SmartContractMethodInput, SmartContractMethod } from 'types/api/contract';
import appConfig from 'configs/app/config';
import arrowIcon from 'icons/arrows/down-right.svg';
import ContractMethodField from './ContractMethodField';
interface Props<T extends SmartContractMethod> {
data: T;
caller: (data: T, args: Array<string>) => Promise<Array<Array<string>>>;
isWrite?: boolean;
}
const getFieldName = (name: string, index: number): string => name || String(index);
const sortFields = (data: Array<SmartContractMethodInput>) => ([ a ]: [string, string], [ b ]: [string, string]): 1 | -1 | 0 => {
const fieldNames = data.map(({ name }, index) => getFieldName(name, index));
const indexA = fieldNames.indexOf(a);
const indexB = fieldNames.indexOf(b);
if (indexA > indexB) {
return 1;
}
if (indexA < indexB) {
return -1;
}
return 0;
};
const ContractMethodCallable = <T extends SmartContractMethod>({ data, caller, isWrite }: Props<T>) => {
const inputs = React.useMemo(() => {
return data.payable && (!('inputs' in data) || data.inputs.length === 0) ? [ {
name: 'value',
type: appConfig.network.currency.symbol,
internalType: appConfig.network.currency.symbol,
} as SmartContractMethodInput ] : data.inputs;
}, [ data ]);
const { control, handleSubmit, setValue } = useForm<MethodFormFields>({
defaultValues: _fromPairs(inputs.map(({ name }, index) => [ getFieldName(name, index), '' ])),
});
const [ result, setResult ] = React.useState<Array<Array<string>>>([ ]);
const onSubmit: SubmitHandler<MethodFormFields> = React.useCallback(async(formData) => {
const args = Object.entries(formData)
.sort(sortFields(inputs))
.map(([ , value ]) => value);
const result = await caller(data, args);
setResult(result);
}, [ caller, data, inputs ]);
const resultBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
return (
<Box>
<chakra.form
noValidate
display="flex"
columnGap={ 3 }
flexDir={{ base: 'column', lg: 'row' }}
rowGap={ 2 }
alignItems={{ base: 'flex-start', lg: 'center' }}
onSubmit={ handleSubmit(onSubmit) }
flexWrap="wrap"
>
{ inputs.map(({ type, name }, index) => {
const fieldName = getFieldName(name, index);
return (
<ContractMethodField
key={ fieldName }
name={ fieldName }
placeholder={ `${ name }(${ type })` }
control={ control }
setValue={ setValue }
/>
);
}) }
<Button
variant="outline"
size="sm"
flexShrink={ 0 }
type="submit"
>
{ isWrite ? 'Write' : 'Query' }
</Button>
</chakra.form>
{ 'outputs' in data && data.outputs.length > 0 && (
<Flex mt={ 3 }>
<Icon as={ arrowIcon } boxSize={ 5 } mr={ 1 }/>
<Text>{ data.outputs.map(({ type }) => type).join(', ') }</Text>
</Flex>
) }
{ result.length > 0 && (
<Box mt={ 3 } p={ 4 } borderRadius="md" bgColor={ resultBgColor } fontSize="sm">
<p>
[ <chakra.span fontWeight={ 600 }>{ 'name' in data ? data.name : '' }</chakra.span> method response ]
</p>
<p>[</p>
{ result.map(([ key, value ], index) => (
<chakra.p key={ index } whiteSpace="break-spaces" wordBreak="break-all"> { key }: { value }</chakra.p>
)) }
<p>]</p>
</Box>
) }
</Box>
);
};
export default React.memo(ContractMethodCallable) as typeof ContractMethodCallable;
import { Checkbox, Flex, chakra } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import type { ChangeEvent } from 'react';
import React from 'react';
import type { SmartContractMethodOutput } from 'types/api/contract';
import appConfig from 'configs/app/config';
import { WEI } from 'lib/consts';
interface Props {
data: SmartContractMethodOutput;
}
const ContractMethodStatic = ({ data }: Props) => {
const isBigInt = data.type.includes('int256') || data.type.includes('int128');
const [ value, setValue ] = React.useState(isBigInt && data.value ? BigNumber(data.value).toFixed() : data.value);
const [ label, setLabel ] = React.useState('WEI');
const handleCheckboxChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
if (!data.value) {
return;
}
if (event.target.checked) {
setValue(BigNumber(data.value).div(WEI).toFixed());
setLabel(appConfig.network.currency.symbol || 'ETH');
} else {
setValue(BigNumber(data.value).toFixed());
setLabel('WEI');
}
}, [ data.value ]);
return (
<Flex flexDir={{ base: 'column', lg: 'row' }} columnGap={ 2 } rowGap={ 2 }>
<chakra.span wordBreak="break-all">({ data.type }): { value }</chakra.span>
{ isBigInt && <Checkbox onChange={ handleCheckboxChange }>{ label }</Checkbox> }
</Flex>
);
};
export default ContractMethodStatic;
import { FormControl, Input, InputGroup, InputRightElement } from '@chakra-ui/react';
import React from 'react';
import type { Control, ControllerRenderProps, UseFormSetValue } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import type { MethodFormFields } from './types';
import InputClearButton from 'ui/shared/InputClearButton';
interface Props {
control: Control<MethodFormFields>;
setValue: UseFormSetValue<MethodFormFields>;
placeholder: string;
name: string;
}
const ContractMethodField = ({ control, name, placeholder, setValue }: Props) => {
const ref = React.useRef<HTMLInputElement>(null);
const handleClear = React.useCallback(() => {
setValue(name, '');
ref.current?.focus();
}, [ name, setValue ]);
const renderInput = React.useCallback(({ field }: { field: ControllerRenderProps<MethodFormFields> }) => {
return (
<FormControl id={ name } maxW={{ base: '100%', lg: 'calc((100% - 24px) / 3)' }}>
<InputGroup size="xs">
<Input
{ ...field }
ref={ ref }
placeholder={ placeholder }
/>
{ field.value && (
<InputRightElement>
<InputClearButton onClick={ handleClear }/>
</InputRightElement>
) }
</InputGroup>
</FormControl>
);
}, [ handleClear, name, placeholder ]);
return (
<Controller
name={ name }
control={ control }
render={ renderInput }
/>
);
};
export default React.memo(ContractMethodField);
import { Accordion, AccordionButton, AccordionIcon, AccordionItem, AccordionPanel, Box, Flex, Icon, Link, Tooltip } from '@chakra-ui/react';
import _range from 'lodash/range';
import React from 'react';
import type { SmartContractMethod } from 'types/api/contract';
import infoIcon from 'icons/info.svg';
interface Props<T extends SmartContractMethod> {
data: Array<T>;
renderContent: (item: T, index: number, id: number) => React.ReactNode;
}
const ContractMethodsAccordion = <T extends SmartContractMethod>({ data, renderContent }: Props<T>) => {
const [ expandedSections, setExpandedSections ] = React.useState<Array<number>>([]);
const [ id, setId ] = React.useState(0);
const handleAccordionStateChange = React.useCallback((newValue: Array<number>) => {
setExpandedSections(newValue);
}, []);
const handleExpandAll = React.useCallback(() => {
if (!data) {
return;
}
if (expandedSections.length < data.length) {
setExpandedSections(_range(0, data.length));
} else {
setExpandedSections([]);
}
}, [ data, expandedSections.length ]);
const handleReset = React.useCallback(() => {
setId((id) => id + 1);
}, []);
return (
<Accordion allowMultiple position="relative" onChange={ handleAccordionStateChange } index={ expandedSections }>
{ data.map((item, index) => {
return (
<AccordionItem key={ index } as="section">
<h2>
<AccordionButton px={ 0 } py={ 3 } _hover={{ bgColor: 'inherit' }}>
<Box as="span" fontFamily="heading" fontWeight={ 500 } fontSize="lg" mr={ 1 }>
{ index + 1 }. { item.type === 'fallback' ? 'fallback' : item.name }
</Box>
{ item.type === 'fallback' && (
<Tooltip
label={ `The fallback function is executed on a call to the contract if none of the other functions match
the given function signature, or if no data was supplied at all and there is no receive Ether function.
The fallback function always receives data, but in order to also receive Ether it must be marked payable.` }
placement="top"
maxW="320px"
>
<Box cursor="pointer" display="inherit">
<Icon as={ infoIcon } boxSize={ 5 }/>
</Box>
</Tooltip>
) }
<AccordionIcon/>
</AccordionButton>
</h2>
<AccordionPanel pb={ 4 } px={ 0 }>
{ renderContent(item, index, id) }
</AccordionPanel>
</AccordionItem>
);
}) }
<Flex columnGap={ 3 } position="absolute" top={ 0 } right={ 0 } py={ 3 } lineHeight="27px">
<Link onClick={ handleExpandAll }>{ expandedSections.length === data.length ? 'Collapse' : 'Expand' } all</Link>
<Link onClick={ handleReset }>Reset</Link>
</Flex>
</Accordion>
);
};
export default React.memo(ContractMethodsAccordion) as typeof ContractMethodsAccordion;
import { Flex } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { SmartContractReadMethod } from 'types/api/contract';
import useApiFetch from 'lib/api/useApiFetch';
import useApiQuery from 'lib/api/useApiQuery';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ContractMethodCallable from './ContractMethodCallable';
import ContractMethodConstant from './ContractMethodConstant';
interface Props {
isProxy?: boolean;
}
const ContractRead = ({ isProxy }: Props) => {
const router = useRouter();
const apiFetch = useApiFetch();
const addressHash = router.query.id?.toString();
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_read_proxy' : 'contract_methods_read', {
pathParams: { id: addressHash },
queryOptions: {
enabled: Boolean(router.query.id),
},
});
const contractCaller = React.useCallback(async(item: SmartContractReadMethod, args: Array<string>) => {
await apiFetch('contract_method_query', {
pathParams: { id: addressHash },
fetchParams: {
method: 'POST',
body: {
args,
method_id: item.method_id,
contract_type: isProxy ? 'proxy' : 'regular',
},
},
});
return [ [ 'string', 'this is mock' ] ];
}, [ addressHash, apiFetch, isProxy ]);
const renderContent = React.useCallback((item: SmartContractReadMethod, index: number, id: number) => {
if (item.inputs.length === 0) {
return (
<Flex flexDir="column" rowGap={ 1 }>
{ item.outputs.map((output, index) => <ContractMethodConstant key={ index } data={ output }/>) }
</Flex>
);
}
return (
<ContractMethodCallable
key={ id + '_' + index }
data={ item }
caller={ contractCaller }
/>
);
}, [ contractCaller ]);
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return <ContentLoader/>;
}
return <ContractMethodsAccordion data={ data } renderContent={ renderContent }/>;
};
export default ContractRead;
import { useRouter } from 'next/router';
import React from 'react';
import type { SmartContractWriteMethod } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery';
import ContractMethodsAccordion from 'ui/address/contract/ContractMethodsAccordion';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ContractMethodCallable from './ContractMethodCallable';
interface Props {
isProxy?: boolean;
}
const ContractWrite = ({ isProxy }: Props) => {
const router = useRouter();
const addressHash = router.query.id?.toString();
const { data, isLoading, isError } = useApiQuery(isProxy ? 'contract_methods_write_proxy' : 'contract_methods_write', {
pathParams: { id: addressHash },
queryOptions: {
enabled: Boolean(router.query.id),
},
});
const contractInfo = useApiQuery('contract', {
pathParams: { id: addressHash },
queryOptions: {
enabled: Boolean(router.query.id),
},
});
const contractCaller = React.useCallback(async() => {
// eslint-disable-next-line no-console
console.log('__>__', contractInfo);
return [ [ 'string', 'this is mock' ] ];
}, [ contractInfo ]);
const renderContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => {
return (
<ContractMethodCallable
key={ id + '_' + index }
data={ item }
caller={ contractCaller }
isWrite
/>
);
}, [ contractCaller ]);
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return <ContentLoader/>;
}
return <ContractMethodsAccordion data={ data } renderContent={ renderContent }/>;
};
export default ContractWrite;
export type MethodFormFields = Record<string, string>;
import { Box, Button, Icon, Skeleton, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, Button, Icon, Skeleton, Text, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import arrowIcon from 'icons/arrows/east-mini.svg'; import arrowIcon from 'icons/arrows/east-mini.svg';
import tokensIcon from 'icons/tokens.svg'; import tokensIcon from 'icons/tokens.svg';
import { ZERO } from 'lib/consts';
import type { EnhancedData } from './utils'; import type { EnhancedData } from '../utils/tokenUtils';
import { getTokenBalanceTotal } from '../utils/tokenUtils';
interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
...@@ -16,7 +15,7 @@ interface Props { ...@@ -16,7 +15,7 @@ interface Props {
} }
const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => { const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const totalBn = data.reduce((result, item) => !item.usd ? result : result.plus(BigNumber(item.usd)), ZERO); const totalBn = getTokenBalanceTotal(data);
const skeletonBgColor = useColorModeValue('white', 'black'); const skeletonBgColor = useColorModeValue('white', 'black');
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
......
...@@ -6,7 +6,7 @@ import link from 'lib/link/link'; ...@@ -6,7 +6,7 @@ import link from 'lib/link/link';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import TokenLogo from 'ui/shared/TokenLogo'; import TokenLogo from 'ui/shared/TokenLogo';
import type { EnhancedData } from './utils'; import type { EnhancedData } from '../utils/tokenUtils';
interface Props { interface Props {
data: EnhancedData; data: EnhancedData;
......
...@@ -8,9 +8,9 @@ import type { TokenType } from 'types/api/tokenInfo'; ...@@ -8,9 +8,9 @@ import type { TokenType } from 'types/api/tokenInfo';
import arrowIcon from 'icons/arrows/east.svg'; import arrowIcon from 'icons/arrows/east.svg';
import searchIcon from 'icons/search.svg'; import searchIcon from 'icons/search.svg';
import type { Sort, EnhancedData } from '../utils/tokenUtils';
import { sortTokenGroups, sortingFns } from '../utils/tokenUtils';
import TokenSelectItem from './TokenSelectItem'; import TokenSelectItem from './TokenSelectItem';
import type { Sort, EnhancedData } from './utils';
import { sortTokenGroups, sortingFns } from './utils';
interface Props { interface Props {
searchTerm: string; searchTerm: string;
......
...@@ -4,8 +4,8 @@ import React from 'react'; ...@@ -4,8 +4,8 @@ import React from 'react';
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
import type { Sort } from './utils'; import type { Sort } from '../utils/tokenUtils';
import { calculateUsdValue, filterTokens } from './utils'; import { calculateUsdValue, filterTokens } from '../utils/tokenUtils';
export default function useData(data: Array<AddressTokenBalance>) { export default function useData(data: Array<AddressTokenBalance>) {
const [ searchTerm, setSearchTerm ] = React.useState(''); const [ searchTerm, setSearchTerm ] = React.useState('');
......
import { Center, Flex, Icon, Link, Text, LinkBox, LinkOverlay, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import NFTIcon from 'icons/nft_shield.svg';
import link from 'lib/link/link';
import TokenLogo from 'ui/shared/TokenLogo';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
type Props = AddressTokenBalance;
const NFTItem = ({ token, token_id: tokenId }: Props) => {
const tokenLink = link('token_index', { hash: token.address });
return (
<LinkBox
w={{ base: 'calc((100% - 12px)/2)', lg: '210px' }}
h={{ base: 'auto', lg: '272px' }}
border="1px solid"
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
borderRadius="12px"
p="10px"
_hover={{ boxShadow: 'md' }}
fontSize="sm"
fontWeight={ 500 }
lineHeight="20px"
>
<LinkOverlay href={ tokenLink }/>
<Center
w={{ base: '100%', lg: '182px' }}
h={{ base: 'calc((100vw - 36px)/2 - 12px)', lg: '182px' }}
bg={ useColorModeValue('blackAlpha.50', 'whiteAlpha.50') }
mb="18px"
borderRadius="12px"
>
<Icon as={ NFTIcon } boxSize="112px" color={ useColorModeValue('blackAlpha.500', 'whiteAlpha.500') }/>
</Center>
{ tokenId && (
<Flex mb={ 2 } ml={ 1 }>
<Text whiteSpace="pre" variant="secondary">ID# </Text>
<TruncatedTextTooltip label={ tokenId }>
<Link
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
href={ link('token_instance_item', { hash: token.address, id: tokenId }) }
>
{ tokenId }
</Link>
</TruncatedTextTooltip>
</Flex>
) }
{ token.name && (
<Flex alignItems="center">
<TokenLogo hash={ token.address } name={ token.name } boxSize={ 6 } ml={ 1 } mr={ 1 }/>
<TruncatedTextTooltip label={ token.name }>
<Text variant="secondary" overflow="hidden" whiteSpace="nowrap" textOverflow="ellipsis">{ token.name }</Text>
</TruncatedTextTooltip>
</Flex>
) }
</LinkBox>
);
};
export default NFTItem;
import { Flex, Skeleton } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router';
import React from 'react';
import appConfig from 'configs/app/config';
import useApiQuery from 'lib/api/useApiQuery';
import getCurrencyValue from 'lib/getCurrencyValue';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import { getTokenBalanceTotal, calculateUsdValue } from '../utils/tokenUtils';
import TokenBalancesItem from './TokenBalancesItem';
const TokenBalances = () => {
const router = useRouter();
const addressQuery = useApiQuery('address', {
pathParams: { id: router.query.id?.toString() },
queryOptions: { enabled: Boolean(router.query.id) },
});
const balancesQuery = useApiQuery('address_token_balances', {
pathParams: { id: addressQuery.data?.hash },
queryOptions: { enabled: Boolean(addressQuery.data) },
});
if (addressQuery.isError || balancesQuery.isError) {
return <DataFetchAlert/>;
}
if (addressQuery.isLoading || balancesQuery.isLoading) {
const item = <Skeleton w={{ base: '100%', lg: '240px' }} h="82px" borderRadius="16px"/>;
return (
<Flex columnGap={ 3 } rowGap={ 3 } mt={{ base: '6px', lg: 0 }} flexDirection={{ base: 'column', lg: 'row' }}>
{ item }
{ item }
{ item }
</Flex>
);
}
const addressData = addressQuery.data;
const { valueStr: nativeValue, usd: nativeUsd } = getCurrencyValue({
value: addressData.coin_balance || '0',
accuracy: 8,
accuracyUsd: 2,
exchangeRate: addressData.exchange_rate,
decimals: String(appConfig.network.currency.decimals),
});
const tokenBalanceBn = getTokenBalanceTotal(balancesQuery.data.map(calculateUsdValue)).toFixed(2);
const totalUsd = nativeUsd ? BigNumber(nativeUsd).toNumber() + BigNumber(tokenBalanceBn).toNumber() : undefined;
return (
<Flex columnGap={ 3 } rowGap={ 3 } mt={{ base: '6px', lg: 0 }} flexDirection={{ base: 'column', lg: 'row' }}>
<TokenBalancesItem name="Net Worth" value={ totalUsd ? `$${ totalUsd } USD` : 'N/A' }/>
<TokenBalancesItem
name={ `${ appConfig.network.currency.symbol } Balance` }
value={ (nativeUsd ? `$${ nativeUsd } USD | ` : '') + `${ nativeValue } ${ appConfig.network.currency.symbol }` }
/>
<TokenBalancesItem
name="Tokens"
value={
`$${ tokenBalanceBn } USD ` +
(balancesQuery.data.length ? ` | ${ balancesQuery.data.length } ${ balancesQuery.data.length === 1 ? 'token' : 'tokens' }` : '')
}
/>
</Flex>
);
};
export default React.memo(TokenBalances);
import { Box, Flex, Icon, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import walletIcon from 'icons/wallet.svg';
const TokenBalancesItem = ({ name, value }: {name: string; value: string }) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
return (
<Flex p={ 5 } bgColor={ bgColor } borderRadius="16px" alignItems="center">
<Icon as={ walletIcon } boxSize="30px" mr={ 3 }/>
<Box>
<Text variant="secondary" fontSize="xs">{ name }</Text>
<Text fontWeight="500">{ value }</Text>
</Box>
</Flex>
);
};
export default React.memo(TokenBalancesItem);
import { Flex, HStack, Text } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TokenLogo from 'ui/shared/TokenLogo';
import AddressAddToMetaMask from '../details/AddressAddToMetaMask';
type Props = AddressTokenBalance;
const TokensListItem = ({ token, value }: Props) => {
const tokenString = [ token.name, token.symbol && `(${ token.symbol })` ].filter(Boolean).join(' ');
const {
valueStr: tokenQuantity,
usd: tokenValue,
} = getCurrencyValue({ value: value, exchangeRate: token.exchange_rate, decimals: token.decimals, accuracy: 8, accuracyUsd: 2 });
return (
<ListItemMobile rowGap={ 2 }>
<Flex alignItems="center" width="100%">
<TokenLogo hash={ token.address } name={ token.name } boxSize={ 6 } mr={ 2 }/>
<AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString }/>
</Flex>
<Flex alignItems="center" pl={ 8 }>
<AddressLink hash={ token.address } type="address" truncation="constant"/>
<CopyToClipboard text={ token.address } ml={ 1 }/>
<AddressAddToMetaMask token={ token } ml={ 2 }/>
</Flex>
{ token.exchange_rate !== undefined && token.exchange_rate !== null && (
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Price</Text>
<Text fontSize="sm" variant="secondary">{ `$${ token.exchange_rate }` }</Text>
</HStack>
) }
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Quantity</Text>
<Text fontSize="sm" variant="secondary">{ tokenQuantity }</Text>
</HStack>
{ tokenValue !== undefined && (
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value</Text>
<Text fontSize="sm" variant="secondary">{ tokenValue }</Text>
</HStack>
) }
</ListItemMobile>
);
};
export default TokensListItem;
import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import { default as Thead } from 'ui/shared/TheadSticky';
import TokensTableItem from './TokensTableItem';
interface Props {
data: Array<AddressTokenBalance>;
top: number;
}
const TokensTable = ({ data, top }: Props) => {
return (
<Table variant="simple" size="sm">
<Thead top={ top }>
<Tr>
<Th width="30%">Asset</Th>
<Th width="30%">Contract address</Th>
<Th width="10%" isNumeric>Price</Th>
<Th width="15%" isNumeric>Quantity</Th>
<Th width="15%" isNumeric>Value</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<TokensTableItem key={ item.token.address } { ...item }/>
)) }
</Tbody>
</Table>
);
};
export default TokensTable;
import { Tr, Td, Flex } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import getCurrencyValue from 'lib/getCurrencyValue';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import TokenLogo from 'ui/shared/TokenLogo';
import AddressAddToMetaMask from '../details/AddressAddToMetaMask';
type Props = AddressTokenBalance;
const TokensTableItem = ({
token,
value,
}: Props) => {
const tokenString = [ token.name, token.symbol && `(${ token.symbol })` ].filter(Boolean).join(' ');
const {
valueStr: tokenQuantity,
usd: tokenValue,
} = getCurrencyValue({ value: value, exchangeRate: token.exchange_rate, decimals: token.decimals, accuracy: 8, accuracyUsd: 2 });
return (
<Tr>
<Td verticalAlign="middle">
<Flex alignItems="center">
<TokenLogo hash={ token.address } name={ token.name } boxSize={ 6 } mr={ 2 }/>
<AddressLink fontWeight="700" hash={ token.address } type="token" alias={ tokenString }/>
</Flex>
</Td>
<Td verticalAlign="middle">
<Flex alignItems="center" width="150px" justifyContent="space-between">
<Flex alignItems="center">
<AddressLink hash={ token.address } type="address" truncation="constant"/>
<CopyToClipboard text={ token.address } ml={ 1 }/>
</Flex>
<AddressAddToMetaMask token={ token } ml={ 4 }/>
</Flex>
</Td>
<Td isNumeric verticalAlign="middle">
{ token.exchange_rate ? `$${ token.exchange_rate }` : '-' }
</Td>
<Td isNumeric verticalAlign="middle">
{ tokenQuantity }
</Td>
<Td isNumeric verticalAlign="middle">
{ tokenValue ? `$${ tokenValue }` : '-' }
</Td>
</Tr>
);
};
export default React.memo(TokensTableItem);
import { Flex, Skeleton, Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { AddressTokensResponse } from 'types/api/address';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
import NFTItem from './NFTItem';
type Props = {
tokensQuery: UseQueryResult<AddressTokensResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
}
const TokensWithIds = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
if (isError) {
return <DataFetchAlert/>;
}
const bar = isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
);
if (isLoading) {
return (
<>
{ bar }
<Flex columnGap={ 6 } rowGap={ 6 } flexWrap="wrap">
<Skeleton w="210px" h="272px"/>
<Skeleton w="210px" h="272px"/>
<Skeleton w="210px" h="272px"/>
</Flex>
</>
);
}
if (!data.items.length) {
return <Text as="span">There are no tokens of selected type.</Text>;
}
return (
<>
{ bar }
<Flex columnGap={{ base: 3, lg: 6 }} rowGap={{ base: 3, lg: 6 }} flexWrap="wrap">
{ data.items.map(item => <NFTItem key={ item.token.address } { ...item }/>) }
</Flex>
</>
);
};
export default TokensWithIds;
import { Text, Show, Hide } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { AddressTokensResponse } from 'types/api/address';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import TokensListItem from './TokensListItem';
import TokensTable from './TokensTable';
type Props = {
tokensQuery: UseQueryResult<AddressTokensResponse> & {
pagination: PaginationProps;
isPaginationVisible: boolean;
};
}
const TokensWithoutIds = ({ tokensQuery }: Props) => {
const isMobile = useIsMobile();
const { isError, isLoading, data, pagination, isPaginationVisible } = tokensQuery;
if (isError) {
return <DataFetchAlert/>;
}
const bar = isMobile && isPaginationVisible && (
<ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/>
</ActionBar>
);
if (isLoading) {
return (
<>
{ bar }
<Hide below="lg" ssr={ false }><SkeletonTable columns={ [ '30%', '30%', '10%', '20%', '10%' ] }/></Hide>
<Show below="lg" ssr={ false }><SkeletonList/></Show>
</>
);
}
if (data.items.length === 0) {
return <Text as="span">There are no tokens of selected type.</Text>;
}
return (
<>
{ bar }
<Hide below="lg" ssr={ false }><TokensTable data={ data.items } top={ isPaginationVisible ? 72 : 0 }/></Hide>
<Show below="lg" ssr={ false }>{ data.items.map(item => <TokensListItem key={ item.token.address } { ...item }/>) }</Show>
</>
);
};
export default TokensWithoutIds;
...@@ -3,6 +3,8 @@ import BigNumber from 'bignumber.js'; ...@@ -3,6 +3,8 @@ import BigNumber from 'bignumber.js';
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressTokenBalance } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo'; import type { TokenType } from 'types/api/tokenInfo';
import { ZERO } from 'lib/consts';
export type EnhancedData = AddressTokenBalance & { export type EnhancedData = AddressTokenBalance & {
usd?: BigNumber ; usd?: BigNumber ;
} }
...@@ -77,3 +79,7 @@ export const calculateUsdValue = (data: AddressTokenBalance): EnhancedData => { ...@@ -77,3 +79,7 @@ export const calculateUsdValue = (data: AddressTokenBalance): EnhancedData => {
usd: BigNumber(data.value).div(BigNumber(10 ** decimals)).multipliedBy(BigNumber(exchangeRate)), usd: BigNumber(data.value).div(BigNumber(10 ** decimals)).multipliedBy(BigNumber(exchangeRate)),
}; };
}; };
export const getTokenBalanceTotal = (data: Array<EnhancedData>) => {
return data.reduce((result, item) => !item.usd ? result : result.plus(BigNumber(item.usd)), ZERO);
};
...@@ -27,7 +27,6 @@ test('regular block +@mobile +@dark-mode', async({ mount, page }) => { ...@@ -27,7 +27,6 @@ test('regular block +@mobile +@dark-mode', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
await page.waitForResponse(API_URL),
await page.getByText('View details').click(); await page.getByText('View details').click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
...@@ -46,7 +45,6 @@ test('genesis block', async({ mount, page }) => { ...@@ -46,7 +45,6 @@ test('genesis block', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
await page.waitForResponse(API_URL),
await page.getByText('View details').click(); await page.getByText('View details').click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
......
...@@ -31,7 +31,7 @@ const BlockDetails = () => { ...@@ -31,7 +31,7 @@ const BlockDetails = () => {
const [ isExpanded, setIsExpanded ] = React.useState(false); const [ isExpanded, setIsExpanded ] = React.useState(false);
const router = useRouter(); const router = useRouter();
const { data, isLoading, isError, error } = useApiQuery<'block', { status: number }>('block', { const { data, isLoading, isError, error } = useApiQuery('block', {
pathParams: { id: router.query.id?.toString() }, pathParams: { id: router.query.id?.toString() },
queryOptions: { enabled: Boolean(router.query.id) }, queryOptions: { enabled: Boolean(router.query.id) },
}); });
...@@ -59,11 +59,11 @@ const BlockDetails = () => { ...@@ -59,11 +59,11 @@ const BlockDetails = () => {
} }
if (isError) { if (isError) {
if (error?.payload?.status === 404) { if (error?.status === 404) {
return <span>This block has not been processed yet.</span>; throw Error('Block not found', { cause: error as unknown as Error });
} }
if (error?.payload?.status === 422) { if (error?.status === 422) {
throw Error('Invalid block number', { cause: error as unknown as Error }); throw Error('Invalid block number', { cause: error as unknown as Error });
} }
......
...@@ -2,6 +2,7 @@ import { Flex, Skeleton, Tag, Box } from '@chakra-ui/react'; ...@@ -2,6 +2,7 @@ import { Flex, Skeleton, Tag, Box } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
...@@ -12,15 +13,26 @@ import AddressContract from 'ui/address/AddressContract'; ...@@ -12,15 +13,26 @@ import AddressContract from 'ui/address/AddressContract';
import AddressDetails from 'ui/address/AddressDetails'; import AddressDetails from 'ui/address/AddressDetails';
import AddressInternalTxs from 'ui/address/AddressInternalTxs'; import AddressInternalTxs from 'ui/address/AddressInternalTxs';
import AddressLogs from 'ui/address/AddressLogs'; import AddressLogs from 'ui/address/AddressLogs';
import AddressTokens from 'ui/address/AddressTokens';
import AddressTokenTransfers from 'ui/address/AddressTokenTransfers'; import AddressTokenTransfers from 'ui/address/AddressTokenTransfers';
import AddressTxs from 'ui/address/AddressTxs'; import AddressTxs from 'ui/address/AddressTxs';
import ContractCode from 'ui/address/contract/ContractCode'; import ContractCode from 'ui/address/contract/ContractCode';
import ContractRead from 'ui/address/contract/ContractRead';
import ContractWrite from 'ui/address/contract/ContractWrite';
import TextAd from 'ui/shared/ad/TextAd'; import TextAd from 'ui/shared/ad/TextAd';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs'; import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
export const tokenTabsByType: Record<TokenType, string> = {
'ERC-20': 'tokens_erc20',
'ERC-721': 'tokens_erc721',
'ERC-1155': 'tokens_erc1155',
} as const;
const TOKEN_TABS = Object.values(tokenTabsByType);
const AddressPageContent = () => { const AddressPageContent = () => {
const router = useRouter(); const router = useRouter();
...@@ -44,19 +56,19 @@ const AddressPageContent = () => { ...@@ -44,19 +56,19 @@ const AddressPageContent = () => {
{ id: 'contact_decompiled_code', title: 'Decompiled code', component: <div>Decompiled code</div> } : { id: 'contact_decompiled_code', title: 'Decompiled code', component: <div>Decompiled code</div> } :
undefined, undefined,
addressQuery.data?.has_methods_read ? addressQuery.data?.has_methods_read ?
{ id: 'read_contract', title: 'Read contract', component: <div>Read contract</div> } : { id: 'read_contract', title: 'Read contract', component: <ContractRead/> } :
undefined, undefined,
addressQuery.data?.has_methods_read_proxy ? addressQuery.data?.has_methods_read_proxy ?
{ id: 'read_proxy', title: 'Read proxy', component: <div>Read proxy</div> } : { id: 'read_proxy', title: 'Read proxy', component: <ContractRead isProxy/> } :
undefined, undefined,
addressQuery.data?.has_custom_methods_read ? addressQuery.data?.has_custom_methods_read ?
{ id: 'read_custom_methods', title: 'Read custom methods', component: <div>Read custom methods</div> } : { id: 'read_custom_methods', title: 'Read custom methods', component: <div>Read custom methods</div> } :
undefined, undefined,
addressQuery.data?.has_methods_write ? addressQuery.data?.has_methods_write ?
{ id: 'write_contract', title: 'Write contract', component: <div>Write contract</div> } : { id: 'write_contract', title: 'Write contract', component: <ContractWrite/> } :
undefined, undefined,
addressQuery.data?.has_methods_write_proxy ? addressQuery.data?.has_methods_write_proxy ?
{ id: 'write_proxy', title: 'Write proxy', component: <div>Write proxy</div> } : { id: 'write_proxy', title: 'Write proxy', component: <ContractWrite isProxy/> } :
undefined, undefined,
addressQuery.data?.has_custom_methods_write ? addressQuery.data?.has_custom_methods_write ?
{ id: 'write_custom_methods', title: 'Write custom methods', component: <div>Write custom methods</div> } : { id: 'write_custom_methods', title: 'Write custom methods', component: <div>Write custom methods</div> } :
...@@ -70,7 +82,7 @@ const AddressPageContent = () => { ...@@ -70,7 +82,7 @@ const AddressPageContent = () => {
addressQuery.data?.has_token_transfers ? addressQuery.data?.has_token_transfers ?
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> } : { id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers scrollRef={ tabsScrollRef }/> } :
undefined, undefined,
addressQuery.data?.has_tokens ? { id: 'tokens', title: 'Tokens', component: null } : undefined, addressQuery.data?.has_tokens ? { id: 'tokens', title: 'Tokens', component: <AddressTokens/>, subTabs: TOKEN_TABS } : undefined,
{ id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs scrollRef={ tabsScrollRef }/> }, { id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs scrollRef={ tabsScrollRef }/> },
{ id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance/> }, { id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance/> },
addressQuery.data?.has_validated_blocks ? addressQuery.data?.has_validated_blocks ?
...@@ -81,7 +93,7 @@ const AddressPageContent = () => { ...@@ -81,7 +93,7 @@ const AddressPageContent = () => {
id: 'contract', id: 'contract',
title: 'Contract', title: 'Contract',
component: <AddressContract tabs={ contractTabs }/>, component: <AddressContract tabs={ contractTabs }/>,
subTabs: contractTabs, subTabs: contractTabs.map(tab => tab.id),
} : undefined, } : undefined,
].filter(notEmpty); ].filter(notEmpty);
}, [ addressQuery.data, contractTabs ]); }, [ addressQuery.data, contractTabs ]);
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as searchMock from 'mocks/search/index';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import SearchResults from './SearchResults';
test('search by name +@mobile +@dark-mode', async({ mount, page }) => {
const hooksConfig = {
router: {
query: { q: 'o' },
},
};
await page.route(buildApiUrl('search') + '?q=o', (route) => route.fulfill({
status: 200,
body: JSON.stringify({
items: [
searchMock.token1,
searchMock.token2,
searchMock.contract1,
],
}),
}));
const component = await mount(
<TestApp>
<SearchResults/>
</TestApp>,
{ hooksConfig },
);
await expect(component.locator('main')).toHaveScreenshot();
});
test('search by address hash +@mobile', async({ mount, page }) => {
const hooksConfig = {
router: {
query: { q: searchMock.address1.address },
},
};
await page.route(buildApiUrl('search') + `?q=${ searchMock.address1.address }`, (route) => route.fulfill({
status: 200,
body: JSON.stringify({
items: [
searchMock.address1,
],
}),
}));
const component = await mount(
<TestApp>
<SearchResults/>
</TestApp>,
{ hooksConfig },
);
await expect(component.locator('main')).toHaveScreenshot();
});
test('search by block number +@mobile', async({ mount, page }) => {
const hooksConfig = {
router: {
query: { q: searchMock.block1.block_number },
},
};
await page.route(buildApiUrl('search') + `?q=${ searchMock.block1.block_number }`, (route) => route.fulfill({
status: 200,
body: JSON.stringify({
items: [
searchMock.block1,
],
}),
}));
const component = await mount(
<TestApp>
<SearchResults/>
</TestApp>,
{ hooksConfig },
);
await expect(component.locator('main')).toHaveScreenshot();
});
test('search by block hash +@mobile', async({ mount, page }) => {
const hooksConfig = {
router: {
query: { q: searchMock.block1.block_hash },
},
};
await page.route(buildApiUrl('search') + `?q=${ searchMock.block1.block_hash }`, (route) => route.fulfill({
status: 200,
body: JSON.stringify({
items: [
searchMock.block1,
],
}),
}));
const component = await mount(
<TestApp>
<SearchResults/>
</TestApp>,
{ hooksConfig },
);
await expect(component.locator('main')).toHaveScreenshot();
});
test('search by tx hash +@mobile', async({ mount, page }) => {
const hooksConfig = {
router: {
query: { q: searchMock.tx1.tx_hash },
},
};
await page.route(buildApiUrl('search') + `?q=${ searchMock.tx1.tx_hash }`, (route) => route.fulfill({
status: 200,
body: JSON.stringify({
items: [
searchMock.tx1,
],
}),
}));
const component = await mount(
<TestApp>
<SearchResults/>
</TestApp>,
{ hooksConfig },
);
await expect(component.locator('main')).toHaveScreenshot();
});
import { Box, chakra, Table, Tbody, Tr, Th, Skeleton, Show, Hide } from '@chakra-ui/react';
import type { FormEvent } from 'react';
import React from 'react';
import SearchResultListItem from 'ui/searchResults/SearchResultListItem';
import SearchResultTableItem from 'ui/searchResults/SearchResultTableItem';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import { default as Thead } from 'ui/shared/TheadSticky';
import Header from 'ui/snippets/header/Header';
import SearchBarInput from 'ui/snippets/searchBar/SearchBarInput';
import useSearchQuery from 'ui/snippets/searchBar/useSearchQuery';
const SearchResultsPageContent = () => {
const { query, searchTerm, debouncedSearchTerm, handleSearchTermChange } = useSearchQuery(true);
const { data, isError, isLoading, pagination, isPaginationVisible } = query;
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
}, [ ]);
const content = (() => {
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<Box>
<Show below="lg">
<SkeletonList/>
</Show>
<Hide below="lg">
<SkeletonTable columns={ [ '50%', '50%', '150px' ] }/>
</Hide>
</Box>
);
}
if (data.items.length === 0) {
return null;
}
return (
<>
<Show below="lg" ssr={ false }>
{ data.items.map((item, index) => <SearchResultListItem key={ index } data={ item } searchTerm={ debouncedSearchTerm }/>) }
</Show>
<Hide below="lg" ssr={ false }>
<Table variant="simple" size="md" fontWeight={ 500 }>
<Thead top={ isPaginationVisible ? 80 : 0 }>
<Tr>
<Th width="50%">Search Result</Th>
<Th width="50%"/>
<Th width="150px">Category</Th>
</Tr>
</Thead>
<Tbody>
{ data.items.map((item, index) => <SearchResultTableItem key={ index } data={ item } searchTerm={ debouncedSearchTerm }/>) }
</Tbody>
</Table>
</Hide>
</>
);
})();
const bar = (() => {
if (isError) {
return null;
}
const text = isLoading ? (
<Skeleton h={ 6 } w="280px" borderRadius="full" mb={ isPaginationVisible ? 0 : 6 }/>
) : (
(
<Box mb={ isPaginationVisible ? 0 : 6 } lineHeight="32px">
<span>Found </span>
<chakra.span fontWeight={ 700 }>
{ pagination.page > 1 ? 50 : data.items.length }{ data.next_page_params || pagination.page > 1 ? '+' : '' }
</chakra.span>
<span> matching result{ data.items.length > 1 || pagination.page > 1 ? 's' : '' } for </span>
<chakra.span fontWeight={ 700 }>{ debouncedSearchTerm }</chakra.span>
</Box>
)
);
if (!isPaginationVisible) {
return text;
}
return (
<>
<Box display={{ base: 'block', lg: 'none' }}>{ text }</Box>
<ActionBar mt={{ base: 0, lg: -6 }} alignItems="center">
<Box display={{ base: 'none', lg: 'block' }}>{ text }</Box>
<Pagination { ...pagination }/>
</ActionBar>
</>
);
})();
const inputRef = React.useRef<HTMLFormElement>(null);
const handelHide = React.useCallback(() => {
inputRef.current?.querySelector('input')?.blur();
}, [ ]);
const handleClear = React.useCallback(() => {
handleSearchTermChange('');
inputRef.current?.querySelector('input')?.focus();
}, [ handleSearchTermChange ]);
const renderSearchBar = React.useCallback(() => {
return (
<SearchBarInput
ref={ inputRef }
onChange={ handleSearchTermChange }
onSubmit={ handleSubmit }
value={ searchTerm }
onHide={ handelHide }
onClear={ handleClear }
/>
);
}, [ handleSearchTermChange, handleSubmit, searchTerm, handelHide, handleClear ]);
const renderHeader = React.useCallback(() => {
return <Header renderSearchBar={ renderSearchBar }/>;
}, [ renderSearchBar ]);
return (
<Page renderHeader={ renderHeader }>
<PageTitle text="Search results"/>
{ bar }
{ content }
</Page>
);
};
export default SearchResultsPageContent;
...@@ -12,12 +12,16 @@ import useStats from '../stats/useStats'; ...@@ -12,12 +12,16 @@ import useStats from '../stats/useStats';
const Stats = () => { const Stats = () => {
const { const {
section, isLoading,
isError,
sections,
currentSection,
handleSectionChange, handleSectionChange,
interval, interval,
handleIntervalChange, handleIntervalChange,
debounceFilterCharts, debounceFilterCharts,
displayedCharts, displayedCharts,
filterQuery,
} = useStats(); } = useStats();
return ( return (
...@@ -30,7 +34,8 @@ const Stats = () => { ...@@ -30,7 +34,8 @@ const Stats = () => {
<Box mb={{ base: 6, sm: 8 }}> <Box mb={{ base: 6, sm: 8 }}>
<StatsFilters <StatsFilters
section={ section } sections={ sections }
currentSection={ currentSection }
onSectionChange={ handleSectionChange } onSectionChange={ handleSectionChange }
interval={ interval } interval={ interval }
onIntervalChange={ handleIntervalChange } onIntervalChange={ handleIntervalChange }
...@@ -39,6 +44,9 @@ const Stats = () => { ...@@ -39,6 +44,9 @@ const Stats = () => {
</Box> </Box>
<ChartsWidgetsList <ChartsWidgetsList
filterQuery={ filterQuery }
isError={ isError }
isLoading={ isLoading }
charts={ displayedCharts } charts={ displayedCharts }
interval={ interval } interval={ interval }
/> />
......
...@@ -35,7 +35,7 @@ const Transactions = () => { ...@@ -35,7 +35,7 @@ const Transactions = () => {
]; ];
return ( return (
<Page hideMobileHeaderOnScrollDown> <Page>
<Box h="100%"> <Box h="100%">
<PageTitle text="Transactions" withTextAd/> <PageTitle text="Transactions" withTextAd/>
<RoutedTabs <RoutedTabs
......
import { Text, Link, Flex, Icon, Box, chakra } from '@chakra-ui/react';
import React from 'react';
import type { SearchResultItem } from 'types/api/search';
import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg';
import highlightText from 'lib/highlightText';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
data: SearchResultItem;
searchTerm: string;
}
const SearchResultListItem = ({ data, searchTerm }: Props) => {
const firstRow = (() => {
switch (data.type) {
case 'token': {
const name = data.name + (data.symbol ? ` (${ data.symbol })` : '');
return (
<Flex alignItems="flex-start">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/>
<Link ml={ 2 } href={ link('token_index', { hash: data.address }) } fontWeight={ 700 } wordBreak="break-all">
<chakra.span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</Link>
</Flex>
);
}
case 'contract':
case 'address': {
const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase();
return (
<Address>
<AddressIcon address={{ hash: data.address, is_contract: data.type === 'contract', implementation_name: null }} mr={ 2 } flexShrink={ 0 }/>
<Box as={ shouldHighlightHash ? 'mark' : 'span' } display="block" whiteSpace="nowrap" overflow="hidden">
<AddressLink hash={ data.address } fontWeight={ 700 } display="block" w="100%"/>
</Box>
</Address>
);
}
case 'block': {
const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase();
return (
<Flex alignItems="center">
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<Link fontWeight={ 700 } href={ link('block', { id: String(data.block_number) }) }>
<Box as={ shouldHighlightHash ? 'span' : 'mark' }>{ data.block_number }</Box>
</Link>
</Flex>
);
}
case 'transaction': {
return (
<Flex alignItems="center" overflow="hidden">
<Icon as={ txIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<chakra.mark display="block" overflow="hidden">
<AddressLink hash={ data.tx_hash } type="transaction" fontWeight={ 700 } display="block"/>
</chakra.mark>
</Flex>
);
}
}
})();
const secondRow = (() => {
switch (data.type) {
case 'token': {
return (
<HashStringShortenDynamic hash={ data.address }/>
);
}
case 'block': {
const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase();
return (
<Box as={ shouldHighlightHash ? 'mark' : 'span' } display="block" w="100%" whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ data.block_hash }/>
</Box>
);
}
case 'contract':
case 'address': {
const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase();
return data.name ? <span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? data.name : highlightText(data.name, searchTerm) }}/> : null;
}
default:
return null;
}
})();
return (
<ListItemMobile py={ 3 } fontSize="sm" rowGap={ 2 }>
<Flex justifyContent="space-between" w="100%" overflow="hidden" lineHeight={ 6 }>
{ firstRow }
<Text variant="secondary" ml={ 8 } textTransform="capitalize">{ data.type }</Text>
</Flex>
{ secondRow }
</ListItemMobile>
);
};
export default SearchResultListItem;
import { Tr, Td, Text, Link, Flex, Icon, Box } from '@chakra-ui/react';
import React from 'react';
import type { SearchResultItem } from 'types/api/search';
import blockIcon from 'icons/block.svg';
import txIcon from 'icons/transactions.svg';
import highlightText from 'lib/highlightText';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
data: SearchResultItem;
searchTerm: string;
}
const SearchResultTableItem = ({ data, searchTerm }: Props) => {
const content = (() => {
switch (data.type) {
case 'token': {
const name = data.name + (data.symbol ? ` (${ data.symbol })` : '');
return (
<>
<Td fontSize="sm">
<Flex alignItems="center">
<TokenLogo boxSize={ 6 } hash={ data.address } name={ data.name } flexShrink={ 0 }/>
<Link ml={ 2 } href={ link('token_index', { hash: data.address }) } fontWeight={ 700 } wordBreak="break-all">
<span dangerouslySetInnerHTML={{ __html: highlightText(name, searchTerm) }}/>
</Link>
</Flex>
</Td>
<Td fontSize="sm" verticalAlign="middle">
<Box whiteSpace="nowrap" overflow="hidden">
<HashStringShortenDynamic hash={ data.address }/>
</Box>
</Td>
</>
);
}
case 'contract':
case 'address': {
if (data.name) {
const shouldHighlightHash = data.address.toLowerCase() === searchTerm.toLowerCase();
return (
<>
<Td fontSize="sm">
<Flex alignItems="center" overflow="hidden">
<AddressIcon address={{ hash: data.address, is_contract: data.type === 'contract', implementation_name: null }} mr={ 2 } flexShrink={ 0 }/>
<Link href={ link('address_index', { id: data.address }) } fontWeight={ 700 } overflow="hidden" whiteSpace="nowrap">
<Box as={ shouldHighlightHash ? 'mark' : 'span' } display="block">
<HashStringShortenDynamic hash={ data.address }/>
</Box>
</Link>
</Flex>
</Td>
<Td fontSize="sm" verticalAlign="middle">
<span dangerouslySetInnerHTML={{ __html: shouldHighlightHash ? data.name : highlightText(data.name, searchTerm) }}/>
</Td>
</>
);
}
return (
<Td colSpan={ 2 } fontSize="sm">
<Address>
<AddressIcon address={{ hash: data.address, is_contract: data.type === 'contract', implementation_name: null }} mr={ 2 } flexShrink={ 0 }/>
<mark>
<AddressLink hash={ data.address } type="address" fontWeight={ 700 }/>
</mark>
</Address>
</Td>
);
}
case 'block': {
const shouldHighlightHash = data.block_hash.toLowerCase() === searchTerm.toLowerCase();
return (
<>
<Td fontSize="sm">
<Flex alignItems="center">
<Icon as={ blockIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<Link fontWeight={ 700 } href={ link('block', { id: String(data.block_number) }) }>
<Box as={ shouldHighlightHash ? 'span' : 'mark' }>{ data.block_number }</Box>
</Link>
</Flex>
</Td>
<Td fontSize="sm" verticalAlign="middle">
<Box overflow="hidden" whiteSpace="nowrap" as={ shouldHighlightHash ? 'mark' : 'span' } display="block">
<HashStringShortenDynamic hash={ data.block_hash }/>
</Box>
</Td>
</>
);
}
case 'transaction': {
return (
<Td colSpan={ 2 } fontSize="sm">
<Flex alignItems="center">
<Icon as={ txIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
<mark>
<AddressLink hash={ data.tx_hash } type="transaction" fontWeight={ 700 }/>
</mark>
</Flex>
</Td>
);
}
}
})();
return (
<Tr>
{ content }
<Td fontSize="sm" textTransform="capitalize" verticalAlign="middle">
<Text variant="secondary">
{ data.type }
</Text>
</Td>
</Tr>
);
};
export default React.memo(SearchResultTableItem);
...@@ -9,9 +9,10 @@ const runnerAnimation = keyframes` ...@@ -9,9 +9,10 @@ const runnerAnimation = keyframes`
interface Props { interface Props {
className?: string; className?: string;
text?: string;
} }
const ContentLoader = ({ className }: Props) => { const ContentLoader = ({ className, text }: Props) => {
return ( return (
<Box display="inline-block" className={ className }> <Box display="inline-block" className={ className }>
<Box <Box
...@@ -30,7 +31,9 @@ const ContentLoader = ({ className }: Props) => { ...@@ -30,7 +31,9 @@ const ContentLoader = ({ className }: Props) => {
borderRadius: 'full', borderRadius: 'full',
}} }}
/> />
<Text mt={ 6 } variant="secondary">Loading data, please wait... </Text> <Text mt={ 6 } variant="secondary">
{ text || 'Loading data, please wait...' }
</Text>
</Box> </Box>
); );
}; };
......
import { chakra, Icon, IconButton, useColorModeValue } from '@chakra-ui/react'; import { chakra, Icon, IconButton, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import crossIcon from 'icons/cross.svg'; import errorIcon from 'icons/status/error.svg';
interface Props { interface Props {
onClick: () => void; onClick: () => void;
className?: string;
} }
const InputClearButton = ({ onClick }: Props) => { const InputClearButton = ({ onClick, className }: Props) => {
const iconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600'); const iconColor = useColorModeValue('gray.300', 'gray.600');
const iconColorHover = useColorModeValue('gray.200', 'gray.500');
return ( return (
<IconButton <IconButton
colorScheme="gray" className={ className }
colorScheme="none"
aria-label="Clear input" aria-label="Clear input"
title="Clear input" title="Clear input"
boxSize={ 6 } boxSize={ 6 }
icon={ <Icon as={ crossIcon } boxSize={ 4 } color={ iconColor }/> } icon={ <Icon as={ errorIcon } boxSize={ 3 } color={ iconColor } focusable={ false } _hover={{ color: iconColorHover }}/> }
size="sm" size="sm"
onClick={ onClick } onClick={ onClick }
/> />
......
...@@ -12,22 +12,22 @@ import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop'; ...@@ -12,22 +12,22 @@ import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop';
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
wrapChildren?: boolean; wrapChildren?: boolean;
hideMobileHeaderOnScrollDown?: boolean;
isHomePage?: boolean; isHomePage?: boolean;
renderHeader?: () => React.ReactNode;
} }
const Page = ({ const Page = ({
children, children,
wrapChildren = true, wrapChildren = true,
hideMobileHeaderOnScrollDown,
isHomePage, isHomePage,
renderHeader,
}: Props) => { }: Props) => {
useGetCsrfToken(); useGetCsrfToken();
const renderErrorScreen = React.useCallback((error?: Error) => { const renderErrorScreen = React.useCallback((error?: Error) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const statusCode = (error?.cause as any)?.error?.status || 500; const statusCode = (error?.cause as any)?.status || 500;
const isInvalidTxHash = error?.message.includes('Invalid tx hash'); const isInvalidTxHash = error?.message.includes('Invalid tx hash');
if (wrapChildren) { if (wrapChildren) {
...@@ -46,7 +46,10 @@ const Page = ({ ...@@ -46,7 +46,10 @@ const Page = ({
<Flex w="100%" minH="100vh" alignItems="stretch"> <Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/> <NavigationDesktop/>
<Flex flexDir="column" flexGrow={ 1 } w={{ base: '100%', lg: 'auto' }}> <Flex flexDir="column" flexGrow={ 1 } w={{ base: '100%', lg: 'auto' }}>
<Header isHomePage={ isHomePage } hideOnScrollDown={ hideMobileHeaderOnScrollDown }/> { renderHeader ?
renderHeader() :
<Header isHomePage={ isHomePage }/>
}
<ErrorBoundary renderErrorScreen={ renderErrorScreen }> <ErrorBoundary renderErrorScreen={ renderErrorScreen }>
{ renderedChildren } { renderedChildren }
</ErrorBoundary> </ErrorBoundary>
......
...@@ -9,12 +9,11 @@ export type Props = { ...@@ -9,12 +9,11 @@ export type Props = {
onPrevPageClick: () => void; onPrevPageClick: () => void;
resetPage: () => void; resetPage: () => void;
hasNextPage: boolean; hasNextPage: boolean;
hasPaginationParams?: boolean;
className?: string; className?: string;
canGoBackwards: boolean; canGoBackwards: boolean;
} }
const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNextPage, hasPaginationParams, className, canGoBackwards }: Props) => { const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNextPage, className, canGoBackwards }: Props) => {
return ( return (
<Flex <Flex
...@@ -26,7 +25,7 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext ...@@ -26,7 +25,7 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext
variant="outline" variant="outline"
size="sm" size="sm"
onClick={ resetPage } onClick={ resetPage }
disabled={ !hasPaginationParams } disabled={ page === 1 }
mr={ 4 } mr={ 4 }
> >
First First
...@@ -49,7 +48,6 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext ...@@ -49,7 +48,6 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext
fontWeight={ 400 } fontWeight={ 400 }
h={ 8 } h={ 8 }
cursor="unset" cursor="unset"
disabled={ hasPaginationParams && page === 1 }
> >
{ page } { page }
</Button> </Button>
......
...@@ -62,7 +62,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, . ...@@ -62,7 +62,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
let tabIndex = 0; let tabIndex = 0;
const tabFromRoute = router.query.tab; const tabFromRoute = router.query.tab;
if (tabFromRoute) { if (tabFromRoute) {
tabIndex = tabs.findIndex(({ id, subTabs }) => id === tabFromRoute || subTabs?.some(({ id }) => id === tabFromRoute)); tabIndex = tabs.findIndex(({ id, subTabs }) => id === tabFromRoute || subTabs?.some((id) => id === tabFromRoute));
if (tabIndex < 0) { if (tabIndex < 0) {
tabIndex = 0; tabIndex = 0;
} }
......
...@@ -2,7 +2,7 @@ export interface RoutedTab { ...@@ -2,7 +2,7 @@ export interface RoutedTab {
id: string; id: string;
title: string; title: string;
component: React.ReactNode; component: React.ReactNode;
subTabs?: Array<RoutedSubTab>; subTabs?: Array<string>;
} }
export type RoutedSubTab = Omit<RoutedTab, 'subTabs'>; export type RoutedSubTab = Omit<RoutedTab, 'subTabs'>;
......
...@@ -10,10 +10,20 @@ type AdData = { ...@@ -10,10 +10,20 @@ type AdData = {
thumbnail: string; thumbnail: string;
url: string; url: string;
cta_button: string; cta_button: string;
impressionUrl: string; impressionUrl?: string;
}; };
} }
// const MOCK: AdData = {
// ad: {
// url: 'https://unsplash.com/s/photos/cute-kitten',
// thumbnail: 'https://placekitten.com/40/40',
// name: 'All about kitties',
// description_short: 'To see millions picture of cute kitties',
// cta_button: 'click here',
// },
// };
const CoinzillaTextAd = ({ className }: {className?: string}) => { const CoinzillaTextAd = ({ className }: {className?: string}) => {
const [ adData, setAdData ] = React.useState<AdData | null>(null); const [ adData, setAdData ] = React.useState<AdData | null>(null);
useEffect(() => { useEffect(() => {
......
...@@ -7,14 +7,14 @@ import type { AddressParam } from 'types/api/addressParams'; ...@@ -7,14 +7,14 @@ import type { AddressParam } from 'types/api/addressParams';
import AddressContractIcon from 'ui/shared/address/AddressContractIcon'; import AddressContractIcon from 'ui/shared/address/AddressContractIcon';
type Props = { type Props = {
address: AddressParam; address: Pick<AddressParam, 'is_contract' | 'hash' | 'implementation_name'>;
className?: string; className?: string;
} }
const AddressIcon = ({ address, className }: Props) => { const AddressIcon = ({ address, className }: Props) => {
if (address.is_contract) { if (address.is_contract) {
return ( return (
<AddressContractIcon/> <AddressContractIcon className={ className }/>
); );
} }
......
import { Box, Grid, Icon, IconButton, Menu, MenuButton, MenuItem, MenuList, Text, Tooltip, useColorModeValue, VisuallyHidden } from '@chakra-ui/react'; import {
Box,
Flex,
Grid,
Icon,
IconButton, Link,
Menu,
MenuButton,
MenuItem,
MenuList,
Text,
Tooltip,
useColorModeValue,
VisuallyHidden,
} from '@chakra-ui/react';
import domToImage from 'dom-to-image'; import domToImage from 'dom-to-image';
import React, { useRef, useCallback, useState } from 'react'; import React, { useRef, useCallback, useState } from 'react';
...@@ -10,6 +24,7 @@ import scopeIcon from 'icons/scope.svg'; ...@@ -10,6 +24,7 @@ import scopeIcon from 'icons/scope.svg';
import svgFileIcon from 'icons/svg_file.svg'; import svgFileIcon from 'icons/svg_file.svg';
import dotsIcon from 'icons/vertical_dots.svg'; import dotsIcon from 'icons/vertical_dots.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import { apos } from 'lib/html-entities';
import saveAsCSV from 'lib/saveAsCSV'; import saveAsCSV from 'lib/saveAsCSV';
import ChartWidgetGraph from './ChartWidgetGraph'; import ChartWidgetGraph from './ChartWidgetGraph';
...@@ -22,11 +37,12 @@ type Props = { ...@@ -22,11 +37,12 @@ type Props = {
description?: string; description?: string;
isLoading: boolean; isLoading: boolean;
chartHeight?: string; chartHeight?: string;
isError: boolean;
} }
const DOWNLOAD_IMAGE_SCALE = 5; const DOWNLOAD_IMAGE_SCALE = 5;
const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Props) => { const ChartWidget = ({ items, title, description, isLoading, chartHeight, isError }: Props) => {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [ isFullscreen, setIsFullscreen ] = useState(false); const [ isFullscreen, setIsFullscreen ] = useState(false);
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true); const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
...@@ -92,64 +108,66 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop ...@@ -92,64 +108,66 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop
}, [ items, title ]); }, [ items, title ]);
if (isLoading) { if (isLoading) {
return <ChartWidgetSkeleton hasDescription={ Boolean(description) } chartHeight={ chartHeight }/>; return <ChartWidgetSkeleton hasDescription={ Boolean(description) }/>;
} }
if (items) { return (
return ( <>
<> <Box
<Box height={ chartHeight }
height={ chartHeight } display="flex"
ref={ ref } flexDirection="column"
padding={{ base: 3, lg: 4 }} ref={ ref }
borderRadius="md" padding={{ base: 3, lg: 4 }}
border="1px" borderRadius="md"
borderColor={ borderColor } border="1px"
borderColor={ borderColor }
>
<Grid
gridTemplateColumns="auto auto 36px"
gridColumnGap={ 2 }
> >
<Grid <Text
gridTemplateColumns="auto auto 36px" fontWeight={ 600 }
gridColumnGap={ 2 } fontSize="md"
lineHeight={ 6 }
as="p"
size={{ base: 'xs', lg: 'sm' }}
> >
{ title }
</Text>
{ description && (
<Text <Text
fontWeight={ 600 } mb={ 1 }
fontSize="md" gridColumn={ 1 }
lineHeight={ 6 }
as="p" as="p"
size={{ base: 'xs', lg: 'sm' }} variant="secondary"
fontSize="xs"
> >
{ title } { description }
</Text> </Text>
) }
{ description && ( <Tooltip label="Reset zoom">
<Text <IconButton
mb={ 1 } hidden={ isZoomResetInitial }
gridColumn={ 1 } aria-label="Reset zoom"
as="p" colorScheme="blue"
variant="secondary" w={ 9 }
fontSize="xs" h={ 8 }
> gridColumn={ 2 }
{ description } justifySelf="end"
</Text> alignSelf="top"
) } gridRow="1/3"
size="sm"
<Tooltip label="Reset zoom"> variant="outline"
<IconButton onClick={ handleZoomResetClick }
hidden={ isZoomResetInitial } icon={ <Icon as={ repeatArrowIcon } w={ 4 } h={ 4 }/> }
aria-label="Reset zoom" />
colorScheme="blue" </Tooltip>
w={ 9 }
h={ 8 }
gridColumn={ 2 }
justifySelf="end"
alignSelf="top"
gridRow="1/3"
size="sm"
variant="outline"
onClick={ handleZoomResetClick }
icon={ <Icon as={ repeatArrowIcon } w={ 4 } h={ 4 }/> }
/>
</Tooltip>
{ !isError && (
<Menu> <Menu>
<MenuButton <MenuButton
gridColumn={ 3 } gridColumn={ 3 }
...@@ -195,9 +213,11 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop ...@@ -195,9 +213,11 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop
</MenuItem> </MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
</Grid> ) }
</Grid>
<Box h={ chartHeight || 'auto' }> { items ? (
<Box h={ chartHeight || 'auto' } maxW="100%">
<ChartWidgetGraph <ChartWidgetGraph
margin={{ bottom: 20 }} margin={{ bottom: 20 }}
items={ items } items={ items }
...@@ -206,8 +226,26 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop ...@@ -206,8 +226,26 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop
title={ title } title={ title }
/> />
</Box> </Box>
</Box> ) : (
<Flex
alignItems="center"
justifyContent="center"
flexGrow={ 1 }
py={ 4 }
>
<Text
variant="secondary"
fontSize="sm"
textAlign="center"
>
{ `The data didn${ apos }t load. Please, ` }
<Link href={ window.document.location.href }>try to reload the page.</Link>
</Text>
</Flex>
) }
</Box>
{ items && (
<FullscreenChartModal <FullscreenChartModal
isOpen={ isFullscreen } isOpen={ isFullscreen }
items={ items } items={ items }
...@@ -215,11 +253,9 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop ...@@ -215,11 +253,9 @@ const ChartWidget = ({ items, title, description, isLoading, chartHeight }: Prop
description={ description } description={ description }
onClose={ clearFullscreenChart } onClose={ clearFullscreenChart }
/> />
</> ) }
); </>
} );
return null;
}; };
export default React.memo(ChartWidget); export default React.memo(ChartWidget);
...@@ -70,7 +70,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title ...@@ -70,7 +70,7 @@ const ChartWidgetGraph = ({ isEnlarged, items, onZoom, isZoomResetInitial, title
}, [ isZoomResetInitial, items ]); }, [ isZoomResetInitial, items ]);
return ( return (
<svg width={ width || '100%' } height={ height || '100%' } ref={ ref } cursor="pointer" id={ chartId } opacity={ width ? 1 : 0 }> <svg width="100%" height={ height || '100%' } ref={ ref } cursor="pointer" id={ chartId } opacity={ width ? 1 : 0 }>
<g transform={ `translate(${ chartMargin?.left || 0 },${ chartMargin?.top || 0 })` }> <g transform={ `translate(${ chartMargin?.left || 0 },${ chartMargin?.top || 0 })` }>
<ChartGridLine <ChartGridLine
......
...@@ -26,8 +26,8 @@ export default function useChartSize(svgEl: SVGSVGElement | null, margin?: Chart ...@@ -26,8 +26,8 @@ export default function useChartSize(svgEl: SVGSVGElement | null, margin?: Chart
setRect({ width: 0, height: 0 }); setRect({ width: 0, height: 0 });
timeoutId = window.setTimeout(() => { timeoutId = window.setTimeout(() => {
setRect(calculateRect()); setRect(calculateRect());
}, 0); }, 100);
}, 200); }, 100);
const resizeObserver = new ResizeObserver(resizeHandler); const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(content); resizeObserver.observe(content);
......
...@@ -13,13 +13,15 @@ import ColorModeToggler from './ColorModeToggler'; ...@@ -13,13 +13,15 @@ import ColorModeToggler from './ColorModeToggler';
type Props = { type Props = {
isHomePage?: boolean; isHomePage?: boolean;
hideOnScrollDown?: boolean; renderSearchBar?: () => React.ReactNode;
} }
const Header = ({ hideOnScrollDown, isHomePage }: Props) => { const Header = ({ isHomePage, renderSearchBar }: Props) => {
const bgColor = useColorModeValue('white', 'black'); const bgColor = useColorModeValue('white', 'black');
const scrollDirection = useScrollDirection(); const scrollDirection = useScrollDirection();
const searchBar = renderSearchBar ? renderSearchBar() : <SearchBar/>;
return ( return (
<> <>
<Box bgColor={ bgColor } display={{ base: 'block', lg: 'none' }}> <Box bgColor={ bgColor } display={{ base: 'block', lg: 'none' }}>
...@@ -37,13 +39,13 @@ const Header = ({ hideOnScrollDown, isHomePage }: Props) => { ...@@ -37,13 +39,13 @@ const Header = ({ hideOnScrollDown, isHomePage }: Props) => {
zIndex="sticky2" zIndex="sticky2"
transitionProperty="box-shadow" transitionProperty="box-shadow"
transitionDuration="slow" transitionDuration="slow"
boxShadow={ !hideOnScrollDown && scrollDirection === 'down' ? 'md' : 'none' } boxShadow={ scrollDirection === 'down' ? 'md' : 'none' }
> >
<Burger/> <Burger/>
<NetworkLogo/> <NetworkLogo/>
<ProfileMenuMobile/> <ProfileMenuMobile/>
</Flex> </Flex>
{ !isHomePage && <SearchBar withShadow={ !hideOnScrollDown }/> } { !isHomePage && searchBar }
</Box> </Box>
<Box <Box
paddingX={ 12 } paddingX={ 12 }
...@@ -61,7 +63,7 @@ const Header = ({ hideOnScrollDown, isHomePage }: Props) => { ...@@ -61,7 +63,7 @@ const Header = ({ hideOnScrollDown, isHomePage }: Props) => {
paddingBottom="52px" paddingBottom="52px"
> >
<Box width="100%"> <Box width="100%">
<SearchBar/> { searchBar }
</Box> </Box>
<ColorModeToggler/> <ColorModeToggler/>
<ProfileMenuDesktop/> <ProfileMenuDesktop/>
......
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.
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