Commit 43f1e0dd authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into tx-api-update

parents 1a558d07 2cdc9bbd
# app config # app config
NEXT_PUBLIC_APP_ENV=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_ENV__
NEXT_PUBLIC_APP_INSTANCE=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_INSTANCE__ NEXT_PUBLIC_APP_INSTANCE=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_INSTANCE__
NEXT_PUBLIC_APP_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_PROTOCOL__ NEXT_PUBLIC_APP_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_PROTOCOL__
NEXT_PUBLIC_APP_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_HOST__ NEXT_PUBLIC_APP_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_HOST__
......
...@@ -59,10 +59,7 @@ jobs: ...@@ -59,10 +59,7 @@ jobs:
tags: ghcr.io/blockscout/frontend:prerelease-${{ env.SHORT_SHA }} tags: ghcr.io/blockscout/frontend:prerelease-${{ env.SHORT_SHA }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
build-args: | build-args: |
SENTRY_DSN=${{ secrets.SENTRY_DSN }} GIT_COMMIT_SHA=${{ env.SHORT_SHA }}
NEXT_PUBLIC_SENTRY_DSN=${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}
SENTRY_CSP_REPORT_URI=${{ secrets.SENTRY_CSP_REPORT_URI }}
SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
deploy_and_tests: deploy_and_tests:
needs: push_to_registry needs: push_to_registry
......
...@@ -79,6 +79,7 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -79,6 +79,7 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_APP_PROTOCOL | `http \| https` *(optional)* | App protocol (`https` used as default value) | `https` | | NEXT_PUBLIC_APP_PROTOCOL | `http \| https` *(optional)* | App protocol (`https` used as default value) | `https` |
| NEXT_PUBLIC_APP_HOST | `string` | App host | `blockscout.com` | | NEXT_PUBLIC_APP_HOST | `string` | App host | `blockscout.com` |
| NEXT_PUBLIC_APP_PORT | `number` *(optional)* | Port where app is running. Have to be provided if it is different to default port | `3000` | | NEXT_PUBLIC_APP_PORT | `number` *(optional)* | Port where app is running. Have to be provided if it is different to default port | `3000` |
| NEXT_PUBLIC_APP_ENV | `string` *(optional)* | Current app env (e.g development, review or production). Used for Sentry.io configuration | `production` |
### API configuration ### API configuration
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
const env = process.env.VERCEL_ENV || process.env.NODE_ENV; const env = process.env.VERCEL_ENV || process.env.NODE_ENV;
const isDev = env === 'development'; const isDev = env === 'development';
const baseUrl = [ const baseUrl = [
process.env.NEXT_PUBLIC_APP_PROTOCOL || 'https', process.env.NEXT_PUBLIC_APP_PROTOCOL?.replaceAll('\'', '"') || 'https',
'://', '://',
process.env.NEXT_PUBLIC_VERCEL_URL || process.env.NEXT_PUBLIC_APP_HOST, process.env.NEXT_PUBLIC_VERCEL_URL || process.env.NEXT_PUBLIC_APP_HOST,
process.env.NEXT_PUBLIC_APP_PORT ? ':' + process.env.NEXT_PUBLIC_APP_PORT : '', process.env.NEXT_PUBLIC_APP_PORT?.replaceAll('\'', '"') ? ':' + process.env.NEXT_PUBLIC_APP_PORT : '',
].join(''); ].join('');
const DEFAULT_CURRENCY_DECIMALS = 18; const DEFAULT_CURRENCY_DECIMALS = 18;
......
...@@ -3,6 +3,7 @@ NEXT_PUBLIC_APP_PROTOCOL=http ...@@ -3,6 +3,7 @@ NEXT_PUBLIC_APP_PROTOCOL=http
NEXT_PUBLIC_APP_HOST=localhost NEXT_PUBLIC_APP_HOST=localhost
NEXT_PUBLIC_APP_PORT=3000 NEXT_PUBLIC_APP_PORT=3000
NEXT_PUBLIC_APP_INSTANCE=local NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_APP_ENV=development
# ui config # ui config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
......
...@@ -2,7 +2,7 @@ import type * as Sentry from '@sentry/react'; ...@@ -2,7 +2,7 @@ import type * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing'; import { BrowserTracing } from '@sentry/tracing';
export const config: Sentry.BrowserOptions = { export const config: Sentry.BrowserOptions = {
environment: process.env.VERCEL_ENV || process.env.NODE_ENV, environment: process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_APP_ENV || process.env.NODE_ENV,
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
release: process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA, release: process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
integrations: [ new BrowserTracing() ], integrations: [ new BrowserTracing() ],
......
...@@ -94,7 +94,7 @@ ...@@ -94,7 +94,7 @@
"shortDescription": "Yield Farming for Synthetic Assets from LP tokens", "shortDescription": "Yield Farming for Synthetic Assets from LP tokens",
"site": "https://farms.baoswap.xyz/", "site": "https://farms.baoswap.xyz/",
"description": "Yield Farming for Synthetic Assets from LP tokens", "description": "Yield Farming for Synthetic Assets from LP tokens",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/", "url": "https://approval-tracker.vercel.app",
"twitter": "https://twitter.com/EasyStaking", "twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking", "telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
...@@ -114,7 +114,7 @@ ...@@ -114,7 +114,7 @@
"shortDescription": "During the Unit protocol development, we faced difficulty finding a reliable, flexible protocol for stablecoin swap without interface censorship.", "shortDescription": "During the Unit protocol development, we faced difficulty finding a reliable, flexible protocol for stablecoin swap without interface censorship.",
"site": "https://xdai.component.finance", "site": "https://xdai.component.finance",
"description": "During the Unit protocol development, we faced difficulty finding a reliable, flexible protocol for stablecoin swap without interface censorship.", "description": "During the Unit protocol development, we faced difficulty finding a reliable, flexible protocol for stablecoin swap without interface censorship.",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/", "url": "https://approval-tracker.vercel.app",
"twitter": "https://twitter.com/EasyStaking", "twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking", "telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
...@@ -134,7 +134,7 @@ ...@@ -134,7 +134,7 @@
"shortDescription": "View, deposit and withdraw for all V3 Pools", "shortDescription": "View, deposit and withdraw for all V3 Pools",
"site": "https://app.pooltogether.com/", "site": "https://app.pooltogether.com/",
"description": "View, deposit and withdraw for all V3 Pools", "description": "View, deposit and withdraw for all V3 Pools",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/", "url": "https://approval-tracker.vercel.app",
"twitter": "https://twitter.com/EasyStaking", "twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking", "telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
...@@ -154,7 +154,7 @@ ...@@ -154,7 +154,7 @@
"shortDescription": "A governance-enabled automated market maker with adjustable fees.", "shortDescription": "A governance-enabled automated market maker with adjustable fees.",
"site": "https://swapr.eth.limo", "site": "https://swapr.eth.limo",
"description": "A governance-enabled automated market maker with adjustable fees.", "description": "A governance-enabled automated market maker with adjustable fees.",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/", "url": "https://approval-tracker.vercel.app",
"twitter": "https://twitter.com/EasyStaking", "twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking", "telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
...@@ -174,7 +174,7 @@ ...@@ -174,7 +174,7 @@
"shortDescription": "AMM DEX on the xDai chain. Its uniqueness comes from the ability to trade securities tokens, specifically, tokenized real estate tokens.", "shortDescription": "AMM DEX on the xDai chain. Its uniqueness comes from the ability to trade securities tokens, specifically, tokenized real estate tokens.",
"site": "https://app.levinswap.org/", "site": "https://app.levinswap.org/",
"description": "AMM DEX on the xDai chain. Its uniqueness comes from the ability to trade securities tokens, specifically, tokenized real estate tokens.", "description": "AMM DEX on the xDai chain. Its uniqueness comes from the ability to trade securities tokens, specifically, tokenized real estate tokens.",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/", "url": "https://approval-tracker.vercel.app",
"twitter": "https://twitter.com/EasyStaking", "twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking", "telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
...@@ -194,7 +194,7 @@ ...@@ -194,7 +194,7 @@
"shortDescription": "Decentralized prediction markets on Ethereum", "shortDescription": "Decentralized prediction markets on Ethereum",
"site": "https://xdai.omen.eth.link/", "site": "https://xdai.omen.eth.link/",
"description": "Decentralized prediction markets on Ethereum", "description": "Decentralized prediction markets on Ethereum",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/", "url": "https://approval-tracker.vercel.app",
"twitter": "https://twitter.com/EasyStaking", "twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking", "telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
...@@ -214,7 +214,7 @@ ...@@ -214,7 +214,7 @@
"shortDescription": "NFT artwork created and sold on xDAI using meta transactions, burner wallets, and bridged to Ethereum", "shortDescription": "NFT artwork created and sold on xDAI using meta transactions, burner wallets, and bridged to Ethereum",
"site": "https://nifty.ink/explore", "site": "https://nifty.ink/explore",
"description": "NFT artwork created and sold on xDAI using meta transactions, burner wallets, and bridged to Ethereum", "description": "NFT artwork created and sold on xDAI using meta transactions, burner wallets, and bridged to Ethereum",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/", "url": "https://approval-tracker.vercel.app",
"twitter": "https://twitter.com/EasyStaking", "twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking", "telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
...@@ -234,7 +234,7 @@ ...@@ -234,7 +234,7 @@
"shortDescription": "Every chess game is one-of-a-kind. Make yours a collectible.", "shortDescription": "Every chess game is one-of-a-kind. Make yours a collectible.",
"site": "https://treasure.chess.com/", "site": "https://treasure.chess.com/",
"description": "Every chess game is one-of-a-kind. Make yours a collectible.", "description": "Every chess game is one-of-a-kind. Make yours a collectible.",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/", "url": "https://approval-tracker.vercel.app",
"twitter": "https://twitter.com/EasyStaking", "twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking", "telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
...@@ -254,7 +254,7 @@ ...@@ -254,7 +254,7 @@
"shortDescription": "A truly decentralised non-profit platform owned and managed by the Digital Arts community, bringing together Artists, Creators and Collectors as One.", "shortDescription": "A truly decentralised non-profit platform owned and managed by the Digital Arts community, bringing together Artists, Creators and Collectors as One.",
"site": "https://www.unique.one/", "site": "https://www.unique.one/",
"description": "A truly decentralised non-profit platform owned and managed by the Digital Arts community, bringing together Artists, Creators and Collectors as One.", "description": "A truly decentralised non-profit platform owned and managed by the Digital Arts community, bringing together Artists, Creators and Collectors as One.",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/", "url": "https://approval-tracker.vercel.app",
"twitter": "https://twitter.com/EasyStaking", "twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking", "telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
...@@ -274,7 +274,7 @@ ...@@ -274,7 +274,7 @@
"shortDescription": "A Community That Empowers NFT Artists", "shortDescription": "A Community That Empowers NFT Artists",
"site": "https://www.coldtruthculture.io/", "site": "https://www.coldtruthculture.io/",
"description": "A Community That Empowers NFT Artists", "description": "A Community That Empowers NFT Artists",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/", "url": "https://approval-tracker.vercel.app",
"twitter": "https://twitter.com/EasyStaking", "twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking", "telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
...@@ -294,7 +294,7 @@ ...@@ -294,7 +294,7 @@
"shortDescription": "Token bridge between the Gnosis Chain and the Ethereum network", "shortDescription": "Token bridge between the Gnosis Chain and the Ethereum network",
"site": "https://bridge.gnosischain.com/", "site": "https://bridge.gnosischain.com/",
"description": "Token bridge between the Gnosis Chain and the Ethereum network", "description": "Token bridge between the Gnosis Chain and the Ethereum network",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/", "url": "https://approval-tracker.vercel.app",
"twitter": "https://twitter.com/EasyStaking", "twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking", "telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
...@@ -314,7 +314,7 @@ ...@@ -314,7 +314,7 @@
"shortDescription": "The OmniBridge multi-token extension is the simplest way to transfer ANY ERC20/ ERC677 /ERC827 token to and from the xDai chain.", "shortDescription": "The OmniBridge multi-token extension is the simplest way to transfer ANY ERC20/ ERC677 /ERC827 token to and from the xDai chain.",
"site": "https://omni.gnosischain.com/bridge", "site": "https://omni.gnosischain.com/bridge",
"description": "The OmniBridge multi-token extension is the simplest way to transfer ANY ERC20/ ERC677 /ERC827 token to and from the xDai chain.", "description": "The OmniBridge multi-token extension is the simplest way to transfer ANY ERC20/ ERC677 /ERC827 token to and from the xDai chain.",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/", "url": "https://approval-tracker.vercel.app",
"twitter": "https://twitter.com/EasyStaking", "twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking", "telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
...@@ -334,7 +334,7 @@ ...@@ -334,7 +334,7 @@
"shortDescription": "Gnosis Safe is the most trusted platform to manage digital assets on Ethereum", "shortDescription": "Gnosis Safe is the most trusted platform to manage digital assets on Ethereum",
"site": "https://gnosis-safe.io/", "site": "https://gnosis-safe.io/",
"description": "Gnosis Safe is the most trusted platform to manage digital assets on Ethereum", "description": "Gnosis Safe is the most trusted platform to manage digital assets on Ethereum",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/", "url": "https://approval-tracker.vercel.app",
"twitter": "https://twitter.com/EasyStaking", "twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking", "telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
...@@ -354,7 +354,7 @@ ...@@ -354,7 +354,7 @@
"shortDescription": "Send ERC20 token or ETH. Batch sender. Bulk Sender. Token Multisender allows you to airdrop tokens in a few transactions in trustless way. Batch sending ERC20, Ethereum tokens.", "shortDescription": "Send ERC20 token or ETH. Batch sender. Bulk Sender. Token Multisender allows you to airdrop tokens in a few transactions in trustless way. Batch sending ERC20, Ethereum tokens.",
"site": "https://multisender.app/", "site": "https://multisender.app/",
"description": "Send ERC20 token or ETH. Batch sender. Bulk Sender. Token Multisender allows you to airdrop tokens in a few transactions in trustless way. Batch sending ERC20, Ethereum tokens.", "description": "Send ERC20 token or ETH. Batch sender. Bulk Sender. Token Multisender allows you to airdrop tokens in a few transactions in trustless way. Batch sending ERC20, Ethereum tokens.",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/", "url": "https://approval-tracker.vercel.app",
"twitter": "https://twitter.com/EasyStaking", "twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking", "telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
...@@ -374,7 +374,7 @@ ...@@ -374,7 +374,7 @@
"shortDescription": "Distribute ether or tokens to multiple addresses", "shortDescription": "Distribute ether or tokens to multiple addresses",
"site": "https://disperse.app/", "site": "https://disperse.app/",
"description": "Distribute ether or tokens to multiple addresses", "description": "Distribute ether or tokens to multiple addresses",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/", "url": "https://approval-tracker.vercel.app",
"twitter": "https://twitter.com/EasyStaking", "twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking", "telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
...@@ -394,7 +394,7 @@ ...@@ -394,7 +394,7 @@
"shortDescription": "DEX or Decentralized Exchange, also called an Automated Market Maker (AMM) in Decentralized Finance (DeFi). Symmetric is live on gnosis chain (xDai) and Celo.", "shortDescription": "DEX or Decentralized Exchange, also called an Automated Market Maker (AMM) in Decentralized Finance (DeFi). Symmetric is live on gnosis chain (xDai) and Celo.",
"site": "https://symmetric.finance/", "site": "https://symmetric.finance/",
"description": "DEX or Decentralized Exchange, also called an Automated Market Maker (AMM) in Decentralized Finance (DeFi). Symmetric is live on gnosis chain (xDai) and Celo.", "description": "DEX or Decentralized Exchange, also called an Automated Market Maker (AMM) in Decentralized Finance (DeFi). Symmetric is live on gnosis chain (xDai) and Celo.",
"url": "https://blockscout-allowance-mainnet-stage.vercel.app/", "url": "https://approval-tracker.vercel.app",
"twitter": "https://twitter.com/EasyStaking", "twitter": "https://twitter.com/EasyStaking",
"telegram": "https://t.me/easystaking", "telegram": "https://t.me/easystaking",
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
......
...@@ -21,6 +21,8 @@ blockscout: ...@@ -21,6 +21,8 @@ blockscout:
# enable https # enable https
tls: tls:
enabled: true enabled: true
path:
- "/"
# probes # probes
livenessProbe: livenessProbe:
enabled: true enabled: true
...@@ -263,6 +265,8 @@ frontend: ...@@ -263,6 +265,8 @@ frontend:
# enable https # enable https
tls: tls:
enabled: true enabled: true
path:
- "/"
resources: resources:
limits: limits:
memory: memory:
...@@ -292,7 +296,9 @@ frontend: ...@@ -292,7 +296,9 @@ frontend:
NEXT_PUBLIC_FOOTER_TWITTER_LINK: NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom _default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_INSTANCE: NEXT_PUBLIC_APP_INSTANCE:
_default: local _default: unknown
NEXT_PUBLIC_APP_ENV:
_default: e2e
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK: NEXT_PUBLIC_FOOTER_TELEGRAM_LINK:
_default: https://t.me/poa_network _default: https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK: NEXT_PUBLIC_FOOTER_STAKING_LINK:
......
...@@ -21,6 +21,8 @@ blockscout: ...@@ -21,6 +21,8 @@ blockscout:
# enable https # enable https
tls: tls:
enabled: true enabled: true
path:
- "/"
# probes # probes
livenessProbe: livenessProbe:
enabled: true enabled: true
...@@ -263,6 +265,8 @@ frontend: ...@@ -263,6 +265,8 @@ frontend:
# enable https # enable https
tls: tls:
enabled: true enabled: true
path:
- "/"
resources: resources:
limits: limits:
memory: memory:
...@@ -279,16 +283,16 @@ frontend: ...@@ -279,16 +283,16 @@ frontend:
enabled: true enabled: true
app: blockscout app: blockscout
environment: environment:
NEXT_PUBLIC_APP_PORT:
_default: 80
NEXT_PUBLIC_BLOCKSCOUT_VERSION: NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.8-beta _default: v4.1.8-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK: NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout _default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK: NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom _default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV:
_default: production
NEXT_PUBLIC_APP_INSTANCE: NEXT_PUBLIC_APP_INSTANCE:
_default: review _default: unknown
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK: NEXT_PUBLIC_FOOTER_TELEGRAM_LINK:
_default: https://t.me/poa_network _default: https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK: NEXT_PUBLIC_FOOTER_STAKING_LINK:
...@@ -307,15 +311,15 @@ frontend: ...@@ -307,15 +311,15 @@ frontend:
_default: 77 _default: 77
NEXT_PUBLIC_NETWORK_CURRENCY_NAME: NEXT_PUBLIC_NETWORK_CURRENCY_NAME:
_default: POA Network Sokol _default: POA Network Sokol
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL: NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL:
_default: SPOA _default: SPOA
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS: NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS:
......
...@@ -21,6 +21,8 @@ blockscout: ...@@ -21,6 +21,8 @@ blockscout:
# enable https # enable https
tls: tls:
enabled: true enabled: true
path:
- "/"
# probes # probes
livenessProbe: livenessProbe:
enabled: true enabled: true
...@@ -263,6 +265,8 @@ frontend: ...@@ -263,6 +265,8 @@ frontend:
# enable https # enable https
tls: tls:
enabled: true enabled: true
path:
- "/"
resources: resources:
limits: limits:
memory: memory:
...@@ -279,16 +283,16 @@ frontend: ...@@ -279,16 +283,16 @@ frontend:
enabled: true enabled: true
app: blockscout app: blockscout
environment: environment:
NEXT_PUBLIC_APP_PORT:
_default: 80
NEXT_PUBLIC_BLOCKSCOUT_VERSION: NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.8-beta _default: v4.1.8-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK: NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout _default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK: NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom _default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV:
_default: preview
NEXT_PUBLIC_APP_INSTANCE: NEXT_PUBLIC_APP_INSTANCE:
_default: review _default: unknown
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK: NEXT_PUBLIC_FOOTER_TELEGRAM_LINK:
_default: https://t.me/poa_network _default: https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK: NEXT_PUBLIC_FOOTER_STAKING_LINK:
......
...@@ -4,3 +4,9 @@ export const WEI = new BigNumber(10 ** 18); ...@@ -4,3 +4,9 @@ export const WEI = new BigNumber(10 ** 18);
export const GWEI = new BigNumber(10 ** 9); export const GWEI = new BigNumber(10 ** 9);
export const WEI_IN_GWEI = WEI.dividedBy(GWEI); export const WEI_IN_GWEI = WEI.dividedBy(GWEI);
export const ZERO = new BigNumber(0); export const ZERO = new BigNumber(0);
export const SECOND = 1_000;
export const MINUTE = 60 * SECOND;
export const HOUR = 60 * MINUTE;
export const DAY = 24 * HOUR;
export const WEEK = 7 * DAY;
...@@ -33,7 +33,18 @@ function getMarketplaceAppsOrigins() { ...@@ -33,7 +33,18 @@ function getMarketplaceAppsOrigins() {
} }
function getMarketplaceAppsLogosOrigins() { function getMarketplaceAppsLogosOrigins() {
return getMarketplaceApps().map(({ logo }) => logo); return getMarketplaceApps().map(({ logo }) => new URL(logo));
}
// we cannot use lodash/uniq in middleware code since it calls new Set() and it'is causing an error in Nextjs
// "Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime"
function unique(array: Array<string | undefined>) {
const set: Record<string, boolean> = {};
for (const item of array) {
item && (set[item] = true);
}
return Object.keys(set);
} }
function makePolicyMap() { function makePolicyMap() {
...@@ -91,11 +102,17 @@ function makePolicyMap() { ...@@ -91,11 +102,17 @@ function makePolicyMap() {
// github avatars // github avatars
'avatars.githubusercontent.com', 'avatars.githubusercontent.com',
// other github assets (e.g trustwallet token icons)
'raw.githubusercontent.com',
// auth0 assets
's.gravatar.com',
// network assets // network assets
...networkExternalAssets.map((url) => url.host), ...networkExternalAssets.map((url) => url.host),
// marketplace apps logos // marketplace apps logos
...getMarketplaceAppsLogosOrigins(), ...getMarketplaceAppsLogosOrigins().map((url) => url.host),
], ],
'font-src': [ 'font-src': [
...@@ -133,7 +150,8 @@ function getCspPolicy() { ...@@ -133,7 +150,8 @@ function getCspPolicy() {
return; return;
} }
return [ key, value.join(' ') ].join(' '); const uniqueValues = unique(value);
return [ key, uniqueValues.join(' ') ].join(' ');
}) })
.filter(Boolean) .filter(Boolean)
.join(';'); .join(';');
......
...@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query'; ...@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import type { UserInfo } from 'types/api/account'; import type { UserInfo } from 'types/api/account';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
interface Error { interface Error {
...@@ -19,5 +20,6 @@ export default function useFetchProfileInfo() { ...@@ -19,5 +20,6 @@ export default function useFetchProfileInfo() {
return fetch('/api/account/profile'); return fetch('/api/account/profile');
}, { }, {
refetchOnMount: false, refetchOnMount: false,
enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
}); });
} }
import React from 'react';
import { DAY, HOUR, MINUTE, SECOND } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
function getUnits(diff: number) {
if (diff < MINUTE) {
return [ SECOND, MINUTE ];
}
if (diff < HOUR) {
return [ MINUTE, HOUR ];
}
if (diff < DAY) {
return [ HOUR, DAY ];
}
return [ DAY, 2 * DAY ];
}
function getUpdateParams(ts: string) {
const timeDiff = Date.now() - new Date(ts).getTime();
const [ unit, higherUnit ] = getUnits(timeDiff);
if (unit === DAY) {
return { interval: DAY };
}
const leftover = unit - timeDiff % unit;
return {
startTimeout: unit === SECOND ?
0 :
// here we assume that in current dayjs locale time difference is rounded by Math.round function
// so we have to update displayed value whenever time comes over the middle of the unit interval
// since it will be rounded to the upper bound
(leftover < unit / 2 ? leftover + unit / 2 : leftover - unit / 2) + SECOND,
endTimeout: higherUnit - timeDiff + SECOND,
interval: unit,
};
}
export default function useTimeAgoIncrement(ts: string, isEnabled?: boolean) {
const [ value, setValue ] = React.useState(dayjs(ts).fromNow());
React.useEffect(() => {
const timeouts: Array<number> = [];
const intervals: Array<number> = [];
const startIncrement = () => {
const { startTimeout, interval, endTimeout } = getUpdateParams(ts);
if (!startTimeout && !endTimeout) {
return;
}
let intervalId: number;
const startTimeoutId = window.setTimeout(() => {
setValue(dayjs(ts).fromNow());
intervalId = window.setInterval(() => {
setValue(dayjs(ts).fromNow());
}, interval);
intervals.push(intervalId);
}, startTimeout);
const endTimeoutId = window.setTimeout(() => {
window.clearInterval(intervalId);
startIncrement();
}, endTimeout);
timeouts.push(startTimeoutId);
timeouts.push(endTimeoutId);
};
isEnabled && startIncrement();
return () => {
timeouts.forEach(window.clearTimeout);
intervals.forEach(window.clearInterval);
};
}, [ isEnabled, ts ]);
return value;
}
export type TTxsFilters = {
filter: 'pending' | 'validated';
type?: Array<TypeFilter>;
method?: Array<MethodFilter>;
}
export type TypeFilter = 'token_transfer' | 'contract_creation' | 'contract_call' | 'coin_transfer' | 'token_creation';
export type MethodFilter = 'approve' | 'transfer' | 'multicall' | 'mint' | 'commit';
export enum QueryKeys { export enum QueryKeys {
csrf = 'csrf', csrf = 'csrf',
profile = 'profile', profile = 'profile',
transactionsPending = 'transactions_pending', transactions = 'transactions',
transactionsValidated = 'transactions_validated',
tx = 'tx', tx = 'tx',
txInternals = 'tx-internals', txInternals = 'tx-internals',
txLog = 'tx-log', txLog = 'tx-log',
......
import { Text } from '@chakra-ui/react';
import React from 'react';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
interface Props {
ts: string;
isEnabled?: boolean;
}
const BlockTimestamp = ({ ts, isEnabled }: Props) => {
const timeAgo = useTimeAgoIncrement(ts, isEnabled);
return <Text variant="secondary" fontWeight={ 400 }>{ timeAgo }</Text>;
};
export default React.memo(BlockTimestamp);
...@@ -12,7 +12,8 @@ interface Props { ...@@ -12,7 +12,8 @@ interface Props {
const BlocksList = ({ data }: Props) => { const BlocksList = ({ data }: Props) => {
return ( return (
<Box mt={ 8 }> <Box mt={ 8 }>
{ data.map((item) => <BlocksListItem key={ item.height } data={ item }/>) } { /* TODO prop "enableTimeIncrement" should be set to false for second and later pages */ }
{ data.map((item) => <BlocksListItem key={ item.height } data={ item } enableTimeIncrement/>) }
</Box> </Box>
); );
}; };
......
...@@ -8,9 +8,9 @@ import type { Block } from 'types/api/block'; ...@@ -8,9 +8,9 @@ import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import { WEI, ZERO } from 'lib/consts'; import { WEI, ZERO } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link'; import link from 'lib/link/link';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile'; import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
...@@ -19,9 +19,10 @@ import Utilization from 'ui/shared/Utilization'; ...@@ -19,9 +19,10 @@ import Utilization from 'ui/shared/Utilization';
interface Props { interface Props {
data: Block; data: Block;
isPending?: boolean; isPending?: boolean;
enableTimeIncrement?: boolean;
} }
const BlocksListItem = ({ data, isPending }: Props) => { const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
const totalReward = data.rewards const totalReward = data.rewards
?.map(({ reward }) => BigNumber(reward)) ?.map(({ reward }) => BigNumber(reward))
.reduce((result, item) => result.plus(item), ZERO) || ZERO; .reduce((result, item) => result.plus(item), ZERO) || ZERO;
...@@ -40,7 +41,7 @@ const BlocksListItem = ({ data, isPending }: Props) => { ...@@ -40,7 +41,7 @@ const BlocksListItem = ({ data, isPending }: Props) => {
{ data.height } { data.height }
</Link> </Link>
</Flex> </Flex>
<Text variant="secondary" fontWeight={ 400 }>{ dayjs(data.timestamp).fromNow() }</Text> <BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/>
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Size</Text> <Text fontWeight={ 500 }>Size</Text>
......
...@@ -29,7 +29,8 @@ const BlocksTable = ({ data }: Props) => { ...@@ -29,7 +29,8 @@ const BlocksTable = ({ data }: Props) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => <BlocksTableItem key={ item.height } data={ item }/>) } { /* TODO prop "enableTimeIncrement" should be set to false for second and later pages */ }
{ data.map((item) => <BlocksTableItem key={ item.height } data={ item } enableTimeIncrement/>) }
</Tbody> </Tbody>
</Table> </Table>
</TableContainer> </TableContainer>
......
import { Tr, Td, Text, Link, Flex, Box, Icon, Tooltip, Spinner, useColorModeValue } from '@chakra-ui/react'; import { Tr, Td, Link, Flex, Box, Icon, Tooltip, Spinner, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
...@@ -6,8 +6,8 @@ import type { Block } from 'types/api/block'; ...@@ -6,8 +6,8 @@ import type { Block } from 'types/api/block';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import { WEI, ZERO } from 'lib/consts'; import { WEI, ZERO } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link'; import link from 'lib/link/link';
import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import Utilization from 'ui/shared/Utilization'; import Utilization from 'ui/shared/Utilization';
...@@ -15,9 +15,10 @@ import Utilization from 'ui/shared/Utilization'; ...@@ -15,9 +15,10 @@ import Utilization from 'ui/shared/Utilization';
interface Props { interface Props {
data: Block; data: Block;
isPending?: boolean; isPending?: boolean;
enableTimeIncrement?: boolean;
} }
const BlocksTableItem = ({ data, isPending }: Props) => { const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
const totalReward = data.rewards const totalReward = data.rewards
?.map(({ reward }) => BigNumber(reward)) ?.map(({ reward }) => BigNumber(reward))
.reduce((result, item) => result.plus(item), ZERO) || ZERO; .reduce((result, item) => result.plus(item), ZERO) || ZERO;
...@@ -27,7 +28,7 @@ const BlocksTableItem = ({ data, isPending }: Props) => { ...@@ -27,7 +28,7 @@ const BlocksTableItem = ({ data, isPending }: Props) => {
return ( return (
<Tr> <Tr>
<Td fontSize="sm"> <Td fontSize="sm">
<Flex columnGap={ 2 } alignItems="center"> <Flex columnGap={ 2 } alignItems="center" mb={ 2 }>
{ isPending && <Spinner size="sm" flexShrink={ 0 }/> } { isPending && <Spinner size="sm" flexShrink={ 0 }/> }
<Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations"> <Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations">
<Link <Link
...@@ -38,7 +39,7 @@ const BlocksTableItem = ({ data, isPending }: Props) => { ...@@ -38,7 +39,7 @@ const BlocksTableItem = ({ data, isPending }: Props) => {
</Link> </Link>
</Tooltip> </Tooltip>
</Flex> </Flex>
<Text variant="secondary" mt={ 2 } fontWeight={ 400 }>{ dayjs(data.timestamp).fromNow() }</Text> <BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/>
</Td> </Td>
<Td fontSize="sm">{ data.size.toLocaleString('en') } bytes</Td> <Td fontSize="sm">{ data.size.toLocaleString('en') } bytes</Td>
<Td fontSize="sm"> <Td fontSize="sm">
......
...@@ -4,6 +4,7 @@ import React from 'react'; ...@@ -4,6 +4,7 @@ import React from 'react';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import PageContent from 'ui/shared/Page/PageContent'; import PageContent from 'ui/shared/Page/PageContent';
import Header from 'ui/snippets/header/Header'; import Header from 'ui/snippets/header/Header';
...@@ -17,7 +18,9 @@ interface Props { ...@@ -17,7 +18,9 @@ interface Props {
const Page = ({ children, wrapChildren = true }: Props) => { const Page = ({ children, wrapChildren = true }: Props) => {
const fetch = useFetch(); const fetch = useFetch();
useQuery<unknown, unknown, unknown>([ QueryKeys.csrf ], async() => await fetch('/api/account/csrf')); useQuery<unknown, unknown, unknown>([ QueryKeys.csrf ], async() => await fetch('/api/account/csrf'), {
enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
});
const renderedChildren = wrapChildren ? ( const renderedChildren = wrapChildren ? (
<PageContent>{ children }</PageContent> <PageContent>{ children }</PageContent>
......
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
TabPanels, TabPanels,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system'; import type { StyleProps } from '@chakra-ui/styled-system';
import { pick } from 'lodash';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
...@@ -16,6 +17,8 @@ import useIsMobile from 'lib/hooks/useIsMobile'; ...@@ -16,6 +17,8 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import RoutedTabsMenu from './RoutedTabsMenu'; import RoutedTabsMenu from './RoutedTabsMenu';
import useAdaptiveTabs from './useAdaptiveTabs'; import useAdaptiveTabs from './useAdaptiveTabs';
const PRESERVED_QUERY = [ 'network_type', 'network_sub_type' ];
const hiddenItemStyles: StyleProps = { const hiddenItemStyles: StyleProps = {
position: 'absolute', position: 'absolute',
top: '-9999px', top: '-9999px',
...@@ -49,7 +52,7 @@ const RoutedTabs = ({ tabs }: Props) => { ...@@ -49,7 +52,7 @@ const RoutedTabs = ({ tabs }: Props) => {
const handleTabChange = React.useCallback((index: number) => { const handleTabChange = React.useCallback((index: number) => {
const nextTab = tabs[index]; const nextTab = tabs[index];
router.query.tab = nextTab.id; router.query = { ...pick(router.query, PRESERVED_QUERY), tab: nextTab.id };
router.push(router); router.push(router);
}, [ tabs, router ]); }, [ tabs, router ]);
......
...@@ -25,4 +25,4 @@ const Utilization = ({ className, value, colorScheme = 'green' }: Props) => { ...@@ -25,4 +25,4 @@ const Utilization = ({ className, value, colorScheme = 'green' }: Props) => {
); );
}; };
export default chakra(Utilization); export default React.memo(chakra(Utilization));
import { Alert, Box, HStack, Show, Button } from '@chakra-ui/react'; import { Alert, Box, HStack, Show, Button } from '@chakra-ui/react';
import React, { useState, useCallback } from 'react'; import React, { useState, useCallback } from 'react';
import type { TTxsFilters } from 'types/api/txsFilters';
import type { Sort } from 'types/client/txs-sort'; import type { Sort } from 'types/client/txs-sort';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import FilterButton from 'ui/shared/FilterButton'; // import FilterInput from 'ui/shared/FilterInput';
import FilterInput from 'ui/shared/FilterInput';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SortButton from 'ui/shared/SortButton'; import SortButton from 'ui/shared/SortButton';
// import TxsFilters from './TxsFilters';
import TxsSkeletonDesktop from './TxsSkeletonDesktop'; import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsSkeletonMobile from './TxsSkeletonMobile'; import TxsSkeletonMobile from './TxsSkeletonMobile';
import TxsWithSort from './TxsWithSort'; import TxsWithSort from './TxsWithSort';
import useQueryWithPages from './useQueryWithPages'; import useQueryWithPages from './useQueryWithPages';
type Props = { type Props = {
queryName: string;
showDescription?: boolean; showDescription?: boolean;
stateFilter: 'validated' | 'pending'; stateFilter: TTxsFilters['filter'];
} }
const TxsContent = ({ const TxsContent = ({
showDescription, showDescription,
queryName,
stateFilter, stateFilter,
}: Props) => { }: Props) => {
const [ sorting, setSorting ] = useState<Sort>(); const [ sorting, setSorting ] = useState<Sort>();
// const [ filters, setFilters ] = useState<Partial<TTxsFilters>>({ type: [], method: [] });
const sort = useCallback((field: 'val' | 'fee') => () => { const sort = useCallback((field: 'val' | 'fee') => () => {
if (field === 'val') { if (field === 'val') {
...@@ -62,7 +62,8 @@ const TxsContent = ({ ...@@ -62,7 +62,8 @@ const TxsContent = ({
onNextPageClick, onNextPageClick,
hasPagination, hasPagination,
resetPage, resetPage,
} = useQueryWithPages(queryName, stateFilter); } = useQueryWithPages({ filter: stateFilter });
// } = useQueryWithPages({ ...filters, filter: stateFilter });
const isMobile = useIsMobile(false); const isMobile = useIsMobile(false);
...@@ -91,13 +92,12 @@ const TxsContent = ({ ...@@ -91,13 +92,12 @@ const TxsContent = ({
<> <>
{ showDescription && <Box mb={ 12 }>Only the first 10,000 elements are displayed</Box> } { showDescription && <Box mb={ 12 }>Only the first 10,000 elements are displayed</Box> }
<HStack mb={ 6 }> <HStack mb={ 6 }>
{ /* TODO */ } { /* api is not implemented */ }
<FilterButton { /* <TxsFilters
isActive={ false } filters={ filters }
// eslint-disable-next-line react/jsx-no-bind onFiltersChange={ setFilters }
onClick={ () => {} }
appliedFiltersNum={ 0 } appliedFiltersNum={ 0 }
/> /> */ }
{ isMobile && ( { isMobile && (
<SortButton <SortButton
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
...@@ -106,13 +106,14 @@ const TxsContent = ({ ...@@ -106,13 +106,14 @@ const TxsContent = ({
display={{ base: 'block', lg: 'none' }} display={{ base: 'block', lg: 'none' }}
/> />
) } ) }
<FilterInput { /* api is not implemented */ }
{ /* <FilterInput
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onChange={ () => {} } onChange={ () => {} }
maxW="360px" maxW="360px"
size="xs" size="xs"
placeholder="Search by addresses, hash, method..." placeholder="Search by addresses, hash, method..."
/> /> */ }
</HStack> </HStack>
{ content } { content }
<Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}> <Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}>
......
import {
Button,
Checkbox,
CheckboxGroup,
Grid,
Link,
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
Text,
useColorModeValue,
useDisclosure,
Flex,
} from '@chakra-ui/react';
import React, { useCallback, useState } from 'react';
import type { TTxsFilters, TypeFilter, MethodFilter } from 'types/api/txsFilters';
import FilterButton from 'ui/shared/FilterButton';
interface Props {
appliedFiltersNum?: number;
filters: Partial<TTxsFilters>;
onFiltersChange: (val: Partial<TTxsFilters>) => void;
}
const TYPE_OPTIONS = [
{ title: 'Token transfer', id: 'token_transfer' },
{ title: 'Contract Creation', id: 'contract_creation' },
{ title: 'Contract Call', id: 'contract_call' },
{ title: 'Coin Transfer', id: 'coin_transfer' },
{ title: 'Token Creation', id: 'token_creation' },
];
const METHOD_OPTIONS = [
{ title: 'Approve', id: 'approve' },
{ title: 'Transfer', id: 'transfer' },
{ title: 'Multicall', id: 'multicall' },
{ title: 'Mint', id: 'mint' },
{ title: 'Commit', id: 'commit' },
];
// TODO: i think we need to reload page after applying filters,
// because we need to reset pagination, clear query caches, reconnect websocket...
// also mobile version of filters is not implemented
const TxsFilters = ({ onFiltersChange, filters, appliedFiltersNum }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const [ typeFilter, setTypeFilter ] = useState<Array<TypeFilter>>(filters.type || []);
const [ methodFilter, setMethodFilter ] = useState<Array<MethodFilter>>(filters.method || []);
const onTypeFilterChange = useCallback((val: Array<TypeFilter>) => {
setTypeFilter(val);
}, []);
const onMethodFilterChange = useCallback((val: Array<MethodFilter>) => {
setMethodFilter(val);
}, []);
const onFilterReset = useCallback(() => {
setTypeFilter([]);
setMethodFilter([]);
onFiltersChange({ type: [], method: [] });
onClose();
}, [ onClose, onFiltersChange ]);
const onFilterApply = useCallback(() => {
onFiltersChange({ type: typeFilter, method: methodFilter } as Partial<TTxsFilters>);
onClose();
}, [ onClose, onFiltersChange, typeFilter, methodFilter ]);
const borderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<FilterButton
isActive={ isOpen || Number(appliedFiltersNum) > 0 }
onClick={ onToggle }
appliedFiltersNum={ appliedFiltersNum }
/>
</PopoverTrigger>
<PopoverContent w={{ md: '100%', lg: '438px' }}>
<PopoverBody px={ 4 } py={ 6 }>
<Text variant="secondary" fontWeight="600" fontSize="sm">Type</Text>
<Grid gridTemplateColumns="1fr 1fr" rowGap={ 5 } mt={ 4 } mb={ 4 } pb={ 6 } borderBottom="1px solid" borderColor={ borderColor }>
<CheckboxGroup size="lg" onChange={ onTypeFilterChange } defaultValue={ typeFilter }>
{ TYPE_OPTIONS.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
</CheckboxGroup>
</Grid>
<Text variant="secondary" fontWeight="600" fontSize="sm">Method</Text>
<Grid gridTemplateColumns="1fr 1fr" rowGap={ 5 } mt={ 4 } mb={ 4 } pb={ 6 } borderBottom="1px solid" borderColor={ borderColor }>
<CheckboxGroup size="lg" onChange={ onMethodFilterChange } defaultValue={ methodFilter }>
{ METHOD_OPTIONS.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
</CheckboxGroup>
</Grid>
<Flex alignItems="center" justifyContent="space-between">
<Link fontSize="sm" onClick={ onFilterReset }>Reset filters</Link>
<Button variant="outline" size="sm" onClick={ onFilterApply }>Apply</Button>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(TxsFilters);
import React from 'react'; import React from 'react';
import { QueryKeys } from 'types/client/queries';
import TxsContent from './TxsContent'; import TxsContent from './TxsContent';
type Props = { type Props = {
...@@ -11,7 +9,6 @@ type Props = { ...@@ -11,7 +9,6 @@ type Props = {
const TxsTab = ({ tab }: Props) => { const TxsTab = ({ tab }: Props) => {
return ( return (
<TxsContent <TxsContent
queryName={ tab === 'validated' ? QueryKeys.transactionsValidated : QueryKeys.transactionsPending }
showDescription={ tab === 'validated' } showDescription={ tab === 'validated' }
stateFilter={ tab } stateFilter={ tab }
/> />
......
...@@ -5,12 +5,14 @@ import React, { useCallback } from 'react'; ...@@ -5,12 +5,14 @@ import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll'; import { animateScroll } from 'react-scroll';
import type { TransactionsResponse } from 'types/api/transaction'; import type { TransactionsResponse } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
const PAGINATION_FIELDS = [ 'block_number', 'index', 'items_count' ]; const PAGINATION_FIELDS = [ 'block_number', 'index', 'items_count' ];
export default function useQueryWithPages(queryName: string, filter: string) { export default function useQueryWithPages(filters: TTxsFilters) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const [ page, setPage ] = React.useState(1); const [ page, setPage ] = React.useState(1);
...@@ -19,13 +21,19 @@ export default function useQueryWithPages(queryName: string, filter: string) { ...@@ -19,13 +21,19 @@ export default function useQueryWithPages(queryName: string, filter: string) {
const fetch = useFetch(); const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, TransactionsResponse>( const { data, isLoading, isError } = useQuery<unknown, unknown, TransactionsResponse>(
[ queryName, { page } ], [ QueryKeys.transactions, { page, filters } ],
async() => { async() => {
const params: Array<string> = []; const params: Array<string> = [];
Object.entries(currPageParams).forEach(([ key, val ]) => params.push(`${ key }=${ val }`)); Object.entries({ ...filters, ...currPageParams }).forEach(([ key, val ]) => {
if (Array.isArray(val)) {
val.length && params.push(`${ key }=${ val.join(',') }`);
} else if (val) {
params.push(`${ key }=${ val }`);
}
});
return fetch(`/api/transactions?filter=${ filter }${ params.length ? '&' + params.join('&') : '' }`); return fetch(`/api/transactions${ params.length ? '?' + params.join('&') : '' }`);
}, },
{ staleTime: Infinity }, { staleTime: Infinity },
); );
...@@ -43,9 +51,11 @@ export default function useQueryWithPages(queryName: string, filter: string) { ...@@ -43,9 +51,11 @@ export default function useQueryWithPages(queryName: string, filter: string) {
} }
const nextPageQuery = { ...router.query }; const nextPageQuery = { ...router.query };
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString()); Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }); router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
animateScroll.scrollToTop({ duration: 0 }); .then(() => {
setPage(prev => prev + 1); animateScroll.scrollToTop({ duration: 0 });
setPage(prev => prev + 1);
});
}, [ data, page, pageParams, router ]); }, [ data, page, pageParams, router ]);
const onPrevPageClick = useCallback(() => { const onPrevPageClick = useCallback(() => {
...@@ -60,9 +70,11 @@ export default function useQueryWithPages(queryName: string, filter: string) { ...@@ -60,9 +70,11 @@ export default function useQueryWithPages(queryName: string, filter: string) {
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString()); Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
} }
router.query = nextPageQuery; router.query = nextPageQuery;
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }); router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
animateScroll.scrollToTop({ duration: 0 }); .then(() => {
setPage(prev => prev - 1); animateScroll.scrollToTop({ duration: 0 });
setPage(prev => prev - 1);
});
}, [ router, page, pageParams ]); }, [ router, page, pageParams ]);
const resetPage = useCallback(() => { const resetPage = useCallback(() => {
......
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