Merge branch 'main' of github.com:blockscout/frontend into feat/verified-tokens
@@ -13,7 +13,7 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
|
||||
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
|
||||
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup']
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
|
||||
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
|
||||
|
||||
# network config
|
||||
|
||||
@@ -3,7 +3,7 @@ NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
|
||||
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
|
||||
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup']
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
|
||||
#NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=linear-gradient(136.9deg, \#235643 1.5%, \#16191E 77.77%)
|
||||
#NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR=\#DCFE76
|
||||
NEXT_PUBLIC_NETWORK_LOGO=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/network-logos/poa.svg
|
||||
|
||||
@@ -12,7 +12,7 @@ NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
|
||||
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
|
||||
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
|
||||
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup']
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
|
||||
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
|
||||
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
|
||||
NEXT_PUBLIC_FEATURED_NETWORKS=
|
||||
|
||||
@@ -16,8 +16,6 @@ blockscout:
|
||||
_default: ENC[AES256_GCM,data:prh45OKeStfh+hPRZLvN981E+yPjjN1E0cI2uIMT+28=,iv:/ZrYd29B2LUZn2s55w/tWmDURDvQwdIDl8JZl8dQhxY=,tag:n2S7OYoag+kI3rNrnmSlTg==,type:str]
|
||||
ACCOUNT_AUTH0_CLIENT_SECRET:
|
||||
_default: ENC[AES256_GCM,data:bgP4VwZ91eMzJVQ3/+fkqNCwBIAv3P12qRoGWu9QkfCKpj3e+Dwt1qjsYV1iNkcjQAJx3jOpVGlsbgdcBGFzHw==,iv:gpa6tbkxHv56wwI3Owdnr5MArJYdiVO3A0UMOsrgCls=,tag:CIgupR1ZNjsVtFYVG2OtHA==,type:str]
|
||||
ACCOUNT_AUTH0_CALLBACK_URL:
|
||||
_default: ENC[AES256_GCM,data:+EoMWtXdJA5DnRprCaps38VbQlLcJikKvB7FA0VMhaw+aq1d/rh5yIxl/CQBIo2eO8EESoVz6eUjzvgchbmv2IaiGgG3DnZx,iv:nfPmwEZMXM2SOIZ3OMr5u7GTbX6ni5VuFpn5SyHKMfE=,tag:4ePZ5NZ+rdBZf8xWDfEbzA==,type:str]
|
||||
ACCOUNT_SENDGRID_API_KEY:
|
||||
_default: ENC[AES256_GCM,data:i08k9qiHA/31nyAri5pIm8MqCUZXLWjvQgcYpAyrcsToszt/L+QsYVcdCEerI0udtd2gJvLuRz3k8GcpZ9OfQB2Y0kJn,iv:Pt3rg7GjhfDw5S4VV9HpLSDsO4AhXlGIsNhdc5rYqCQ=,tag:zL/bLVgSS41kgnv69GxLMg==,type:str]
|
||||
ACCOUNT_SENDGRID_SENDER:
|
||||
@@ -88,8 +86,8 @@ sops:
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age: []
|
||||
lastmodified: "2023-04-27T16:27:47Z"
|
||||
mac: ENC[AES256_GCM,data:TUIxPt/HMLzkWlPdBcM2mKGlNU+6ob+EWvtfKWAHZaVhH2qwoi+HI+Ncx+j3H9/MR6q9Uhhr611s8rCF0LdwbsK+LGdOBBql+45vfmCE1XRl/DS0LFdZxkZn4ub49C9Uf8g0DOVt02NJQNh3ZNaBa+l2nIirh643hLvzFCCqBM8=,iv:tqRFfzSk0W+NzEADzIWy08aUnpJgcIPBHhUhM0Kf+VA=,tag:0IiapCTs0UITa38cwVyZqQ==,type:str]
|
||||
lastmodified: "2023-05-09T10:35:44Z"
|
||||
mac: ENC[AES256_GCM,data:r+iWj2hSNkaAzn3duafu6mUloN5sxIcHf2oW6IpYpNrQzi/oFe509iutYqIZ9r0AOygckcyJkjgNq//ki7SZr+7texfuIGYTfH0371b67ltUuiSgdfv7ONzwr7hA7M+qqOdyvPXqti6yNSrbCSlVec1ph95EKUAlMvqDxh3/wOs=,iv:QtsiyCaNgc1YwQA3p3fT8GRI4f8AA216JcMNyK+3PlY=,tag:nogyCZ+qsMEuuHVdGucjrg==,type:str]
|
||||
pgp:
|
||||
- created_at: "2022-09-22T09:52:10Z"
|
||||
enc: |
|
||||
|
||||
@@ -5,7 +5,7 @@ blockscout:
|
||||
app: blockscout
|
||||
enabled: true
|
||||
image:
|
||||
_default: &image blockscout/blockscout-optimism-l2-advanced:5.1.2-prerelease-465ba09e
|
||||
_default: &image blockscout/blockscout-optimism-l2-advanced:5.1.4-prerelease-a8c134e4
|
||||
replicas:
|
||||
app: 1
|
||||
# init container
|
||||
@@ -398,7 +398,7 @@ frontend:
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS:
|
||||
_default: "['daily_txs']"
|
||||
NEXT_PUBLIC_IS_TESTNET:
|
||||
_default: "true"
|
||||
_default: true
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST:
|
||||
_default: https://visualizer-optimism-goerli.test.aws-k8s.blockscout.com
|
||||
NEXT_PUBLIC_CONTRACT_INFO_API_HOST:
|
||||
|
||||
@@ -34,7 +34,7 @@ blockscout:
|
||||
DATABASE_URL:
|
||||
_default: ENC[AES256_GCM,data:r5JQJH+pq4zwo9ceFP4I8inttRFFxKKaw9+l9hDqeDMcEIPljAfJxvSF3noVlyfNtqO0ru+3GaTumDamdb9Hw2Y=,iv:OwCvrIgXpxVMI5QQfT7ZDGcsXBdzIcuv0wUSxtwkYrM=,tag:o0GFp3F+jTxw8Hg4Ce/IpQ==,type:str]
|
||||
API_SENSITIVE_ENDPOINTS_KEY:
|
||||
_default: ENC[AES256_GCM,data:cghX4B0yt18xT8kd/buPd6L/zLo/jIZzyVSDPoDGHiDMXoQRxHuHtITQbCjD+w0UdFZqDvDvAC6VUc5veCfITw==,iv:h9iTyEEhxSmStgA8ycncELR1A6AcWezk+zYIyo5Fe/Q=,tag:cC/ye9+Z9MvNR3h5o7JBJg==,type:str]
|
||||
_default: ENC[AES256_GCM,data:QYhtvpV8sv38+UArgk3bfR6Au+s65PJvQn3AZRLk8loAKe/BurTvtZ81bu0vH51jZVvxc/gAcnocim4j5Y+fvA==,iv:Nktux8P7bskPWgRGnDbAUCz1xJjKy7Brdc7EDoG3e+A=,tag:kbm3LVO42B2fygWcVVsc6g==,type:str]
|
||||
ACCOUNT_DATABASE_URL:
|
||||
_default: ENC[AES256_GCM,data:HF5y8ezV5TiLqeh98WDp4rXQeUfSBETyWVHOyNZNy5pt4MdiKTdFBLauOJpD6YHWynMFsd8IJLRNLrBn4qGe3RfwprR6v3WN9Q==,iv:B9AJXO7EJexsPgDHb5s5tzpadVYoZ79fyaL8NOYXSEw=,tag:lyNKQ13Q+pn+3DrAXIzIKQ==,type:str]
|
||||
ACCOUNT_REDIS_URL:
|
||||
@@ -44,13 +44,7 @@ blockscout:
|
||||
ACCOUNT_AUTH0_CLIENT_ID:
|
||||
_default: ENC[AES256_GCM,data:xasZDDg1IuGbKSTm9Opjh43RcqDSzE3dMdmynP0gejo=,iv:TTRqPOkwMxAQIpmc7DklBoPdZzn3FsAprxwXtHY/KSk=,tag:ZI7zQ5zyjjYOqjzy2DyY7Q==,type:str]
|
||||
ACCOUNT_AUTH0_CLIENT_SECRET:
|
||||
_default: ENC[AES256_GCM,data:iYnptgxESZs216/m0ArCciXfirPTxVjt5urKTATyYv3uadokC54ofzMZagG/ZVvNY48+Uxh4YGzwySyLOOM0SA==,iv:UQoZf3tesEjAJ7E5YruoZmVclsYfS5EmSo4z90wfHfE=,tag:er+hARrkCrjoQugqvRr4ow==,type:str]
|
||||
ACCOUNT_AUTH0_CALLBACK_URL:
|
||||
_default: ENC[AES256_GCM,data:GhyNTVlRvDpfW6swKExjVy65I3S+q6ITJWH8xp5TXuudp7hAtI4ujkYmXaa7D1BCaineTLhQ8UgHXFAgodOlrsGP1g41LIh6T2SGhrXTGGLbFw==,iv:ZflinC/h35jkrVAx0x7Z7U9mRridl85f7f6nfoc0Xts=,tag:40zL2KM9uPg/lRYv5JMxqw==,type:str]
|
||||
ACCOUNT_AUTH0_LOGOUT_RETURN_URL:
|
||||
_default: ENC[AES256_GCM,data:mTY6sjNKZ+VEvH47eyyoUHt//beWvuxyreu+1WrmMvkdkwR6jgXnSXh+1pTuiG8e3it96Egxraz0hjZxlkY9kSmE91/dfoqKTns=,iv:op8OuOXXeBmmUnVkCL14j4HrfjHHoN3n2XZqLdg03Mw=,tag:bbFQYi0Aa1sUdfoUjuQ/+w==,type:str]
|
||||
ACCOUNT_AUTH0_LOGOUT_URL:
|
||||
_default: ENC[AES256_GCM,data:IZFfi6pn+hy7g0wnEtP9TYHH1fNiC2gqgRHVdgm4C9smPerEvS0pq9dBwVY=,iv:BxbSInFQ6GE2loTv+IzdYr25PlyzdWZI1wdT6r+uvBg=,tag:psujCU372k59OGmlRdH9Fg==,type:str]
|
||||
_default: ENC[AES256_GCM,data:JkZ/gfdXn/8F5GDFROfR+8eMLGETzmkNhwcgCq118QThwTPYtLt6t4Wxa0fs5STJI/v15w8q+xAptHHUxtKJEg==,iv:I+XyuB8eZR+IpKIGmjLon1iFbJssVaDYpusQzjyRiec=,tag:APcnH6F4sM4uuRZdziUgQw==,type:str]
|
||||
RE_CAPTCHA_SECRET_KEY:
|
||||
_default: ENC[AES256_GCM,data:e9jfs4PwY+UPk0EuXZxERylKUFSUo6zsAz8oly3YwwybipP+A5rSBQ==,iv:TvrD/a+6+11IGDVFLUCn8U+H3v1YfIRrcndOVdt/taI=,tag:OAFVIznkeeeVX9UsigfP4A==,type:str]
|
||||
RE_CAPTCHA_CLIENT_KEY:
|
||||
@@ -155,8 +149,8 @@ sops:
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age: []
|
||||
lastmodified: "2023-05-09T16:11:07Z"
|
||||
mac: ENC[AES256_GCM,data:JHnHHGNOw84O5No7UDTLSGnLwUJHUUZaY/UBPetpa99OFDMlzFvEnjIh4DjIqhVLXR5DBZJr7BTibQLyo6y/aXlWKDxHvLklLGunKQaQ1Xl/JzDKRTYlAs/JsJRR0ElzzyPmbCKbSJOX0tAtNAuhH6EFKYg+HLYmPHtYcvM1Kck=,iv:OsKv7efW0xJ4ZT/38Uyq0feqVC4i2x4srzG+mqd2Avg=,tag:MWfjO8r3iJSMGluqIHc9jA==,type:str]
|
||||
lastmodified: "2023-05-10T12:56:02Z"
|
||||
mac: ENC[AES256_GCM,data:s/AX12T/dTpTcStmR5lcOtUw6AngvauXdDt+56A3wMoJwWSeWm5lVkVK9d/moAZgmuMv5Uji6pVVzohp8QFAoyQvGJdd8eFUqaYDkm6oGkuJidzLGq9qsbJL89bQ7R2hZ7A8JlamgXE0jJmkgsdKQlkjOH2gMdfikaSujPY1kUM=,iv:HKCiMhApXmEfNiUR0oEXupG/atiw3xhEckCRl63AHBE=,tag:76KiFG2GuwRz7Cn3/CO4/g==,type:str]
|
||||
pgp:
|
||||
- created_at: "2022-09-14T13:42:28Z"
|
||||
enc: |
|
||||
|
||||
@@ -359,7 +359,7 @@ frontend:
|
||||
NEXT_PUBLIC_NETWORK_RPC_URL:
|
||||
_default: https://rpc.ankr.com/eth_goerli
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS:
|
||||
_default: "['daily_txs','coin_price','market_cup']"
|
||||
_default: "['daily_txs','coin_price','market_cap']"
|
||||
NEXT_PUBLIC_API_SPEC_URL:
|
||||
_default: https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
|
||||
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL:
|
||||
|
||||
@@ -126,7 +126,7 @@ frontend:
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS:
|
||||
_default: "['daily_txs']"
|
||||
NEXT_PUBLIC_IS_TESTNET:
|
||||
_default: "true"
|
||||
_default: true
|
||||
NEXT_PUBLIC_VISUALIZE_API_HOST:
|
||||
_default: https://visualizer-optimism-goerli.test.aws-k8s.blockscout.com
|
||||
NEXT_PUBLIC_CONTRACT_INFO_API_HOST:
|
||||
|
||||
@@ -114,7 +114,7 @@ frontend:
|
||||
_default: https://blockscout.com/auth/logout
|
||||
review: blockscout-main.test.aws-k8s.blockscout.com/auth/logout
|
||||
NEXT_PUBLIC_HOMEPAGE_CHARTS:
|
||||
_default: "['daily_txs','coin_price','market_cup']"
|
||||
_default: "['daily_txs','coin_price','market_cap']"
|
||||
NEXT_PUBLIC_APP_HOST:
|
||||
_default: blockscout.com
|
||||
NEXT_PUBLIC_NETWORK_RPC_URL:
|
||||
|
||||
@@ -41,7 +41,7 @@ The app instance could be customized by passing following variables to NodeJS en
|
||||
| NEXT_PUBLIC_AUTH_URL | `string` | Account auth base url; the login path (`/auth/auth0`) will be added to it at execution time. Required if account is supported for the app instance. | - | - | `https://blockscout.com` |
|
||||
| NEXT_PUBLIC_LOGOUT_URL | `string` | Account logout url. Required if account is supported for the app instance. | - | - | `https://blockscoutcom.us.auth0.com/v2/logout` |
|
||||
| NEXT_PUBLIC_LOGOUT_RETURN_URL | `string` | Account logout return url. Required if account is supported for the app instance. | - | - | `https://blockscout.com/poa/core/auth/logout` |
|
||||
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cup'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cup']` |
|
||||
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cap'>` | List of charts displayed on the home page | - | - | `['daily_txs','coin_price','market_cap']` |
|
||||
| NEXT_PUBLIC_HOMEPAGE_PLATE_TEXT_COLOR | `string` | Text color of the hero plate on the homepage (escape "#" symbol if you use HEX color codes) | `\#FFFFFF \| rgb(220, 254, 118)` | `\#DCFE76` |
|
||||
| NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT | `string` | Gradient value for hero plate on the homepage (escape "#" symbol if you use HEX color codes) | - | `radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%)` | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 72% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)` |
|
||||
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` | Set to false if network doesn't have gas tracker | - | `true` | `false` |
|
||||
|
||||
@@ -51,11 +51,11 @@ import type {
|
||||
} from 'types/api/token';
|
||||
import type { TokensResponse, TokensFilters, TokenInstanceTransferResponse } from 'types/api/tokens';
|
||||
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
|
||||
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
|
||||
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction, TransactionsResponseWatchlist } from 'types/api/transaction';
|
||||
import type { TTxsFilters } from 'types/api/txsFilters';
|
||||
import type { TxStateChanges } from 'types/api/txStateChanges';
|
||||
import type { VisualizedContract } from 'types/api/visualization';
|
||||
import type { WithdrawalsResponse } from 'types/api/withdrawals';
|
||||
import type { WithdrawalsResponse, WithdrawalsCounters } from 'types/api/withdrawals';
|
||||
import type { ArrayElement } from 'types/utils';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
@@ -186,6 +186,11 @@ export const RESOURCES = {
|
||||
paginationFields: [ 'filter' as const, 'hash' as const, 'inserted_at' as const ],
|
||||
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
|
||||
},
|
||||
txs_watchlist: {
|
||||
path: '/api/v2/transactions/watchlist',
|
||||
paginationFields: [ 'filter' as const, 'hash' as const, 'inserted_at' as const ],
|
||||
filterFields: [ ],
|
||||
},
|
||||
tx: {
|
||||
path: '/api/v2/transactions/:hash',
|
||||
pathParams: [ 'hash' as const ],
|
||||
@@ -221,6 +226,9 @@ export const RESOURCES = {
|
||||
paginationFields: [ 'index' as const, 'items_count' as const ],
|
||||
filterFields: [],
|
||||
},
|
||||
withdrawals_counters: {
|
||||
path: '/api/v2/withdrawals/counters',
|
||||
},
|
||||
|
||||
// ADDRESSES
|
||||
addresses: {
|
||||
@@ -417,6 +425,9 @@ export const RESOURCES = {
|
||||
homepage_txs: {
|
||||
path: '/api/v2/main-page/transactions',
|
||||
},
|
||||
homepage_txs_watchlist: {
|
||||
path: '/api/v2/main-page/transactions/watchlist',
|
||||
},
|
||||
homepage_indexing_status: {
|
||||
path: '/api/v2/main-page/indexing-status',
|
||||
},
|
||||
@@ -537,7 +548,7 @@ export interface ResourceError<T = unknown> {
|
||||
export type ResourceErrorAccount<T> = ResourceError<{ errors: T }>
|
||||
|
||||
export type PaginatedResources = 'blocks' | 'block_txs' |
|
||||
'txs_validated' | 'txs_pending' |
|
||||
'txs_validated' | 'txs_pending' | 'txs_watchlist' |
|
||||
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' |
|
||||
'addresses' |
|
||||
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
|
||||
@@ -568,6 +579,7 @@ Q extends 'homepage_chart_txs' ? ChartTransactionResponse :
|
||||
Q extends 'homepage_chart_market' ? ChartMarketResponse :
|
||||
Q extends 'homepage_blocks' ? Array<Block> :
|
||||
Q extends 'homepage_txs' ? Array<Transaction> :
|
||||
Q extends 'homepage_txs_watchlist' ? Array<Transaction> :
|
||||
Q extends 'homepage_deposits' ? Array<L2DepositsItem> :
|
||||
Q extends 'homepage_indexing_status' ? IndexingStatus :
|
||||
Q extends 'stats_counters' ? Counters :
|
||||
@@ -579,6 +591,7 @@ Q extends 'block_txs' ? BlockTransactionsResponse :
|
||||
Q extends 'block_withdrawals' ? BlockWithdrawalsResponse :
|
||||
Q extends 'txs_validated' ? TransactionsResponseValidated :
|
||||
Q extends 'txs_pending' ? TransactionsResponsePending :
|
||||
Q extends 'txs_watchlist' ? TransactionsResponseWatchlist :
|
||||
Q extends 'tx' ? Transaction :
|
||||
Q extends 'tx_internal_txs' ? InternalTransactionsResponse :
|
||||
Q extends 'tx_logs' ? LogsResponseTx :
|
||||
@@ -620,6 +633,7 @@ Q extends 'verified_contracts_counters' ? VerifiedContractsCounters :
|
||||
Q extends 'visualize_sol2uml' ? VisualizedContract :
|
||||
Q extends 'contract_verification_config' ? SmartContractVerificationConfig :
|
||||
Q extends 'withdrawals' ? WithdrawalsResponse :
|
||||
Q extends 'withdrawals_counters' ? WithdrawalsCounters :
|
||||
Q extends 'l2_output_roots' ? L2OutputRootsResponse :
|
||||
Q extends 'l2_withdrawals' ? L2WithdrawalsResponse :
|
||||
Q extends 'l2_deposits' ? L2DepositsResponse :
|
||||
|
||||
@@ -32,6 +32,7 @@ export function app(): CspDev.DirectiveDescriptor {
|
||||
appConfig.api.endpoint,
|
||||
appConfig.api.socket,
|
||||
appConfig.statsApi.endpoint,
|
||||
appConfig.visualizeApi.endpoint,
|
||||
|
||||
// chain RPC server
|
||||
appConfig.network.rpcUrl,
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import appConfig from 'configs/app/config';
|
||||
import { useAppContext } from 'lib/appContext';
|
||||
import * as cookies from 'lib/cookies';
|
||||
|
||||
export default function useHasAccount() {
|
||||
const appProps = useAppContext();
|
||||
|
||||
if (!appConfig.isAccountSupported) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cookiesString = appProps.cookies;
|
||||
const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, cookiesString));
|
||||
return hasAuth;
|
||||
}
|
||||
@@ -198,7 +198,7 @@ export default function useNavItems(): ReturnType {
|
||||
|
||||
const accountNavItems = [
|
||||
{
|
||||
text: 'Watchlist',
|
||||
text: 'Watch list',
|
||||
nextRoute: { pathname: '/account/watchlist' as const },
|
||||
icon: watchlistIcon,
|
||||
isActive: pathname === '/account/watchlist',
|
||||
|
||||
@@ -17,7 +17,9 @@ export function middleware(req: NextRequest) {
|
||||
}
|
||||
|
||||
// we don't have any info from router here, so just do straight forward sub-string search (sorry)
|
||||
const isAccountRoute = req.nextUrl.pathname.includes('/account/');
|
||||
const isAccountRoute =
|
||||
req.nextUrl.pathname.includes('/account/') ||
|
||||
(req.nextUrl.pathname === '/txs' && req.nextUrl.searchParams.get('tab') === 'watchlist');
|
||||
const isProfileRoute = req.nextUrl.pathname.includes('/auth/profile');
|
||||
const apiToken = req.cookies.get(NAMES.API_TOKEN);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AddressTokenBalance } from 'types/api/address';
|
||||
|
||||
import * as tokens from 'mocks/tokens/tokenInfo';
|
||||
import * as tokenInstance from 'mocks/tokens/tokenInstance';
|
||||
|
||||
export const erc20a: AddressTokenBalance = {
|
||||
token: tokens.tokenInfoERC20a,
|
||||
@@ -59,24 +60,28 @@ export const erc721LongSymbol: AddressTokenBalance = {
|
||||
export const erc1155a: AddressTokenBalance = {
|
||||
token: tokens.tokenInfoERC1155a,
|
||||
token_id: '42',
|
||||
token_instance: tokenInstance.base,
|
||||
value: '24',
|
||||
};
|
||||
|
||||
export const erc1155b: AddressTokenBalance = {
|
||||
token: tokens.tokenInfoERC1155b,
|
||||
token_id: '100010000000001',
|
||||
token_instance: tokenInstance.base,
|
||||
value: '11',
|
||||
};
|
||||
|
||||
export const erc1155withoutName: AddressTokenBalance = {
|
||||
token: tokens.tokenInfoERC1155WithoutName,
|
||||
token_id: '64532245',
|
||||
token_instance: tokenInstance.base,
|
||||
value: '42',
|
||||
};
|
||||
|
||||
export const erc1155LongId: AddressTokenBalance = {
|
||||
token: tokens.tokenInfoERC1155b,
|
||||
token_id: '483200961027732618117991942553110860267520',
|
||||
token_instance: tokenInstance.base,
|
||||
value: '42',
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export const data = {
|
||||
items: [
|
||||
{
|
||||
amount: '192175',
|
||||
amount: '192175000000000',
|
||||
block_number: 43242,
|
||||
index: 11688,
|
||||
receiver: {
|
||||
@@ -15,7 +15,7 @@ export const data = {
|
||||
validator_index: 49622,
|
||||
},
|
||||
{
|
||||
amount: '192175',
|
||||
amount: '192175000000000',
|
||||
block_number: 43242,
|
||||
index: 11687,
|
||||
receiver: {
|
||||
@@ -29,7 +29,7 @@ export const data = {
|
||||
validator_index: 49621,
|
||||
},
|
||||
{
|
||||
amount: '182773',
|
||||
amount: '182773000000000',
|
||||
block_number: 43242,
|
||||
index: 11686,
|
||||
receiver: {
|
||||
|
||||
@@ -5,7 +5,7 @@ import React from 'react';
|
||||
import MarketplaceApp from 'ui/pages/MarketplaceApp';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
|
||||
const AppPage: NextPage = () => {
|
||||
const MarketplaceAppPage: NextPage = () => {
|
||||
return (
|
||||
<Page wrapChildren={ false }>
|
||||
<Head><title>Blockscout | Marketplace</title></Head>
|
||||
@@ -14,6 +14,6 @@ const AppPage: NextPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AppPage;
|
||||
export default MarketplaceAppPage;
|
||||
|
||||
export { getServerSideProps } from 'lib/next/getServerSideProps';
|
||||
|
||||
@@ -2,21 +2,21 @@ import type { NextPage } from 'next';
|
||||
import Head from 'next/head';
|
||||
import React from 'react';
|
||||
|
||||
import Apps from 'ui/pages/Apps';
|
||||
import Marketplace from 'ui/pages/Marketplace';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
|
||||
const AppsPage: NextPage = () => {
|
||||
const MarketplacePage: NextPage = () => {
|
||||
return (
|
||||
<Page>
|
||||
<PageTitle text="Apps"/>
|
||||
<PageTitle text="Marketplace"/>
|
||||
<Head><title>Blockscout | Marketplace</title></Head>
|
||||
|
||||
<Apps/>
|
||||
<Marketplace/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppsPage;
|
||||
export default MarketplacePage;
|
||||
|
||||
export { getServerSideProps } from 'lib/next/getServerSideProps';
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Transaction } from 'types/api/transaction';
|
||||
import type { AddressTag, WatchlistName } from './addressParams';
|
||||
import type { Block } from './block';
|
||||
import type { InternalTransaction } from './internalTransaction';
|
||||
import type { TokenInfo, TokenType } from './token';
|
||||
import type { TokenInfo, TokenInstance, TokenType } from './token';
|
||||
import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer';
|
||||
|
||||
export interface Address {
|
||||
@@ -48,6 +48,7 @@ export interface AddressTokenBalance {
|
||||
token: TokenInfo;
|
||||
token_id: string | null;
|
||||
value: string;
|
||||
token_instance?: TokenInstance;
|
||||
}
|
||||
|
||||
export interface AddressTokensResponse {
|
||||
|
||||
@@ -70,6 +70,15 @@ export interface TransactionsResponsePending {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface TransactionsResponseWatchlist {
|
||||
items: Array<Transaction>;
|
||||
next_page_params: {
|
||||
inserted_at: string;
|
||||
hash: string;
|
||||
filter: 'pending';
|
||||
} | null;
|
||||
}
|
||||
|
||||
export type TransactionType = 'token_transfer' | 'contract_creation' | 'contract_call' | 'token_creation' | 'coin_transfer'
|
||||
|
||||
export type TxsResponse = TransactionsResponseValidated | TransactionsResponsePending | BlockTransactionsResponse;
|
||||
|
||||
@@ -16,3 +16,8 @@ export type WithdrawalsItem = {
|
||||
timestamp: string;
|
||||
validator_index: number;
|
||||
}
|
||||
|
||||
export type WithdrawalsCounters = {
|
||||
withdrawal_count: string;
|
||||
withdrawal_sum: string;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
export type AppItemPreview = {
|
||||
export type MarketplaceAppPreview = {
|
||||
id: string;
|
||||
external?: boolean;
|
||||
title: string;
|
||||
logo: string;
|
||||
logoDarkMode?: string;
|
||||
shortDescription: string;
|
||||
categories: Array<string>;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type AppItemOverview = AppItemPreview & {
|
||||
export type MarketplaceAppOverview = MarketplaceAppPreview & {
|
||||
author: string;
|
||||
description: string;
|
||||
site?: string;
|
||||
@@ -17,7 +18,7 @@ export type AppItemOverview = AppItemPreview & {
|
||||
github?: string;
|
||||
}
|
||||
|
||||
export enum AppCategory {
|
||||
export enum MarketplaceCategory {
|
||||
ALL = 'All apps',
|
||||
FAVORITES = 'Favorites',
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { RoutedSubTab } from 'ui/shared/RoutedTabs/types';
|
||||
import type { RoutedSubTab } from 'ui/shared/Tabs/types';
|
||||
|
||||
import { ContractContextProvider } from 'ui/address/contract/context';
|
||||
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
|
||||
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
|
||||
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -9,7 +9,7 @@ import useQueryWithPages from 'lib/hooks/useQueryWithPages';
|
||||
import { tokenTabsByType } from 'ui/pages/Address';
|
||||
import Pagination from 'ui/shared/Pagination';
|
||||
import type { Props as PaginationProps } from 'ui/shared/Pagination';
|
||||
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
|
||||
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
|
||||
|
||||
import ERC1155Tokens from './tokens/ERC1155Tokens';
|
||||
import ERC20Tokens from './tokens/ERC20Tokens';
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
@@ -4,13 +4,13 @@ import React from 'react';
|
||||
|
||||
import type { AddressTokenBalance } from 'types/api/address';
|
||||
|
||||
import NftImage from 'ui/shared/nft/NftImage';
|
||||
import NftMedia from 'ui/shared/nft/NftMedia';
|
||||
import TokenLogo from 'ui/shared/TokenLogo';
|
||||
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
|
||||
|
||||
type Props = AddressTokenBalance;
|
||||
|
||||
const NFTItem = ({ token, token_id: tokenId }: Props) => {
|
||||
const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance }: Props) => {
|
||||
const tokenLink = route({ pathname: '/token/[hash]', query: { hash: token.address } });
|
||||
|
||||
return (
|
||||
@@ -26,11 +26,10 @@ const NFTItem = ({ token, token_id: tokenId }: Props) => {
|
||||
lineHeight="20px"
|
||||
>
|
||||
<LinkOverlay href={ tokenLink }>
|
||||
<NftImage
|
||||
<NftMedia
|
||||
mb="18px"
|
||||
url={ null }
|
||||
fallbackPadding="30px"
|
||||
cursor="pointer"
|
||||
imageUrl={ tokenInstance?.image_url || null }
|
||||
animationUrl={ tokenInstance?.animation_url || null }
|
||||
/>
|
||||
</LinkOverlay>
|
||||
{ tokenId && (
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import _unique from 'lodash/uniq';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import type { AppItemOverview } from 'types/client/apps';
|
||||
import { AppCategory } from 'types/client/apps';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import type { ResourceError } from 'lib/api/resources';
|
||||
import useDebounce from 'lib/hooks/useDebounce';
|
||||
import useApiFetch from 'lib/hooks/useFetch';
|
||||
|
||||
const favoriteAppsLocalStorageKey = 'favoriteApps';
|
||||
|
||||
function getFavoriteApps() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(favoriteAppsLocalStorageKey) || '[]') as Array<string>;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function isAppNameMatches(q: string, app: AppItemOverview) {
|
||||
return app.title.toLowerCase().includes(q.toLowerCase());
|
||||
}
|
||||
|
||||
function isAppCategoryMatches(category: string, app: AppItemOverview, favoriteApps: Array<string>) {
|
||||
return category === AppCategory.ALL ||
|
||||
(category === AppCategory.FAVORITES && favoriteApps.includes(app.id)) ||
|
||||
app.categories.includes(category);
|
||||
}
|
||||
|
||||
export default function useMarketplaceApps() {
|
||||
const [ selectedAppId, setSelectedAppId ] = useState<string | null>(null);
|
||||
const [ selectedCategoryId, setSelectedCategoryId ] = useState<string>(AppCategory.ALL);
|
||||
const [ filterQuery, setFilterQuery ] = useState('');
|
||||
const [ favoriteApps, setFavoriteApps ] = useState<Array<string>>([]);
|
||||
|
||||
const apiFetch = useApiFetch();
|
||||
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<AppItemOverview>>(
|
||||
[ 'marketplace-apps' ],
|
||||
async() => apiFetch(appConfig.marketplaceConfigUrl || ''),
|
||||
{
|
||||
select: (data) => (data as Array<AppItemOverview>).sort((a, b) => a.title.localeCompare(b.title)),
|
||||
});
|
||||
|
||||
const handleFavoriteClick = useCallback((id: string, isFavorite: boolean) => {
|
||||
const favoriteApps = getFavoriteApps();
|
||||
|
||||
if (isFavorite) {
|
||||
const result = favoriteApps.filter((appId: string) => appId !== id);
|
||||
setFavoriteApps(result);
|
||||
localStorage.setItem(favoriteAppsLocalStorageKey, JSON.stringify(result));
|
||||
} else {
|
||||
favoriteApps.push(id);
|
||||
localStorage.setItem(favoriteAppsLocalStorageKey, JSON.stringify(favoriteApps));
|
||||
setFavoriteApps(favoriteApps);
|
||||
}
|
||||
}, [ ]);
|
||||
|
||||
const showAppInfo = useCallback((id: string) => {
|
||||
setSelectedAppId(id);
|
||||
}, []);
|
||||
|
||||
const debouncedFilterQuery = useDebounce(filterQuery, 500);
|
||||
const clearSelectedAppId = useCallback(() => setSelectedAppId(null), []);
|
||||
|
||||
const handleCategoryChange = useCallback((newCategory: string) => {
|
||||
setSelectedCategoryId(newCategory);
|
||||
}, []);
|
||||
|
||||
const displayedApps = React.useMemo(() => {
|
||||
return data?.filter(app => isAppNameMatches(debouncedFilterQuery, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps)) || [];
|
||||
}, [ selectedCategoryId, data, debouncedFilterQuery, favoriteApps ]);
|
||||
|
||||
const categories = React.useMemo(() => {
|
||||
return _unique(data?.map(app => app.categories).flat()) || [];
|
||||
}, [ data ]);
|
||||
|
||||
useEffect(() => {
|
||||
setFavoriteApps(getFavoriteApps());
|
||||
}, [ ]);
|
||||
|
||||
return React.useMemo(() => ({
|
||||
selectedCategoryId,
|
||||
onCategoryChange: handleCategoryChange,
|
||||
onSearchInputChange: setFilterQuery,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
categories,
|
||||
displayedApps,
|
||||
showAppInfo,
|
||||
selectedAppId,
|
||||
clearSelectedAppId,
|
||||
favoriteApps,
|
||||
onFavoriteClick: handleFavoriteClick,
|
||||
}), [
|
||||
selectedCategoryId,
|
||||
categories,
|
||||
clearSelectedAppId,
|
||||
selectedAppId,
|
||||
displayedApps,
|
||||
error,
|
||||
favoriteApps,
|
||||
handleCategoryChange,
|
||||
handleFavoriteClick,
|
||||
isError,
|
||||
isLoading,
|
||||
showAppInfo,
|
||||
]);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import { test as base, expect } from '@playwright/experimental-ct-react';
|
||||
import { test as base, expect, devices } from '@playwright/experimental-ct-react';
|
||||
import React from 'react';
|
||||
|
||||
import * as statsMock from 'mocks/stats/index';
|
||||
import * as txMock from 'mocks/txs/tx';
|
||||
import * as socketServer from 'playwright/fixtures/socketServer';
|
||||
import TestApp from 'playwright/TestApp';
|
||||
@@ -13,11 +12,29 @@ export const test = base.extend<socketServer.SocketServerFixture>({
|
||||
createSocket: socketServer.createSocket,
|
||||
});
|
||||
|
||||
test('default view +@mobile +@dark-mode', async({ mount, page }) => {
|
||||
await page.route(buildApiUrl('homepage_stats'), (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(statsMock.base),
|
||||
}));
|
||||
test.describe('mobile', () => {
|
||||
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
|
||||
test('default view', async({ mount, page }) => {
|
||||
await page.route(buildApiUrl('homepage_txs'), (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify([
|
||||
txMock.base,
|
||||
txMock.withContractCreation,
|
||||
txMock.withTokenTransfer,
|
||||
]),
|
||||
}));
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<LatestTxs/>
|
||||
</TestApp>,
|
||||
);
|
||||
|
||||
await expect(component).toHaveScreenshot();
|
||||
});
|
||||
});
|
||||
|
||||
test('default view +@dark-mode', async({ mount, page }) => {
|
||||
await page.route(buildApiUrl('homepage_txs'), (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify([
|
||||
@@ -47,10 +64,6 @@ test.describe('socket', () => {
|
||||
};
|
||||
|
||||
test('new item', async({ mount, page, createSocket }) => {
|
||||
await page.route(buildApiUrl('homepage_stats'), (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(statsMock.base),
|
||||
}));
|
||||
await page.route(buildApiUrl('homepage_txs'), (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify([
|
||||
|
||||
@@ -11,6 +11,7 @@ const LatestTxsItemSkeleton = () => {
|
||||
return (
|
||||
<Box
|
||||
width="100%"
|
||||
minW="700px"
|
||||
borderTop="1px solid"
|
||||
borderColor="divider"
|
||||
py={ 4 }
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Box, Flex, Text } from '@chakra-ui/react';
|
||||
import { route } from 'nextjs-routes';
|
||||
import React from 'react';
|
||||
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
|
||||
import LinkInternal from 'ui/shared/LinkInternal';
|
||||
|
||||
import LatestTxsItem from './LatestTxsItem';
|
||||
import LatestTxsItemSkeleton from './LatestTxsItemSkeleton';
|
||||
|
||||
const LatestWatchlistTxs = () => {
|
||||
useRedirectForInvalidAuthToken();
|
||||
const isMobile = useIsMobile();
|
||||
const txsCount = isMobile ? 2 : 6;
|
||||
const { data, isLoading, isError } = useApiQuery('homepage_txs_watchlist');
|
||||
|
||||
if (isLoading) {
|
||||
return <>{ Array.from(Array(txsCount)).map((item, index) => <LatestTxsItemSkeleton key={ index }/>) }</>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <Text mt={ 4 }>No data. Please reload page.</Text>;
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return <Text mt={ 4 }>There are no transactions.</Text>;
|
||||
}
|
||||
|
||||
if (data) {
|
||||
const txsUrl = route({ pathname: '/txs', query: { tab: 'watchlist' } });
|
||||
return (
|
||||
<>
|
||||
<Box mb={{ base: 3, lg: 4 }}>
|
||||
{ data.slice(0, txsCount).map((tx => <LatestTxsItem key={ tx.hash } tx={ tx }/>)) }
|
||||
</Box>
|
||||
<Flex justifyContent="center">
|
||||
<LinkInternal fontSize="sm" href={ txsUrl }>View all watch list transactions</LinkInternal>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default LatestWatchlistTxs;
|
||||
@@ -1,29 +1,25 @@
|
||||
import { Heading, Tab, Tabs, TabList, TabPanel, TabPanels } from '@chakra-ui/react';
|
||||
import { Heading } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import useHasAccount from 'lib/hooks/useHasAccount';
|
||||
import LatestDeposits from 'ui/home/LatestDeposits';
|
||||
import LatestTxs from 'ui/home/LatestTxs';
|
||||
import LatestWatchlistTxs from 'ui/home/LatestWatchlistTxs';
|
||||
import TabsWithScroll from 'ui/shared/Tabs/TabsWithScroll';
|
||||
|
||||
const TransactionsHome = () => {
|
||||
if (appConfig.L2.isL2Network) {
|
||||
const hasAccount = useHasAccount();
|
||||
if (appConfig.L2.isL2Network || hasAccount) {
|
||||
const tabs = [
|
||||
{ id: 'txn', title: 'Latest txn', component: <LatestTxs/> },
|
||||
appConfig.L2.isL2Network && { id: 'deposits', title: 'Deposits (L1→L2 txn)', component: <LatestDeposits/> },
|
||||
hasAccount && { id: 'watchlist', title: 'Watch list', component: <LatestWatchlistTxs/> },
|
||||
].filter(Boolean);
|
||||
return (
|
||||
<>
|
||||
<Heading as="h4" size="sm" mb={ 4 }>Transactions</Heading>
|
||||
<Tabs isLazy lazyBehavior="keepMounted" defaultIndex={ 0 } variant="soft-rounded">
|
||||
<TabList>
|
||||
<Tab key="txn">Latest txn</Tab>
|
||||
<Tab key="deposits">Deposits (L1→L2 txn)</Tab>
|
||||
</TabList>
|
||||
<TabPanels mt={ 4 }>
|
||||
<TabPanel key="txn" p={ 0 }>
|
||||
<LatestTxs/>
|
||||
</TabPanel>
|
||||
<TabPanel key="deposits" p={ 0 }>
|
||||
<LatestDeposits/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<TabsWithScroll tabs={ tabs } lazyBehavior="keepMounted"/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 30 KiB |
@@ -5,7 +5,7 @@ import type { ResourcePayload } from 'lib/api/resources';
|
||||
|
||||
export type ChartsResources = 'homepage_chart_txs' | 'homepage_chart_market';
|
||||
|
||||
export type ChainIndicatorId = 'daily_txs' | 'coin_price' | 'market_cup';
|
||||
export type ChainIndicatorId = 'daily_txs' | 'coin_price' | 'market_cap';
|
||||
|
||||
export interface TChainIndicator<R extends ChartsResources> {
|
||||
id: ChainIndicatorId;
|
||||
|
||||
@@ -46,7 +46,7 @@ const coinPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
|
||||
};
|
||||
|
||||
const marketPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
|
||||
id: 'market_cup',
|
||||
id: 'market_cap',
|
||||
title: 'Market cap',
|
||||
value: (stats) => '$' + Number(stats.market_cap).toLocaleString(undefined, { maximumFractionDigits: 0, notation: 'compact' }),
|
||||
icon: <Icon as={ globeIcon } boxSize={ 6 } bgColor="#6A5DCC" borderRadius="base" color="white"/>,
|
||||
|
||||
@@ -2,26 +2,27 @@ import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, Text, useColorMod
|
||||
import type { MouseEvent } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import type { AppItemPreview } from 'types/client/apps';
|
||||
import type { MarketplaceAppPreview } from 'types/client/marketplace';
|
||||
|
||||
import northEastIcon from 'icons/arrows/north-east.svg';
|
||||
import starFilledIcon from 'icons/star_filled.svg';
|
||||
import starOutlineIcon from 'icons/star_outline.svg';
|
||||
|
||||
import AppCardLink from './AppCardLink';
|
||||
import MarketplaceAppCardLink from './MarketplaceAppCardLink';
|
||||
|
||||
interface Props extends AppItemPreview {
|
||||
interface Props extends MarketplaceAppPreview {
|
||||
onInfoClick: (id: string) => void;
|
||||
isFavorite: boolean;
|
||||
onFavoriteClick: (id: string, isFavorite: boolean) => void;
|
||||
}
|
||||
|
||||
const AppCard = ({
|
||||
const MarketplaceAppCard = ({
|
||||
id,
|
||||
url,
|
||||
external,
|
||||
title,
|
||||
logo,
|
||||
logoDarkMode,
|
||||
shortDescription,
|
||||
categories,
|
||||
onInfoClick,
|
||||
@@ -39,6 +40,8 @@ const AppCard = ({
|
||||
onFavoriteClick(id, isFavorite);
|
||||
}, [ onFavoriteClick, id, isFavorite ]);
|
||||
|
||||
const logoUrl = useColorModeValue(logo, logoDarkMode || logo);
|
||||
|
||||
return (
|
||||
<LinkBox
|
||||
_hover={{
|
||||
@@ -73,7 +76,7 @@ const AppCard = ({
|
||||
justifyContent="center"
|
||||
>
|
||||
<Image
|
||||
src={ logo }
|
||||
src={ logoUrl }
|
||||
alt={ `${ title } app icon` }
|
||||
/>
|
||||
</Box>
|
||||
@@ -85,7 +88,7 @@ const AppCard = ({
|
||||
size={{ base: 'xs', sm: 'sm' }}
|
||||
fontWeight="semibold"
|
||||
>
|
||||
<AppCardLink
|
||||
<MarketplaceAppCardLink
|
||||
id={ id }
|
||||
url={ url }
|
||||
external={ external }
|
||||
@@ -158,4 +161,4 @@ const AppCard = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AppCard);
|
||||
export default React.memo(MarketplaceAppCard);
|
||||
@@ -9,7 +9,7 @@ type Props = {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const AppLink = ({ url, external, id, title }: Props) => {
|
||||
const MarketplaceAppCardLink = ({ url, external, id, title }: Props) => {
|
||||
return external ? (
|
||||
<LinkOverlay href={ url } isExternal={ true }>
|
||||
{ title }
|
||||
@@ -23,4 +23,4 @@ const AppLink = ({ url, external, id, title }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AppLink;
|
||||
export default MarketplaceAppCardLink;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Heading, Skeleton, SkeletonCircle, useColorModeValue } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
export const AppCardSkeleton = () => {
|
||||
const MarketplaceAppCardSkeleton = () => {
|
||||
return (
|
||||
<Box
|
||||
borderRadius="md"
|
||||
@@ -43,3 +43,5 @@ export const AppCardSkeleton = () => {
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarketplaceAppCardSkeleton;
|
||||
@@ -1,10 +1,10 @@
|
||||
import {
|
||||
Box, Flex, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody,
|
||||
ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text,
|
||||
ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text, useColorModeValue,
|
||||
} from '@chakra-ui/react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import type { AppItemOverview } from 'types/client/apps';
|
||||
import type { MarketplaceAppOverview } from 'types/client/marketplace';
|
||||
|
||||
import linkIcon from 'icons/link.svg';
|
||||
import ghIcon from 'icons/social/git.svg';
|
||||
@@ -15,16 +15,16 @@ import starOutlineIcon from 'icons/star_outline.svg';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import { nbsp } from 'lib/html-entities';
|
||||
|
||||
import AppModalLink from './AppModalLink';
|
||||
import MarketplaceAppModalLink from './MarketplaceAppModalLink';
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
isFavorite: boolean;
|
||||
onFavoriteClick: (id: string, isFavorite: boolean) => void;
|
||||
data: AppItemOverview;
|
||||
data: MarketplaceAppOverview;
|
||||
}
|
||||
|
||||
const AppModal = ({
|
||||
const MarketplaceAppModal = ({
|
||||
onClose,
|
||||
isFavorite,
|
||||
onFavoriteClick,
|
||||
@@ -41,6 +41,7 @@ const AppModal = ({
|
||||
telegram,
|
||||
twitter,
|
||||
logo,
|
||||
logoDarkMode,
|
||||
categories,
|
||||
} = data;
|
||||
|
||||
@@ -64,6 +65,7 @@ const AppModal = ({
|
||||
}, [ onFavoriteClick, data.id, isFavorite ]);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const logoUrl = useColorModeValue(logo, logoDarkMode || logo);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -89,7 +91,7 @@ const AppModal = ({
|
||||
gridRow={{ base: '1 / 3', sm: '1 / 4' }}
|
||||
>
|
||||
<Image
|
||||
src={ logo }
|
||||
src={ logoUrl }
|
||||
alt={ `${ title } app icon` }
|
||||
/>
|
||||
</Flex>
|
||||
@@ -120,7 +122,7 @@ const AppModal = ({
|
||||
marginTop={{ base: 6, sm: 0 }}
|
||||
>
|
||||
<Box display="flex">
|
||||
<AppModalLink
|
||||
<MarketplaceAppModalLink
|
||||
id={ data.id }
|
||||
url={ url }
|
||||
external={ external }
|
||||
@@ -241,4 +243,4 @@ const AppModal = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default AppModal;
|
||||
export default MarketplaceAppModal;
|
||||
@@ -9,7 +9,7 @@ type Props = {
|
||||
title: string;
|
||||
}
|
||||
|
||||
const AppModalLink = ({ url, external, id }: Props) => {
|
||||
const MarketplaceAppModalLink = ({ url, external, id }: Props) => {
|
||||
const buttonProps = {
|
||||
size: 'sm',
|
||||
marginRight: 2,
|
||||
@@ -36,4 +36,4 @@ const AppModalLink = ({ url, external, id }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default AppModalLink;
|
||||
export default MarketplaceAppModalLink;
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Box, Button, Icon, Menu, MenuButton, MenuList } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AppCategory } from 'types/client/apps';
|
||||
import { MarketplaceCategory } from 'types/client/marketplace';
|
||||
|
||||
import eastMiniArrowIcon from 'icons/arrows/east-mini.svg';
|
||||
|
||||
import CategoriesMenuItem from './CategoriesMenuItem';
|
||||
import MarketplaceCategoriesMenuItem from './MarketplaceCategoriesMenuItem';
|
||||
|
||||
type Props = {
|
||||
categories: Array<string>;
|
||||
@@ -13,10 +13,10 @@ type Props = {
|
||||
onSelect: (category: string) => void;
|
||||
}
|
||||
|
||||
const CategoriesMenu = ({ selectedCategoryId, onSelect, categories }: Props) => {
|
||||
const MarketplaceCategoriesMenu = ({ selectedCategoryId, onSelect, categories }: Props) => {
|
||||
const options = React.useMemo(() => ([
|
||||
AppCategory.FAVORITES,
|
||||
AppCategory.ALL,
|
||||
MarketplaceCategory.FAVORITES,
|
||||
MarketplaceCategory.ALL,
|
||||
...categories,
|
||||
]), [ categories ]);
|
||||
|
||||
@@ -43,7 +43,7 @@ const CategoriesMenu = ({ selectedCategoryId, onSelect, categories }: Props) =>
|
||||
|
||||
<MenuList zIndex={ 3 }>
|
||||
{ options.map((category: string) => (
|
||||
<CategoriesMenuItem
|
||||
<MarketplaceCategoriesMenuItem
|
||||
key={ category }
|
||||
id={ category }
|
||||
onClick={ onSelect }
|
||||
@@ -54,4 +54,4 @@ const CategoriesMenu = ({ selectedCategoryId, onSelect, categories }: Props) =>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(CategoriesMenu);
|
||||
export default React.memo(MarketplaceCategoriesMenu);
|
||||
@@ -2,7 +2,7 @@ import { Icon, MenuItem } from '@chakra-ui/react';
|
||||
import type { FunctionComponent, SVGAttributes } from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { AppCategory } from 'types/client/apps';
|
||||
import { MarketplaceCategory } from 'types/client/marketplace';
|
||||
|
||||
import starFilledIcon from 'icons/star_filled.svg';
|
||||
|
||||
@@ -12,10 +12,10 @@ type Props = {
|
||||
}
|
||||
|
||||
const ICONS: Record<string, FunctionComponent<SVGAttributes<SVGElement>>> = {
|
||||
[AppCategory.FAVORITES]: starFilledIcon,
|
||||
[MarketplaceCategory.FAVORITES]: starFilledIcon,
|
||||
};
|
||||
|
||||
const CategoriesMenuItem = ({ id, onClick }: Props) => {
|
||||
const MarketplaceCategoriesMenuItem = ({ id, onClick }: Props) => {
|
||||
const handleSelection = useCallback(() => {
|
||||
onClick(id);
|
||||
}, [ id, onClick ]);
|
||||
@@ -33,4 +33,4 @@ const CategoriesMenuItem = ({ id, onClick }: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CategoriesMenuItem;
|
||||
export default MarketplaceCategoriesMenuItem;
|
||||
@@ -1,20 +1,21 @@
|
||||
import { Grid, GridItem } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { AppItemPreview } from 'types/client/apps';
|
||||
import type { MarketplaceAppPreview } from 'types/client/marketplace';
|
||||
|
||||
import { apos } from 'lib/html-entities';
|
||||
import AppCard from 'ui/apps/AppCard';
|
||||
import EmptySearchResult from 'ui/apps/EmptySearchResult';
|
||||
import EmptySearchResult from 'ui/shared/EmptySearchResult';
|
||||
|
||||
import MarketplaceAppCard from './MarketplaceAppCard';
|
||||
|
||||
type Props = {
|
||||
apps: Array<AppItemPreview>;
|
||||
apps: Array<MarketplaceAppPreview>;
|
||||
onAppClick: (id: string) => void;
|
||||
favoriteApps: Array<string>;
|
||||
onFavoriteClick: (id: string, isFavorite: boolean) => void;
|
||||
}
|
||||
|
||||
const AppList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Props) => {
|
||||
const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Props) => {
|
||||
return apps.length > 0 ? (
|
||||
<Grid
|
||||
templateColumns={{
|
||||
@@ -28,13 +29,14 @@ const AppList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Props) =>
|
||||
<GridItem
|
||||
key={ app.id }
|
||||
>
|
||||
<AppCard
|
||||
<MarketplaceAppCard
|
||||
onInfoClick={ onAppClick }
|
||||
id={ app.id }
|
||||
external={ app.external }
|
||||
url={ app.url }
|
||||
title={ app.title }
|
||||
logo={ app.logo }
|
||||
logoDarkMode={ app.logoDarkMode }
|
||||
shortDescription={ app.shortDescription }
|
||||
categories={ app.categories }
|
||||
isFavorite={ favoriteApps.includes(app.id) }
|
||||
@@ -48,4 +50,4 @@ const AppList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Props) =>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(AppList);
|
||||
export default React.memo(MarketplaceList);
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Grid, GridItem } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import { AppCardSkeleton } from 'ui/apps/AppCardSkeleton';
|
||||
import MarketplaceAppCardSkeleton from './MarketplaceAppCardSkeleton';
|
||||
|
||||
const applicationStubs = [ ...Array(12) ];
|
||||
|
||||
const AppListSkeleton = () => {
|
||||
const MarketplaceListSkeleton = () => {
|
||||
return (
|
||||
<Grid
|
||||
templateColumns={{
|
||||
@@ -19,11 +19,11 @@ const AppListSkeleton = () => {
|
||||
<GridItem
|
||||
key={ index }
|
||||
>
|
||||
<AppCardSkeleton/>
|
||||
<MarketplaceAppCardSkeleton/>
|
||||
</GridItem>
|
||||
)) }
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppListSkeleton;
|
||||
export default MarketplaceListSkeleton;
|
||||
@@ -0,0 +1,146 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import _pickBy from 'lodash/pickBy';
|
||||
import _unique from 'lodash/uniq';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { MarketplaceAppOverview } from 'types/client/marketplace';
|
||||
import { MarketplaceCategory } from 'types/client/marketplace';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import type { ResourceError } from 'lib/api/resources';
|
||||
import useDebounce from 'lib/hooks/useDebounce';
|
||||
import useApiFetch from 'lib/hooks/useFetch';
|
||||
import getQueryParamString from 'lib/router/getQueryParamString';
|
||||
|
||||
const favoriteAppsLocalStorageKey = 'favoriteApps';
|
||||
|
||||
function getFavoriteApps() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(favoriteAppsLocalStorageKey) || '[]') as Array<string>;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function isAppNameMatches(q: string, app: MarketplaceAppOverview) {
|
||||
return app.title.toLowerCase().includes(q.toLowerCase());
|
||||
}
|
||||
|
||||
function isAppCategoryMatches(category: string, app: MarketplaceAppOverview, favoriteApps: Array<string>) {
|
||||
return category === MarketplaceCategory.ALL ||
|
||||
(category === MarketplaceCategory.FAVORITES && favoriteApps.includes(app.id)) ||
|
||||
app.categories.includes(category);
|
||||
}
|
||||
|
||||
export default function useMarketplace() {
|
||||
const router = useRouter();
|
||||
const defaultCategoryId = getQueryParamString(router.query.category);
|
||||
const defaultFilterQuery = getQueryParamString(router.query.filter);
|
||||
|
||||
const [ selectedAppId, setSelectedAppId ] = React.useState<string | null>(null);
|
||||
const [ selectedCategoryId, setSelectedCategoryId ] = React.useState<string>(MarketplaceCategory.ALL);
|
||||
const [ filterQuery, setFilterQuery ] = React.useState(defaultFilterQuery);
|
||||
const [ favoriteApps, setFavoriteApps ] = React.useState<Array<string>>([]);
|
||||
|
||||
const apiFetch = useApiFetch();
|
||||
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>(
|
||||
[ 'marketplace-apps' ],
|
||||
async() => apiFetch(appConfig.marketplaceConfigUrl || ''),
|
||||
{
|
||||
select: (data) => (data as Array<MarketplaceAppOverview>).sort((a, b) => a.title.localeCompare(b.title)),
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
const handleFavoriteClick = React.useCallback((id: string, isFavorite: boolean) => {
|
||||
const favoriteApps = getFavoriteApps();
|
||||
|
||||
if (isFavorite) {
|
||||
const result = favoriteApps.filter((appId: string) => appId !== id);
|
||||
setFavoriteApps(result);
|
||||
localStorage.setItem(favoriteAppsLocalStorageKey, JSON.stringify(result));
|
||||
} else {
|
||||
favoriteApps.push(id);
|
||||
localStorage.setItem(favoriteAppsLocalStorageKey, JSON.stringify(favoriteApps));
|
||||
setFavoriteApps(favoriteApps);
|
||||
}
|
||||
}, [ ]);
|
||||
|
||||
const showAppInfo = React.useCallback((id: string) => {
|
||||
setSelectedAppId(id);
|
||||
}, []);
|
||||
|
||||
const debouncedFilterQuery = useDebounce(filterQuery, 500);
|
||||
const clearSelectedAppId = React.useCallback(() => setSelectedAppId(null), []);
|
||||
|
||||
const handleCategoryChange = React.useCallback((newCategory: string) => {
|
||||
setSelectedCategoryId(newCategory);
|
||||
}, []);
|
||||
|
||||
const displayedApps = React.useMemo(() => {
|
||||
return data?.filter(app => isAppNameMatches(debouncedFilterQuery, app) && isAppCategoryMatches(selectedCategoryId, app, favoriteApps)) || [];
|
||||
}, [ selectedCategoryId, data, debouncedFilterQuery, favoriteApps ]);
|
||||
|
||||
const categories = React.useMemo(() => {
|
||||
return _unique(data?.map(app => app.categories).flat()) || [];
|
||||
}, [ data ]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setFavoriteApps(getFavoriteApps());
|
||||
}, [ ]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isLoading && !isError) {
|
||||
const isValidDefaultCategory = categories.includes(defaultCategoryId);
|
||||
isValidDefaultCategory && setSelectedCategoryId(defaultCategoryId);
|
||||
}
|
||||
// run only when data is loaded
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ isLoading ]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const query = _pickBy({
|
||||
category: selectedCategoryId === MarketplaceCategory.ALL ? undefined : selectedCategoryId,
|
||||
filter: debouncedFilterQuery,
|
||||
}, Boolean);
|
||||
router.replace(
|
||||
{ pathname: '/apps', query },
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
// omit router in the deps because router.push() somehow modifies it
|
||||
// and we get infinite re-renders then
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [ debouncedFilterQuery, selectedCategoryId ]);
|
||||
|
||||
return React.useMemo(() => ({
|
||||
selectedCategoryId,
|
||||
onCategoryChange: handleCategoryChange,
|
||||
filterQuery: debouncedFilterQuery,
|
||||
onSearchInputChange: setFilterQuery,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
categories,
|
||||
displayedApps,
|
||||
showAppInfo,
|
||||
selectedAppId,
|
||||
clearSelectedAppId,
|
||||
favoriteApps,
|
||||
onFavoriteClick: handleFavoriteClick,
|
||||
}), [
|
||||
selectedCategoryId,
|
||||
categories,
|
||||
clearSelectedAppId,
|
||||
selectedAppId,
|
||||
displayedApps,
|
||||
error,
|
||||
favoriteApps,
|
||||
handleCategoryChange,
|
||||
handleFavoriteClick,
|
||||
isError,
|
||||
isLoading,
|
||||
showAppInfo,
|
||||
debouncedFilterQuery,
|
||||
]);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { TokenType } from 'types/api/token';
|
||||
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
|
||||
import type { RoutedTab } from 'ui/shared/Tabs/types';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import iconSuccess from 'icons/status/success.svg';
|
||||
@@ -24,8 +24,8 @@ import AddressWithdrawals from 'ui/address/AddressWithdrawals';
|
||||
import TextAd from 'ui/shared/ad/TextAd';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
|
||||
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
|
||||
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
|
||||
|
||||
export const tokenTabsByType: Record<TokenType, string> = {
|
||||
'ERC-20': 'tokens_erc20',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Skeleton } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
|
||||
import type { RoutedTab } from 'ui/shared/Tabs/types';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
@@ -16,8 +16,8 @@ import TextAd from 'ui/shared/ad/TextAd';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import type { Props as PaginationProps } from 'ui/shared/Pagination';
|
||||
import Pagination from 'ui/shared/Pagination';
|
||||
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
|
||||
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
|
||||
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
|
||||
import TxsContent from 'ui/txs/TxsContent';
|
||||
|
||||
const TAB_LIST_PROPS = {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { BlockType } from 'types/api/block';
|
||||
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
|
||||
import type { RoutedTab } from 'ui/shared/Tabs/types';
|
||||
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
|
||||
@@ -10,7 +10,7 @@ import BlocksContent from 'ui/blocks/BlocksContent';
|
||||
import BlocksTabSlot from 'ui/blocks/BlocksTabSlot';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
|
||||
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
|
||||
|
||||
const TAB_TO_TYPE: Record<string, BlockType> = {
|
||||
blocks: 'block',
|
||||
|
||||
@@ -3,15 +3,15 @@ import React from 'react';
|
||||
|
||||
import config from 'configs/app/config';
|
||||
import PlusIcon from 'icons/plus.svg';
|
||||
import AppList from 'ui/apps/AppList';
|
||||
import AppListSkeleton from 'ui/apps/AppListSkeleton';
|
||||
import AppModal from 'ui/apps/AppModal';
|
||||
import CategoriesMenu from 'ui/apps/CategoriesMenu';
|
||||
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
|
||||
import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu';
|
||||
import MarketplaceList from 'ui/marketplace/MarketplaceList';
|
||||
import MarketplaceListSkeleton from 'ui/marketplace/MarketplaceListSkeleton';
|
||||
import FilterInput from 'ui/shared/filters/FilterInput';
|
||||
|
||||
import useMarketplaceApps from '../apps/useMarketplaceApps';
|
||||
import useMarketplace from '../marketplace/useMarketplace';
|
||||
|
||||
const Apps = () => {
|
||||
const Marketplace = () => {
|
||||
const {
|
||||
isLoading,
|
||||
isError,
|
||||
@@ -19,6 +19,7 @@ const Apps = () => {
|
||||
selectedCategoryId,
|
||||
categories,
|
||||
onCategoryChange,
|
||||
filterQuery,
|
||||
onSearchInputChange,
|
||||
showAppInfo,
|
||||
displayedApps,
|
||||
@@ -26,7 +27,7 @@ const Apps = () => {
|
||||
clearSelectedAppId,
|
||||
favoriteApps,
|
||||
onFavoriteClick,
|
||||
} = useMarketplaceApps();
|
||||
} = useMarketplace();
|
||||
|
||||
if (isError) {
|
||||
throw new Error('Unable to get apps list', { cause: error });
|
||||
@@ -40,17 +41,22 @@ const Apps = () => {
|
||||
display="flex"
|
||||
flexDirection={{ base: 'column', sm: 'row' }}
|
||||
>
|
||||
<CategoriesMenu
|
||||
<MarketplaceCategoriesMenu
|
||||
categories={ categories }
|
||||
selectedCategoryId={ selectedCategoryId }
|
||||
onSelect={ onCategoryChange }
|
||||
/>
|
||||
|
||||
<FilterInput onChange={ onSearchInputChange } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/>
|
||||
<FilterInput
|
||||
initialValue={ filterQuery }
|
||||
onChange={ onSearchInputChange }
|
||||
marginBottom={{ base: '4', lg: '6' }}
|
||||
placeholder="Find app"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{ isLoading ? <AppListSkeleton/> : (
|
||||
<AppList
|
||||
{ isLoading ? <MarketplaceListSkeleton/> : (
|
||||
<MarketplaceList
|
||||
apps={ displayedApps }
|
||||
onAppClick={ showAppInfo }
|
||||
favoriteApps={ favoriteApps }
|
||||
@@ -59,7 +65,7 @@ const Apps = () => {
|
||||
) }
|
||||
|
||||
{ selectedApp && (
|
||||
<AppModal
|
||||
<MarketplaceAppModal
|
||||
onClose={ clearSelectedAppId }
|
||||
isFavorite={ favoriteApps.includes(selectedApp.id) }
|
||||
onFavoriteClick={ onFavoriteClick }
|
||||
@@ -90,4 +96,4 @@ const Apps = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Apps;
|
||||
export default Marketplace;
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
|
||||
import { route } from 'nextjs-routes';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { AppItemOverview } from 'types/client/apps';
|
||||
import type { MarketplaceAppOverview } from 'types/client/marketplace';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import type { ResourceError } from 'lib/api/resources';
|
||||
@@ -26,15 +26,15 @@ const MarketplaceApp = () => {
|
||||
const router = useRouter();
|
||||
const id = getQueryParamString(router.query.id);
|
||||
|
||||
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, AppItemOverview>(
|
||||
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>(
|
||||
[ 'marketplace-apps', id ],
|
||||
async() => {
|
||||
const result = await apiFetch<Array<AppItemOverview>, unknown>(appConfig.marketplaceConfigUrl || '');
|
||||
const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(appConfig.marketplaceConfigUrl || '');
|
||||
if (!Array.isArray(result)) {
|
||||
throw result;
|
||||
}
|
||||
|
||||
const item = result.find((app: AppItemOverview) => app.id === id);
|
||||
const item = result.find((app: MarketplaceAppOverview) => app.id === id);
|
||||
if (!item) {
|
||||
throw { status: 404 };
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
|
||||
import type { RoutedTab } from 'ui/shared/Tabs/types';
|
||||
|
||||
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
|
||||
import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags';
|
||||
import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
|
||||
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
|
||||
|
||||
const TABS: Array<RoutedTab> = [
|
||||
{ id: 'address', title: 'Address', component: <PrivateAddressTags/> },
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, { useEffect } from 'react';
|
||||
|
||||
import type { SocketMessage } from 'lib/socket/types';
|
||||
import type { TokenInfo } from 'types/api/token';
|
||||
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
|
||||
import type { RoutedTab } from 'ui/shared/Tabs/types';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import iconSuccess from 'icons/status/success.svg';
|
||||
@@ -26,8 +26,8 @@ import Tag from 'ui/shared/chakra/Tag';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import type { Props as PaginationProps } from 'ui/shared/Pagination';
|
||||
import Pagination from 'ui/shared/Pagination';
|
||||
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
|
||||
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
|
||||
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
|
||||
import TokenLogo from 'ui/shared/TokenLogo';
|
||||
import TokenContractInfo from 'ui/token/TokenContractInfo';
|
||||
import TokenDetails from 'ui/token/TokenDetails';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Flex, Tag } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
|
||||
import type { RoutedTab } from 'ui/shared/Tabs/types';
|
||||
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import { useAppContext } from 'lib/appContext';
|
||||
@@ -11,7 +11,7 @@ import TextAd from 'ui/shared/ad/TextAd';
|
||||
import NetworkExplorers from 'ui/shared/NetworkExplorers';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
|
||||
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
|
||||
import TxDetails from 'ui/tx/TxDetails';
|
||||
import TxInternals from 'ui/tx/TxInternals';
|
||||
import TxLogs from 'ui/tx/TxLogs';
|
||||
|
||||
@@ -2,17 +2,19 @@ import { Box } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
|
||||
import type { RoutedTab } from 'ui/shared/Tabs/types';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import useHasAccount from 'lib/hooks/useHasAccount';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
|
||||
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
|
||||
import Page from 'ui/shared/Page/Page';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
|
||||
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
|
||||
import TxsContent from 'ui/txs/TxsContent';
|
||||
import TxsTabSlot from 'ui/txs/TxsTabSlot';
|
||||
import TxsWatchlist from 'ui/txs/TxsWatchlist';
|
||||
|
||||
const TAB_LIST_PROPS = {
|
||||
marginBottom: 0,
|
||||
@@ -24,27 +26,49 @@ const Transactions = () => {
|
||||
const verifiedTitle = appConfig.network.verificationType === 'validation' ? 'Validated' : 'Mined';
|
||||
const router = useRouter();
|
||||
const isMobile = useIsMobile();
|
||||
const filter = router.query.tab === 'pending' ? 'pending' : 'validated';
|
||||
const txsQuery = useQueryWithPages({
|
||||
resourceName: filter === 'validated' ? 'txs_validated' : 'txs_pending',
|
||||
filters: { filter },
|
||||
resourceName: router.query.tab === 'pending' ? 'txs_pending' : 'txs_validated',
|
||||
filters: { filter: router.query.tab === 'pending' ? 'pending' : 'validated' },
|
||||
options: {
|
||||
enabled: !router.query.tab || router.query.tab === 'validated' || router.query.tab === 'pending',
|
||||
},
|
||||
});
|
||||
|
||||
const txsWatchlistQuery = useQueryWithPages({
|
||||
resourceName: 'txs_watchlist',
|
||||
options: {
|
||||
enabled: router.query.tab === 'watchlist',
|
||||
},
|
||||
});
|
||||
|
||||
const { num, socketAlert } = useNewTxsSocket();
|
||||
|
||||
const isFirstPage = txsQuery.pagination.page === 1;
|
||||
const hasAccount = useHasAccount();
|
||||
|
||||
const tabs: Array<RoutedTab> = [
|
||||
{
|
||||
id: 'validated',
|
||||
title: verifiedTitle,
|
||||
component: <TxsContent query={ txsQuery } showSocketInfo={ isFirstPage } socketInfoNum={ num } socketInfoAlert={ socketAlert }/> },
|
||||
component: <TxsContent query={ txsQuery } showSocketInfo={ txsQuery.pagination.page === 1 } socketInfoNum={ num } socketInfoAlert={ socketAlert }/> },
|
||||
{
|
||||
id: 'pending',
|
||||
title: 'Pending',
|
||||
component: <TxsContent query={ txsQuery } showBlockInfo={ false } showSocketInfo={ isFirstPage } socketInfoNum={ num } socketInfoAlert={ socketAlert }/>,
|
||||
component: (
|
||||
<TxsContent
|
||||
query={ txsQuery }
|
||||
showBlockInfo={ false }
|
||||
showSocketInfo={ txsQuery.pagination.page === 1 }
|
||||
socketInfoNum={ num }
|
||||
socketInfoAlert={ socketAlert }
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
hasAccount ? {
|
||||
id: 'watchlist',
|
||||
title: 'Watch list',
|
||||
component: <TxsWatchlist query={ txsWatchlistQuery }/>,
|
||||
} : undefined,
|
||||
].filter(Boolean);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
|
||||
@@ -8,6 +8,7 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
|
||||
import Withdrawals from './Withdrawals';
|
||||
|
||||
const WITHDRAWALS_API_URL = buildApiUrl('withdrawals');
|
||||
const WITHDRAWALS_COUNTERS_API_URL = buildApiUrl('withdrawals_counters');
|
||||
|
||||
test('base view +@mobile', async({ mount, page }) => {
|
||||
await page.route('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242', (route) => route.fulfill({
|
||||
@@ -20,6 +21,11 @@ test('base view +@mobile', async({ mount, page }) => {
|
||||
body: JSON.stringify(withdrawalsData),
|
||||
}));
|
||||
|
||||
await page.route(WITHDRAWALS_COUNTERS_API_URL, (route) => route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify({ withdrawal_count: '111111', withdrawal_sum: '1010101010110101001101010' }),
|
||||
}));
|
||||
|
||||
const component = await mount(
|
||||
<TestApp>
|
||||
<Withdrawals/>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Flex, Hide, Show } from '@chakra-ui/react';
|
||||
import { Flex, Hide, Show, Skeleton, Text } from '@chakra-ui/react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import React from 'react';
|
||||
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
import getCurrencyValue from 'lib/getCurrencyValue';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
|
||||
import ActionBar from 'ui/shared/ActionBar';
|
||||
@@ -18,6 +21,8 @@ const Withdrawals = () => {
|
||||
resourceName: 'withdrawals',
|
||||
});
|
||||
|
||||
const countersQuery = useApiQuery('withdrawals_counters');
|
||||
|
||||
const content = data?.items ? (
|
||||
<>
|
||||
<Show below="lg" ssr={ false }>{ data.items.map((item => <WithdrawalsListItem key={ item.index } item={ item } view="list"/>)) }</Show>
|
||||
@@ -25,14 +30,43 @@ const Withdrawals = () => {
|
||||
</>
|
||||
) : null;
|
||||
|
||||
const actionBar = isPaginationVisible ? (
|
||||
<ActionBar mt={ -6 }>
|
||||
<Flex alignItems="center" justifyContent="space-between" w="100%">
|
||||
{ !isMobile }
|
||||
<Pagination ml="auto" { ...pagination }/>
|
||||
</Flex>
|
||||
</ActionBar>
|
||||
) : null;
|
||||
const text = (() => {
|
||||
if (countersQuery.isLoading) {
|
||||
return (
|
||||
<Skeleton
|
||||
w={{ base: '100%', lg: '320px' }}
|
||||
h="24px"
|
||||
mb={{ base: 6, lg: isPaginationVisible ? 0 : 7 }}
|
||||
mt={{ base: 0, lg: isPaginationVisible ? 0 : 7 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (countersQuery.isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { valueStr } = getCurrencyValue({ value: countersQuery.data.withdrawal_sum });
|
||||
return (
|
||||
<Text mb={{ base: 6, lg: isPaginationVisible ? 0 : 6 }} lineHeight={{ base: '24px', lg: '32px' }}>
|
||||
{ BigNumber(countersQuery.data.withdrawal_count).toFormat() } withdrawals processed and { valueStr } ETH withdrawn
|
||||
</Text>
|
||||
);
|
||||
})();
|
||||
|
||||
const actionBar = (
|
||||
<>
|
||||
{ (isMobile || !isPaginationVisible) && text }
|
||||
{ isPaginationVisible && (
|
||||
<ActionBar mt={ -6 }>
|
||||
<Flex alignItems="center" justifyContent="space-between" w="100%">
|
||||
{ !isMobile && text }
|
||||
<Pagination ml="auto" { ...pagination }/>
|
||||
</Flex>
|
||||
</ActionBar>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
|
||||
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 60 KiB |
@@ -24,7 +24,7 @@ const CopyToClipboard = ({ text, className, isLoading }: Props) => {
|
||||
}, [ hasCopied ]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton boxSize={ 5 } className={ className } borderRadius="sm" flexShrink={ 0 }/>;
|
||||
return <Skeleton boxSize={ 5 } className={ className } borderRadius="sm" flexShrink={ 0 } ml={ 1 }/>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Text, chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import EmptySearchResult from 'ui/apps/EmptySearchResult';
|
||||
import EmptySearchResult from 'ui/shared/EmptySearchResult';
|
||||
|
||||
import DataFetchAlert from './DataFetchAlert';
|
||||
import SkeletonList from './skeletons/SkeletonList';
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { ChakraProps, ThemingProps } from '@chakra-ui/react';
|
||||
import { chakra } from '@chakra-ui/react';
|
||||
import _pickBy from 'lodash/pickBy';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
|
||||
import type { RoutedTab } from './types';
|
||||
|
||||
import TabsWithScroll from './TabsWithScroll';
|
||||
|
||||
interface Props extends ThemingProps<'Tabs'> {
|
||||
tabs: Array<RoutedTab>;
|
||||
tabListProps?: ChakraProps | (({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => ChakraProps);
|
||||
rightSlot?: React.ReactNode;
|
||||
stickyEnabled?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, ...themeProps }: Props) => {
|
||||
const router = useRouter();
|
||||
|
||||
let tabIndex = 0;
|
||||
const tabFromRoute = router.query.tab;
|
||||
if (tabFromRoute) {
|
||||
tabIndex = tabs.findIndex(({ id, subTabs }) => id === tabFromRoute || subTabs?.some((id) => id === tabFromRoute));
|
||||
if (tabIndex < 0) {
|
||||
tabIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const tabsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleTabChange = React.useCallback((index: number) => {
|
||||
const nextTab = tabs[index];
|
||||
|
||||
const queryForPathname = _pickBy(router.query, (value, key) => router.pathname.includes(`[${ key }]`));
|
||||
router.push(
|
||||
{ pathname: router.pathname, query: { ...queryForPathname, tab: nextTab.id } },
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
}, [ tabs, router ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (router.query.scroll_to_tabs) {
|
||||
tabsRef?.current?.scrollIntoView(true);
|
||||
delete router.query.scroll_to_tabs;
|
||||
router.push(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: router.query,
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
}
|
||||
// replicate componentDidMount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TabsWithScroll
|
||||
tabs={ tabs }
|
||||
tabListProps={ tabListProps }
|
||||
rightSlot={ rightSlot }
|
||||
stickyEnabled={ stickyEnabled }
|
||||
onTabChange={ handleTabChange }
|
||||
defaultTabIndex={ tabIndex }
|
||||
{ ...themeProps }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(chakra(RoutedTabs));
|
||||
@@ -10,13 +10,13 @@ import { Popover,
|
||||
import type { StyleProps } from '@chakra-ui/styled-system';
|
||||
import React from 'react';
|
||||
|
||||
import type { MenuButton, RoutedTab } from './types';
|
||||
import type { MenuButton, TabItem } from './types';
|
||||
|
||||
import { menuButton } from './utils';
|
||||
|
||||
interface Props {
|
||||
tabs: Array<RoutedTab | MenuButton>;
|
||||
activeTab?: RoutedTab;
|
||||
tabs: Array<TabItem | MenuButton>;
|
||||
activeTab?: TabItem;
|
||||
tabsCut: number;
|
||||
isActive: boolean;
|
||||
styles?: StyleProps;
|
||||
@@ -25,7 +25,7 @@ interface Props {
|
||||
size: ButtonProps['size'];
|
||||
}
|
||||
|
||||
const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, activeTab, size }: Props) => {
|
||||
const TabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, activeTab, size }: Props) => {
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||
|
||||
const handleItemClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
@@ -69,4 +69,4 @@ const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRe
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(RoutedTabsMenu);
|
||||
export default React.memo(TabsMenu);
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { LazyMode } from '@chakra-ui/lazy-utils';
|
||||
import type { ChakraProps, ThemingProps } from '@chakra-ui/react';
|
||||
import {
|
||||
Tab,
|
||||
@@ -10,17 +11,15 @@ import {
|
||||
chakra,
|
||||
} from '@chakra-ui/react';
|
||||
import type { StyleProps } from '@chakra-ui/styled-system';
|
||||
import _pickBy from 'lodash/pickBy';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import type { RoutedTab } from './types';
|
||||
import type { TabItem } from './types';
|
||||
|
||||
import { useScrollDirection } from 'lib/contexts/scrollDirection';
|
||||
import useIsMobile from 'lib/hooks/useIsMobile';
|
||||
import useIsSticky from 'lib/hooks/useIsSticky';
|
||||
|
||||
import RoutedTabsMenu from './RoutedTabsMenu';
|
||||
import TabsMenu from './TabsMenu';
|
||||
import useAdaptiveTabs from './useAdaptiveTabs';
|
||||
|
||||
const hiddenItemStyles: StyleProps = {
|
||||
@@ -31,18 +30,29 @@ const hiddenItemStyles: StyleProps = {
|
||||
};
|
||||
|
||||
interface Props extends ThemingProps<'Tabs'> {
|
||||
tabs: Array<RoutedTab>;
|
||||
tabs: Array<TabItem>;
|
||||
lazyBehavior?: LazyMode;
|
||||
tabListProps?: ChakraProps | (({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => ChakraProps);
|
||||
rightSlot?: React.ReactNode;
|
||||
stickyEnabled?: boolean;
|
||||
onTabChange?: (index: number) => void;
|
||||
defaultTabIndex?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, ...themeProps }: Props) => {
|
||||
const router = useRouter();
|
||||
const TabsWithScroll = ({
|
||||
tabs,
|
||||
lazyBehavior,
|
||||
tabListProps,
|
||||
rightSlot,
|
||||
stickyEnabled,
|
||||
onTabChange,
|
||||
defaultTabIndex,
|
||||
className,
|
||||
...themeProps
|
||||
}: Props) => {
|
||||
const scrollDirection = useScrollDirection();
|
||||
const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1);
|
||||
|
||||
const [ activeTabIndex, setActiveTabIndex ] = useState<number>(defaultTabIndex || 0);
|
||||
const isMobile = useIsMobile();
|
||||
const tabsRef = useRef<HTMLDivElement>(null);
|
||||
const { tabsCut, tabsList, tabsRefs, listRef, rightSlotRef } = useAdaptiveTabs(tabs, isMobile);
|
||||
@@ -50,46 +60,14 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
|
||||
const listBgColor = useColorModeValue('white', 'black');
|
||||
|
||||
const handleTabChange = React.useCallback((index: number) => {
|
||||
const nextTab = tabs[index];
|
||||
|
||||
const queryForPathname = _pickBy(router.query, (value, key) => router.pathname.includes(`[${ key }]`));
|
||||
router.push(
|
||||
{ pathname: router.pathname, query: { ...queryForPathname, tab: nextTab.id } },
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
}, [ tabs, router ]);
|
||||
onTabChange ? onTabChange(index) : setActiveTabIndex(index);
|
||||
}, [ onTabChange ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (router.query.scroll_to_tabs) {
|
||||
tabsRef?.current?.scrollIntoView(true);
|
||||
delete router.query.scroll_to_tabs;
|
||||
router.push(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: router.query,
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true },
|
||||
);
|
||||
if (defaultTabIndex !== undefined) {
|
||||
setActiveTabIndex(defaultTabIndex);
|
||||
}
|
||||
// replicate componentDidMount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (router.isReady) {
|
||||
let tabIndex = 0;
|
||||
const tabFromRoute = router.query.tab;
|
||||
if (tabFromRoute) {
|
||||
tabIndex = tabs.findIndex(({ id, subTabs }) => id === tabFromRoute || subTabs?.some((id) => id === tabFromRoute));
|
||||
if (tabIndex < 0) {
|
||||
tabIndex = 0;
|
||||
}
|
||||
}
|
||||
setActiveTabIndex(tabIndex);
|
||||
}
|
||||
}, [ tabs, router, activeTabIndex ]);
|
||||
}, [ defaultTabIndex ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTabIndex < tabs.length && isMobile) {
|
||||
@@ -97,7 +75,6 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
|
||||
const activeTabRef = tabsRefs[activeTabIndex];
|
||||
if (activeTabRef.current && listRef.current) {
|
||||
const activeTabRect = activeTabRef.current.getBoundingClientRect();
|
||||
|
||||
listRef.current.scrollTo({
|
||||
left: activeTabRect.left + listRef.current.scrollLeft - 16,
|
||||
behavior: 'smooth',
|
||||
@@ -125,6 +102,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
|
||||
position="relative"
|
||||
size={ themeProps.size || 'md' }
|
||||
ref={ tabsRef }
|
||||
lazyBehavior={ lazyBehavior }
|
||||
>
|
||||
<TabList
|
||||
marginBottom={{ base: 6, lg: 8 }}
|
||||
@@ -160,7 +138,7 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
|
||||
{ tabsList.map((tab, index) => {
|
||||
if (!tab.id) {
|
||||
return (
|
||||
<RoutedTabsMenu
|
||||
<TabsMenu
|
||||
key="menu"
|
||||
tabs={ tabs }
|
||||
activeTab={ tabs[activeTabIndex] }
|
||||
@@ -201,4 +179,4 @@ const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled, className, .
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(chakra(RoutedTabs));
|
||||
export default React.memo(chakra(TabsWithScroll));
|
||||
@@ -1,13 +1,14 @@
|
||||
import type React from 'react';
|
||||
|
||||
export interface RoutedTab {
|
||||
export interface TabItem {
|
||||
id: string;
|
||||
title: string | (() => React.ReactNode);
|
||||
component: React.ReactNode;
|
||||
subTabs?: Array<string>;
|
||||
}
|
||||
|
||||
export type RoutedSubTab = Omit<RoutedTab, 'subTabs'>;
|
||||
export type RoutedTab = TabItem & { subTabs?: Array<string> }
|
||||
|
||||
export type RoutedSubTab = Omit<TabItem, 'subTabs'>;
|
||||
|
||||
export interface MenuButton {
|
||||
id: null;
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AspectRatio, chakra, Skeleton } from '@chakra-ui/react';
|
||||
import type { StaticRoute } from 'nextjs-routes';
|
||||
import { route } from 'nextjs-routes';
|
||||
import React from 'react';
|
||||
|
||||
@@ -35,7 +36,7 @@ const NftMedia = ({ imageUrl, animationUrl, className, isLoading }: Props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = route({ pathname: '/api/media-type', query: { url: animationUrl } });
|
||||
const url = route({ pathname: '/node-api/media-type' as StaticRoute<'/api/media-type'>['pathname'], query: { url: animationUrl } });
|
||||
fetch(url)
|
||||
.then((response) => response.json())
|
||||
.then((_data) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Flex, Skeleton, chakra } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
import type { RoutedTab } from '../RoutedTabs/types';
|
||||
import type { RoutedTab } from '../Tabs/types';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
|
||||
@@ -44,7 +44,7 @@ const Burger = () => {
|
||||
<DrawerOverlay/>
|
||||
<DrawerContent maxWidth="260px">
|
||||
<DrawerBody p={ 6 } display="flex" flexDirection="column">
|
||||
<Icon as={ testnetIcon } h="14px" w="auto" color="red.400" alignSelf="flex-start"/>
|
||||
{ appConfig.network.isTestnet && <Icon as={ testnetIcon } h="14px" w="auto" color="red.400" alignSelf="flex-start"/> }
|
||||
<Flex alignItems="center" justifyContent="space-between">
|
||||
<NetworkLogo onClick={ handleNetworkLogoClick }/>
|
||||
{ appConfig.featuredNetworks ? (
|
||||
|
||||
@@ -6,6 +6,7 @@ import chevronIcon from 'icons/arrows/east-mini.svg';
|
||||
import testnetIcon from 'icons/testnet.svg';
|
||||
import { useAppContext } from 'lib/appContext';
|
||||
import * as cookies from 'lib/cookies';
|
||||
import useHasAccount from 'lib/hooks/useHasAccount';
|
||||
import useNavItems, { isGroupItem } from 'lib/hooks/useNavItems';
|
||||
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
|
||||
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
|
||||
@@ -28,11 +29,10 @@ const NavigationDesktop = () => {
|
||||
isNavBarCollapsed = false;
|
||||
}
|
||||
|
||||
const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, cookiesString));
|
||||
|
||||
const { mainNavItems, accountNavItems } = useNavItems();
|
||||
|
||||
const hasAccount = hasAuth && appConfig.isAccountSupported;
|
||||
const hasAccount = useHasAccount();
|
||||
|
||||
const [ isCollapsed, setCollapsedState ] = React.useState<boolean | undefined>(isNavBarCollapsed);
|
||||
|
||||
const handleTogglerClick = React.useCallback(() => {
|
||||
@@ -61,7 +61,7 @@ const NavigationDesktop = () => {
|
||||
width={{ lg: isExpanded ? '229px' : '92px', xl: isCollapsed ? '92px' : '229px' }}
|
||||
{ ...getDefaultTransitionProps({ transitionProperty: 'width, padding' }) }
|
||||
>
|
||||
<Icon as={ testnetIcon } h="14px" w="auto" color="red.400" pl={ 3 } alignSelf="flex-start"/>
|
||||
{ appConfig.network.isTestnet && <Icon as={ testnetIcon } h="14px" w="auto" color="red.400" pl={ 3 } alignSelf="flex-start"/> }
|
||||
<Box
|
||||
as="header"
|
||||
display="flex"
|
||||
|
||||
@@ -2,9 +2,8 @@ import { Box, Flex, Text, Icon, VStack, useColorModeValue } from '@chakra-ui/rea
|
||||
import { animate, motion, useMotionValue } from 'framer-motion';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import appConfig from 'configs/app/config';
|
||||
import chevronIcon from 'icons/arrows/east-mini.svg';
|
||||
import * as cookies from 'lib/cookies';
|
||||
import useHasAccount from 'lib/hooks/useHasAccount';
|
||||
import useNavItems, { isGroupItem } from 'lib/hooks/useNavItems';
|
||||
import NavFooter from 'ui/snippets/navigation/NavFooter';
|
||||
import NavLink from 'ui/snippets/navigation/NavLink';
|
||||
@@ -30,8 +29,7 @@ const NavigationMobile = () => {
|
||||
animate(subX, 250, { ease: 'easeInOut', onComplete: () => setOpenedGroupIndex(-1) });
|
||||
}, [ mainX, subX ]);
|
||||
|
||||
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
|
||||
const hasAccount = appConfig.isAccountSupported && isAuth;
|
||||
const hasAccount = useHasAccount();
|
||||
|
||||
const iconColor = useColorModeValue('blue.600', 'blue.300');
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import type { StatsChartsSection } from 'types/api/stats';
|
||||
import type { StatsIntervalIds } from 'types/client/stats';
|
||||
|
||||
import { apos } from 'lib/html-entities';
|
||||
import ChartWidgetSkeleton from 'ui/shared/chart/ChartWidgetSkeleton';
|
||||
import EmptySearchResult from 'ui/shared/EmptySearchResult';
|
||||
|
||||
import EmptySearchResult from '../apps/EmptySearchResult';
|
||||
import ChartWidgetSkeleton from '../shared/chart/ChartWidgetSkeleton';
|
||||
import ChartsLoadingErrorAlert from './ChartsLoadingErrorAlert';
|
||||
import ChartWidgetContainer from './ChartWidgetContainer';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Box, Tag, Icon } from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
|
||||
import type { RoutedTab } from 'ui/shared/Tabs/types';
|
||||
|
||||
import nftIcon from 'icons/nft_shield.svg';
|
||||
import useApiQuery from 'lib/api/useApiQuery';
|
||||
@@ -15,8 +15,8 @@ import LinkExternal from 'ui/shared/LinkExternal';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
import Pagination from 'ui/shared/Pagination';
|
||||
import type { Props as PaginationProps } from 'ui/shared/Pagination';
|
||||
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
|
||||
import SkeletonTabs from 'ui/shared/skeletons/SkeletonTabs';
|
||||
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
|
||||
import TokenHolders from 'ui/token/TokenHolders/TokenHolders';
|
||||
import TokenTransfer from 'ui/token/TokenTransfer/TokenTransfer';
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { UseQueryResult } from '@tanstack/react-query';
|
||||
import React from 'react';
|
||||
|
||||
import type { TxsResponse } from 'types/api/transaction';
|
||||
|
||||
import useRedirectForInvalidAuthToken from 'lib/hooks/useRedirectForInvalidAuthToken';
|
||||
import type { Props as PaginationProps } from 'ui/shared/Pagination';
|
||||
import TxsContent from 'ui/txs/TxsContent';
|
||||
|
||||
type QueryResult = UseQueryResult<TxsResponse> & {
|
||||
pagination: PaginationProps;
|
||||
isPaginationVisible: boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
query: QueryResult;
|
||||
}
|
||||
|
||||
const TxsWatchlist = ({ query }: Props) => {
|
||||
useRedirectForInvalidAuthToken();
|
||||
return <TxsContent query={ query } showSocketInfo={ false }/>;
|
||||
};
|
||||
|
||||
export default TxsWatchlist;
|
||||