Commit e24ac42e authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into skeletons/rest-pages

parents 05bc3818 0526882e
...@@ -6,6 +6,10 @@ NEXT_PUBLIC_APP_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_HOST__ ...@@ -6,6 +6,10 @@ NEXT_PUBLIC_APP_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_HOST__
NEXT_PUBLIC_APP_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_PORT__ NEXT_PUBLIC_APP_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_PORT__
NEXT_PUBLIC_AUTH_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_AUTH_URL__ NEXT_PUBLIC_AUTH_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_AUTH_URL__
# version config
NEXT_PUBLIC_GIT_COMMIT_SHA=__PLACEHOLDER_FOR_NEXT_PUBLIC_GIT_COMMIT_SHA__
NEXT_PUBLIC_GIT_TAG = __PLACEHOLDER_FOR_NEXT_PUBLIC_GIT_TAG__
# network config # network config
NEXT_PUBLIC_NETWORK_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_NAME__ NEXT_PUBLIC_NETWORK_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_NAME__
NEXT_PUBLIC_NETWORK_SHORT_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_SHORT_NAME__ NEXT_PUBLIC_NETWORK_SHORT_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_SHORT_NAME__
...@@ -26,11 +30,8 @@ NEXT_PUBLIC_IS_TESTNET=__PLACEHOLDER_FOR_NEXT_PUBLIC_IS_TESTNET__ ...@@ -26,11 +30,8 @@ NEXT_PUBLIC_IS_TESTNET=__PLACEHOLDER_FOR_NEXT_PUBLIC_IS_TESTNET__
# ui config # ui config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=__PLACEHOLDER_FOR_NEXT_PUBLIC_BLOCKSCOUT_VERSION__ NEXT_PUBLIC_BLOCKSCOUT_VERSION=__PLACEHOLDER_FOR_NEXT_PUBLIC_BLOCKSCOUT_VERSION__
NEXT_PUBLIC_FOOTER_GITHUB_LINK=__PLACEHOLDER_FOR_NEXT_PUBLIC_FOOTER_GITHUB_LINK__
NEXT_PUBLIC_FOOTER_TWITTER_LINK=__PLACEHOLDER_FOR_NEXT_PUBLIC_FOOTER_TWITTER_LINK__
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=__PLACEHOLDER_FOR_NEXT_PUBLIC_FOOTER_TELEGRAM_LINK__
NEXT_PUBLIC_FOOTER_STAKING_LINK=__PLACEHOLDER_FOR_NEXT_PUBLIC_FOOTER_STAKING_LINK__
NEXT_PUBLIC_FEATURED_NETWORKS=__PLACEHOLDER_FOR_NEXT_PUBLIC_FEATURED_NETWORKS__ NEXT_PUBLIC_FEATURED_NETWORKS=__PLACEHOLDER_FOR_NEXT_PUBLIC_FEATURED_NETWORKS__
NEXT_PUBLIC_FOOTER_LINKS=__PLACEHOLDER_FOR_NEXT_PUBLIC_FOOTER_LINKS__
NEXT_PUBLIC_NETWORK_EXPLORERS=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_EXPLORERS__ NEXT_PUBLIC_NETWORK_EXPLORERS=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_EXPLORERS__
NEXT_PUBLIC_OTHER_LINKS=__PLACEHOLDER_FOR_NEXT_PUBLIC_OTHER_LINKS__ NEXT_PUBLIC_OTHER_LINKS=__PLACEHOLDER_FOR_NEXT_PUBLIC_OTHER_LINKS__
NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_CONFIG_URL__ NEXT_PUBLIC_MARKETPLACE_CONFIG_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_MARKETPLACE_CONFIG_URL__
......
...@@ -35,6 +35,8 @@ WORKDIR /app ...@@ -35,6 +35,8 @@ WORKDIR /app
# pass commit sha to the app (uses by sentry.io as release version) # pass commit sha to the app (uses by sentry.io as release version)
ARG GIT_COMMIT_SHA ARG GIT_COMMIT_SHA
ENV NEXT_PUBLIC_GIT_COMMIT_SHA=$GIT_COMMIT_SHA ENV NEXT_PUBLIC_GIT_COMMIT_SHA=$GIT_COMMIT_SHA
# pass git tag to the app (for the footer link)
ARG GIT_TAG ARG GIT_TAG
ENV NEXT_PUBLIC_GIT_TAG=$GIT_TAG ENV NEXT_PUBLIC_GIT_TAG=$GIT_TAG
......
...@@ -96,15 +96,12 @@ const config = Object.freeze({ ...@@ -96,15 +96,12 @@ const config = Object.freeze({
rpcUrl: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_RPC_URL), rpcUrl: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_RPC_URL),
isTestnet: getEnvValue(process.env.NEXT_PUBLIC_IS_TESTNET) === 'true', isTestnet: getEnvValue(process.env.NEXT_PUBLIC_IS_TESTNET) === 'true',
}, },
footerLinks: {
github: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_GITHUB_LINK),
twitter: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_TWITTER_LINK),
telegram: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_TELEGRAM_LINK),
staking: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_STAKING_LINK),
},
otherLinks: parseEnvJson<Array<NavItemExternal>>(getEnvValue(process.env.NEXT_PUBLIC_OTHER_LINKS)) || [], otherLinks: parseEnvJson<Array<NavItemExternal>>(getEnvValue(process.env.NEXT_PUBLIC_OTHER_LINKS)) || [],
featuredNetworks: getEnvValue(process.env.NEXT_PUBLIC_FEATURED_NETWORKS), featuredNetworks: getEnvValue(process.env.NEXT_PUBLIC_FEATURED_NETWORKS),
footerLinks: getEnvValue(process.env.NEXT_PUBLIC_FOOTER_LINKS),
blockScoutVersion: getEnvValue(process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION), blockScoutVersion: getEnvValue(process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION),
frontendVersion: getEnvValue(process.env.NEXT_PUBLIC_GIT_TAG),
frontendCommit: getEnvValue(process.env.NEXT_PUBLIC_GIT_COMMIT_SHA),
isAccountSupported: getEnvValue(process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED) === 'true', isAccountSupported: getEnvValue(process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED) === 'true',
marketplaceConfigUrl: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_CONFIG_URL), marketplaceConfigUrl: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_CONFIG_URL),
marketplaceSubmitForm: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM), marketplaceSubmitForm: getEnvValue(process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM),
......
...@@ -7,8 +7,6 @@ NEXT_PUBLIC_APP_ENV=development ...@@ -7,8 +7,6 @@ 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
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml NEXT_PUBLIC_API_SPEC_URL=https://raw.githubusercontent.com/blockscout/blockscout-api-v2-swagger/main/swagger.yaml
# api config # api config
......
...@@ -5,8 +5,6 @@ NEXT_PUBLIC_APP_ENV=testing ...@@ -5,8 +5,6 @@ NEXT_PUBLIC_APP_ENV=testing
# ui config # ui config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_IS_TESTNET=true NEXT_PUBLIC_IS_TESTNET=true
# api config # api config
......
...@@ -7,10 +7,6 @@ NEXT_PUBLIC_APP_ENV=development ...@@ -7,10 +7,6 @@ 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
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}] NEXT_PUBLIC_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_cap'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
......
# ui config # ui config
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json NEXT_PUBLIC_FEATURED_NETWORKS=https://raw.githubusercontent.com/blockscout/frontend-configs/dev/configs/featured-networks/eth-goerli.json
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address','block':'/ethereum/poa/core/block'}}] NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address','block':'/ethereum/poa/core/block'}}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
......
...@@ -7,16 +7,14 @@ NEXT_PUBLIC_APP_ENV=testing ...@@ -7,16 +7,14 @@ NEXT_PUBLIC_APP_ENV=testing
# ui config # ui config
NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}] NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Bitquery','baseUrl':'https://explorer.bitquery.io/','paths':{'tx':'/goerli/tx','address':'/goerli/address','token':'/goerli/token','block':'/goerli/block'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address','token':'/token','block':'/block'}}]
NEXT_PUBLIC_GIT_TAG=v1.0.11
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cap']
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND= NEXT_PUBLIC_HOMEPAGE_PLATE_BACKGROUND=
NEXT_PUBLIC_FEATURED_NETWORKS= NEXT_PUBLIC_FEATURED_NETWORKS=
NEXT_PUBLIC_FOOTER_LINKS=
NEXT_PUBLIC_NETWORK_LOGO= NEXT_PUBLIC_NETWORK_LOGO=
NEXT_PUBLIC_NETWORK_LOGO_DARK= NEXT_PUBLIC_NETWORK_LOGO_DARK=
NEXT_PUBLIC_NETWORK_ICON= NEXT_PUBLIC_NETWORK_ICON=
......
...@@ -352,10 +352,6 @@ frontend: ...@@ -352,10 +352,6 @@ frontend:
environment: environment:
NEXT_PUBLIC_BLOCKSCOUT_VERSION: NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v5.1.5-beta _default: v5.1.5-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV: NEXT_PUBLIC_APP_ENV:
_default: stable _default: stable
NEXT_PUBLIC_APP_INSTANCE: NEXT_PUBLIC_APP_INSTANCE:
......
...@@ -275,10 +275,6 @@ frontend: ...@@ -275,10 +275,6 @@ frontend:
_default: / _default: /
NEXT_PUBLIC_BLOCKSCOUT_VERSION: NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v5.1.2-beta _default: v5.1.2-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV: NEXT_PUBLIC_APP_ENV:
_default: stable _default: stable
NEXT_PUBLIC_APP_INSTANCE: NEXT_PUBLIC_APP_INSTANCE:
......
...@@ -61,10 +61,6 @@ frontend: ...@@ -61,10 +61,6 @@ frontend:
environment: environment:
NEXT_PUBLIC_BLOCKSCOUT_VERSION: NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v5.1.0-beta _default: v5.1.0-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV: NEXT_PUBLIC_APP_ENV:
_default: preview _default: preview
NEXT_PUBLIC_APP_INSTANCE: NEXT_PUBLIC_APP_INSTANCE:
......
...@@ -57,18 +57,10 @@ frontend: ...@@ -57,18 +57,10 @@ frontend:
environment: environment:
NEXT_PUBLIC_BLOCKSCOUT_VERSION: NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v5.1.2-beta _default: v5.1.2-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK:
_default: https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK:
_default: https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_APP_ENV: NEXT_PUBLIC_APP_ENV:
_default: preview _default: preview
NEXT_PUBLIC_APP_INSTANCE: NEXT_PUBLIC_APP_INSTANCE:
_default: eth_goerli _default: eth_goerli
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK:
_default: https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK:
_default: https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_NETWORK_NAME: NEXT_PUBLIC_NETWORK_NAME:
_default: Göerli _default: Göerli
NEXT_PUBLIC_NETWORK_SHORT_NAME: NEXT_PUBLIC_NETWORK_SHORT_NAME:
......
...@@ -30,11 +30,8 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -30,11 +30,8 @@ The app instance could be customized by passing following variables to NodeJS en
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| NEXT_PUBLIC_FEATURED_NETWORKS | `string` | URL of configuration file (`.json` format only) which contains list of featured networks that will be shown in the network menu. See [below](#featured-network-configuration-properties) list of available properties for particular network | - | - | `https://example.com/featured_networks_config.json` | | NEXT_PUBLIC_FEATURED_NETWORKS | `string` | URL of configuration file (`.json` format only) which contains list of featured networks that will be shown in the network menu. See [below](#featured-network-configuration-properties) list of available properties for particular network | - | - | `https://example.com/featured_networks_config.json` |
| NEXT_PUBLIC_OTHER_LINKS | `Array<{url: string; text: string}>` | List of links for the "Other" navigation menu | - | - | `[{'url':'https://blockscout.com','text':'Blockscout'}]` | | NEXT_PUBLIC_OTHER_LINKS | `Array<{url: string; text: string}>` | List of links for the "Other" navigation menu | - | - | `[{'url':'https://blockscout.com','text':'Blockscout'}]` |
| NEXT_PUBLIC_FOOTER_LINKS | `string` | URL of configuration file (`.json` format only) which contains list of link groups to be displayed in the footer. See [below](#footer-links-configuration-properties) list of available properties for particular group | - | - | `https://example.com/footer_links_config.json` |
| NEXT_PUBLIC_BLOCKSCOUT_VERSION | `string` | Current running version of Blockscout (used to display link to release in the footer) | - | - | `v.5.1.0-beta` | NEXT_PUBLIC_BLOCKSCOUT_VERSION | `string` | Current running version of Blockscout (used to display link to release in the footer) | - | - | `v.5.1.0-beta`
| NEXT_PUBLIC_FOOTER_GITHUB_LINK | `string` | Link to Github in the footer | - | - | `https://github.com/blockscout/blockscout` |
| NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` | Link to Twitter in the footer | - | - | `https://www.twitter.com/blockscoutcom` |
| NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` | Link to Telegram in the footer | - | - | `https://t.me/poa_network` |
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` | Link to staking dashboard in the footer | - | - | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | `string` | URL of configuration file (`.json` format only) which contains list of apps that will be shown on the marketplace page. See [below](#marketplace-app-configuration-properties) list of available properties for an app | - | - | `https://example.com/marketplace_config.json` | | NEXT_PUBLIC_MARKETPLACE_CONFIG_URL | `string` | URL of configuration file (`.json` format only) which contains list of apps that will be shown on the marketplace page. See [below](#marketplace-app-configuration-properties) list of available properties for an app | - | - | `https://example.com/marketplace_config.json` |
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | - | - | `https://airtable.com/shrqUAcjgGJ4jU88C` | | NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | - | - | `https://airtable.com/shrqUAcjgGJ4jU88C` |
| NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | - | - | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` | | NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | - | - | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` |
...@@ -108,6 +105,13 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i ...@@ -108,6 +105,13 @@ For each application, you need to specify the `MarketplaceCategoryId` to which i
*Note* The url of an entity will be constructed as `<baseUrl><paths[<entity-type>]><entity-id>`, e.g `https://explorer.anyblock.tools/ethereum/poa/core/tx/<tx-id>` *Note* The url of an entity will be constructed as `<baseUrl><paths[<entity-type>]><entity-id>`, e.g `https://explorer.anyblock.tools/ethereum/poa/core/tx/<tx-id>`
## Footer links configuration properties
| Variable | Type| Description | Is required | Default value | Example value |
| --- | --- | --- | --- | --- | --- |
| title | `string` | Title of link group | yes | - | `Company` |
| links | `Array<{'text':string;'url':string;}>` | list of links | yes | - | `[{'text':'Homepage','url':'https://www.blockscout.com'}]` |
## App configuration ## App configuration
......
import type { ExternalProvider } from 'types/client/wallets'; import type { WindowProvider } from 'wagmi';
type CPreferences = { type CPreferences = {
zone: string; zone: string;
...@@ -8,9 +8,7 @@ type CPreferences = { ...@@ -8,9 +8,7 @@ type CPreferences = {
declare global { declare global {
export interface Window { export interface Window {
ethereum?: { ethereum?: WindowProvider;
providers?: Array<ExternalProvider>;
};
coinzilla_display: Array<CPreferences>; coinzilla_display: Array<CPreferences>;
ga?: { ga?: {
getAll: () => Array<{ get: (prop: string) => string }>; getAll: () => Array<{ get: (prop: string) => string }>;
......
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<path fill="currentColor" d="M13.76 7.444h2.88a5.76 5.76 0 0 1 0 11.52v2.52c-3.6-1.44-8.64-3.6-8.64-8.28a5.76 5.76 0 0 1 5.76-5.76Zm1.44 10.08h1.44a4.32 4.32 0 1 0 0-8.64h-2.88a4.32 4.32 0 0 0-4.32 4.32c0 2.6 1.772 4.296 5.76 6.106v-1.786Z"/>
<path fill="currentColor" d="M10.4 3.444H7.2a6.32 6.32 0 0 0-4.526 1.923A6.65 6.65 0 0 0 .8 10.008c0 1.741.674 3.41 1.874 4.642A6.32 6.32 0 0 0 7.2 16.572v2.872c4-1.64 9.6-4.102 9.6-9.436a6.65 6.65 0 0 0-1.875-4.641A6.32 6.32 0 0 0 10.4 3.444ZM8.8 14.931H7.2c-.63 0-1.255-.127-1.837-.374a4.793 4.793 0 0 1-1.557-1.068c-.446-.457-.8-1-1.04-1.597a5.033 5.033 0 0 1 0-3.768 4.933 4.933 0 0 1 1.04-1.597c.445-.457.975-.82 1.557-1.067A4.696 4.696 0 0 1 7.2 5.085h3.2a4.74 4.74 0 0 1 3.394 1.442 4.988 4.988 0 0 1 1.406 3.481c0 2.962-1.97 4.895-6.4 6.958v-2.035Z"/>
</svg>
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<path d="M15 25c5.523 0 10-4.477 10-10S20.523 5 15 5 5 9.477 5 15s4.477 10 10 10Z" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M21.667 22.333a6.666 6.666 0 1 0-13.334 0" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
<path d="M15 15.667a4 4 0 1 0 0-8 4 4 0 0 0 0 8Z" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linejoin="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 15">
<mask id="discord_svg__a" width="20" height="15" x="0" y="0" fill="#000" maskUnits="userSpaceOnUse">
<path fill="#fff" d="M0 0h20v15H0z"/>
<path fill-rule="evenodd" d="M18.133 6.988a22.044 22.044 0 0 0-1.62-4.36.295.295 0 0 0-.1-.108 12.246 12.246 0 0 0-2.142-1.008C12.891 1.01 12.238 1 12.211 1a.281.281 0 0 0-.281.242l-.084.586a18.723 18.723 0 0 0-3.696 0l-.084-.586A.281.281 0 0 0 7.785 1c-.027 0-.68 0-2.06.508-.743.267-1.459.607-2.136 1.012a.295.295 0 0 0-.1.108 22.038 22.038 0 0 0-1.62 4.36C1.142 9.924 1 11.393 1 11.454a.281.281 0 0 0 .069.21 7.763 7.763 0 0 0 2.56 1.792c.888.378 1.824.635 2.782.762a.281.281 0 0 0 .27-.14l.76-1.301a14.77 14.77 0 0 0 2.559.21 14.83 14.83 0 0 0 2.559-.213l.76 1.304a.28.28 0 0 0 .241.14h.028a10.73 10.73 0 0 0 2.782-.756 7.76 7.76 0 0 0 2.559-1.792.28.28 0 0 0 .069-.21c.001-.067-.132-1.536-.866-4.472Zm-1.977 5.95c-.785.325-1.606.557-2.446.69l-.574-.983c.659-.175 2.028-.627 3.07-1.575a.283.283 0 0 0-.378-.422c-1.245 1.138-3.097 1.512-3.176 1.528h-.01c-.87.169-1.755.252-2.641.248a13.58 13.58 0 0 1-2.641-.24h-.01c-.02 0-1.905-.37-3.178-1.528a.283.283 0 1 0-.378.422c1.042.948 2.412 1.406 3.07 1.575l-.574.984a10.7 10.7 0 0 1-2.445-.692 7.345 7.345 0 0 1-2.271-1.567c.046-.41.236-1.838.844-4.253a22.03 22.03 0 0 1 1.535-4.168 12.397 12.397 0 0 1 1.969-.916 8.627 8.627 0 0 1 1.628-.449l.05.336c-.563.14-1.948.543-3.045 1.34a.281.281 0 0 0 .332.454c1.23-.894 2.925-1.268 3.07-1.3h.004A16.65 16.65 0 0 1 10 2.304c.682-.003 1.363.036 2.04.118.128.026 1.828.398 3.074 1.3a.281.281 0 1 0 .332-.454c-1.097-.797-2.476-1.198-3.044-1.34l.049-.336a8.68 8.68 0 0 1 1.627.449 12.4 12.4 0 0 1 1.968.916 21.974 21.974 0 0 1 1.547 4.168c.603 2.415.793 3.843.844 4.253a7.345 7.345 0 0 1-2.282 1.56ZM7.36 6.652c-.866 0-1.57.787-1.57 1.755 0 .967.703 1.753 1.57 1.753.868 0 1.57-.786 1.57-1.753 0-.968-.705-1.755-1.57-1.755Zm0 2.953c-.562 0-1.008-.534-1.008-1.191s.451-1.193 1.008-1.193c.557 0 1.007.535 1.007 1.193S7.916 9.598 7.36 9.598v.007Zm3.71-1.198c0-.968.704-1.755 1.57-1.755.865 0 1.57.787 1.57 1.755 0 .967-.705 1.753-1.57 1.753-.867 0-1.57-.786-1.57-1.753Zm.562.007c0 .657.445 1.19 1.007 1.19V9.6c.557 0 1.008-.526 1.008-1.184 0-.658-.452-1.193-1.008-1.193-.555 0-1.007.536-1.007 1.193Z" clip-rule="evenodd"/>
</mask>
<path fill="currentColor" fill-rule="evenodd" d="M18.133 6.988a22.044 22.044 0 0 0-1.62-4.36.295.295 0 0 0-.1-.108 12.246 12.246 0 0 0-2.142-1.008C12.891 1.01 12.238 1 12.211 1a.281.281 0 0 0-.281.242l-.084.586a18.723 18.723 0 0 0-3.696 0l-.084-.586A.281.281 0 0 0 7.785 1c-.027 0-.68 0-2.06.508-.743.267-1.459.607-2.136 1.012a.295.295 0 0 0-.1.108 22.038 22.038 0 0 0-1.62 4.36C1.142 9.924 1 11.393 1 11.454a.281.281 0 0 0 .069.21 7.763 7.763 0 0 0 2.56 1.792c.888.378 1.824.635 2.782.762a.281.281 0 0 0 .27-.14l.76-1.301a14.77 14.77 0 0 0 2.559.21 14.83 14.83 0 0 0 2.559-.213l.76 1.304a.28.28 0 0 0 .241.14h.028a10.73 10.73 0 0 0 2.782-.756 7.76 7.76 0 0 0 2.559-1.792.28.28 0 0 0 .069-.21c.001-.067-.132-1.536-.866-4.472Zm-1.977 5.95c-.785.325-1.606.557-2.446.69l-.574-.983c.659-.175 2.028-.627 3.07-1.575a.283.283 0 0 0-.378-.422c-1.245 1.138-3.097 1.512-3.176 1.528h-.01c-.87.169-1.755.252-2.641.248a13.58 13.58 0 0 1-2.641-.24h-.01c-.02 0-1.905-.37-3.178-1.528a.283.283 0 1 0-.378.422c1.042.948 2.412 1.406 3.07 1.575l-.574.984a10.7 10.7 0 0 1-2.445-.692 7.345 7.345 0 0 1-2.271-1.567c.046-.41.236-1.838.844-4.253a22.03 22.03 0 0 1 1.535-4.168 12.397 12.397 0 0 1 1.969-.916 8.627 8.627 0 0 1 1.628-.449l.05.336c-.563.14-1.948.543-3.045 1.34a.281.281 0 0 0 .332.454c1.23-.894 2.925-1.268 3.07-1.3h.004A16.65 16.65 0 0 1 10 2.304c.682-.003 1.363.036 2.04.118.128.026 1.828.398 3.074 1.3a.281.281 0 1 0 .332-.454c-1.097-.797-2.476-1.198-3.044-1.34l.049-.336a8.68 8.68 0 0 1 1.627.449 12.4 12.4 0 0 1 1.968.916 21.974 21.974 0 0 1 1.547 4.168c.603 2.415.793 3.843.844 4.253a7.345 7.345 0 0 1-2.282 1.56ZM7.36 6.652c-.866 0-1.57.787-1.57 1.755 0 .967.703 1.753 1.57 1.753.868 0 1.57-.786 1.57-1.753 0-.968-.705-1.755-1.57-1.755Zm0 2.953c-.562 0-1.008-.534-1.008-1.191s.451-1.193 1.008-1.193c.557 0 1.007.535 1.007 1.193S7.916 9.598 7.36 9.598v.007Zm3.71-1.198c0-.968.704-1.755 1.57-1.755.865 0 1.57.787 1.57 1.755 0 .967-.705 1.753-1.57 1.753-.867 0-1.57-.786-1.57-1.753Zm.562.007c0 .657.445 1.19 1.007 1.19V9.6c.557 0 1.008-.526 1.008-1.184 0-.658-.452-1.193-1.008-1.193-.555 0-1.007.536-1.007 1.193Z" clip-rule="evenodd"/>
<path fill="currentColor" d="m16.513 2.628.358-.178-.006-.01-.352.188Zm1.62 4.36.388-.097v-.002l-.388.099Zm-1.72-4.468.217-.336-.006-.004-.006-.004-.205.344ZM14.27 1.512l-.136.376h.002l.134-.376ZM12.211 1l-.004.4h.004V1Zm-.186.068-.26-.303.26.303Zm-.095.174.396.057v-.001l-.396-.056Zm-.084.586-.04.398.381.038.055-.379-.396-.057Zm-3.696 0-.396.057.055.38.38-.039-.039-.398Zm-.084-.586-.396.056.396-.056Zm-.095-.174.26-.303-.26.303ZM7.785 1v.4h.003L7.785 1Zm-2.06.508.135.376h.003l-.139-.376ZM3.588 2.52l-.206-.343-.012.007.218.336Zm-.1.108-.353-.189-.005.011.358.178Zm-1.62 4.36-.388-.1v.003l.388.097ZM1 11.454l.399.035.001-.018v-.017H1Zm.013.112.38-.124-.38.124Zm.056.098-.3.265.004.005.005.005.291-.274Zm2.56 1.792-.16.367.003.001.156-.368Zm2.782.762-.053.397h.008l.008.002.037-.399Zm.27-.14-.345-.202-.002.003.347.199Zm.76-1.301.067-.394-.273-.047-.14.24.345.2Zm2.559.21.001-.4H10l.002.4Zm2.559-.213.346-.201-.14-.24-.274.047.068.394Zm.76 1.304.346-.2-.001-.002-.346.202Zm.101.102.202-.346-.201.346Zm.14.038-.002.4h.002v-.4Zm.028 0v.4h.027l.026-.003-.053-.397Zm2.782-.756.156.368.002-.001-.158-.367Zm2.559-1.792.29.275.006-.005.004-.006-.3-.264Zm.056-.098-.38-.124.38.124Zm.013-.113-.4-.008v.022l.002.022.398-.036Zm-5.289 2.17-.345.201.137.237.271-.043-.063-.395Zm2.446-.692.153.37.006-.003-.16-.367Zm-3.02-.292-.102-.387-.508.134.265.454.345-.201Zm3.07-1.575-.267-.298-.002.002.27.296Zm.067-.089-.361-.173.36.173Zm.027-.107-.4-.022.4.022Zm-.016-.11-.377.134.377-.133Zm-.056-.094.298-.267-.298.267Zm-.088-.067.172-.36-.172.36Zm-.108-.027-.021.4.021-.4Zm-.109.015.133.378-.133-.377Zm-.095.057-.267-.298-.003.003.27.295Zm-3.176 1.528-.079-.392.08.392Zm-.002 0v.4h.04l.04-.008-.08-.392Zm-.008 0v-.4h-.039l-.037.008.075.392ZM10 12.424l.001-.4h-.004l.003.4Zm-2.641-.24.075-.392-.037-.007H7.36v.4Zm-3.187-1.528.27-.296-.002-.002-.268.298Zm-.204-.072-.022-.4.022.4Zm-.196.094-.297-.267.297.267Zm.022.4.27-.296-.003-.002-.267.298Zm3.07 1.575.345.201.267-.457-.513-.131-.1.387Zm-.574.984-.062.396.27.042.138-.236-.346-.202Zm-2.445-.692-.162.367.008.003.154-.37Zm-2.271-1.567-.398-.045-.021.19.134.135.285-.28Zm.844-4.253-.388-.1v.003l.388.097Zm1.535-4.168-.2-.346-.106.061-.053.11.36.175Zm1.969-.916.136.376.007-.003-.143-.373Zm1.628-.449.396-.058-.06-.406-.404.07.068.394Zm.05.336.096.388.352-.088-.053-.358-.396.058Zm-3.045 1.34-.235-.323.236.323Zm-.074.081-.342-.208.342.208Zm-.038.103-.395-.061.395.061Zm.235.321.061-.395-.061.395Zm.11-.004-.095-.39.094.39Zm.099-.047L4.653 3.4h-.001l.236.323Zm3.07-1.3.087.391-.087-.39Zm.004 0-.048-.397-.02.002-.018.004.086.39ZM10 2.304l-.002.4h.003l-.001-.4Zm2.04.118.082-.392-.017-.003-.017-.002-.048.397Zm3.074 1.3.236-.323h-.001l-.235.323Zm.21.051-.062-.395.061.395Zm.234-.321.395-.061-.395.061Zm-.112-.184.236-.323h-.001l-.235.323Zm-3.044-1.34-.396-.058-.053.358.351.088.098-.388Zm.049-.336.068-.394-.405-.07-.06.406.397.058Zm1.627.449-.144.373.007.003.137-.376Zm1.968.916.36-.176-.054-.11-.106-.06-.2.346Zm1.547 4.168.388-.097v-.003l-.388.1Zm.844 4.253.284.281.137-.137-.024-.194-.397.05ZM7.36 9.606v.4h.4v-.4h-.4Zm0-.007v-.4h-.4v.4h.4Zm5.28.007v.4h.4v-.4h-.4Zm0-.007v-.4h-.4v.4h.4Zm3.514-6.793a21.645 21.645 0 0 1 1.59 4.28l.775-.197a22.449 22.449 0 0 0-1.65-4.439l-.715.356Zm.04.05a.105.105 0 0 1-.035-.038l.705-.379a.695.695 0 0 0-.235-.255l-.434.672Zm-2.058-.967c.72.257 1.414.584 2.071.975l.41-.688a12.647 12.647 0 0 0-2.212-1.04l-.27.753ZM12.21 1.4c-.028 0 .578-.001 1.924.488l.273-.752C12.992.62 12.293.6 12.21.6v.8Zm.075-.029a.119.119 0 0 1-.079.029l.008-.8a.681.681 0 0 0-.45.165l.52.606Zm.04-.073a.119.119 0 0 1-.04.073l-.522-.606a.681.681 0 0 0-.23.42l.792.113Zm-.085.587.085-.586-.792-.114-.084.586.792.114Zm-4.051.341a18.358 18.358 0 0 1 3.616 0l.08-.796a19.12 19.12 0 0 0-3.775 0l.079.796ZM7.67 1.3l.084.586.792-.114-.084-.586-.792.114Zm.04.072a.119.119 0 0 1-.04-.073l.792-.112a.681.681 0 0 0-.23-.421l-.522.606Zm.079.029a.119.119 0 0 1-.08-.029l.523-.606A.681.681 0 0 0 7.78.6l.008.8Zm-1.926.483a9.294 9.294 0 0 1 1.473-.426A3.785 3.785 0 0 1 7.78 1.4h.005V.6c-.079 0-.778.01-2.199.532l.277.751Zm-2.07.98c.656-.392 1.348-.72 2.067-.979l-.271-.753c-.767.277-1.506.627-2.206 1.046l.41.686Zm.048-.045a.105.105 0 0 1-.035.038l-.435-.672a.695.695 0 0 0-.235.255l.705.379ZM2.256 7.087a21.65 21.65 0 0 1 1.59-4.28l-.715-.357A22.438 22.438 0 0 0 1.48 6.89l.775.198Zm-.855 4.367c0 .016.005-.059.03-.26.024-.187.064-.462.126-.828.124-.732.339-1.823.7-3.282L1.48 6.89a46.404 46.404 0 0 0-.712 3.34c-.064.377-.105.664-.13.861a4.254 4.254 0 0 0-.037.362h.8Zm-.007-.012a.119.119 0 0 1 .006.047l-.797-.071a.681.681 0 0 0 .03.272l.761-.248ZM1.37 11.4c.011.012.02.027.024.042l-.76.248a.68.68 0 0 0 .136.239l.6-.529Zm2.418 1.689a7.362 7.362 0 0 1-2.427-1.7l-.582.55a8.16 8.16 0 0 0 2.692 1.884l.317-.734Zm2.677.733a10.328 10.328 0 0 1-2.68-.734l-.312.736c.922.392 1.893.658 2.886.79l.106-.792Zm-.082.01a.119.119 0 0 1 .066-.012l-.074.796a.682.682 0 0 0 .378-.074l-.37-.71Zm-.048.047a.12.12 0 0 1 .048-.046l.37.71a.681.681 0 0 0 .276-.267l-.694-.397Zm.761-1.304-.76 1.3.692.404.759-1.3-.691-.404Zm2.903.011a14.377 14.377 0 0 1-2.49-.204l-.135.79c.869.147 1.748.219 2.629.214l-.004-.8Zm2.493-.206a14.38 14.38 0 0 1-2.49.206l-.003.8c.88.004 1.76-.069 2.628-.218l-.135-.788Zm1.173 1.496-.76-1.303-.69.402.759 1.304.69-.403Zm-.042-.042c.018.01.032.026.043.043l-.693.4a.68.68 0 0 0 .247.249l.402-.692Zm-.06-.016a.12.12 0 0 1 .06.016l-.403.692a.68.68 0 0 0 .339.092l.005-.8Zm.026 0h-.028v.8h.028v-.8Zm2.626-.725c-.856.362-1.757.607-2.678.729l.104.793c.993-.13 1.963-.395 2.886-.785l-.312-.737Zm2.424-1.698a7.364 7.364 0 0 1-2.427 1.7l.317.734a8.163 8.163 0 0 0 2.692-1.884l-.582-.55Zm-.033.053a.118.118 0 0 1 .024-.042l.6.528a.681.681 0 0 0 .136-.238l-.76-.248Zm-.006.047a.118.118 0 0 1 .006-.047l.76.248a.682.682 0 0 0 .031-.273l-.796.072Zm-.855-4.41c.364 1.458.579 2.55.702 3.281a19.276 19.276 0 0 1 .147 1.04c.006.061.005.065.005.045l.8.017a1.4 1.4 0 0 0-.009-.137 20.03 20.03 0 0 0-.154-1.098 44.227 44.227 0 0 0-.715-3.342l-.776.194Zm-3.972 6.939c.87-.138 1.722-.38 2.536-.717l-.307-.74c-.756.315-1.546.538-2.355.667l.126.79Zm-.982-1.178.574.984.69-.402-.573-.985-.691.403Zm3.146-2.072c-.97.882-2.264 1.315-2.903 1.484l.205.773c.678-.18 2.122-.652 3.236-1.665l-.538-.592Zm-.025.034a.118.118 0 0 1 .027-.036l.534.595a.684.684 0 0 0 .16-.213l-.721-.346Zm-.012.044a.116.116 0 0 1 .012-.044l.721.346a.682.682 0 0 0 .066-.258l-.799-.044Zm.007.046a.117.117 0 0 1-.007-.046l.8.044a.682.682 0 0 0-.038-.264l-.755.266Zm.023.039a.116.116 0 0 1-.023-.04l.755-.265a.682.682 0 0 0-.136-.23l-.596.535Zm.037.027a.116.116 0 0 1-.037-.027l.596-.534a.684.684 0 0 0-.213-.16l-.346.721Zm.044.011a.116.116 0 0 1-.044-.011l.345-.721a.682.682 0 0 0-.258-.066l-.043.798Zm.045-.006a.117.117 0 0 1-.045.006l.043-.798a.682.682 0 0 0-.264.037l.266.755Zm.039-.023a.116.116 0 0 1-.04.023l-.265-.755a.684.684 0 0 0-.23.136l.535.596Zm-3.363 1.622c.08-.016 2.032-.405 3.366-1.625l-.54-.59c-1.156 1.057-2.907 1.415-2.985 1.43l.159.785Zm-.002 0h.002l-.16-.784h-.002l.16.784Zm-.088.008h.008v-.8h-.008v.8Zm-2.643.248a13.98 13.98 0 0 0 2.718-.255l-.152-.785a13.19 13.19 0 0 1-2.563.24l-.003.8Zm-2.714-.246c.896.17 1.807.253 2.719.246l-.006-.8a13.19 13.19 0 0 1-2.563-.232l-.15.786Zm.066.007h.009v-.8h-.01v.8Zm-3.447-1.633c.69.627 1.531 1.03 2.188 1.277a8.823 8.823 0 0 0 1.083.328 3.754 3.754 0 0 0 .114.023l.014.002a.32.32 0 0 0 .048.003v-.8a.381.381 0 0 1 .047.003l.011.001h.005a.634.634 0 0 1-.066-.013 8.056 8.056 0 0 1-.975-.296c-.61-.229-1.347-.589-1.93-1.12l-.539.592Zm.087.032a.117.117 0 0 1-.085-.03l.535-.596a.683.683 0 0 0-.494-.173l.044.799Zm.08-.039a.117.117 0 0 1-.08.039l-.044-.8a.684.684 0 0 0-.471.227l.595.534Zm.03-.084a.117.117 0 0 1-.03.084l-.595-.534a.683.683 0 0 0-.174.493l.799-.043Zm-.039-.08c.023.02.037.049.039.08l-.799.043c.01.181.091.351.226.472l.534-.596Zm2.902 1.485c-.634-.163-1.929-.6-2.9-1.484l-.538.592c1.113 1.013 2.557 1.492 3.24 1.666l.198-.774Zm-.327 1.573.574-.985-.692-.402-.573.984.69.403Zm-2.945-.524c.814.339 1.666.58 2.537.718l.125-.79A10.3 10.3 0 0 1 4 12.575l-.308.739ZM1.29 11.658a7.745 7.745 0 0 0 2.394 1.654l.323-.732a6.944 6.944 0 0 1-2.147-1.483l-.57.561Zm.74-4.63c-.611 2.431-.804 3.878-.853 4.305l.795.09c.045-.392.231-1.803.834-4.2l-.775-.195Zm1.565-4.246A22.372 22.372 0 0 0 2.03 7.026l.775.199a21.572 21.572 0 0 1 1.508-4.092l-.72-.351Zm2.191-1.117a12.84 12.84 0 0 0-2.032.946l.4.693a12 12 0 0 1 1.905-.887l-.273-.752Zm1.697-.467a8.93 8.93 0 0 0-1.704.47l.287.746a8.227 8.227 0 0 1 1.553-.428l-.136-.788Zm.513.672-.05-.336-.79.116.049.336.791-.116ZM4.791 3.592c1.033-.75 2.356-1.138 2.906-1.276l-.194-.776c-.574.144-2.021.56-3.182 1.405l.47.647Zm.032-.035a.119.119 0 0 1-.031.034l-.472-.646a.681.681 0 0 0-.18.196l.683.416Zm.016-.043a.119.119 0 0 1-.016.043l-.683-.416a.681.681 0 0 0-.091.25l.79.123Zm-.021-.089a.119.119 0 0 1 .021.089l-.79-.123a.681.681 0 0 0 .123.506l.646-.472Zm-.078-.047a.119.119 0 0 1 .078.047l-.646.472a.681.681 0 0 0 .445.271l.123-.79Zm-.046.002a.119.119 0 0 1 .046-.002l-.123.79a.7.7 0 0 0 .266-.01l-.19-.778Zm-.042.02a.119.119 0 0 1 .042-.02l.189.777a.681.681 0 0 0 .24-.112l-.47-.645Zm3.22-1.368c-.145.032-1.915.419-3.22 1.367l.471.647c1.156-.84 2.775-1.2 2.922-1.233l-.173-.781Zm.004 0h-.004l.173.781h.003l-.172-.782Zm2.126-.128a17.049 17.049 0 0 0-2.088.12l.095.795A16.25 16.25 0 0 1 10 2.704l.003-.8Zm2.087.12a16.943 16.943 0 0 0-2.09-.12l.003.8a16.14 16.14 0 0 1 1.991.115l.096-.794Zm3.26 1.374c-1.32-.956-3.093-1.34-3.226-1.368l-.164.783c.123.026 1.75.384 2.921 1.233l.47-.648Zm-.086-.02a.119.119 0 0 1 .088.021l-.472.646a.679.679 0 0 0 .507.123l-.123-.79Zm-.078.047a.119.119 0 0 1 .078-.047l.123.79a.681.681 0 0 0 .445-.27l-.646-.473Zm-.021.089a.119.119 0 0 1 .021-.089l.646.472a.681.681 0 0 0 .123-.506l-.79.123Zm.047.077a.118.118 0 0 1-.047-.077l.79-.123a.681.681 0 0 0-.271-.446l-.472.646Zm-2.906-1.275c.557.14 1.874.525 2.907 1.276l.47-.647c-1.161-.844-2.602-1.26-3.183-1.405l-.194.776Zm-.25-.782-.048.336.791.116.05-.336-.792-.116Zm2.167.133a9.083 9.083 0 0 0-1.702-.47l-.137.79c.53.091 1.05.234 1.552.427l.287-.747Zm2.025.944a12.796 12.796 0 0 0-2.032-.946l-.273.752c.66.24 1.297.536 1.905.887l.4-.693Zm1.734 4.414a22.376 22.376 0 0 0-1.575-4.244l-.718.353a21.572 21.572 0 0 1 1.519 4.092l.774-.201Zm.854 4.303c-.053-.423-.245-1.867-.853-4.3l-.776.194c.599 2.397.786 3.81.835 4.205l.794-.098Zm-2.52 1.976a7.745 7.745 0 0 0 2.407-1.645l-.568-.563a6.946 6.946 0 0 1-2.158 1.474l.32.734ZM6.191 8.408c0-.79.566-1.355 1.17-1.355v-.8c-1.127 0-1.97 1.01-1.97 2.155h.8ZM7.36 9.76c-.606 0-1.17-.564-1.17-1.353h-.8c0 1.146.841 2.153 1.97 2.153v-.8Zm1.17-1.353c0 .79-.564 1.353-1.17 1.353v.8c1.13 0 1.97-1.008 1.97-2.153h-.8ZM7.36 7.053c.603 0 1.17.565 1.17 1.355h.8c0-1.145-.843-2.155-1.97-2.155v.8ZM5.953 8.415c0 .81.563 1.59 1.408 1.59v-.8c-.28 0-.608-.287-.608-.79h-.8Zm1.408-1.593c-.838 0-1.408.781-1.408 1.593h.8c0-.502.333-.793.608-.793v-.8Zm1.407 1.593c0-.813-.568-1.593-1.407-1.593v.8c.275 0 .607.29.607.793h.8ZM7.361 9.999c.834 0 1.407-.767 1.407-1.584h-.8c0 .5-.33.784-.607.784v.8Zm.4-.393v-.007h-.8v.007h.8Zm4.88-3.353c-1.127 0-1.97 1.01-1.97 2.155h.8c0-.79.566-1.355 1.17-1.355v-.8Zm1.97 2.155c0-1.146-.843-2.155-1.97-2.155v.8c.604 0 1.17.565 1.17 1.355h.8Zm-1.97 2.153c1.126 0 1.97-1.007 1.97-2.153h-.8c0 .789-.566 1.353-1.17 1.353v.8Zm-1.97-2.153c0 1.145.841 2.153 1.97 2.153v-.8c-.605 0-1.17-.564-1.17-1.353h-.8Zm1.97.798c-.28 0-.608-.288-.608-.791h-.8c0 .81.562 1.59 1.407 1.59v-.8Zm-.4.393v.007h.8v-.007h-.8Zm1.007-1.184c0 .499-.329.784-.608.784v.8c.835 0 1.408-.767 1.408-1.584h-.8Zm-.608-.793c.275 0 .608.29.608.793h.8c0-.814-.572-1.593-1.408-1.593v.8Zm-.607.793c0-.502.334-.793.607-.793v-.8c-.837 0-1.407.782-1.407 1.593h.8Z" mask="url(#discord_svg__a)"/>
</svg>
...@@ -18,7 +18,8 @@ export default function buildUrl<R extends ResourceName>( ...@@ -18,7 +18,8 @@ export default function buildUrl<R extends ResourceName>(
const url = new URL(compile(path)(pathParams), baseUrl); const url = new URL(compile(path)(pathParams), baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => { queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
value && url.searchParams.append(key, String(value)); // there are some pagination params that can be null for the next page
(value || value === null) && url.searchParams.append(key, String(value));
}); });
return url.toString(); return url.toString();
......
...@@ -160,7 +160,6 @@ export const RESOURCES = { ...@@ -160,7 +160,6 @@ export const RESOURCES = {
// BLOCKS, TXS // BLOCKS, TXS
blocks: { blocks: {
path: '/api/v2/blocks', path: '/api/v2/blocks',
paginationFields: [ 'block_number' as const, 'items_count' as const ],
filterFields: [ 'type' as const ], filterFields: [ 'type' as const ],
}, },
block: { block: {
...@@ -170,28 +169,23 @@ export const RESOURCES = { ...@@ -170,28 +169,23 @@ export const RESOURCES = {
block_txs: { block_txs: {
path: '/api/v2/blocks/:height_or_hash/transactions', path: '/api/v2/blocks/:height_or_hash/transactions',
pathParams: [ 'height_or_hash' as const ], pathParams: [ 'height_or_hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
block_withdrawals: { block_withdrawals: {
path: '/api/v2/blocks/:height_or_hash/withdrawals', path: '/api/v2/blocks/:height_or_hash/withdrawals',
pathParams: [ 'height_or_hash' as const ], pathParams: [ 'height_or_hash' as const ],
paginationFields: [ 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
txs_validated: { txs_validated: {
path: '/api/v2/transactions', path: '/api/v2/transactions',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'filter' as const, 'index' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ], filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
}, },
txs_pending: { txs_pending: {
path: '/api/v2/transactions', path: '/api/v2/transactions',
paginationFields: [ 'filter' as const, 'hash' as const, 'inserted_at' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'method' as const ], filterFields: [ 'filter' as const, 'type' as const, 'method' as const ],
}, },
txs_watchlist: { txs_watchlist: {
path: '/api/v2/transactions/watchlist', path: '/api/v2/transactions/watchlist',
paginationFields: [ 'block_number' as const, 'index' as const, 'items_count' as const ],
filterFields: [ ], filterFields: [ ],
}, },
tx: { tx: {
...@@ -201,19 +195,16 @@ export const RESOURCES = { ...@@ -201,19 +195,16 @@ export const RESOURCES = {
tx_internal_txs: { tx_internal_txs: {
path: '/api/v2/transactions/:hash/internal-transactions', path: '/api/v2/transactions/:hash/internal-transactions',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'transaction_hash' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ ], filterFields: [ ],
}, },
tx_logs: { tx_logs: {
path: '/api/v2/transactions/:hash/logs', path: '/api/v2/transactions/:hash/logs',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'transaction_hash' as const, 'index' as const ],
filterFields: [ ], filterFields: [ ],
}, },
tx_token_transfers: { tx_token_transfers: {
path: '/api/v2/transactions/:hash/token-transfers', path: '/api/v2/transactions/:hash/token-transfers',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'transaction_hash' as const, 'index' as const ],
filterFields: [ 'type' as const ], filterFields: [ 'type' as const ],
}, },
tx_raw_trace: { tx_raw_trace: {
...@@ -226,7 +217,6 @@ export const RESOURCES = { ...@@ -226,7 +217,6 @@ export const RESOURCES = {
}, },
withdrawals: { withdrawals: {
path: '/api/v2/withdrawals', path: '/api/v2/withdrawals',
paginationFields: [ 'index' as const, 'items_count' as const ],
filterFields: [], filterFields: [],
}, },
withdrawals_counters: { withdrawals_counters: {
...@@ -236,7 +226,6 @@ export const RESOURCES = { ...@@ -236,7 +226,6 @@ export const RESOURCES = {
// ADDRESSES // ADDRESSES
addresses: { addresses: {
path: '/api/v2/addresses/', path: '/api/v2/addresses/',
paginationFields: [ 'fetched_coin_balance' as const, 'hash' as const, 'items_count' as const ],
filterFields: [ ], filterFields: [ ],
}, },
...@@ -256,31 +245,26 @@ export const RESOURCES = { ...@@ -256,31 +245,26 @@ export const RESOURCES = {
address_txs: { address_txs: {
path: '/api/v2/addresses/:hash/transactions', path: '/api/v2/addresses/:hash/transactions',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [ 'filter' as const ], filterFields: [ 'filter' as const ],
}, },
address_internal_txs: { address_internal_txs: {
path: '/api/v2/addresses/:hash/internal-transactions', path: '/api/v2/addresses/:hash/internal-transactions',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ 'filter' as const ], filterFields: [ 'filter' as const ],
}, },
address_token_transfers: { address_token_transfers: {
path: '/api/v2/addresses/:hash/token-transfers', path: '/api/v2/addresses/:hash/token-transfers',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'token' as const ], filterFields: [ 'filter' as const, 'type' as const, 'token' as const ],
}, },
address_blocks_validated: { address_blocks_validated: {
path: '/api/v2/addresses/:hash/blocks-validated', path: '/api/v2/addresses/:hash/blocks-validated',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'block_number' as const ],
filterFields: [ ], filterFields: [ ],
}, },
address_coin_balance: { address_coin_balance: {
path: '/api/v2/addresses/:hash/coin-balance-history', path: '/api/v2/addresses/:hash/coin-balance-history',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'block_number' as const ],
filterFields: [ ], filterFields: [ ],
}, },
address_coin_balance_chart: { address_coin_balance_chart: {
...@@ -290,19 +274,16 @@ export const RESOURCES = { ...@@ -290,19 +274,16 @@ export const RESOURCES = {
address_logs: { address_logs: {
path: '/api/v2/addresses/:hash/logs', path: '/api/v2/addresses/:hash/logs',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'transaction_index' as const, 'index' as const, 'block_number' as const ],
filterFields: [ ], filterFields: [ ],
}, },
address_tokens: { address_tokens: {
path: '/api/v2/addresses/:hash/tokens', path: '/api/v2/addresses/:hash/tokens',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'token_name' as const, 'token_type' as const, 'value' as const, 'fiat_value' as const, 'id' as const ],
filterFields: [ 'type' as const ], filterFields: [ 'type' as const ],
}, },
address_withdrawals: { address_withdrawals: {
path: '/api/v2/addresses/:hash/withdrawals', path: '/api/v2/addresses/:hash/withdrawals',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -341,7 +322,6 @@ export const RESOURCES = { ...@@ -341,7 +322,6 @@ export const RESOURCES = {
verified_contracts: { verified_contracts: {
path: '/api/v2/smart-contracts', path: '/api/v2/smart-contracts',
paginationFields: [ 'items_count' as const, 'smart_contract_id' as const ],
filterFields: [ 'q' as const, 'filter' as const ], filterFields: [ 'q' as const, 'filter' as const ],
}, },
verified_contracts_counters: { verified_contracts_counters: {
...@@ -366,24 +346,20 @@ export const RESOURCES = { ...@@ -366,24 +346,20 @@ export const RESOURCES = {
token_holders: { token_holders: {
path: '/api/v2/tokens/:hash/holders', path: '/api/v2/tokens/:hash/holders',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'items_count' as const, 'value' as const ],
filterFields: [], filterFields: [],
}, },
token_transfers: { token_transfers: {
path: '/api/v2/tokens/:hash/transfers', path: '/api/v2/tokens/:hash/transfers',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [], filterFields: [],
}, },
token_inventory: { token_inventory: {
path: '/api/v2/tokens/:hash/instances', path: '/api/v2/tokens/:hash/instances',
pathParams: [ 'hash' as const ], pathParams: [ 'hash' as const ],
paginationFields: [ 'unique_token' as const ],
filterFields: [], filterFields: [],
}, },
tokens: { tokens: {
path: '/api/v2/tokens', path: '/api/v2/tokens',
paginationFields: [ 'holder_count' as const, 'items_count' as const, 'name' as const, 'market_cap' as const ],
filterFields: [ 'q' as const, 'type' as const ], filterFields: [ 'q' as const, 'type' as const ],
}, },
...@@ -399,13 +375,11 @@ export const RESOURCES = { ...@@ -399,13 +375,11 @@ export const RESOURCES = {
token_instance_transfers: { token_instance_transfers: {
path: '/api/v2/tokens/:hash/instances/:id/transfers', path: '/api/v2/tokens/:hash/instances/:id/transfers',
pathParams: [ 'hash' as const, 'id' as const ], pathParams: [ 'hash' as const, 'id' as const ],
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'token_id' as const ],
filterFields: [], filterFields: [],
}, },
token_instance_holders: { token_instance_holders: {
path: '/api/v2/tokens/:hash/instances/:id/holders', path: '/api/v2/tokens/:hash/instances/:id/holders',
pathParams: [ 'hash' as const, 'id' as const ], pathParams: [ 'hash' as const, 'id' as const ],
paginationFields: [ 'items_count' as const, 'token_id' as const, 'value' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -438,17 +412,6 @@ export const RESOURCES = { ...@@ -438,17 +412,6 @@ export const RESOURCES = {
// SEARCH // SEARCH
search: { search: {
path: '/api/v2/search', path: '/api/v2/search',
paginationFields: [
'address_hash' as const,
'block_hash' as const,
'holder_count' as const,
'inserted_at' as const,
'item_type' as const,
'items_count' as const,
'name' as const,
'q' as const,
'tx_hash' as const,
],
filterFields: [ 'q' ], filterFields: [ 'q' ],
}, },
search_check_redirect: { search_check_redirect: {
...@@ -463,7 +426,6 @@ export const RESOURCES = { ...@@ -463,7 +426,6 @@ export const RESOURCES = {
// L2 // L2
l2_deposits: { l2_deposits: {
path: '/api/v2/optimism/deposits', path: '/api/v2/optimism/deposits',
paginationFields: [ 'nonce' as const, 'items_count' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -473,7 +435,6 @@ export const RESOURCES = { ...@@ -473,7 +435,6 @@ export const RESOURCES = {
l2_withdrawals: { l2_withdrawals: {
path: '/api/v2/optimism/withdrawals', path: '/api/v2/optimism/withdrawals',
paginationFields: [ 'nonce' as const, 'items_count' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -483,7 +444,6 @@ export const RESOURCES = { ...@@ -483,7 +444,6 @@ export const RESOURCES = {
l2_output_roots: { l2_output_roots: {
path: '/api/v2/optimism/output-roots', path: '/api/v2/optimism/output-roots',
paginationFields: [ 'index' as const, 'items_count' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -493,7 +453,6 @@ export const RESOURCES = { ...@@ -493,7 +453,6 @@ export const RESOURCES = {
l2_txn_batches: { l2_txn_batches: {
path: '/api/v2/optimism/txn-batches', path: '/api/v2/optimism/txn-batches',
paginationFields: [ 'block_number' as const, 'items_count' as const ],
filterFields: [], filterFields: [],
}, },
...@@ -527,10 +486,6 @@ export type ResourceFiltersKey<R extends ResourceName> = typeof RESOURCES[R] ext ...@@ -527,10 +486,6 @@ export type ResourceFiltersKey<R extends ResourceName> = typeof RESOURCES[R] ext
ArrayElement<typeof RESOURCES[R]['filterFields']> : ArrayElement<typeof RESOURCES[R]['filterFields']> :
never; never;
export type ResourcePaginationKey<R extends ResourceName> = typeof RESOURCES[R] extends {paginationFields: Array<unknown>} ?
ArrayElement<typeof RESOURCES[R]['paginationFields']> :
never;
export const resourceKey = (x: keyof typeof RESOURCES) => x; export const resourceKey = (x: keyof typeof RESOURCES) => x;
type ResourcePathParamName<Resource extends ResourceName> = type ResourcePathParamName<Resource extends ResourceName> =
......
...@@ -10,8 +10,7 @@ export function walletConnect(): CspDev.DirectiveDescriptor { ...@@ -10,8 +10,7 @@ export function walletConnect(): CspDev.DirectiveDescriptor {
return { return {
'connect-src': [ 'connect-src': [
'*.walletconnect.com', '*.walletconnect.com',
'wss://*.bridge.walletconnect.org', 'wss://relay.walletconnect.com',
'wss://www.walletlink.org',
], ],
'img-src': [ 'img-src': [
'*.walletconnect.com', '*.walletconnect.com',
......
import React from 'react';
import appConfig from 'configs/app/config';
import isBrowser from 'lib/isBrowser';
const base = 'https://github.com/blockscout/blockscout/issues/new/';
const bodyTemplate = `*Describe your issue here.*
### Environment
* Backend Version/branch/commit: ${ appConfig.blockScoutVersion }
* Frontend Version+commit: ${ [ appConfig.frontendVersion, appConfig.frontendCommit ].filter(Boolean).join('+') }
* User Agent: __userAgent__
### Steps to reproduce
*Tell us how to reproduce this issue. ❤️ if you can push up a branch to your fork with a regression test we can run to reproduce locally.*
### Expected behaviour
*Tell us what should happen.*
### Actual behaviour
*Tell us what happens instead.*`;
const labels = 'new UI';
const title = `${ appConfig.network.name }: <Issue Title>`;
export default function useIssueUrl() {
const [ userAgent, setUserAgent ] = React.useState('');
const isInBrowser = isBrowser();
React.useEffect(() => {
if (isInBrowser) {
setUserAgent(window.navigator.userAgent);
}
}, [ isInBrowser ]);
const params = new URLSearchParams({ labels, title, body: bodyTemplate.replace('__userAgent__', userAgent) });
return base + '?' + params.toString();
}
...@@ -15,7 +15,6 @@ import globeIcon from 'icons/globe-b.svg'; ...@@ -15,7 +15,6 @@ import globeIcon from 'icons/globe-b.svg';
import graphQLIcon from 'icons/graphQL.svg'; import graphQLIcon from 'icons/graphQL.svg';
import outputRootsIcon from 'icons/output_roots.svg'; import outputRootsIcon from 'icons/output_roots.svg';
import privateTagIcon from 'icons/privattags.svg'; import privateTagIcon from 'icons/privattags.svg';
import profileIcon from 'icons/profile.svg';
import publicTagIcon from 'icons/publictags.svg'; import publicTagIcon from 'icons/publictags.svg';
import apiDocsIcon from 'icons/restAPI.svg'; import apiDocsIcon from 'icons/restAPI.svg';
import rpcIcon from 'icons/RPC.svg'; import rpcIcon from 'icons/RPC.svg';
...@@ -27,6 +26,7 @@ import txnBatchIcon from 'icons/txn_batches.svg'; ...@@ -27,6 +26,7 @@ import txnBatchIcon from 'icons/txn_batches.svg';
import verifiedIcon from 'icons/verified.svg'; import verifiedIcon from 'icons/verified.svg';
import watchlistIcon from 'icons/watchlist.svg'; import watchlistIcon from 'icons/watchlist.svg';
import { rightLineArrow } from 'lib/html-entities'; import { rightLineArrow } from 'lib/html-entities';
import UserAvatar from 'ui/shared/UserAvatar';
interface ReturnType { interface ReturnType {
mainNavItems: Array<NavItem | NavGroupItem>; mainNavItems: Array<NavItem | NavGroupItem>;
...@@ -213,7 +213,7 @@ export default function useNavItems(): ReturnType { ...@@ -213,7 +213,7 @@ export default function useNavItems(): ReturnType {
const profileItem = { const profileItem = {
text: 'My profile', text: 'My profile',
nextRoute: { pathname: '/auth/profile' as const }, nextRoute: { pathname: '/auth/profile' as const },
icon: profileIcon, iconComponent: UserAvatar,
isActive: pathname === '/auth/profile', isActive: pathname === '/auth/profile',
}; };
......
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import difference from 'lodash/difference';
import mapValues from 'lodash/mapValues';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import pick from 'lodash/pick';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll'; import { animateScroll } from 'react-scroll';
import type { PaginatedResources, PaginatedResponse, PaginationFilters } from 'lib/api/resources'; import type { PaginatedResources, PaginationFilters } from 'lib/api/resources';
import { RESOURCES } from 'lib/api/resources'; import { RESOURCES } from 'lib/api/resources';
import type { Params as UseApiQueryParams } from 'lib/api/useApiQuery'; import type { Params as UseApiQueryParams } from 'lib/api/useApiQuery';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
interface Params<Resource extends PaginatedResources> { interface Params<Resource extends PaginatedResources> {
resourceName: Resource; resourceName: Resource;
...@@ -20,6 +18,18 @@ interface Params<Resource extends PaginatedResources> { ...@@ -20,6 +18,18 @@ interface Params<Resource extends PaginatedResources> {
scrollRef?: React.RefObject<HTMLDivElement>; scrollRef?: React.RefObject<HTMLDivElement>;
} }
type NextPageParams = Record<string, unknown>;
function getPaginationParamsFromQuery(queryString: string | Array<string> | undefined) {
if (queryString) {
try {
return JSON.parse(decodeURIComponent(getQueryParamString(queryString))) as NextPageParams;
} catch (error) {}
}
return {};
}
export default function useQueryWithPages<Resource extends PaginatedResources>({ export default function useQueryWithPages<Resource extends PaginatedResources>({
resourceName, resourceName,
filters, filters,
...@@ -31,14 +41,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -31,14 +41,9 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
type NextPageParams = {
[K in keyof PaginatedResponse<Resource>['next_page_params']]: string;
}
const currPageParams = mapValues(pick(router.query, resource.paginationFields), (value) => value?.toString()) as NextPageParams;
const [ page, setPage ] = React.useState<number>(router.query.page && !Array.isArray(router.query.page) ? Number(router.query.page) : 1); const [ page, setPage ] = React.useState<number>(router.query.page && !Array.isArray(router.query.page) ? Number(router.query.page) : 1);
const [ pageParams, setPageParams ] = React.useState<Record<number, NextPageParams>>({ const [ pageParams, setPageParams ] = React.useState<Record<number, NextPageParams>>({
[page]: currPageParams, [page]: getPaginationParamsFromQuery(router.query.next_page_params),
}); });
const [ hasPagination, setHasPagination ] = React.useState(page > 1); const [ hasPagination, setHasPagination ] = React.useState(page > 1);
...@@ -65,21 +70,21 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -65,21 +70,21 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
// we hide next page button if no next_page_params // we hide next page button if no next_page_params
return; return;
} }
const nextPageParams = data.next_page_params;
setPageParams((prev) => ({ setPageParams((prev) => ({
...prev, ...prev,
[page + 1]: mapValues(nextPageParams, (value) => String(value)) as NextPageParams, [page + 1]: data.next_page_params as NextPageParams,
})); }));
setPage(prev => prev + 1); setPage(prev => prev + 1);
const nextPageQuery = { ...router.query }; const nextPageQuery = {
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = String(val)); ...router.query,
nextPageQuery.page = String(page + 1); page: String(page + 1),
setHasPagination(true); next_page_params: encodeURIComponent(JSON.stringify(data.next_page_params)),
};
setHasPagination(true);
scrollToTop(); scrollToTop();
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }); router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true });
}, [ data?.next_page_params, page, router, scrollToTop ]); }, [ data?.next_page_params, page, router, scrollToTop ]);
...@@ -88,7 +93,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -88,7 +93,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
// we dont have pagination params for the first page // we dont have pagination params for the first page
let nextPageQuery: typeof router.query = { ...router.query }; let nextPageQuery: typeof router.query = { ...router.query };
if (page === 2) { if (page === 2) {
nextPageQuery = omit(router.query, difference(resource.paginationFields, resource.filterFields), 'page'); nextPageQuery = omit(router.query, [ 'next_page_params', 'page' ]);
canGoBackwards.current = true; canGoBackwards.current = true;
} else { } else {
const nextPageParams = pageParams[page - 1]; const nextPageParams = pageParams[page - 1];
...@@ -103,13 +108,13 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -103,13 +108,13 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] }); page === 2 && queryClient.removeQueries({ queryKey: [ resourceName ] });
}); });
setHasPagination(true); setHasPagination(true);
}, [ router, page, resource.paginationFields, resource.filterFields, pageParams, scrollToTop, queryClient, resourceName ]); }, [ router, page, pageParams, scrollToTop, queryClient, resourceName ]);
const resetPage = useCallback(() => { const resetPage = useCallback(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] }); queryClient.removeQueries({ queryKey: [ resourceName ] });
scrollToTop(); scrollToTop();
const nextRouterQuery = omit(router.query, difference(resource.paginationFields, resource.filterFields), 'page'); const nextRouterQuery = omit(router.query, [ 'next_page_params', 'page' ]);
router.push({ pathname: router.pathname, query: nextRouterQuery }, undefined, { shallow: true }).then(() => { router.push({ pathname: router.pathname, query: nextRouterQuery }, undefined, { shallow: true }).then(() => {
queryClient.removeQueries({ queryKey: [ resourceName ] }); queryClient.removeQueries({ queryKey: [ resourceName ] });
setPage(1); setPage(1);
...@@ -123,10 +128,10 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -123,10 +128,10 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
}); });
setHasPagination(true); setHasPagination(true);
}, [ queryClient, resourceName, router, resource.paginationFields, resource.filterFields, scrollToTop ]); }, [ queryClient, resourceName, router, scrollToTop ]);
const onFilterChange = useCallback((newFilters: PaginationFilters<Resource> | undefined) => { const onFilterChange = useCallback((newFilters: PaginationFilters<Resource> | undefined) => {
const newQuery = omit<typeof router.query>(router.query, resource.paginationFields, 'page', resource.filterFields); const newQuery = omit<typeof router.query>(router.query, 'next_page_params', 'page', resource.filterFields);
if (newFilters) { if (newFilters) {
Object.entries(newFilters).forEach(([ key, value ]) => { Object.entries(newFilters).forEach(([ key, value ]) => {
if (value && value.length) { if (value && value.length) {
...@@ -147,7 +152,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({ ...@@ -147,7 +152,7 @@ export default function useQueryWithPages<Resource extends PaginatedResources>({
setPage(1); setPage(1);
setPageParams({}); setPageParams({});
}); });
}, [ router, resource.paginationFields, resource.filterFields, scrollToTop ]); }, [ router, resource.filterFields, scrollToTop ]);
const nextPageParams = data?.next_page_params; const nextPageParams = data?.next_page_params;
......
...@@ -49,9 +49,9 @@ export namespace SocketMessage { ...@@ -49,9 +49,9 @@ export namespace SocketMessage {
SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>; SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>;
export type AddressTokenBalance = SocketMessageParamsGeneric<'token_balance', { block_number: number }>; export type AddressTokenBalance = SocketMessageParamsGeneric<'token_balance', { block_number: number }>;
export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', { coin_balance: AddressCoinBalanceHistoryItem }>; export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', { coin_balance: AddressCoinBalanceHistoryItem }>;
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>; export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transactions: Array<Transaction> }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>; export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transactions: Array<Transaction> }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>; export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfers: Array<TokenTransfer> }>;
export type AddressChangedBytecode = SocketMessageParamsGeneric<'changed_bytecode', Record<string, never>>; export type AddressChangedBytecode = SocketMessageParamsGeneric<'changed_bytecode', Record<string, never>>;
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>; export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>;
export type TokenTotalSupply = SocketMessageParamsGeneric<'total_supply', {total_supply: number }>; export type TokenTotalSupply = SocketMessageParamsGeneric<'total_supply', {total_supply: number }>;
......
import React from 'react'; import React from 'react';
import type { WindowProvider } from 'wagmi';
import type { ExternalProvider } from 'types/client/wallets'; import 'wagmi/window';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
export default function useProvider() { export default function useProvider() {
const [ provider, setProvider ] = React.useState<ExternalProvider>(); const [ provider, setProvider ] = React.useState<WindowProvider>();
React.useEffect(() => { React.useEffect(() => {
if (!('ethereum' in window)) { if (!('ethereum' in window)) {
......
...@@ -5,13 +5,11 @@ import metamaskIcon from 'icons/wallets/metamask.svg'; ...@@ -5,13 +5,11 @@ import metamaskIcon from 'icons/wallets/metamask.svg';
export const WALLETS_INFO: Record<WalletType, WalletInfo> = { export const WALLETS_INFO: Record<WalletType, WalletInfo> = {
metamask: { metamask: {
add_token_text: 'Add token to MetaMask', name: 'MetaMask',
add_network_text: 'Add network to MetaMask',
icon: metamaskIcon, icon: metamaskIcon,
}, },
coinbase: { coinbase: {
add_token_text: 'Add token to Coinbase Wallet', name: 'Coinbase Wallet',
add_network_text: 'Add network to Coinbase Wallet',
icon: coinbaseIcon, icon: coinbaseIcon,
}, },
}; };
import type { CustomLinksGroup } from 'types/footerLinks';
export const FOOTER_LINKS: Array<CustomLinksGroup> = [
{
title: 'Company',
links: [
{
text: 'Advertise',
url: 'https://coinzilla.com/',
},
{
text: 'Staking',
url: '',
},
{
text: 'Contact us',
url: '',
},
{
text: 'Brand assets',
url: '',
},
{
text: 'Term of service',
url: '',
},
],
},
{
title: 'Community',
links: [
{
text: 'API docs',
url: '',
},
{
text: 'Knowledge base',
url: '',
},
{
text: 'Network status',
url: '',
},
{
text: 'Learn Alphabet',
url: '',
},
],
},
{
title: 'Product',
links: [
{
text: 'Stake Alphabet',
url: '',
},
{
text: 'Build token',
url: '',
},
{
text: 'Build DAPPS',
url: '',
},
{
text: 'NFT marketplace',
url: '',
},
{
text: 'Become validator',
url: '',
},
],
},
];
...@@ -251,3 +251,26 @@ export const l2tx: Transaction = { ...@@ -251,3 +251,26 @@ export const l2tx: Transaction = {
l1_gas_used: '17060', l1_gas_used: '17060',
l1_fee: '1584574188135760', l1_fee: '1584574188135760',
}; };
export const base2 = {
...base,
hash: '0x02d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
from: {
...base.from,
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
},
};
export const base3 = {
...base,
hash: '0x12d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
from: {
...base.from,
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
},
};
export const base4 = {
...base,
hash: '0x22d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
};
...@@ -28,6 +28,7 @@ const moduleExports = withTM({ ...@@ -28,6 +28,7 @@ const moduleExports = withTM({
use: [ '@svgr/webpack' ], use: [ '@svgr/webpack' ],
}, },
); );
config.resolve.fallback = { fs: false, net: false, tls: false };
return config; return config;
}, },
......
...@@ -43,14 +43,13 @@ ...@@ -43,14 +43,13 @@
"@tanstack/react-query-devtools": "^4.0.10", "@tanstack/react-query-devtools": "^4.0.10",
"@types/papaparse": "^5.3.5", "@types/papaparse": "^5.3.5",
"@types/react-scroll": "^1.8.4", "@types/react-scroll": "^1.8.4",
"@web3modal/ethereum": "^2.0.0-rc.2", "@web3modal/ethereum": "^2.4.1",
"@web3modal/react": "^2.0.0-rc.2", "@web3modal/react": "^2.4.1",
"bignumber.js": "^9.1.0", "bignumber.js": "^9.1.0",
"chakra-react-select": "^4.4.3", "chakra-react-select": "^4.4.3",
"d3": "^7.6.1", "d3": "^7.6.1",
"dayjs": "^1.11.5", "dayjs": "^1.11.5",
"dom-to-image": "^2.6.0", "dom-to-image": "^2.6.0",
"ethers": "^5.7.2",
"framer-motion": "^6.5.1", "framer-motion": "^6.5.1",
"graphiql": "^2.2.0", "graphiql": "^2.2.0",
"graphql": "^16.6.0", "graphql": "^16.6.0",
...@@ -78,7 +77,8 @@ ...@@ -78,7 +77,8 @@
"react-scroll": "^1.8.7", "react-scroll": "^1.8.7",
"swagger-ui-react": "^4.15.5", "swagger-ui-react": "^4.15.5",
"use-font-face-observer": "^1.2.1", "use-font-face-observer": "^1.2.1",
"wagmi": "^0.10.6" "viem": "^0.3.39",
"wagmi": "^1.0.9"
}, },
"devDependencies": { "devDependencies": {
"@playwright/experimental-ct-react": "1.32.3", "@playwright/experimental-ct-react": "1.32.3",
...@@ -100,6 +100,7 @@ ...@@ -100,6 +100,7 @@
"@types/swagger-ui-react": "^4.11.0", "@types/swagger-ui-react": "^4.11.0",
"@types/ws": "^8.5.3", "@types/ws": "^8.5.3",
"@typescript-eslint/eslint-plugin": "^5.53.0", "@typescript-eslint/eslint-plugin": "^5.53.0",
"@vitejs/plugin-react": "^4.0.0",
"css-loader": "^6.7.3", "css-loader": "^6.7.3",
"dotenv-cli": "^6.0.0", "dotenv-cli": "^6.0.0",
"eslint": "^8.32.0", "eslint": "^8.32.0",
......
import { ChakraProvider } from '@chakra-ui/react'; import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { providers } from 'ethers'; import { w3mProvider } from '@web3modal/ethereum';
import React from 'react'; import React from 'react';
import { createClient, WagmiConfig } from 'wagmi'; import { configureChains, createConfig, WagmiConfig } from 'wagmi';
import { mainnet } from 'wagmi/chains'; import { mainnet } from 'wagmi/chains';
import { MockConnector } from 'wagmi/connectors/mock';
import { AppContextProvider } from 'lib/appContext'; import { AppContextProvider } from 'lib/appContext';
import type { Props as PageProps } from 'lib/next/getServerSideProps'; import type { Props as PageProps } from 'lib/next/getServerSideProps';
...@@ -28,25 +27,17 @@ const defaultAppContext = { ...@@ -28,25 +27,17 @@ const defaultAppContext = {
}; };
// >>> Web3 stuff // >>> Web3 stuff
const provider = new providers.JsonRpcProvider( const { publicClient } = configureChains(
'http://localhost:8545', [ mainnet ],
{ [
name: 'POA', w3mProvider({ projectId: '' }),
chainId: 99, ],
},
); );
const connector = new MockConnector({ const wagmiConfig = createConfig({
chains: [ mainnet ], autoConnect: false,
options: { connectors: [ ],
signer: provider.getSigner(), publicClient,
},
});
const wagmiClient = createClient({
autoConnect: true,
connectors: [ connector ],
provider,
}); });
// <<<< // <<<<
...@@ -65,7 +56,7 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props ...@@ -65,7 +56,7 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props
<QueryClientProvider client={ queryClient }> <QueryClientProvider client={ queryClient }>
<SocketProvider url={ withSocket ? `ws://localhost:${ PORT }` : undefined }> <SocketProvider url={ withSocket ? `ws://localhost:${ PORT }` : undefined }>
<AppContextProvider { ...appContext }> <AppContextProvider { ...appContext }>
<WagmiConfig client={ wagmiClient }> <WagmiConfig config={ wagmiConfig }>
{ children } { children }
</WagmiConfig> </WagmiConfig>
</AppContextProvider> </AppContextProvider>
......
...@@ -5,6 +5,8 @@ import { WebSocketServer } from 'ws'; ...@@ -5,6 +5,8 @@ import { WebSocketServer } from 'ws';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address'; import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block'; import type { NewBlockSocketResponse } from 'types/api/block';
import type { SmartContractVerificationResponse } from 'types/api/contract'; import type { SmartContractVerificationResponse } from 'types/api/contract';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction';
type ReturnType = () => Promise<WebSocket>; type ReturnType = () => Promise<WebSocket>;
...@@ -58,11 +60,14 @@ export const joinChannel = async(socket: WebSocket, channelName: string) => { ...@@ -58,11 +60,14 @@ export const joinChannel = async(socket: WebSocket, channelName: string) => {
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'coin_balance', payload: { coin_balance: AddressCoinBalanceHistoryItem }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'coin_balance', payload: { coin_balance: AddressCoinBalanceHistoryItem }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_balance', payload: { block_number: number }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_balance', payload: { block_number: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transactions: Array<Transaction> }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_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: 'pending_transaction', payload: { pending_transactions: Array<Transaction> }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'verification_result', payload: SmartContractVerificationResponse): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'verification_result', payload: SmartContractVerificationResponse): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'total_supply', payload: { total_supply: number}): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'total_supply', payload: { total_supply: number}): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'changed_bytecode', payload: Record<string, never>): void; export function sendMessage(socket: WebSocket, channel: Channel, msg: 'changed_bytecode', payload: Record<string, never>): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_transfer', payload: { token_transfers: Array<TokenTransfer> }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void { export function sendMessage(socket: WebSocket, channel: Channel, msg: string, payload: unknown): void {
socket.send(JSON.stringify([ socket.send(JSON.stringify([
...channel, ...channel,
......
...@@ -18,7 +18,7 @@ const variantPrimary = defineStyle((props) => { ...@@ -18,7 +18,7 @@ const variantPrimary = defineStyle((props) => {
const variantSecondary = defineStyle((props) => { const variantSecondary = defineStyle((props) => {
return { return {
color: mode('gray.500', 'gray.500')(props), color: mode('gray.600', 'gray.500')(props),
_hover: { _hover: {
color: mode('gray.600', 'gray.400')(props), color: mode('gray.600', 'gray.400')(props),
}, },
......
import type { Route } from 'nextjs-routes'; import type { Route } from 'nextjs-routes';
import type React from 'react';
type NavIconOrComponent = {
icon?: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
} | {
iconComponent?: React.FC<{size?: number}>;
};
type NavItemCommon = { type NavItemCommon = {
text: string; text: string;
icon?: React.FunctionComponent<React.SVGAttributes<SVGElement>>; } & NavIconOrComponent;
}
export type NavItemInternal = NavItemCommon & { export type NavItemInternal = NavItemCommon & {
nextRoute: Route; nextRoute: Route;
......
import type { providers } from 'ethers';
export type WalletType = 'metamask' | 'coinbase'; export type WalletType = 'metamask' | 'coinbase';
export interface WalletInfo { export interface WalletInfo {
add_token_text: string; name: string;
add_network_text: string;
icon: React.ElementType; icon: React.ElementType;
} }
export interface ExternalProvider extends providers.ExternalProvider {
isCoinbaseWallet?: boolean;
// have to patch ethers here, since params could be not only an array
// eslint-disable-next-line @typescript-eslint/no-explicit-any
request?: (request: { method: string; params?: any }) => Promise<any>;
}
type CustomLink = {
text: string;
url: string;
}
export type CustomLinksGroup = {
title: string;
links: Array<CustomLink>;
}
export type ArrayElement<ArrayType extends Array<unknown>> = export type ArrayElement<ArrType> = ArrType extends ReadonlyArray<infer ElementType>
ArrayType extends Array<(infer ElementType)> ? ElementType : never; ? ElementType
: never;
export type ExcludeNull<T> = T extends null ? never : T; export type ExcludeNull<T> = T extends null ? never : T;
......
import { test, expect } from '@playwright/experimental-ct-react'; import { test, expect } from '@playwright/experimental-ct-react';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { WindowProvider } from 'wagmi';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
...@@ -74,7 +75,7 @@ test('token', async({ mount, page }) => { ...@@ -74,7 +75,7 @@ test('token', async({ mount, page }) => {
await page.evaluate(() => { await page.evaluate(() => {
window.ethereum = { window.ethereum = {
providers: [ { isMetaMask: true } ], providers: [ { isMetaMask: true } ],
}; }as WindowProvider;
}); });
const component = await mount( const component = await mount(
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { test, expect, devices } from '@playwright/experimental-ct-react'; import { test as base, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { erc1155A } from 'mocks/tokens/tokenTransfer'; import * as tokenTransferMock from 'mocks/tokens/tokenTransfer';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTokenTransfers from './AddressTokenTransfers'; import AddressTokenTransfers from './AddressTokenTransfers';
const API_URL = buildApiUrl('address_token_transfers', { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }) + const CURRENT_ADDRESS = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859';
const API_URL = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) +
'?token=0x1189a607CEac2f0E14867de4EB15b15C9FFB5859'; '?token=0x1189a607CEac2f0E14867de4EB15b15C9FFB5859';
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', token: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' }, query: { hash: CURRENT_ADDRESS, token: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' },
}, },
}; };
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('with token filter and pagination', async({ mount, page }) => { test('with token filter and pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155A ], next_page_params: { block_number: 1 } }), body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
})); }));
const component = await mount( const component = await mount(
...@@ -37,7 +48,7 @@ test('with token filter and pagination', async({ mount, page }) => { ...@@ -37,7 +48,7 @@ test('with token filter and pagination', async({ mount, page }) => {
test('with token filter and no pagination', async({ mount, page }) => { test('with token filter and no pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155A ] }), body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ] }),
})); }));
const component = await mount( const component = await mount(
...@@ -57,7 +68,7 @@ test.describe('mobile', () => { ...@@ -57,7 +68,7 @@ test.describe('mobile', () => {
test('with token filter and pagination', async({ mount, page }) => { test('with token filter and pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155A ], next_page_params: { block_number: 1 } }), body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
})); }));
const component = await mount( const component = await mount(
...@@ -74,7 +85,7 @@ test.describe('mobile', () => { ...@@ -74,7 +85,7 @@ test.describe('mobile', () => {
test('with token filter and no pagination', async({ mount, page }) => { test('with token filter and no pagination', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ erc1155A ] }), body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ] }),
})); }));
const component = await mount( const component = await mount(
...@@ -88,3 +99,160 @@ test.describe('mobile', () => { ...@@ -88,3 +99,160 @@ test.describe('mobile', () => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
}); });
test.describe('socket', () => {
test('without overload', async({ mount, page, createSocket }) => {
const hooksConfigNoToken = {
router: {
query: { hash: CURRENT_ADDRESS },
},
};
const API_URL_NO_TOKEN = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=';
await page.route(API_URL_NO_TOKEN, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/>
</TestApp>,
{ hooksConfig: hooksConfigNoToken },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'token_transfer', { token_transfers: [ tokenTransferMock.erc1155B, tokenTransferMock.erc1155C ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(4);
});
test('with overload', async({ mount, page, createSocket }) => {
const hooksConfigNoToken = {
router: {
query: { hash: CURRENT_ADDRESS },
},
};
const API_URL_NO_TOKEN = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=';
await page.route(API_URL_NO_TOKEN, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers overloadCount={ 2 }/>
</TestApp>,
{ hooksConfig: hooksConfigNoToken },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'token_transfer', { token_transfers: [ tokenTransferMock.erc1155B, tokenTransferMock.erc1155C ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
const counter = await page.locator('tbody tr:nth-child(1)').textContent();
expect(counter?.startsWith('1 ')).toBe(true);
});
test('without overload, with filters', async({ mount, page, createSocket }) => {
const hooksConfigWithFilter = {
router: {
query: { hash: CURRENT_ADDRESS, type: 'ERC-1155' },
},
};
const API_URL_WITH_FILTER = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=ERC-1155';
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/>
</TestApp>,
{ hooksConfig: hooksConfigWithFilter },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'token_transfer', { token_transfers: [ tokenTransferMock.erc1155B, tokenTransferMock.erc20 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
});
test('with overload, with filters', async({ mount, page, createSocket }) => {
const hooksConfigWithFilter = {
router: {
query: { hash: CURRENT_ADDRESS, type: 'ERC-1155' },
},
};
const API_URL_WITH_FILTER = buildApiUrl('address_token_transfers', { hash: CURRENT_ADDRESS }) + '?type=ERC-1155';
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ tokenTransferMock.erc1155A ], next_page_params: { block_number: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers overloadCount={ 2 }/>
</TestApp>,
{ hooksConfig: hooksConfigWithFilter },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(
socket,
channel,
'token_transfer',
{ token_transfers: [ tokenTransferMock.erc1155B, tokenTransferMock.erc20, tokenTransferMock.erc1155C, tokenTransferMock.erc721 ] },
);
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
const counter = await page.locator('tbody tr:nth-child(1)').textContent();
expect(counter?.startsWith('1 ')).toBe(true);
});
});
...@@ -63,7 +63,13 @@ const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?: ...@@ -63,7 +63,13 @@ const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?:
return true; return true;
}; };
const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => { type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
// for tests only
overloadCount?: number;
}
const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => {
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -117,11 +123,26 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -117,11 +123,26 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => { const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => {
setSocketAlert(''); setSocketAlert('');
if (data?.items && data.items.length >= OVERLOAD_COUNT) { const newItems: Array<TokenTransfer> = [];
if (matchFilters(filters, payload.token_transfer, currentAddress)) { let newCount = 0;
setNewItemsCount(prev => prev + 1);
payload.token_transfers.forEach(transfer => {
if (data?.items && data.items.length + newItems.length >= overloadCount) {
if (matchFilters(filters, transfer, currentAddress)) {
newCount++;
} }
} else { } else {
if (matchFilters(filters, transfer, currentAddress)) {
newItems.push(transfer);
}
}
});
if (newCount > 0) {
setNewItemsCount(prev => prev + newCount);
}
if (newItems.length > 0) {
queryClient.setQueryData( queryClient.setQueryData(
getResourceKey('address_token_transfers', { pathParams: { hash: currentAddress }, queryParams: { ...filters } }), getResourceKey('address_token_transfers', { pathParams: { hash: currentAddress }, queryParams: { ...filters } }),
(prevData: AddressTokenTransferResponse | undefined) => { (prevData: AddressTokenTransferResponse | undefined) => {
...@@ -129,19 +150,17 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD ...@@ -129,19 +150,17 @@ const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLD
return; return;
} }
if (!matchFilters(filters, payload.token_transfer, currentAddress)) {
return prevData;
}
return { return {
...prevData, ...prevData,
items: [ items: [
payload.token_transfer, ...newItems,
...prevData.items, ...prevData.items,
], ],
}; };
}); },
);
} }
}; };
const handleSocketClose = React.useCallback(() => { const handleSocketClose = React.useCallback(() => {
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react'; import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import { base as txMock } from 'mocks/txs/tx'; import * as txMock from 'mocks/txs/tx';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTxs from './AddressTxs'; import AddressTxs from './AddressTxs';
const API_URL = buildApiUrl('address_txs', { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }); const CURRENT_ADDRESS = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859';
const API_URL = buildApiUrl('address_txs', { hash: CURRENT_ADDRESS });
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }, query: { hash: CURRENT_ADDRESS },
}, },
}; };
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('address txs +@mobile +@desktop-xl', async({ mount, page }) => { test('address txs +@mobile +@desktop-xl', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify({ items: [ txMock, txMock ], next_page_params: { block: 1 } }), body: JSON.stringify({ items: [ txMock.base, txMock.base ], next_page_params: { block: 1 } }),
})); }));
const component = await mount( const component = await mount(
...@@ -32,3 +43,167 @@ test('address txs +@mobile +@desktop-xl', async({ mount, page }) => { ...@@ -32,3 +43,167 @@ test('address txs +@mobile +@desktop-xl', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test.describe('socket', () => {
test('without overload', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base4 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(4);
});
test('with update', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.pending ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base, txMock.base2 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
});
test('with overload', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs overloadCount={ 2 }/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base3, txMock.base4 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
const counter = await page.locator('tbody tr:nth-child(1)').textContent();
expect(counter?.startsWith('2 ')).toBe(true);
});
test('without overload, with filters', async({ mount, page, createSocket }) => {
const hooksConfigWithFilter = {
router: {
query: { hash: CURRENT_ADDRESS, filter: 'from' },
},
};
const API_URL_WITH_FILTER = buildApiUrl('address_txs', { hash: CURRENT_ADDRESS }) + '?filter=from';
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs/>
</TestApp>,
{ hooksConfig: hooksConfigWithFilter },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base4 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
});
test('with overload, with filters', async({ mount, page, createSocket }) => {
const hooksConfigWithFilter = {
router: {
query: { hash: CURRENT_ADDRESS, filter: 'from' },
},
};
const API_URL_WITH_FILTER = buildApiUrl('address_txs', { hash: CURRENT_ADDRESS }) + '?filter=from';
await page.route(API_URL_WITH_FILTER, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock.base ], next_page_params: { block: 1 } }),
}));
await mount(
<TestApp withSocket>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs overloadCount={ 2 }/>
</TestApp>,
{ hooksConfig: hooksConfigWithFilter },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base3, txMock.base4 ] });
await page.waitForSelector('tbody tr:nth-child(3)');
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
const counter = await page.locator('tbody tr:nth-child(1)').textContent();
expect(counter?.startsWith('1 ')).toBe(true);
});
});
...@@ -5,6 +5,7 @@ import React from 'react'; ...@@ -5,6 +5,7 @@ import React from 'react';
import type { SocketMessage } from 'lib/socket/types'; import type { SocketMessage } from 'lib/socket/types';
import type { AddressFromToFilter, AddressTransactionsResponse } from 'types/api/address'; import type { AddressFromToFilter, AddressTransactionsResponse } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address'; import { AddressFromToFilterValues } from 'types/api/address';
import type { Transaction } from 'types/api/transaction';
import { getResourceKey } from 'lib/api/useApiQuery'; import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
...@@ -26,7 +27,27 @@ const OVERLOAD_COUNT = 75; ...@@ -26,7 +27,27 @@ const OVERLOAD_COUNT = 75;
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues); const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => { const matchFilter = (filterValue: AddressFromToFilter, transaction: Transaction, address?: string) => {
if (!filterValue) {
return true;
}
if (filterValue === 'from') {
return transaction.from.hash === address;
}
if (filterValue === 'to') {
return transaction.to?.hash === address;
}
};
type Props = {
scrollRef?: React.RefObject<HTMLDivElement>;
// for tests only
overloadCount?: number;
}
const AddressTxs = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Props) => {
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
...@@ -62,16 +83,6 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -62,16 +83,6 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = (payload) => { const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = (payload) => {
setSocketAlert(''); setSocketAlert('');
if (addressTxsQuery.data?.items && addressTxsQuery.data.items.length >= OVERLOAD_COUNT) {
if (
!filterValue ||
(filterValue === 'from' && payload.transaction.from.hash === currentAddress) ||
(filterValue === 'to' && payload.transaction.to?.hash === currentAddress)
) {
setNewItemsCount(prev => prev + 1);
}
}
queryClient.setQueryData( queryClient.setQueryData(
getResourceKey('address_txs', { pathParams: { hash: currentAddress }, queryParams: { filter: filterValue } }), getResourceKey('address_txs', { pathParams: { hash: currentAddress }, queryParams: { filter: filterValue } }),
(prevData: AddressTransactionsResponse | undefined) => { (prevData: AddressTransactionsResponse | undefined) => {
...@@ -79,30 +90,33 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>} ...@@ -79,30 +90,33 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
return; return;
} }
const currIndex = prevData.items.findIndex((tx) => tx.hash === payload.transaction.hash); const newItems: Array<Transaction> = [];
let newCount = 0;
payload.transactions.forEach(tx => {
const currIndex = prevData.items.findIndex((item) => item.hash === tx.hash);
if (currIndex > -1) { if (currIndex > -1) {
prevData.items[currIndex] = payload.transaction; prevData.items[currIndex] = tx;
return prevData; } else {
if (matchFilter(filterValue, tx, currentAddress)) {
if (newItems.length + prevData.items.length >= overloadCount) {
newCount++;
} else {
newItems.push(tx);
} }
if (prevData.items.length >= OVERLOAD_COUNT) {
return prevData;
} }
if (filterValue) {
if (
(filterValue === 'from' && payload.transaction.from.hash !== currentAddress) ||
(filterValue === 'to' && payload.transaction.to?.hash !== currentAddress)
) {
return prevData;
} }
});
if (newCount > 0) {
setNewItemsCount(prev => prev + newCount);
} }
return { return {
...prevData, ...prevData,
items: [ items: [
payload.transaction, ...newItems,
...prevData.items, ...prevData.items,
], ],
}; };
......
...@@ -7,7 +7,6 @@ import { useForm } from 'react-hook-form'; ...@@ -7,7 +7,6 @@ import { useForm } from 'react-hook-form';
import type { MethodFormFields, ContractMethodCallResult } from './types'; import type { MethodFormFields, ContractMethodCallResult } from './types';
import type { SmartContractMethodInput, SmartContractMethod } from 'types/api/contract'; import type { SmartContractMethodInput, SmartContractMethod } from 'types/api/contract';
import appConfig from 'configs/app/config';
import arrowIcon from 'icons/arrows/down-right.svg'; import arrowIcon from 'icons/arrows/down-right.svg';
import ContractMethodField from './ContractMethodField'; import ContractMethodField from './ContractMethodField';
...@@ -25,7 +24,7 @@ interface Props<T extends SmartContractMethod> { ...@@ -25,7 +24,7 @@ interface Props<T extends SmartContractMethod> {
isWrite?: boolean; isWrite?: boolean;
} }
const getFieldName = (name: string, index: number): string => name || String(index); const getFieldName = (name: string | undefined, index: number): string => name || String(index);
const sortFields = (data: Array<SmartContractMethodInput>) => ([ a ]: [string, string], [ b ]: [string, string]): 1 | -1 | 0 => { const sortFields = (data: Array<SmartContractMethodInput>) => ([ a ]: [string, string], [ b ]: [string, string]): 1 | -1 | 0 => {
const fieldNames = data.map(({ name }, index) => getFieldName(name, index)); const fieldNames = data.map(({ name }, index) => getFieldName(name, index));
...@@ -67,14 +66,14 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit, ...@@ -67,14 +66,14 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
const [ result, setResult ] = React.useState<ContractMethodCallResult<T>>(); const [ result, setResult ] = React.useState<ContractMethodCallResult<T>>();
const [ isLoading, setLoading ] = React.useState(false); const [ isLoading, setLoading ] = React.useState(false);
const inputs = React.useMemo(() => { const inputs: Array<SmartContractMethodInput> = React.useMemo(() => {
return [ return [
...('inputs' in data ? data.inputs : []), ...('inputs' in data ? data.inputs : []),
...(data.stateMutability === 'payable' ? [ { ...('stateMutability' in data && data.stateMutability === 'payable' ? [ {
name: 'value', name: 'value',
type: appConfig.network.currency.symbol, type: 'uint256' as const,
internalType: appConfig.network.currency.symbol, internalType: 'uint256' as const,
} as SmartContractMethodInput ] : []), } ] : []),
]; ];
}, [ data ]); }, [ data ]);
......
import React from 'react'; import React from 'react';
import { useAccount, useSigner, useNetwork, useSwitchNetwork } from 'wagmi'; import { useAccount, useWalletClient, useNetwork, useSwitchNetwork } from 'wagmi';
import type { SmartContractWriteMethod } from 'types/api/contract'; import type { SmartContractWriteMethod } from 'types/api/contract';
...@@ -24,7 +24,7 @@ interface Props { ...@@ -24,7 +24,7 @@ interface Props {
} }
const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => { const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
const { data: signer } = useSigner(); const { data: walletClient } = useWalletClient();
const { isConnected } = useAccount(); const { isConnected } = useAccount();
const { chain } = useNetwork(); const { chain } = useNetwork();
const { switchNetworkAsync } = useSwitchNetwork(); const { switchNetworkAsync } = useSwitchNetwork();
...@@ -39,17 +39,17 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => { ...@@ -39,17 +39,17 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
}, },
}); });
const { contract, proxy, custom } = useContractContext(); const { contractInfo, customInfo, proxyInfo } = useContractContext();
const _contract = (() => { const abi = (() => {
if (isProxy) { if (isProxy) {
return proxy; return proxyInfo?.abi;
} }
if (isCustomAbi) { if (isCustomAbi) {
return custom; return customInfo?.abi;
} }
return contract; return contractInfo?.abi;
})(); })();
const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array<string | Array<unknown>>) => { const handleMethodFormSubmit = React.useCallback(async(item: SmartContractWriteMethod, args: Array<string | Array<unknown>>) => {
...@@ -61,30 +61,37 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => { ...@@ -61,30 +61,37 @@ const ContractWrite = ({ addressHash, isProxy, isCustomAbi }: Props) => {
await switchNetworkAsync?.(Number(config.network.id)); await switchNetworkAsync?.(Number(config.network.id));
} }
if (!_contract) { if (!abi) {
throw new Error('Something went wrong. Try again later.'); throw new Error('Something went wrong. Try again later.');
} }
if (item.type === 'receive') { if (item.type === 'receive' || item.type === 'fallback') {
const value = args[0] ? getNativeCoinValue(args[0]) : '0'; const value = getNativeCoinValue(args[0]);
const result = await signer?.sendTransaction({ const hash = await walletClient?.sendTransaction({
to: addressHash, to: addressHash as `0x${ string }` | undefined,
value, value,
}); });
return { hash: result?.hash as string }; return { hash };
} }
const _args = item.stateMutability === 'payable' ? args.slice(0, -1) : args; const _args = 'stateMutability' in item && item.stateMutability === 'payable' ? args.slice(0, -1) : args;
const value = item.stateMutability === 'payable' ? getNativeCoinValue(args[args.length - 1]) : undefined; const value = 'stateMutability' in item && item.stateMutability === 'payable' ? getNativeCoinValue(args[args.length - 1]) : undefined;
const methodName = item.type === 'fallback' ? 'fallback' : item.name; const methodName = item.name;
const result = await _contract[methodName](..._args, { if (!methodName) {
gasLimit: 100_000, throw new Error('Method name is not defined');
value, }
const hash = await walletClient?.writeContract({
args: _args,
abi: abi,
functionName: methodName,
address: addressHash as `0x${ string }`,
value: value as undefined,
}); });
return { hash: result.hash as string }; return { hash };
}, [ _contract, addressHash, chain, isConnected, signer, switchNetworkAsync ]); }, [ isConnected, chain, abi, walletClient, addressHash, switchNetworkAsync ]);
const renderContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => { const renderContent = React.useCallback((item: SmartContractWriteMethod, index: number, id: number) => {
return ( return (
......
...@@ -12,7 +12,7 @@ test('loading', async({ mount }) => { ...@@ -12,7 +12,7 @@ test('loading', async({ mount }) => {
error: null, error: null,
}, },
result: { result: {
hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29', hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`,
}, },
onSettle: () => {}, onSettle: () => {},
}; };
...@@ -33,7 +33,7 @@ test('success', async({ mount }) => { ...@@ -33,7 +33,7 @@ test('success', async({ mount }) => {
error: null, error: null,
}, },
result: { result: {
hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29', hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`,
}, },
onSettle: () => {}, onSettle: () => {},
}; };
...@@ -57,7 +57,7 @@ test('error +@mobile', async({ mount }) => { ...@@ -57,7 +57,7 @@ test('error +@mobile', async({ mount }) => {
} as Error, } as Error,
}, },
result: { result: {
hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29', hash: '0x363574E6C5C71c343d7348093D84320c76d5Dd29' as `0x${ string }`,
}, },
onSettle: () => {}, onSettle: () => {},
}; };
......
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import type { Contract } from 'ethers';
import React from 'react'; import React from 'react';
import { useContract, useProvider, useSigner } from 'wagmi';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
import type { SmartContract } from 'types/api/contract';
import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
...@@ -13,20 +12,18 @@ type ProviderProps = { ...@@ -13,20 +12,18 @@ type ProviderProps = {
} }
type TContractContext = { type TContractContext = {
contract: Contract | null; contractInfo: SmartContract | undefined;
proxy: Contract | null; proxyInfo: SmartContract | undefined;
custom: Contract | null; customInfo: SmartContract | undefined;
}; };
const ContractContext = React.createContext<TContractContext>({ const ContractContext = React.createContext<TContractContext>({
contract: null, proxyInfo: undefined,
proxy: null, contractInfo: undefined,
custom: null, customInfo: undefined,
}); });
export function ContractContextProvider({ addressHash, children }: ProviderProps) { export function ContractContextProvider({ addressHash, children }: ProviderProps) {
const provider = useProvider();
const { data: signer } = useSigner();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: contractInfo } = useApiQuery('contract', { const { data: contractInfo } = useApiQuery('contract', {
...@@ -58,27 +55,11 @@ export function ContractContextProvider({ addressHash, children }: ProviderProps ...@@ -58,27 +55,11 @@ export function ContractContextProvider({ addressHash, children }: ProviderProps
}, },
}); });
const contract = useContract({
address: addressHash,
abi: contractInfo?.abi ?? undefined,
signerOrProvider: signer ?? provider,
});
const proxy = useContract({
address: addressInfo?.implementation_address ?? undefined,
abi: proxyInfo?.abi ?? undefined,
signerOrProvider: signer ?? provider,
});
const custom = useContract({
address: addressHash,
abi: customInfo ?? undefined,
signerOrProvider: signer ?? provider,
});
const value = React.useMemo(() => ({ const value = React.useMemo(() => ({
contract, proxyInfo,
proxy, contractInfo,
custom, customInfo,
}), [ contract, proxy, custom ]); } as TContractContext), [ proxyInfo, contractInfo, customInfo ]);
return ( return (
<ContractContext.Provider value={ value }> <ContractContext.Provider value={ value }>
......
...@@ -6,7 +6,7 @@ export type MethodFormFields = Record<string, string>; ...@@ -6,7 +6,7 @@ export type MethodFormFields = Record<string, string>;
export type ContractMethodReadResult = SmartContractQueryMethodRead | ResourceError; export type ContractMethodReadResult = SmartContractQueryMethodRead | ResourceError;
export type ContractMethodWriteResult = Error | { hash: string } | undefined; export type ContractMethodWriteResult = Error | { hash: `0x${ string }` | undefined } | undefined;
export type ContractMethodCallResult<T extends SmartContractMethod> = export type ContractMethodCallResult<T extends SmartContractMethod> =
T extends { method_id: string } ? ContractMethodReadResult : ContractMethodWriteResult; T extends { method_id: string } ? ContractMethodReadResult : ContractMethodWriteResult;
import BigNumber from 'bignumber.js';
import config from 'configs/app/config'; import config from 'configs/app/config';
export const getNativeCoinValue = (value: string | Array<unknown>) => { export const getNativeCoinValue = (value: string | Array<unknown>) => {
const _value = Array.isArray(value) ? value[0] : value; const _value = Array.isArray(value) ? value[0] : value;
if (typeof _value !== 'string') { if (typeof _value !== 'string') {
return '0'; return BigInt(0);
} }
return BigNumber(_value).times(10 ** config.network.currency.decimals).toString(); return BigInt(Number(_value) * 10 ** config.network.currency.decimals);
}; };
export const addZeroesAllowed = (valueType: string) => { export const addZeroesAllowed = (valueType: string) => {
......
...@@ -187,7 +187,7 @@ const BlockDetails = ({ query }: Props) => { ...@@ -187,7 +187,7 @@ const BlockDetails = ({ query }: Props) => {
isLoading={ isPlaceholderData } isLoading={ isPlaceholderData }
> >
<Skeleton isLoaded={ !isPlaceholderData }> <Skeleton isLoaded={ !isPlaceholderData }>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: heightOrHash, tab: 'withdrawals' } }) }> <LinkInternal href={ route({ pathname: '/block/[height_or_hash]', query: { height_or_hash: heightOrHash, tab: 'withdrawals' } }) }>
{ data.withdrawals_count } withdrawal{ data.withdrawals_count === 1 ? '' : 's' } { data.withdrawals_count } withdrawal{ data.withdrawals_count === 1 ? '' : 's' }
</LinkInternal> </LinkInternal>
</Skeleton> </Skeleton>
......
import { Skeleton, Box, Icon } from '@chakra-ui/react'; import { Box, Icon } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -124,7 +124,7 @@ const AddressPageContent = () => { ...@@ -124,7 +124,7 @@ const AddressPageContent = () => {
return ( return (
<> <>
{ addressQuery.isPlaceholderData ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> } <TextAd mb={ 6 }/>
<PageTitle <PageTitle
title={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` } title={ `${ addressQuery.data?.is_contract ? 'Contract' : 'Address' } details` }
backLink={ backLink } backLink={ backLink }
...@@ -135,7 +135,6 @@ const AddressPageContent = () => { ...@@ -135,7 +135,6 @@ const AddressPageContent = () => {
{ /* should stay before tabs to scroll up with pagination */ } { /* should stay before tabs to scroll up with pagination */ }
<Box ref={ tabsScrollRef }></Box> <Box ref={ tabsScrollRef }></Box>
{ addressQuery.isPlaceholderData ? <SkeletonTabs tabs={ tabs }/> : content } { addressQuery.isPlaceholderData ? <SkeletonTabs tabs={ tabs }/> : content }
{ !addressQuery.isPlaceholderData && !addressQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</> </>
); );
}; };
......
...@@ -8,7 +8,6 @@ import Stats from 'ui/home/Stats'; ...@@ -8,7 +8,6 @@ import Stats from 'ui/home/Stats';
import Transactions from 'ui/home/Transactions'; import Transactions from 'ui/home/Transactions';
import AdBanner from 'ui/shared/ad/AdBanner'; import AdBanner from 'ui/shared/ad/AdBanner';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import ColorModeToggler from 'ui/snippets/header/ColorModeToggler';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop'; import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import SearchBar from 'ui/snippets/searchBar/SearchBar'; import SearchBar from 'ui/snippets/searchBar/SearchBar';
...@@ -33,15 +32,9 @@ const Home = () => { ...@@ -33,15 +32,9 @@ const Home = () => {
> >
Welcome to { appConfig.network.name } explorer Welcome to { appConfig.network.name } explorer
</Heading> </Heading>
<Flex <Box display={{ base: 'none', lg: 'block' }}>
alignItems="center"
display={{ base: 'none', lg: 'flex' }}
columnGap={ 12 }
pl={ 4 }
>
<ColorModeToggler trackBg="blackAlpha.900"/>
{ appConfig.isAccountSupported && <ProfileMenuDesktop/> } { appConfig.isAccountSupported && <ProfileMenuDesktop/> }
</Flex> </Box>
</Flex> </Flex>
<LightMode> <LightMode>
<SearchBar isHomepage/> <SearchBar isHomepage/>
......
...@@ -9,7 +9,7 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -9,7 +9,7 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import UserAvatar from 'ui/shared/UserAvatar'; import UserAvatar from 'ui/shared/UserAvatar';
const MyProfile = () => { const MyProfile = () => {
const { data, isLoading, isError, error, isFetched } = useFetchProfileInfo(); const { data, isLoading, isError, error } = useFetchProfileInfo();
useRedirectForInvalidAuthToken(); useRedirectForInvalidAuthToken();
const content = (() => { const content = (() => {
...@@ -26,7 +26,7 @@ const MyProfile = () => { ...@@ -26,7 +26,7 @@ const MyProfile = () => {
return ( return (
<VStack maxW="412px" mt={ 8 } gap={ 5 } alignItems="stretch"> <VStack maxW="412px" mt={ 8 } gap={ 5 } alignItems="stretch">
<UserAvatar size={ 64 } data={ data } isFetched={ isFetched }/> <UserAvatar size={ 64 }/>
<FormControl variant="floating" id="name" isRequired size="lg"> <FormControl variant="floating" id="name" isRequired size="lg">
<Input <Input
required required
......
...@@ -276,8 +276,6 @@ const TokenPageContent = () => { ...@@ -276,8 +276,6 @@ const TokenPageContent = () => {
stickyEnabled={ !isMobile } stickyEnabled={ !isMobile }
/> />
) } ) }
{ !tokenQuery.isLoading && !tokenQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</> </>
); );
}; };
......
...@@ -183,8 +183,6 @@ const TokenInstanceContent = () => { ...@@ -183,8 +183,6 @@ const TokenInstanceContent = () => {
stickyEnabled={ !isMobile } stickyEnabled={ !isMobile }
/> />
) } ) }
{ !tokenInstanceQuery.isLoading && !tokenInstanceQuery.isError && <Box h={{ base: 0, lg: '40vh' }}/> }
</> </>
); );
}; };
......
import { Box, Icon, Tooltip, chakra } from '@chakra-ui/react'; import { Button, Icon, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
...@@ -30,7 +30,9 @@ const NetworkAddToWallet = ({ className }: Props) => { ...@@ -30,7 +30,9 @@ const NetworkAddToWallet = ({ className }: Props) => {
rpcUrls: [ appConfig.network.rpcUrl ], rpcUrls: [ appConfig.network.rpcUrl ],
blockExplorerUrls: [ appConfig.baseUrl ], blockExplorerUrls: [ appConfig.baseUrl ],
} ], } ],
}; // in wagmi types for wallet_addEthereumChain method is not provided
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;
await provider?.request?.(config); await provider?.request?.(config);
toast({ toast({
position: 'top-right', position: 'top-right',
...@@ -59,11 +61,10 @@ const NetworkAddToWallet = ({ className }: Props) => { ...@@ -59,11 +61,10 @@ const NetworkAddToWallet = ({ className }: Props) => {
const defaultWallet = appConfig.web3.defaultWallet; const defaultWallet = appConfig.web3.defaultWallet;
return ( return (
<Tooltip label={ WALLETS_INFO[defaultWallet].add_network_text }> <Button variant="outline" size="sm" onClick={ handleClick } className={ className }>
<Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick }> <Icon as={ WALLETS_INFO[defaultWallet].icon } boxSize={ 5 } mr={ 2 }/>
<Icon as={ WALLETS_INFO[defaultWallet].icon } boxSize={ 5 }/> Add { appConfig.network.name }
</Box> </Button>
</Tooltip>
); );
}; };
......
import { Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode'; import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode';
...@@ -12,6 +12,7 @@ import AppErrorInvalidTxHash from 'ui/shared/AppError/AppErrorInvalidTxHash'; ...@@ -12,6 +12,7 @@ import AppErrorInvalidTxHash from 'ui/shared/AppError/AppErrorInvalidTxHash';
import AppErrorUnverifiedEmail from 'ui/shared/AppError/AppErrorUnverifiedEmail'; import AppErrorUnverifiedEmail from 'ui/shared/AppError/AppErrorUnverifiedEmail';
import ErrorBoundary from 'ui/shared/ErrorBoundary'; import ErrorBoundary from 'ui/shared/ErrorBoundary';
import PageContent from 'ui/shared/Page/PageContent'; import PageContent from 'ui/shared/Page/PageContent';
import Footer from 'ui/snippets/footer/Footer';
import Header from 'ui/snippets/header/Header'; import Header from 'ui/snippets/header/Header';
import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop'; import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop';
...@@ -73,6 +74,7 @@ const Page = ({ ...@@ -73,6 +74,7 @@ const Page = ({
) : children; ) : children;
return ( return (
<Box minWidth={{ base: '100vw', lg: 'fit-content' }}>
<Flex w="100%" minH="100vh" alignItems="stretch"> <Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/> <NavigationDesktop/>
<Flex flexDir="column" flexGrow={ 1 } w={{ base: '100%', lg: 'auto' }}> <Flex flexDir="column" flexGrow={ 1 } w={{ base: '100%', lg: 'auto' }}>
...@@ -85,6 +87,8 @@ const Page = ({ ...@@ -85,6 +87,8 @@ const Page = ({
</ErrorBoundary> </ErrorBoundary>
</Flex> </Flex>
</Flex> </Flex>
<Footer/>
</Box>
); );
}; };
......
...@@ -2,10 +2,9 @@ import { useColorModeValue, useToken, SkeletonCircle, Image, Box } from '@chakra ...@@ -2,10 +2,9 @@ import { useColorModeValue, useToken, SkeletonCircle, Image, Box } from '@chakra
import React from 'react'; import React from 'react';
import Identicon from 'react-identicons'; import Identicon from 'react-identicons';
import type { UserInfo } from 'types/api/account';
import { useAppContext } from 'lib/appContext'; import { useAppContext } from 'lib/appContext';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
const IdenticonComponent = typeof Identicon === 'object' && 'default' in Identicon ? Identicon.default : Identicon; const IdenticonComponent = typeof Identicon === 'object' && 'default' in Identicon ? Identicon.default : Identicon;
...@@ -34,14 +33,13 @@ const FallbackImage = ({ size, id }: { size: number; id: string }) => { ...@@ -34,14 +33,13 @@ const FallbackImage = ({ size, id }: { size: number; id: string }) => {
interface Props { interface Props {
size: number; size: number;
data?: UserInfo;
isFetched: boolean;
} }
const UserAvatar = ({ size, data, isFetched }: Props) => { const UserAvatar = ({ size }: Props) => {
const appProps = useAppContext(); const appProps = useAppContext();
const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, appProps.cookies)); const hasAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN, appProps.cookies));
const [ isImageLoadError, setImageLoadError ] = React.useState(false); const [ isImageLoadError, setImageLoadError ] = React.useState(false);
const { data, isFetched } = useFetchProfileInfo();
const sizeString = `${ size }px`; const sizeString = `${ size }px`;
......
import { useColorModeValue, useToken } from '@chakra-ui/react'; import { useColorModeValue, useToken } from '@chakra-ui/react';
import { import { EthereumClient, w3mConnectors, w3mProvider } from '@web3modal/ethereum';
EthereumClient,
modalConnectors,
walletConnectProvider,
} from '@web3modal/ethereum';
import { Web3Modal } from '@web3modal/react'; import { Web3Modal } from '@web3modal/react';
import React from 'react'; import React from 'react';
import type { Chain } from 'wagmi'; import type { Chain } from 'wagmi';
import { configureChains, createClient, WagmiConfig } from 'wagmi'; import { configureChains, createConfig, WagmiConfig } from 'wagmi';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
const { wagmiClient, ethereumClient } = (() => { const getConfig = () => {
try { try {
if (!appConfig.walletConnect.projectId) {
throw new Error('WalletConnect Project ID is not set');
}
const currentChain: Chain = { const currentChain: Chain = {
id: Number(appConfig.network.id), id: Number(appConfig.network.id),
name: appConfig.network.name || '', name: appConfig.network.name || '',
...@@ -23,6 +23,9 @@ const { wagmiClient, ethereumClient } = (() => { ...@@ -23,6 +23,9 @@ const { wagmiClient, ethereumClient } = (() => {
symbol: appConfig.network.currency.symbol || '', symbol: appConfig.network.currency.symbol || '',
}, },
rpcUrls: { rpcUrls: {
'public': {
http: [ appConfig.network.rpcUrl || '' ],
},
'default': { 'default': {
http: [ appConfig.network.rpcUrl || '' ], http: [ appConfig.network.rpcUrl || '' ],
}, },
...@@ -37,21 +40,23 @@ const { wagmiClient, ethereumClient } = (() => { ...@@ -37,21 +40,23 @@ const { wagmiClient, ethereumClient } = (() => {
const chains = [ currentChain ]; const chains = [ currentChain ];
const { provider } = configureChains(chains, [ const { publicClient } = configureChains(chains, [
walletConnectProvider({ projectId: appConfig.walletConnect.projectId || '' }), w3mProvider({ projectId: appConfig.walletConnect.projectId || '' }),
]); ]);
const wagmiClient = createClient({ const wagmiConfig = createConfig({
autoConnect: true, autoConnect: true,
connectors: modalConnectors({ appName: 'web3Modal', chains }), connectors: w3mConnectors({ projectId: appConfig.walletConnect.projectId, chains, version: 2 }),
provider, publicClient,
}); });
const ethereumClient = new EthereumClient(wagmiClient, chains); const ethereumClient = new EthereumClient(wagmiConfig, chains);
return { wagmiClient, ethereumClient }; return { wagmiConfig, ethereumClient };
} catch (error) { } catch (error) {
return { wagmiClient: undefined, ethereumClient: undefined }; return { wagmiConfig: undefined, ethereumClient: undefined };
} }
})(); };
const { wagmiConfig, ethereumClient } = getConfig();
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
...@@ -62,21 +67,24 @@ const Web3ModalProvider = ({ children, fallback }: Props) => { ...@@ -62,21 +67,24 @@ const Web3ModalProvider = ({ children, fallback }: Props) => {
const modalZIndex = useToken<string>('zIndices', 'modal'); const modalZIndex = useToken<string>('zIndices', 'modal');
const web3ModalTheme = useColorModeValue('light', 'dark'); const web3ModalTheme = useColorModeValue('light', 'dark');
if (!wagmiClient || !ethereumClient) { if (!wagmiConfig || !ethereumClient || !appConfig.walletConnect.projectId) {
return typeof fallback === 'function' ? fallback() : (fallback || null); return typeof fallback === 'function' ? fallback() : (fallback || null);
} }
return ( return (
<WagmiConfig client={ wagmiClient }> <>
<WagmiConfig config={ wagmiConfig }>
{ children } { children }
</WagmiConfig>
<Web3Modal <Web3Modal
projectId={ appConfig.walletConnect.projectId } projectId={ appConfig.walletConnect.projectId }
ethereumClient={ ethereumClient } ethereumClient={ ethereumClient }
themeZIndex={ Number(modalZIndex) }
themeMode={ web3ModalTheme } themeMode={ web3ModalTheme }
themeBackground="themeColor" themeVariables={{
'--w3m-z-index': modalZIndex,
}}
/> />
</WagmiConfig> </>
); );
}; };
......
...@@ -26,7 +26,7 @@ const AddressAddToWallet = ({ className, token, isLoading }: Props) => { ...@@ -26,7 +26,7 @@ const AddressAddToWallet = ({ className, token, isLoading }: Props) => {
type: 'ERC20', // Initially only supports ERC20, but eventually more! type: 'ERC20', // Initially only supports ERC20, but eventually more!
options: { options: {
address: token.address, address: token.address,
symbol: token.symbol, symbol: token.symbol || '',
decimals: Number(token.decimals) || 18, decimals: Number(token.decimals) || 18,
// TODO: add token image when we have it in API // TODO: add token image when we have it in API
// image: '' // image: ''
...@@ -67,7 +67,7 @@ const AddressAddToWallet = ({ className, token, isLoading }: Props) => { ...@@ -67,7 +67,7 @@ const AddressAddToWallet = ({ className, token, isLoading }: Props) => {
const defaultWallet = appConfig.web3.defaultWallet; const defaultWallet = appConfig.web3.defaultWallet;
return ( return (
<Tooltip label={ WALLETS_INFO[defaultWallet].add_token_text }> <Tooltip label={ `Add token to ${ WALLETS_INFO[defaultWallet].name }` }>
<Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick }> <Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick }>
<Icon as={ WALLETS_INFO[defaultWallet].icon } boxSize={ 6 }/> <Icon as={ WALLETS_INFO[defaultWallet].icon } boxSize={ 6 }/>
</Box> </Box>
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import type { WindowProvider } from 'wagmi';
import { FOOTER_LINKS } from 'mocks/config/footerLinks';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
import Footer from './Footer';
const FOOTER_LINKS_URL = 'https://localhost:3000/footer-links.json';
base.describe('with custom links, 4 cols', () => {
const test = base.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_FOOTER_LINKS', value: FOOTER_LINKS_URL },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any,
});
test('base view +@dark-mode +@mobile +@desktop-xl', async({ mount, page }) => {
await page.route(FOOTER_LINKS_URL, (route) => {
return route.fulfill({
body: JSON.stringify(FOOTER_LINKS),
});
});
await mount(
<TestApp>
<Footer/>
</TestApp>,
);
await expect(page).toHaveScreenshot();
});
});
base.describe('with custom links, 2 cols', () => {
const test = base.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_FOOTER_LINKS', value: FOOTER_LINKS_URL },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any,
});
test('base view +@dark-mode +@mobile', async({ mount, page }) => {
await page.route(FOOTER_LINKS_URL, (route) => {
return route.fulfill({
body: JSON.stringify([ FOOTER_LINKS[0] ]),
});
});
await mount(
<TestApp>
<Footer/>
</TestApp>,
);
await expect(page).toHaveScreenshot();
});
});
base.describe('without custom links', () => {
base('base view +@dark-mode +@mobile', async({ mount, page }) => {
await page.evaluate(() => {
window.ethereum = {
providers: [ { isMetaMask: true } ],
} as WindowProvider;
});
await mount(
<TestApp>
<Footer/>
</TestApp>,
);
await expect(page).toHaveScreenshot();
});
});
import { Box, Grid, Flex, Text, Link, VStack, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { CustomLinksGroup } from 'types/footerLinks';
import appConfig from 'configs/app/config';
import discussionsIcon from 'icons/discussions.svg';
import editIcon from 'icons/edit.svg';
import discordIcon from 'icons/social/discord.svg';
import gitIcon from 'icons/social/git.svg';
import twitterIcon from 'icons/social/tweet.svg';
import type { ResourceError } from 'lib/api/resources';
import useFetch from 'lib/hooks/useFetch';
import useIssueUrl from 'lib/hooks/useIssueUrl';
import NetworkAddToWallet from 'ui/shared/NetworkAddToWallet';
import ColorModeToggler from '../header/ColorModeToggler';
import FooterLinkItem from './FooterLinkItem';
const MAX_LINKS_COLUMNS = 3;
const API_VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ appConfig.blockScoutVersion }`;
// const FRONT_VERSION_URL = `https://github.com/blockscout/frontend/tree/${ appConfig.frontendVersion }`;
const Footer = () => {
const issueUrl = useIssueUrl();
const BLOCSKOUT_LINKS = [
{
icon: editIcon,
iconSize: '16px',
text: 'Submit an issue',
url: issueUrl,
},
{
icon: gitIcon,
iconSize: '18px',
text: 'Contribute',
url: 'https://github.com/blockscout/blockscout',
},
{
icon: twitterIcon,
iconSize: '18px',
text: 'Twitter',
url: 'https://www.twitter.com/blockscoutcom',
},
{
icon: discordIcon,
iconSize: '18px',
text: 'Discord',
url: 'https://discord.gg/blockscout',
},
{
icon: discussionsIcon,
iconSize: '20px',
text: 'Discussions',
url: 'https://github.com/orgs/blockscout/discussions',
},
];
const fetch = useFetch();
const { isLoading, data: linksData } = useQuery<unknown, ResourceError<unknown>, Array<CustomLinksGroup>>(
[ 'footer-links' ],
async() => fetch(appConfig.footerLinks || ''),
{
enabled: Boolean(appConfig.footerLinks),
staleTime: Infinity,
});
return (
<Flex direction={{ base: 'column', lg: 'row' }} p={{ base: 4, lg: 9 }} borderTop="1px solid" borderColor="divider">
<Box flexGrow="1" mb={{ base: 8, lg: 0 }}>
<Flex>
<ColorModeToggler/>
<NetworkAddToWallet ml={ 8 }/>
</Flex>
<Box mt={{ base: 5, lg: '44px' }}>
<Link fontSize="xs" href="https://www.blockscout.com">blockscout.com</Link>
</Box>
<Text mt={ 3 } mr={{ base: 0, lg: '114px' }} maxW={{ base: 'unset', lg: '470px' }} fontSize="xs">
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text>
<VStack spacing={ 1 } mt={ 6 } alignItems="start">
{ appConfig.blockScoutVersion && (
<Text fontSize="xs">
Backend: <Link href={ API_VERSION_URL } target="_blank">{ appConfig.blockScoutVersion }</Link>
</Text>
) }
{ (appConfig.frontendVersion || appConfig.frontendCommit) && (
<Text fontSize="xs">
{ /* Frontend: <Link href={ FRONT_VERSION_URL } target="_blank">{ appConfig.frontendVersion }</Link> */ }
Frontend: { [ appConfig.frontendVersion, appConfig.frontendCommit ].filter(Boolean).join('+') }
</Text>
) }
</VStack>
</Box>
<Grid
gap={{ base: 6, lg: 12 }}
gridTemplateColumns={ appConfig.footerLinks ?
{ base: 'repeat(auto-fill, 160px)', lg: `repeat(${ (linksData?.length || MAX_LINKS_COLUMNS) + 1 }, 160px)` } :
'auto'
}
>
<Box minW="160px" w={ appConfig.footerLinks ? '160px' : '100%' }>
{ appConfig.footerLinks && <Text fontWeight={ 500 } mb={ 3 }>Blockscout</Text> }
<Grid
gap={ 1 }
gridTemplateColumns={ appConfig.footerLinks ? '160px' : { base: 'repeat(auto-fill, 160px)', lg: 'repeat(3, 160px)' } }
gridTemplateRows={{ base: 'auto', lg: appConfig.footerLinks ? 'auto' : 'repeat(2, auto)' }}
gridAutoFlow={{ base: 'row', lg: appConfig.footerLinks ? 'row' : 'column' }}
mt={{ base: 0, lg: appConfig.footerLinks ? 0 : '100px' }}
>
{ BLOCSKOUT_LINKS.map(link => <FooterLinkItem { ...link } key={ link.text }/>) }
</Grid>
</Box>
{ appConfig.footerLinks && isLoading && (
Array.from(Array(3)).map((i, index) => (
<Box minW="160px" key={ index }>
<Skeleton w="120px" h="20px" mb={ 6 }/>
<VStack spacing={ 5 } alignItems="start" mb={ 2 }>
{ Array.from(Array(5)).map((i, index) => <Skeleton w="160px" h="14px" key={ index }/>) }
</VStack>
</Box>
))
) }
{ appConfig.footerLinks && linksData && (
linksData.slice(0, MAX_LINKS_COLUMNS).map(linkGroup => (
<Box minW="160px" key={ linkGroup.title }>
<Text fontWeight={ 500 } mb={ 3 }>{ linkGroup.title }</Text>
<VStack spacing={ 1 } alignItems="start">
{ linkGroup.links.map(link => <FooterLinkItem { ...link } key={ link.text }/>) }
</VStack>
</Box>
))
) }
</Grid>
</Flex>
);
};
export default Footer;
import { Center, Icon, Link } from '@chakra-ui/react';
import React from 'react';
type Props = {
icon?: React.FC<React.SVGAttributes<SVGElement>>;
iconSize?: string;
text: string;
url: string;
}
const FooterLinkItem = ({ icon, iconSize, text, url }: Props) => {
return (
<Link href={ url } display="flex" alignItems="center" h="30px" variant="secondary" target="_blank" fontSize="xs">
{ icon && (
<Center minW={ 6 } mr="6px">
<Icon boxSize={ iconSize || 5 } as={ icon }/>
</Center>
) }
{ text }
</Link>
);
};
export default FooterLinkItem;
...@@ -110,7 +110,7 @@ test.describe('auth', () => { ...@@ -110,7 +110,7 @@ test.describe('auth', () => {
}, },
}); });
extendedTest.use({ viewport: { width: devices['iPhone 13 Pro'].viewport.width, height: 1000 } }); extendedTest.use({ viewport: { width: devices['iPhone 13 Pro'].viewport.width, height: 800 } });
extendedTest('base view', async({ mount, page }) => { extendedTest('base view', async({ mount, page }) => {
const component = await mount( const component = await mount(
......
...@@ -10,7 +10,6 @@ import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile'; ...@@ -10,7 +10,6 @@ import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile';
import SearchBar from 'ui/snippets/searchBar/SearchBar'; import SearchBar from 'ui/snippets/searchBar/SearchBar';
import Burger from './Burger'; import Burger from './Burger';
import ColorModeToggler from './ColorModeToggler';
type Props = { type Props = {
isHomePage?: boolean; isHomePage?: boolean;
...@@ -66,7 +65,6 @@ const Header = ({ isHomePage, renderSearchBar }: Props) => { ...@@ -66,7 +65,6 @@ const Header = ({ isHomePage, renderSearchBar }: Props) => {
<Box width="100%"> <Box width="100%">
{ searchBar } { searchBar }
</Box> </Box>
<ColorModeToggler/>
{ appConfig.isAccountSupported && <ProfileMenuDesktop/> } { appConfig.isAccountSupported && <ProfileMenuDesktop/> }
</HStack> </HStack>
) } ) }
......
import { Box, Text, Stack, Icon, Link, VStack } from '@chakra-ui/react';
import React from 'react';
import appConfig from 'configs/app/config';
import ghIcon from 'icons/social/git.svg';
import statsIcon from 'icons/social/stats.svg';
import tgIcon from 'icons/social/telega.svg';
import twIcon from 'icons/social/tweet.svg';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkAddToWallet from 'ui/shared/NetworkAddToWallet';
const SOCIAL_LINKS = [
{ link: appConfig.footerLinks.github, icon: ghIcon, label: 'Github link' },
{ link: appConfig.footerLinks.twitter, icon: twIcon, label: 'Twitter link' },
{ link: appConfig.footerLinks.telegram, icon: tgIcon, label: 'Telegram link' },
{ link: appConfig.footerLinks.staking, icon: statsIcon, label: 'Staking analytic link' },
].filter(({ link }) => link);
const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ appConfig.blockScoutVersion }`;
interface Props {
isCollapsed?: boolean;
hasAccount?: boolean;
}
const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
const isExpanded = isCollapsed === false;
const marginTop = (() => {
if (!hasAccount) {
return 'auto';
}
return { base: 6, lg: 20 };
})();
return (
<VStack
as="footer"
borderTop="1px solid"
borderColor="divider"
width={{ base: '100%', lg: isExpanded ? '180px' : '20px', xl: isCollapsed ? '20px' : '180px' }}
marginTop={ marginTop }
alignItems="flex-start"
alignSelf="center"
color="gray.500"
fontSize="xs"
{ ...getDefaultTransitionProps({ transitionProperty: 'width' }) }
>
<Stack
direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}
mt={{ base: 6, lg: 8 }}
_empty={{
display: 'none',
}}
>
<NetworkAddToWallet/>
{ SOCIAL_LINKS.map(sl => {
return (
<Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label } target="_blank">
<Icon as={ sl.icon } boxSize={ 5 }/>
</Link>
);
}) }
</Stack>
<Box display={{ base: 'block', lg: isExpanded ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }}>
<Text variant="secondary" mt={ 8 }>
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text>
{ appConfig.blockScoutVersion &&
<Text variant="secondary" mt={ 8 }>Version: <Link href={ VERSION_URL } target="_blank">{ appConfig.blockScoutVersion }</Link></Text> }
</Box>
</VStack>
);
};
export default NavFooter;
import { Link, Icon, Text, HStack, Tooltip, Box, useBreakpointValue, chakra, shouldForwardProp } from '@chakra-ui/react'; import { Link, Text, HStack, Tooltip, Box, useBreakpointValue, chakra, shouldForwardProp } from '@chakra-ui/react';
import NextLink from 'next/link'; import NextLink from 'next/link';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import React from 'react'; import React from 'react';
...@@ -8,6 +8,7 @@ import type { NavItem } from 'types/client/navigation-items'; ...@@ -8,6 +8,7 @@ import type { NavItem } from 'types/client/navigation-items';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { isInternalItem } from 'lib/hooks/useNavItems'; import { isInternalItem } from 'lib/hooks/useNavItems';
import NavLinkIcon from './NavLinkIcon';
import useColors from './useColors'; import useColors from './useColors';
import useNavLinkStyleProps from './useNavLinkStyleProps'; import useNavLinkStyleProps from './useNavLinkStyleProps';
...@@ -50,7 +51,7 @@ const NavLink = ({ item, isCollapsed, px, className }: Props) => { ...@@ -50,7 +51,7 @@ const NavLink = ({ item, isCollapsed, px, className }: Props) => {
color={ isInternalLink && item.isActive ? colors.text.active : colors.text.hover } color={ isInternalLink && item.isActive ? colors.text.active : colors.text.hover }
> >
<HStack spacing={ 3 } overflow="hidden"> <HStack spacing={ 3 } overflow="hidden">
{ item.icon && <Icon as={ item.icon } boxSize="30px"/> } <NavLinkIcon item={ item }/>
<Text { ...styleProps.textProps }> <Text { ...styleProps.textProps }>
{ item.text } { item.text }
</Text> </Text>
......
...@@ -17,16 +17,18 @@ import type { NavGroupItem } from 'types/client/navigation-items'; ...@@ -17,16 +17,18 @@ import type { NavGroupItem } from 'types/client/navigation-items';
import chevronIcon from 'icons/arrows/east-mini.svg'; import chevronIcon from 'icons/arrows/east-mini.svg';
import NavLink from './NavLink'; import NavLink from './NavLink';
import NavLinkIcon from './NavLinkIcon';
import useNavLinkStyleProps from './useNavLinkStyleProps'; import useNavLinkStyleProps from './useNavLinkStyleProps';
type Props = NavGroupItem & { type Props = {
item: NavGroupItem;
isCollapsed?: boolean; isCollapsed?: boolean;
} }
const NavLinkGroupDesktop = ({ text, subItems, icon, isCollapsed, isActive }: Props) => { const NavLinkGroupDesktop = ({ item, isCollapsed }: Props) => {
const isExpanded = isCollapsed === false; const isExpanded = isCollapsed === false;
const styleProps = useNavLinkStyleProps({ isCollapsed, isExpanded, isActive }); const styleProps = useNavLinkStyleProps({ isCollapsed, isExpanded, isActive: item.isActive });
return ( return (
<Box as="li" listStyleType="none" w="100%"> <Box as="li" listStyleType="none" w="100%">
...@@ -41,15 +43,15 @@ const NavLinkGroupDesktop = ({ text, subItems, icon, isCollapsed, isActive }: Pr ...@@ -41,15 +43,15 @@ const NavLinkGroupDesktop = ({ text, subItems, icon, isCollapsed, isActive }: Pr
w={{ lg: isExpanded ? '180px' : '60px', xl: isCollapsed ? '60px' : '180px' }} w={{ lg: isExpanded ? '180px' : '60px', xl: isCollapsed ? '60px' : '180px' }}
pl={{ lg: isExpanded ? 3 : '15px', xl: isCollapsed ? '15px' : 3 }} pl={{ lg: isExpanded ? 3 : '15px', xl: isCollapsed ? '15px' : 3 }}
pr={{ lg: isExpanded ? 0 : '15px', xl: isCollapsed ? '15px' : 0 }} pr={{ lg: isExpanded ? 0 : '15px', xl: isCollapsed ? '15px' : 0 }}
aria-label={ `${ text } link group` } aria-label={ `${ item.text } link group` }
position="relative" position="relative"
> >
<HStack spacing={ 3 } overflow="hidden"> <HStack spacing={ 3 } overflow="hidden">
<Icon as={ icon } boxSize="30px"/> <NavLinkIcon item={ item }/>
<Text <Text
{ ...styleProps.textProps } { ...styleProps.textProps }
> >
{ text } { item.text }
</Text> </Text>
<Icon <Icon
as={ chevronIcon } as={ chevronIcon }
...@@ -68,10 +70,10 @@ const NavLinkGroupDesktop = ({ text, subItems, icon, isCollapsed, isActive }: Pr ...@@ -68,10 +70,10 @@ const NavLinkGroupDesktop = ({ text, subItems, icon, isCollapsed, isActive }: Pr
<PopoverContent width="252px" top={{ lg: isExpanded ? '-16px' : 0, xl: isCollapsed ? 0 : '-16px' }}> <PopoverContent width="252px" top={{ lg: isExpanded ? '-16px' : 0, xl: isCollapsed ? 0 : '-16px' }}>
<PopoverBody p={ 4 }> <PopoverBody p={ 4 }>
<Text variant="secondary" fontSize="sm" mb={ 2 } display={{ lg: isExpanded ? 'none' : 'block', xl: isCollapsed ? 'block' : 'none' }}> <Text variant="secondary" fontSize="sm" mb={ 2 } display={{ lg: isExpanded ? 'none' : 'block', xl: isCollapsed ? 'block' : 'none' }}>
{ text } { item.text }
</Text> </Text>
<VStack spacing={ 1 } alignItems="start"> <VStack spacing={ 1 } alignItems="start">
{ subItems.map((item, index) => Array.isArray(item) ? ( { item.subItems.map((subItem, index) => Array.isArray(subItem) ? (
<Box <Box
key={ index } key={ index }
w="100%" w="100%"
...@@ -83,10 +85,10 @@ const NavLinkGroupDesktop = ({ text, subItems, icon, isCollapsed, isActive }: Pr ...@@ -83,10 +85,10 @@ const NavLinkGroupDesktop = ({ text, subItems, icon, isCollapsed, isActive }: Pr
borderColor: 'divider', borderColor: 'divider',
}} }}
> >
{ item.map(subItem => <NavLink key={ subItem.text } item={ subItem } isCollapsed={ false }/>) } { subItem.map(subSubItem => <NavLink key={ subSubItem.text } item={ subSubItem } isCollapsed={ false }/>) }
</Box> </Box>
) : ) :
<NavLink key={ item.text } item={ item } isCollapsed={ false }/>, <NavLink key={ item.text } item={ subItem } isCollapsed={ false }/>,
) } ) }
</VStack> </VStack>
</PopoverBody> </PopoverBody>
......
...@@ -11,15 +11,16 @@ import type { NavGroupItem } from 'types/client/navigation-items'; ...@@ -11,15 +11,16 @@ import type { NavGroupItem } from 'types/client/navigation-items';
import chevronIcon from 'icons/arrows/east-mini.svg'; import chevronIcon from 'icons/arrows/east-mini.svg';
import NavLinkIcon from './NavLinkIcon';
import useNavLinkStyleProps from './useNavLinkStyleProps'; import useNavLinkStyleProps from './useNavLinkStyleProps';
type Props = NavGroupItem & { type Props = {
isCollapsed?: boolean; item: NavGroupItem;
onClick: () => void; onClick: () => void;
} }
const NavLinkGroup = ({ text, icon, isActive, onClick }: Props) => { const NavLinkGroup = ({ item, onClick }: Props) => {
const styleProps = useNavLinkStyleProps({ isActive }); const styleProps = useNavLinkStyleProps({ isActive: item.isActive });
return ( return (
<Box as="li" listStyleType="none" w="100%" onClick={ onClick }> <Box as="li" listStyleType="none" w="100%" onClick={ onClick }>
...@@ -27,15 +28,15 @@ const NavLinkGroup = ({ text, icon, isActive, onClick }: Props) => { ...@@ -27,15 +28,15 @@ const NavLinkGroup = ({ text, icon, isActive, onClick }: Props) => {
{ ...styleProps.itemProps } { ...styleProps.itemProps }
w="100%" w="100%"
px={ 3 } px={ 3 }
aria-label={ `${ text } link group` } aria-label={ `${ item.text } link group` }
> >
<Flex justifyContent="space-between" width="100%" alignItems="center" pr={ 1 }> <Flex justifyContent="space-between" width="100%" alignItems="center" pr={ 1 }>
<HStack spacing={ 3 } overflow="hidden"> <HStack spacing={ 3 } overflow="hidden">
<Icon as={ icon } boxSize="30px"/> <NavLinkIcon item={ item }/>
<Text <Text
{ ...styleProps.textProps } { ...styleProps.textProps }
> >
{ text } { item.text }
</Text> </Text>
</HStack> </HStack>
<Icon as={ chevronIcon } transform="rotate(180deg)" boxSize={ 6 }/> <Icon as={ chevronIcon } transform="rotate(180deg)" boxSize={ 6 }/>
......
import { Icon } from '@chakra-ui/react';
import React from 'react';
import type { NavItem, NavGroupItem } from 'types/client/navigation-items';
const NavLinkIcon = ({ item }: { item: NavItem | NavGroupItem}) => {
if ('icon' in item) {
return <Icon as={ item.icon } boxSize="30px"/>;
}
if ('iconComponent' in item && item.iconComponent) {
const IconComponent = item.iconComponent;
return <IconComponent size={ 30 }/>;
}
return null;
};
export default NavLinkIcon;
...@@ -26,13 +26,7 @@ const test = base.extend({ ...@@ -26,13 +26,7 @@ const test = base.extend({
]) as any, ]) as any,
}); });
test('no auth +@desktop-xl +@dark-mode-xl', async({ page, mount }) => { test('no auth +@desktop-xl +@dark-mode-xl', async({ mount }) => {
await page.evaluate(() => {
window.ethereum = {
providers: [ { isMetaMask: true } ],
};
});
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<Flex w="100%" minH="100vh" alignItems="stretch"> <Flex w="100%" minH="100vh" alignItems="stretch">
......
...@@ -12,7 +12,6 @@ import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; ...@@ -12,7 +12,6 @@ import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import NetworkMenu from 'ui/snippets/networkMenu/NetworkMenu'; import NetworkMenu from 'ui/snippets/networkMenu/NetworkMenu';
import NavFooter from './NavFooter';
import NavLink from './NavLink'; import NavLink from './NavLink';
import NavLinkGroupDesktop from './NavLinkGroupDesktop'; import NavLinkGroupDesktop from './NavLinkGroupDesktop';
...@@ -83,7 +82,7 @@ const NavigationDesktop = () => { ...@@ -83,7 +82,7 @@ const NavigationDesktop = () => {
<VStack as="ul" spacing="1" alignItems="flex-start"> <VStack as="ul" spacing="1" alignItems="flex-start">
{ mainNavItems.map((item) => { { mainNavItems.map((item) => {
if (isGroupItem(item)) { if (isGroupItem(item)) {
return <NavLinkGroupDesktop key={ item.text } { ...item } isCollapsed={ isCollapsed }/>; return <NavLinkGroupDesktop key={ item.text } item={ item } isCollapsed={ isCollapsed }/>;
} else { } else {
return <NavLink key={ item.text } item={ item } isCollapsed={ isCollapsed }/>; return <NavLink key={ item.text } item={ item } isCollapsed={ isCollapsed }/>;
} }
...@@ -97,7 +96,6 @@ const NavigationDesktop = () => { ...@@ -97,7 +96,6 @@ const NavigationDesktop = () => {
</VStack> </VStack>
</Box> </Box>
) } ) }
<NavFooter isCollapsed={ isCollapsed } hasAccount={ hasAccount }/>
<Icon <Icon
as={ chevronIcon } as={ chevronIcon }
width={ 6 } width={ 6 }
......
...@@ -5,7 +5,6 @@ import React, { useCallback } from 'react'; ...@@ -5,7 +5,6 @@ import React, { useCallback } from 'react';
import chevronIcon from 'icons/arrows/east-mini.svg'; import chevronIcon from 'icons/arrows/east-mini.svg';
import useHasAccount from 'lib/hooks/useHasAccount'; import useHasAccount from 'lib/hooks/useHasAccount';
import useNavItems, { isGroupItem } from 'lib/hooks/useNavItems'; import useNavItems, { isGroupItem } from 'lib/hooks/useNavItems';
import NavFooter from 'ui/snippets/navigation/NavFooter';
import NavLink from 'ui/snippets/navigation/NavLink'; import NavLink from 'ui/snippets/navigation/NavLink';
import NavLinkGroupMobile from './NavLinkGroupMobile'; import NavLinkGroupMobile from './NavLinkGroupMobile';
...@@ -58,7 +57,7 @@ const NavigationMobile = () => { ...@@ -58,7 +57,7 @@ const NavigationMobile = () => {
> >
{ mainNavItems.map((item, index) => { { mainNavItems.map((item, index) => {
if (isGroupItem(item)) { if (isGroupItem(item)) {
return <NavLinkGroupMobile key={ item.text } { ...item } onClick={ onGroupItemOpen(index) }/>; return <NavLinkGroupMobile key={ item.text } item={ item } onClick={ onGroupItemOpen(index) }/>;
} else { } else {
return <NavLink key={ item.text } item={ item }/>; return <NavLink key={ item.text } item={ item }/>;
} }
...@@ -78,7 +77,6 @@ const NavigationMobile = () => { ...@@ -78,7 +77,6 @@ const NavigationMobile = () => {
</VStack> </VStack>
</Box> </Box>
) } ) }
<NavFooter hasAccount={ hasAccount }/>
</Box> </Box>
{ openedGroupIndex >= 0 && ( { openedGroupIndex >= 0 && (
<Box <Box
......
import { PopoverContent, PopoverBody, Text, Tabs, TabList, TabPanels, TabPanel, Tab, VStack, Skeleton, Flex } from '@chakra-ui/react'; import { PopoverContent, PopoverBody, Text, Tabs, TabList, TabPanels, TabPanel, Tab, VStack, Skeleton, Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { FeaturedNetwork, NetworkGroup } from 'types/networks'; import type { FeaturedNetwork, NetworkGroup } from 'types/networks';
...@@ -13,12 +13,15 @@ interface Props { ...@@ -13,12 +13,15 @@ interface Props {
const NetworkMenuPopup = ({ items, tabs }: Props) => { const NetworkMenuPopup = ({ items, tabs }: Props) => {
const selectedNetwork = items?.find(({ isActive }) => isActive); const selectedNetwork = items?.find(({ isActive }) => isActive);
const selectedTab = tabs.findIndex((tab) => selectedNetwork?.group === tab); const selectedTab = tabs.findIndex((tab) => selectedNetwork?.group === tab);
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const content = !items || items.length === 0 ? ( const content = !items || items.length === 0 ? (
<> <>
<Skeleton h="30px" w="120px"/> <Skeleton h="30px" w="120px"/>
<Flex mt={ 4 } alignItems="center"> <Flex mt={ 4 } alignItems="center">
<Skeleton h="40px" w="105px"/> <Flex h="40px" w="105px" bgColor={ bgColor } borderRadius="base" px={ 4 } py={ 2 }>
<Skeleton h="24px" w="100%"/>
</Flex>
<Skeleton h="24px" w="68px" mx={ 4 }/> <Skeleton h="24px" w="68px" mx={ 4 }/>
<Skeleton h="24px" w="45px" mx={ 4 }/> <Skeleton h="24px" w="45px" mx={ 4 }/>
</Flex> </Flex>
......
...@@ -53,6 +53,6 @@ test.describe('auth', () => { ...@@ -53,6 +53,6 @@ test.describe('auth', () => {
); );
await component.getByAltText(/Profile picture/i).click(); await component.getByAltText(/Profile picture/i).click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 250, height: 550 } }); await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 250, height: 600 } });
}); });
}); });
...@@ -7,7 +7,7 @@ import UserAvatar from 'ui/shared/UserAvatar'; ...@@ -7,7 +7,7 @@ import UserAvatar from 'ui/shared/UserAvatar';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent'; import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
const ProfileMenuDesktop = () => { const ProfileMenuDesktop = () => {
const { data, isFetched } = useFetchProfileInfo(); const { data } = useFetchProfileInfo();
const loginUrl = useLoginUrl(); const loginUrl = useLoginUrl();
return ( return (
...@@ -21,7 +21,7 @@ const ProfileMenuDesktop = () => { ...@@ -21,7 +21,7 @@ const ProfileMenuDesktop = () => {
as={ data ? undefined : 'a' } as={ data ? undefined : 'a' }
href={ data ? undefined : loginUrl } href={ data ? undefined : loginUrl }
> >
<UserAvatar size={ 50 } data={ data } isFetched={ isFetched }/> <UserAvatar size={ 50 }/>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
{ data && ( { data && (
......
...@@ -8,19 +8,26 @@ import buildApiUrl from 'playwright/utils/buildApiUrl'; ...@@ -8,19 +8,26 @@ import buildApiUrl from 'playwright/utils/buildApiUrl';
import ProfileMenuMobile from './ProfileMenuMobile'; import ProfileMenuMobile from './ProfileMenuMobile';
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('no auth', async({ mount, page }) => { test('no auth', async({ mount, page }) => {
const hooksConfig = {
router: {
asPath: '/',
pathname: '/',
},
};
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<ProfileMenuMobile/> <ProfileMenuMobile/>
</TestApp>, </TestApp>,
{ hooksConfig },
); );
await component.locator('.identicon').click(); await component.locator('.identicon').click();
await expect(page).toHaveScreenshot(); expect(page.url()).toBe('http://localhost:3100/auth/auth0?path=%2F');
}); });
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test.describe('auth', () => { test.describe('auth', () => {
const extendedTest = test.extend({ const extendedTest = test.extend({
context: ({ context }, use) => { context: ({ context }, use) => {
...@@ -49,8 +56,5 @@ test.describe('auth', () => { ...@@ -49,8 +56,5 @@ test.describe('auth', () => {
await component.getByAltText(/Profile picture/i).click(); await component.getByAltText(/Profile picture/i).click();
await expect(page).toHaveScreenshot(); await expect(page).toHaveScreenshot();
await page.locator('div[aria-label="Toggle color mode"]').click();
await expect(page).toHaveScreenshot();
}); });
}); });
import { Flex, Box, Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure, Button } from '@chakra-ui/react'; import { Box, Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclosure, Button } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import useLoginUrl from 'lib/hooks/useLoginUrl'; import useLoginUrl from 'lib/hooks/useLoginUrl';
import UserAvatar from 'ui/shared/UserAvatar'; import UserAvatar from 'ui/shared/UserAvatar';
import ColorModeToggler from 'ui/snippets/header/ColorModeToggler';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent'; import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
const ProfileMenuMobile = () => { const ProfileMenuMobile = () => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const { data, isFetched } = useFetchProfileInfo(); const { data } = useFetchProfileInfo();
const loginUrl = useLoginUrl(); const loginUrl = useLoginUrl();
return ( return (
<> <>
<Box padding={ 2 } onClick={ onOpen }> <Box padding={ 2 } onClick={ data ? onOpen : undefined }>
<UserAvatar size={ 24 } data={ data } isFetched={ isFetched }/> <Button
variant="unstyled"
height="auto"
as={ data ? undefined : 'a' }
href={ data ? undefined : loginUrl }
>
<UserAvatar size={ 24 }/>
</Button>
</Box> </Box>
{ data && (
<Drawer <Drawer
isOpen={ isOpen } isOpen={ isOpen }
placement="right" placement="right"
...@@ -27,22 +34,11 @@ const ProfileMenuMobile = () => { ...@@ -27,22 +34,11 @@ const ProfileMenuMobile = () => {
<DrawerOverlay/> <DrawerOverlay/>
<DrawerContent maxWidth="260px"> <DrawerContent maxWidth="260px">
<DrawerBody p={ 6 }> <DrawerBody p={ 6 }>
<Flex <ProfileMenuContent { ...data }/>
justifyContent="space-between"
alignItems="center"
mb={ 6 }
>
<ColorModeToggler/>
<Box onClick={ onClose }>
<UserAvatar size={ 24 } data={ data } isFetched={ isFetched }/>
</Box>
</Flex>
{ data ? <ProfileMenuContent { ...data }/> : (
<Button size="sm" width="full" variant="outline" as="a" href={ loginUrl }>Sign In</Button>
) }
</DrawerBody> </DrawerBody>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
) }
</> </>
); );
}; };
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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