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
NEXT_PUBLIC_LOGOUT_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_LOGOUT_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_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
NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__
......
......@@ -18,6 +18,8 @@ module.exports = {
'plugin:regexp/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:jest/recommended',
'plugin:playwright/playwright-test',
],
plugins: [
'es5',
......@@ -27,6 +29,7 @@ module.exports = {
'react-hooks',
'jsx-a11y',
'eslint-plugin-import-helpers',
'jest',
],
parser: '@typescript-eslint/parser',
parserOptions: {
......
......@@ -23,6 +23,9 @@ jobs:
- name: Check out the repo
uses: actions/checkout@v3
- name: Inject slug/short variables
uses: rlespinasse/github-slug-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
......@@ -45,7 +48,7 @@ jobs:
- name: Add outputs
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
- name: Build and push
......@@ -56,21 +59,22 @@ jobs:
push: true
cache-from: type=gha
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 }}
build-args: |
GIT_COMMIT_SHA=${{ env.SHORT_SHA }}
GIT_COMMIT_SHA=${{ env.GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT }}
deploy_frontend:
name: Deploy frontend app
needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
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
blockscoutIngressHost: 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
scVerifierIngressHost: sc-verifier
secrets: inherit
......@@ -11,7 +11,7 @@
"detail": "start local dev server",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......@@ -31,7 +31,7 @@
"detail": "start local dev server for POA network",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......@@ -51,7 +51,7 @@
"detail": "start local dev server for Goerli network",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......@@ -77,7 +77,7 @@
},
"presentation": {
"reveal": "never",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......@@ -91,7 +91,7 @@
"detail": "run eslint",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"revealProblems": "onProblem",
},
"icon": {
......@@ -108,11 +108,11 @@
"type": "shell",
"command": "${input:pwDebugFlag} yarn test:pw:local ${relativeFileDirname}/${fileBasename} ${input:pwArgs}",
"problemMatcher": [],
"label": "test: playwright: local for current file",
"label": "pw: local",
"detail": "run visual components tests for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"focus": true,
},
"icon": {
......@@ -127,11 +127,11 @@
"type": "shell",
"command": "yarn test:pw:docker ${relativeFileDirname}/${fileBasename} ${input:pwArgs}",
"problemMatcher": [],
"label": "test: playwright: docker for current file",
"label": "pw: docker",
"detail": "run visual components tests for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"focus": true,
},
"icon": {
......@@ -146,11 +146,11 @@
"type": "shell",
"command": "yarn test:pw:docker ${input:pwArgs}",
"problemMatcher": [],
"label": "test: playwright: docker for all files",
"label": "pw: docker all",
"detail": "run visual components tests",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"focus": true,
},
"icon": {
......@@ -167,11 +167,11 @@
"type": "npm",
"script": "test:jest",
"problemMatcher": [],
"label": "test: jest",
"label": "jest",
"detail": "run jest tests",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"focus": true,
},
"icon": {
......@@ -186,11 +186,11 @@
"type": "npm",
"script": "test:jest:watch",
"problemMatcher": [],
"label": "test: jest: watch",
"label": "jest: watch all",
"detail": "run jest tests in watch mode",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"close": true,
"focus": true,
},
......@@ -206,11 +206,11 @@
"type": "shell",
"command": "yarn test:jest ${relativeFileDirname}/${fileBasename} --watch",
"problemMatcher": [],
"label": "test: jest: watch curent file",
"label": "jest: watch",
"detail": "run jest tests in watch mode for current file",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"focus": true,
},
"icon": {
......@@ -230,7 +230,7 @@
"detail": "build docker image",
"presentation": {
"reveal": "always",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
"focus": true,
......@@ -251,7 +251,7 @@
"detail": "run docker container for POA network",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......@@ -271,7 +271,7 @@
"detail": "format svg files with svgo",
"presentation": {
"reveal": "silent",
"panel": "new",
"panel": "dedicated",
"close": true,
"revealProblems": "onProblem",
},
......
......@@ -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_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_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_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'}}]` |
......@@ -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_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_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
......
......@@ -89,6 +89,8 @@ const config = Object.freeze({
},
homepage: {
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,
showAvgBlockTime: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME) === 'false' ? false : true,
},
......
# 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_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
NEXT_PUBLIC_NETWORK_NAME=Ethereum
......
......@@ -2,8 +2,9 @@
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FEATURED_NETWORKS=[{'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_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
NEXT_PUBLIC_NETWORK_NAME=POA
......
# 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_ENV=testing
......
......@@ -160,14 +160,14 @@ postgres:
resources:
limits:
memory:
_default: "4Gi"
_default: "6Gi"
cpu:
_default: "3"
_default: "4"
requests:
memory:
_default: "4Gi"
_default: "6Gi"
cpu:
_default: "3"
_default: "4"
environment:
POSTGRES_USER:
......@@ -473,7 +473,7 @@ frontend:
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'}]"
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
NEXT_PUBLIC_NETWORK_NAME:
_default: Ethereum
......
......@@ -65,7 +65,7 @@ geth:
frontend:
environment:
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:
_default: ENC[AES256_GCM,data:n/H2AH2n9ovn265iFbbrqeOOWS3s7FXgDv5FXJ2Dz133GuhTaqIU5psWjTraZ/Vh+dVM8M9zBLFfUanCWOz7cerq/QUoSVZjKvIvcyp6F072DGU=,iv:Co/pSR+U0vfkmWR+LDpxcQKDJl0WNbaEZihpzrRJcbc=,tag:HUiCX8WGJXWCDB59Y6iIbw==,type:str]
SENTRY_CSP_REPORT_URI:
......@@ -78,8 +78,8 @@ sops:
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-11-16T11:55:15Z"
mac: ENC[AES256_GCM,data:wd7HZEGH1fJO1ufaUeBtcjUaHS21rcOoiGaQPNptEyEPj6q/60rJ1YQmGqkgi3DNVnGly5FQyYjMIIY3/YqXqTZI6MBhJp4RmpCELbqqzQbAFvbomYURmqG/umeT2+kMrSIF/PXrt4d51e1cod2+H4OY9V09VerH9L07D0nTd48=,iv:dDeTSqvmwps4oQKRVgDqmMf/uxf7Egb+jufwTKtm6F4=,tag:CqOCA4XW7d3C5D4dflIFug==,type:str]
lastmodified: "2022-11-28T16:58:46Z"
mac: ENC[AES256_GCM,data:QJvVfWWWVDk5mI66T9J8EnEyVwmJoGEsWO9Pr8vK7jyC3rhAYD2WdKYfpkbwwMKrJzcMBe7UeaOeEY6aApuMNdobeEjsJAvstXCOBzMe5H9XtAFiAY+oxf8r4ELNvQP/gIBZSja+ehSbXBcaP4DkLn4FboaBhkoE8A37W2R6/QA=,iv:FnIC6iGLEZNwRSrbF81vF6eQuyq0yQHNPRTPrx3FB+8=,tag:LRnZCwYkCh4o8lDUcG2m9A==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -3,7 +3,7 @@ global:
# enable Blockscout deploy
blockscout:
app: blockscout
enabled: true
enabled: false
image:
_default: blockscout/blockscout:latest
replicas:
......@@ -117,7 +117,7 @@ blockscout:
_default: 'true'
postgres:
enabled: true
enabled: false
image: postgres:13.8
port: 5432
......@@ -145,7 +145,7 @@ postgres:
_default: 'trust'
# enable geth deploy
geth:
enabled: true
enabled: false
image:
_default: ethereum/client-go:stable
replicas:
......@@ -200,7 +200,7 @@ geth:
enabled: true
# enable Smart-contract-verifier deploy
scVerifier:
enabled: true
enabled: false
image:
_default: ghcr.io/blockscout/smart-contract-verifier:latest
replicas:
......@@ -347,19 +347,19 @@ frontend:
NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME:
_default: Sokol
_default: Ethereum
NEXT_PUBLIC_NETWORK_SHORT_NAME:
_default: POA
_default: Goerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME:
_default: poa
_default: ethereum
NEXT_PUBLIC_NETWORK_TYPE:
_default: poa_core
_default: goerli
NEXT_PUBLIC_NETWORK_ID:
_default: 77
_default: 5
NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
_default: POA Network Sokol
_default: Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL:
_default: SPOA
_default: ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS:
_default: 18
NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE:
......@@ -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'}]"
NEXT_PUBLIC_API_HOST:
_default: blockscout.com
review: blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_API_BASE_PATH:
_default: /
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM:
......@@ -380,5 +381,11 @@ frontend:
_default: https://blockscoutcom.us.auth0.com/v2/logout
NEXT_PUBLIC_LOGOUT_RETURN_URL:
_default: https://blockscout.com/auth/logout
review: blockscout-main.test.aws-k8s.blockscout.com/auth/logout
NEXT_PUBLIC_HOMEPAGE_CHARTS:
_default: "['daily_txs','coin_price','market_cup']"
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">
<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 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 {
export function Chakra({ cookies, theme, children }: Props) {
const colorModeManager =
typeof cookies === 'string' ?
cookieStorageManagerSSR(cookies) :
cookieStorageManagerSSR(typeof document !== 'undefined' ? document.cookie : cookies) :
localStorageManager;
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';
import { useRouter } from 'next/router';
import React from 'react';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import { ROUTES } from 'lib/link/routes';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
......@@ -36,19 +37,19 @@ function assertIsNewPendingTxResponse(response: unknown): response is { pending_
export default function useNewTxsSocket() {
const router = useRouter();
const [ num, setNum ] = React.useState(0);
const [ num, setNum ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState('');
const { topic, event } = getSocketParams(router);
const handleNewTxMessage = React.useCallback((response: { transaction: number } | { pending_transaction: number } | unknown) => {
if (assertIsNewTxResponse(response)) {
setNum((prev) => prev + response.transaction);
setNum(response.transaction);
}
if (assertIsNewPendingTxResponse(response)) {
setNum((prev) => prev + response.pending_transaction);
setNum(response.pending_transaction);
}
}, []);
}, [ setNum ]);
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please click here to load new transactions.');
......
......@@ -12,6 +12,7 @@ import appConfig from 'configs/app/config';
// baseUrl: 'https://explorer.anyblock.tools',
// paths: {
// tx: '/ethereum/ethereum/goerli/transaction',
// address: '/ethereum/ethereum/goerli/address'
// },
// },
// {
......@@ -19,6 +20,7 @@ import appConfig from 'configs/app/config';
// baseUrl: 'https://goerli.etherscan.io/',
// paths: {
// tx: '/tx',
// address: '/address',
// },
// },
// ]).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);
interface SocketProviderProps {
children: React.ReactNode;
url: string;
url?: string;
options?: Partial<SocketConnectOption>;
}
......@@ -14,6 +14,10 @@ export function SocketProvider({ children, options, url }: SocketProviderProps)
const [ socket, setSocket ] = useState<Socket | null>(null);
useEffect(() => {
if (!url) {
return;
}
const socketInstance = new Socket(url, options);
socketInstance.connect();
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 = {
tx_tag: null,
tx_types: [
'contract_call',
'token_transfer',
],
type: 2,
value: '42000000000000000000',
......@@ -80,6 +79,9 @@ export const withContractCreation: Transaction = {
public_tags: [],
watchlist_names: [],
},
tx_types: [
'contract_creation',
],
};
export const withTokenTransfer: Transaction = {
......@@ -100,6 +102,9 @@ export const withTokenTransfer: Transaction = {
tokenTransferMock.erc1155,
tokenTransferMock.erc1155multiple,
],
tx_types: [
'token_transfer',
],
};
export const withDecodedRevertReason: Transaction = {
......
......@@ -4,16 +4,21 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import type { AppProps } from 'next/app';
import React, { useState } from 'react';
import appConfig from 'configs/app/config';
import { AppContextProvider } from 'lib/appContext';
import { Chakra } from 'lib/Chakra';
import useConfigSentry from 'lib/hooks/useConfigSentry';
import type { ErrorType } from 'lib/hooks/useFetch';
import useScrollDirection from 'lib/hooks/useScrollDirection';
import { SocketProvider } from 'lib/socket/context';
import theme from 'theme';
import ScrollDirectionContext from 'ui/ScrollDirectionContext';
import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
function MyApp({ Component, pageProps }: AppProps) {
useConfigSentry();
const directionContext = useScrollDirection();
const [ queryClient ] = useState(() => new QueryClient({
defaultOptions: {
......@@ -57,7 +62,11 @@ function MyApp({ Component, pageProps }: AppProps) {
<ErrorBoundary renderErrorScreen={ renderErrorScreen } onError={ handleError }>
<AppContextProvider pageProps={ pageProps }>
<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/>
</QueryClientProvider>
</AppContextProvider>
......
......@@ -10,11 +10,11 @@ class MyDocument extends Document {
<Html lang="en">
<Head>
<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"
/>
<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"
/>
<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 = {
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';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import { SocketProvider } from 'lib/socket/context';
import { PORT } from 'playwright/fixtures/socketServer';
import theme from 'theme';
type Props = {
children: React.ReactNode;
withSocket?: boolean;
}
const TestApp = ({ children }: Props) => {
const TestApp = ({ children, withSocket }: Props) => {
const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: {
queries: {
......@@ -21,7 +24,9 @@ const TestApp = ({ children }: Props) => {
return (
<ChakraProvider theme={ theme }>
<QueryClientProvider client={ queryClient }>
{ children }
<SocketProvider url={ withSocket ? `ws://localhost:${ PORT }` : undefined }>
{ children }
</SocketProvider>
</QueryClientProvider>
</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 {
createMultiStyleConfigHelpers,
defineStyle,
} from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
import { runIfFn } from '@chakra-ui/utils';
const { definePartsStyle, defineMultiStyleConfig } =
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({
_disabled: { opacity: 0.2 },
});
const baseStyle = definePartsStyle({
const baseStyle = definePartsStyle((props) => ({
label: baseStyleLabel,
});
control: runIfFn(baseStyleControl, props),
}));
const Checkbox = defineMultiStyleConfig({
baseStyle,
......
......@@ -4,6 +4,7 @@ import { getColor, mode } from '@chakra-ui/theme-tools';
import getDefaultFormColors from '../utils/getDefaultFormColors';
const baseStyle = defineStyle({
display: 'flex',
fontSize: 'md',
marginEnd: '3',
mb: '2',
......
......@@ -9,10 +9,16 @@ const { defineMultiStyleConfig, definePartsStyle } =
const baseStyleLabel = defineStyle({
_disabled: { opacity: 0.2 },
width: 'fit-content',
});
const baseStyleContainer = defineStyle({
width: 'fit-content',
});
const baseStyle = definePartsStyle({
label: baseStyleLabel,
container: baseStyleContainer,
});
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 = {
fontSize: 'sm',
},
td: {
px: 4,
py: 6,
p: 4,
},
}),
sm: definePartsStyle({
......@@ -48,7 +47,7 @@ const sizes = {
},
td: {
px: '10px',
py: 6,
py: 4,
fontSize: 'sm',
fontWeight: 500,
},
......@@ -61,7 +60,7 @@ const sizes = {
},
td: {
px: '6px',
py: 6,
py: 4,
fontSize: 'sm',
fontWeight: 500,
},
......
......@@ -34,6 +34,7 @@ const baseStyleContainer = defineStyle({
display: 'inline-block',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
borderRadius: 'sm',
...transitionProps,
});
......
......@@ -13,6 +13,7 @@ import Popover from './Popover';
import Radio from './Radio';
import Skeleton from './Skeleton';
import Spinner from './Spinner';
import Switch from './Switch';
import Table from './Table';
import Tabs from './Tabs';
import Tag from './Tag';
......@@ -36,6 +37,7 @@ const components = {
Radio,
Skeleton,
Spinner,
Switch,
Tabs,
Table,
Tag,
......
......@@ -25,8 +25,11 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
_disabled: {
opacity: 1,
backgroundColor: mode('gray.200', 'whiteAlpha.200')(props),
border: 'none',
borderColor: 'transparent',
cursor: 'not-allowed',
_hover: {
borderColor: 'transparent',
},
},
_invalid: {
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_addresses: string;
total_transactions: string;
......@@ -12,3 +12,7 @@ export type Stats = {
market_cap: string;
network_utilization_percentage: number;
}
export type Stats = {
total_blocks: string;
}
......@@ -3,6 +3,7 @@ export enum QueryKeys {
profile = 'profile',
txsValidate = 'txs-validated',
txsPending = 'txs-pending',
homeStats='homeStats',
stats='stats',
tx = 'tx',
txInternals = 'tx-internals',
......@@ -16,5 +17,8 @@ export enum QueryKeys {
chartsMarket = 'charts-market',
indexBlocks='indexBlocks',
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 {
'all',
'accounts',
......@@ -5,9 +7,9 @@ export enum StatsSectionId {
'transactions',
'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 {
'all',
'oneMonth',
......@@ -15,5 +17,11 @@ export enum StatsIntervalId {
'sixMonths',
'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 {
title: string;
baseUrl: string;
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';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props = {
data?: ApiKey;
......@@ -33,7 +33,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255;
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',
defaultValues: {
token: data?.api_key || '',
......@@ -71,7 +71,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
});
}
return [ ...(prevData || []), response ];
return [ response, ...(prevData || []) ];
});
onClose();
......@@ -113,7 +113,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
maxLength={ NAME_MAX_LENGTH }
/>
<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>
</FormControl>
);
......@@ -145,7 +145,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button
size="lg"
type="submit"
disabled={ !isValid }
disabled={ !isValid || !isDirty }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Generate API key' }
......
......@@ -30,7 +30,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const renderText = useCallback(() => {
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 ]);
......
import { test, expect } from '@playwright/experimental-ct-react';
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as blockMock from 'mocks/blocks/block';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import BlocksContent from './BlocksContent';
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 }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
......@@ -19,8 +28,52 @@ test('base view +@mobile', async({ mount, page }) => {
<BlocksContent/>
</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();
});
......@@ -2,7 +2,6 @@ import {
Box,
Button,
FormControl,
FormLabel,
Input,
Textarea,
useColorModeValue,
......@@ -16,11 +15,11 @@ import type { CustomAbi, CustomAbis, CustomAbiErrors } from 'types/api/account';
import { QueryKeys } from 'types/client/accountQueries';
import getErrorMessage from 'lib/getErrorMessage';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch';
import { ADDRESS_REGEXP } from 'lib/validations/address';
import AddressInput from 'ui/shared/AddressInput';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props = {
data?: CustomAbi;
......@@ -37,7 +36,7 @@ type Inputs = {
const NAME_MAX_LENGTH = 255;
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: {
contract_address_hash: data?.contract_address_hash || '',
name: data?.name || '',
......@@ -77,7 +76,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
});
}
return [ ...(prevData || []), response ];
return [ response, ...(prevData || []) ];
});
onClose();
......@@ -119,7 +118,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH }
/>
<FormLabel>{ getPlaceholderWithError('Project name', errors.name?.message) }</FormLabel>
<InputPlaceholder text="Project name" error={ errors.name?.message }/>
</FormControl>
);
}, [ errors, formBackgroundColor ]);
......@@ -133,7 +132,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
minH="300px"
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>
);
}, [ errors, formBackgroundColor ]);
......@@ -171,7 +170,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button
size="lg"
type="submit"
disabled={ !isValid }
disabled={ !isValid || !isDirty }
isLoading={ mutation.isLoading }
>
{ data ? 'Save' : 'Create custom ABI' }
......
......@@ -29,7 +29,7 @@ const DeleteCustomAbiModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const renderText = useCallback(() => {
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 ]);
......
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';
import type { SocketMessage } from 'lib/socket/types';
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 useFetch from 'lib/hooks/useFetch';
......@@ -31,8 +31,8 @@ const LatestBlocks = () => {
);
const queryClient = useQueryClient();
const statsQueryResult = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
const statsQueryResult = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
() => fetch('/node-api/stats'),
);
......@@ -109,10 +109,10 @@ const LatestBlocks = () => {
}
return (
<>
<Heading as="h4" size="sm" mb={{ base: 4, lg: 7 }}>Latest Blocks</Heading>
<Box width={{ base: '100%', lg: '280px' }}>
<Heading as="h4" size="sm" mb={ 4 }>Latest Blocks</Heading>
{ 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 = () => {
if (isLoading) {
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 }/>) }
</>
);
......@@ -53,10 +53,10 @@ const LatestTransactions = () => {
}
return (
<>
<Box flexGrow={ 1 }>
<Heading as="h4" size="sm" mb={ 4 }>Latest transactions</Heading>
{ 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 React from 'react';
......@@ -18,18 +18,14 @@ const LatestTxsNotice = ({ className }: Props) => {
content = 'Connection is lost. Please reload page';
} else if (!num) {
content = (
<>
<Spinner size="sm" mr={ 3 }/>
<Text>scanning new transactions ...</Text>
</>
<Text>scanning new transactions...</Text>
);
} else {
const txsUrl = link('txs');
content = (
<>
<Spinner size="sm" mr={ 3 }/>
<Text as="span" whiteSpace="pre">+ { num } new transaction{ num > 1 ? 's' : '' }. </Text>
<Link href={ txsUrl }>View all</Link>
<Link href={ txsUrl }>{ num } more transaction{ num > 1 ? 's' : '' }</Link>
<Text whiteSpace="pre"> ha{ num > 1 ? 've' : 's' } come in</Text>
</>
);
}
......
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';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import { Stats } from 'types/api/stats';
import type { HomeStats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
......@@ -26,8 +26,8 @@ let itemsCount = 5;
const Stats = () => {
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
const { data, isLoading, isError } = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
async() => await fetch(`/node-api/stats`),
);
......@@ -79,10 +79,10 @@ const Stats = () => {
return (
<Grid
gridTemplateColumns={{ lg: `repeat(${ itemsCount }, 1fr)`, base: 'none' }}
gridTemplateRows={{ lg: 'none', base: `repeat(${ itemsCount }, 1fr)` }}
gridTemplateColumns={{ lg: `repeat(${ itemsCount }, 1fr)`, base: '1fr 1fr' }}
gridTemplateRows={{ lg: 'none', base: undefined }}
gridGap="10px"
marginTop="32px"
marginTop="24px"
>
{ content }
</Grid>
......
import { Flex, Icon, Center, Text, LightMode } from '@chakra-ui/react';
import { Flex, Icon, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
type Props = {
......@@ -9,30 +9,21 @@ type Props = {
const StatsItem = ({ icon, title, value }: Props) => {
return (
<LightMode>
<Flex
backgroundColor="blue.50"
padding={ 5 }
borderRadius="16px"
flexDirection={{ base: 'row', lg: 'column', xl: 'row' }}
alignItems="center"
>
<Center
backgroundColor="green.100"
borderRadius="12px"
w={ 10 }
h={ 10 }
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
backgroundColor={ useColorModeValue('blue.50', 'blue.800') }
padding={ 3 }
borderRadius="md"
flexDirection={{ base: 'row', lg: 'column', xl: 'row' }}
alignItems="center"
columnGap={ 3 }
rowGap={ 2 }
>
<Icon as={ icon } boxSize={ 7 }/>
<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>
</LightMode>
</Flex>
);
};
......
......@@ -7,16 +7,16 @@ const StatsItemSkeleton = () => {
return (
<Flex
backgroundColor={ bgColor }
padding={ 5 }
borderRadius="16px"
padding={ 3 }
borderRadius="md"
flexDirection={{ base: 'row', lg: 'column', xl: 'row' }}
alignItems="center"
columnGap={ 3 }
rowGap={ 2 }
>
<Skeleton
w="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' }}>
<Skeleton w="69px" h="10px" mt="4px" mb="8px"/>
......
......@@ -3,24 +3,27 @@ import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
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';
interface Props {
id: ChainIndicatorId;
title: string;
value: (stats: Stats) => string;
value: (stats: HomeStats) => string;
icon: React.ReactNode;
isSelected: boolean;
onClick: (id: ChainIndicatorId) => void;
stats: UseQueryResult<Stats>;
stats: UseQueryResult<HomeStats>;
}
const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats }: Props) => {
const bgColor = useColorModeValue('white', 'black');
const isMobile = useIsMobile();
const activeBgColorDesktop = useColorModeValue('white', 'gray.900');
const activeBgColorMobile = useColorModeValue('white', 'black');
const activeBgColor = isMobile ? activeBgColorMobile : activeBgColorDesktop;
const handleClick = React.useCallback(() => {
onClick(id);
}, [ id, onClick ]);
......@@ -58,11 +61,11 @@ const ChainIndicatorItem = ({ id, title, value, icon, isSelected, onClick, stats
borderRadius="md"
cursor="pointer"
onClick={ handleClick }
bgColor={ isSelected ? bgColor : 'inherit' }
bgColor={ isSelected ? activeBgColor : 'inherit' }
boxShadow={ isSelected ? 'lg' : 'none' }
zIndex={ isSelected ? 1 : 'initial' }
_hover={{
bgColor,
activeBgColor,
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
import { useQuery } from '@tanstack/react-query';
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 appConfig from 'configs/app/config';
......@@ -35,13 +35,15 @@ const ChainIndicators = () => {
const queryResult = useFetchChartData(indicator);
const fetch = useFetch();
const statsQueryResult = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
const statsQueryResult = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
() => fetch('/node-api/stats'),
);
const bgColor = useColorModeValue('white', 'black');
const listBgColor = useColorModeValue('gray.50', 'gray.900');
const bgColorDesktop = useColorModeValue('white', 'gray.900');
const bgColorMobile = useColorModeValue('white', 'black');
const listBgColorDesktop = useColorModeValue('gray.50', 'black');
const listBgColorMobile = useColorModeValue('gray.50', 'gray.900');
if (indicators.length === 0) {
return null;
......@@ -68,7 +70,7 @@ const ChainIndicators = () => {
p={{ base: 0, lg: 8 }}
borderRadius={{ base: 'none', lg: 'lg' }}
boxShadow={{ base: 'none', lg: 'xl' }}
bgColor={ bgColor }
bgColor={{ base: bgColorMobile, lg: bgColorDesktop }}
columnGap={ 12 }
rowGap={ 0 }
flexDir={{ base: 'column', lg: 'row' }}
......@@ -97,7 +99,7 @@ const ChainIndicators = () => {
as="ul"
p={ 3 }
borderRadius="lg"
bgColor={ listBgColor }
bgColor={{ base: listBgColorMobile, lg: listBgColorDesktop }}
rowGap={ 3 }
order={{ base: 1, lg: 2 }}
>
......
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 { TimeChartData } from 'ui/shared/chart/types';
......@@ -10,7 +10,7 @@ export type ChainIndicatorId = 'daily_txs' | 'coin_price' | 'market_cup';
export interface TChainIndicator<Q extends ChartsQueryKeys> {
id: ChainIndicatorId;
title: string;
value: (stats: Stats) => string;
value: (stats: HomeStats) => string;
icon: React.ReactNode;
hint?: string;
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 = () => {
const description = (
<AccountPageDescription>
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>
);
......@@ -107,7 +107,12 @@ const ApiKeysPage: React.FC = () => {
<>
{ description }
{ 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
size="lg"
onClick={ apiKeyModalProps.onOpen }
......
......@@ -24,7 +24,7 @@ const BlockPageContent = () => {
return (
<Page>
<PageTitle text={ `Block #${ router.query.id }` }/>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 12 }}/>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 8 }}/>
</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 React from 'react';
import appConfig from 'configs/app/config';
import ChainIndicators from 'ui/home/indicators/ChainIndicators';
import LatestBlocks from 'ui/home/LatestBlocks';
import LatestTxs from 'ui/home/LatestTxs';
import Stats from 'ui/home/Stats';
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';
const Home = () => {
return (
<Page hasSearch={ false }>
<Page isHomePage>
<Box
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"
padding={{ base: '24px 40px', lg: '48px' }}
padding={{ base: '24px', lg: '48px' }}
minW={{ base: 'unset', lg: '900px' }}
>
<Heading
as="h1"
size={{ base: 'lg', ld: 'xl' }}
fontWeight={{ base: 600, lg: 500 }}
color="white"
mb={{ base: 6, lg: 8 }}
>
Welcome to Blockscout explorer
</Heading>
<LightMode><SearchBar isHomepage/></LightMode>
<Flex mb={{ base: 6, lg: 8 }} justifyContent="space-between">
<Heading
as="h1"
size={{ base: 'md', lg: 'xl' }}
lineHeight={{ base: '32px', lg: '50px' }}
fontWeight={ 500 }
color="white"
>
Welcome to Blockscout explorer
</Heading>
<Flex
alignItems="center"
display={{ base: 'none', lg: 'flex' }}
columnGap={ 12 }
>
<ColorModeToggler trackBg="whiteAlpha.500"/>
<ProfileMenuDesktop/>
</Flex>
</Flex>
<LightMode>
<SearchBar isHomepage/>
</LightMode>
</Box>
<Stats/>
<ChainIndicators/>
<Flex mt={ 12 } direction={{ base: 'column', lg: 'row' }}>
<Box mr={{ base: 0, lg: 12 }} mb={{ base: 8, lg: 0 }} width={{ base: '100%', lg: '280px' }}><LatestBlocks/></Box>
<Box flexGrow={ 1 }><LatestTxs/></Box>
<Flex mt={ 12 } direction={{ base: 'column', lg: 'row' }} columnGap={ 12 } rowGap={ 8 }>
<LatestBlocks/>
<LatestTxs/>
</Flex>
</Page>
);
......
......@@ -23,7 +23,7 @@ const MyProfile = () => {
}
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 }/>
<FormControl variant="floating" id="name" isRequired size="lg">
<Input
......
......@@ -4,19 +4,42 @@ import React from 'react';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import ChartsWidgetsList from '../stats/ChartsWidgetsList';
import NumberWidgetsList from '../stats/NumberWidgetsList';
import StatsFilters from '../stats/StatsFilters';
import WidgetsList from '../stats/WidgetsList';
import useStats from '../stats/useStats';
const Stats = () => {
const {
section,
handleSectionChange,
interval,
handleIntervalChange,
debounceFilterCharts,
displayedCharts,
} = useStats();
return (
<Page>
<PageTitle text="Ethereum Stats"/>
<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>
<WidgetsList/>
<ChartsWidgetsList
charts={ displayedCharts }
/>
</Page>
);
};
......
......@@ -82,7 +82,7 @@ const TransactionPageContent = () => {
</Flex>
) }
</Flex>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 12 }}/>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 8 }}/>
</Page>
);
};
......
......@@ -34,7 +34,7 @@ type Inputs = {
const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const fetch = useFetch();
const [ pending, setPending ] = useState(false);
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
mode: 'all',
defaultValues: {
address: data?.address_hash || '',
......@@ -120,7 +120,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
<Button
size="lg"
type="submit"
disabled={ !isValid }
disabled={ !isValid || !isDirty }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
......
......@@ -40,7 +40,7 @@ const DeletePrivateTagModal: React.FC<Props> = ({ isOpen, onClose, data, type })
const renderText = useCallback(() => {
return (
<Text>Tag<Text fontWeight="600" as="span">{ ` "${ tag || 'tag' }" ` }</Text>will be deleted</Text>
<Text>Tag<Text fontWeight="700" as="span">{ ` "${ tag || 'tag' }" ` }</Text>will be deleted</Text>
);
}, [ tag ]);
......
......@@ -35,7 +35,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
const [ pending, setPending ] = useState(false);
const formBackgroundColor = useColorModeValue('white', 'gray.900');
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
mode: 'all',
defaultValues: {
transaction: data?.transaction_hash || '',
......@@ -119,7 +119,7 @@ const TransactionForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) =>
<Button
size="lg"
type="submit"
disabled={ !isValid }
disabled={ !isValid || !isDirty }
isLoading={ pending }
>
{ data ? 'Save changes' : 'Add tag' }
......
......@@ -48,7 +48,7 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
text = (
<>
<Text display="inline" as="span">Public tag</Text>
<Text fontWeight="600" whiteSpace="pre" as="span">{ ` "${ tags[0] }" ` }</Text>
<Text fontWeight="700" whiteSpace="pre" as="span">{ ` "${ tags[0] }" ` }</Text>
<Text as="span">will be removed.</Text>
</>
);
......@@ -57,15 +57,15 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
const tagsText: Array<JSX.Element | string> = [];
tags.forEach((tag, index) => {
if (index < tags.length - 2) {
tagsText.push(<Text fontWeight="600" whiteSpace="pre" as="span">{ ` "${ tag }"` }</Text>);
tagsText.push(<Text fontWeight="700" whiteSpace="pre" as="span">{ ` "${ tag }"` }</Text>);
tagsText.push(',');
}
if (index === tags.length - 2) {
tagsText.push(<Text fontWeight="600" whiteSpace="pre" as="span">{ ` "${ tag }" ` }</Text>);
tagsText.push(<Text fontWeight="700" whiteSpace="pre" as="span">{ ` "${ tag }" ` }</Text>);
tagsText.push('and');
}
if (index === tags.length - 1) {
tagsText.push(<Text fontWeight="600" whiteSpace="pre" as="span">{ ` "${ tag }" ` }</Text>);
tagsText.push(<Text fontWeight="700" whiteSpace="pre" as="span">{ ` "${ tag }" ` }</Text>);
}
});
text = (
......@@ -76,7 +76,7 @@ const DeletePublicTagModal: React.FC<Props> = ({ isOpen, onClose, data, onDelete
}
return (
<>
<Box marginBottom={ 12 }>
<Box marginBottom={ 8 }>
{ text }
</Box>
<FormControl variant="floating" id="tag-delete" backgroundColor={ formBackgroundColor }>
......
......@@ -4,7 +4,7 @@ import React, { useCallback } from 'react';
import type { ControllerRenderProps, Control, FieldError } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
import type { Inputs } from './PublicTagsForm';
......@@ -25,7 +25,7 @@ export default function PublicTagFormComment({ control, error, size }: Props) {
isInvalid={ Boolean(error) }
/>
<FormLabel>
{ getPlaceholderWithError('Specify the reason for adding tags and color preference(s)', error?.message) }
<InputPlaceholder text="Specify the reason for adding tags and color preference(s)" error={ error?.message }/>
</FormLabel>
</FormControl>
);
......
......@@ -61,7 +61,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const fetch = useFetch();
const inputSize = { base: 'md', lg: 'lg' };
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
defaultValues: {
fullName: data?.full_name || '',
email: data?.email || '',
......@@ -123,7 +123,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
});
}
return [ ...(prevData || []), response ];
return [ response, ...(prevData || []) ];
});
changeToDataScreen(true);
......@@ -237,7 +237,7 @@ const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
<Button
size="lg"
type="submit"
disabled={ !isValid }
disabled={ !isValid || !isDirty }
isLoading={ mutation.isLoading }
>
Send request
......
import type { InputProps } from '@chakra-ui/react';
import { FormControl, FormLabel, Input } from '@chakra-ui/react';
import { FormControl, Input } from '@chakra-ui/react';
import React, { useCallback } from 'react';
import type { ControllerRenderProps, FieldError, FieldValues, Path, Control } from 'react-hook-form';
import { Controller } from 'react-hook-form';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
const TEXT_INPUT_MAX_LENGTH = 255;
......@@ -36,7 +36,7 @@ export default function PublicTagsFormInput<Inputs extends FieldValues>({
isInvalid={ Boolean(error) }
maxLength={ TEXT_INPUT_MAX_LENGTH }
/>
<FormLabel>{ getPlaceholderWithError(label, error?.message) }</FormLabel>
<InputPlaceholder text={ label } error={ error?.message }/>
</FormControl>
);
}, [ label, required, error, size ]);
......
......@@ -39,7 +39,7 @@ const AccountPageDescription = ({ children }: {children: React.ReactNode}) => {
);
return (
<Box position="relative" marginBottom={{ base: 6, lg: 12 }}>
<Box position="relative" marginBottom={{ base: 6, lg: 8 }}>
<Text
ref={ ref }
maxHeight={ needCut && !expanded ? `${ CUT_HEIGHT }px` : 'auto' }
......
......@@ -2,13 +2,12 @@ import type { InputProps } from '@chakra-ui/react';
import {
Input,
FormControl,
FormLabel,
} from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps, FieldError, FieldValues, Path } from 'react-hook-form';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import { ADDRESS_LENGTH } from 'lib/validations/address';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
field: ControllerRenderProps<TInputs, TInputName>;
......@@ -33,7 +32,7 @@ export default function AddressInput<Inputs extends FieldValues, Name extends Pa
isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH }
/>
<FormLabel>{ getPlaceholderWithError(placeholder, error?.message) }</FormLabel>
<InputPlaceholder text={ placeholder } error={ error?.message }/>
</FormControl>
);
}
......@@ -13,6 +13,13 @@ interface Props {
}
const CurrencyValue = ({ value, currency = '', decimals, exchangeRate, className, accuracy, accuracyUsd }: Props) => {
if (value === undefined || value === null) {
return (
<Box as="span" className={ className }>
<Text>N/A</Text>
</Box>
);
}
const valueCurr = BigNumber(value).div(BigNumber(10 ** Number(decimals || '18')));
const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat();
......
......@@ -49,6 +49,7 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) =
placeholder={ placeholder }
borderWidth="2px"
textOverflow="ellipsis"
whiteSpace="nowrap"
/>
{ filterQuery ? (
......
......@@ -49,7 +49,7 @@ export default function FormModal<TData>({
<ModalCloseButton/>
<ModalBody mb={ 0 }>
{ (isAlertVisible || text) && (
<Box marginBottom={{ base: 6, lg: 12 }}>
<Box marginBottom={{ base: 6, lg: 8 }}>
{ text && (
<Text lineHeight="30px" mb={ 3 }>
{ text }
......
import { FormLabel, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
text: string;
error?: string;
}
const InputPlaceholder = ({ text, error }: Props) => {
return (
<FormLabel>
<chakra.span>{ text }</chakra.span>
{ error && <chakra.span order={ 3 } whiteSpace="pre"> - { error }</chakra.span> }
</FormLabel>
);
};
export default InputPlaceholder;
......@@ -4,12 +4,8 @@ import React from 'react';
import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch';
import useScrollDirection from 'lib/hooks/useScrollDirection';
import { SocketProvider } from 'lib/socket/context';
import ScrollDirectionContext from 'ui/ScrollDirectionContext';
import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
import PageContent from 'ui/shared/Page/PageContent';
......@@ -20,14 +16,14 @@ interface Props {
children: React.ReactNode;
wrapChildren?: boolean;
hideMobileHeaderOnScrollDown?: boolean;
hasSearch?: boolean;
isHomePage?: boolean;
}
const Page = ({
children,
wrapChildren = true,
hideMobileHeaderOnScrollDown,
hasSearch = true,
isHomePage,
}: Props) => {
const fetch = useFetch();
......@@ -35,32 +31,26 @@ const Page = ({
enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
});
const directionContext = useScrollDirection();
const renderErrorScreen = React.useCallback(() => {
return wrapChildren ?
<PageContent hasSearch={ hasSearch }><AppError statusCode={ 500 } mt="50px"/></PageContent> :
<PageContent isHomePage={ isHomePage }><AppError statusCode={ 500 } mt="50px"/></PageContent> :
<AppError statusCode={ 500 }/>;
}, [ wrapChildren, hasSearch ]);
}, [ isHomePage, wrapChildren ]);
const renderedChildren = wrapChildren ? (
<PageContent hasSearch={ hasSearch }>{ children }</PageContent>
<PageContent isHomePage={ isHomePage }>{ children }</PageContent>
) : children;
return (
<SocketProvider url={ `${ appConfig.api.socket }${ appConfig.api.basePath }/socket/v2` }>
<ScrollDirectionContext.Provider value={ directionContext }>
<Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/>
<Flex flexDir="column" width="100%">
<Header hasSearch={ hasSearch } hideOnScrollDown={ hideMobileHeaderOnScrollDown }/>
<ErrorBoundary renderErrorScreen={ renderErrorScreen }>
{ renderedChildren }
</ErrorBoundary>
</Flex>
</Flex>
</ScrollDirectionContext.Provider>
</SocketProvider>
<Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/>
<Flex flexDir="column" width="100%">
<Header isHomePage={ isHomePage } hideOnScrollDown={ hideMobileHeaderOnScrollDown }/>
<ErrorBoundary renderErrorScreen={ renderErrorScreen }>
{ renderedChildren }
</ErrorBoundary>
</Flex>
</Flex>
);
};
......
......@@ -3,17 +3,17 @@ import React from 'react';
interface Props {
children: React.ReactNode;
hasSearch?: boolean;
isHomePage?: boolean;
}
const PageContent = ({ children, hasSearch }: Props) => {
const PageContent = ({ children, isHomePage }: Props) => {
return (
<Box
as="main"
w="100%"
paddingX={{ base: 4, lg: 12 }}
paddingBottom={ 10 }
paddingTop={{ base: hasSearch ? '138px' : '88px', lg: 0 }}
paddingTop={{ base: isHomePage ? '88px' : '138px', lg: isHomePage ? 9 : 0 }}
>
{ children }
</Box>
......
......@@ -3,7 +3,7 @@ import React from 'react';
const PageTitle = ({ text }: {text: string}) => {
return (
<Heading as="h1" size="lg" marginBottom={{ base: 6, lg: 8 }}>{ text }</Heading>
<Heading as="h1" size="lg" marginBottom={ 6 }>{ text }</Heading>
);
};
......
import {
Input,
FormControl,
FormLabel,
} from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps, FieldError, FieldValues, Path } from 'react-hook-form';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
const TAG_MAX_LENGTH = 35;
......@@ -24,7 +23,7 @@ function TagInput<Inputs extends FieldValues, Name extends Path<Inputs>>({ field
isInvalid={ Boolean(error) }
maxLength={ TAG_MAX_LENGTH }
/>
<FormLabel>{ getPlaceholderWithError(`Private tag (max 35 characters)`, error?.message) }</FormLabel>
<InputPlaceholder text="Private tag (max 35 characters)" error={ error?.message }/>
</FormControl>
);
}
......
import {
Input,
FormControl,
FormLabel,
} from '@chakra-ui/react';
import React from 'react';
import type { ControllerRenderProps, FieldError, FieldValues } from 'react-hook-form';
import getPlaceholderWithError from 'lib/getPlaceholderWithError';
import { TRANSACTION_HASH_LENGTH } from 'lib/validations/transaction';
import InputPlaceholder from 'ui/shared/InputPlaceholder';
type Props<Field> = {
field: Field;
......@@ -23,7 +22,7 @@ function TransactionInput<Field extends Partial<ControllerRenderProps<FieldValue
isInvalid={ Boolean(error) }
maxLength={ TRANSACTION_HASH_LENGTH }
/>
<FormLabel>{ getPlaceholderWithError('Transaction hash (0x...)', error?.message) }</FormLabel>
<InputPlaceholder text="Transaction hash (0x...)" error={ error?.message }/>
</FormControl>
);
}
......
......@@ -7,10 +7,12 @@ import type { UserInfo } from 'types/api/account';
import { useAppContext } from 'lib/appContext';
import * as cookies from 'lib/cookies';
const IdenticonComponent = typeof Identicon === 'object' && 'default' in Identicon ? Identicon.default : Identicon;
// for those who haven't got profile
// or if we cannot download the profile picture for some reasons
const FallbackImage = ({ size, id }: { size: number; id: string }) => {
const bgColor = useToken('colors', useColorModeValue('blackAlpha.100', 'white'));
const bgColor = useToken('colors', useColorModeValue('gray.100', 'white'));
return (
<Box
......@@ -19,7 +21,7 @@ const FallbackImage = ({ size, id }: { size: number; id: string }) => {
maxHeight={ `${ size }px` }
>
<Box boxSize={ `${ size * 2 }px` } transformOrigin="left top" transform="scale(0.5)" borderRadius="full" overflow="hidden">
<Identicon
<IdenticonComponent
bg={ bgColor }
string={ id }
// the displayed size is doubled for retina displays and then scaled down
......
......@@ -35,7 +35,7 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id,
if (alias) {
return (
<Tooltip label={ hash }>
<Box overflow="hidden" textOverflow="ellipsis">{ alias }</Box>
<Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ alias }</Box>
</Tooltip>
);
}
......
......@@ -21,7 +21,9 @@ import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
export interface ColorModeTogglerProps
extends Omit<UseCheckboxProps, 'isIndeterminate'>,
Omit<HTMLChakraProps<'label'>, keyof UseCheckboxProps>,
ThemingProps<'Switch'> {}
ThemingProps<'Switch'> {
trackBg?: string;
}
const ColorModeToggler = forwardRef<ColorModeTogglerProps, 'input'>((props, ref) => {
const ownProps = omitThemingProps(props);
......@@ -39,7 +41,7 @@ const ColorModeToggler = forwardRef<ColorModeTogglerProps, 'input'>((props, ref)
const transitionProps = getDefaultTransitionProps();
const trackStyles: SystemStyleObject = React.useMemo(() => ({
bg: trackBg,
bgColor: props.trackBg || trackBg,
width: '72px',
height: '32px',
borderRadius: 'full',
......@@ -50,7 +52,7 @@ const ColorModeToggler = forwardRef<ColorModeTogglerProps, 'input'>((props, ref)
cursor: 'pointer',
...transitionProps,
transitionDuration: 'ultra-slow',
}), [ trackBg, transitionProps ]);
}), [ props.trackBg, trackBg, transitionProps ]);
const thumbStyles: SystemStyleObject = React.useMemo(() => ({
bg: thumbBg,
......
......@@ -11,11 +11,11 @@ import Burger from './Burger';
import ColorModeToggler from './ColorModeToggler';
type Props = {
hasSearch: boolean;
isHomePage?: boolean;
hideOnScrollDown?: boolean;
}
const Header = ({ hideOnScrollDown, hasSearch }: Props) => {
const Header = ({ hideOnScrollDown, isHomePage }: Props) => {
const bgColor = useColorModeValue('white', 'black');
return (
......@@ -43,22 +43,27 @@ const Header = ({ hideOnScrollDown, hasSearch }: Props) => {
<NetworkLogo/>
<ProfileMenuMobile/>
</Flex>
{ hasSearch && <SearchBar withShadow={ !hideOnScrollDown }/> }
</Box><HStack
as="header"
width="100%"
alignItems="center"
justifyContent="center"
gap={ 12 }
display={{ base: 'none', lg: 'flex' }}
paddingX={ 12 }
paddingTop={ 9 }
paddingBottom="52px"
>
<Box width="100%">{ hasSearch && <SearchBar/> }</Box>
<ColorModeToggler/>
<ProfileMenuDesktop/>
</HStack>
{ !isHomePage && <SearchBar withShadow={ !hideOnScrollDown }/> }
</Box>
{ !isHomePage && (
<HStack
as="header"
width="100%"
alignItems="center"
justifyContent="center"
gap={ 12 }
display={{ base: 'none', lg: 'flex' }}
paddingX={ 12 }
paddingTop={ 9 }
paddingBottom="52px"
>
<Box width="100%">
<SearchBar/>
</Box>
<ColorModeToggler/>
<ProfileMenuDesktop/>
</HStack>
) }
</>
) }
</ScrollDirectionContext.Consumer>
......
......@@ -41,7 +41,7 @@ const NavLink = ({ text, url, icon, isCollapsed, isActive, px, isNewUi }: Props)
isDisabled={ !isCollapsed }
placement="right"
variant="nav"
gutter={ 15 }
gutter={ 20 }
color={ isActive ? colors.text.active : colors.text.hover }
>
<HStack spacing={ 3 }>
......
......@@ -73,7 +73,7 @@ const NavigationDesktop = () => {
<NetworkLogo isCollapsed={ isCollapsed }/>
<NetworkMenu isCollapsed={ isCollapsed }/>
</Box>
<Box as="nav" mt={ 14 }>
<Box as="nav" mt={ 8 }>
<VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden">
{ mainNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) }
</VStack>
......
......@@ -2,7 +2,7 @@ import { Box, Button, Grid, Heading, Text, useColorModeValue } from '@chakra-ui/
import React, { useCallback } from 'react';
import ChartWidgetGraph from './ChartWidgetGraph';
import { demoData } from './constants/demo-data';
import { demoChartsData } from './constants/demo-charts-data';
type Props = {
apiMethodURL: string;
......@@ -62,9 +62,10 @@ const ChartWidget = ({ title, description }: Props) => {
</Grid>
<ChartWidgetGraph
items={ demoData }
items={ demoChartsData }
onZoom={ handleZoom }
isZoomResetInitial={ isZoomResetInitial }
title={ title }
/>
</Box>
);
......
......@@ -14,6 +14,7 @@ import useChartSize from 'ui/shared/chart/useChartSize';
import useTimeChartController from 'ui/shared/chart/useTimeChartController';
interface Props {
title: string;
items: Array<TimeChartItem>;
onZoom: () => void;
isZoomResetInitial: boolean;
......@@ -21,7 +22,7 @@ interface Props {
const CHART_MARGIN = { bottom: 20, left: 65, right: 30, top: 10 };
const ChartWidgetGraph = ({ items, onZoom, isZoomResetInitial }: Props) => {
const ChartWidgetGraph = ({ items, onZoom, isZoomResetInitial, title }: Props) => {
const ref = React.useRef<SVGSVGElement>(null);
const [ range, setRange ] = React.useState<[ number, number ]>([ 0, Infinity ]);
const { width, height, innerWidth, innerHeight } = useChartSize(ref.current, CHART_MARGIN);
......@@ -30,7 +31,7 @@ const ChartWidgetGraph = ({ items, onZoom, isZoomResetInitial }: Props) => {
const displayedData = useMemo(() => items.slice(range[0], range[1]).map((d) =>
({ ...d, date: new Date(d.date) })), [ items, range ]);
const chartData = [ { items: items, name: 'chart', color } ];
const chartData = [ { items: items, name: title, color } ];
const { yTickFormat, xScale, yScale } = useTimeChartController({
data: [ { items: displayedData, name: 'chart', color } ],
......
import { Grid, GridItem, Heading, List, ListItem } from '@chakra-ui/react';
import React from 'react';
import type { StatsSection } from 'types/client/stats';
import { apos } from 'lib/html-entities';
import EmptySearchResult from '../apps/EmptySearchResult';
import ChartWidget from './ChartWidget';
import { statisticsChartsScheme } from './constants/charts-scheme';
const WidgetsList = () => {
return (
type Props = {
charts: Array<StatsSection>;
}
const ChartsWidgetsList = ({ charts }: Props) => {
const isAnyChartDisplayed = charts.some((section) => section.charts.some(chart => chart.visible));
return isAnyChartDisplayed ? (
<List>
{
statisticsChartsScheme.map((section) => (
charts.map((section) => (
<ListItem
display={ section.charts.every((chart) => !chart.visible) ? 'none' : 'block' }
key={ section.id }
mb={ 8 }
_last={{
......@@ -30,7 +41,10 @@ const WidgetsList = () => {
gap={ 4 }
>
{ section.charts.map((chart) => (
<GridItem key={ chart.id }>
<GridItem
key={ chart.id }
display={ chart.visible ? 'block' : 'none' }
>
<ChartWidget
apiMethodURL={ chart.apiMethodURL }
title={ chart.title }
......@@ -43,7 +57,9 @@ const WidgetsList = () => {
))
}
</List>
) : (
<EmptySearchResult text={ `Couldn${ apos }t find a chart that matches your filter query.` }/>
);
};
export default WidgetsList;
export default ChartsWidgetsList;
import { Box, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
type Props = {
label: string;
value: string;
}
const NumberWidget = ({ label, value }: Props) => {
return (
<Box
border="1px"
borderColor={ useColorModeValue('gray.200', 'gray.600') }
p={ 3 }
borderRadius={ 12 }
>
<Text
variant="secondary"
fontSize="xs"
>
{ label }
</Text>
<Text
fontWeight={ 500 }
fontSize="lg"
>
{ value }
</Text>
</Box>
);
};
export default NumberWidget;
import { Box, Skeleton, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const NumberWidgetSkeleton = () => {
return (
<Box
border="1px"
borderColor={ useColorModeValue('gray.200', 'gray.600') }
p={ 3 }
borderRadius={ 12 }
>
<Skeleton w="70px" h="10px" mb={ 2 }/>
<Skeleton w="100px" h="27px"/>
</Box>
);
};
export default NumberWidgetSkeleton;
import { Grid } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { Stats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import NumberWidget from './NumberWidget';
import NumberWidgetSkeleton from './NumberWidgetSkeleton';
const skeletonsCount = 4;
const NumberWidgetsList = () => {
const fetch = useFetch();
const { data, isLoading } = useQuery<unknown, unknown, Stats>(
[ QueryKeys.stats ],
// TODO: Just temporary. Remove this when the API is ready.
async() => await fetch(`/node-api/stats`),
);
return (
<Grid
gridTemplateColumns={{ base: 'repeat(2, 1fr)', lg: 'repeat(4, 1fr)' }}
gridGap={ 4 }
>
{ isLoading ? [ ...Array(skeletonsCount) ]
.map((e, i) => <NumberWidgetSkeleton key={ i }/>) :
(
<NumberWidget
label="Total blocks"
value={ Number(data?.total_blocks).toLocaleString() }
/>
) }
</Grid>
);
};
export default NumberWidgetsList;
......@@ -4,7 +4,7 @@ import React, { useCallback } from 'react';
import eastMiniArrowIcon from 'icons/arrows/east-mini.svg';
type Props<T extends string> = {
items: Array<{id: T; value: string}>;
items: Array<{id: T; title: string}>;
selectedId: T;
onSelect: (id: T) => void;
}
......@@ -32,7 +32,7 @@ export function StatsDropdownMenu<T extends string>({ items, selectedId, onSelec
display="flex"
alignItems="center"
>
{ selectedCategory?.value }
{ selectedCategory?.title }
<Icon transform="rotate(-90deg)" ml={{ base: 'auto', sm: 1 }} as={ eastMiniArrowIcon } w={ 5 } h={ 5 }/>
</Box>
</MenuButton>
......@@ -48,7 +48,7 @@ export function StatsDropdownMenu<T extends string>({ items, selectedId, onSelec
key={ item.id }
value={ item.id }
>
{ item.value }
{ item.title }
</MenuItemOption>
)) }
</MenuOptionGroup>
......
import { Grid, GridItem } from '@chakra-ui/react';
import debounce from 'lodash/debounce';
import React, { useCallback, useState } from 'react';
import React from 'react';
import type { StatsInterval, StatsIntervalIds, StatsSection, StatsSectionIds } from 'types/client/stats';
......@@ -11,21 +10,29 @@ import StatsDropdownMenu from './StatsDropdownMenu';
const sectionsList = Object.keys(STATS_SECTIONS).map((id: string) => ({
id: id,
value: STATS_SECTIONS[id as StatsSectionIds],
title: STATS_SECTIONS[id as StatsSectionIds],
})) as Array<StatsSection>;
const intervalList = Object.keys(STATS_INTERVALS).map((id: string) => ({
id: id,
value: STATS_INTERVALS[id as StatsIntervalIds],
title: STATS_INTERVALS[id as StatsIntervalIds],
})) as Array<StatsInterval>;
const StatsFilters = () => {
const [ selectedSectionId, setSelectedSectionId ] = useState<StatsSectionIds>('all');
const [ selectedIntervalId, setSelectedIntervalId ] = useState<StatsIntervalIds>('all');
const [ , setFilterQuery ] = useState('');
type Props = {
section: StatsSectionIds;
onSectionChange: (newSection: StatsSectionIds) => void;
interval: StatsIntervalIds;
onIntervalChange: (newInterval: StatsIntervalIds) => void;
onFilterInputChange: (q: string) => void;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
const debounceFilterCharts = useCallback(debounce(q => setFilterQuery(q), 500), []);
const StatsFilters = ({
section,
onSectionChange,
interval,
onIntervalChange,
onFilterInputChange,
}: Props) => {
return (
<Grid
......@@ -42,7 +49,7 @@ const StatsFilters = () => {
area="input"
>
<FilterInput
onChange={ debounceFilterCharts }
onChange={ onFilterInputChange }
placeholder="Find chart, metric..."/>
</GridItem>
......@@ -52,8 +59,8 @@ const StatsFilters = () => {
>
<StatsDropdownMenu
items={ sectionsList }
selectedId={ selectedSectionId }
onSelect={ setSelectedSectionId }
selectedId={ section }
onSelect={ onSectionChange }
/>
</GridItem>
......@@ -63,8 +70,8 @@ const StatsFilters = () => {
>
<StatsDropdownMenu
items={ intervalList }
selectedId={ selectedIntervalId }
onSelect={ setSelectedIntervalId }
selectedId={ interval }
onSelect={ onIntervalChange }
/>
</GridItem>
</Grid>
......
export const statisticsChartsScheme = [
import type { StatsSection } from 'types/client/stats';
export const statsChartsScheme: Array<StatsSection> = [
{
id: 'blocks',
title: 'Blocks',
......
import type { TimeChartItem } from '../../shared/chart/types';
import type { TimeChartItem } from 'ui/shared/chart/types';
export const demoData: Array<TimeChartItem> = [ { date: new Date('2022-10-17T00:00:00.000Z'), value: 432670 }, {
export const demoChartsData: Array<TimeChartItem> = [ { date: new Date('2022-10-17T00:00:00.000Z'), value: 432670 }, {
date: new Date('2022-10-18T00:00:00.000Z'),
value: 370100,
}, { date: new Date('2022-10-19T00:00:00.000Z'), value: 283234 }, { date: new Date('2022-10-20T00:00:00.000Z'), value: 420910 }, {
......
import type { StatsSectionIds, StatsIntervalIds } from 'types/client/stats';
export const STATS_SECTIONS: { [key in StatsSectionIds]: string } = {
export const STATS_SECTIONS: { [key in StatsSectionIds]?: string } = {
all: 'All stats',
accounts: 'Accounts',
blocks: 'Blocks',
......
import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useState } from 'react';
import type { StatsChart, StatsIntervalIds, StatsSection, StatsSectionIds } from 'types/client/stats';
import { statsChartsScheme } from './constants/charts-scheme';
function isSectionMatches(section: StatsSection, currentSection: StatsSectionIds): boolean {
return currentSection === 'all' || section.id === currentSection;
}
function isChartNameMatches(q: string, chart: StatsChart) {
return chart.title.toLowerCase().includes(q.toLowerCase());
}
export default function useStats() {
const [ isLoading, setIsLoading ] = useState(true);
const [ defaultCharts, setDefaultCharts ] = useState<Array<StatsSection>>();
const [ displayedCharts, setDisplayedCharts ] = useState<Array<StatsSection>>([]);
const [ section, setSection ] = useState<StatsSectionIds>('all');
const [ interval, setInterval ] = useState<StatsIntervalIds>('all');
const [ filterQuery, setFilterQuery ] = useState('');
// eslint-disable-next-line react-hooks/exhaustive-deps
const debounceFilterCharts = useCallback(debounce(q => setFilterQuery(q), 500), []);
const filterCharts = useCallback((q: string, currentSection: StatsSectionIds) => {
const charts = defaultCharts
?.map((section: StatsSection) => {
const charts = section.charts.map((chart: StatsChart) => ({
...chart,
visible: isSectionMatches(section, currentSection) && isChartNameMatches(q, chart),
}));
return {
...section,
charts,
};
});
setDisplayedCharts(charts || []);
}, [ defaultCharts ]);
const handleSectionChange = useCallback((newSection: StatsSectionIds) => {
setSection(newSection);
}, []);
const handleIntervalChange = useCallback((newInterval: StatsIntervalIds) => {
setInterval(newInterval);
}, []);
useEffect(() => {
filterCharts(filterQuery, section);
}, [ filterQuery, section, filterCharts ]);
useEffect(() => {
setDefaultCharts(statsChartsScheme);
setDisplayedCharts(statsChartsScheme);
setIsLoading(false);
}, []);
return React.useMemo(() => ({
section,
handleSectionChange,
interval,
handleIntervalChange,
debounceFilterCharts,
isLoading,
displayedCharts,
}), [
section,
handleSectionChange,
interval,
handleIntervalChange,
debounceFilterCharts,
displayedCharts,
isLoading,
]);
}
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { ROUTES } from 'lib/link/routes';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import TxsNewItemNotice from './TxsNewItemNotice';
const hooksConfig = {
router: {
pathname: ROUTES.txs.pattern,
query: {},
},
};
export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
test.describe.configure({ mode: 'serial' });
test('new item in validated txs list', async({ mount, createSocket }) => {
const component = await mount(
<TestApp withSocket>
<TxsNewItemNotice/>
</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();
});
test('2 new items in validated txs list', async({ mount, page, createSocket }) => {
const component = await mount(
<TestApp withSocket>
<TxsNewItemNotice/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'transactions:new_transaction');
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
await page.waitForSelector('text=2 more');
await expect(component).toHaveScreenshot();
});
test('connection loss', async({ mount, createSocket }) => {
const component = await mount(
<TestApp withSocket>
<TxsNewItemNotice/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
await socketServer.joinChannel(socket, 'transactions:new_transaction');
socket.close();
await expect(component).toHaveScreenshot();
});
test('fetching', async({ mount, createSocket }) => {
const component = await mount(
<TestApp withSocket>
<TxsNewItemNotice/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
await socketServer.joinChannel(socket, 'transactions:new_transaction');
await expect(component).toHaveScreenshot();
});
import { Alert, Spinner, Text, Link, chakra } from '@chakra-ui/react';
import { Alert, Link, Text, chakra } from '@chakra-ui/react';
import React from 'react';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
......@@ -8,7 +8,7 @@ interface InjectedProps {
}
interface Props {
children: (props: InjectedProps) => JSX.Element;
children?: (props: InjectedProps) => JSX.Element;
className?: string;
}
......@@ -36,19 +36,22 @@ const TxsNewItemNotice = ({ children, className }: Props) => {
}
if (!num) {
return null;
return (
<Alert className={ className } status="warning" p={ 4 } fontWeight={ 400 }>
scanning new transactions...
</Alert>
);
}
return (
<Alert className={ className } status="warning" p={ 4 } fontWeight={ 400 }>
<Spinner size="sm" mr={ 3 }/>
<Text as="span" whiteSpace="pre">+ { num } new transaction{ num > 1 ? 's' : '' }. </Text>
<Link onClick={ handleClick }>View in list</Link>
<Link onClick={ handleClick }>{ num } more transaction{ num > 1 ? 's' : '' }</Link>
<Text whiteSpace="pre"> ha{ num > 1 ? 've' : 's' } come in</Text>
</Alert>
);
})();
return children({ content });
return children ? children({ content }) : content;
};
export default chakra(TxsNewItemNotice);
......@@ -73,7 +73,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
notificationsDefault = data.notification_settings;
}
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
defaultValues: {
address: data?.address_hash || '',
tag: data?.name || '',
......@@ -191,7 +191,7 @@ const AddressForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
size="lg"
type="submit"
isLoading={ pending }
disabled={ !isValid }
disabled={ !isValid || !isDirty }
>
{ data ? 'Save changes' : 'Add address' }
</Button>
......
......@@ -35,7 +35,7 @@ const DeleteAddressModal: React.FC<Props> = ({ isOpen, onClose, data }) => {
const renderModalContent = useCallback(() => {
const addressString = isMobile ? [ address.slice(0, 4), address.slice(-4) ].join('...') : address;
return (
<Text>Address <Text fontWeight="600" as="span"> { addressString || 'address' }</Text> will be deleted</Text>
<Text>Address <Text fontWeight="700" as="span"> { addressString || 'address' }</Text> will be deleted</Text>
);
}, [ address, isMobile ]);
......
import { HStack, VStack, Text, Icon, useColorModeValue, Flex } from '@chakra-ui/react';
import { HStack, VStack, Text, Icon, Flex } from '@chakra-ui/react';
import React from 'react';
import type { TWatchlistItem } from 'types/client/account';
......@@ -12,12 +12,10 @@ import CurrencyValue from 'ui/shared/CurrencyValue';
import TokenLogo from 'ui/shared/TokenLogo';
const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
const mainTextColor = useColorModeValue('gray.700', 'gray.50');
const infoItemsPaddingLeft = { base: 0, lg: 8 };
const infoItemsPaddingLeft = { base: 1, lg: 8 };
return (
<VStack spacing={ 2 } align="stretch" fontWeight={ 500 } color="gray.700">
<VStack spacing={ 2 } align="stretch" fontWeight={ 500 }>
<AddressSnippet address={ item.address_hash }/>
<Flex fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft } flexWrap="wrap" alignItems="center" rowGap={ 1 }>
{ appConfig.network.currency.address && (
......@@ -40,8 +38,8 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
</Flex>
{ item.tokens_count && (
<HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
<Icon as={ TokensIcon } marginRight="10px" w="17px" h="16px"/>
<Text color={ mainTextColor }>{ `Tokens:${ nbsp }` + item.tokens_count }</Text>
<Icon as={ TokensIcon } mr={ 2 } w="17px" h="16px"/>
<Text>{ `Tokens:${ nbsp }` + item.tokens_count }</Text>
{ /* api does not provide token prices */ }
{ /* <Text variant="secondary">{ `${ nbsp }($${ item.tokensUSD } USD)` }</Text> */ }
<Text variant="secondary">{ `${ nbsp }(N/A)` }</Text>
......@@ -50,8 +48,8 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
{ /* api does not provide token prices */ }
{ /* { item.address_balance && (
<HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
<Icon as={ WalletIcon } marginRight="10px" w="16px" h="16px"/>
<Text color={ mainTextColor }>{ `Net worth:${ nbsp }` }</Text>
<Icon as={ WalletIcon } mr={ 2 } w="16px" h="16px"/>
<Text>{ `Net worth:${ nbsp }` }</Text>
<Link href="#">{ `$${ item.totalUSD } USD` }</Link>
</HStack>
) } */ }
......
......@@ -1952,7 +1952,7 @@
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.10.tgz#78a42897c2cf8db9fd5f1811f7590393b77774c7"
integrity sha512-w0Ou3Z83LOYEkwaui2M8VwIp+nLi/NA60lBLMvaJ+vXVMcsARYdEzLNE7RSm4+lSg4zq4d7fAVuzk7PNQ5JFgg==
"@eslint/eslintrc@^1.3.0":
"@eslint/eslintrc@^1.3.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.3.tgz#2b044ab39fdfa75b4688184f9e573ce3c5b0ff95"
integrity sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==
......@@ -2309,14 +2309,19 @@
"@ethersproject/properties" "^5.7.0"
"@ethersproject/strings" "^5.7.0"
"@humanwhocodes/config-array@^0.9.2":
version "0.9.5"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7"
integrity sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==
"@humanwhocodes/config-array@^0.11.6":
version "0.11.7"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.7.tgz#38aec044c6c828f6ed51d5d7ae3d9b9faf6dbb0f"
integrity sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==
dependencies:
"@humanwhocodes/object-schema" "^1.2.1"
debug "^4.1.1"
minimatch "^3.0.4"
minimatch "^3.0.5"
"@humanwhocodes/module-importer@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
"@humanwhocodes/object-schema@^1.2.1":
version "1.2.1"
......@@ -2730,7 +2735,7 @@
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
"@nodelib/fs.walk@^1.2.3":
"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
......@@ -3515,6 +3520,11 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
"@types/semver@^7.3.12":
version "7.3.13"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==
"@types/stack-utils@^2.0.0":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
......@@ -3525,6 +3535,13 @@
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==
"@types/ws@^8.5.3":
version "8.5.3"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d"
integrity sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==
dependencies:
"@types/node" "*"
"@types/yargs-parser@*":
version "21.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
......@@ -3569,6 +3586,14 @@
"@typescript-eslint/types" "5.40.0"
"@typescript-eslint/visitor-keys" "5.40.0"
"@typescript-eslint/scope-manager@5.45.0":
version "5.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.45.0.tgz#7a4ac1bfa9544bff3f620ab85947945938319a96"
integrity sha512-noDMjr87Arp/PuVrtvN3dXiJstQR1+XlQ4R1EvzG+NMgXi8CuMCXpb8JqNtFHKceVSQ985BZhfRdowJzbv4yKw==
dependencies:
"@typescript-eslint/types" "5.45.0"
"@typescript-eslint/visitor-keys" "5.45.0"
"@typescript-eslint/type-utils@5.40.0":
version "5.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.40.0.tgz#4964099d0158355e72d67a370249d7fc03331126"
......@@ -3584,6 +3609,11 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.40.0.tgz#8de07e118a10b8f63c99e174a3860f75608c822e"
integrity sha512-V1KdQRTXsYpf1Y1fXCeZ+uhjW48Niiw0VGt4V8yzuaDTU8Z1Xl7yQDyQNqyAFcVhpYXIVCEuxSIWTsLDpHgTbw==
"@typescript-eslint/types@5.45.0":
version "5.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.45.0.tgz#794760b9037ee4154c09549ef5a96599621109c5"
integrity sha512-QQij+u/vgskA66azc9dCmx+rev79PzX8uDHpsqSjEFtfF2gBUTRCpvYMh2gw2ghkJabNkPlSUCimsyBEQZd1DA==
"@typescript-eslint/typescript-estree@5.40.0":
version "5.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.40.0.tgz#e305e6a5d65226efa5471ee0f12e0ffaab6d3075"
......@@ -3597,6 +3627,19 @@
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/typescript-estree@5.45.0":
version "5.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.45.0.tgz#f70a0d646d7f38c0dfd6936a5e171a77f1e5291d"
integrity sha512-maRhLGSzqUpFcZgXxg1qc/+H0bT36lHK4APhp0AEUVrpSwXiRAomm/JGjSG+kNUio5kAa3uekCYu/47cnGn5EQ==
dependencies:
"@typescript-eslint/types" "5.45.0"
"@typescript-eslint/visitor-keys" "5.45.0"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/utils@5.40.0":
version "5.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.40.0.tgz#647f56a875fd09d33c6abd70913c3dd50759b772"
......@@ -3610,6 +3653,20 @@
eslint-utils "^3.0.0"
semver "^7.3.7"
"@typescript-eslint/utils@^5.10.0":
version "5.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.45.0.tgz#9cca2996eee1b8615485a6918a5c763629c7acf5"
integrity sha512-OUg2JvsVI1oIee/SwiejTot2OxwU8a7UfTFMOdlhD2y+Hl6memUSL4s98bpUTo8EpVEr0lmwlU7JSu/p2QpSvA==
dependencies:
"@types/json-schema" "^7.0.9"
"@types/semver" "^7.3.12"
"@typescript-eslint/scope-manager" "5.45.0"
"@typescript-eslint/types" "5.45.0"
"@typescript-eslint/typescript-estree" "5.45.0"
eslint-scope "^5.1.1"
eslint-utils "^3.0.0"
semver "^7.3.7"
"@typescript-eslint/visitor-keys@5.40.0":
version "5.40.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.40.0.tgz#dd2d38097f68e0d2e1e06cb9f73c0173aca54b68"
......@@ -3618,6 +3675,14 @@
"@typescript-eslint/types" "5.40.0"
eslint-visitor-keys "^3.3.0"
"@typescript-eslint/visitor-keys@5.45.0":
version "5.45.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.45.0.tgz#e0d160e9e7fdb7f8da697a5b78e7a14a22a70528"
integrity sha512-jc6Eccbn2RtQPr1s7th6jJWQHBHI6GBVQkCHoJFQ5UreaKm59Vxw+ynQUPPY2u2Amquc+7tmEoC2G52ApsGNNg==
dependencies:
"@typescript-eslint/types" "5.45.0"
eslint-visitor-keys "^3.3.0"
"@vitejs/plugin-react@^2.2.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-2.2.0.tgz#1b9f63b8b6bc3f56258d20cd19b33f5cc761ce6e"
......@@ -5222,6 +5287,13 @@ eslint-plugin-import@^2.26.0:
resolve "^1.22.0"
tsconfig-paths "^3.14.1"
eslint-plugin-jest@^27.1.6:
version "27.1.6"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.1.6.tgz#361d943f07d1978838e6b852c44a579f3879e332"
integrity sha512-XA7RFLSrlQF9IGtAmhddkUkBuICCTuryfOTfCSWcZHiHb69OilIH05oozH2XA6CEOtztnOd0vgXyvxZodkxGjg==
dependencies:
"@typescript-eslint/utils" "^5.10.0"
eslint-plugin-jsx-a11y@^6.5.1:
version "6.6.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.6.1.tgz#93736fc91b83fdc38cc8d115deedfc3091aef1ff"
......@@ -5241,6 +5313,11 @@ eslint-plugin-jsx-a11y@^6.5.1:
minimatch "^3.1.2"
semver "^6.3.0"
eslint-plugin-playwright@^0.11.2:
version "0.11.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-playwright/-/eslint-plugin-playwright-0.11.2.tgz#876057d4ab19d00b44bf004e27d5ace2c8bffada"
integrity sha512-uRLRLk7uTzc8NE6t4wBU8dijQwHvC66R/h7xwdM779jsJjMUtSmeaB8ayRkkpfwi+UU5BEfwvDANwmE+ccMVDw==
eslint-plugin-react-hooks@^4.5.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3"
......@@ -5313,13 +5390,15 @@ eslint-visitor-keys@^3.3.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
eslint@8.16.0:
version "8.16.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.16.0.tgz#6d936e2d524599f2a86c708483b4c372c5d3bbae"
integrity sha512-MBndsoXY/PeVTDJeWsYj7kLZ5hQpJOfMYLsF6LicLHQWbRDG19lK5jOix4DPl8yY4SUFcE3txy86OzFLWT+yoA==
eslint@^8.28.0:
version "8.28.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.28.0.tgz#81a680732634677cc890134bcdd9fdfea8e63d6e"
integrity sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ==
dependencies:
"@eslint/eslintrc" "^1.3.0"
"@humanwhocodes/config-array" "^0.9.2"
"@eslint/eslintrc" "^1.3.3"
"@humanwhocodes/config-array" "^0.11.6"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
......@@ -5329,18 +5408,21 @@ eslint@8.16.0:
eslint-scope "^7.1.1"
eslint-utils "^3.0.0"
eslint-visitor-keys "^3.3.0"
espree "^9.3.2"
espree "^9.4.0"
esquery "^1.4.0"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
file-entry-cache "^6.0.1"
functional-red-black-tree "^1.0.1"
glob-parent "^6.0.1"
find-up "^5.0.0"
glob-parent "^6.0.2"
globals "^13.15.0"
grapheme-splitter "^1.0.4"
ignore "^5.2.0"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
is-path-inside "^3.0.3"
js-sdsl "^4.1.4"
js-yaml "^4.1.0"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
......@@ -5352,9 +5434,8 @@ eslint@8.16.0:
strip-ansi "^6.0.1"
strip-json-comments "^3.1.0"
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
espree@^9.3.2, espree@^9.4.0:
espree@^9.4.0:
version "9.4.0"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.0.tgz#cd4bc3d6e9336c433265fc0aa016fc1aaf182f8a"
integrity sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==
......@@ -5591,6 +5672,14 @@ find-up@^4.0.0, find-up@^4.1.0:
locate-path "^5.0.0"
path-exists "^4.0.0"
find-up@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
dependencies:
locate-path "^6.0.0"
path-exists "^4.0.0"
flat-cache@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
......@@ -5692,11 +5781,6 @@ function.prototype.name@^1.1.5:
es-abstract "^1.19.0"
functions-have-names "^1.2.2"
functional-red-black-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==
functions-have-names@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
......@@ -5765,7 +5849,7 @@ glob-parent@^5.1.2:
dependencies:
is-glob "^4.0.1"
glob-parent@^6.0.1:
glob-parent@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
......@@ -6182,6 +6266,11 @@ is-number@^7.0.0:
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-path-inside@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-potential-custom-element-name@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
......@@ -6704,6 +6793,11 @@ joycon@^3.1.1:
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
js-sdsl@^4.1.4:
version "4.2.0"
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0"
integrity sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==
js-sha3@0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
......@@ -6913,6 +7007,13 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
locate-path@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
dependencies:
p-locate "^5.0.0"
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
......@@ -7063,7 +7164,7 @@ minimalistic-crypto-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==
minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
......@@ -7356,7 +7457,7 @@ p-limit@^2.2.0:
dependencies:
p-try "^2.0.0"
p-limit@^3.1.0:
p-limit@^3.0.2, p-limit@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
......@@ -7370,6 +7471,13 @@ p-locate@^4.1.0:
dependencies:
p-limit "^2.2.0"
p-locate@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
dependencies:
p-limit "^3.0.2"
p-map@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
......@@ -8782,11 +8890,6 @@ v8-compile-cache-lib@^3.0.1:
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
v8-compile-cache@^2.0.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
v8-to-istanbul@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4"
......@@ -8971,7 +9074,7 @@ ws@7.4.6:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
ws@^8.9.0:
ws@^8.11.0, ws@^8.9.0:
version "8.11.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143"
integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==
......
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