Commit 6fbf652a authored by isstuev's avatar isstuev
parents e94d00e5 f54a17da
...@@ -31,6 +31,9 @@ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_SU ...@@ -31,6 +31,9 @@ NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_SU
NEXT_PUBLIC_LOGOUT_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_URL__ NEXT_PUBLIC_LOGOUT_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_URL__
NEXT_PUBLIC_LOGOUT_RETURN_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_RETURN_URL__ NEXT_PUBLIC_LOGOUT_RETURN_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_RETURN_URL__
NEXT_PUBLIC_HOMEPAGE_CHARTS=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_CHARTS__ NEXT_PUBLIC_HOMEPAGE_CHARTS=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_CHARTS__
NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT__
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER__
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME__
# api config # api config
NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__ NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__
......
...@@ -18,6 +18,8 @@ module.exports = { ...@@ -18,6 +18,8 @@ module.exports = {
'plugin:regexp/recommended', 'plugin:regexp/recommended',
'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:jest/recommended',
'plugin:playwright/playwright-test',
], ],
plugins: [ plugins: [
'es5', 'es5',
...@@ -27,6 +29,7 @@ module.exports = { ...@@ -27,6 +29,7 @@ module.exports = {
'react-hooks', 'react-hooks',
'jsx-a11y', 'jsx-a11y',
'eslint-plugin-import-helpers', 'eslint-plugin-import-helpers',
'jest',
], ],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
parserOptions: { parserOptions: {
......
...@@ -23,6 +23,9 @@ jobs: ...@@ -23,6 +23,9 @@ jobs:
- name: Check out the repo - name: Check out the repo
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v2
...@@ -45,7 +48,7 @@ jobs: ...@@ -45,7 +48,7 @@ jobs:
- name: Add outputs - name: Add outputs
run: | run: |
echo "::set-output name=short-sha::${{ env.SHORT_SHA }}" echo "::set-output name=short-sha::${{ env.GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT }}"
id: output-step id: output-step
- name: Build and push - name: Build and push
...@@ -56,21 +59,22 @@ jobs: ...@@ -56,21 +59,22 @@ jobs:
push: true push: true
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
tags: ghcr.io/blockscout/frontend:prerelease-${{ env.SHORT_SHA }} tags: ghcr.io/blockscout/frontend:prerelease-${{ env.GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: | build-args: |
GIT_COMMIT_SHA=${{ env.SHORT_SHA }} GIT_COMMIT_SHA=${{ env.GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT }}
deploy_frontend: deploy_frontend:
name: Deploy frontend app name: Deploy frontend app
needs: push_to_registry needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with: with:
env_vars: VALUES_DIR=deploy/values/review,APP_NAME=bs-stack env_vars: VALUES_DIR=deploy/values/review,APP_NAME=bs-stack,DOCKER_IMAGE=prerelease-$GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT
globalEnv: review
appNamespace: review-front-$GITHUB_HEAD_REF_SLUG appNamespace: review-front-$GITHUB_HEAD_REF_SLUG
blockscoutIngressHost: blockscout blockscoutIngressHost: blockscout
frontendIngressHost: blockscout frontendIngressHost: blockscout
frontendImage: ghcr.io/blockscout/frontend:prerelease-${{ needs.push_to_registry.outputs.shortSha }} frontendImage: ghcr.io/blockscout/frontend:prerelease-$GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT
gethIngressHost: geth gethIngressHost: geth
scVerifierIngressHost: sc-verifier scVerifierIngressHost: sc-verifier
secrets: inherit secrets: inherit
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
"detail": "start local dev server", "detail": "start local dev server",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "new", "panel": "dedicated",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
"detail": "start local dev server for POA network", "detail": "start local dev server for POA network",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "new", "panel": "dedicated",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
"detail": "start local dev server for Goerli network", "detail": "start local dev server for Goerli network",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "new", "panel": "dedicated",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
}, },
"presentation": { "presentation": {
"reveal": "never", "reveal": "never",
"panel": "new", "panel": "dedicated",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
"detail": "run eslint", "detail": "run eslint",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "new", "panel": "dedicated",
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
"icon": { "icon": {
...@@ -108,11 +108,11 @@ ...@@ -108,11 +108,11 @@
"type": "shell", "type": "shell",
"command": "${input:pwDebugFlag} yarn test:pw:local ${relativeFileDirname}/${fileBasename} ${input:pwArgs}", "command": "${input:pwDebugFlag} yarn test:pw:local ${relativeFileDirname}/${fileBasename} ${input:pwArgs}",
"problemMatcher": [], "problemMatcher": [],
"label": "test: playwright: local for current file", "label": "pw: local",
"detail": "run visual components tests for current file", "detail": "run visual components tests for current file",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new", "panel": "dedicated",
"focus": true, "focus": true,
}, },
"icon": { "icon": {
...@@ -127,11 +127,11 @@ ...@@ -127,11 +127,11 @@
"type": "shell", "type": "shell",
"command": "yarn test:pw:docker ${relativeFileDirname}/${fileBasename} ${input:pwArgs}", "command": "yarn test:pw:docker ${relativeFileDirname}/${fileBasename} ${input:pwArgs}",
"problemMatcher": [], "problemMatcher": [],
"label": "test: playwright: docker for current file", "label": "pw: docker",
"detail": "run visual components tests for current file", "detail": "run visual components tests for current file",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new", "panel": "dedicated",
"focus": true, "focus": true,
}, },
"icon": { "icon": {
...@@ -146,11 +146,11 @@ ...@@ -146,11 +146,11 @@
"type": "shell", "type": "shell",
"command": "yarn test:pw:docker ${input:pwArgs}", "command": "yarn test:pw:docker ${input:pwArgs}",
"problemMatcher": [], "problemMatcher": [],
"label": "test: playwright: docker for all files", "label": "pw: docker all",
"detail": "run visual components tests", "detail": "run visual components tests",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new", "panel": "dedicated",
"focus": true, "focus": true,
}, },
"icon": { "icon": {
...@@ -167,11 +167,11 @@ ...@@ -167,11 +167,11 @@
"type": "npm", "type": "npm",
"script": "test:jest", "script": "test:jest",
"problemMatcher": [], "problemMatcher": [],
"label": "test: jest", "label": "jest",
"detail": "run jest tests", "detail": "run jest tests",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new", "panel": "dedicated",
"focus": true, "focus": true,
}, },
"icon": { "icon": {
...@@ -186,11 +186,11 @@ ...@@ -186,11 +186,11 @@
"type": "npm", "type": "npm",
"script": "test:jest:watch", "script": "test:jest:watch",
"problemMatcher": [], "problemMatcher": [],
"label": "test: jest: watch", "label": "jest: watch all",
"detail": "run jest tests in watch mode", "detail": "run jest tests in watch mode",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new", "panel": "dedicated",
"close": true, "close": true,
"focus": true, "focus": true,
}, },
...@@ -206,11 +206,11 @@ ...@@ -206,11 +206,11 @@
"type": "shell", "type": "shell",
"command": "yarn test:jest ${relativeFileDirname}/${fileBasename} --watch", "command": "yarn test:jest ${relativeFileDirname}/${fileBasename} --watch",
"problemMatcher": [], "problemMatcher": [],
"label": "test: jest: watch curent file", "label": "jest: watch",
"detail": "run jest tests in watch mode for current file", "detail": "run jest tests in watch mode for current file",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new", "panel": "dedicated",
"focus": true, "focus": true,
}, },
"icon": { "icon": {
...@@ -230,7 +230,7 @@ ...@@ -230,7 +230,7 @@
"detail": "build docker image", "detail": "build docker image",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "new", "panel": "dedicated",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
"focus": true, "focus": true,
...@@ -251,7 +251,7 @@ ...@@ -251,7 +251,7 @@
"detail": "run docker container for POA network", "detail": "run docker container for POA network",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "new", "panel": "dedicated",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
...@@ -271,7 +271,7 @@ ...@@ -271,7 +271,7 @@
"detail": "format svg files with svgo", "detail": "format svg files with svgo",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "new", "panel": "dedicated",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
......
...@@ -64,8 +64,6 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -64,8 +64,6 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` *(optional)* | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` | | NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` *(optional)* | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` |
| NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` *(optional)* | Link to Telegram in the footer | `https://t.me/poa_network` | | NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` *(optional)* | Link to Telegram in the footer | `https://t.me/poa_network` |
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` | | NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` *(optional)* | Set to false if network doesn't have gas tracker | `true` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` *(optional)* | Set to false if average block time is useless for the network | `true` |
| NEXT_PUBLIC_MARKETPLACE_APP_LIST | `Array<MarketplaceApp>` where `MarketplaceApp` can have following [properties](#marketplace-app-configuration-properties) | List of apps that will be shown on the marketplace page | `[{'author': 'Bob', 'id': 'app', 'external': true, 'title': 'The App', 'logo': 'https://foo.app/icon.png', 'categories': ['security'], 'shortDescription': 'Awesome app', 'site': 'https://foo.app', 'description': 'The best app', 'url': 'https://foo.app/launch'}]` | | NEXT_PUBLIC_MARKETPLACE_APP_LIST | `Array<MarketplaceApp>` where `MarketplaceApp` can have following [properties](#marketplace-app-configuration-properties) | List of apps that will be shown on the marketplace page | `[{'author': 'Bob', 'id': 'app', 'external': true, 'title': 'The App', 'logo': 'https://foo.app/icon.png', 'categories': ['security'], 'shortDescription': 'Awesome app', 'site': 'https://foo.app', 'description': 'The best app', 'url': 'https://foo.app/launch'}]` |
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` | | NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` |
| NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` | | NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` |
...@@ -73,6 +71,9 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -73,6 +71,9 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_LOGOUT_URL | `string` *(optional)* | Account logout url | `https://blockscoutcom.us.auth0.com/v2/logout` | | NEXT_PUBLIC_LOGOUT_URL | `string` *(optional)* | Account logout url | `https://blockscoutcom.us.auth0.com/v2/logout` |
| NEXT_PUBLIC_LOGOUT_RETURN_URL | `string` *(optional)* | Account logout return url | `https://blockscout.com/poa/core/auth/logout` | | NEXT_PUBLIC_LOGOUT_RETURN_URL | `string` *(optional)* | Account logout return url | `https://blockscout.com/poa/core/auth/logout` |
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cup'>` *(optional)* | List of charts displayed on the home page | `['daily_txs','coin_price','market_cup']` | | NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cup'>` *(optional)* | List of charts displayed on the home page | `['daily_txs','coin_price','market_cup']` |
| NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT | `string` *(optional)* | Gradient value for hero plate on the homepage | `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` *(optional)* | Set to false if network doesn't have gas tracker | `true` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` *(optional)* | Set to false if average block time is useless for the network | `true` |
### App configuration ### App configuration
......
...@@ -89,6 +89,8 @@ const config = Object.freeze({ ...@@ -89,6 +89,8 @@ const config = Object.freeze({
}, },
homepage: { homepage: {
charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_CHARTS)) || [], charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_CHARTS)) || [],
plateGradient: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT) ||
'radial-gradient(103.03% 103.03% at 0% 0%, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%)',
showGasTracker: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER) === 'false' ? false : true, showGasTracker: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER) === 'false' ? false : true,
showAvgBlockTime: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME) === 'false' ? false : true, showAvgBlockTime: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME) === 'false' ? false : true,
}, },
......
# ui config # ui config
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Astar (EVM)','url':'https://blockscout.com/astar','group':'mainnets','type':'astar'},{'title':'Shiden (EVM)','url':'https://blockscout.com/shiden','group':'mainnets','type':'astar'},{'title':'Klaytn Mainnet (Cypress)','url':'https://klaytn-mainnet.aws-k8s.blockscout.com/','group':'mainnets','type':'klaytn'},{'title':'Goerli','url':'https://blockscout.com/eth/goerli/','group':'testnets','type':'goerli'},{'title':'Optimism Goerli','url':'https://blockscout.com/optimism/goerli/','group':'testnets','type':'optimism_goerli'},{'title':'Optimism Bedrock Alpha','url':'https://blockscout.com/optimism/bedrock-alpha','group':'testnets','type':'optimism_bedrock_alpha'},{'title':'Gnosis Chiado','url':'https://blockscout.com/gnosis/chiado/','group':'testnets','type':'gnosis_chiado'},{'title':'Shibuya (EVM)','url':'https://blockscout.com/shibuya','group':'testnets','type':'shibuya'},{'title':'Optimism Opcraft','url':'https://blockscout.com/optimism/opcraft','group':'other','type':'optimism_opcraft'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'other','type':'optimism_gnosis'},{'title':'ARTIS-Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'other','type':'poa_core'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'other','type':'poa_sokol'}] NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Astar (EVM)','url':'https://blockscout.com/astar','group':'mainnets','type':'astar'},{'title':'Shiden (EVM)','url':'https://blockscout.com/shiden','group':'mainnets','type':'astar'},{'title':'Klaytn Mainnet (Cypress)','url':'https://klaytn-mainnet.aws-k8s.blockscout.com/','group':'mainnets','type':'klaytn'},{'title':'Goerli','url':'https://blockscout.com/eth/goerli/','group':'testnets','type':'goerli'},{'title':'Optimism Goerli','url':'https://blockscout.com/optimism/goerli/','group':'testnets','type':'optimism_goerli'},{'title':'Optimism Bedrock Alpha','url':'https://blockscout.com/optimism/bedrock-alpha','group':'testnets','type':'optimism_bedrock_alpha'},{'title':'Gnosis Chiado','url':'https://blockscout.com/gnosis/chiado/','group':'testnets','type':'gnosis_chiado'},{'title':'Shibuya (EVM)','url':'https://blockscout.com/shibuya','group':'testnets','type':'shibuya'},{'title':'Optimism Opcraft','url':'https://blockscout.com/optimism/opcraft','group':'other','type':'optimism_opcraft'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'other','type':'optimism_gnosis'},{'title':'ARTIS-Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'other','type':'poa_core'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'other','type':'poa_sokol'}]
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx'}}] NEXT_PUBLIC_NETWORK_EXPLORERS=[{'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'}}]
# network config # network config
NEXT_PUBLIC_NETWORK_NAME=Ethereum NEXT_PUBLIC_NETWORK_NAME=Ethereum
......
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60','type':'xdai_optimism'},{'title':'Arbitrum on xDai','url':'https://blockscout.com/xdai/aox','group':'mainnets'},{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'mainnets','type':'poa_core'},{'title':'RSK','url':'https://blockscout.com/rsk/mainnet','group':'mainnets','type':'rsk_mainnet'},{'title':'Gnosis Chain Testnet','url':'https://blockscout.com/xdai/testnet','group':'testnets','type':'xdai_testnet'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'testnets','type':'poa_sokol'},{'title':'ARTIS Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'Astar','url':'https://blockscout.com/astar','group':'other','type':'astar'}] NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60','type':'xdai_optimism'},{'title':'Arbitrum on xDai','url':'https://blockscout.com/xdai/aox','group':'mainnets'},{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'mainnets','type':'poa_core'},{'title':'RSK','url':'https://blockscout.com/rsk/mainnet','group':'mainnets','type':'rsk_mainnet'},{'title':'Gnosis Chain Testnet','url':'https://blockscout.com/xdai/testnet','group':'testnets','type':'xdai_testnet'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'testnets','type':'poa_sokol'},{'title':'ARTIS Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'Astar','url':'https://blockscout.com/astar','group':'other','type':'astar'}]
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction'}}] 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_cup']
NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=radial-gradient(at 12% 37%, hsla(324,73%,67%,1) 0px, transparent 50%), radial-gradient(at 62% 14%, hsla(256,87%,73%,1) 0px, transparent 50%), radial-gradient(at 84% 80%, hsla(128,75%,73%,1) 0px, transparent 50%), radial-gradient(at 57% 46%, hsla(285,63%,72%,1) 0px, transparent 50%), radial-gradient(at 37% 30%, hsla(174,70%,61%,1) 0px, transparent 50%), radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 67% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)
# network config # network config
NEXT_PUBLIC_NETWORK_NAME=POA NEXT_PUBLIC_NETWORK_NAME=POA
......
# app config # app config
NEXT_PUBLIC_APP_HOST=blockscout.com NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3100
NEXT_PUBLIC_APP_INSTANCE=pw NEXT_PUBLIC_APP_INSTANCE=pw
NEXT_PUBLIC_APP_ENV=testing NEXT_PUBLIC_APP_ENV=testing
......
...@@ -160,14 +160,14 @@ postgres: ...@@ -160,14 +160,14 @@ postgres:
resources: resources:
limits: limits:
memory: memory:
_default: "4Gi" _default: "6Gi"
cpu: cpu:
_default: "3" _default: "4"
requests: requests:
memory: memory:
_default: "4Gi" _default: "6Gi"
cpu: cpu:
_default: "3" _default: "4"
environment: environment:
POSTGRES_USER: POSTGRES_USER:
...@@ -473,7 +473,7 @@ frontend: ...@@ -473,7 +473,7 @@ frontend:
NEXT_PUBLIC_FEATURED_NETWORKS: NEXT_PUBLIC_FEATURED_NETWORKS:
_default: "[{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60','type':'xdai_optimism'},{'title':'Arbitrum on xDai','url':'https://blockscout.com/xdai/aox','group':'mainnets'},{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'mainnets','type':'poa_core'},{'title':'RSK','url':'https://blockscout.com/rsk/mainnet','group':'mainnets','type':'rsk_mainnet'},{'title':'Gnosis Chain Testnet','url':'https://blockscout.com/xdai/testnet','group':'testnets','type':'xdai_testnet'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'testnets','type':'poa_sokol'},{'title':'ARTIS Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'Astar','url':'https://blockscout.com/astar','group':'other','type':'astar'}]" _default: "[{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60','type':'xdai_optimism'},{'title':'Arbitrum on xDai','url':'https://blockscout.com/xdai/aox','group':'mainnets'},{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'mainnets','type':'poa_core'},{'title':'RSK','url':'https://blockscout.com/rsk/mainnet','group':'mainnets','type':'rsk_mainnet'},{'title':'Gnosis Chain Testnet','url':'https://blockscout.com/xdai/testnet','group':'testnets','type':'xdai_testnet'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'testnets','type':'poa_sokol'},{'title':'ARTIS Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'Astar','url':'https://blockscout.com/astar','group':'other','type':'astar'}]"
NEXT_PUBLIC_NETWORK_EXPLORERS: NEXT_PUBLIC_NETWORK_EXPLORERS:
_default: "[{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io','paths':{'tx':'/tx'}}]" _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'}}]"
# network config # network config
NEXT_PUBLIC_NETWORK_NAME: NEXT_PUBLIC_NETWORK_NAME:
_default: Ethereum _default: Ethereum
......
...@@ -65,7 +65,7 @@ geth: ...@@ -65,7 +65,7 @@ geth:
frontend: frontend:
environment: environment:
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS: NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS:
_default: ENC[AES256_GCM,data:1SAbzZhCs/vzdftIX0WVLtImH27NJ6SwENee4uTu2p+ZyUso3nQCLUUm,iv:apyLxt2dQ5RN33ra1Q1sAy2cyplG9FSryksQru2ghlA=,tag:PVcCNt0bz1TfQewUebV5LA==,type:str] _default: ENC[AES256_GCM,data:yShwsa6ajoFXg/6QSgEARkZRVVrwrdsR69NSmyvBH2O5EUQ0OvsWpW64,iv:K/HT6C9pYCK63LNyF3HERFc79vDS4cB0H4pINIlNhh0=,tag:X0HqeAP01diTvDOwoEP6lw==,type:str]
NEXT_PUBLIC_SENTRY_DSN: NEXT_PUBLIC_SENTRY_DSN:
_default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str] _default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str]
SENTRY_CSP_REPORT_URI: SENTRY_CSP_REPORT_URI:
...@@ -78,8 +78,8 @@ sops: ...@@ -78,8 +78,8 @@ sops:
azure_kv: [] azure_kv: []
hc_vault: [] hc_vault: []
age: [] age: []
lastmodified: "2022-11-16T11:55:15Z" lastmodified: "2022-11-28T16:58:46Z"
mac: ENC[AES256_GCM,data:wd7HZEGH1fJO1ufaUeBtcjUaHS21rcOoiGaQPNptEyEPj6q/60rJ1YQmGqkgi3DNVnGly5FQyYjMIIY3/YqXqTZI6MBhJp4RmpCELbqqzQbAFvbomYURmqG/umeT2+kMrSIF/PXrt4d51e1cod2+H4OY9V09VerH9L07D0nTd48=,iv:dDeTSqvmwps4oQKRVgDqmMf/uxf7Egb+jufwTKtm6F4=,tag:CqOCA4XW7d3C5D4dflIFug==,type:str] mac: ENC[AES256_GCM,data:QJvVfWWWVDk5mI66T9J8EnEyVwmJoGEsWO9Pr8vK7jyC3rhAYD2WdKYfpkbwwMKrJzcMBe7UeaOeEY6aApuMNdobeEjsJAvstXCOBzMe5H9XtAFiAY+oxf8r4ELNvQP/gIBZSja+ehSbXBcaP4DkLn4FboaBhkoE8A37W2R6/QA=,iv:FnIC6iGLEZNwRSrbF81vF6eQuyq0yQHNPRTPrx3FB+8=,tag:LRnZCwYkCh4o8lDUcG2m9A==,type:str]
pgp: pgp:
- created_at: "2022-09-14T13:42:28Z" - created_at: "2022-09-14T13:42:28Z"
enc: | enc: |
......
...@@ -3,7 +3,7 @@ global: ...@@ -3,7 +3,7 @@ global:
# enable Blockscout deploy # enable Blockscout deploy
blockscout: blockscout:
app: blockscout app: blockscout
enabled: true enabled: false
image: image:
_default: blockscout/blockscout:latest _default: blockscout/blockscout:latest
replicas: replicas:
...@@ -117,7 +117,7 @@ blockscout: ...@@ -117,7 +117,7 @@ blockscout:
_default: 'true' _default: 'true'
postgres: postgres:
enabled: true enabled: false
image: postgres:13.8 image: postgres:13.8
port: 5432 port: 5432
...@@ -145,7 +145,7 @@ postgres: ...@@ -145,7 +145,7 @@ postgres:
_default: 'trust' _default: 'trust'
# enable geth deploy # enable geth deploy
geth: geth:
enabled: true enabled: false
image: image:
_default: ethereum/client-go:stable _default: ethereum/client-go:stable
replicas: replicas:
...@@ -200,7 +200,7 @@ geth: ...@@ -200,7 +200,7 @@ geth:
enabled: true enabled: true
# enable Smart-contract-verifier deploy # enable Smart-contract-verifier deploy
scVerifier: scVerifier:
enabled: true enabled: false
image: image:
_default: ghcr.io/blockscout/smart-contract-verifier:latest _default: ghcr.io/blockscout/smart-contract-verifier:latest
replicas: replicas:
...@@ -347,19 +347,19 @@ frontend: ...@@ -347,19 +347,19 @@ frontend:
NEXT_PUBLIC_FOOTER_STAKING_LINK: NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking _default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME: NEXT_PUBLIC_NETWORK_NAME:
_default: Sokol _default: Ethereum
NEXT_PUBLIC_NETWORK_SHORT_NAME: NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: POA _default: Goerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME: NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: poa _default: ethereum
NEXT_PUBLIC_NETWORK_TYPE: NEXT_PUBLIC_NETWORK_TYPE:
_default: poa_core _default: goerli
NEXT_PUBLIC_NETWORK_ID: NEXT_PUBLIC_NETWORK_ID:
_default: 77 _default: 5
NEXT_PUBLIC_NETWORK_CURRENCY_NAME: NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
_default: POA Network Sokol _default: Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL:
_default: SPOA _default: ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS:
_default: 18 _default: 18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE: NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE:
...@@ -370,6 +370,7 @@ frontend: ...@@ -370,6 +370,7 @@ frontend:
_default: "[{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60','type':'xdai_optimism'},{'title':'Arbitrum on xDai','url':'https://blockscout.com/xdai/aox','group':'mainnets'},{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'mainnets','type':'poa_core'},{'title':'RSK','url':'https://blockscout.com/rsk/mainnet','group':'mainnets','type':'rsk_mainnet'},{'title':'Gnosis Chain Testnet','url':'https://blockscout.com/xdai/testnet','group':'testnets','type':'xdai_testnet'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'testnets','type':'poa_sokol'},{'title':'ARTIS Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'Astar','url':'https://blockscout.com/astar','group':'other','type':'astar'}]" _default: "[{'title':'Gnosis Chain','url':'https://blockscout.com/xdai/mainnet','group':'mainnets','type':'xdai_mainnet'},{'title':'Optimism on Gnosis Chain','url':'https://blockscout.com/xdai/optimism','group':'mainnets','icon':'https://www.fillmurray.com/60/60','type':'xdai_optimism'},{'title':'Arbitrum on xDai','url':'https://blockscout.com/xdai/aox','group':'mainnets'},{'title':'Ethereum','url':'https://blockscout.com/eth/mainnet','group':'mainnets','type':'eth_mainnet'},{'title':'Ethereum Classic','url':'https://blockscout.com/etx/mainnet','group':'mainnets','type':'etc_mainnet'},{'title':'POA','url':'https://blockscout.com/poa/core','group':'mainnets','type':'poa_core'},{'title':'RSK','url':'https://blockscout.com/rsk/mainnet','group':'mainnets','type':'rsk_mainnet'},{'title':'Gnosis Chain Testnet','url':'https://blockscout.com/xdai/testnet','group':'testnets','type':'xdai_testnet'},{'title':'POA Sokol','url':'https://blockscout.com/poa/sokol','group':'testnets','type':'poa_sokol'},{'title':'ARTIS Σ1','url':'https://blockscout.com/artis/sigma1','group':'other','type':'artis_sigma1'},{'title':'LUKSO L14','url':'https://blockscout.com/lukso/l14','group':'other','type':'lukso_l14'},{'title':'Astar','url':'https://blockscout.com/astar','group':'other','type':'astar'}]"
NEXT_PUBLIC_API_HOST: NEXT_PUBLIC_API_HOST:
_default: blockscout.com _default: blockscout.com
review: blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_API_BASE_PATH: NEXT_PUBLIC_API_BASE_PATH:
_default: / _default: /
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM:
...@@ -380,5 +381,11 @@ frontend: ...@@ -380,5 +381,11 @@ frontend:
_default: https://blockscoutcom.us.auth0.com/v2/logout _default: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_LOGOUT_RETURN_URL: NEXT_PUBLIC_LOGOUT_RETURN_URL:
_default: https://blockscout.com/auth/logout _default: https://blockscout.com/auth/logout
review: blockscout-main.test.aws-k8s.blockscout.com/auth/logout
NEXT_PUBLIC_HOMEPAGE_CHARTS: NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs','coin_price','market_cup']" _default: "['daily_txs','coin_price','market_cup']"
NEXT_PUBLIC_APP_HOST:
_default: blockscout.com
review: blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_NETWORK_EXPLORERS:
_default: [{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.2 19.4c3.972 0 7.2-3.228 7.2-7.2S16.172 5 12.2 5A7.206 7.206 0 0 0 5 12.2c0 3.972 3.228 7.2 7.2 7.2Zm-5.574-4.332 2.926-.023c.325 1.173.871 2.311 1.614 3.333a6.28 6.28 0 0 1-4.54-3.31Zm3.67-4.842h3.646a9.853 9.853 0 0 1 .023 3.867l-3.67.023a9.544 9.544 0 0 1 0-3.89Zm1.823 7.885a9.323 9.323 0 0 1-1.591-3.066l3.193-.023a9.813 9.813 0 0 1-1.602 3.089Zm.929.302a10.24 10.24 0 0 0 1.637-3.403l3.124-.023a6.286 6.286 0 0 1-4.761 3.426ZM18.47 12.2c0 .65-.105 1.277-.279 1.858l-3.286.023c.232-1.277.22-2.578-.023-3.855h3.263c.209.615.325 1.289.325 1.974Zm-.72-2.903h-3.09a10.675 10.675 0 0 0-1.613-3.31 6.244 6.244 0 0 1 4.703 3.31Zm-4.053 0H10.54a9.593 9.593 0 0 1 1.58-3.008 9.595 9.595 0 0 1 1.58 3.008Zm-2.532-3.275a10.317 10.317 0 0 0-1.59 3.275H6.648a6.249 6.249 0 0 1 4.517-3.275Zm-1.811 4.204a10.685 10.685 0 0 0-.012 3.89l-3.1.023a6.173 6.173 0 0 1 .012-3.914h3.1Z" fill="currentColor"/> <path d="M12.2 19.4c3.972 0 7.2-3.228 7.2-7.2S16.172 5 12.2 5A7.206 7.206 0 0 0 5 12.2c0 3.972 3.228 7.2 7.2 7.2Zm-5.574-4.332 2.926-.023a10.424 10.424 0 0 0 1.614 3.333 6.28 6.28 0 0 1-4.54-3.31Zm3.67-4.842h3.646a9.853 9.853 0 0 1 .023 3.867l-3.67.023a9.544 9.544 0 0 1 0-3.89Zm1.823 7.885a9.323 9.323 0 0 1-1.591-3.066l3.193-.023a9.813 9.813 0 0 1-1.602 3.089Zm.929.302a10.24 10.24 0 0 0 1.637-3.403l3.124-.023a6.286 6.286 0 0 1-4.761 3.426ZM18.47 12.2c0 .65-.105 1.277-.279 1.858l-3.286.023c.232-1.277.22-2.578-.023-3.855h3.263a6.17 6.17 0 0 1 .325 1.974Zm-.72-2.903h-3.09a10.675 10.675 0 0 0-1.613-3.31 6.244 6.244 0 0 1 4.703 3.31Zm-4.053 0H10.54a9.593 9.593 0 0 1 1.58-3.008 9.595 9.595 0 0 1 1.58 3.008Zm-2.532-3.275a10.317 10.317 0 0 0-1.59 3.275H6.648a6.249 6.249 0 0 1 4.517-3.275Zm-1.811 4.204a10.685 10.685 0 0 0-.012 3.89l-3.1.023a6.173 6.173 0 0 1 .012-3.914h3.1Z" fill="currentColor"/>
</svg> </svg>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.004 2.61 13.2 8.432l1.443-3.434 6.36-2.386Z" fill="#E2761B"/>
<path d="m2.988 2.61 7.741 5.876-1.372-3.489-6.369-2.386Zm15.208 13.492L16.117 19.3l4.447 1.229 1.279-4.356-3.647-.07Zm-16.032.071 1.271 4.356L7.882 19.3l-2.078-3.198-3.64.071Z" fill="#E4761B"/>
<path d="m7.631 10.7-1.24 1.882 4.417.197-.157-4.765L7.63 10.7Zm8.73 0L13.3 7.959l-.101 4.82 4.408-.197-1.248-1.883Zm-8.479 8.6 2.651-1.3-2.29-1.795-.36 3.095ZM13.46 18l2.658 1.3-.368-3.095L13.459 18Z" fill="#E4761B"/>
<path d="M16.117 19.3 13.458 18l.212 1.741-.023.733 2.47-1.174Zm-8.235 0 2.47 1.174-.015-.733.196-1.74-2.65 1.299Z" fill="#D7C1B3"/>
<path d="M10.392 15.055 8.18 14.4l1.561-.717.65 1.37Zm3.208 0 .65-1.37 1.57.716-2.22.654Z" fill="#233447"/>
<path d="m7.882 19.3.377-3.198-2.455.071L7.882 19.3Zm7.859-3.198.376 3.198 2.079-3.127-2.455-.07Zm1.867-3.52-4.408.197.408 2.276.65-1.37 1.57.716 1.78-1.82Zm-9.428 1.82 1.569-.718.643 1.37.416-2.275-4.416-.197 1.788 1.82Z" fill="#CD6116"/>
<path d="m6.392 12.582 1.85 3.623L8.18 14.4l-1.788-1.82Zm9.435 1.82-.078 1.803 1.859-3.623-1.78 1.82Zm-5.02-1.623-.415 2.276.518 2.686.117-3.537-.22-1.425Zm2.393 0-.212 1.417.094 3.545.526-2.686-.408-2.276Z" fill="#E4751F"/>
<path d="m13.608 15.055-.526 2.686.377.26 2.29-1.796.078-1.804-2.22.654ZM8.18 14.4l.063 1.804L10.533 18l.377-.26-.518-2.685L8.18 14.4Z" fill="#F6851B"/>
<path d="m13.647 20.474.023-.733-.196-.173h-2.957l-.18.173.016.733-2.47-1.174.862.709 1.749 1.22h3.004l1.757-1.22.862-.709-2.47 1.174Z" fill="#C0AD9E"/>
<path d="m13.459 18-.377-.26H10.91l-.377.26-.196 1.741.18-.173h2.957l.196.173-.211-1.74Z" fill="#161616"/>
<path d="M21.333 8.81 22 5.595l-.996-2.985-7.545 5.623L16.36 10.7l4.102 1.206.91-1.064-.392-.283.628-.575-.487-.378.628-.48-.416-.316ZM2 5.595l.666 3.213-.423.315.627.48-.478.379.627.575-.392.283.902 1.064 4.102-1.206 2.902-2.465L2.988 2.61 2 5.596Z" fill="#763D16"/>
<path d="M20.462 11.904 16.361 10.7l1.247 1.883-1.86 3.623 2.448-.032h3.647l-1.38-4.269ZM7.632 10.7l-4.103 1.205-1.365 4.27h3.64l2.439.03-1.851-3.622 1.24-1.883Zm5.568 2.08.259-4.545 1.192-3.237H9.357l1.176 3.237.275 4.545.094 1.433.008 3.529h2.172l.016-3.529.102-1.433Z" fill="#F6851B"/>
</svg>
<svg viewBox="0 0 20 20" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M15.938 13.125h-2.5a.312.312 0 0 0-.313.313v2.5c0 .172.14.312.313.312h2.5c.172 0 .312-.14.312-.313v-2.5a.312.312 0 0 0-.313-.312Zm-3.125-2.5h-1.876a.312.312 0 0 0-.312.313v1.874c0 .173.14.313.313.313h1.874c.173 0 .313-.14.313-.313v-1.874a.312.312 0 0 0-.313-.313Zm5.625 5.625h-1.875a.312.312 0 0 0-.313.313v1.875c0 .172.14.312.313.312h1.875c.172 0 .312-.14.312-.313v-1.875a.312.312 0 0 0-.313-.312Zm0-5.625h-1.25a.312.312 0 0 0-.313.313v1.25c0 .172.14.312.313.312h1.25c.172 0 .312-.14.312-.313v-1.25a.312.312 0 0 0-.313-.312Zm-6.25 6.25h-1.25a.312.312 0 0 0-.313.313v1.25c0 .172.14.312.313.312h1.25c.172 0 .312-.14.312-.313v-1.25a.312.312 0 0 0-.313-.312ZM17.5 1.25h-5.625a1.25 1.25 0 0 0-1.25 1.25v5.625a1.25 1.25 0 0 0 1.25 1.25H17.5a1.25 1.25 0 0 0 1.25-1.25V2.5a1.25 1.25 0 0 0-1.25-1.25Zm-1.25 5.313a.312.312 0 0 1-.313.312h-2.5a.313.313 0 0 1-.312-.313v-2.5a.312.312 0 0 1 .313-.312h2.5a.313.313 0 0 1 .312.313v2.5ZM8.125 1.25H2.5A1.25 1.25 0 0 0 1.25 2.5v5.625a1.25 1.25 0 0 0 1.25 1.25h5.625a1.25 1.25 0 0 0 1.25-1.25V2.5a1.25 1.25 0 0 0-1.25-1.25Zm-1.25 5.313a.312.312 0 0 1-.313.312h-2.5a.312.312 0 0 1-.312-.313v-2.5a.312.312 0 0 1 .313-.312h2.5a.312.312 0 0 1 .312.313v2.5Zm1.25 4.062H2.5a1.25 1.25 0 0 0-1.25 1.25V17.5a1.25 1.25 0 0 0 1.25 1.25h5.625a1.25 1.25 0 0 0 1.25-1.25v-5.625a1.25 1.25 0 0 0-1.25-1.25Zm-1.25 5.313a.313.313 0 0 1-.313.312h-2.5a.312.312 0 0 1-.312-.313v-2.5a.313.313 0 0 1 .313-.312h2.5a.312.312 0 0 1 .312.313v2.5Z"/>
</svg>
...@@ -13,7 +13,7 @@ interface Props extends ChakraProviderProps { ...@@ -13,7 +13,7 @@ interface Props extends ChakraProviderProps {
export function Chakra({ cookies, theme, children }: Props) { export function Chakra({ cookies, theme, children }: Props) {
const colorModeManager = const colorModeManager =
typeof cookies === 'string' ? typeof cookies === 'string' ?
cookieStorageManagerSSR(cookies) : cookieStorageManagerSSR(typeof document !== 'undefined' ? document.cookie : cookies) :
localStorageManager; localStorageManager;
return ( return (
......
export default function getPlaceholderWithError(text: string, errorText?: string) {
return `${ text }${ errorText ? ' - ' + errorText : '' }`;
}
import _clamp from 'lodash/clamp';
import React from 'react';
const MAX_DELAY = 500;
const MIN_DELAY = 50;
export default function useGradualIncrement(initialValue: number): [number, (inc: number) => void] {
const [ num, setNum ] = React.useState(initialValue);
const queue = React.useRef<number>(0);
const timeoutId = React.useRef(0);
const incrementDelayed = React.useCallback(() => {
if (queue.current === 0) {
return;
}
queue.current--;
setNum(prev => prev + 1);
timeoutId.current = 0;
}, []);
const increment = React.useCallback((inc: number) => {
if (inc < 1) {
return;
}
queue.current += inc;
if (!timeoutId.current) {
timeoutId.current = window.setTimeout(incrementDelayed, 0);
}
}, [ incrementDelayed ]);
React.useEffect(() => {
if (queue.current > 0 && !timeoutId.current) {
const delay = _clamp(MAX_DELAY / queue.current * 1.5, MIN_DELAY, MAX_DELAY);
timeoutId.current = window.setTimeout(incrementDelayed, delay);
}
}, [ incrementDelayed, num ]);
React.useEffect(() => {
return () => {
window.clearTimeout(timeoutId.current);
};
}, []);
return [ num, increment ];
}
...@@ -2,6 +2,7 @@ import type { NextRouter } from 'next/router'; ...@@ -2,6 +2,7 @@ import type { NextRouter } from 'next/router';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import { ROUTES } from 'lib/link/routes'; import { ROUTES } from 'lib/link/routes';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
...@@ -36,19 +37,19 @@ function assertIsNewPendingTxResponse(response: unknown): response is { pending_ ...@@ -36,19 +37,19 @@ function assertIsNewPendingTxResponse(response: unknown): response is { pending_
export default function useNewTxsSocket() { export default function useNewTxsSocket() {
const router = useRouter(); const router = useRouter();
const [ num, setNum ] = React.useState(0); const [ num, setNum ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
const { topic, event } = getSocketParams(router); const { topic, event } = getSocketParams(router);
const handleNewTxMessage = React.useCallback((response: { transaction: number } | { pending_transaction: number } | unknown) => { const handleNewTxMessage = React.useCallback((response: { transaction: number } | { pending_transaction: number } | unknown) => {
if (assertIsNewTxResponse(response)) { if (assertIsNewTxResponse(response)) {
setNum((prev) => prev + response.transaction); setNum(response.transaction);
} }
if (assertIsNewPendingTxResponse(response)) { if (assertIsNewPendingTxResponse(response)) {
setNum((prev) => prev + response.pending_transaction); setNum(response.pending_transaction);
} }
}, []); }, [ setNum ]);
const handleSocketClose = React.useCallback(() => { const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please click here to load new transactions.'); setSocketAlert('Connection is lost. Please click here to load new transactions.');
......
...@@ -12,6 +12,7 @@ import appConfig from 'configs/app/config'; ...@@ -12,6 +12,7 @@ import appConfig from 'configs/app/config';
// baseUrl: 'https://explorer.anyblock.tools', // baseUrl: 'https://explorer.anyblock.tools',
// paths: { // paths: {
// tx: '/ethereum/ethereum/goerli/transaction', // tx: '/ethereum/ethereum/goerli/transaction',
// address: '/ethereum/ethereum/goerli/address'
// }, // },
// }, // },
// { // {
...@@ -19,6 +20,7 @@ import appConfig from 'configs/app/config'; ...@@ -19,6 +20,7 @@ import appConfig from 'configs/app/config';
// baseUrl: 'https://goerli.etherscan.io/', // baseUrl: 'https://goerli.etherscan.io/',
// paths: { // paths: {
// tx: '/tx', // tx: '/tx',
// address: '/address',
// }, // },
// }, // },
// ]).replaceAll('"', '\''); // ]).replaceAll('"', '\'');
......
import type { PageParams } from './types';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
export default function getSeo(params: PageParams) {
const networkTitle = getNetworkTitle();
return {
title: params ? `${ params.id } - ${ networkTitle }` : '',
description: params ?
`View the account balance, transactions, and other data for ${ params.id } on the ${ networkTitle }` :
'',
};
}
export type PageParams = {
id: string;
}
...@@ -6,7 +6,7 @@ export const SocketContext = React.createContext<Socket | null>(null); ...@@ -6,7 +6,7 @@ export const SocketContext = React.createContext<Socket | null>(null);
interface SocketProviderProps { interface SocketProviderProps {
children: React.ReactNode; children: React.ReactNode;
url: string; url?: string;
options?: Partial<SocketConnectOption>; options?: Partial<SocketConnectOption>;
} }
...@@ -14,6 +14,10 @@ export function SocketProvider({ children, options, url }: SocketProviderProps) ...@@ -14,6 +14,10 @@ export function SocketProvider({ children, options, url }: SocketProviderProps)
const [ socket, setSocket ] = useState<Socket | null>(null); const [ socket, setSocket ] = useState<Socket | null>(null);
useEffect(() => { useEffect(() => {
if (!url) {
return;
}
const socketInstance = new Socket(url, options); const socketInstance = new Socket(url, options);
socketInstance.connect(); socketInstance.connect();
setSocket(socketInstance); setSocket(socketInstance);
......
export const base = {
chart_data: [
{
date: '2022-11-28',
tx_count: 26815,
},
{
date: '2022-11-27',
tx_count: 34784,
},
{
date: '2022-11-26',
tx_count: 77527,
},
{
date: '2022-11-25',
tx_count: 39687,
},
{
date: '2022-11-24',
tx_count: 40752,
},
{
date: '2022-11-23',
tx_count: 32569,
},
{
date: '2022-11-22',
tx_count: 34449,
},
{
date: '2022-11-21',
tx_count: 106047,
},
{
date: '2022-11-20',
tx_count: 107713,
},
{
date: '2022-11-19',
tx_count: 96311,
},
{
date: '2022-11-18',
tx_count: 30828,
},
{
date: '2022-11-17',
tx_count: 27422,
},
{
date: '2022-11-16',
tx_count: 75898,
},
{
date: '2022-11-15',
tx_count: 84084,
},
{
date: '2022-11-14',
tx_count: 62266,
},
{
date: '2022-11-13',
tx_count: 22338,
},
{
date: '2022-11-12',
tx_count: 86764,
},
{
date: '2022-11-11',
tx_count: 79493,
},
{
date: '2022-11-10',
tx_count: 92887,
},
{
date: '2022-11-09',
tx_count: 43691,
},
{
date: '2022-11-08',
tx_count: 74197,
},
{
date: '2022-11-07',
tx_count: 58131,
},
{
date: '2022-11-06',
tx_count: 62477,
},
{
date: '2022-11-05',
tx_count: 82897,
},
{
date: '2022-11-04',
tx_count: 91725,
},
{
date: '2022-11-03',
tx_count: 83667,
},
{
date: '2022-11-02',
tx_count: 63743,
},
{
date: '2022-11-01',
tx_count: 152059,
},
{
date: '2022-10-31',
tx_count: 62519,
},
{
date: '2022-10-30',
tx_count: 48569,
},
{
date: '2022-10-29',
tx_count: 36789,
},
],
};
import type { HomeStats } from 'types/api/stats';
export const base: HomeStats = {
average_block_time: 6212.0,
coin_price: '0.00199678',
gas_prices: {
average: 48.0,
fast: 67.5,
slow: 48.0,
},
gas_used_today: '4108680603',
market_cap: '330809.96443288102524',
network_utilization_percentage: 1.55372064,
static_gas_price: '10',
total_addresses: '19667249',
total_blocks: '30215608',
total_gas_used: '0',
total_transactions: '82258122',
transactions_today: '26815',
};
...@@ -61,7 +61,6 @@ export const base: Transaction = { ...@@ -61,7 +61,6 @@ export const base: Transaction = {
tx_tag: null, tx_tag: null,
tx_types: [ tx_types: [
'contract_call', 'contract_call',
'token_transfer',
], ],
type: 2, type: 2,
value: '42000000000000000000', value: '42000000000000000000',
...@@ -80,6 +79,9 @@ export const withContractCreation: Transaction = { ...@@ -80,6 +79,9 @@ export const withContractCreation: Transaction = {
public_tags: [], public_tags: [],
watchlist_names: [], watchlist_names: [],
}, },
tx_types: [
'contract_creation',
],
}; };
export const withTokenTransfer: Transaction = { export const withTokenTransfer: Transaction = {
...@@ -100,6 +102,9 @@ export const withTokenTransfer: Transaction = { ...@@ -100,6 +102,9 @@ export const withTokenTransfer: Transaction = {
tokenTransferMock.erc1155, tokenTransferMock.erc1155,
tokenTransferMock.erc1155multiple, tokenTransferMock.erc1155multiple,
], ],
tx_types: [
'token_transfer',
],
}; };
export const withDecodedRevertReason: Transaction = { export const withDecodedRevertReason: Transaction = {
......
...@@ -4,16 +4,21 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; ...@@ -4,16 +4,21 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppProps } from 'next/app'; import type { AppProps } from 'next/app';
import React, { useState } from 'react'; import React, { useState } from 'react';
import appConfig from 'configs/app/config';
import { AppContextProvider } from 'lib/appContext'; import { AppContextProvider } from 'lib/appContext';
import { Chakra } from 'lib/Chakra'; import { Chakra } from 'lib/Chakra';
import useConfigSentry from 'lib/hooks/useConfigSentry'; import useConfigSentry from 'lib/hooks/useConfigSentry';
import type { ErrorType } from 'lib/hooks/useFetch'; import type { ErrorType } from 'lib/hooks/useFetch';
import useScrollDirection from 'lib/hooks/useScrollDirection';
import { SocketProvider } from 'lib/socket/context';
import theme from 'theme'; import theme from 'theme';
import ScrollDirectionContext from 'ui/ScrollDirectionContext';
import AppError from 'ui/shared/AppError/AppError'; import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary'; import ErrorBoundary from 'ui/shared/ErrorBoundary';
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
useConfigSentry(); useConfigSentry();
const directionContext = useScrollDirection();
const [ queryClient ] = useState(() => new QueryClient({ const [ queryClient ] = useState(() => new QueryClient({
defaultOptions: { defaultOptions: {
...@@ -57,7 +62,11 @@ function MyApp({ Component, pageProps }: AppProps) { ...@@ -57,7 +62,11 @@ function MyApp({ Component, pageProps }: AppProps) {
<ErrorBoundary renderErrorScreen={ renderErrorScreen } onError={ handleError }> <ErrorBoundary renderErrorScreen={ renderErrorScreen } onError={ handleError }>
<AppContextProvider pageProps={ pageProps }> <AppContextProvider pageProps={ pageProps }>
<QueryClientProvider client={ queryClient }> <QueryClientProvider client={ queryClient }>
<Component { ...pageProps }/> <ScrollDirectionContext.Provider value={ directionContext }>
<SocketProvider url={ `${ appConfig.api.socket }${ appConfig.api.basePath }/socket/v2` }>
<Component { ...pageProps }/>
</SocketProvider>
</ScrollDirectionContext.Provider>
<ReactQueryDevtools/> <ReactQueryDevtools/>
</QueryClientProvider> </QueryClientProvider>
</AppContextProvider> </AppContextProvider>
......
...@@ -10,11 +10,11 @@ class MyDocument extends Document { ...@@ -10,11 +10,11 @@ class MyDocument extends Document {
<Html lang="en"> <Html lang="en">
<Head> <Head>
<link <link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
<link <link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600&display=swap" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
<link rel="icon" sizes="32x32" type="image/png" href="/static/favicon-32x32.png"/> <link rel="icon" sizes="32x32" type="image/png" href="/static/favicon-32x32.png"/>
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import type { PageParams } from 'lib/next/address/types';
import getSeo from 'lib/next/address/getSeo';
import Address from 'ui/pages/Address';
const AddressPage: NextPage<PageParams> = ({ id }: PageParams) => {
const { title, description } = getSeo({ id });
return (
<>
<Head>
<title>{ title }</title>
<meta name="description" content={ description }/>
</Head>
<Address/>
</>
);
};
export default AddressPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/addresses/${ req.query.id }/counters`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/addresses/${ req.query.id }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/addresses/${ req.query.id }/token-balances`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
...@@ -88,6 +88,14 @@ const config: PlaywrightTestConfig = { ...@@ -88,6 +88,14 @@ const config: PlaywrightTestConfig = {
colorScheme: 'dark', colorScheme: 'dark',
}, },
}, },
{
name: 'dark color mode mobile',
grep: /\+@dark-mode-mobile/,
use: {
...devices['iPhone 13 Pro'],
colorScheme: 'dark',
},
},
], ],
}; };
......
...@@ -2,13 +2,16 @@ import { ChakraProvider } from '@chakra-ui/react'; ...@@ -2,13 +2,16 @@ import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import { SocketProvider } from 'lib/socket/context';
import { PORT } from 'playwright/fixtures/socketServer';
import theme from 'theme'; import theme from 'theme';
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode;
withSocket?: boolean;
} }
const TestApp = ({ children }: Props) => { const TestApp = ({ children, withSocket }: Props) => {
const [ queryClient ] = React.useState(() => new QueryClient({ const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
...@@ -21,7 +24,9 @@ const TestApp = ({ children }: Props) => { ...@@ -21,7 +24,9 @@ const TestApp = ({ children }: Props) => {
return ( return (
<ChakraProvider theme={ theme }> <ChakraProvider theme={ theme }>
<QueryClientProvider client={ queryClient }> <QueryClientProvider client={ queryClient }>
{ children } <SocketProvider url={ withSocket ? `ws://localhost:${ PORT }` : undefined }>
{ children }
</SocketProvider>
</QueryClientProvider> </QueryClientProvider>
</ChakraProvider> </ChakraProvider>
); );
......
import type { TestFixture } from '@playwright/test';
import type { WebSocket } from 'ws';
import { WebSocketServer } from 'ws';
import type { NewBlockSocketResponse } from 'types/api/block';
type ReturnType = () => Promise<WebSocket>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ArgsType = any;
type Channel = [string, string, string];
export interface SocketServerFixture {
createSocket: ReturnType;
}
export const PORT = 3200;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const createSocket: TestFixture<ReturnType, ArgsType> = async({ page }, use) => {
const socketServer = new WebSocketServer({ port: PORT });
const connectionPromise = new Promise<WebSocket>((resolve) => {
socketServer.on('connection', (socket: WebSocket) => {
resolve(socket);
});
});
await use(() => connectionPromise);
socketServer.close();
};
export const joinChannel = async(socket: WebSocket, channelName: string) => {
return new Promise<[string, string, string]>((resolve, reject) => {
socket.on('message', (msg) => {
try {
const payload: Array<string> = JSON.parse(msg.toString());
if (channelName === payload[2] && payload[3] === 'phx_join') {
socket.send(JSON.stringify([
payload[0],
payload[1],
payload[2],
'phx_reply',
{ response: {}, status: 'ok' },
]));
resolve([ payload[0], payload[1], payload[2] ]);
}
} catch (error) {
reject(error);
}
});
});
};
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void {
socket.send(JSON.stringify([
...channel,
msg,
payload,
]));
}
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -3,17 +3,39 @@ import { ...@@ -3,17 +3,39 @@ import {
createMultiStyleConfigHelpers, createMultiStyleConfigHelpers,
defineStyle, defineStyle,
} from '@chakra-ui/styled-system'; } from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
import { runIfFn } from '@chakra-ui/utils';
const { definePartsStyle, defineMultiStyleConfig } = const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(parts.keys); createMultiStyleConfigHelpers(parts.keys);
const baseStyleControl = defineStyle((props) => {
const { colorScheme: c } = props;
return {
_checked: {
bg: mode(`${ c }.500`, `${ c }.300`)(props),
borderColor: mode(`${ c }.500`, `${ c }.300`)(props),
_hover: {
bg: mode(`${ c }.600`, `${ c }.400`)(props),
borderColor: mode(`${ c }.600`, `${ c }.400`)(props),
},
},
_indeterminate: {
bg: mode(`${ c }.500`, `${ c }.300`)(props),
borderColor: mode(`${ c }.500`, `${ c }.300`)(props),
},
};
});
const baseStyleLabel = defineStyle({ const baseStyleLabel = defineStyle({
_disabled: { opacity: 0.2 }, _disabled: { opacity: 0.2 },
}); });
const baseStyle = definePartsStyle({ const baseStyle = definePartsStyle((props) => ({
label: baseStyleLabel, label: baseStyleLabel,
}); control: runIfFn(baseStyleControl, props),
}));
const Checkbox = defineMultiStyleConfig({ const Checkbox = defineMultiStyleConfig({
baseStyle, baseStyle,
......
...@@ -4,6 +4,7 @@ import { getColor, mode } from '@chakra-ui/theme-tools'; ...@@ -4,6 +4,7 @@ import { getColor, mode } from '@chakra-ui/theme-tools';
import getDefaultFormColors from '../utils/getDefaultFormColors'; import getDefaultFormColors from '../utils/getDefaultFormColors';
const baseStyle = defineStyle({ const baseStyle = defineStyle({
display: 'flex',
fontSize: 'md', fontSize: 'md',
marginEnd: '3', marginEnd: '3',
mb: '2', mb: '2',
......
...@@ -9,10 +9,16 @@ const { defineMultiStyleConfig, definePartsStyle } = ...@@ -9,10 +9,16 @@ const { defineMultiStyleConfig, definePartsStyle } =
const baseStyleLabel = defineStyle({ const baseStyleLabel = defineStyle({
_disabled: { opacity: 0.2 }, _disabled: { opacity: 0.2 },
width: 'fit-content',
});
const baseStyleContainer = defineStyle({
width: 'fit-content',
}); });
const baseStyle = definePartsStyle({ const baseStyle = definePartsStyle({
label: baseStyleLabel, label: baseStyleLabel,
container: baseStyleContainer,
}); });
const Radio = defineMultiStyleConfig({ const Radio = defineMultiStyleConfig({
......
import { switchAnatomy as parts } from '@chakra-ui/anatomy';
import { defineStyle, createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys);
const baseStyleTrack = defineStyle((props) => {
const { colorScheme: c } = props;
return {
_checked: {
bg: mode(`${ c }.500`, `${ c }.300`)(props),
_hover: {
bg: mode(`${ c }.600`, `${ c }.400`)(props),
},
},
};
});
const baseStyle = definePartsStyle((props) => ({
track: baseStyleTrack(props),
}));
const Switch = defineMultiStyleConfig({
baseStyle,
});
export default Switch;
...@@ -36,8 +36,7 @@ const sizes = { ...@@ -36,8 +36,7 @@ const sizes = {
fontSize: 'sm', fontSize: 'sm',
}, },
td: { td: {
px: 4, p: 4,
py: 6,
}, },
}), }),
sm: definePartsStyle({ sm: definePartsStyle({
...@@ -48,7 +47,7 @@ const sizes = { ...@@ -48,7 +47,7 @@ const sizes = {
}, },
td: { td: {
px: '10px', px: '10px',
py: 6, py: 4,
fontSize: 'sm', fontSize: 'sm',
fontWeight: 500, fontWeight: 500,
}, },
...@@ -61,7 +60,7 @@ const sizes = { ...@@ -61,7 +60,7 @@ const sizes = {
}, },
td: { td: {
px: '6px', px: '6px',
py: 6, py: 4,
fontSize: 'sm', fontSize: 'sm',
fontWeight: 500, fontWeight: 500,
}, },
......
...@@ -34,6 +34,7 @@ const baseStyleContainer = defineStyle({ ...@@ -34,6 +34,7 @@ const baseStyleContainer = defineStyle({
display: 'inline-block', display: 'inline-block',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
borderRadius: 'sm', borderRadius: 'sm',
...transitionProps, ...transitionProps,
}); });
......
...@@ -13,6 +13,7 @@ import Popover from './Popover'; ...@@ -13,6 +13,7 @@ import Popover from './Popover';
import Radio from './Radio'; import Radio from './Radio';
import Skeleton from './Skeleton'; import Skeleton from './Skeleton';
import Spinner from './Spinner'; import Spinner from './Spinner';
import Switch from './Switch';
import Table from './Table'; import Table from './Table';
import Tabs from './Tabs'; import Tabs from './Tabs';
import Tag from './Tag'; import Tag from './Tag';
...@@ -36,6 +37,7 @@ const components = { ...@@ -36,6 +37,7 @@ const components = {
Radio, Radio,
Skeleton, Skeleton,
Spinner, Spinner,
Switch,
Tabs, Tabs,
Table, Table,
Tag, Tag,
......
...@@ -25,8 +25,11 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) { ...@@ -25,8 +25,11 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
_disabled: { _disabled: {
opacity: 1, opacity: 1,
backgroundColor: mode('gray.200', 'whiteAlpha.200')(props), backgroundColor: mode('gray.200', 'whiteAlpha.200')(props),
border: 'none', borderColor: 'transparent',
cursor: 'not-allowed', cursor: 'not-allowed',
_hover: {
borderColor: 'transparent',
},
}, },
_invalid: { _invalid: {
borderColor: getColor(theme, ec), borderColor: getColor(theme, ec),
......
import type { AddressTag, WatchlistName } from './addressParams';
import type { TokenInfo } from './tokenInfo';
export interface Address {
block_number_balance_updated_at: number | null;
coin_balance: string | null;
creator_address_hash: string | null;
creation_tx_hash: string | null;
exchange_rate: string | null;
hash: string;
implementation_address: string | null;
implementation_name: string | null;
is_contract: boolean;
is_verified: boolean;
name: string | null;
private_tags: Array<AddressTag> | null;
public_tags: Array<AddressTag> | null;
tokenInfo: TokenInfo | null;
watchlist_names: Array<WatchlistName> | null;
}
export interface AddressCounters {
transaction_count: string;
token_transfer_count: string;
gas_usage_count: string;
validation_count: string | null;
}
export interface AddressTokenBalance {
token: TokenInfo;
token_id: string | null;
value: string;
}
export type Stats = { export type HomeStats = {
total_blocks: string; total_blocks: string;
total_addresses: string; total_addresses: string;
total_transactions: string; total_transactions: string;
...@@ -12,3 +12,7 @@ export type Stats = { ...@@ -12,3 +12,7 @@ export type Stats = {
market_cap: string; market_cap: string;
network_utilization_percentage: number; network_utilization_percentage: number;
} }
export type Stats = {
total_blocks: string;
}
...@@ -3,6 +3,7 @@ export enum QueryKeys { ...@@ -3,6 +3,7 @@ export enum QueryKeys {
profile = 'profile', profile = 'profile',
txsValidate = 'txs-validated', txsValidate = 'txs-validated',
txsPending = 'txs-pending', txsPending = 'txs-pending',
homeStats='homeStats',
stats='stats', stats='stats',
tx = 'tx', tx = 'tx',
txInternals = 'tx-internals', txInternals = 'tx-internals',
...@@ -16,5 +17,8 @@ export enum QueryKeys { ...@@ -16,5 +17,8 @@ export enum QueryKeys {
chartsMarket = 'charts-market', chartsMarket = 'charts-market',
indexBlocks='indexBlocks', indexBlocks='indexBlocks',
indexTxs='indexTxs', indexTxs='indexTxs',
jsonRpcUrl='json-rpc-url' jsonRpcUrl='json-rpc-url',
address='address',
addressCounters='address-counters',
addressTokenBalances='address-token-balances',
} }
export type StatsSection = { id: StatsSectionIds; title: string; charts: Array<StatsChart> }
export type StatsSectionIds = keyof typeof StatsSectionId;
export enum StatsSectionId { export enum StatsSectionId {
'all', 'all',
'accounts', 'accounts',
...@@ -5,9 +7,9 @@ export enum StatsSectionId { ...@@ -5,9 +7,9 @@ export enum StatsSectionId {
'transactions', 'transactions',
'gas', 'gas',
} }
export type StatsSectionIds = keyof typeof StatsSectionId;
export type StatsSection = { id: StatsSectionIds; value: string }
export type StatsInterval = { id: StatsIntervalIds; title: string }
export type StatsIntervalIds = keyof typeof StatsIntervalId;
export enum StatsIntervalId { export enum StatsIntervalId {
'all', 'all',
'oneMonth', 'oneMonth',
...@@ -15,5 +17,11 @@ export enum StatsIntervalId { ...@@ -15,5 +17,11 @@ export enum StatsIntervalId {
'sixMonths', 'sixMonths',
'oneYear', 'oneYear',
} }
export type StatsIntervalIds = keyof typeof StatsIntervalId;
export type StatsInterval = { id: StatsIntervalIds; value: string } export type StatsChart = {
visible?: boolean;
id: string;
title: string;
description: string;
apiMethodURL: string;
}
...@@ -17,7 +17,8 @@ export interface NetworkExplorer { ...@@ -17,7 +17,8 @@ export interface NetworkExplorer {
title: string; title: string;
baseUrl: string; baseUrl: string;
paths: { paths: {
tx: string; tx?: string;
address?: string;
}; };
} }
......
import { Box, Flex, Text, Icon, Button, Grid, Select } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import { useRouter } from 'next/router';
import React from 'react';
import type { Address as TAddress, AddressCounters, AddressTokenBalance } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import metamaskIcon from 'icons/metamask.svg';
import qrCodeIcon from 'icons/qr_code.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import walletIcon from 'icons/wallet.svg';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressIcon from 'ui/shared/address/AddressIcon';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import ExternalLink from 'ui/shared/ExternalLink';
import HashStringShorten from 'ui/shared/HashStringShorten';
interface Props {
addressQuery: UseQueryResult<TAddress>;
}
const AddressDetails = ({ addressQuery }: Props) => {
const router = useRouter();
const fetch = useFetch();
const isMobile = useIsMobile();
const countersQuery = useQuery<unknown, unknown, AddressCounters>(
[ QueryKeys.addressCounters, router.query.id ],
async() => await fetch(`/node-api/addresses/${ router.query.id }/counters`),
{
enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
},
);
const tokenBalancesQuery = useQuery<unknown, unknown, Array<AddressTokenBalance>>(
[ QueryKeys.addressTokenBalances, router.query.id ],
async() => await fetch(`/node-api/addresses/${ router.query.id }/token-balances`),
{
enabled: Boolean(router.query.id) && Boolean(addressQuery.data),
},
);
if (countersQuery.isLoading || addressQuery.isLoading || tokenBalancesQuery.isLoading) {
return <Box>loading</Box>;
}
if (countersQuery.isError || addressQuery.isError || tokenBalancesQuery.isError) {
return <Box>error</Box>;
}
const explorers = appConfig.network.explorers.filter(({ paths }) => paths.address);
return (
<Box>
<Flex alignItems="center">
<AddressIcon hash={ addressQuery.data.hash }/>
<Text ml={ 2 } fontFamily="heading" fontWeight={ 500 }>
{ isMobile ? <HashStringShorten hash={ addressQuery.data.hash }/> : addressQuery.data.hash }
</Text>
<CopyToClipboard text={ addressQuery.data.hash }/>
<Icon as={ metamaskIcon } boxSize={ 6 } ml={ 2 }/>
<Button variant="outline" size="sm" ml={ 3 }>
<Icon as={ starOutlineIcon } boxSize={ 5 }/>
</Button>
<Button variant="outline" size="sm" ml={ 2 }>
<Icon as={ qrCodeIcon } boxSize={ 5 }/>
</Button>
</Flex>
{ explorers.length > 0 && (
<Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap">
<Text>Verify with other explorers</Text>
{ explorers.map((explorer) => {
const url = new URL(explorer.paths.tx + '/' + router.query.id, explorer.baseUrl);
return <ExternalLink key={ explorer.baseUrl } title={ explorer.title } href={ url.toString() }/>;
}) }
</Flex>
) }
<Grid
mt={ 8 }
columnGap={ 8 }
rowGap={{ base: 3, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
>
<DetailsInfoItem
title="Tokens"
hint="All tokens in the account and total value."
alignSelf="center"
>
{ tokenBalancesQuery.data.length > 0 ? (
<>
{ /* TODO will be fixed later when we implement select with custom menu */ }
<Select
size="sm"
borderRadius="base"
focusBorderColor="none"
display="inline-block"
w="auto"
>
{ tokenBalancesQuery.data.map((token) =>
<option key={ token.token.address } value={ token.token.address }>{ token.token.symbol }</option>) }
</Select>
<Button variant="outline" size="sm" ml={ 3 }>
<Icon as={ walletIcon } boxSize={ 5 }/>
</Button>
</>
) : (
'-'
) }
</DetailsInfoItem>
<DetailsInfoItem
title="Transactions"
hint="Number of transactions related to this address."
>
{ Number(countersQuery.data.transaction_count).toLocaleString() }
</DetailsInfoItem>
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address."
>
{ Number(countersQuery.data.token_transfer_count).toLocaleString() }
</DetailsInfoItem>
<DetailsInfoItem
title="Gas used"
hint="Gas used by the address."
>
{ BigNumber(countersQuery.data.gas_usage_count).toFormat() }
</DetailsInfoItem>
{ countersQuery.data.validation_count && (
<DetailsInfoItem
title="Blocks validated"
hint="Number of blocks validated by this validator."
>
{ Number(countersQuery.data.validation_count).toLocaleString() }
</DetailsInfoItem>
) }
</Grid>
</Box>
);
};
export default React.memo(AddressDetails);
...@@ -15,9 +15,9 @@ import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account'; ...@@ -15,9 +15,9 @@ import type { ApiKey, ApiKeys, ApiKeyErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries'; import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import type { ErrorType } from 'lib/hooks/useFetch'; import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props = { type Props = {
data?: ApiKey; data?: ApiKey;
...@@ -33,7 +33,7 @@ type Inputs = { ...@@ -33,7 +33,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255; const NAME_MAX_LENGTH = 255;
const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
mode: 'all', mode: 'all',
defaultValues: { defaultValues: {
token: data?.api_key || '', token: data?.api_key || '',
...@@ -71,7 +71,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -71,7 +71,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}); });
} }
return [ ...(prevData || []), response ]; return [ response, ...(prevData || []) ];
}); });
onClose(); onClose();
...@@ -113,7 +113,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -113,7 +113,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
maxLength={ NAME_MAX_LENGTH } maxLength={ NAME_MAX_LENGTH }
/> />
<FormLabel> <FormLabel>
{ getPlaceholderWithError('Application name for API key (e.g Web3 project)', errors.name?.message) } <InputPlaceholder text="Application name for API key (e.g Web3 project)" error={ errors.name?.message }/>
</FormLabel> </FormLabel>
</FormControl> </FormControl>
); );
...@@ -145,7 +145,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -145,7 +145,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button <Button
size="lg" size="lg"
type="submit" type="submit"
disabled={ !isValid } disabled={ !isValid || !isDirty }
isLoading={ mutation.isLoading } isLoading={ mutation.isLoading }
> >
{ data ? 'Save' : 'Generate API key' } { data ? 'Save' : 'Generate API key' }
......
...@@ -30,7 +30,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => { ...@@ -30,7 +30,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const renderText = useCallback(() => { const renderText = useCallback(() => {
return ( return (
<Text> API key for <Text fontWeight="600" as="span">{ ` "${ data.name || 'name' }" ` }</Text> will be deleted </Text> <Text> API key for <Text fontWeight="700" as="span">{ ` "${ data.name || 'name' }" ` }</Text> will be deleted </Text>
); );
}, [ data.name ]); }, [ data.name ]);
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as blockMock from 'mocks/blocks/block'; import * as blockMock from 'mocks/blocks/block';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import BlocksContent from './BlocksContent'; import BlocksContent from './BlocksContent';
const API_URL = '/node-api/blocks'; const API_URL = '/node-api/blocks';
export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
// FIXME
// test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' });
test('base view +@mobile', async({ mount, page }) => { test('base view +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
...@@ -19,8 +28,52 @@ test('base view +@mobile', async({ mount, page }) => { ...@@ -19,8 +28,52 @@ test('base view +@mobile', async({ mount, page }) => {
<BlocksContent/> <BlocksContent/>
</TestApp>, </TestApp>,
); );
await page.waitForResponse(API_URL);
await expect(component).toHaveScreenshot();
});
test('new item from socket', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.baseListResponse),
}));
const component = await mount(
<TestApp withSocket>
<BlocksContent/>
</TestApp>,
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'blocks:new_block');
socketServer.sendMessage(socket, channel, 'new_block', {
average_block_time: '6212.0',
block: {
...blockMock.base,
height: blockMock.base.height + 1,
timestamp: '2022-11-11T11:59:58Z',
},
});
await expect(component).toHaveScreenshot();
});
test('socket error', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.baseListResponse),
}));
const component = await mount(
<TestApp withSocket>
<BlocksContent/>
</TestApp>,
);
await page.waitForResponse(API_URL), const socket = await createSocket();
await socketServer.joinChannel(socket, 'blocks:new_block');
socket.close();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -2,7 +2,6 @@ import { ...@@ -2,7 +2,6 @@ import {
Box, Box,
Button, Button,
FormControl, FormControl,
FormLabel,
Input, Input,
Textarea, Textarea,
useColorModeValue, useColorModeValue,
...@@ -16,11 +15,11 @@ import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account'; ...@@ -16,11 +15,11 @@ import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries'; import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import type { ErrorType } from 'lib/hooks/useFetch'; import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import { ADDRESS_REGEXP } from 'lib/validations/address'; import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput'; import AddressInput from 'ui/shared/AddressInput';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props = { type Props = {
data?: CustomAbi; data?: CustomAbi;
...@@ -37,7 +36,7 @@ type Inputs = { ...@@ -37,7 +36,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255; const NAME_MAX_LENGTH = 255;
const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, formState: { errors, isValid }, handleSubmit, setError } = useForm<Inputs>({ const { control, formState: { errors, isValid, isDirty }, handleSubmit, setError } = useForm<Inputs>({
defaultValues: { defaultValues: {
contract_address_hash: data?.contract_address_hash || '', contract_address_hash: data?.contract_address_hash || '',
name: data?.name || '', name: data?.name || '',
...@@ -77,7 +76,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -77,7 +76,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
}); });
} }
return [ ...(prevData || []), response ]; return [ response, ...(prevData || []) ];
}); });
onClose(); onClose();
...@@ -119,7 +118,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -119,7 +118,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
isInvalid={ Boolean(errors.name) } isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH } maxLength={ NAME_MAX_LENGTH }
/> />
<FormLabel>{ getPlaceholderWithError('Project name', errors.name?.message) }</FormLabel> <InputPlaceholder text="Project name" error={ errors.name?.message }/>
</FormControl> </FormControl>
); );
}, [ errors, formBackgroundColor ]); }, [ errors, formBackgroundColor ]);
...@@ -133,7 +132,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -133,7 +132,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
minH="300px" minH="300px"
isInvalid={ Boolean(errors.abi) } isInvalid={ Boolean(errors.abi) }
/> />
<FormLabel>{ getPlaceholderWithError(`Custom ABI [{...}] (JSON format)`, errors.abi?.message) }</FormLabel> <InputPlaceholder text="Custom ABI [{...}] (JSON format)" error={ errors.abi?.message }/>
</FormControl> </FormControl>
); );
}, [ errors, formBackgroundColor ]); }, [ errors, formBackgroundColor ]);
...@@ -171,7 +170,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => { ...@@ -171,7 +170,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button <Button
size="lg" size="lg"
type="submit" type="submit"
disabled={ !isValid } disabled={ !isValid || !isDirty }
isLoading={ mutation.isLoading } isLoading={ mutation.isLoading }
> >
{ data ? 'Save' : 'Create custom ABI' } { data ? 'Save' : 'Create custom ABI' }
......
...@@ -29,7 +29,7 @@ const DeleteCustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data }) => { ...@@ -29,7 +29,7 @@ const DeleteCustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const renderText = useCallback(() => { const renderText = useCallback(() => {
return ( return (
<Text>Custom ABI for<Text fontWeight="600" as="span">{ ` "${ data.name || 'name' }" ` }</Text>will be deleted</Text> <Text>Custom ABI for<Text fontWeight="700" as="span">{ ` "${ data.name || 'name' }" ` }</Text>will be deleted</Text>
); );
}, [ data.name ]); }, [ data.name ]);
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import * as statsMock from 'mocks/stats/index';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import LatestBlocks from './LatestBlocks';
const STATS_API_URL = '/node-api/stats';
const BLOCKS_API_URL = '/node-api/index/blocks';
export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
test('default view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify([
blockMock.base,
blockMock.base2,
]),
}));
const component = await mount(
<TestApp>
<LatestBlocks/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test.describe('socket', () => {
test.describe.configure({ mode: 'serial' });
test('new item', async({ mount, page, createSocket }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify([
blockMock.base,
blockMock.base2,
]),
}));
const component = await mount(
<TestApp withSocket>
<LatestBlocks/>
</TestApp>,
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'blocks:new_block');
socketServer.sendMessage(socket, channel, 'new_block', {
average_block_time: '6212.0',
block: {
...blockMock.base,
height: blockMock.base.height + 1,
timestamp: '2022-11-11T11:59:58Z',
},
});
await expect(component).toHaveScreenshot();
});
});
...@@ -5,7 +5,7 @@ import React from 'react'; ...@@ -5,7 +5,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { Block } from 'types/api/block'; import type { Block } from 'types/api/block';
import type { Stats } from 'types/api/stats'; import type { HomeStats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
...@@ -31,8 +31,8 @@ const LatestBlocks = () => { ...@@ -31,8 +31,8 @@ const LatestBlocks = () => {
); );
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const statsQueryResult = useQuery<unknown, unknown, Stats>( const statsQueryResult = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.stats ], [ QueryKeys.homeStats ],
() => fetch('/node-api/stats'), () => fetch('/node-api/stats'),
); );
...@@ -109,10 +109,10 @@ const LatestBlocks = () => { ...@@ -109,10 +109,10 @@ const LatestBlocks = () => {
} }
return ( return (
<> <Box width={{ base: '100%', lg: '280px' }}>
<Heading as="h4" size="sm" mb={{ base: 4, lg: 7 }}>Latest Blocks</Heading> <Heading as="h4" size="sm" mb={ 4 }>Latest Blocks</Heading>
{ content } { content }
</> </Box>
); );
}; };
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { ROUTES } from 'lib/link/routes';
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';
import LatestTxs from './LatestTxs';
export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
test('default view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route('/node-api/stats', (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route('/node-api/index/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.describe('socket', () => {
test.describe.configure({ mode: 'serial' });
const hooksConfig = {
router: {
pathname: ROUTES.network_index.pattern,
query: {},
},
};
test('new item', async({ mount, page, createSocket }) => {
await page.route('/node-api/stats', (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route('/node-api/index/txs', (route) => route.fulfill({
status: 200,
body: JSON.stringify([
txMock.base,
txMock.withContractCreation,
txMock.withTokenTransfer,
]),
}));
const component = await mount(
<TestApp withSocket>
<LatestTxs/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'transactions:new_transaction');
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
await expect(component).toHaveScreenshot();
});
});
...@@ -27,7 +27,7 @@ const LatestTransactions = () => { ...@@ -27,7 +27,7 @@ const LatestTransactions = () => {
if (isLoading) { if (isLoading) {
content = ( content = (
<> <>
<Skeleton h="56px" w="100%"/> <Skeleton h="56px" w="100%" borderBottomLeftRadius={ 0 } borderBottomRightRadius={ 0 }/>
{ Array.from(Array(txsCount)).map((item, index) => <LatestTxsItemSkeleton key={ index }/>) } { Array.from(Array(txsCount)).map((item, index) => <LatestTxsItemSkeleton key={ index }/>) }
</> </>
); );
...@@ -53,10 +53,10 @@ const LatestTransactions = () => { ...@@ -53,10 +53,10 @@ const LatestTransactions = () => {
} }
return ( return (
<> <Box flexGrow={ 1 }>
<Heading as="h4" size="sm" mb={ 4 }>Latest transactions</Heading> <Heading as="h4" size="sm" mb={ 4 }>Latest transactions</Heading>
{ content } { content }
</> </Box>
); );
}; };
......
import { Alert, Spinner, Text, Link, useColorModeValue, useTheme } from '@chakra-ui/react'; import { Alert, Text, Link, useColorModeValue, useTheme } from '@chakra-ui/react';
import { transparentize } from '@chakra-ui/theme-tools'; import { transparentize } from '@chakra-ui/theme-tools';
import React from 'react'; import React from 'react';
...@@ -18,18 +18,14 @@ const LatestTxsNotice = ({ className }: Props) => { ...@@ -18,18 +18,14 @@ const LatestTxsNotice = ({ className }: Props) => {
content = 'Connection is lost. Please reload page'; content = 'Connection is lost. Please reload page';
} else if (!num) { } else if (!num) {
content = ( content = (
<> <Text>scanning new transactions...</Text>
<Spinner size="sm" mr={ 3 }/>
<Text>scanning new transactions ...</Text>
</>
); );
} else { } else {
const txsUrl = link('txs'); const txsUrl = link('txs');
content = ( content = (
<> <>
<Spinner size="sm" mr={ 3 }/> <Link href={ txsUrl }>{ num } more transaction{ num > 1 ? 's' : '' }</Link>
<Text as="span" whiteSpace="pre">+ { num } new transaction{ num > 1 ? 's' : '' }. </Text> <Text whiteSpace="pre"> ha{ num > 1 ? 've' : 's' } come in</Text>
<Link href={ txsUrl }>View all</Link>
</> </>
); );
} }
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as statsMock from 'mocks/stats/index';
import TestApp from 'playwright/TestApp';
import Stats from './Stats';
const API_URL = '/node-api/stats';
test('all items +@mobile +@dark-mode +@desktop-xl', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
const component = await mount(
<TestApp>
<Stats/>
</TestApp>,
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
...@@ -2,7 +2,7 @@ import { Grid } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Grid } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import { Stats } from 'types/api/stats'; import type { HomeStats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
...@@ -26,8 +26,8 @@ let itemsCount = 5; ...@@ -26,8 +26,8 @@ let itemsCount = 5;
const Stats = () => { const Stats = () => {
const fetch = useFetch(); const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Stats>( const { data, isLoading, isError } = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.stats ], [ QueryKeys.homeStats ],
async() => await fetch(`/node-api/stats`), async() => await fetch(`/node-api/stats`),
); );
...@@ -79,10 +79,10 @@ const Stats = () => { ...@@ -79,10 +79,10 @@ const Stats = () => {
return ( return (
<Grid <Grid
gridTemplateColumns={{ lg: `repeat(${ itemsCount }, 1fr)`, base: 'none' }} gridTemplateColumns={{ lg: `repeat(${ itemsCount }, 1fr)`, base: '1fr 1fr' }}
gridTemplateRows={{ lg: 'none', base: `repeat(${ itemsCount }, 1fr)` }} gridTemplateRows={{ lg: 'none', base: undefined }}
gridGap="10px" gridGap="10px"
marginTop="32px" marginTop="24px"
> >
{ content } { content }
</Grid> </Grid>
......
import { Flex, Icon, Center, Text, LightMode } from '@chakra-ui/react'; import { Flex, Icon, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
type Props = { type Props = {
...@@ -9,30 +9,21 @@ type Props = { ...@@ -9,30 +9,21 @@ type Props = {
const StatsItem = ({ icon, title, value }: Props) => { const StatsItem = ({ icon, title, value }: Props) => {
return ( return (
<LightMode> <Flex
<Flex backgroundColor={ useColorModeValue('blue.50', 'blue.800') }
backgroundColor="blue.50" padding={ 3 }
padding={ 5 } borderRadius="md"
borderRadius="16px" flexDirection={{ base: 'row', lg: 'column', xl: 'row' }}
flexDirection={{ base: 'row', lg: 'column', xl: 'row' }} alignItems="center"
alignItems="center" columnGap={ 3 }
> rowGap={ 2 }
<Center >
backgroundColor="green.100" <Icon as={ icon } boxSize={ 7 }/>
borderRadius="12px" <Flex flexDirection="column" alignItems={{ base: 'start', lg: 'center', xl: 'start' }}>
w={ 10 } <Text variant="secondary" fontSize="xs" lineHeight="16px">{ title }</Text>
h={ 10 } <Text fontWeight={ 500 } fontSize="md">{ value }</Text>
mr={{ base: 4, lg: 0, xl: 4 }}
mb={{ base: 0, lg: 2, xl: 0 }}
>
<Icon as={ icon } boxSize={ 7 } color="black"/>
</Center>
<Flex flexDirection="column" alignItems={{ base: 'start', lg: 'center', xl: 'start' }}>
<Text variant="secondary" fontSize="xs" lineHeight="16px">{ title }</Text>
<Text fontWeight={ 500 } fontSize="md">{ value }</Text>
</Flex>
</Flex> </Flex>
</LightMode> </Flex>
); );
}; };
......
...@@ -7,16 +7,16 @@ const StatsItemSkeleton = () => { ...@@ -7,16 +7,16 @@ const StatsItemSkeleton = () => {
return ( return (
<Flex <Flex
backgroundColor={ bgColor } backgroundColor={ bgColor }
padding={ 5 } padding={ 3 }
borderRadius="16px" borderRadius="md"
flexDirection={{ base: 'row', lg: 'column', xl: 'row' }} flexDirection={{ base: 'row', lg: 'column', xl: 'row' }}
alignItems="center" alignItems="center"
columnGap={ 3 }
rowGap={ 2 }
> >
<Skeleton <Skeleton
w="40px" w="40px"
h="40px" h="40px"
mr={{ base: 4, lg: 0, xl: 4 }}
mb={{ base: 0, lg: 2, xl: 0 }}
/> />
<Flex flexDirection="column" alignItems={{ base: 'start', lg: 'center', xl: 'start' }}> <Flex flexDirection="column" alignItems={{ base: 'start', lg: 'center', xl: 'start' }}>
<Skeleton w="69px" h="10px" mt="4px" mb="8px"/> <Skeleton w="69px" h="10px" mt="4px" mb="8px"/>
......
...@@ -3,24 +3,27 @@ import type { UseQueryResult } from '@tanstack/react-query'; ...@@ -3,24 +3,27 @@ import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { ChainIndicatorId } from './types'; import type { ChainIndicatorId } from './types';
import type { Stats } from 'types/api/stats'; import type { HomeStats } from 'types/api/stats';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
interface Props { interface Props {
id: ChainIndicatorId; id: ChainIndicatorId;
title: string; title: string;
value: (stats: Stats) => string; value: (stats: HomeStats) => string;
icon: React.ReactNode; icon: React.ReactNode;
isSelected: boolean; isSelected: boolean;
onClick: (id: ChainIndicatorId) => void; onClick: (id: ChainIndicatorId) => void;
stats: UseQueryResult<Stats>; stats: UseQueryResult<HomeStats>;
} }
const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats }: Props) => { const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats }: Props) => {
const bgColor = useColorModeValue('white', 'black');
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const activeBgColorDesktop = useColorModeValue('white', 'gray.900');
const activeBgColorMobile = useColorModeValue('white', 'black');
const activeBgColor = isMobile ? activeBgColorMobile : activeBgColorDesktop;
const handleClick = React.useCallback(() => { const handleClick = React.useCallback(() => {
onClick(id); onClick(id);
}, [ id, onClick ]); }, [ id, onClick ]);
...@@ -58,11 +61,11 @@ const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats ...@@ -58,11 +61,11 @@ const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats
borderRadius="md" borderRadius="md"
cursor="pointer" cursor="pointer"
onClick={ handleClick } onClick={ handleClick }
bgColor={ isSelected ? bgColor : 'inherit' } bgColor={ isSelected ? activeBgColor : 'inherit' }
boxShadow={ isSelected ? 'lg' : 'none' } boxShadow={ isSelected ? 'lg' : 'none' }
zIndex={ isSelected ? 1 : 'initial' } zIndex={ isSelected ? 1 : 'initial' }
_hover={{ _hover={{
bgColor, activeBgColor,
zIndex: 1, zIndex: 1,
}} }}
> >
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as dailyTxsMock from 'mocks/stats/daily_txs';
import * as statsMock from 'mocks/stats/index';
import TestApp from 'playwright/TestApp';
import ChainIndicators from './ChainIndicators';
const STATS_API_URL = '/node-api/stats';
const TX_CHART_API_URL = '/node-api/stats/charts/transactions';
test('daily txs chart +@mobile +@dark-mode +@dark-mode-mobile', async({ mount, page }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route(TX_CHART_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(dailyTxsMock.base),
}));
const component = await mount(
<TestApp>
<ChainIndicators/>
</TestApp>,
);
await page.waitForResponse(STATS_API_URL),
await page.hover('.ChartOverlay', { position: { x: 100, y: 100 } });
await expect(component).toHaveScreenshot();
});
...@@ -2,7 +2,7 @@ import { Box, Flex, Icon, Skeleton, Text, Tooltip, useColorModeValue } from '@ch ...@@ -2,7 +2,7 @@ import { Box, Flex, Icon, Skeleton, Text, Tooltip, useColorModeValue } from '@ch
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { Stats } from 'types/api/stats'; import type { HomeStats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
...@@ -35,13 +35,15 @@ const ChainIndicators = () => { ...@@ -35,13 +35,15 @@ const ChainIndicators = () => {
const queryResult = useFetchChartData(indicator); const queryResult = useFetchChartData(indicator);
const fetch = useFetch(); const fetch = useFetch();
const statsQueryResult = useQuery<unknown, unknown, Stats>( const statsQueryResult = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.stats ], [ QueryKeys.homeStats ],
() => fetch('/node-api/stats'), () => fetch('/node-api/stats'),
); );
const bgColor = useColorModeValue('white', 'black'); const bgColorDesktop = useColorModeValue('white', 'gray.900');
const listBgColor = useColorModeValue('gray.50', 'gray.900'); const bgColorMobile = useColorModeValue('white', 'black');
const listBgColorDesktop = useColorModeValue('gray.50', 'black');
const listBgColorMobile = useColorModeValue('gray.50', 'gray.900');
if (indicators.length === 0) { if (indicators.length === 0) {
return null; return null;
...@@ -68,7 +70,7 @@ const ChainIndicators = () => { ...@@ -68,7 +70,7 @@ const ChainIndicators = () => {
p={{ base: 0, lg: 8 }} p={{ base: 0, lg: 8 }}
borderRadius={{ base: 'none', lg: 'lg' }} borderRadius={{ base: 'none', lg: 'lg' }}
boxShadow={{ base: 'none', lg: 'xl' }} boxShadow={{ base: 'none', lg: 'xl' }}
bgColor={ bgColor } bgColor={{ base: bgColorMobile, lg: bgColorDesktop }}
columnGap={ 12 } columnGap={ 12 }
rowGap={ 0 } rowGap={ 0 }
flexDir={{ base: 'column', lg: 'row' }} flexDir={{ base: 'column', lg: 'row' }}
...@@ -97,7 +99,7 @@ const ChainIndicators = () => { ...@@ -97,7 +99,7 @@ const ChainIndicators = () => {
as="ul" as="ul"
p={ 3 } p={ 3 }
borderRadius="lg" borderRadius="lg"
bgColor={ listBgColor } bgColor={{ base: listBgColorMobile, lg: listBgColorDesktop }}
rowGap={ 3 } rowGap={ 3 }
order={{ base: 1, lg: 2 }} order={{ base: 1, lg: 2 }}
> >
......
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts'; import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { Stats } from 'types/api/stats'; import type { HomeStats } from 'types/api/stats';
import type { QueryKeys } from 'types/client/queries'; import type { QueryKeys } from 'types/client/queries';
import type { TimeChartData } from 'ui/shared/chart/types'; import type { TimeChartData } from 'ui/shared/chart/types';
...@@ -10,7 +10,7 @@ export type ChainIndicatorId = 'daily_txs' | 'coin_price' | 'market_cup'; ...@@ -10,7 +10,7 @@ export type ChainIndicatorId = 'daily_txs' | 'coin_price' | 'market_cup';
export interface TChainIndicator<Q extends ChartsQueryKeys> { export interface TChainIndicator<Q extends ChartsQueryKeys> {
id: ChainIndicatorId; id: ChainIndicatorId;
title: string; title: string;
value: (stats: Stats) => string; value: (stats: HomeStats) => string;
icon: React.ReactNode; icon: React.ReactNode;
hint?: string; hint?: string;
api: { api: {
......
import { Flex, Tag } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { Address } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import AddressDetails from 'ui/address/AddressDetails';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const AddressPageContent = () => {
const router = useRouter();
const fetch = useFetch();
const addressQuery = useQuery<unknown, unknown, Address>(
[ QueryKeys.address, router.query.id ],
async() => await fetch(`/node-api/addresses/${ router.query.id }`),
{
enabled: Boolean(router.query.id),
},
);
const tags = [
...(addressQuery.data?.private_tags || []),
...(addressQuery.data?.public_tags || []),
...(addressQuery.data?.watchlist_names || []),
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
return (
<Page>
<Flex alignItems="center" columnGap={ 3 }>
<PageTitle text={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }/>
{ tags.length > 0 && (
<Flex mb={ 6 } columnGap={ 2 }>
{ tags }
</Flex>
) }
</Flex>
<AddressDetails addressQuery={ addressQuery }/>
</Page>
);
};
export default AddressPageContent;
...@@ -57,7 +57,7 @@ const ApiKeysPage: React.FC = () => { ...@@ -57,7 +57,7 @@ const ApiKeysPage: React.FC = () => {
const description = ( const description = (
<AccountPageDescription> <AccountPageDescription>
Create API keys to use for your RPC and EthRPC API requests. For more information, see { space } Create API keys to use for your RPC and EthRPC API requests. For more information, see { space }
<Link href="https://docs.blockscout.com/for-users/api#api-keys">“How to use a Blockscout API key”</Link>. <Link href="https://docs.blockscout.com/for-users/api#api-keys" target="_blank">“How to use a Blockscout API key”</Link>.
</AccountPageDescription> </AccountPageDescription>
); );
...@@ -107,7 +107,12 @@ const ApiKeysPage: React.FC = () => { ...@@ -107,7 +107,12 @@ const ApiKeysPage: React.FC = () => {
<> <>
{ description } { description }
{ Boolean(data.length) && list } { Boolean(data.length) && list }
<Stack marginTop={ 8 } spacing={ 5 } direction={{ base: 'column', lg: 'row' }}> <Stack
marginTop={ 8 }
spacing={ 5 }
direction={{ base: 'column', lg: 'row' }}
align={{ base: 'start', lg: 'center' }}
>
<Button <Button
size="lg" size="lg"
onClick={ apiKeyModalProps.onOpen } onClick={ apiKeyModalProps.onOpen }
......
...@@ -24,7 +24,7 @@ const BlockPageContent = () => { ...@@ -24,7 +24,7 @@ const BlockPageContent = () => {
return ( return (
<Page> <Page>
<PageTitle text={ `Block #${ router.query.id }` }/> <PageTitle text={ `Block #${ router.query.id }` }/>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 12 }}/> <RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 8 }}/>
</Page> </Page>
); );
}; };
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import * as dailyTxsMock from 'mocks/stats/daily_txs';
import * as statsMock from 'mocks/stats/index';
import * as txMock from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import Home from './Home';
test('default view -@default +@desktop-xl +@mobile +@dark-mode', async({ mount, page }) => {
await page.route('/node-api/stats', (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
await page.route('/node-api/index/blocks', (route) => route.fulfill({
status: 200,
body: JSON.stringify([
blockMock.base,
blockMock.base2,
]),
}));
await page.route('/node-api/index/txs', (route) => route.fulfill({
status: 200,
body: JSON.stringify([
txMock.base,
txMock.withContractCreation,
txMock.withTokenTransfer,
]),
}));
await page.route('/node-api/stats/charts/transactions', (route) => route.fulfill({
status: 200,
body: JSON.stringify(dailyTxsMock.base),
}));
const component = await mount(
<TestApp>
<Home/>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
import { Box, Heading, Flex, LightMode } from '@chakra-ui/react'; import { Box, Heading, Flex, LightMode } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config';
import ChainIndicators from 'ui/home/indicators/ChainIndicators'; import ChainIndicators from 'ui/home/indicators/ChainIndicators';
import LatestBlocks from 'ui/home/LatestBlocks'; import LatestBlocks from 'ui/home/LatestBlocks';
import LatestTxs from 'ui/home/LatestTxs'; import LatestTxs from 'ui/home/LatestTxs';
import Stats from 'ui/home/Stats'; import Stats from 'ui/home/Stats';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import ColorModeToggler from 'ui/snippets/header/ColorModeToggler';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import SearchBar from 'ui/snippets/searchBar/SearchBar'; import SearchBar from 'ui/snippets/searchBar/SearchBar';
const Home = () => { const Home = () => {
return ( return (
<Page hasSearch={ false }> <Page isHomePage>
<Box <Box
w="100%" w="100%"
backgroundImage="radial-gradient(farthest-corner at 0 0, rgba(183, 148, 244, 0.8) 0%, rgba(0, 163, 196, 0.8) 100%)" backgroundImage={ appConfig.homepage.plateGradient }
backgroundColor="blue.400"
borderRadius="24px" borderRadius="24px"
padding={{ base: '24px 40px', lg: '48px' }} padding={{ base: '24px', lg: '48px' }}
minW={{ base: 'unset', lg: '900px' }} minW={{ base: 'unset', lg: '900px' }}
> >
<Heading <Flex mb={{ base: 6, lg: 8 }} justifyContent="space-between">
as="h1" <Heading
size={{ base: 'lg', ld: 'xl' }} as="h1"
fontWeight={{ base: 600, lg: 500 }} size={{ base: 'md', lg: 'xl' }}
color="white" lineHeight={{ base: '32px', lg: '50px' }}
mb={{ base: 6, lg: 8 }} fontWeight={ 500 }
> color="white"
Welcome to Blockscout explorer >
</Heading> Welcome to Blockscout explorer
<LightMode><SearchBar isHomepage/></LightMode> </Heading>
<Flex
alignItems="center"
display={{ base: 'none', lg: 'flex' }}
columnGap={ 12 }
>
<ColorModeToggler trackBg="whiteAlpha.500"/>
<ProfileMenuDesktop/>
</Flex>
</Flex>
<LightMode>
<SearchBar isHomepage/>
</LightMode>
</Box> </Box>
<Stats/> <Stats/>
<ChainIndicators/> <ChainIndicators/>
<Flex mt={ 12 } direction={{ base: 'column', lg: 'row' }}> <Flex mt={ 12 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 8 }>
<Box mr={{ base: 0, lg: 12 }} mb={{ base: 8, lg: 0 }} width={{ base: '100%', lg: '280px' }}><LatestBlocks/></Box> <LatestBlocks/>
<Box flexGrow={ 1 }><LatestTxs/></Box> <LatestTxs/>
</Flex> </Flex>
</Page> </Page>
); );
......
...@@ -23,7 +23,7 @@ const MyProfile = () => { ...@@ -23,7 +23,7 @@ const MyProfile = () => {
} }
return ( return (
<VStack maxW="412px" mt={ 12 } gap={ 5 } alignItems="stretch"> <VStack maxW="412px" mt={ 8 } gap={ 5 } alignItems="stretch">
<UserAvatar size={ 64 } data={ data } isFetched={ isFetched }/> <UserAvatar size={ 64 } data={ data } isFetched={ isFetched }/>
<FormControl variant="floating" id="name" isRequired size="lg"> <FormControl variant="floating" id="name" isRequired size="lg">
<Input <Input
......
...@@ -4,19 +4,42 @@ import React from 'react'; ...@@ -4,19 +4,42 @@ import React from 'react';
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 ChartsWidgetsList from '../stats/ChartsWidgetsList';
import NumberWidgetsList from '../stats/NumberWidgetsList';
import StatsFilters from '../stats/StatsFilters'; import StatsFilters from '../stats/StatsFilters';
import WidgetsList from '../stats/WidgetsList'; import useStats from '../stats/useStats';
const Stats = () => { const Stats = () => {
const {
section,
handleSectionChange,
interval,
handleIntervalChange,
debounceFilterCharts,
displayedCharts,
} = useStats();
return ( return (
<Page> <Page>
<PageTitle text="Ethereum Stats"/> <PageTitle text="Ethereum Stats"/>
<Box mb={{ base: 6, sm: 8 }}> <Box mb={{ base: 6, sm: 8 }}>
<StatsFilters/> <NumberWidgetsList/>
</Box>
<Box mb={{ base: 6, sm: 8 }}>
<StatsFilters
section={ section }
onSectionChange={ handleSectionChange }
interval={ interval }
onIntervalChange={ handleIntervalChange }
onFilterInputChange={ debounceFilterCharts }
/>
</Box> </Box>
<WidgetsList/> <ChartsWidgetsList
charts={ displayedCharts }
/>
</Page> </Page>
); );
}; };
......
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.
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