Commit e5f10595 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into fix-ci-cd

parents 82984260 5969ae25
...@@ -4,4 +4,4 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout ...@@ -4,4 +4,4 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_SUPPORTED_NETWORKS=[{"name":"Gnosis Chain","type":"xdai","subType":"mainnet","group":"mainnets","isAccountSupported":true,"chainId":100},{"name":"Optimism on Gnosis Chain","shortName":"OoG","type":"xdai","subType":"optimism","group":"mainnets","icon":"https://www.fillmurray.com/60/60","logo":"https://www.fillmurray.com/240/60","chainId":300},{"name":"Arbitrum on xDai","type":"xdai","subType":"aox","group":"mainnets","chainId":200},{"name":"Ethereum","shortName":"ETH","type":"eth","subType":"mainnet","group":"mainnets","chainId":1},{"name":"Ethereum Classic","shortName":"ETC","type":"etc","subType":"mainnet","group":"mainnets","chainId":61},{"name":"POA","shortName":"POA","type":"poa","subType":"core","group":"mainnets","chainId":99},{"name":"RSK","shortName":"RBTC","type":"rsk","subType":"mainnet","group":"mainnets","chainId":30},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets","isAccountSupported":true},{"name":"POA Sokol","shortName":"POA","type":"poa","subType":"sokol","group":"testnets","chainId":77},{"name":"ARTIS Σ1","type":"artis","subType":"sigma1","group":"other","chainId":246529},{"name":"LUKSO L14","shortName":"POA","type":"lukso","subType":"l14","group":"other","chainId":22}] NEXT_PUBLIC_SUPPORTED_NETWORKS=[{"name":"Gnosis Chain","type":"xdai","subType":"mainnet","group":"mainnets","isAccountSupported":true,"chainId":100,"currency":"xDAI"},{"name":"Optimism on Gnosis Chain","shortName":"OoG","type":"xdai","subType":"optimism","group":"mainnets","icon":"https://www.fillmurray.com/60/60","chainId":300,"currency":"xDAI"},{"name":"Arbitrum on xDai","type":"xdai","subType":"aox","group":"mainnets","chainId":200,"currency":"xDAI"},{"name":"Ethereum","shortName":"ETH","type":"eth","subType":"mainnet","group":"mainnets","chainId":1,"currency":"ETH"},{"name":"Ethereum Classic","shortName":"ETC","type":"etc","subType":"mainnet","group":"mainnets","chainId":61,"currency":"ETC"},{"name":"POA","shortName":"POA","type":"poa","subType":"core","group":"mainnets","chainId":99,"currency":"POA","nativeTokenAddress": "0x029a799563238d0e75e20be2f4bda0ea68d00172"},{"name":"RSK","shortName":"RBTC","type":"rsk","subType":"mainnet","group":"mainnets","chainId":30,"currency":"RBTC"},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets","isAccountSupported":true,"currency":"xDAI"},{"name":"POA Sokol","shortName":"POA","type":"poa","subType":"sokol","group":"testnets","chainId":77,"currency":"SPOA"},{"name":"ARTIS Σ1","type":"artis","subType":"sigma1","group":"other","chainId":246529,"currency":"ATS"},{"name":"LUKSO L14","shortName":"POA","type":"lukso","subType":"l14","group":"other","chainId":22,"currency":"LYX"},{"name":"Astar","type":"astar","group":"other","chainId":22,"currency":"ASTR"}]
const RESTRICTED_MODULES = { const RESTRICTED_MODULES = {
paths: [ paths: [
{ name: 'dayjs', message: 'Please use lib/date/dayjs.ts' }, { name: 'dayjs', message: 'Please use lib/date/dayjs.ts instead of directly importing dayjs' },
{ name: '@chakra-ui/icons', message: 'Using @chakra-ui/icons is prohibited. Please use regular svg-icon instead (see examples in "icons/" folder)' },
], ],
}; };
module.exports = { module.exports = {
......
...@@ -36,20 +36,24 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -36,20 +36,24 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` *(optional)* | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` | | NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` *(optional)* | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` |
| NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` *(optional)* | Link to Telegram in the footer | `https://t.me/poa_network` | | NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` *(optional)* | Link to Telegram in the footer | `https://t.me/poa_network` |
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` | | NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_SUPPORTED_NETWORKS | `Array<Network>` where `Network` can have following [properties](#network-configuration-properties) | Configuration of supported networks | `[{'name':'Gnosis Chain','type':'xdai','subType':'mainnet','group':'mainnets','isAccountSupported':true, 'chainId': 100,'icon':'https://www.fillmurray.com/60/60','logo':'https://www.fillmurray.com/240/40'}]` | | NEXT_PUBLIC_SUPPORTED_NETWORKS | `Array<Network>` where `Network` can have following [properties](#network-configuration-properties) | Configuration of supported networks | `[{"name":"POA","type":"poa","subType":"core","group":"mainnets","isAccountSupported":true,"chainId":99,"currency":"POA"}]` |
### Network configuration properties ### Network configuration properties
| Property | Type | Description | Example value | Property | Type | Description | Example value
| --- | --- | --- | --- | | --- | --- | --- | --- |
| name | `string` | Displayed name of the network | `'Gnosis Chain'` | | name | `string` | Displayed name of the network | `"Gnosis Chain"` |
| chainId | `number` | Id of the network. Could be seen there – [https://chainlist.org/](https://chainlist.org/) | `1` | | shortName | `string` | Used for SEO attributes (page title and description) | `"OoG"` |
| type | `string` | Network type (used as first part of the base path) | `'xdai'` | | chainId | `number` | Id of the network. Could be found here – [https://chainlist.org/](https://chainlist.org/) | `1` |
| currency | `string` | Network currency symbol. Could be found here – [https://chainlist.org/](https://chainlist.org/) | `"xDAI"` |
| nativeTokenAddress | `string` | Address of network's native token | `"0x029a799563238d0e75e20be2f4bda0ea68d00172"` |
| type | `string` | Network type (used as first part of the base path) | `"xdai"` |
| subType | `string` | Network subtype (used as second part of the base path) | `"mainnet"` | | subType | `string` | Network subtype (used as second part of the base path) | `"mainnet"` |
| group | `mainnets \| testnets \| other` | Indicates in which tab network appears in the menu | `'mainnets'` | | group | `mainnets \| testnets \| other` | Indicates in which tab network appears in the menu | `'mainnets'` |
| isAccountSupported | `boolean` *(optional)* | Set to true if network has account feature | `true` | | isAccountSupported | `boolean` *(optional)* | Set to true if network has account feature | `true` |
| icon | `string` *(optional)* | Network icon; if not provided, will fallback to icon predefined in the project; if the project doesn't have icon for such network then the common placeholder will be shown; *Note* that icon size should be 30px by 30px | `'https://www.fillmurray.com/60/60'` | | icon | `string` *(optional)* | Network icon; if not provided, will fallback to icon predefined in the project; if the project doesn't have icon for such network then the common placeholder will be shown; *Note* that icon size should be 30px by 30px | `"https://www.fillmurray.com/60/60"` |
| logo | `string` *(optional)* | Network logo; if not provided, will fallback to logo predefined in the project; if the project doesn't have logo for such network then the common placeholder will be shown; *Note* that logo height should be 20px and width less than 120px | `'https://www.fillmurray.com/240/40'` | | logo | `string` *(optional)* | Network logo; if not provided, will fallback to logo predefined in the project; if the project doesn't have logo for such network then the common placeholder will be shown; *Note* that logo height should be 20px and width less than 120px | `"https://www.fillmurray.com/240/40"` |
| assetsNamePath | `string` *(optional)* | Network name for constructing url of token logos according to template `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/${assetsNamePath}/assets/${tokenAddress}/logo.png`. It should match network name in TrustWallet assets repo, see the full list [here](https://github.com/trustwallet/assets/tree/master/blockchains). The project already has some pre-defined mapping for popular network, which is match assetsNamePath against provided network type and sub-type. So typically you don't need to provide this variable, if you network is in the list or its type in config is conformed to a name in TrustWallet repo. If it is not the case, pass value here | `"ethereum"` |
*Note* the base path for the network is built up from its `type` and `subType` like so `https://blockscout.com/<type>/<subType>` *Note* the base path for the network is built up from its `type` and `subType` like so `https://blockscout.com/<type>/<subType>`
......
/* eslint-disable max-len */ /* eslint-disable max-len */
export const tx = { export const tx = {
hash: '0x1ea365d2144796f793883534aa51bf20d23292b19478994eede23dfc599e7c34', hash: '0x1ea365d2144796f793883534aa51bf20d23292b19478994eede23dfc599e7c34',
status: 'success', status: 'success' as TxStatus,
block_num: 15006918, block_num: 15006918,
confirmation_num: 283, confirmation_num: 283,
confirmation_duration: 30, confirmation_duration: 30,
timestamp: 1662623567695, timestamp: 1662623567695,
address_from: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830', address_from: {
address_to: '0x35317007D203b8a86CA727ad44E473E40450E378', hash: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830',
type: 'Address',
alias: '',
},
address_to: {
hash: '0x35317007D203b8a86CA727ad44E473E40450E378',
type: 'Contract',
alias: '',
},
amount: { amount: {
value: 0.03, value: 0.03,
value_usd: 35.5, value_usd: 35.5,
...@@ -36,7 +44,12 @@ export const tx = { ...@@ -36,7 +44,12 @@ export const tx = {
position: 342, position: 342,
input_hex: '0x42842e0e0000000000000000000000007767dac225a233ea1055d79fb227b1696d538b75000000000000000000000000fc3017c31fe752fc48e904050ea5d6edfc38a1b00000000000000000000000000000000000000000000000000000000000000e3b', input_hex: '0x42842e0e0000000000000000000000007767dac225a233ea1055d79fb227b1696d538b75000000000000000000000000fc3017c31fe752fc48e904050ea5d6edfc38a1b00000000000000000000000000000000000000000000000000000000000000e3b',
transferred_tokens: [ transferred_tokens: [
{ from: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', to: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e', token: 'USDT', amount: 192.7, usd: 194.05 }, { from: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', to: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e', token: { symbol: 'VIK', hash: '0xADFE00d92e5A16e773891F59780e6e54f40B532e', name: 'Viktor Coin' }, amount: 192.7, usd: 194.05 },
{ from: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', to: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', token: 'TOKE', amount: 76.1851851851846, usd: 194.05 }, { from: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', to: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', token: { symbol: 'PAO', hash: '0xC98a06220239818B086CD96756d4E3bC41EC848E', name: 'POA Candy' }, amount: 76.1851851851846, usd: 194.05 },
], ],
txType: 'transaction' as TxType,
}; };
export type TxType = 'contract-call' | 'transaction' | 'token-transfer' | 'internal-tx' | 'multicall';
export type TxStatus = 'success' | 'failed' | 'pending';
import type { TxInternalsType } from 'types/api/tx';
export const data = [ export const data = [
{ {
id: 1, id: 1,
type: 'call', type: 'call' as TxInternalsType,
status: 'success' as const, status: 'success' as const,
from: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', from: { hash: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01' },
to: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e', to: { hash: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e' },
value: 0.25207646303, value: 0.25207646303,
gasLimit: 369472, gasLimit: 369472,
}, },
{ {
id: 2, id: 2,
type: 'delegate call', type: 'delegate_call' as TxInternalsType,
status: 'error' as const, status: 'success' as const,
from: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', from: { hash: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' },
to: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', to: { hash: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01' },
value: 0.5633333, value: 0.5633333,
gasLimit: 340022, gasLimit: 340022,
}, },
{ {
id: 3, id: 3,
type: 'static call', type: 'static_call' as TxInternalsType,
status: 'success' as const, status: 'failed' as const,
from: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830', from: { hash: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830' },
to: '0x35317007D203b8a86CA727ad44E473E40450E378', to: { hash: '0x35317007D203b8a86CA727ad44E473E40450E378' },
value: 0.421152366, value: 0.421152366,
gasLimit: 509333, gasLimit: 509333,
}, },
......
import { tx } from './tx';
import type { TxType, TxStatus } from './tx';
export const txs = [
{
...tx,
method: 'Withdraw',
txType: 'transaction' as TxType,
errorText: '',
},
{
...tx,
status: 'failed' as TxStatus,
errorText: 'Error: (Awaiting internal transactions for reason)',
txType: 'contract-call' as TxType,
method: 'CommitHash CommitHash CommitHash CommitHash',
amount: {
value: 0.04,
value_usd: 35.5,
},
fee: {
value: 0.002295904453623692,
value_usd: 2.84,
},
},
{
...tx,
status: 'pending' as TxStatus,
txType: 'token-transfer' as TxType,
method: 'Multicall',
address_from: {
hash: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830',
alias: 'tkdkdkdkdkdkdkdkdkdkdkdkdkdkd.eth',
type: 'ENS name',
},
amount: {
value: 0.02,
value_usd: 35.5,
},
fee: {
value: 0.002495904453623692,
value_usd: 2.84,
},
errorText: '',
},
];
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.535 11.293a1 1 0 0 0 0 1.414l3.536 3.536a1 1 0 1 1-1.414 1.414l-4.95-4.95a1 1 0 0 1 0-1.414l4.95-4.95a1 1 0 1 1 1.414 1.414l-3.536 3.536Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m11.003 6.276-5.267 5.267a.667.667 0 0 1-.943-.943l5.266-5.267H5.67A.667.667 0 1 1 5.67 4h6.667v6.667a.667.667 0 0 1-1.333 0V6.276Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.774 13.088a.936.936 0 0 0 0 1.325l2.814 2.812a.935.935 0 0 0 1.324 0l2.813-2.812A.935.935 0 1 0 8.4 13.088l-1.213 1.21v-9.95a.954.954 0 0 0-.938-.937.954.954 0 0 0-.937.938v9.95L4.1 13.088a.937.937 0 0 0-1.327 0Zm10.039 2.538a.937.937 0 1 0 1.875 0V5.702l1.213 1.21a.935.935 0 1 0 1.324-1.324l-2.812-2.813a.936.936 0 0 0-1.325 0l-2.812 2.813A.935.935 0 1 0 11.6 6.912l1.213-1.21v9.924Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#link_svg__a)">
<path d="M5.547 6.073c1.303-1.303 3.469-1.303 4.772 0 1.171 1.172 1.345 3.04.382 4.387l-.026.038a.75.75 0 0 1-1.221-.872l.026-.038a1.89 1.89 0 0 0-2.874-2.435l-2.63 2.632c-.738.717-.738 1.934 0 2.672a1.887 1.887 0 0 0 2.433.202l.038-.047a.772.772 0 0 1 1.045.194.75.75 0 0 1-.173 1.048l-.038.026a3.389 3.389 0 0 1-4.366-5.154l2.632-2.653Zm6.914 5.833A3.388 3.388 0 0 1 7.307 7.54l.026-.038c.22-.335.689-.415 1.045-.173.338.22.417.689.176 1.045l-.026.038c-.537.731-.452 1.781.202 2.435a1.893 1.893 0 0 0 2.671 0l2.63-2.632c.738-.738.738-1.955 0-2.672a1.887 1.887 0 0 0-2.433-.202l-.037.026c-.338.242-.806.143-1.046-.174a.749.749 0 0 1 .174-1.046l.037-.026a3.389 3.389 0 0 1 4.367 5.153l-2.632 2.632Z" fill="currentColor"/>
</g>
<defs>
<clipPath id="link_svg__a">
<path fill="#fff" transform="translate(1.504 3)" d="M0 0h15v12H0z"/>
</clipPath>
</defs>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m15.026 13.848 2.98 2.978a.834.834 0 1 1-1.18 1.18l-2.978-2.98a7.467 7.467 0 0 1-4.681 1.64c-4.14 0-7.5-3.36-7.5-7.5 0-4.14 3.36-7.5 7.5-7.5 4.14 0 7.5 3.36 7.5 7.5a7.466 7.466 0 0 1-1.641 4.681Zm-1.672-.619A5.814 5.814 0 0 0 15 9.167a5.832 5.832 0 0 0-5.833-5.834 5.831 5.831 0 0 0-5.834 5.834A5.832 5.832 0 0 0 9.167 15a5.814 5.814 0 0 0 4.062-1.646l.125-.125Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.76 17.333a.603.603 0 0 1-.294-.075L9 14.798l-4.467 2.46a.606.606 0 0 1-.663-.051.657.657 0 0 1-.213-.285.69.69 0 0 1-.038-.36l.854-5.21-3.616-3.69a.69.69 0 0 1-.16-.677.663.663 0 0 1 .194-.301c.09-.08.2-.131.316-.149l4.994-.76 2.234-4.74a.65.65 0 0 1 .232-.269.61.61 0 0 1 .666 0c.1.065.18.158.233.269l2.233 4.74 4.994.76c.117.018.226.07.316.149a.66.66 0 0 1 .193.3.69.69 0 0 1-.16.678l-3.614 3.69.853 5.21a.692.692 0 0 1-.14.537.634.634 0 0 1-.216.173.605.605 0 0 1-.265.061Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 18 18" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 18 18" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.76 17.333a.604.604 0 0 1-.294-.075l.293.075Zm.004 0a.625.625 0 0 0 .477-.234.671.671 0 0 0 .14-.538l-.853-5.21 3.615-3.689a.69.69 0 0 0 .16-.677.663.663 0 0 0-.194-.301.617.617 0 0 0-.316-.149l-4.884-.743a.208.208 0 0 1-.157-.117l-2.186-4.64a.65.65 0 0 0-.233-.269.61.61 0 0 0-.666 0 .65.65 0 0 0-.232.269l-2.186 4.64a.208.208 0 0 1-.158.117l-4.884.743a.618.618 0 0 0-.316.149.663.663 0 0 0-.193.3.69.69 0 0 0 .16.678l3.54 3.614a.208.208 0 0 1 .058.18l-.837 5.105a.69.69 0 0 0 .038.36.657.657 0 0 0 .213.286.613.613 0 0 0 .663.05L8.9 14.854a.208.208 0 0 1 .2 0l4.366 2.405m-7.795-2.915c-.028.172.154.3.307.216L8.9 12.95a.208.208 0 0 1 .2 0l2.923 1.61a.208.208 0 0 0 .306-.216l-.566-3.452a.208.208 0 0 1 .057-.18l2.486-2.536a.208.208 0 0 0-.118-.351l-3.408-.519a.208.208 0 0 1-.157-.117L9.189 4.145a.208.208 0 0 0-.377 0L7.378 7.19a.208.208 0 0 1-.158.117l-3.408.519a.208.208 0 0 0-.117.351l2.485 2.537a.208.208 0 0 1 .057.18l-.566 3.45Zm8.093 2.99h-.003.003Z" fill="#4A5568"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M13.76 17.333a.604.604 0 0 1-.294-.075l.293.075Zm.004 0a.625.625 0 0 0 .477-.234.671.671 0 0 0 .14-.538l-.853-5.21 3.615-3.689a.69.69 0 0 0 .16-.677.663.663 0 0 0-.194-.301.617.617 0 0 0-.316-.149l-4.884-.743a.208.208 0 0 1-.157-.117l-2.186-4.64a.65.65 0 0 0-.233-.269.61.61 0 0 0-.666 0 .65.65 0 0 0-.232.269l-2.186 4.64a.208.208 0 0 1-.158.117l-4.884.743a.618.618 0 0 0-.316.149.663.663 0 0 0-.193.3.69.69 0 0 0 .16.678l3.54 3.614a.208.208 0 0 1 .058.18l-.837 5.105a.69.69 0 0 0 .038.36.657.657 0 0 0 .213.286.613.613 0 0 0 .663.05L8.9 14.854a.208.208 0 0 1 .2 0l4.366 2.405m-7.795-2.915c-.028.172.154.3.307.216L8.9 12.95a.208.208 0 0 1 .2 0l2.923 1.61a.208.208 0 0 0 .306-.216l-.566-3.452a.208.208 0 0 1 .057-.18l2.486-2.536a.208.208 0 0 0-.118-.351l-3.408-.519a.208.208 0 0 1-.157-.117L9.189 4.145a.208.208 0 0 0-.377 0L7.378 7.19a.208.208 0 0 1-.158.117l-3.408.519a.208.208 0 0 0-.117.351l2.485 2.537a.208.208 0 0 1 .057.18l-.566 3.45Zm8.093 2.99h-.003.003Z"/>
</svg> </svg>
<svg viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="5" cy="5" r="5" fill="#D9DBE0"/>
<circle cx="5" cy="5" r="2.5" fill="#707886"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m4.7 14.167-.592.591a.833.833 0 0 0 0 1.175.833.833 0 0 0 1.175 0l.592-.591A.833.833 0 0 0 4.7 14.167ZM4.166 10a.833.833 0 0 0-.833-.833H2.5a.833.833 0 1 0 0 1.666h.833A.833.833 0 0 0 4.166 10ZM10 4.167a.833.833 0 0 0 .833-.834V2.5a.833.833 0 1 0-1.667 0v.833a.833.833 0 0 0 .834.834ZM4.7 5.875a.833.833 0 0 0 1.175 0 .833.833 0 0 0 0-1.175l-.592-.592a.833.833 0 0 0-1.175 1.175l.592.592Zm10 .242a.833.833 0 0 0 .583-.242l.592-.592A.832.832 0 1 0 14.7 4.108l-.534.592a.833.833 0 0 0 0 1.175.833.833 0 0 0 .55.242H14.7Zm2.8 3.05h-.834a.833.833 0 0 0 0 1.666h.834a.833.833 0 1 0 0-1.666ZM10 15.833a.833.833 0 0 0-.834.834v.833a.833.833 0 1 0 1.667 0v-.833a.833.833 0 0 0-.833-.834Zm5.3-1.666a.833.833 0 0 0-1.134 1.133l.592.592a.833.833 0 0 0 1.175 0 .833.833 0 0 0 0-1.175l-.633-.55ZM10 5.417A4.583 4.583 0 1 0 14.583 10 4.592 4.592 0 0 0 10 5.417Zm0 7.5a2.917 2.917 0 1 1 0-5.833 2.917 2.917 0 0 1 0 5.833Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path fill="url(#toke_svg__a)" d="M0 0h20v20H0z"/>
<defs>
<pattern id="toke_svg__a" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#toke_svg__b" transform="scale(.03125)"/>
</pattern>
<image id="toke_svg__b" width="32" height="32" xlink:href="data:image/png;base64,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"/>
</defs>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path fill="url(#usdt_svg__a)" d="M0 0h20v20H0z"/>
<defs>
<pattern id="usdt_svg__a" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#usdt_svg__b" transform="scale(.03125)"/>
</pattern>
<image id="usdt_svg__b" width="32" height="32" xlink:href="data:image/png;base64,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"/>
</defs>
</svg>
...@@ -12,7 +12,7 @@ export enum NAMES { ...@@ -12,7 +12,7 @@ export enum NAMES {
export function get(name?: string | undefined | null) { export function get(name?: string | undefined | null) {
if (!isBrowser()) { if (!isBrowser()) {
return () => {}; return undefined;
} }
return Cookies.get(name); return Cookies.get(name);
} }
......
// eslint-disable-next-line no-restricted-imports // eslint-disable-next-line no-restricted-imports
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import localizedFormat from 'dayjs/plugin/localizedFormat'; import localizedFormat from 'dayjs/plugin/localizedFormat';
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime';
import updateLocale from 'dayjs/plugin/updateLocale'; import updateLocale from 'dayjs/plugin/updateLocale';
dayjs.locale('en'); dayjs.locale('en');
dayjs.extend(relativeTime); const relativeTimeConfig = {
thresholds: [
{ l: 's', r: 1 },
{ l: 'ss', r: 59, d: 'second' },
{ l: 'm', r: 1 },
{ l: 'mm', r: 59, d: 'minute' },
{ l: 'h', r: 1 },
{ l: 'hh', r: 23, d: 'hour' },
{ l: 'd', r: 1 },
{ l: 'dd', r: 29, d: 'day' },
{ l: 'M', r: 1 },
{ l: 'MM', r: 11, d: 'month' },
{ l: 'y' },
{ l: 'yy', d: 'year' },
],
};
dayjs.extend(relativeTime, relativeTimeConfig);
dayjs.extend(updateLocale); dayjs.extend(updateLocale);
dayjs.extend(localizedFormat); dayjs.extend(localizedFormat);
dayjs.extend(duration);
dayjs.updateLocale('en', { dayjs.updateLocale('en', {
formats: { formats: {
LLLL: 'MMMM-DD-YYYY HH:mm:ss A Z UTC', LLLL: 'MMMM-DD-YYYY HH:mm:ss A Z UTC',
}, },
relativeTime: {
s: 'a sec',
ss: '%d secs',
future: 'in %s',
past: '%s ago',
m: 'a min',
mm: '%d mins',
h: 'an hour',
hh: '%d hours',
d: 'a day',
dd: '%d days',
M: 'a month',
MM: '%d months',
y: 'a year',
yy: '%d years',
},
}); });
export default dayjs; export default dayjs;
...@@ -4,7 +4,7 @@ import abiIcon from 'icons/ABI.svg'; ...@@ -4,7 +4,7 @@ import abiIcon from 'icons/ABI.svg';
import apiKeysIcon from 'icons/API.svg'; import apiKeysIcon from 'icons/API.svg';
import appsIcon from 'icons/apps.svg'; import appsIcon from 'icons/apps.svg';
import blocksIcon from 'icons/block.svg'; import blocksIcon from 'icons/block.svg';
import gearIcon from 'icons/gear.svg'; // import gearIcon from 'icons/gear.svg';
import privateTagIcon from 'icons/privattags.svg'; import privateTagIcon from 'icons/privattags.svg';
import profileIcon from 'icons/profile.svg'; import profileIcon from 'icons/profile.svg';
import publicTagIcon from 'icons/publictags.svg'; import publicTagIcon from 'icons/publictags.svg';
...@@ -21,10 +21,13 @@ export default function useNavItems() { ...@@ -21,10 +21,13 @@ export default function useNavItems() {
return React.useMemo(() => { return React.useMemo(() => {
const mainNavItems = [ const mainNavItems = [
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute === 'blocks' }, { text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute === 'blocks' },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx') }, { text: 'Transactions', url: link('txs_validated'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx') },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens' }, { text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens' },
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps' }, { text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps' },
{ text: 'Other', url: link('other'), icon: gearIcon, isActive: currentRoute === 'other' }, // there should be custom site sections like Stats, Faucet, More, etc but never an 'other'
// examples https://explorer-edgenet.polygon.technology/ and https://explorer.celo.org/
// at this stage custom menu items is under development, we will implement it later
// { text: 'Other', url: link('other'), icon: gearIcon, isActive: currentRoute === 'other' },
]; ];
const accountNavItems = [ const accountNavItems = [
......
...@@ -6,15 +6,15 @@ import type { RouteName } from './routes'; ...@@ -6,15 +6,15 @@ import type { RouteName } from './routes';
const PATH_PARAM_REGEXP = /\/\[(\w+)\]/g; const PATH_PARAM_REGEXP = /\/\[(\w+)\]/g;
export function link(routeName: RouteName, urlParams?: Record<string, string | undefined>, queryParams?: Record<string, string>): string { export function link(routeName: RouteName, urlParams?: Record<string, Array<string> | string | undefined>, queryParams?: Record<string, string>): string {
const route = ROUTES[routeName]; const route = ROUTES[routeName];
if (!route) { if (!route) {
return ''; return '';
} }
const network = findNetwork({ const network = findNetwork({
network_type: urlParams?.network_type || '', network_type: typeof urlParams?.network_type === 'string' ? urlParams?.network_type : '',
network_sub_type: urlParams?.network_sub_type, network_sub_type: typeof urlParams?.network_sub_type === 'string' ? urlParams?.network_sub_type : undefined,
}); });
const path = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => { const path = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => {
...@@ -22,7 +22,13 @@ export function link(routeName: RouteName, urlParams?: Record<string, string | u ...@@ -22,7 +22,13 @@ export function link(routeName: RouteName, urlParams?: Record<string, string | u
return ''; return '';
} }
const paramValue = urlParams?.[paramName]; let paramValue = urlParams?.[paramName];
if (Array.isArray(paramValue)) {
// FIXME we don't have yet params as array, but typescript says that we could
// dun't know how to manage it, fix me if you find an issue
paramValue = paramValue.join(',');
}
return paramValue ? `/${ paramValue }` : ''; return paramValue ? `/${ paramValue }` : '';
}); });
......
...@@ -45,10 +45,14 @@ export const ROUTES = { ...@@ -45,10 +45,14 @@ export const ROUTES = {
}, },
// TRANSACTIONS // TRANSACTIONS
txs: { txs_validated: {
pattern: `${ BASE_PATH }/txs`, pattern: `${ BASE_PATH }/txs`,
crossNetworkNavigation: true, crossNetworkNavigation: true,
}, },
txs_pending: {
pattern: `${ BASE_PATH }/pending-transactions`,
crossNetworkNavigation: true,
},
tx_index: { tx_index: {
pattern: `${ BASE_PATH }/tx/[id]`, pattern: `${ BASE_PATH }/tx/[id]`,
}, },
...@@ -70,6 +74,9 @@ export const ROUTES = { ...@@ -70,6 +74,9 @@ export const ROUTES = {
pattern: `${ BASE_PATH }/blocks`, pattern: `${ BASE_PATH }/blocks`,
crossNetworkNavigation: true, crossNetworkNavigation: true,
}, },
block: {
pattern: `${ BASE_PATH }/block/[id]`,
},
// TOKENS // TOKENS
tokens: { tokens: {
......
...@@ -11,10 +11,6 @@ export default function useLink() { ...@@ -11,10 +11,6 @@ export default function useLink() {
const networkSubType = router.query.network_sub_type; const networkSubType = router.query.network_sub_type;
return React.useCallback((...args: LinkBuilderParams) => { return React.useCallback((...args: LinkBuilderParams) => {
if (typeof networkType !== 'string' || typeof networkSubType !== 'string') {
return '';
}
return link(args[0], { network_type: networkType, network_sub_type: networkSubType, ...args[1] }, args[2]); return link(args[0], { network_type: networkType, network_sub_type: networkSubType, ...args[1] }, args[2]);
}, [ networkType, networkSubType ]); }, [ networkType, networkSubType ]);
} }
...@@ -61,6 +61,7 @@ export default NETWORKS; ...@@ -61,6 +61,7 @@ export default NETWORKS;
// group: 'mainnets', // group: 'mainnets',
// isAccountSupported: true, // isAccountSupported: true,
// chainId: 100, // chainId: 100,
// currency: 'xDAI',
// }, // },
// { // {
// name: 'Optimism on Gnosis Chain', // name: 'Optimism on Gnosis Chain',
...@@ -71,6 +72,7 @@ export default NETWORKS; ...@@ -71,6 +72,7 @@ export default NETWORKS;
// icon: 'https://www.fillmurray.com/60/60', // icon: 'https://www.fillmurray.com/60/60',
// logo: 'https://www.fillmurray.com/240/60', // logo: 'https://www.fillmurray.com/240/60',
// chainId: 300, // chainId: 300,
// currency: 'xDAI',
// }, // },
// { // {
// name: 'Arbitrum on xDai', // name: 'Arbitrum on xDai',
...@@ -78,6 +80,7 @@ export default NETWORKS; ...@@ -78,6 +80,7 @@ export default NETWORKS;
// subType: 'aox', // subType: 'aox',
// group: 'mainnets', // group: 'mainnets',
// chainId: 200, // chainId: 200,
// currency: 'xDAI',
// }, // },
// { // {
// name: 'Ethereum', // name: 'Ethereum',
...@@ -86,6 +89,7 @@ export default NETWORKS; ...@@ -86,6 +89,7 @@ export default NETWORKS;
// subType: 'mainnet', // subType: 'mainnet',
// group: 'mainnets', // group: 'mainnets',
// chainId: 1, // chainId: 1,
// currency: 'ETH',
// }, // },
// { // {
// name: 'Ethereum Classic', // name: 'Ethereum Classic',
...@@ -94,6 +98,7 @@ export default NETWORKS; ...@@ -94,6 +98,7 @@ export default NETWORKS;
// subType: 'mainnet', // subType: 'mainnet',
// group: 'mainnets', // group: 'mainnets',
// chainId: 61, // chainId: 61,
// currency: 'ETC',
// }, // },
// { // {
// name: 'POA', // name: 'POA',
...@@ -102,6 +107,9 @@ export default NETWORKS; ...@@ -102,6 +107,9 @@ export default NETWORKS;
// subType: 'core', // subType: 'core',
// group: 'mainnets', // group: 'mainnets',
// chainId: 99, // chainId: 99,
// currency: 'POA',
// isAccountSupported: true,
// nativeTokenAddress: '0x029a799563238d0e75e20be2f4bda0ea68d00172',
// }, // },
// { // {
// name: 'RSK', // name: 'RSK',
...@@ -110,6 +118,7 @@ export default NETWORKS; ...@@ -110,6 +118,7 @@ export default NETWORKS;
// subType: 'mainnet', // subType: 'mainnet',
// group: 'mainnets', // group: 'mainnets',
// chainId: 30, // chainId: 30,
// currency: 'RBTC',
// }, // },
// { // {
// name: 'Gnosis Chain Testnet', // name: 'Gnosis Chain Testnet',
...@@ -117,6 +126,7 @@ export default NETWORKS; ...@@ -117,6 +126,7 @@ export default NETWORKS;
// subType: 'testnet', // subType: 'testnet',
// group: 'testnets', // group: 'testnets',
// isAccountSupported: true, // isAccountSupported: true,
// currency: 'xDAI',
// }, // },
// { // {
// name: 'POA Sokol', // name: 'POA Sokol',
...@@ -125,6 +135,7 @@ export default NETWORKS; ...@@ -125,6 +135,7 @@ export default NETWORKS;
// subType: 'sokol', // subType: 'sokol',
// group: 'testnets', // group: 'testnets',
// chainId: 77, // chainId: 77,
// currency: 'SPOA',
// }, // },
// { // {
// name: 'ARTIS Σ1', // name: 'ARTIS Σ1',
...@@ -132,6 +143,7 @@ export default NETWORKS; ...@@ -132,6 +143,7 @@ export default NETWORKS;
// subType: 'sigma1', // subType: 'sigma1',
// group: 'other', // group: 'other',
// chainId: 246529, // chainId: 246529,
// currency: 'ATS',
// }, // },
// { // {
// name: 'LUKSO L14', // name: 'LUKSO L14',
...@@ -140,5 +152,13 @@ export default NETWORKS; ...@@ -140,5 +152,13 @@ export default NETWORKS;
// subType: 'l14', // subType: 'l14',
// group: 'other', // group: 'other',
// chainId: 22, // chainId: 22,
// currency: 'LYX',
// },
// {
// name: 'Astar',
// type: 'astar',
// group: 'other',
// chainId: 22,
// currency: 'ASTR',
// }, // },
// ]); // ]);
import type { GetStaticPaths } from 'next';
export const getStaticPaths: GetStaticPaths = async() => {
return { paths: [], fallback: true };
};
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
"format-svg": "./node_modules/.bin/svgo -r ./icons" "format-svg": "./node_modules/.bin/svgo -r ./icons"
}, },
"dependencies": { "dependencies": {
"@chakra-ui/icons": "^2.0.2",
"@chakra-ui/react": "2.3.1", "@chakra-ui/react": "2.3.1",
"@chakra-ui/theme-tools": "^2.0.2", "@chakra-ui/theme-tools": "^2.0.2",
"@emotion/react": "^11", "@emotion/react": "^11",
......
...@@ -19,7 +19,7 @@ const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => { ...@@ -19,7 +19,7 @@ const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
return ( return (
<> <>
<Head><title>{ title }</title></Head> <Head><title>{ title }</title></Head>
<PrivateTags tab="address"/> <PrivateTags tab="private_tags_address"/>
</> </>
); );
}; };
......
...@@ -19,7 +19,7 @@ const TransactionTagsPage: NextPage<Props> = ({ pageParams }: Props) => { ...@@ -19,7 +19,7 @@ const TransactionTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
return ( return (
<> <>
<Head><title>{ title }</title></Head> <Head><title>{ title }</title></Head>
<PrivateTags tab="transaction"/> <PrivateTags tab="private_tags_tx"/>
</> </>
); );
}; };
......
...@@ -2,13 +2,13 @@ import Head from 'next/head'; ...@@ -2,13 +2,13 @@ import Head from 'next/head';
import React from 'react'; import React from 'react';
import Apps from 'ui/pages/Apps'; import Apps from 'ui/pages/Apps';
import Page from 'ui/shared/Page'; import Page from 'ui/shared/Page/Page';
import PageHeader from 'ui/shared/PageHeader'; import PageTitle from 'ui/shared/Page/PageTitle';
const AppsPage = () => { const AppsPage = () => {
return ( return (
<Page> <Page>
<PageHeader text="Apps"/> <PageTitle text="Apps"/>
<Head><title>Apps</title></Head> <Head><title>Apps</title></Head>
<Apps/> <Apps/>
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import App from 'ui/pages/App';
const AppPage: NextPage = () => {
return (
<>
<Head><title>App Card Page</title></Head>
<App/>
</>
);
};
export default AppPage;
export { getStaticPaths } from 'lib/next/apps/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code } from '@chakra-ui/react';
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import { useRouter } from 'next/router'; import Head from 'next/head';
import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
import * as cookies from 'lib/cookies'; import Home from 'ui/pages/Home';
import useNetwork from 'lib/hooks/useNetwork';
import useToast from 'lib/hooks/useToast';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
const Home: NextPage = () => {
const router = useRouter();
const selectedNetwork = useNetwork();
const toast = useToast();
const [ isFormVisible, setFormVisibility ] = React.useState(false);
const [ token, setToken ] = React.useState('');
React.useEffect(() => {
const token = cookies.get(cookies.NAMES.API_TOKEN);
setFormVisibility(Boolean(!token && selectedNetwork?.isAccountSupported));
}, [ selectedNetwork?.isAccountSupported ]);
const handleTokenChange = React.useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
setToken(event.target.value);
}, []);
const handleSetTokenClick = React.useCallback(() => {
cookies.set(cookies.NAMES.API_TOKEN, token);
setToken('');
toast({
position: 'top-right',
title: 'Success 🥳',
description: 'Successfully set cookie',
status: 'success',
variant: 'subtle',
isClosable: true,
onCloseComplete: () => {
setFormVisibility(false);
},
});
}, [ toast, token ]);
const prodUrl = new URL(`/${ router.query.network_type }/${ router.query.network_sub_type }`, 'https://blockscout.com').toString();
const HomePage: NextPage = () => {
return ( return (
<Page> <>
<VStack gap={ 4 } alignItems="flex-start" maxW="800px"> <Head><title>Home Page</title></Head>
<PageHeader text={ <Home/>
`Home Page for ${ selectedNetwork?.name } network` </>
}/>
{ /* will be deleted when we move to new CI */ }
{ isFormVisible && (
<>
<Alert status="error" flexDirection="column" alignItems="flex-start">
<AlertTitle fontSize="md">
!!! Temporary solution for authentication !!!
</AlertTitle>
<AlertDescription mt={ 3 }>
To Sign in go to <Link href={ prodUrl } target="_blank">{ prodUrl }</Link> first, sign in there, copy obtained API token from cookie
<Code ml={ 1 }>{ cookies.NAMES.API_TOKEN }</Code> and paste it in the form below. After submitting the form you should be successfully
authenticated in current environment
</AlertDescription>
</Alert>
<Textarea value={ token } onChange={ handleTokenChange } placeholder="API token"/>
<Button onClick={ handleSetTokenClick }>Set cookie</Button>
</>
) }
</VStack>
</Page>
); );
}; };
export default Home; export default HomePage;
export { getStaticPaths } from 'lib/next/getStaticPaths'; export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps'; export { getStaticProps } from 'lib/next/getStaticProps';
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Transactions from 'ui/pages/Transactions';
type PageParams = {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
return (
<>
<Head><title>{ title }</title></Head>
<Transactions tab="txs_pending"/>
</>
);
};
export default AddressTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -11,7 +11,7 @@ type Props = { ...@@ -11,7 +11,7 @@ type Props = {
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => { const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return ( return (
<TransactionNextPage tab="details" pageParams={ pageParams }/> <TransactionNextPage tab="tx_index" pageParams={ pageParams }/>
); );
}; };
......
...@@ -10,7 +10,7 @@ type Props = { ...@@ -10,7 +10,7 @@ type Props = {
} }
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => { const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return <TransactionNextPage pageParams={ pageParams } tab="internal_txn"/>; return <TransactionNextPage pageParams={ pageParams } tab="tx_internal"/>;
}; };
export default TransactionPage; export default TransactionPage;
......
...@@ -10,7 +10,7 @@ type Props = { ...@@ -10,7 +10,7 @@ type Props = {
} }
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => { const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return <TransactionNextPage pageParams={ pageParams } tab="logs"/>; return <TransactionNextPage pageParams={ pageParams } tab="tx_logs"/>;
}; };
export default TransactionPage; export default TransactionPage;
......
...@@ -10,7 +10,7 @@ type Props = { ...@@ -10,7 +10,7 @@ type Props = {
} }
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => { const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return <TransactionNextPage pageParams={ pageParams } tab="raw_trace"/>; return <TransactionNextPage pageParams={ pageParams } tab="tx_raw_trace"/>;
}; };
export default TransactionPage; export default TransactionPage;
......
...@@ -10,7 +10,7 @@ type Props = { ...@@ -10,7 +10,7 @@ type Props = {
} }
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => { const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return <TransactionNextPage pageParams={ pageParams } tab="state"/>; return <TransactionNextPage pageParams={ pageParams } tab="tx_state"/>;
}; };
export default TransactionPage; export default TransactionPage;
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Transactions from 'ui/pages/Transactions';
type PageParams = {
network_type: string;
network_sub_type: string;
}
type Props = {
pageParams: PageParams;
}
const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
const title = getNetworkTitle(pageParams || {});
return (
<>
<Head><title>{ title }</title></Head>
<Transactions tab="txs_validated"/>
</>
);
};
export default AddressTagsPage;
export { getStaticPaths } from 'lib/next/account/getStaticPaths';
export { getStaticProps } from 'lib/next/getStaticProps';
...@@ -47,7 +47,7 @@ const variantOutline = defineStyle((props) => { ...@@ -47,7 +47,7 @@ const variantOutline = defineStyle((props) => {
const isGrayTheme = c === 'gray' || c === 'gray-dark'; const isGrayTheme = c === 'gray' || c === 'gray-dark';
const color = isGrayTheme ? mode('blackAlpha.800', 'whiteAlpha.800')(props) : mode(`${ c }.600`, `${ c }.300`)(props); const color = isGrayTheme ? mode('blackAlpha.800', 'whiteAlpha.800')(props) : mode(`${ c }.600`, `${ c }.300`)(props);
const borderColor = isGrayTheme ? mode('gray.200', 'gray.600')(props) : mode(`${ c }.600`, `${ c }.300`)(props); const borderColor = isGrayTheme ? mode('gray.300', 'gray.600')(props) : mode(`${ c }.600`, `${ c }.300`)(props);
const activeBg = isGrayTheme ? mode('blue.50', 'gray.600')(props) : mode(`${ c }.50`, 'gray.600')(props); const activeBg = isGrayTheme ? mode('blue.50', 'gray.600')(props) : mode(`${ c }.50`, 'gray.600')(props);
const activeColor = (() => { const activeColor = (() => {
if (c === 'gray') { if (c === 'gray') {
...@@ -106,15 +106,61 @@ const variantSimple = defineStyle((props) => { ...@@ -106,15 +106,61 @@ const variantSimple = defineStyle((props) => {
}; };
}); });
const variantGhost = defineStyle((props) => {
const { colorScheme: c } = props;
const activeBg = mode(`${ c }.50`, 'gray.800')(props);
return {
bg: 'transparent',
color: mode(`${ c }.700`, 'gray.400')(props),
_active: {
color: mode(`${ c }.700`, 'gray.50')(props),
bg: mode(`${ c }.50`, 'gray.800')(props),
},
_hover: {
color: `${ c }.400`,
_active: {
bg: props.isActive ? activeBg : 'transparent',
color: mode(`${ c }.700`, 'gray.50')(props),
},
},
};
});
const variantSubtle = defineStyle((props) => {
const { colorScheme: c } = props;
if (c === 'gray') {
return {
bg: mode('blackAlpha.200', 'whiteAlpha.200')(props),
color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
_hover: {
color: 'blue.400',
},
};
}
return {
bg: `${ c }.100`,
color: `${ c }.600`,
_hover: {
color: 'blue.400',
},
};
});
const variants = { const variants = {
solid: variantSolid, solid: variantSolid,
outline: variantOutline, outline: variantOutline,
simple: variantSimple, simple: variantSimple,
ghost: variantGhost,
subtle: variantSubtle,
}; };
const baseStyle = defineStyle({ const baseStyle = defineStyle({
fontWeight: 600, fontWeight: 600,
borderRadius: 'base', borderRadius: 'base',
overflow: 'hidden',
}); });
const sizes = { const sizes = {
......
...@@ -53,6 +53,19 @@ const sizes = { ...@@ -53,6 +53,19 @@ const sizes = {
fontWeight: 500, fontWeight: 500,
}, },
}), }),
xs: definePartsStyle({
th: {
px: '6px',
py: '10px',
fontSize: 'sm',
},
td: {
px: '6px',
py: 6,
fontSize: 'sm',
fontWeight: 500,
},
}),
}; };
const variants = { const variants = {
......
...@@ -23,11 +23,8 @@ const sizes = { ...@@ -23,11 +23,8 @@ const sizes = {
minH: 6, minH: 6,
minW: 6, minW: 6,
fontSize: 'sm', fontSize: 'sm',
lineHeight: 'sm',
px: 2, px: 2,
py: '2px', py: '2px',
},
label: {
lineHeight: 5, lineHeight: 5,
}, },
}), }),
......
...@@ -7,6 +7,7 @@ const global = (props: StyleFunctionProps) => ({ ...@@ -7,6 +7,7 @@ const global = (props: StyleFunctionProps) => ({
body: { body: {
bg: mode('white', 'black')(props), bg: mode('white', 'black')(props),
...getDefaultTransitionProps(), ...getDefaultTransitionProps(),
'-webkit-tap-highlight-color': 'transparent',
}, },
form: { form: {
w: '100%', w: '100%',
......
export type TxInternalsType = 'call' | 'delegate_call' | 'static_call' | 'create' | 'create2' | 'self_destruct' | 'reward'
export type Sort = 'val-desc' | 'val-asc' | 'fee-desc' | 'fee-asc' | undefined;
...@@ -4,8 +4,9 @@ export type NetworkGroup = 'mainnets' | 'testnets' | 'other'; ...@@ -4,8 +4,9 @@ export type NetworkGroup = 'mainnets' | 'testnets' | 'other';
export interface Network { export interface Network {
name: string; name: string;
// https://chainlist.org/ chainId: number; // https://chainlist.org/
chainId?: number; currency: string;
nativeTokenAddress: string;
shortName?: string; shortName?: string;
// basePath = /<type>/<subType>, e.g. /xdai/mainnet // basePath = /<type>/<subType>, e.g. /xdai/mainnet
type: string; type: string;
...@@ -14,4 +15,5 @@ export interface Network { ...@@ -14,4 +15,5 @@ export interface Network {
icon?: FunctionComponent<SVGAttributes<SVGElement>> | string; icon?: FunctionComponent<SVGAttributes<SVGElement>> | string;
logo?: FunctionComponent<SVGAttributes<SVGElement>> | string; logo?: FunctionComponent<SVGAttributes<SVGElement>> | string;
isAccountSupported?: boolean; isAccountSupported?: boolean;
assetsNamePath?: string;
} }
import { Box, Heading, Image, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, LinkOverlay, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import type { MouseEvent } from 'react';
import React, { useCallback } from 'react';
import type { AppItemPreview } from 'types/client/apps'; import type { AppItemPreview } from 'types/client/apps';
const AppCard = ({ title, logo, shortDescription, categories }: AppItemPreview) => { import northEastIcon from 'icons/arrows/north-east.svg';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
interface Props extends AppItemPreview {
onInfoClick: (id: string) => void;
isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
}
const AppCard = ({ id,
title,
logo,
shortDescription,
categories,
onInfoClick,
isFavorite,
onFavoriteClick,
}: Props) => {
const categoriesLabel = categories.map(c => c.name).join(', '); const categoriesLabel = categories.map(c => c.name).join(', ');
const handleInfoClick = useCallback((event: MouseEvent) => {
event.preventDefault();
onInfoClick(id);
}, [ onInfoClick, id ]);
const handleFavoriteClick = useCallback(() => {
onFavoriteClick(id, isFavorite);
}, [ onFavoriteClick, id, isFavorite ]);
return ( return (
<Box <LinkBox
borderRadius={{ base: 'none', sm: 'md' }} _hover={{
boxShadow: 'md',
}}
_focusWithin={{
boxShadow: 'md',
}}
borderRadius="md"
height="100%" height="100%"
padding={{ base: '16px', sm: '20px' }} padding={{ base: 3, sm: '20px' }}
boxShadow={ `0 0 0 1px ${ useColorModeValue('var(--chakra-colors-gray-200)', 'var(--chakra-colors-gray-600)') }` } border="1px"
borderColor={ useColorModeValue('gray.200', 'gray.600') }
> >
<Box overflow="hidden" height="100%"> <Box
display={{ base: 'grid', sm: 'block' }}
gridTemplateColumns={{ base: '64px 1fr', sm: '1fr' }}
gridTemplateRows={{ base: '20px 20px auto', sm: 'none' }}
gridRowGap={{ base: 2, sm: 'none' }}
gridColumnGap={{ base: 4, sm: 'none' }}
height="100%"
>
<Box <Box
gridRow={{ base: '1 / 4', sm: 'auto' }}
marginBottom={ 4 } marginBottom={ 4 }
w={{ base: '64px', sm: '96px' }} w={{ base: '64px', sm: '96px' }}
h={{ base: '64px', sm: '96px' }} h={{ base: '64px', sm: '96px' }}
...@@ -26,12 +70,17 @@ const AppCard = ({ title, logo, shortDescription, categories }: AppItemPreview) ...@@ -26,12 +70,17 @@ const AppCard = ({ title, logo, shortDescription, categories }: AppItemPreview)
</Box> </Box>
<Heading <Heading
gridColumn={{ base: 2, sm: 'auto' }}
as="h3" as="h3"
marginBottom={ 2 } marginBottom={ 2 }
fontSize={{ base: 'sm', sm: 'lg' }} fontSize={{ base: 'sm', sm: 'lg' }}
fontWeight="semibold" fontWeight="semibold"
> >
{ title } <LinkOverlay
href="#"
>
{ title }
</LinkOverlay>
</Heading> </Heading>
<Text <Text
...@@ -49,9 +98,53 @@ const AppCard = ({ title, logo, shortDescription, categories }: AppItemPreview) ...@@ -49,9 +98,53 @@ const AppCard = ({ title, logo, shortDescription, categories }: AppItemPreview)
> >
{ shortDescription } { shortDescription }
</Text> </Text>
<Box
position="absolute"
right={{ base: 3, sm: '20px' }}
bottom={{ base: 3, sm: '20px' }}
paddingTop={ 1 }
paddingLeft={ 8 }
bgGradient={ `linear(to-r, transparent, ${ useColorModeValue('white', 'black') } 20%)` }
>
<Link
fontSize={{ base: 'xs', sm: 'sm' }}
display="flex"
alignItems="center"
paddingRight={{ sm: 2 }}
maxW="100%"
overflow="hidden"
href="#"
onClick={ handleInfoClick }
>
More
<Icon
as={ northEastIcon }
marginLeft={ 1 }
/>
</Link>
</Box>
<IconButton
position="absolute"
right={{ base: 3, sm: '20px' }}
top={{ base: 3, sm: '20px' }}
aria-label="Mark as favorite"
title="Mark as favorite"
variant="ghost"
colorScheme="gray"
w={ 9 }
h={ 8 }
onClick={ handleFavoriteClick }
icon={ isFavorite ?
<Icon as={ starFilledIcon } w={ 4 } h={ 4 } color="yellow.400"/> :
<Icon as={ starOutlineIcon } w={ 4 } h={ 4 } color="gray.300"/>
}
/>
</Box> </Box>
</Box> </LinkBox>
); );
}; };
export default AppCard; export default React.memo(AppCard);
import { Grid, GridItem, VisuallyHidden, Heading } from '@chakra-ui/react'; import { Grid, GridItem, VisuallyHidden, Heading } from '@chakra-ui/react';
import React from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import type { AppItemPreview } from 'types/client/apps'; import type { AppItemPreview } from 'types/client/apps';
import { apos } from 'lib/html-entities';
import AppCard from 'ui/apps/AppCard'; import AppCard from 'ui/apps/AppCard';
import EmptySearchResult from 'ui/apps/EmptySearchResult'; import EmptySearchResult from 'ui/apps/EmptySearchResult';
import AppModal from './AppModal';
type Props = { type Props = {
apps: Array<AppItemPreview>; apps: Array<AppItemPreview>;
onAppClick: (id: string) => void;
displayedAppId: string | null;
onModalClose: () => void;
}
function getFavoriteApps() {
try {
return JSON.parse(localStorage.getItem('favoriteApps') || '[]');
} catch (e) {
return [];
}
} }
const AppList = ({ apps }: Props) => { const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => {
const [ favoriteApps, setFavoriteApps ] = useState<Array<string>>([]);
const handleFavoriteClick = useCallback((id: string, isFavorite: boolean) => {
const favoriteApps = getFavoriteApps();
if (isFavorite) {
const result = favoriteApps.filter((appId: string) => appId !== id);
setFavoriteApps(result);
localStorage.setItem('favoriteApps', JSON.stringify(result));
} else {
favoriteApps.push(id);
localStorage.setItem('favoriteApps', JSON.stringify(favoriteApps));
setFavoriteApps(favoriteApps);
}
}, [ ]);
useEffect(() => {
setFavoriteApps(getFavoriteApps());
}, [ ]);
return ( return (
<> <>
<VisuallyHidden> <VisuallyHidden>
...@@ -20,32 +54,43 @@ const AppList = ({ apps }: Props) => { ...@@ -20,32 +54,43 @@ const AppList = ({ apps }: Props) => {
{ apps.length > 0 ? ( { apps.length > 0 ? (
<Grid <Grid
templateColumns={{ templateColumns={{
base: 'repeat(auto-fill, minmax(160px, 1fr))', sm: 'repeat(auto-fill, minmax(170px, 1fr))',
sm: 'repeat(auto-fill, minmax(200px, 1fr))',
lg: 'repeat(auto-fill, minmax(260px, 1fr))', lg: 'repeat(auto-fill, minmax(260px, 1fr))',
}} }}
autoRows="1fr" autoRows="1fr"
gap={{ base: '1px', sm: '24px' }} gap={{ base: '16px', sm: '24px' }}
> >
{ apps.map((app) => ( { apps.map((app) => (
<GridItem <GridItem
key={ app.id } key={ app.id }
> >
<AppCard <AppCard
onInfoClick={ onAppClick }
id={ app.id } id={ app.id }
title={ app.title } title={ app.title }
logo={ app.logo } logo={ app.logo }
shortDescription={ app.shortDescription } shortDescription={ app.shortDescription }
categories={ app.categories } categories={ app.categories }
isFavorite={ favoriteApps.includes(app.id) }
onFavoriteClick={ handleFavoriteClick }
/> />
</GridItem> </GridItem>
)) } )) }
</Grid> </Grid>
) : ( ) : (
<EmptySearchResult/> <EmptySearchResult text={ `Couldn${ apos }t find an app that matches your filter query.` }/>
) }
{ displayedAppId && (
<AppModal
id={ displayedAppId }
onClose={ onModalClose }
isFavorite={ favoriteApps.includes(displayedAppId) }
onFavoriteClick={ handleFavoriteClick }
/>
) } ) }
</> </>
); );
}; };
export default AppList; export default React.memo(AppList);
import { LinkIcon, StarIcon } from '@chakra-ui/icons';
import { import {
Box, Button, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody, Box, Button, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody,
ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text,
...@@ -9,29 +8,27 @@ import React, { useCallback } from 'react'; ...@@ -9,29 +8,27 @@ import React, { useCallback } from 'react';
import type { AppCategory, AppItemOverview } from 'types/client/apps'; import type { AppCategory, AppItemOverview } from 'types/client/apps';
import { TEMPORARY_DEMO_APPS } from 'data/apps'; import { TEMPORARY_DEMO_APPS } from 'data/apps';
import linkIcon from 'icons/link.svg';
import ghIcon from 'icons/social/git.svg'; import ghIcon from 'icons/social/git.svg';
import tgIcon from 'icons/social/telega.svg'; import tgIcon from 'icons/social/telega.svg';
import twIcon from 'icons/social/tweet.svg'; import twIcon from 'icons/social/tweet.svg';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg'; import starOutlineIcon from 'icons/star_outline.svg';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
type Props = { type Props = {
id: string | null; id: string;
onClose: () => void; onClose: () => void;
isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
} }
const AppModal = ({ const AppModal = ({
id, id,
onClose, onClose,
isFavorite,
onFavoriteClick,
}: Props) => { }: Props) => {
const handleFavorite = useCallback(() => {
// TODO: implement
}, []);
if (!id) {
return null;
}
const { const {
title, title,
author, author,
...@@ -45,8 +42,6 @@ const AppModal = ({ ...@@ -45,8 +42,6 @@ const AppModal = ({
categories, categories,
} = TEMPORARY_DEMO_APPS.find(app => app.id === id) as AppItemOverview; } = TEMPORARY_DEMO_APPS.find(app => app.id === id) as AppItemOverview;
const isFavorite = false;
const socialLinks = [ const socialLinks = [
Boolean(telegram) && { Boolean(telegram) && {
icon: tgIcon, icon: tgIcon,
...@@ -62,6 +57,10 @@ const AppModal = ({ ...@@ -62,6 +57,10 @@ const AppModal = ({
}, },
].filter(Boolean) as Array<{ icon: FunctionComponent; url: string }>; ].filter(Boolean) as Array<{ icon: FunctionComponent; url: string }>;
const handleFavoriteClick = useCallback(() => {
onFavoriteClick(id, isFavorite);
}, [ onFavoriteClick, id, isFavorite ]);
return ( return (
<Modal <Modal
isOpen={ Boolean(id) } isOpen={ Boolean(id) }
...@@ -135,10 +134,10 @@ const AppModal = ({ ...@@ -135,10 +134,10 @@ const AppModal = ({
colorScheme="gray" colorScheme="gray"
w={ 9 } w={ 9 }
h={ 8 } h={ 8 }
onClick={ handleFavorite } onClick={ handleFavoriteClick }
icon={ isFavorite ? icon={ isFavorite ?
<Icon as={ StarIcon } w={ 4 } h={ 4 } color="yellow.400"/> : <Icon as={ starFilledIcon } w={ 4 } h={ 4 } color="yellow.400"/> :
<Icon as={ starOutlineIcon } w={ 4 } h={ 4 }/> } <Icon as={ starOutlineIcon } w={ 4 } h={ 4 } color="gray.300"/> }
/> />
</Box> </Box>
</Box> </Box>
...@@ -188,10 +187,10 @@ const AppModal = ({ ...@@ -188,10 +187,10 @@ const AppModal = ({
overflow="hidden" overflow="hidden"
> >
<Icon <Icon
as={ LinkIcon } as={ linkIcon }
display="inline" display="inline"
verticalAlign="baseline" verticalAlign="baseline"
boxSize={ 3 } boxSize="18px"
marginRight={ 2 } marginRight={ 2 }
/> />
......
...@@ -2,9 +2,12 @@ import { Box, Heading, Icon, Text } from '@chakra-ui/react'; ...@@ -2,9 +2,12 @@ import { Box, Heading, Icon, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import emptyIcon from 'icons/empty_search_result.svg'; import emptyIcon from 'icons/empty_search_result.svg';
import { apos } from 'lib/html-entities';
const EmptySearchResult = () => { interface Props {
text: string;
}
const EmptySearchResult = ({ text }: Props) => {
return ( return (
<Box <Box
display="flex" display="flex"
...@@ -31,7 +34,7 @@ const EmptySearchResult = () => { ...@@ -31,7 +34,7 @@ const EmptySearchResult = () => {
variant="secondary" variant="secondary"
align="center" align="center"
> >
Couldn{ apos }t find an app that matches your filter query. { text }
</Text> </Text>
</Box> </Box>
); );
......
...@@ -12,8 +12,8 @@ import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem'; ...@@ -12,8 +12,8 @@ import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem';
import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable'; import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable';
import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal'; import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import Page from 'ui/shared/Page'; import Page from 'ui/shared/Page/Page';
import PageHeader from 'ui/shared/PageHeader'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile'; import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
...@@ -128,7 +128,7 @@ const ApiKeysPage: React.FC = () => { ...@@ -128,7 +128,7 @@ const ApiKeysPage: React.FC = () => {
return ( return (
<Page> <Page>
<Box h="100%"> <Box h="100%">
<PageHeader text="API keys"/> <PageTitle text="API keys"/>
{ content } { content }
</Box> </Box>
</Page> </Page>
......
import { Center, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import Page from 'ui/shared/Page/Page';
const App = () => {
return (
<Page wrapChildren={ false }>
<Center as="main" bgColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } h="100%" paddingTop={{ base: '138px', lg: 0 }}>
3rd party app content
</Center>
</Page>
);
};
export default App;
...@@ -5,15 +5,18 @@ import type { AppItemOverview } from 'types/client/apps'; ...@@ -5,15 +5,18 @@ import type { AppItemOverview } from 'types/client/apps';
import { TEMPORARY_DEMO_APPS } from 'data/apps'; import { TEMPORARY_DEMO_APPS } from 'data/apps';
import AppList from 'ui/apps/AppList'; import AppList from 'ui/apps/AppList';
import AppModal from 'ui/apps/AppModal'; import FilterInput from 'ui/shared/FilterInput';
import FilterInput from 'ui/apps/FilterInput';
const defaultDisplayedApps = [ ...TEMPORARY_DEMO_APPS ] const defaultDisplayedApps = [ ...TEMPORARY_DEMO_APPS ]
.sort((a, b) => a.title.localeCompare(b.title)); .sort((a, b) => a.title.localeCompare(b.title));
const Apps = () => { const Apps = () => {
const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>(defaultDisplayedApps); const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>(defaultDisplayedApps);
const [ displayedAppId, setDisplayedAppId ] = useState<string | null>('component'); const [ displayedAppId, setDisplayedAppId ] = useState<string | null>(null);
const showAppInfo = useCallback((id: string) => {
setDisplayedAppId(id);
}, []);
const filterApps = (q: string) => { const filterApps = (q: string) => {
const apps = displayedApps const apps = displayedApps
...@@ -29,11 +32,12 @@ const Apps = () => { ...@@ -29,11 +32,12 @@ const Apps = () => {
return ( return (
<> <>
<FilterInput onChange={ debounceFilterApps }/> <FilterInput onChange={ debounceFilterApps } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/>
<AppList apps={ displayedApps }/> <AppList
<AppModal apps={ displayedApps }
id={ displayedAppId } onAppClick={ showAppInfo }
onClose={ clearDisplayedAppId } displayedAppId={ displayedAppId }
onModalClose={ clearDisplayedAppId }
/> />
</> </>
); );
......
...@@ -11,8 +11,8 @@ import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem'; ...@@ -11,8 +11,8 @@ import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem';
import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable'; import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable';
import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal'; import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import Page from 'ui/shared/Page'; import Page from 'ui/shared/Page/Page';
import PageHeader from 'ui/shared/PageHeader'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile'; import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
...@@ -116,7 +116,7 @@ const CustomAbiPage: React.FC = () => { ...@@ -116,7 +116,7 @@ const CustomAbiPage: React.FC = () => {
return ( return (
<Page> <Page>
<Box h="100%"> <Box h="100%">
<PageHeader text="Custom ABI"/> <PageTitle text="Custom ABI"/>
{ content } { content }
</Box> </Box>
</Page> </Page>
......
import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import type { ChangeEvent } from 'react';
import React from 'react';
import * as cookies from 'lib/cookies';
import useNetwork from 'lib/hooks/useNetwork';
import useToast from 'lib/hooks/useToast';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const Home = () => {
const router = useRouter();
const selectedNetwork = useNetwork();
const toast = useToast();
const [ isFormVisible, setFormVisibility ] = React.useState(false);
const [ token, setToken ] = React.useState('');
React.useEffect(() => {
const token = cookies.get(cookies.NAMES.API_TOKEN);
setFormVisibility(Boolean(!token && selectedNetwork?.isAccountSupported));
}, [ selectedNetwork?.isAccountSupported ]);
const handleTokenChange = React.useCallback((event: ChangeEvent<HTMLTextAreaElement>) => {
setToken(event.target.value);
}, []);
const handleSetTokenClick = React.useCallback(() => {
cookies.set(cookies.NAMES.API_TOKEN, token);
setToken('');
toast({
position: 'top-right',
title: 'Success 🥳',
description: 'Successfully set cookie',
status: 'success',
variant: 'subtle',
isClosable: true,
onCloseComplete: () => {
setFormVisibility(false);
},
});
}, [ toast, token ]);
const prodUrl = new URL(`/${ router.query.network_type }/${ router.query.network_sub_type }`, 'https://blockscout.com').toString();
return (
<Page>
<VStack gap={ 4 } alignItems="flex-start" maxW="800px">
<PageTitle text={
`Home Page for ${ selectedNetwork?.name } network`
}/>
{ /* will be deleted when we move to new CI */ }
{ isFormVisible && (
<>
<Alert status="error" flexDirection="column" alignItems="flex-start">
<AlertTitle fontSize="md">
!!! Temporary solution for authentication !!!
</AlertTitle>
<AlertDescription mt={ 3 }>
To Sign in go to <Link href={ prodUrl } target="_blank">{ prodUrl }</Link> first, sign in there, copy obtained API token from cookie
<Code ml={ 1 }>{ cookies.NAMES.API_TOKEN }</Code> and paste it in the form below. After submitting the form you should be successfully
authenticated in current environment
</AlertDescription>
</Alert>
<Textarea value={ token } onChange={ handleTokenChange } placeholder="API token"/>
<Button onClick={ handleSetTokenClick }>Set cookie</Button>
</>
) }
</VStack>
</Page>
);
};
export default Home;
...@@ -4,8 +4,8 @@ import React from 'react'; ...@@ -4,8 +4,8 @@ import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page'; import Page from 'ui/shared/Page/Page';
import PageHeader from 'ui/shared/PageHeader'; import PageTitle from 'ui/shared/Page/PageTitle';
import UserAvatar from 'ui/shared/UserAvatar'; import UserAvatar from 'ui/shared/UserAvatar';
const MyProfile = () => { const MyProfile = () => {
...@@ -56,7 +56,7 @@ const MyProfile = () => { ...@@ -56,7 +56,7 @@ const MyProfile = () => {
return ( return (
<Page> <Page>
<PageHeader text="My profile"/> <PageTitle text="My profile"/>
{ content } { content }
</Page> </Page>
); );
......
import { import React from 'react';
Box,
Tab, import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
Tabs,
TabList,
TabPanel,
TabPanels,
} from '@chakra-ui/react';
import React, { useCallback, useState } from 'react';
import useLink from 'lib/link/useLink';
import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags'; import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags';
import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags'; import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags';
import Page from 'ui/shared/Page'; import Page from 'ui/shared/Page/Page';
import PageHeader from 'ui/shared/PageHeader'; import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
const TABS = [ 'address', 'transaction' ] as const;
type TabName = typeof TABS[number]; const TABS: Array<RoutedTab> = [
{ routeName: 'private_tags_address', title: 'Address', component: <PrivateAddressTags/> },
{ routeName: 'private_tags_tx', title: 'Transaction', component: <PrivateTransactionTags/> },
];
type Props = { type Props = {
tab: TabName; tab: RoutedTab['routeName'];
} }
const PrivateTags = ({ tab }: Props) => { const PrivateTags = ({ tab }: Props) => {
const [ , setActiveTab ] = useState<TabName>(tab);
const link = useLink();
const onChangeTab = useCallback((index: number) => {
setActiveTab(TABS[index]);
const newUrl = link(TABS[index] === 'address' ? 'private_tags_address' : 'private_tags_tx');
history.replaceState(history.state, '', newUrl);
}, [ link ]);
return ( return (
<Page> <Page>
<Box h="100%"> <PageTitle text="Private tags"/>
<PageHeader text="Private tags"/> <RoutedTabs tabs={ TABS } defaultActiveTab={ tab }/>
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ onChangeTab } defaultIndex={ TABS.indexOf(tab) }>
<TabList marginBottom={{ base: 6, lg: 8 }}>
<Tab>Address</Tab>
<Tab>Transaction</Tab>
</TabList>
<TabPanels>
<TabPanel padding={ 0 }>
<PrivateAddressTags/>
</TabPanel>
<TabPanel padding={ 0 }>
<PrivateTransactionTags/>
</TabPanel>
</TabPanels>
</Tabs>
</Box>
</Page> </Page>
); );
}; };
......
import { ArrowBackIcon } from '@chakra-ui/icons'; import { Link, Text, Icon } from '@chakra-ui/react';
import { Box, Link, Text } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { animateScroll } from 'react-scroll'; import { animateScroll } from 'react-scroll';
import type { PublicTag } from 'types/api/account'; import type { PublicTag } from 'types/api/account';
import eastArrowIcon from 'icons/arrows/east.svg';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import PublicTagsData from 'ui/publicTags/PublicTagsData'; import PublicTagsData from 'ui/publicTags/PublicTagsData';
import PublicTagsForm from 'ui/publicTags/PublicTagsForm/PublicTagsForm'; import PublicTagsForm from 'ui/publicTags/PublicTagsForm/PublicTagsForm';
import Page from 'ui/shared/Page'; import Page from 'ui/shared/Page/Page';
import PageHeader from 'ui/shared/PageHeader'; import PageTitle from 'ui/shared/Page/PageTitle';
type TScreen = 'data' | 'form'; type TScreen = 'data' | 'form';
...@@ -77,16 +77,14 @@ const PublicTagsComponent: React.FC = () => { ...@@ -77,16 +77,14 @@ const PublicTagsComponent: React.FC = () => {
return ( return (
<Page> <Page>
<Box h="100%"> { isMobile && screen === 'form' && (
{ isMobile && screen === 'form' && ( <Link display="inline-flex" alignItems="center" mb={ 6 } onClick={ onGoBack }>
<Link display="inline-flex" alignItems="center" mb={ 6 } onClick={ onGoBack }> <Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)"/>
<ArrowBackIcon w={ 6 } h={ 6 }/> <Text variant="inherit" fontSize="sm" ml={ 2 }>Public tags</Text>
<Text variant="inherit" fontSize="sm" ml={ 2 }>Public tags</Text> </Link>
</Link> ) }
) } <PageTitle text={ header }/>
<PageHeader text={ header }/> { content }
{ content }
</Box>
</Page> </Page>
); );
}; };
......
import { import { Flex, Link, Icon } from '@chakra-ui/react';
Tab,
Tabs,
TabList,
TabPanel,
TabPanels,
} from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { RouteName } from 'lib/link/routes'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import eastArrowIcon from 'icons/arrows/east.svg';
import useLink from 'lib/link/useLink'; import useLink from 'lib/link/useLink';
import Page from 'ui/shared/Page'; import ExternalLink from 'ui/shared/ExternalLink';
import PageHeader from 'ui/shared/PageHeader'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TxDetails from 'ui/tx/TxDetails'; import TxDetails from 'ui/tx/TxDetails';
import TxInternals from 'ui/tx/TxInternals'; import TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs'; import TxLogs from 'ui/tx/TxLogs';
import TxRawTrace from 'ui/tx/TxRawTrace'; import TxRawTrace from 'ui/tx/TxRawTrace';
import TxState from 'ui/tx/TxState'; import TxState from 'ui/tx/TxState';
interface Tab { const TABS: Array<RoutedTab> = [
type: 'details' | 'internal_txn' | 'logs' | 'raw_trace' | 'state'; { routeName: 'tx_index', title: 'Details', component: <TxDetails/> },
name: string; { routeName: 'tx_internal', title: 'Internal txn', component: <TxInternals/> },
path?: string; { routeName: 'tx_logs', title: 'Logs', component: <TxLogs/> },
component?: React.ReactNode; { routeName: 'tx_state', title: 'State', component: <TxState/> },
routeName: RouteName; { routeName: 'tx_raw_trace', title: 'Raw trace', component: <TxRawTrace/> },
}
const TABS: Array<Tab> = [
{ type: 'details', routeName: 'tx_index', name: 'Details', component: <TxDetails/> },
{ type: 'internal_txn', routeName: 'tx_internal', name: 'Internal txn', component: <TxInternals/> },
{ type: 'logs', routeName: 'tx_logs', name: 'Logs', component: <TxLogs/> },
{ type: 'state', routeName: 'tx_state', name: 'State', component: <TxState/> },
{ type: 'raw_trace', routeName: 'tx_raw_trace', name: 'Raw trace', component: <TxRawTrace/> },
]; ];
export interface Props { export interface Props {
tab: Tab['type']; tab: RoutedTab['routeName'];
} }
const TransactionPageContent = ({ tab }: Props) => { const TransactionPageContent = ({ tab }: Props) => {
const [ , setActiveTab ] = React.useState<Tab['type']>(tab);
const router = useRouter();
const link = useLink(); const link = useLink();
const handleTabChange = React.useCallback((index: number) => {
const nextTab = TABS[index];
setActiveTab(nextTab.type);
const newUrl = link(nextTab.routeName, { id: router.query.id as string });
window.history.replaceState(history.state, '', newUrl);
}, [ setActiveTab, link, router.query.id ]);
const defaultIndex = TABS.map(({ type }) => type).indexOf(tab);
return ( return (
<Page> <Page>
<PageHeader text="Transaction details"/> { /* TODO should be shown only when navigating from txs list */ }
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ handleTabChange } defaultIndex={ defaultIndex }> <Link mb={ 6 } display="inline-flex" href={ link('txs_validated') }>
<TabList marginBottom={{ base: 6, lg: 8 }} flexWrap="wrap"> <Icon as={ eastArrowIcon } boxSize={ 6 } mr={ 2 } transform="rotate(180deg)"/>
{ TABS.map((tab) => <Tab key={ tab.type }>{ tab.name }</Tab>) } Transactions
</TabList> </Link>
<TabPanels> <PageTitle text="Transaction details"/>
{ TABS.map((tab) => <TabPanel padding={ 0 } key={ tab.type }>{ tab.component || tab.name }</TabPanel>) } <Flex marginLeft="auto" alignItems="center" flexWrap="wrap" columnGap={ 6 } rowGap={ 3 } mb={ 6 }>
</TabPanels> <ExternalLink title="Open in Tenderly" href="#"/>
</Tabs> <ExternalLink title="Open in Blockchair" href="#"/>
<ExternalLink title="Open in Etherscan" href="#"/>
</Flex>
<RoutedTabs
tabs={ TABS }
defaultActiveTab={ tab }
/>
</Page> </Page>
); );
}; };
......
import {
Box,
} from '@chakra-ui/react';
import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TxsPending from 'ui/txs/TxsPending';
import TxsValidated from 'ui/txs/TxsValidated';
const TABS: Array<RoutedTab> = [
{ routeName: 'txs_validated', title: 'Validated', component: <TxsValidated/> },
{ routeName: 'txs_pending', title: 'Pending', component: <TxsPending/> },
];
type Props = {
tab: RoutedTab['routeName'];
}
const Transactions = ({ tab }: Props) => {
return (
<Page>
<Box h="100%">
<PageTitle text="Transactions"/>
<RoutedTabs tabs={ TABS } defaultActiveTab={ tab }/>
</Box>
</Page>
);
};
export default Transactions;
...@@ -8,8 +8,8 @@ import useFetch from 'lib/hooks/useFetch'; ...@@ -8,8 +8,8 @@ import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page'; import Page from 'ui/shared/Page/Page';
import PageHeader from 'ui/shared/PageHeader'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile'; import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
import AddressModal from 'ui/watchlist/AddressModal/AddressModal'; import AddressModal from 'ui/watchlist/AddressModal/AddressModal';
...@@ -113,7 +113,7 @@ const WatchList: React.FC = () => { ...@@ -113,7 +113,7 @@ const WatchList: React.FC = () => {
return ( return (
<Page> <Page>
<Box h="100%"> <Box h="100%">
<PageHeader text="Watch list"/> <PageTitle text="Watch list"/>
{ content } { content }
</Box> </Box>
</Page> </Page>
......
import { VStack, useColorModeValue } from '@chakra-ui/react'; import { Flex, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
className?: string;
} }
const AccountListItemMobile = ({ children }: Props) => { const AccountListItemMobile = ({ children, className }: Props) => {
return ( return (
<VStack <Flex
gap={ 4 } rowGap={ 6 }
alignItems="flex-start" alignItems="flex-start"
flexDirection="column"
paddingY={ 6 } paddingY={ 6 }
borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderTopWidth="1px" borderTopWidth="1px"
_last={{ _last={{
borderBottomWidth: '1px', borderBottomWidth: '1px',
}} }}
className={ className }
> >
{ children } { children }
</VStack> </Flex>
); );
}; };
export default AccountListItemMobile; export default chakra(AccountListItemMobile);
...@@ -19,7 +19,7 @@ const AddressSnippet = ({ address, subtitle }: Props) => { ...@@ -19,7 +19,7 @@ const AddressSnippet = ({ address, subtitle }: Props) => {
<AddressLink hash={ address } fontWeight="600" ml={ 2 }/> <AddressLink hash={ address } fontWeight="600" ml={ 2 }/>
<CopyToClipboard text={ address } ml={ 1 }/> <CopyToClipboard text={ address } ml={ 1 }/>
</Address> </Address>
{ subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 } ml={{ base: 0, lg: 8 }}>{ subtitle }</Text> } { subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 } ml={ 8 }>{ subtitle }</Text> }
</Box> </Box>
); );
}; };
......
...@@ -13,7 +13,7 @@ interface Props extends HTMLChakraProps<'div'> { ...@@ -13,7 +13,7 @@ interface Props extends HTMLChakraProps<'div'> {
const DetailsInfoItem = ({ title, hint, children, ...styles }: Props) => { const DetailsInfoItem = ({ title, hint, children, ...styles }: Props) => {
return ( return (
<> <>
<GridItem py={ 2 } lineHeight={ 5 } { ...styles } whiteSpace="nowrap"> <GridItem py={{ base: 1, lg: 2 }} lineHeight={ 5 } { ...styles } whiteSpace="nowrap" _notFirst={{ mt: { base: 3, lg: 0 } }}>
<Flex columnGap={ 2 } alignItems="center"> <Flex columnGap={ 2 } alignItems="center">
<Tooltip <Tooltip
label={ hint } label={ hint }
...@@ -24,10 +24,20 @@ const DetailsInfoItem = ({ title, hint, children, ...styles }: Props) => { ...@@ -24,10 +24,20 @@ const DetailsInfoItem = ({ title, hint, children, ...styles }: Props) => {
<Icon as={ infoIcon } boxSize={ 5 }/> <Icon as={ infoIcon } boxSize={ 5 }/>
</Box> </Box>
</Tooltip> </Tooltip>
<Text fontWeight={ 500 }>{ title }</Text> <Text fontWeight={{ base: 700, lg: 500 }}>{ title }</Text>
</Flex> </Flex>
</GridItem> </GridItem>
<GridItem display="flex" alignItems="center" py={ 2 } lineHeight={ 5 } whiteSpace="nowrap" { ...styles }> <GridItem
display="flex"
alignItems="center"
flexWrap="wrap"
rowGap={ 3 }
pl={{ base: 7, lg: 0 }}
py={{ base: 1, lg: 2 }}
lineHeight={ 5 }
whiteSpace="nowrap"
{ ...styles }
>
{ children } { children }
</GridItem> </GridItem>
</> </>
......
import { Link, Icon } from '@chakra-ui/react';
import React from 'react';
import arrowIcon from 'icons/arrows/north-east.svg';
interface Props {
href: string;
title: string;
}
const ExternalLink = ({ href, title }: Props) => {
return (
<Link fontSize="sm" display="inline-flex" alignItems="center" target="_blank" href={ href }>
{ title }
<Icon as={ arrowIcon } boxSize={ 4 }/>
</Link>
);
};
export default React.memo(ExternalLink);
import { Box, Button, Circle, Icon, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import filterIcon from 'icons/filter.svg';
const FilterIcon = <Icon as={ filterIcon } boxSize={ 5 } mr={{ base: 0, lg: 2 }}/>;
interface Props {
isActive: boolean;
appliedFiltersNum?: number;
onClick: () => void;
}
const FilterButton = ({ isActive, appliedFiltersNum, onClick }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const badgeColor = useColorModeValue('white', 'black');
const badgeBgColor = useColorModeValue('blue.700', 'gray.50');
return (
<Button
ref={ ref }
rightIcon={ appliedFiltersNum ? <Circle bg={ badgeBgColor } size={ 5 } color={ badgeColor }>{ appliedFiltersNum }</Circle> : undefined }
size="sm"
fontWeight="500"
variant="outline"
colorScheme="gray-dark"
onClick={ onClick }
isActive={ isActive }
px={ 1.5 }
flexShrink={ 0 }
>
{ FilterIcon }
<Box display={{ base: 'none', lg: 'block' }}>Filter</Box>
</Button>
);
};
export default React.forwardRef(FilterButton);
import { SearchIcon } from '@chakra-ui/icons'; import { Input, InputGroup, InputLeftElement, Icon, useColorModeValue, chakra } from '@chakra-ui/react';
import { Input, InputGroup, InputLeftElement, useColorModeValue } from '@chakra-ui/react';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import searchIcon from 'icons/search.svg';
type Props = { type Props = {
onChange: (q: string) => void; onChange: (searchTerm: string) => void;
className?: string;
size?: 'xs' | 'sm' | 'md' | 'lg';
placeholder: string;
} }
const FilterInput = ({ onChange }: Props) => { const FilterInput = ({ onChange, className, size = 'sm', placeholder }: Props) => {
const [ filterQuery, setFilterQuery ] = useState(''); const [ filterQuery, setFilterQuery ] = useState('');
const handleFilterQueryChange = useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleFilterQueryChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
...@@ -19,22 +23,25 @@ const FilterInput = ({ onChange }: Props) => { ...@@ -19,22 +23,25 @@ const FilterInput = ({ onChange }: Props) => {
return ( return (
<InputGroup <InputGroup
size="sm" size={ size }
className={ className }
> >
<InputLeftElement <InputLeftElement
pointerEvents="none" pointerEvents="none"
> >
<SearchIcon color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/> <Icon as={ searchIcon } color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/>
</InputLeftElement> </InputLeftElement>
<Input <Input
size="sm" size={ size }
value={ filterQuery } value={ filterQuery }
onChange={ handleFilterQueryChange } onChange={ handleFilterQueryChange }
marginBottom={{ base: '4', lg: '6' }} placeholder={ placeholder }
borderWidth="2px"
textOverflow="ellipsis"
/> />
</InputGroup> </InputGroup>
); );
}; };
export default FilterInput; export default chakra(FilterInput);
import { SearchIcon } from '@chakra-ui/icons';
import { Flex, Icon, Button, Circle, InputGroup, InputLeftElement, Input, useColorModeValue } from '@chakra-ui/react';
import type { ChangeEvent } from 'react';
import React from 'react';
import filterIcon from 'icons/filter.svg';
const FilterIcon = <Icon as={ filterIcon } boxSize={ 5 }/>;
const Filters = () => {
const [ isActive, setIsActive ] = React.useState(false);
const [ value, setValue ] = React.useState('');
const handleClick = React.useCallback(() => {
setIsActive(flag => !flag);
}, []);
const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
}, []);
const badgeColor = useColorModeValue('white', 'black');
const badgeBgColor = useColorModeValue('blue.700', 'gray.50');
const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200');
return (
<Flex>
<Button
leftIcon={ FilterIcon }
rightIcon={ isActive ? <Circle bg={ badgeBgColor } size={ 5 } color={ badgeColor }>2</Circle> : undefined }
size="sm"
variant="outline"
colorScheme="gray-dark"
borderWidth="1px"
onClick={ handleClick }
isActive={ isActive }
px={ 1.5 }
>
Filter
</Button>
<InputGroup size="xs" ml={ 3 } maxW="360px">
<InputLeftElement ml={ 1 }>
<SearchIcon w={ 5 } h={ 5 } color={ searchIconColor }/>
</InputLeftElement>
<Input
paddingInlineStart="38px"
placeholder="Search by addresses, hash, method..."
ml="1px"
onChange={ handleChange }
borderColor={ inputBorderColor }
value={ value }
size="xs"
/>
</InputGroup>
</Flex>
);
};
export default Filters;
import { Box, HStack, VStack } from '@chakra-ui/react'; import { Flex } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import PageContent from 'ui/shared/Page/PageContent';
import Header from 'ui/blocks/header/Header'; import Header from 'ui/snippets/header/Header';
import NavigationDesktop from 'ui/blocks/navigation/NavigationDesktop'; import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop';
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
wrapChildren?: boolean;
} }
const Page = ({ children }: Props) => { const Page = ({ children, wrapChildren = true }: Props) => {
const isMobile = useIsMobile();
const router = useRouter(); const router = useRouter();
const fetch = useFetch(); const fetch = useFetch();
...@@ -32,24 +32,18 @@ const Page = ({ children }: Props) => { ...@@ -32,24 +32,18 @@ const Page = ({ children }: Props) => {
} }
}, [ networkType, networkSubType ]); }, [ networkType, networkSubType ]);
const renderedChildren = wrapChildren ? (
<PageContent>{ children }</PageContent>
) : children;
return ( return (
<HStack <Flex w="100%" minH="100vh" alignItems="stretch">
w="100%" <NavigationDesktop/>
minH="100vh" <Flex flexDir="column" width="100%">
alignItems="stretch"
>
{ !isMobile && <NavigationDesktop/> }
<VStack width="100%" paddingX={ isMobile ? 4 : 8 } paddingTop={ isMobile ? 0 : 9 } paddingBottom={ 10 } spacing={ 0 }>
<Header/> <Header/>
<Box { renderedChildren }
as="main" </Flex>
w="100%" </Flex>
paddingTop={ isMobile ? '138px' : '52px' }
>
{ children }
</Box>
</VStack>
</HStack>
); );
}; };
......
import { Box } from '@chakra-ui/react';
import React from 'react';
interface Props {
children: React.ReactNode;
}
const PageContent = ({ children }: Props) => {
return (
<Box
as="main"
w="100%"
paddingX={{ base: 4, lg: 12 }}
paddingBottom={ 10 }
paddingTop={{ base: '138px', lg: 0 }}
>
{ children }
</Box>
);
};
export default PageContent;
import { Heading } from '@chakra-ui/react'; import { Heading } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
const PageHeader = ({ text }: {text: string}) => { const PageTitle = ({ text }: {text: string}) => {
return ( return (
<Heading as="h1" size="lg" marginBottom={{ base: 6, lg: 8 }}>{ text }</Heading> <Heading as="h1" size="lg" marginBottom={{ base: 6, lg: 8 }}>{ text }</Heading>
); );
}; };
export default PageHeader; export default PageTitle;
import { Button, Flex, Input, Icon, IconButton } from '@chakra-ui/react';
import React from 'react';
import arrowIcon from 'icons/arrows/east-mini.svg';
type Props = {
currentPage: number;
maxPage?: number;
}
const MAX_PAGE_DEFAULT = 50;
const Pagination = ({ currentPage, maxPage }: Props) => {
const pageNumber = (
<Flex alignItems="center">
<Button
variant="outline"
colorScheme="gray"
size="sm"
isActive
borderWidth="1px"
fontWeight={ 400 }
mr={ 3 }
h={ 8 }
>
{ currentPage }
</Button>
of
<Button
variant="outline"
colorScheme="gray"
size="sm"
width={ 8 }
borderWidth="1px"
fontWeight={ 400 }
ml={ 3 }
>
{ maxPage || MAX_PAGE_DEFAULT }
</Button>
</Flex>
);
return (
<Flex
fontSize="sm"
width={{ base: '100%', lg: 'auto' }}
justifyContent={{ base: 'space-between', lg: 'unset' }}
alignItems="center"
>
<Flex alignItems="center" justifyContent="space-between" w={{ base: '100%', lg: 'auto' }}>
<IconButton
variant="outline"
size="sm"
aria-label="Next page"
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> }
mr={ 8 }
/>
{ pageNumber }
<IconButton
variant="outline"
size="sm"
aria-label="Next page"
w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> }
ml={ 8 }
/>
</Flex>
<Flex alignItems="center" width="132px" ml={ 16 } display={{ base: 'none', lg: 'flex' }}>
Go to <Input w="84px" size="xs" ml={ 2 }/>
</Flex>
</Flex>
);
};
export default Pagination;
import { Box, Icon, IconButton, chakra } from '@chakra-ui/react';
import React from 'react';
import eastArrow from 'icons/arrows/east-mini.svg';
interface Props {
className?: string;
}
const PrevNext = ({ className }: Props) => {
return (
<Box className={ className }>
<IconButton
aria-label="prev"
icon={ <Icon as={ eastArrow } boxSize={ 6 }/> }
h={ 6 }
borderRadius="sm"
variant="subtle"
colorScheme="gray"
/>
<IconButton
aria-label="next"
icon={ <Icon as={ eastArrow }boxSize={ 6 } transform="rotate(180deg)"/> }
h={ 6 }
borderRadius="sm"
variant="subtle"
colorScheme="gray"
ml="10px"
/>
</Box>
);
};
export default chakra(PrevNext);
import {
Tab,
Tabs,
TabList,
TabPanel,
TabPanels,
} from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system';
import { useRouter } from 'next/router';
import React from 'react';
import type { RoutedTab } from './types';
import useIsMobile from 'lib/hooks/useIsMobile';
import { link } from 'lib/link/link';
import RoutedTabsMenu from './RoutedTabsMenu';
import useAdaptiveTabs from './useAdaptiveTabs';
const hiddenItemStyles: StyleProps = {
position: 'absolute',
top: '-9999px',
left: '-9999px',
visibility: 'hidden',
};
interface Props {
tabs: Array<RoutedTab>;
defaultActiveTab: RoutedTab['routeName'];
}
const RoutedTabs = ({ tabs, defaultActiveTab }: Props) => {
const defaultIndex = tabs.findIndex(({ routeName }) => routeName === defaultActiveTab);
const isMobile = useIsMobile();
const [ activeTab ] = React.useState<number>(defaultIndex);
const { tabsCut, tabsList, tabsRefs, listRef } = useAdaptiveTabs(tabs, isMobile);
const router = useRouter();
const handleTabChange = React.useCallback((index: number) => {
const nextTab = tabs[index];
if (nextTab.routeName) {
const newUrl = link(nextTab.routeName, router.query);
router.push(newUrl, undefined, { shallow: true });
}
}, [ tabs, router ]);
return (
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ handleTabChange } index={ activeTab }>
<TabList
marginBottom={{ base: 6, lg: 12 }}
flexWrap="nowrap"
whiteSpace="nowrap"
ref={ listRef }
overflowY="hidden"
overflowX={{ base: 'auto', lg: undefined }}
overscrollBehaviorX="contain"
css={{
'scroll-snap-type': 'x mandatory',
// hide scrollbar
'&::-webkit-scrollbar': { /* Chromiums */
display: 'none',
},
'-ms-overflow-style': 'none', /* IE and Edge */
'scrollbar-width': 'none', /* Firefox */
}}
>
{ tabsList.map((tab, index) => {
if (!tab.routeName) {
return (
<RoutedTabsMenu
key="menu"
tabs={ tabs }
activeTab={ tabs[activeTab] }
tabsCut={ tabsCut }
isActive={ activeTab >= tabsCut }
styles={ tabsCut < tabs.length ?
// initially our cut is 0 and we don't want to show the menu button too
// but we want to keep it in the tabs row so it won't collapse
// that's why we only change opacity but not the position itself
{ opacity: tabsCut === 0 ? 0 : 1 } :
hiddenItemStyles
}
onItemClick={ handleTabChange }
buttonRef={ tabsRefs[index] }
/>
);
}
return (
<Tab
key={ tab.routeName }
ref={ tabsRefs[index] }
{ ...(index < tabsCut ? {} : hiddenItemStyles) }
scrollSnapAlign="start"
>
{ tab.title }
</Tab>
);
}) }
</TabList>
<TabPanels>
{ tabsList.map((tab) => <TabPanel padding={ 0 } key={ tab.routeName }>{ tab.component }</TabPanel>) }
</TabPanels>
</Tabs>
);
};
export default React.memo(RoutedTabs);
import { Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
Button,
useDisclosure,
} from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system';
import React from 'react';
import type { MenuButton, RoutedTab } from './types';
import { menuButton } from './utils';
interface Props {
tabs: Array<RoutedTab | MenuButton>;
activeTab: RoutedTab;
tabsCut: number;
isActive: boolean;
styles?: StyleProps;
onItemClick: (index: number) => void;
buttonRef: React.RefObject<HTMLButtonElement>;
}
const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, activeTab }: Props) => {
const { isOpen, onClose, onOpen } = useDisclosure();
const handleItemClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
onClose();
const tabIndex = (event.target as HTMLButtonElement).getAttribute('data-index');
if (tabIndex) {
onItemClick(tabsCut + Number(tabIndex));
}
}, [ onClose, onItemClick, tabsCut ]);
return (
<Popover isLazy placement="bottom-end" key="more" isOpen={ isOpen } onClose={ onClose } onOpen={ onOpen } closeDelay={ 0 }>
<PopoverTrigger>
<Button
variant="ghost"
isActive={ isOpen || isActive }
ref={ buttonRef }
{ ...styles }
>
{ menuButton.title }
</Button>
</PopoverTrigger>
<PopoverContent w="auto">
<PopoverBody display="flex" flexDir="column">
{ tabs.slice(tabsCut).map((tab, index) => (
<Button
key={ tab.routeName }
variant="ghost"
onClick={ handleItemClick }
isActive={ activeTab.routeName === tab.routeName }
justifyContent="left"
data-index={ index }
>
{ tab.title }
</Button>
)) }
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(RoutedTabsMenu);
import type { RouteName } from 'lib/link/routes';
export interface RoutedTab {
// for simplicity we use routeName as an id
// if we migrate to non-Next.js router that should be revised
// id: string;
routeName: RouteName | null;
title: string;
component: React.ReactNode;
}
export interface MenuButton {
routeName: null;
title: string;
component: null;
}
import _debounce from 'lodash/debounce';
import React from 'react';
import type { RoutedTab } from './types';
import { menuButton } from './utils';
export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boolean) {
// to avoid flickering we set initial value to 0
// so there will be no displayed tabs initially
const [ tabsCut, setTabsCut ] = React.useState(disabled ? tabs.length : 0);
const [ tabsRefs, setTabsRefs ] = React.useState<Array<React.RefObject<HTMLButtonElement>>>([]);
const listRef = React.useRef<HTMLDivElement>(null);
const calculateCut = React.useCallback(() => {
const listWidth = listRef.current?.getBoundingClientRect().width;
const tabWidths = tabsRefs.map((tab) => tab.current?.getBoundingClientRect().width);
const menuWidth = tabWidths.at(-1);
if (!listWidth || !menuWidth) {
return tabs.length;
}
const { visibleNum } = tabWidths.slice(0, -1).reduce((result, item, index) => {
if (!item) {
return result;
}
if (result.accWidth + item <= listWidth - menuWidth) {
return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item };
}
if (result.accWidth + item <= listWidth && index === tabWidths.length - 2) {
return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item };
}
return result;
}, { visibleNum: 0, accWidth: 0 });
return visibleNum;
}, [ tabs.length, tabsRefs ]);
const tabsList = React.useMemo(() => {
if (disabled) {
return tabs;
}
return [ ...tabs, menuButton ];
}, [ tabs, disabled ]);
React.useEffect(() => {
!disabled && setTabsRefs(tabsList.map((_, index) => tabsRefs[index] || React.createRef()));
// imitate componentDidMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
if (tabsRefs.length > 0) {
setTabsCut(calculateCut());
}
}, [ calculateCut, tabsRefs ]);
React.useEffect(() => {
if (tabsRefs.length === 0) {
return;
}
const resizeHandler = _debounce(() => {
setTabsCut(calculateCut());
}, 100);
const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(document.body);
return function cleanup() {
resizeObserver.unobserve(document.body);
};
}, [ calculateCut, tabsRefs.length ]);
return React.useMemo(() => {
return {
tabsCut,
tabsList,
tabsRefs,
listRef,
};
}, [ tabsList, tabsCut, tabsRefs, listRef ]);
}
import type { MenuButton } from './types';
import { middot } from 'lib/html-entities';
export const menuButton: MenuButton = {
routeName: null,
title: `${ middot }${ middot }${ middot }`,
component: null,
};
import { Icon, IconButton, chakra } from '@chakra-ui/react';
import React from 'react';
import upDownArrow from 'icons/arrows/up-down.svg';
type Props = {
handleSort: () => void;
isSortActive: boolean;
className?: string;
}
const SortButton = ({ handleSort, isSortActive, className }: Props) => {
return (
<IconButton
icon={ <Icon as={ upDownArrow } boxSize={ 5 }/> }
aria-label="sort"
size="sm"
variant="outline"
colorScheme="gray-dark"
minWidth="36px"
onClick={ handleSort }
isActive={ isSortActive }
className={ className }
/>
);
};
export default chakra(SortButton);
import { chakra } from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system';
import React from 'react';
const TextSeparator = (props: StyleProps) => {
return <chakra.span mx={ 3 } { ...props }>|</chakra.span>;
};
export default React.memo(TextSeparator);
import { Center, Icon, Link, Text, chakra } from '@chakra-ui/react';
import React from 'react';
import tokeIcon from 'icons/tokens/toke.svg';
import usdtIcon from 'icons/tokens/usdt.svg';
import useLink from 'lib/link/useLink';
// temporary solution
// don't know where to get icons and addresses yet
const TOKENS = {
USDT: {
fullName: 'Tether USD',
symbol: 'USDT',
icon: usdtIcon,
address: '0x9bD35A17C9C7c8820f89e0277e2046CDC57aCB15',
},
TOKE: {
fullName: 'Tokemak',
symbol: 'TOKE',
icon: tokeIcon,
address: '0x9bD35A17C9C7c8820f89e0277e2046CDC57aCB15',
},
};
interface Props {
symbol: string;
className?: string;
}
const Token = ({ symbol, className }: Props) => {
const token = TOKENS[symbol as keyof typeof TOKENS];
const link = useLink();
if (!token) {
return null;
}
const url = link('token_index', { id: token.address });
return (
<Center className={ className }>
<Icon as={ token.icon } boxSize={ 5 }/>
<Link href={ url } target="_blank" ml={ 1 }>
{ token.fullName }
</Link>
<Text ml={ 1 }>({ token.symbol })</Text>
</Center>
);
};
export default chakra(Token);
import { Image, chakra } from '@chakra-ui/react';
import React from 'react';
import type { Network } from 'types/networks';
import useNetwork from 'lib/hooks/useNetwork';
const EmptyElement = () => null;
const ASSETS_PATH_MAP: Record<string, string> = {
'xdai/mainnet': 'xdai',
'xdai/testnet': 'xdai',
'xdai/optimism': 'optimism',
'xdai/aox': 'arbitrum',
'eth/mainnet': 'ethereum',
'etc/mainnet': 'classic',
'poa/core': 'poa',
};
const getAssetsPath = (network: Network) => {
if (network.assetsNamePath) {
return network.assetsNamePath;
}
const key = [ network.type, network.subType ].filter(Boolean).join('/');
const nameFromMap = ASSETS_PATH_MAP[key];
return nameFromMap || network.type;
};
interface Props {
hash: string;
name: string;
className?: string;
}
const TokenLogo = ({ hash, name, className }: Props) => {
const network = useNetwork();
if (!network) {
return null;
}
const assetsPath = getAssetsPath(network);
const logoSrc = `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/${ assetsPath }/assets/${ hash }/logo.png`;
return <Image className={ className } src={ logoSrc } alt={ `${ name } logo` } fallback={ <EmptyElement/> }/>;
};
export default React.memo(chakra(TokenLogo));
import { Center, Link, Text, chakra } from '@chakra-ui/react';
import React from 'react';
import useLink from 'lib/link/useLink';
import TokenLogo from 'ui/shared/TokenLogo';
interface Props {
symbol: string;
hash: string;
name: string;
className?: string;
}
const TokenSnippet = ({ symbol, hash, name, className }: Props) => {
const link = useLink();
const url = link('token_index', { id: hash });
return (
<Center className={ className } columnGap={ 1 }>
<TokenLogo boxSize={ 5 } hash={ hash } name={ name }/>
<Link href={ url } target="_blank">
{ name }
</Link>
<Text variant="secondary">({ symbol })</Text>
</Center>
);
};
export default chakra(TokenSnippet);
import { Tag, TagLabel, TagLeftIcon, Tooltip } from '@chakra-ui/react';
import React from 'react';
import errorIcon from 'icons/status/error.svg';
import pendingIcon from 'icons/status/pending.svg';
import successIcon from 'icons/status/success.svg';
export interface Props {
status: 'success' | 'failed' | 'pending';
errorText?: string;
}
const TxStatus = ({ status, errorText }: Props) => {
let label;
let icon;
let colorScheme;
switch (status) {
case 'success':
label = 'Success';
icon = successIcon;
colorScheme = 'green';
break;
case 'failed':
label = 'Failed';
icon = errorIcon;
colorScheme = 'red';
break;
case 'pending':
label = 'Pending';
icon = pendingIcon;
// FIXME: it's not gray on mockups
// need to implement new color scheme or redefine colors here
colorScheme = 'gray';
break;
}
return (
<Tooltip label={ errorText }>
<Tag colorScheme={ colorScheme } display="inline-flex">
<TagLeftIcon boxSize={ 2.5 } as={ icon }/>
<TagLabel>{ label }</TagLabel>
</Tag>
</Tooltip>
);
};
export default TxStatus;
import { Box, Flex, Text, chakra } from '@chakra-ui/react'; import { Box, Flex, Text, chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
interface Props { interface Props {
className?: string; className?: string;
value: number; value: number;
colorScheme?: 'green' | 'gray';
} }
const WIDTH = 50; const WIDTH = 50;
const Utilization = ({ className, value }: Props) => { const Utilization = ({ className, value, colorScheme = 'green' }: Props) => {
const valueString = (value * 100).toFixed(2) + '%'; const valueString = (value * 100).toFixed(2) + '%';
const colorGrayScheme = useColorModeValue('gray.500', 'gray.500');
const color = colorScheme === 'gray' ? colorGrayScheme : 'green.500';
return ( return (
<Flex className={ className } alignItems="center"> <Flex className={ className } alignItems="center">
<Box bg="gray.100" w={ `${ WIDTH }px` } h="4px" borderRadius="full" overflow="hidden"> <Box bg={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } w={ `${ WIDTH }px` } h="4px" borderRadius="full" overflow="hidden">
<Box bg="green.500" w={ valueString } h="100%"/> <Box bg={ color } w={ valueString } h="100%"/>
</Box> </Box>
<Text color="green.500" ml="10px" fontWeight="bold">{ valueString }</Text> <Text color={ color } ml="10px" fontWeight="bold">{ valueString }</Text>
</Flex> </Flex>
); );
}; };
......
import { Link, chakra, shouldForwardProp } from '@chakra-ui/react'; import { Link, chakra, shouldForwardProp, Tooltip, Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useLink from 'lib/link/useLink'; import useLink from 'lib/link/useLink';
...@@ -6,25 +6,36 @@ import HashStringShorten from 'ui/shared/HashStringShorten'; ...@@ -6,25 +6,36 @@ import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props { interface Props {
type?: 'address' | 'transaction' | 'token'; type?: 'address' | 'transaction' | 'token' | 'block';
alias?: string;
className?: string; className?: string;
hash: string; hash: string;
truncation?: 'constant' | 'dynamic'| 'none'; truncation?: 'constant' | 'dynamic'| 'none';
fontWeight?: string; fontWeight?: string;
id?: string;
} }
const AddressLink = ({ type, className, truncation = 'dynamic', hash, fontWeight }: Props) => { const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, fontWeight }: Props) => {
const link = useLink(); const link = useLink();
let url; let url;
if (type === 'transaction') { if (type === 'transaction') {
url = link('tx_index', { id: hash }); url = link('tx_index', { id: id || hash });
} else if (type === 'token') { } else if (type === 'token') {
url = link('token_index', { id: hash }); url = link('token_index', { id: id || hash });
} else if (type === 'block') {
url = link('block', { id: id || hash });
} else { } else {
url = link('address_index', { id: hash }); url = link('address_index', { id: id || hash });
} }
const content = (() => { const content = (() => {
if (alias) {
return (
<Tooltip label={ hash }>
<Box overflow="hidden" textOverflow="ellipsis">{ alias }</Box>
</Tooltip>
);
}
switch (truncation) { switch (truncation) {
case 'constant': case 'constant':
return <HashStringShorten hash={ hash }/>; return <HashStringShorten hash={ hash }/>;
......
...@@ -2,10 +2,10 @@ import { Icon, Box, Flex, Drawer, DrawerOverlay, DrawerContent, DrawerBody, useC ...@@ -2,10 +2,10 @@ import { Icon, Box, Flex, Drawer, DrawerOverlay, DrawerContent, DrawerBody, useC
import React from 'react'; import React from 'react';
import burgerIcon from 'icons/burger.svg'; import burgerIcon from 'icons/burger.svg';
import NavigationMobile from 'ui/blocks/navigation/NavigationMobile'; import NavigationMobile from 'ui/snippets/navigation/NavigationMobile';
import NetworkLogo from 'ui/blocks/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import NetworkMenuButton from 'ui/blocks/networkMenu/NetworkMenuButton'; import NetworkMenuButton from 'ui/snippets/networkMenu/NetworkMenuButton';
import NetworkMenuContentMobile from 'ui/blocks/networkMenu/NetworkMenuContentMobile'; import NetworkMenuContentMobile from 'ui/snippets/networkMenu/NetworkMenuContentMobile';
const Burger = () => { const Burger = () => {
const iconColor = useColorModeValue('gray.600', 'white'); const iconColor = useColorModeValue('gray.600', 'white');
......
import type { UseCheckboxProps } from '@chakra-ui/checkbox'; import type { UseCheckboxProps } from '@chakra-ui/checkbox';
import { useCheckbox } from '@chakra-ui/checkbox'; import { useCheckbox } from '@chakra-ui/checkbox';
import { SunIcon } from '@chakra-ui/icons';
import { useColorMode, useColorModeValue, Icon } from '@chakra-ui/react'; import { useColorMode, useColorModeValue, Icon } from '@chakra-ui/react';
import type { import type {
SystemStyleObject, SystemStyleObject,
...@@ -16,6 +15,7 @@ import { dataAttr, __DEV__ } from '@chakra-ui/utils'; ...@@ -16,6 +15,7 @@ import { dataAttr, __DEV__ } from '@chakra-ui/utils';
import * as React from 'react'; import * as React from 'react';
import moonIcon from 'icons/moon.svg'; import moonIcon from 'icons/moon.svg';
import sunIcon from 'icons/sun.svg';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
export interface ColorModeTogglerProps export interface ColorModeTogglerProps
...@@ -101,10 +101,11 @@ const ColorModeToggler = forwardRef<ColorModeTogglerProps, 'input'>((props, ref) ...@@ -101,10 +101,11 @@ const ColorModeToggler = forwardRef<ColorModeTogglerProps, 'input'>((props, ref)
data-hover={ dataAttr(state.isHovered) } data-hover={ dataAttr(state.isHovered) }
__css={ thumbStyles } __css={ thumbStyles }
/> />
<SunIcon <Icon
boxSize={ 4 } boxSize={ 5 }
margin={ 2 } margin={ 1.5 }
zIndex="docked" zIndex="docked"
as={ sunIcon }
color={ useColorModeValue('gray.500', 'blue.600') } color={ useColorModeValue('gray.500', 'blue.600') }
{ ...transitionProps } { ...transitionProps }
/> />
......
import { HStack, Box, Flex, useColorModeValue } from '@chakra-ui/react'; import { HStack, Box, Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile'; import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import NetworkLogo from 'ui/blocks/networkMenu/NetworkLogo'; import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import ProfileMenuDesktop from 'ui/blocks/profileMenu/ProfileMenuDesktop'; import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile';
import ProfileMenuMobile from 'ui/blocks/profileMenu/ProfileMenuMobile'; import SearchBar from 'ui/snippets/searchBar/SearchBar';
import SearchBar from 'ui/blocks/searchBar/SearchBar';
import Burger from './Burger'; import Burger from './Burger';
import ColorModeToggler from './ColorModeToggler'; import ColorModeToggler from './ColorModeToggler';
const Header = () => { const Header = () => {
const isMobile = useIsMobile();
const bgColor = useColorModeValue('white', 'black'); const bgColor = useColorModeValue('white', 'black');
return (
if (isMobile) { <>
return ( <Box bgColor={ bgColor } display={{ base: 'block', lg: 'none' }}>
<Box bgColor={ bgColor }>
<Flex <Flex
as="header" as="header"
position="fixed" position="fixed"
...@@ -36,21 +33,22 @@ const Header = () => { ...@@ -36,21 +33,22 @@ const Header = () => {
</Flex> </Flex>
<SearchBar/> <SearchBar/>
</Box> </Box>
); <HStack
} as="header"
width="100%"
return ( alignItems="center"
<HStack justifyContent="center"
as="header" gap={ 12 }
width="100%" display={{ base: 'none', lg: 'flex' }}
alignItems="center" paddingX={ 12 }
justifyContent="center" paddingTop={ 9 }
gap={ 12 } paddingBottom="52px"
> >
<SearchBar/> <SearchBar/>
<ColorModeToggler/> <ColorModeToggler/>
<ProfileMenuDesktop/> <ProfileMenuDesktop/>
</HStack> </HStack>
</>
); );
}; };
......
import { VStack, Text, Stack, Icon, Link, useColorModeValue } from '@chakra-ui/react'; import { Box, VStack, Text, Stack, Icon, Link, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import ghIcon from 'icons/social/git.svg'; import ghIcon from 'icons/social/git.svg';
import statsIcon from 'icons/social/stats.svg'; import statsIcon from 'icons/social/stats.svg';
import tgIcon from 'icons/social/telega.svg'; import tgIcon from 'icons/social/telega.svg';
import twIcon from 'icons/social/tweet.svg'; import twIcon from 'icons/social/tweet.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
const SOCIAL_LINKS = [ const SOCIAL_LINKS = [
...@@ -24,22 +23,13 @@ interface Props { ...@@ -24,22 +23,13 @@ interface Props {
} }
const NavFooter = ({ isCollapsed, hasAccount }: Props) => { const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
const isMobile = useIsMobile(); const isExpanded = isCollapsed === false;
const width = (() => {
if (isMobile) {
return '100%';
}
return isCollapsed ? '20px' : '180px';
})();
const marginTop = (() => { const marginTop = (() => {
if (!hasAccount) { if (!hasAccount) {
return 'auto'; return 'auto';
} }
return isMobile ? 6 : 20; return { base: 6, lg: 20 };
})(); })();
return ( return (
...@@ -48,8 +38,8 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => { ...@@ -48,8 +38,8 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
spacing={ 8 } spacing={ 8 }
borderTop="1px solid" borderTop="1px solid"
borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
width={ width } width={{ base: '100%', lg: isExpanded ? '180px' : '20px', xl: isCollapsed ? '20px' : '180px' }}
paddingTop={ isMobile ? 6 : 8 } paddingTop={{ base: 6, lg: 8 }}
marginTop={ marginTop } marginTop={ marginTop }
alignItems="flex-start" alignItems="flex-start"
alignSelf="center" alignSelf="center"
...@@ -57,7 +47,7 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => { ...@@ -57,7 +47,7 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
fontSize="xs" fontSize="xs"
{ ...getDefaultTransitionProps({ transitionProperty: 'width' }) } { ...getDefaultTransitionProps({ transitionProperty: 'width' }) }
> >
<Stack direction={ isCollapsed ? 'column' : 'row' }> <Stack direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}>
{ SOCIAL_LINKS.map(sl => { { SOCIAL_LINKS.map(sl => {
return ( return (
<Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label }> <Link href={ sl.link } key={ sl.link } variant="secondary" w={ 5 } h={ 5 } aria-label={ sl.label }>
...@@ -66,14 +56,12 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => { ...@@ -66,14 +56,12 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
); );
}) } }) }
</Stack> </Stack>
{ !isCollapsed && ( <Box display={{ base: 'block', lg: isExpanded ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }}>
<> <Text variant="secondary" mb={ 8 }>
<Text variant="secondary">
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks. Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text> </Text>
<Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ BLOCKSCOUT_VERSION }</Link></Text> <Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ BLOCKSCOUT_VERSION }</Link></Text>
</> </Box>
) }
</VStack> </VStack>
); );
}; };
......
...@@ -2,7 +2,6 @@ import { Link, Icon, Text, HStack, Tooltip, Box } from '@chakra-ui/react'; ...@@ -2,7 +2,6 @@ import { Link, Icon, Text, HStack, Tooltip, Box } from '@chakra-ui/react';
import NextLink from 'next/link'; import NextLink from 'next/link';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import useColors from './useColors'; import useColors from './useColors';
...@@ -18,21 +17,15 @@ interface Props { ...@@ -18,21 +17,15 @@ interface Props {
const NavLink = ({ text, url, icon, isCollapsed, isActive, px }: Props) => { const NavLink = ({ text, url, icon, isCollapsed, isActive, px }: Props) => {
const colors = useColors(); const colors = useColors();
const isMobile = useIsMobile();
const width = (() => {
if (isMobile) {
return '100%';
}
return isCollapsed ? '60px' : '180px'; const isExpanded = isCollapsed === false;
})();
return ( return (
<Box as="li" listStyleType="none" w="100%"> <Box as="li" listStyleType="none" w="100%">
<NextLink href={ url } passHref> <NextLink href={ url } passHref>
<Link <Link
w={ width } w={{ base: '100%', lg: isExpanded ? '180px' : '60px', xl: isCollapsed ? '60px' : '180px' }}
px={ px || (isCollapsed ? '15px' : 3) } px={ px || { base: 3, lg: isExpanded ? 3 : '15px', xl: isCollapsed ? '15px' : 3 } }
py={ 2.5 } py={ 2.5 }
display="flex" display="flex"
color={ isActive ? colors.text.active : colors.text.default } color={ isActive ? colors.text.active : colors.text.default }
...@@ -53,7 +46,14 @@ const NavLink = ({ text, url, icon, isCollapsed, isActive, px }: Props) => { ...@@ -53,7 +46,14 @@ const NavLink = ({ text, url, icon, isCollapsed, isActive, px }: Props) => {
> >
<HStack spacing={ 3 }> <HStack spacing={ 3 }>
<Icon as={ icon } boxSize="30px"/> <Icon as={ icon } boxSize="30px"/>
{ !isCollapsed && <Text variant="inherit" fontSize="sm" lineHeight="20px">{ text }</Text> } <Text
variant="inherit"
fontSize="sm"
lineHeight="20px"
display={{ base: 'block', lg: isExpanded ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }}
>
{ text }
</Text>
</HStack> </HStack>
</Tooltip> </Tooltip>
</Link> </Link>
......
import { ChevronLeftIcon } from '@chakra-ui/icons'; import { Flex, Box, VStack, Icon, useColorModeValue } from '@chakra-ui/react';
import { Flex, Box, VStack, useColorModeValue, useBreakpointValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import chevronIcon from 'icons/arrows/east-mini.svg';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems'; import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork'; import useNetwork from 'lib/hooks/useNetwork';
import isBrowser from 'lib/isBrowser';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkLogo from 'ui/blocks/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import NetworkMenu from 'ui/blocks/networkMenu/NetworkMenu'; import NetworkMenu from 'ui/snippets/networkMenu/NetworkMenu';
import NavFooter from './NavFooter'; import NavFooter from './NavFooter';
import NavLink from './NavLink'; import NavLink from './NavLink';
...@@ -15,18 +16,24 @@ import NavLink from './NavLink'; ...@@ -15,18 +16,24 @@ import NavLink from './NavLink';
const NavigationDesktop = () => { const NavigationDesktop = () => {
const { mainNavItems, accountNavItems } = useNavItems(); const { mainNavItems, accountNavItems } = useNavItems();
const selectedNetwork = useNetwork(); const selectedNetwork = useNetwork();
const isLargeScreen = useBreakpointValue({ base: false, xl: true });
const navBarCollapsedCookie = cookies.get(cookies.NAMES.NAV_BAR_COLLAPSED);
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const hasAccount = selectedNetwork?.isAccountSupported && isAuth;
const [ isCollapsed, setCollapsedState ] = React.useState(navBarCollapsedCookie === 'true'); const isInBrowser = isBrowser();
const [ hasAccount, setHasAccount ] = React.useState(false);
const [ isCollapsed, setCollapsedState ] = React.useState<boolean | undefined>();
React.useEffect(() => { React.useEffect(() => {
if (!navBarCollapsedCookie) { const navBarCollapsedCookie = cookies.get(cookies.NAMES.NAV_BAR_COLLAPSED);
setCollapsedState(!isLargeScreen); const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
if (isInBrowser) {
if (navBarCollapsedCookie === 'true') {
setCollapsedState(true);
}
if (navBarCollapsedCookie === 'false') {
setCollapsedState(false);
}
setHasAccount(Boolean(selectedNetwork?.isAccountSupported && isAuth && isInBrowser));
} }
}, [ isLargeScreen, navBarCollapsedCookie ]); }, [ isInBrowser, selectedNetwork?.isAccountSupported ]);
const handleTogglerClick = React.useCallback(() => { const handleTogglerClick = React.useCallback(() => {
setCollapsedState((flag) => !flag); setCollapsedState((flag) => !flag);
...@@ -40,16 +47,19 @@ const NavigationDesktop = () => { ...@@ -40,16 +47,19 @@ const NavigationDesktop = () => {
borderColor: useColorModeValue('blackAlpha.200', 'whiteAlpha.200'), borderColor: useColorModeValue('blackAlpha.200', 'whiteAlpha.200'),
}; };
const isExpanded = isCollapsed === false;
return ( return (
<Flex <Flex
display={{ base: 'none', lg: 'flex' }}
position="relative" position="relative"
flexDirection="column" flexDirection="column"
alignItems="flex-start" alignItems="flex-start"
borderRight="1px solid" borderRight="1px solid"
borderColor={ containerBorderColor } borderColor={ containerBorderColor }
px={ isCollapsed ? 4 : 6 } px={{ lg: isExpanded ? 6 : 4, xl: isCollapsed ? 4 : 6 }}
py={ 12 } py={ 12 }
width={ isCollapsed ? '92px' : '229px' } width={{ lg: isExpanded ? '229px' : '92px', xl: isCollapsed ? '92px' : '229px' }}
{ ...getDefaultTransitionProps({ transitionProperty: 'width, padding' }) } { ...getDefaultTransitionProps({ transitionProperty: 'width, padding' }) }
> >
<Box <Box
...@@ -78,19 +88,20 @@ const NavigationDesktop = () => { ...@@ -78,19 +88,20 @@ const NavigationDesktop = () => {
</Box> </Box>
) } ) }
<NavFooter isCollapsed={ isCollapsed } hasAccount={ hasAccount }/> <NavFooter isCollapsed={ isCollapsed } hasAccount={ hasAccount }/>
<ChevronLeftIcon <Icon
as={ chevronIcon }
width={ 6 } width={ 6 }
height={ 6 } height={ 6 }
border="1px" border="1px"
_hover={{ color: 'blue.400' }} _hover={{ color: 'blue.400' }}
borderRadius="base" borderRadius="base"
{ ...chevronIconStyles } { ...chevronIconStyles }
transform={ isCollapsed ? 'rotate(180deg)' : 'rotate(0)' } transform={{ lg: isExpanded ? 'rotate(0)' : 'rotate(180deg)', xl: isCollapsed ? 'rotate(180deg)' : 'rotate(0)' }}
{ ...getDefaultTransitionProps({ transitionProperty: 'transform, left' }) } { ...getDefaultTransitionProps({ transitionProperty: 'transform, left' }) }
transformOrigin="center" transformOrigin="center"
position="fixed" position="absolute"
top="104px" top="104px"
left={ isCollapsed ? '80px' : '216px' } left={{ lg: isExpanded ? '216px' : '80px', xl: isCollapsed ? '80px' : '216px' }}
cursor="pointer" cursor="pointer"
onClick={ handleTogglerClick } onClick={ handleTogglerClick }
/> />
......
...@@ -4,8 +4,8 @@ import React from 'react'; ...@@ -4,8 +4,8 @@ import React from 'react';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems'; import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork'; import useNetwork from 'lib/hooks/useNetwork';
import NavFooter from 'ui/blocks/navigation/NavFooter'; import NavFooter from 'ui/snippets/navigation/NavFooter';
import NavLink from 'ui/blocks/navigation/NavLink'; import NavLink from 'ui/snippets/navigation/NavLink';
const NavigationMobile = () => { const NavigationMobile = () => {
const { mainNavItems, accountNavItems } = useNavItems(); const { mainNavItems, accountNavItems } = useNavItems();
......
...@@ -57,7 +57,7 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => { ...@@ -57,7 +57,7 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
<NextLink href={ href } passHref> <NextLink href={ href } passHref>
<Box <Box
as="a" as="a"
width={ isCollapsed ? '0' : '113px' } width={{ base: '113px', lg: isCollapsed === false ? '113px' : 0, xl: isCollapsed ? '0' : '113px' }}
display="inline-flex" display="inline-flex"
overflow="hidden" overflow="hidden"
onClick={ onClick } onClick={ onClick }
......
...@@ -4,7 +4,7 @@ import React from 'react'; ...@@ -4,7 +4,7 @@ import React from 'react';
import NetworkMenuButton from './NetworkMenuButton'; import NetworkMenuButton from './NetworkMenuButton';
import NetworkMenuContentDesktop from './NetworkMenuContentDesktop'; import NetworkMenuContentDesktop from './NetworkMenuContentDesktop';
interface Props { interface Props {
isCollapsed: boolean; isCollapsed?: boolean;
} }
const NetworkMenu = ({ isCollapsed }: Props) => { const NetworkMenu = ({ isCollapsed }: Props) => {
...@@ -13,7 +13,7 @@ const NetworkMenu = ({ isCollapsed }: Props) => { ...@@ -13,7 +13,7 @@ const NetworkMenu = ({ isCollapsed }: Props) => {
{ ({ isOpen }) => ( { ({ isOpen }) => (
<> <>
<PopoverTrigger> <PopoverTrigger>
<Box marginLeft={ isCollapsed ? '0px' : '7px' }> <Box marginLeft={{ base: '7px', lg: isCollapsed === false ? '7px' : '0px', xl: isCollapsed ? '0px' : '7px' }}>
<NetworkMenuButton isActive={ isOpen }/> <NetworkMenuButton isActive={ isOpen }/>
</Box> </Box>
</PopoverTrigger> </PopoverTrigger>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment