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>
......
...@@ -5,7 +5,7 @@ import type { UserInfo } from 'types/api/account'; ...@@ -5,7 +5,7 @@ import type { UserInfo } from 'types/api/account';
import useNavItems from 'lib/hooks/useNavItems'; import useNavItems from 'lib/hooks/useNavItems';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NavLink from 'ui/blocks/navigation/NavLink'; import NavLink from 'ui/snippets/navigation/NavLink';
type Props = UserInfo; type Props = UserInfo;
...@@ -33,10 +33,10 @@ const ProfileMenuContent = ({ name, nickname, email }: Props) => { ...@@ -33,10 +33,10 @@ const ProfileMenuContent = ({ name, nickname, email }: Props) => {
> >
{ email } { email }
</Text> </Text>
<NavLink { ...profileItem } isActive={ undefined } px="0px"/> <NavLink { ...profileItem } isActive={ undefined } px="0px" isCollapsed={ false }/>
<Box as="nav" mt={ 2 } pt={ 2 } borderTopColor={ borderColor } borderTopWidth="1px" { ...getDefaultTransitionProps() }> <Box as="nav" mt={ 2 } pt={ 2 } borderTopColor={ borderColor } borderTopWidth="1px" { ...getDefaultTransitionProps() }>
<VStack as="ul" spacing="0" alignItems="flex-start" overflow="hidden"> <VStack as="ul" spacing="0" alignItems="flex-start" overflow="hidden">
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item } isActive={ undefined } px="0px"/>) } { accountNavItems.map((item) => <NavLink key={ item.text } { ...item } isActive={ undefined } isCollapsed={ false } px="0px"/>) }
</VStack> </VStack>
</Box> </Box>
<Box mt={ 2 } pt={ 3 } borderTopColor={ borderColor } borderTopWidth="1px" { ...getDefaultTransitionProps() }> <Box mt={ 2 } pt={ 3 } borderTopColor={ borderColor } borderTopWidth="1px" { ...getDefaultTransitionProps() }>
......
...@@ -2,8 +2,8 @@ import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button } from '@c ...@@ -2,8 +2,8 @@ import { Popover, PopoverContent, PopoverBody, PopoverTrigger, Button } from '@c
import React from 'react'; import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import ProfileMenuContent from 'ui/blocks/profileMenu/ProfileMenuContent';
import UserAvatar from 'ui/shared/UserAvatar'; import UserAvatar from 'ui/shared/UserAvatar';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
const ProfileMenuDesktop = () => { const ProfileMenuDesktop = () => {
const { data } = useFetchProfileInfo(); const { data } = useFetchProfileInfo();
...@@ -11,7 +11,7 @@ const ProfileMenuDesktop = () => { ...@@ -11,7 +11,7 @@ const ProfileMenuDesktop = () => {
return ( return (
<Popover openDelay={ 300 } placement="bottom-end" gutter={ 10 } isLazy> <Popover openDelay={ 300 } placement="bottom-end" gutter={ 10 } isLazy>
<PopoverTrigger> <PopoverTrigger>
<Button variant="unstyled" display="inline-flex" height="auto"> <Button variant="unstyled" display="inline-flex" height="auto" flexShrink={ 0 }>
<UserAvatar size={ 50 } data={ data }/> <UserAvatar size={ 50 } data={ data }/>
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
......
...@@ -2,9 +2,9 @@ import { Flex, Box, Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclos ...@@ -2,9 +2,9 @@ import { Flex, Box, Drawer, DrawerOverlay, DrawerContent, DrawerBody, useDisclos
import React from 'react'; import React from 'react';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo'; import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import ColorModeToggler from 'ui/blocks/header/ColorModeToggler';
import ProfileMenuContent from 'ui/blocks/profileMenu/ProfileMenuContent';
import UserAvatar from 'ui/shared/UserAvatar'; import UserAvatar from 'ui/shared/UserAvatar';
import ColorModeToggler from 'ui/snippets/header/ColorModeToggler';
import ProfileMenuContent from 'ui/snippets/profileMenu/ProfileMenuContent';
const ProfileMenuMobile = () => { const ProfileMenuMobile = () => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
......
import type { ChangeEvent, FormEvent } from 'react'; import type { ChangeEvent, FormEvent } from 'react';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import useLink from 'lib/link/useLink'; import useLink from 'lib/link/useLink';
import SearchBarDesktop from './SearchBarDesktop'; import SearchBarDesktop from './SearchBarDesktop';
...@@ -10,7 +9,6 @@ import SearchBarMobile from './SearchBarMobile'; ...@@ -10,7 +9,6 @@ import SearchBarMobile from './SearchBarMobile';
const SearchBar = () => { const SearchBar = () => {
const [ value, setValue ] = React.useState(''); const [ value, setValue ] = React.useState('');
const link = useLink(); const link = useLink();
const isMobile = useIsMobile();
const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value); setValue(event.target.value);
...@@ -22,14 +20,11 @@ const SearchBar = () => { ...@@ -22,14 +20,11 @@ const SearchBar = () => {
window.location.assign(url); window.location.assign(url);
}, [ link, value ]); }, [ link, value ]);
if (isMobile) {
return (
<SearchBarMobile onChange={ handleChange } onSubmit={ handleSubmit }/>
);
}
return ( return (
<SearchBarDesktop onChange={ handleChange } onSubmit={ handleSubmit }/> <>
<SearchBarDesktop onChange={ handleChange } onSubmit={ handleSubmit }/>
<SearchBarMobile onChange={ handleChange } onSubmit={ handleSubmit }/>
</>
); );
}; };
......
import { SearchIcon } from '@chakra-ui/icons'; import { InputGroup, Input, InputLeftAddon, InputLeftElement, Icon, chakra, useColorModeValue } from '@chakra-ui/react';
import { InputGroup, Input, InputLeftAddon, InputLeftElement, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ChangeEvent, FormEvent } from 'react'; import type { ChangeEvent, FormEvent } from 'react';
import searchIcon from 'icons/search.svg';
interface Props { interface Props {
onChange: (event: ChangeEvent<HTMLInputElement>) => void; onChange: (event: ChangeEvent<HTMLInputElement>) => void;
onSubmit: (event: FormEvent<HTMLFormElement>) => void; onSubmit: (event: FormEvent<HTMLFormElement>) => void;
...@@ -10,11 +11,11 @@ interface Props { ...@@ -10,11 +11,11 @@ interface Props {
const SearchBarDesktop = ({ onChange, onSubmit }: Props) => { const SearchBarDesktop = ({ onChange, onSubmit }: Props) => {
return ( return (
<form noValidate onSubmit={ onSubmit }> <chakra.form noValidate onSubmit={ onSubmit } display={{ base: 'none', lg: 'block' }} w="100%">
<InputGroup> <InputGroup>
<InputLeftAddon w="111px">All filters</InputLeftAddon> <InputLeftAddon w="111px">All filters</InputLeftAddon>
<InputLeftElement w={ 6 } ml="132px" mr={ 2.5 }> <InputLeftElement w={ 6 } ml="132px" mr={ 2.5 }>
<SearchIcon w={ 5 } h={ 5 } color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/> <Icon as={ searchIcon } boxSize={ 6 } color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/>
</InputLeftElement> </InputLeftElement>
<Input <Input
paddingInlineStart="50px" paddingInlineStart="50px"
...@@ -24,7 +25,7 @@ const SearchBarDesktop = ({ onChange, onSubmit }: Props) => { ...@@ -24,7 +25,7 @@ const SearchBarDesktop = ({ onChange, onSubmit }: Props) => {
borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') } borderColor={ useColorModeValue('blackAlpha.100', 'whiteAlpha.200') }
/> />
</InputGroup> </InputGroup>
</form> </chakra.form>
); );
}; };
......
import { SearchIcon } from '@chakra-ui/icons'; import { InputGroup, Input, InputLeftElement, Icon, useColorModeValue, chakra } from '@chakra-ui/react';
import { InputGroup, Input, InputLeftElement, useColorModeValue, chakra } from '@chakra-ui/react';
import clamp from 'lodash/clamp'; import clamp from 'lodash/clamp';
import throttle from 'lodash/throttle'; import throttle from 'lodash/throttle';
import React from 'react'; import React from 'react';
import type { ChangeEvent, FormEvent } from 'react'; import type { ChangeEvent, FormEvent } from 'react';
import searchIcon from 'icons/search.svg';
import isBrowser from 'lib/isBrowser'; import isBrowser from 'lib/isBrowser';
const SCROLL_DIFF_THRESHOLD = 20; const SCROLL_DIFF_THRESHOLD = 20;
...@@ -61,10 +61,12 @@ const SearchBarMobile = ({ onChange, onSubmit }: Props) => { ...@@ -61,10 +61,12 @@ const SearchBarMobile = ({ onChange, onSubmit }: Props) => {
transform={ isVisible ? 'translateY(0)' : 'translateY(-100%)' } transform={ isVisible ? 'translateY(0)' : 'translateY(-100%)' }
transitionProperty="transform" transitionProperty="transform"
transitionDuration="slow" transitionDuration="slow"
display={{ base: 'block', lg: 'none' }}
w="100%"
> >
<InputGroup size="sm"> <InputGroup size="sm">
<InputLeftElement > <InputLeftElement >
<SearchIcon w={ 4 } h={ 4 } color={ searchIconColor }/> <Icon as={ searchIcon } boxSize={ 4 } color={ searchIconColor }/>
</InputLeftElement> </InputLeftElement>
<Input <Input
paddingInlineStart="38px" paddingInlineStart="38px"
......
import { Center, Icon, Text } from '@chakra-ui/react'; import { Flex, Icon, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import rightArrowIcon from 'icons/arrows/right.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import Token from 'ui/shared/Token'; import TokenSnippet from 'ui/shared/TokenSnippet';
interface Props { interface Props {
from: string; from: string;
to: string; to: string;
token: string;
amount: number; amount: number;
usd: number; usd: number;
token: {
symbol: string;
hash: string;
name: string;
};
} }
const TokenTransfer = ({ from, to, amount, usd, token }: Props) => { const TokenTransfer = ({ from, to, amount, usd, token }: Props) => {
return ( return (
<Center> <Flex alignItems="center" flexWrap="wrap" columnGap={ 3 } rowGap={ 3 }>
<AddressLink fontWeight="500" hash={ from } truncation="constant"/> <Flex alignItems="center">
<Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } color="gray.500"/> <AddressLink fontWeight="500" hash={ from } truncation="constant"/>
<AddressLink fontWeight="500" hash={ to } truncation="constant"/> <Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } color="gray.500"/>
<Text fontWeight={ 500 } as="span" ml={ 4 }>For:{ space } <AddressLink fontWeight="500" hash={ to } truncation="constant"/>
</Flex>
<Text fontWeight={ 500 } as="span">For:{ space }
<Text fontWeight={ 600 } as="span">{ amount }</Text>{ space } <Text fontWeight={ 600 } as="span">{ amount }</Text>{ space }
<Text fontWeight={ 400 } variant="secondary" as="span">(${ usd.toFixed(2) })</Text> <Text fontWeight={ 400 } variant="secondary" as="span">(${ usd.toFixed(2) })</Text>
</Text> </Text>
<Token symbol={ token } ml={ 3 }/> <TokenSnippet { ...token }/>
</Center> </Flex>
); );
}; };
......
...@@ -10,12 +10,13 @@ interface RowProps { ...@@ -10,12 +10,13 @@ interface RowProps {
isLast?: boolean; isLast?: boolean;
name: string; name: string;
type: string; type: string;
indexed?: boolean;
} }
const PADDING = 4; const PADDING = 4;
const GAP = 5; const GAP = 5;
const TableRow = ({ isLast, name, type, children }: RowProps) => { const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
return ( return (
...@@ -38,6 +39,14 @@ const TableRow = ({ isLast, name, type, children }: RowProps) => { ...@@ -38,6 +39,14 @@ const TableRow = ({ isLast, name, type, children }: RowProps) => {
> >
{ type } { type }
</GridItem> </GridItem>
<GridItem
pr={ GAP }
pt={ GAP }
pb={ isLast ? PADDING : 0 }
bgColor={ bgColor }
>
{ indexed ? 'true' : 'false' }
</GridItem>
<GridItem <GridItem
pr={ PADDING } pr={ PADDING }
pt={ GAP } pt={ GAP }
...@@ -55,28 +64,31 @@ const TxDecodedInputData = () => { ...@@ -55,28 +64,31 @@ const TxDecodedInputData = () => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
return ( return (
<Grid gridTemplateColumns="minmax(80px, auto) minmax(80px, auto) 1fr" fontSize="sm" lineHeight={ 5 } w="100%"> <Grid gridTemplateColumns="minmax(80px, auto) minmax(80px, auto) minmax(80px, auto) minmax(0, 1fr)" fontSize="sm" lineHeight={ 5 } w="100%">
{ /* FIRST PART OF BLOCK */ } { /* FIRST PART OF BLOCK */ }
<GridItem fontWeight={ 600 } pl={ PADDING } pr={ GAP }>Method Id</GridItem> <GridItem fontWeight={ 600 } pl={{ base: 0, lg: PADDING }} pr={{ base: 0, lg: GAP }} colSpan={{ base: 4, lg: undefined }}>Method Id</GridItem>
<GridItem colSpan={ 2 } pr={ PADDING }>0xddf252ad</GridItem> <GridItem colSpan={{ base: 4, lg: 3 }} pr={{ base: 0, lg: PADDING }} mt={{ base: 2, lg: 0 }}>0xddf252ad</GridItem>
<GridItem <GridItem
py={ 2 } py={ 2 }
mt={ 2 } mt={ 2 }
pl={ PADDING } pl={{ base: 0, lg: PADDING }}
pr={ GAP } pr={{ base: 0, lg: GAP }}
fontWeight={ 600 } fontWeight={ 600 }
borderTopColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } borderTopColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderTopWidth="1px" borderTopWidth="1px"
colSpan={{ base: 4, lg: undefined }}
> >
Call Call
</GridItem> </GridItem>
<GridItem <GridItem
py={ 2 } py={{ base: 0, lg: 2 }}
mt={ 2 } mt={{ base: 0, lg: 2 }}
colSpan={ 2 } mb={{ base: 2, lg: 0 }}
pr={ PADDING } colSpan={{ base: 4, lg: 3 }}
pr={{ base: 0, lg: PADDING }}
borderTopColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } borderTopColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderTopWidth="1px" borderTopWidth={{ base: '0px', lg: '1px' }}
whiteSpace="normal"
> >
Transfer(address indexed from, address indexed to, uint256 indexed tokenId) Transfer(address indexed from, address indexed to, uint256 indexed tokenId)
</GridItem> </GridItem>
...@@ -100,6 +112,15 @@ const TxDecodedInputData = () => { ...@@ -100,6 +112,15 @@ const TxDecodedInputData = () => {
> >
Type Type
</GridItem> </GridItem>
<GridItem
pr={ GAP }
pt={ PADDING }
pb={ 1 }
bgColor={ bgColor }
fontWeight={ 600 }
>
Inde<wbr/>xed?
</GridItem>
<GridItem <GridItem
pr={ PADDING } pr={ PADDING }
pt={ PADDING } pt={ PADDING }
...@@ -115,7 +136,7 @@ const TxDecodedInputData = () => { ...@@ -115,7 +136,7 @@ const TxDecodedInputData = () => {
<CopyToClipboard text="0x0000000000000000000000000000000000000000"/> <CopyToClipboard text="0x0000000000000000000000000000000000000000"/>
</Address> </Address>
</TableRow> </TableRow>
<TableRow name="from" type="address"> <TableRow name="to" type="address" indexed>
<Address justifyContent="space-between"> <Address justifyContent="space-between">
<AddressLink hash="0xcf0c50b7ea8af37d57380a0ac199d55b0782c718"/> <AddressLink hash="0xcf0c50b7ea8af37d57380a0ac199d55b0782c718"/>
<CopyToClipboard text="0xcf0c50b7ea8af37d57380a0ac199d55b0782c718"/> <CopyToClipboard text="0xcf0c50b7ea8af37d57380a0ac199d55b0782c718"/>
......
import { Grid, GridItem, Text, Box, Icon, Link, Tag, Flex } from '@chakra-ui/react'; import { Grid, GridItem, Text, Box, Icon, Link, Tag, Flex, Tooltip, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { scroller, Element } from 'react-scroll'; import { scroller, Element } from 'react-scroll';
import { tx } from 'data/tx'; import { tx } from 'data/tx';
import clockIcon from 'icons/clock.svg'; import clockIcon from 'icons/clock.svg';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import errorIcon from 'icons/status/error.svg';
import successIcon from 'icons/status/success.svg'; import successIcon from 'icons/status/success.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import useNetwork from 'lib/hooks/useNetwork';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import PrevNext from 'ui/shared/PrevNext';
import RawInputData from 'ui/shared/RawInputData'; import RawInputData from 'ui/shared/RawInputData';
import Token from 'ui/shared/Token'; import TextSeparator from 'ui/shared/TextSeparator';
import TokenSnippet from 'ui/shared/TokenSnippet';
import type { Props as TxStatusProps } from 'ui/shared/TxStatus';
import TxStatus from 'ui/shared/TxStatus';
import Utilization from 'ui/shared/Utilization'; import Utilization from 'ui/shared/Utilization';
import TokenTransfer from 'ui/tx/TokenTransfer'; import TokenTransfer from 'ui/tx/TokenTransfer';
import TxDecodedInputData from 'ui/tx/TxDecodedInputData'; import TxDecodedInputData from 'ui/tx/TxDecodedInputData';
import type { Props as TxStatusProps } from 'ui/tx/TxStatus';
import TxStatus from 'ui/tx/TxStatus';
const TxDetails = () => { const TxDetails = () => {
const [ isExpanded, setIsExpanded ] = React.useState(false); const selectedNetwork = useNetwork();
const leftSeparatorStyles = { const [ isExpanded, setIsExpanded ] = React.useState(false);
ml: 3,
pl: 3,
borderLeftWidth: '1px',
borderLeftColor: 'gray.700',
};
const handleCutClick = React.useCallback(() => { const handleCutClick = React.useCallback(() => {
setIsExpanded((flag) => !flag); setIsExpanded((flag) => !flag);
...@@ -39,13 +39,17 @@ const TxDetails = () => { ...@@ -39,13 +39,17 @@ const TxDetails = () => {
}, []); }, []);
return ( return (
<Grid columnGap={ 8 } rowGap={ 3 } templateColumns="auto 1fr"> <Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}>
<DetailsInfoItem <DetailsInfoItem
title="Transaction hash" title="Transaction hash"
hint="Unique character string (TxID) assigned to every verified transaction." hint="Unique character string (TxID) assigned to every verified transaction."
flexWrap="nowrap"
> >
{ tx.hash } <Box overflow="hidden">
<HashStringShortenDynamic hash={ tx.hash }/>
</Box>
<CopyToClipboard text={ tx.hash }/> <CopyToClipboard text={ tx.hash }/>
<PrevNext ml={ 7 }/>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Status" title="Status"
...@@ -58,7 +62,8 @@ const TxDetails = () => { ...@@ -58,7 +62,8 @@ const TxDetails = () => {
hint="Block number containing the transaction." hint="Block number containing the transaction."
> >
<Text>{ tx.block_num }</Text> <Text>{ tx.block_num }</Text>
<Text { ...leftSeparatorStyles } borderLeftColor="gray.500" variant="secondary"> <TextSeparator color="gray.500"/>
<Text variant="secondary">
{ tx.confirmation_num } Block confirmations { tx.confirmation_num } Block confirmations
</Text> </Text>
</DetailsInfoItem> </DetailsInfoItem>
...@@ -68,71 +73,83 @@ const TxDetails = () => { ...@@ -68,71 +73,83 @@ const TxDetails = () => {
> >
<Icon as={ clockIcon } boxSize={ 5 } color="gray.500"/> <Icon as={ clockIcon } boxSize={ 5 } color="gray.500"/>
<Text ml={ 1 }>{ dayjs(tx.timestamp).fromNow() }</Text> <Text ml={ 1 }>{ dayjs(tx.timestamp).fromNow() }</Text>
<Text { ...leftSeparatorStyles }>{ dayjs(tx.timestamp).format('LLLL') }</Text> <TextSeparator/>
<Text { ...leftSeparatorStyles } borderLeftColor="gray.500" variant="secondary"> <Text whiteSpace="normal">{ dayjs(tx.timestamp).format('LLLL') }<TextSeparator color="gray.500"/></Text>
<Text variant="secondary">
Confirmed within { tx.confirmation_duration } secs Confirmed within { tx.confirmation_duration } secs
</Text> </Text>
</DetailsInfoItem> </DetailsInfoItem>
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 3, lg: 8 }}/>
<DetailsInfoItem <DetailsInfoItem
title="From" title="From"
hint="Address (external or contract) sending the transaction." hint="Address (external or contract) sending the transaction."
mt={ 8 }
> >
<Address> <Address>
<AddressIcon hash={ tx.address_from }/> <AddressIcon hash={ tx.address_from.hash }/>
<AddressLink ml={ 2 } hash={ tx.address_from }/> <AddressLink ml={ 2 } hash={ tx.address_from.hash }/>
<CopyToClipboard text={ tx.address_from }/> <CopyToClipboard text={ tx.address_from.hash }/>
</Address> </Address>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Interacted with contract" title="Interacted with contract"
hint="Address (external or contract) receiving the transaction." hint="Address (external or contract) receiving the transaction."
flexWrap={{ base: 'wrap', lg: 'nowrap' }}
> >
<Address> <Address mr={ 3 }>
<AddressIcon hash={ tx.address_to }/> <AddressIcon hash={ tx.address_to.hash }/>
<AddressLink ml={ 2 } hash={ tx.address_to }/> <AddressLink ml={ 2 } hash={ tx.address_to.hash }/>
<CopyToClipboard text={ tx.address_to }/> <CopyToClipboard text={ tx.address_to.hash }/>
</Address> </Address>
<Tag colorScheme="orange" variant="solid" ml={ 3 }>SANA</Tag> <Tag colorScheme="orange" variant="solid" flexShrink={ 0 }>SANA</Tag>
<Icon as={ successIcon } boxSize={ 4 } ml={ 2 } color="green.500"/> <Tooltip label="Contract execution completed">
<Token symbol="USDT" ml={ 3 }/> <chakra.span display="inline-flex">
<Icon as={ successIcon } boxSize={ 4 } ml={ 2 } color="green.500" cursor="pointer"/>
</chakra.span>
</Tooltip>
<Tooltip label="Error occured during contract execution">
<chakra.span display="inline-flex">
<Icon as={ errorIcon } boxSize={ 4 } ml={ 2 } color="red.500" cursor="pointer"/>
</chakra.span>
</Tooltip>
<TokenSnippet symbol="UP" name="User Pay" hash="0xA17ed5dFc62D0a3E74D69a0503AE9FdA65d9f212" ml={ 3 }/>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Token transferred" title="Token transferred"
hint="List of token transferred in the transaction." hint="List of token transferred in the transaction."
> >
<Flex flexDirection="column" alignItems="flex-start" rowGap={ 5 }> <Flex flexDirection="column" alignItems="flex-start" rowGap={ 5 } w="100%">
{ tx.transferred_tokens.map((item) => <TokenTransfer key={ item.token } { ...item }/>) } { tx.transferred_tokens.map((item) => <TokenTransfer key={ item.token.hash } { ...item }/>) }
</Flex> </Flex>
</DetailsInfoItem> </DetailsInfoItem>
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 3, lg: 8 }}/>
<DetailsInfoItem <DetailsInfoItem
title="Value" title="Value"
hint="Value sent in the native token (and USD) if applicable." hint="Value sent in the native token (and USD) if applicable."
mt={ 8 }
> >
<Text>{ tx.amount.value } Ether</Text> <Text>{ tx.amount.value } { selectedNetwork?.currency }</Text>
<Text variant="secondary" ml={ 1 }>(${ tx.amount.value_usd.toFixed(2) })</Text> <Text variant="secondary" ml={ 1 }>(${ tx.amount.value_usd.toFixed(2) })</Text>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Transaction fee" title="Transaction fee"
hint="Total transaction fee." hint="Total transaction fee."
> >
<Text>{ tx.fee.value } Ether</Text> <Text>{ tx.fee.value } { selectedNetwork?.currency }</Text>
<Text variant="secondary" ml={ 1 }>(${ tx.fee.value_usd.toFixed(2) })</Text> <Text variant="secondary" ml={ 1 }>(${ tx.fee.value_usd.toFixed(2) })</Text>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Gas price" title="Gas price"
hint="Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage." hint="Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage."
> >
<Text>{ tx.gas_price.toLocaleString('en', { minimumFractionDigits: 18 }) } Ether</Text> <Text mr={ 1 }>{ tx.gas_price.toLocaleString('en', { minimumFractionDigits: 18 }) } { selectedNetwork?.currency }</Text>
<Text variant="secondary" ml={ 1 }>({ (tx.gas_price * Math.pow(10, 18)).toFixed(0) } Gwei)</Text> <Text variant="secondary">({ (tx.gas_price * Math.pow(10, 18)).toFixed(0) } Gwei)</Text>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Gas limit & usage by txn" title="Gas limit & usage by txn"
hint="Actual gas amount used by the transaction." hint="Actual gas amount used by the transaction."
> >
<Text>{ tx.gas_used.toLocaleString('en') }</Text> <Text>{ tx.gas_used.toLocaleString('en') }</Text>
<Text { ...leftSeparatorStyles }>{ tx.gas_limit.toLocaleString('en') }</Text> <TextSeparator/>
<Text >{ tx.gas_limit.toLocaleString('en') }</Text>
<Utilization ml={ 4 } value={ tx.gas_used / tx.gas_limit }/> <Utilization ml={ 4 } value={ tx.gas_used / tx.gas_limit }/>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
...@@ -143,25 +160,27 @@ const TxDetails = () => { ...@@ -143,25 +160,27 @@ const TxDetails = () => {
<Box> <Box>
<Text as="span" fontWeight="500">Base: </Text> <Text as="span" fontWeight="500">Base: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.base }</Text> <Text fontWeight="600" as="span">{ tx.gas_fees.base }</Text>
<TextSeparator/>
</Box> </Box>
<Box { ...leftSeparatorStyles }> <Box>
<Text as="span" fontWeight="500">Max: </Text> <Text as="span" fontWeight="500">Max: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.max }</Text> <Text fontWeight="600" as="span">{ tx.gas_fees.max }</Text>
<TextSeparator/>
</Box> </Box>
<Box { ...leftSeparatorStyles }> <Box>
<Text as="span" fontWeight="500">Max priority: </Text> <Text as="span" fontWeight="500">Max priority: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.max_priority }</Text> <Text fontWeight="600" as="span">{ tx.gas_fees.max_priority }</Text>
</Box> </Box>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Burnt fees" title="Burnt fees"
hint="Amount of ETH burned for this transaction. Equals Block Base Fee per Gas * Gas Used." hint={ `Amount of ${ selectedNetwork?.currency } burned for this transaction. Equals Block Base Fee per Gas * Gas Used.` }
> >
<Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/> <Icon as={ flameIcon } boxSize={ 5 } color="gray.500"/>
<Text ml={ 1 }>{ tx.burnt_fees.value.toLocaleString('en', { minimumFractionDigits: 18 }) } Ether</Text> <Text ml={ 1 } mr={ 1 }>{ tx.burnt_fees.value.toLocaleString('en', { minimumFractionDigits: 18 }) } { selectedNetwork?.currency }</Text>
<Text variant="secondary" ml={ 1 }>(${ tx.burnt_fees.value_usd.toFixed(2) })</Text> <Text variant="secondary">(${ tx.burnt_fees.value_usd.toFixed(2) })</Text>
</DetailsInfoItem> </DetailsInfoItem>
<GridItem colSpan={ 2 }> <GridItem colSpan={{ base: undefined, lg: 2 }}>
<Element name="TxDetails__cutLink"> <Element name="TxDetails__cutLink">
<Link <Link
mt={ 6 } mt={ 6 }
...@@ -177,8 +196,8 @@ const TxDetails = () => { ...@@ -177,8 +196,8 @@ const TxDetails = () => {
</GridItem> </GridItem>
{ isExpanded && ( { isExpanded && (
<> <>
<GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>
<DetailsInfoItem <DetailsInfoItem
mt={ 4 }
title="Other" title="Other"
hint="Other data related to this transaction." hint="Other data related to this transaction."
> >
...@@ -186,12 +205,14 @@ const TxDetails = () => { ...@@ -186,12 +205,14 @@ const TxDetails = () => {
<Text as="span" fontWeight="500">Txn type: </Text> <Text as="span" fontWeight="500">Txn type: </Text>
<Text fontWeight="600" as="span">{ tx.type.value }</Text> <Text fontWeight="600" as="span">{ tx.type.value }</Text>
<Text fontWeight="400" as="span" ml={ 1 }>({ tx.type.eip })</Text> <Text fontWeight="400" as="span" ml={ 1 }>({ tx.type.eip })</Text>
<TextSeparator/>
</Box> </Box>
<Box { ...leftSeparatorStyles }> <Box>
<Text as="span" fontWeight="500">Nonce: </Text> <Text as="span" fontWeight="500">Nonce: </Text>
<Text fontWeight="600" as="span">{ tx.nonce }</Text> <Text fontWeight="600" as="span">{ tx.nonce }</Text>
<TextSeparator/>
</Box> </Box>
<Box { ...leftSeparatorStyles }> <Box>
<Text as="span" fontWeight="500">Position: </Text> <Text as="span" fontWeight="500">Position: </Text>
<Text fontWeight="600" as="span">{ tx.position }</Text> <Text fontWeight="600" as="span">{ tx.position }</Text>
</Box> </Box>
...@@ -204,7 +225,7 @@ const TxDetails = () => { ...@@ -204,7 +225,7 @@ const TxDetails = () => {
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Decoded input data" title="Decoded input data"
hint="hmmmmmmmmmmm" hint="Decoded input data"
> >
<TxDecodedInputData/> <TxDecodedInputData/>
</DetailsInfoItem> </DetailsInfoItem>
......
import { Box, Table, Thead, Tbody, Tr, Th, TableContainer } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TxInternalsType } from 'types/api/tx';
import type ArrayElement from 'types/utils/ArrayElement';
import { data } from 'data/txInternal'; import { data } from 'data/txInternal';
import Filters from 'ui/shared/Filters'; import useIsMobile from 'lib/hooks/useIsMobile';
import TxInternalsTableItem from 'ui/tx/internals/TxInternalsTableItem'; import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import FilterInput from 'ui/shared/FilterInput';
import TxInternalsFilter from 'ui/tx/internals/TxInternalsFilter';
import TxInternalsList from 'ui/tx/internals/TxInternalsList';
import TxInternalsTable from 'ui/tx/internals/TxInternalsTable';
const searchFn = (searchTerm: string) => (item: ArrayElement<typeof data>): boolean => {
const formattedSearchTerm = searchTerm.toLowerCase();
return item.type.toLowerCase().includes(formattedSearchTerm) ||
item.from.hash.toLowerCase().includes(formattedSearchTerm) ||
item.to.hash.toLowerCase().includes(formattedSearchTerm);
};
const TxInternals = () => { const TxInternals = () => {
const [ filters, setFilters ] = React.useState<Array<TxInternalsType>>([]);
const [ searchTerm, setSearchTerm ] = React.useState<string>('');
const isMobile = useIsMobile();
const handleFilterChange = React.useCallback((nextValue: Array<TxInternalsType>) => {
setFilters(nextValue);
}, []);
const content = (() => {
const filteredData = data
.filter(({ type }) => filters.length > 0 ? filters.includes(type) : true)
.filter(searchFn(searchTerm));
if (filteredData.length === 0) {
return <EmptySearchResult text={ `Couldn${ apos }t find any transaction that matches your query.` }/>;
}
return isMobile ? <TxInternalsList data={ filteredData }/> : <TxInternalsTable data={ filteredData }/>;
})();
return ( return (
<Box> <Box>
<Filters/> <Flex mb={ 6 }>
<TableContainer width="100%" mt={ 6 }> <TxInternalsFilter onFilterChange={ handleFilterChange } defaultFilters={ filters } appliedFiltersNum={ filters.length }/>
<Table variant="simple" minWidth="950px" size="sm"> <FilterInput onChange={ setSearchTerm } maxW="360px" ml={ 3 } size="xs" placeholder="Search by addresses, hash, method..."/>
<Thead> </Flex>
<Tr> { content }
<Th width="20%">Type</Th>
<Th width="calc(20% + 40px)" pr="0">From</Th>
<Th width="calc(20% - 40px)" pl="0">To</Th>
<Th width="20%" isNumeric>Value</Th>
<Th width="20%" isNumeric>Gas limit</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<TxInternalsTableItem
key={ item.id }
{ ...item }
/>
)) }
</Tbody>
</Table>
</TableContainer>
</Box> </Box>
); );
}; };
......
import { import { Accordion, Text } from '@chakra-ui/react';
Accordion,
Text,
Table,
Thead,
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { data } from 'data/txState'; import useIsMobile from 'lib/hooks/useIsMobile';
import TxStateList from 'ui/tx/state/TxStateList';
import TxStateTableItem from './state/TxStateTableItem'; import TxStateTable from 'ui/tx/state/TxStateTable';
const CURRENCY = 'ETH';
const TxState = () => { const TxState = () => {
const isMobile = useIsMobile();
const list = isMobile ? <TxStateList/> : <TxStateTable/>;
return ( return (
<> <>
<Text> <Text>
A set of information that represents the current state is updated when a transaction takes place on the network. The below is a summary of those changes A set of information that represents the current state is updated when a transaction takes place on the network. The below is a summary of those changes
</Text> </Text>
<Accordion allowToggle allowMultiple> <Accordion allowMultiple defaultIndex={ [] }>
<TableContainer width="100%" mt={ 6 }> { list }
<Table variant="simple" minWidth="950px" size="sm">
<Thead>
<Tr>
<Th width="92px">Storage</Th>
<Th width="146px">Address</Th>
<Th width="120px">Miner</Th>
<Th width="33%" isNumeric>{ `After ${ CURRENCY }` }</Th>
<Th width="33%" isNumeric>{ `Before ${ CURRENCY }` }</Th>
<Th width="33%" isNumeric>{ `State difference ${ CURRENCY }` }</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item, index) => <TxStateTableItem txStateItem={ item } key={ index }/>) }
</Tbody>
</Table>
</TableContainer>
</Accordion> </Accordion>
</> </>
); );
......
import { Tag, TagLabel, TagLeftIcon } from '@chakra-ui/react';
import React from 'react';
import errorIcon from 'icons/status/error.svg';
import successIcon from 'icons/status/success.svg';
export interface Props {
status: 'success' | 'error';
}
const TxStatus = ({ status }: Props) => {
const label = status === 'success' ? 'Success' : 'Error';
const icon = status === 'success' ? successIcon : errorIcon;
const colorScheme = status === 'success' ? 'green' : 'red';
return (
<Tag colorScheme={ colorScheme } display="inline-flex">
<TagLeftIcon boxSize={ 2.5 } as={ icon }/>
<TagLabel>{ label }</TagLabel>
</Tag>
);
};
export default TxStatus;
import { Popover, PopoverTrigger, PopoverContent, PopoverBody, CheckboxGroup, Checkbox, Text, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import type { TxInternalsType } from 'types/api/tx';
import FilterButton from 'ui/shared/FilterButton';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
interface Props {
appliedFiltersNum?: number;
defaultFilters: Array<TxInternalsType>;
onFilterChange: (nextValue: Array<TxInternalsType>) => void;
}
const TxInternalsFilter = ({ onFilterChange, defaultFilters, appliedFiltersNum }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<FilterButton
isActive={ isOpen || Number(appliedFiltersNum) > 0 }
onClick={ onToggle }
appliedFiltersNum={ appliedFiltersNum }
/>
</PopoverTrigger>
<PopoverContent w={{ md: '100%', lg: '438px' }}>
<PopoverBody px={ 4 } py={ 6 } display="grid" gridTemplateColumns="1fr 1fr" rowGap={ 5 }>
<CheckboxGroup size="lg" onChange={ onFilterChange } defaultValue={ defaultFilters }>
{ TX_INTERNALS_ITEMS.map(({ title, id }) => <Checkbox key={ id } value={ id }><Text fontSize="md">{ title }</Text></Checkbox>) }
</CheckboxGroup>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(TxInternalsFilter);
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { data as txData } from 'data/txInternal';
import useNetwork from 'lib/hooks/useNetwork';
import TxInternalsListItem from 'ui/tx/internals/TxInternalsListItem';
const TxInternalsList = ({ data }: { data: typeof txData}) => {
const selectedNetwork = useNetwork();
return (
<Box mt={ 6 }>
{ data.map((item) => <TxInternalsListItem key={ item.id } { ...item } currency={ selectedNetwork?.currency }/>) }
</Box>
);
};
export default TxInternalsList;
import { Flex, Tag, Icon, Box, HStack, Text } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { data } from 'data/txInternal';
import eastArrowIcon from 'icons/arrows/east.svg';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/shared/TxStatus';
type Props = ArrayElement<typeof data> & { currency?: string };
const TxInternalsListItem = ({ type, status, from, to, value, gasLimit, currency }: Props) => {
return (
<AccountListItemMobile rowGap={ 3 }>
<Flex>
<Tag colorScheme="cyan" mr={ 2 }>{ capitalize(type) }</Tag>
<TxStatus status={ status }/>
</Flex>
<Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)">
<AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/>
</Address>
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
<Address width="calc((100% - 48px) / 2)">
<AddressIcon hash={ to.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to.hash }/>
</Address>
</Box>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Value { currency }</Text>
<Text fontSize="sm" variant="secondary">{ value }</Text>
</HStack>
<HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Gas limit</Text>
<Text fontSize="sm" variant="secondary">{ gasLimit.toLocaleString('en') }</Text>
</HStack>
</AccountListItemMobile>
);
};
export default TxInternalsListItem;
import { Table, Thead, Tbody, Tr, Th, TableContainer } from '@chakra-ui/react';
import React from 'react';
import type { data as txData } from 'data/txInternal';
import useNetwork from 'lib/hooks/useNetwork';
import TxInternalsTableItem from 'ui/tx/internals/TxInternalsTableItem';
const TxInternalsTable = ({ data }: { data: typeof txData}) => {
const selectedNetwork = useNetwork();
return (
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" size="sm">
<Thead>
<Tr>
<Th width="28%">Type</Th>
<Th width="20%">From</Th>
<Th width="24px" px={ 0 }/>
<Th width="20%">To</Th>
<Th width="16%" isNumeric>Value { selectedNetwork?.currency }</Th>
<Th width="16%" isNumeric>Gas limit</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item) => (
<TxInternalsTableItem key={ item.id } { ...item }/>
)) }
</Tbody>
</Table>
</TableContainer>
);
};
export default TxInternalsTable;
import { Tr, Td, Tag, Icon } from '@chakra-ui/react'; import { Tr, Td, Tag, Icon, Box } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
import rightArrowIcon from 'icons/arrows/right.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/tx/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
interface Props { interface Props {
type: string; type: string;
status: 'success' | 'error'; status: 'success' | 'failed' | 'pending';
from: string; from: { hash: string; alias?: string};
to: string; to: { hash: string; alias?: string};
value: number; value: number;
gasLimit: number; gasLimit: number;
} }
const TxInternalTableItem = ({ type, status, from, to, value, gasLimit }: Props) => { const TxInternalTableItem = ({ type, status, from, to, value, gasLimit }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
return ( return (
<Tr alignItems="top"> <Tr alignItems="top">
<Td> <Td>
<Tag colorScheme="cyan" mr={ 2 }>{ capitalize(type) }</Tag> { typeTitle && (
<Box w="126px" display="inline-block">
<Tag colorScheme="cyan" mr={ 5 }>{ typeTitle }</Tag>
</Box>
) }
<TxStatus status={ status }/> <TxStatus status={ status }/>
</Td> </Td>
<Td pr="0"> <Td>
<Address> <Address>
<AddressIcon hash={ from }/> <AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from }/> <AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.alias } flexGrow={ 1 }/>
<Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } flexShrink={ 0 } color="gray.500"/>
</Address> </Address>
</Td> </Td>
<Td pl="0"> <Td px={ 0 }>
<Icon as={ rightArrowIcon } boxSize={ 6 } color="gray.500"/>
</Td>
<Td>
<Address> <Address>
<AddressIcon hash={ to }/> <AddressIcon hash={ to.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ to }/> <AddressLink hash={ to.hash } alias={ to.alias } fontWeight="500" ml={ 2 }/>
</Address> </Address>
</Td> </Td>
<Td isNumeric> <Td isNumeric>
......
import type { TxInternalsType } from 'types/api/tx';
interface TxInternalsTypeItem {
title: string;
id: TxInternalsType;
}
export const TX_INTERNALS_ITEMS: Array<TxInternalsTypeItem> = [
{ title: 'Call', id: 'call' },
{ title: 'Delegate call', id: 'delegate_call' },
{ title: 'Static call', id: 'static_call' },
{ title: 'Create', id: 'create' },
{ title: 'Create2', id: 'create2' },
{ title: 'Self-destruct', id: 'self_destruct' },
{ title: 'Reward', id: 'reward' },
];
import { SearchIcon } from '@chakra-ui/icons'; import { Text, Grid, GridItem, Link, Tooltip, Button, Icon, useColorModeValue } from '@chakra-ui/react';
import { Text, Grid, GridItem, Link, Tooltip, Button, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import searchIcon from 'icons/search.svg';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
...@@ -15,14 +15,29 @@ interface Props { ...@@ -15,14 +15,29 @@ interface Props {
index: number; index: number;
} }
const RowHeader = ({ children }: { children: React.ReactNode }) => <GridItem><Text fontWeight={ 500 }>{ children }</Text></GridItem>; const RowHeader = ({ children }: { children: React.ReactNode }) => (
<GridItem _notFirst={{ my: { base: 4, lg: 0 } }}>
<Text fontWeight={ 500 }>{ children }</Text>
</GridItem>
);
const TxLogItem = ({ address, index, topics, data }: Props) => { const TxLogItem = ({ address, index, topics, data }: Props) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const dataBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const dataBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
return ( return (
<Grid gridTemplateColumns="200px 1fr" gap={ 8 } py={ 8 } _notFirst={{ borderTopWidth: '1px', borderTopColor: borderColor }}> <Grid
gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: '200px minmax(0, 1fr)' }}
gap={{ base: 2, lg: 8 }}
py={ 8 }
_notFirst={{
borderTopWidth: '1px',
borderTopColor: borderColor,
}}
_first={{
pt: 0,
}}
>
<RowHeader>Address</RowHeader> <RowHeader>Address</RowHeader>
<GridItem display="flex" alignItems="center"> <GridItem display="flex" alignItems="center">
<Address> <Address>
...@@ -30,12 +45,12 @@ const TxLogItem = ({ address, index, topics, data }: Props) => { ...@@ -30,12 +45,12 @@ const TxLogItem = ({ address, index, topics, data }: Props) => {
<AddressLink hash={ address } ml={ 2 }/> <AddressLink hash={ address } ml={ 2 }/>
</Address> </Address>
<Tooltip label="Find matches topic"> <Tooltip label="Find matches topic">
<Link ml={ 2 }> <Link ml={ 2 } display="inline-flex">
<SearchIcon w={ 5 } h={ 5 }/> <Icon as={ searchIcon } boxSize={ 5 }/>
</Link> </Link>
</Tooltip> </Tooltip>
<Tooltip label="Log index"> <Tooltip label="Log index">
<Button variant="outline" isActive ml="auto" size="sm" fontWeight={ 400 }> <Button variant="outline" colorScheme="gray" isActive ml={{ base: 9, lg: 'auto' }} size="sm" fontWeight={ 400 }>
{ index } { index }
</Button> </Button>
</Tooltip> </Tooltip>
......
import { Flex, Button, Text, Select } from '@chakra-ui/react'; import { Flex, Button, Select, Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props { interface Props {
hex: string; hex: string;
index: number; index: number;
...@@ -17,18 +19,29 @@ const TxLogTopic = ({ hex, index }: Props) => { ...@@ -17,18 +19,29 @@ const TxLogTopic = ({ hex, index }: Props) => {
}, []); }, []);
return ( return (
<Flex alignItems="center" px={ 3 } _notFirst={{ mt: 3 }}> <Flex alignItems="center" px={{ base: 0, lg: 3 }} _notFirst={{ mt: 3 }} overflow="hidden" maxW="100%">
<Button variant="outline" isActive size="xs" fontWeight={ 400 } mr={ 3 } w={ 6 }> <Button variant="outline" colorScheme="gray" isActive size="xs" fontWeight={ 400 } mr={ 3 } w={ 6 }>
{ index } { index }
</Button> </Button>
{ /* temporary condition juse to show different states of the component */ } { /* temporary condition juse to show different states of the component */ }
{ /* delete when ther will be real data */ } { /* delete when ther will be real data */ }
{ index > 0 && ( { index > 0 && (
<Select size="sm" borderRadius="base" value={ selectedDataType } onChange={ handleSelectChange } focusBorderColor="none" w="75px" mr={ 3 }> <Select
size="sm"
borderRadius="base"
value={ selectedDataType }
onChange={ handleSelectChange }
focusBorderColor="none"
w="75px"
mr={ 3 }
flexShrink={ 0 }
>
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) } { OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
</Select> </Select>
) } ) }
<Text>{ hex }</Text> <Box overflow="hidden" whiteSpace="nowrap">
<HashStringShortenDynamic hash={ hex }/>
</Box>
</Flex> </Flex>
); );
}; };
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import { data } from 'data/txState';
import TxStateListItem from 'ui/tx/state/TxStateListItem';
const TxStateList = () => {
return (
<Box mt={ 6 }>
{ data.map((item, index) => <TxStateListItem key={ index } { ...item }/>) }
</Box>
);
};
export default TxStateList;
import { AccordionItem, AccordionButton, AccordionIcon, Button, Flex, Text, Link, StatArrow, Stat, AccordionPanel } from '@chakra-ui/react';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { data } from 'data/txState';
import useNetwork from 'lib/hooks/useNetwork';
import { nbsp } from 'lib/html-entities';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TextSeparator from 'ui/shared/TextSeparator';
import TxStateStorageItem from './TxStateStorageItem';
type Props = ArrayElement<typeof data>;
const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props) => {
const selectedNetwork = useNetwork();
const hasStorageData = Boolean(storage?.length);
return (
<AccountListItemMobile>
<AccordionItem isDisabled={ !hasStorageData } border={ 0 } w="100%" display="flex" flexDirection="column" rowGap={ 3 }>
{ ({ isExpanded }) => (
<>
<Flex>
<Address flexGrow={ 1 }>
<AddressIcon hash={ address }/>
<AddressLink hash={ address } fontWeight="500" ml={ 2 }/>
</Address>
<AccordionButton
_hover={{ background: 'unset' }}
padding="0"
ml={ 4 }
w="auto"
>
<Button
variant="outline"
borderWidth="1px"
// button can't be inside button (AccordionButton)
as="div"
isActive={ isExpanded }
size="sm"
fontWeight={ 400 }
isDisabled={ !hasStorageData }
colorScheme="gray"
// AccordionButton has its own opacity rule when disabled
_disabled={{ opacity: 1 }}
>
{ storage?.length || '0' }
</Button>
<AccordionIcon color="blue.600" width="30px"/>
</AccordionButton>
</Flex>
<Flex rowGap={ 2 } flexDir="column" fontSize="sm">
<Text fontWeight={ 600 }>Miner</Text>
<Link>{ miner }</Link>
</Flex>
<Flex rowGap={ 2 } flexDir="column" fontSize="sm">
<Text fontWeight={ 600 }>Before</Text>
<Flex>
<Text>{ before.balance } { selectedNetwork?.currency }</Text>
<TextSeparator/>
{ typeof before.nonce !== 'undefined' && <Text>Nonce:{ nbsp }{ before.nonce }</Text> }
</Flex>
</Flex>
<Flex rowGap={ 2 } flexDir="column" fontSize="sm">
<Text fontWeight={ 600 }>After</Text>
<Text>{ after.balance } { selectedNetwork?.currency }</Text>
{ typeof after.nonce !== 'undefined' && <Text>Nonce:{ nbsp }{ after.nonce }</Text> }
</Flex>
<Flex rowGap={ 2 } flexDir="column" fontSize="sm">
<Text fontWeight={ 600 }>State difference</Text>
<Stat>
{ diff } { selectedNetwork?.currency }
<StatArrow ml={ 2 } type={ Number(diff) > 0 ? 'increase' : 'decrease' }/>
</Stat>
</Flex>
{ hasStorageData && (
<AccordionPanel fontWeight={ 500 } p={ 0 }>
{ storage?.map((storageItem, index) => <TxStateStorageItem key={ index } storageItem={ storageItem }/>) }
</AccordionPanel>
) }
</>
) }
</AccordionItem>
</AccountListItemMobile>
);
};
export default TxStateListItem;
...@@ -2,6 +2,7 @@ import { ...@@ -2,6 +2,7 @@ import {
Grid, Grid,
GridItem, GridItem,
Select, Select,
Box,
useColorModeValue, useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -20,7 +21,7 @@ const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage}) ...@@ -20,7 +21,7 @@ const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage})
const OPTIONS = [ 'Hex', 'Number', 'Text', 'Address' ]; const OPTIONS = [ 'Hex', 'Number', 'Text', 'Address' ];
return ( return (
<Grid <Grid
gridTemplateColumns="auto 1fr" gridTemplateColumns={{ base: '70px minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}
columnGap={ 3 } columnGap={ 3 }
rowGap={ 4 } rowGap={ 4 }
px={ 6 } px={ 6 }
...@@ -28,11 +29,12 @@ const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage}) ...@@ -28,11 +29,12 @@ const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage})
background="blackAlpha.50" background="blackAlpha.50"
borderRadius="12px" borderRadius="12px"
mb={ 4 } mb={ 4 }
fontSize="sm"
> >
{ gridData.map((item) => ( { gridData.map((item) => (
<> <React.Fragment key={ item.name }>
<GridItem alignSelf="center" fontWeight={ 600 } textAlign="end">{ item.name }</GridItem> <GridItem alignSelf={{ base: 'start', lg: 'center' }} fontWeight={{ base: 500, lg: 600 }} textAlign="end">{ item.name }</GridItem>
<GridItem> <GridItem display="flex" flexDir={{ base: 'column', lg: 'row' }} rowGap={ 2 } alignItems={{ base: 'flex-start', lg: 'center' }} >
{ item.select && ( { item.select && (
<Select <Select
size="sm" size="sm"
...@@ -46,9 +48,11 @@ const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage}) ...@@ -46,9 +48,11 @@ const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage})
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) } { OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
</Select> </Select>
) } ) }
{ item.value } <Box fontWeight={{ base: 400, lg: 500 }} maxW="100%">
{ item.value }
</Box>
</GridItem> </GridItem>
</> </React.Fragment>
)) } )) }
</Grid> </Grid>
); );
......
import {
Table,
Thead,
Tbody,
Tr,
Th,
TableContainer,
} from '@chakra-ui/react';
import React from 'react';
import { data } from 'data/txState';
import useNetwork from 'lib/hooks/useNetwork';
import TxStateTableItem from 'ui/tx/state/TxStateTableItem';
const TxStateTable = () => {
const selectedNetwork = useNetwork();
return (
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="950px" size="sm">
<Thead>
<Tr>
<Th width="92px">Storage</Th>
<Th width="146px">Address</Th>
<Th width="120px">Miner</Th>
<Th width="33%" isNumeric>{ `After ${ selectedNetwork?.currency }` }</Th>
<Th width="33%" isNumeric>{ `Before ${ selectedNetwork?.currency }` }</Th>
<Th width="33%" isNumeric>{ `State difference ${ selectedNetwork?.currency }` }</Th>
</Tr>
</Thead>
<Tbody>
{ data.map((item, index) => <TxStateTableItem txStateItem={ item } key={ index }/>) }
</Tbody>
</Table>
</TableContainer>
);
};
export default TxStateTable;
import { Box, Heading, Text, Flex, Link, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { txs } from 'data/txs';
import useNetwork from 'lib/hooks/useNetwork';
import useLink from 'lib/link/useLink';
import TextSeparator from 'ui/shared/TextSeparator';
import Utilization from 'ui/shared/Utilization';
const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => {
const selectedNetwork = useNetwork();
const sectionBorderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
const sectionProps = {
borderBottom: '1px solid',
borderColor: sectionBorderColor,
paddingBottom: 4,
};
const sectionTitleProps = {
color: 'gray.500',
fontWeight: 600,
marginBottom: 3,
};
const link = useLink();
return (
<>
<Heading as="h4" fontSize="18px" mb={ 6 }>Additional info </Heading>
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Transaction fee</Text>
<Flex>
<Text>{ tx.fee.value } { selectedNetwork?.currency }</Text>
<Text variant="secondary" ml={ 1 }>(${ tx.fee.value_usd.toFixed(2) })</Text>
</Flex>
</Box>
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Gas limit & usage by transaction</Text>
<Flex>
<Text>{ tx.gas_used.toLocaleString('en') }</Text>
<TextSeparator/>
<Text>{ tx.gas_limit.toLocaleString('en') }</Text>
<Utilization ml={ 4 } value={ tx.gas_used / tx.gas_limit }/>
</Flex>
</Box>
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Gas fees (Gwei)</Text>
<Box>
<Text as="span" fontWeight="500">Base: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.base }</Text>
</Box>
<Box>
<Text as="span" fontWeight="500">Max: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.max }</Text>
</Box>
<Box>
<Text as="span" fontWeight="500">Max priority: </Text>
<Text fontWeight="600" as="span">{ tx.gas_fees.max_priority }</Text>
</Box>
</Box>
<Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Others</Text>
<Box>
<Text as="span" fontWeight="500">Txn type: </Text>
<Text fontWeight="600" as="span">{ tx.type.value }</Text>
<Text fontWeight="400" as="span" ml={ 1 }>({ tx.type.eip })</Text>
</Box>
<Box>
<Text as="span" fontWeight="500">Nonce: </Text>
<Text fontWeight="600" as="span">{ tx.nonce }</Text>
</Box>
<Box>
<Text as="span" fontWeight="500">Position: </Text>
<Text fontWeight="600" as="span">{ tx.position }</Text>
</Box>
</Box>
<Link href={ link('tx_index', { id: tx.hash }) }>More details</Link>
</>
);
};
export default TxAdditionalInfo;
import {
Icon,
Center,
useColorModeValue,
} from '@chakra-ui/react';
import React from 'react';
import infoIcon from 'icons/info.svg';
const TxAdditionalInfoButton = ({ isOpen, onClick }: {isOpen?: boolean; onClick?: () => void}, ref: React.ForwardedRef<HTMLDivElement>) => {
const infoBgColor = useColorModeValue('blue.50', 'gray.600');
const infoColor = useColorModeValue('blue.600', 'blue.300');
return (
<Center ref={ ref } background={ isOpen ? infoBgColor : 'unset' } borderRadius="8px" w="30px" h="30px" onClick={ onClick }>
<Icon
as={ infoIcon }
boxSize={ 5 }
color={ infoColor }
_hover={{ color: 'blue.400' }}
/>
</Center>
);
};
export default React.forwardRef(TxAdditionalInfoButton);
import { Tag } from '@chakra-ui/react';
import React from 'react';
export interface Props {
type: 'contract-call' | 'transaction' | 'token-transfer' | 'internal-tx' | 'multicall';
}
const TxStatus = ({ type }: Props) => {
let label;
let colorScheme;
switch (type) {
case 'contract-call':
label = 'Contract call';
colorScheme = 'blue';
break;
case 'transaction':
label = 'Transaction';
colorScheme = 'purple';
break;
case 'token-transfer':
label = 'Token transfer';
colorScheme = 'orange';
break;
case 'internal-tx':
label = 'Internal txn';
colorScheme = 'cyan';
break;
case 'multicall':
label = 'Multicall';
colorScheme = 'teal';
break;
}
return (
<Tag colorScheme={ colorScheme }>
{ label }
</Tag>
);
};
export default TxStatus;
import { Box, HStack, Show } from '@chakra-ui/react';
import React, { useCallback, useEffect, useState } from 'react';
import type { Sort } from 'types/client/txs-sort';
import { txs } from 'data/txs';
import FilterButton from 'ui/shared/FilterButton';
import FilterInput from 'ui/shared/FilterInput';
import Pagination from 'ui/shared/Pagination';
import SortButton from 'ui/shared/SortButton';
import TxsListItem from './TxsListItem';
import TxsTable from './TxsTable';
type Props = {
isPending?: boolean;
}
const TxsContent = ({ isPending }: Props) => {
const [ sorting, setSorting ] = useState<Sort>();
const [ sortedTxs, setSortedTxs ] = useState(txs);
// sorting should be preserved with pagination!
const sort = useCallback((field: 'val' | 'fee') => () => {
if (field === 'val') {
setSorting((prevVal => {
if (prevVal === 'val-asc') {
return undefined;
}
if (prevVal === 'val-desc') {
return 'val-asc';
}
return 'val-desc';
}));
}
if (field === 'fee') {
setSorting((prevVal => {
if (prevVal === 'fee-asc') {
return undefined;
}
if (prevVal === 'fee-desc') {
return 'fee-asc';
}
return 'fee-desc';
}));
}
}, []);
useEffect(() => {
switch (sorting) {
case 'val-desc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => tx1.amount.value - tx2.amount.value));
break;
case 'val-asc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => tx2.amount.value - tx1.amount.value));
break;
case 'fee-desc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => tx1.fee.value - tx2.fee.value));
break;
case 'fee-asc':
setSortedTxs([ ...txs ].sort((tx1, tx2) => tx2.fee.value - tx1.fee.value));
break;
default:
setSortedTxs(txs);
}
}, [ sorting ]);
return (
<>
{ !isPending && <Box mb={ 12 }>Only the first 10,000 elements are displayed</Box> }
<HStack mb={ 6 }>
{ /* TODO */ }
<FilterButton
isActive={ false }
// eslint-disable-next-line react/jsx-no-bind
onClick={ () => {} }
appliedFiltersNum={ 0 }
/>
<SortButton
// eslint-disable-next-line react/jsx-no-bind
handleSort={ () => {} }
isSortActive={ Boolean(sorting) }
display={{ base: 'block', lg: 'none' }}
/>
<FilterInput
// eslint-disable-next-line react/jsx-no-bind
onChange={ () => {} }
maxW="360px"
size="xs"
placeholder="Search by addresses, hash, method..."
/>
</HStack>
<Show below="lg">{ sortedTxs.map(tx => <TxsListItem tx={ tx } key={ tx.hash }/>) }</Show>
<Show above="lg"><TxsTable txs={ sortedTxs } sort={ sort } sorting={ sorting }/></Show>
<Box mx={{ base: 0, lg: 6 }} my={{ base: 6, lg: 3 }}>
<Pagination currentPage={ 1 }/>
</Box>
</>
);
};
export default TxsContent;
import {
HStack,
Box,
Flex,
Icon,
Link,
Modal,
ModalContent,
ModalCloseButton,
Text,
useColorModeValue,
useDisclosure } from '@chakra-ui/react';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { txs } from 'data/txs';
import rightArrowIcon from 'icons/arrows/east.svg';
import transactionIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import useNetwork from 'lib/hooks/useNetwork';
import useLink from 'lib/link/useLink';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxAdditionalInfoButton from 'ui/txs/TxAdditionalInfoButton';
import TxType from 'ui/txs/TxType';
const TxsListItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
const { isOpen, onOpen, onClose } = useDisclosure();
const selectedNetwork = useNetwork();
const iconColor = useColorModeValue('blue.600', 'blue.300');
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const link = useLink();
return (
<>
<Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor: { borderColor } }}>
<Flex justifyContent="space-between" mt={ 4 }>
<HStack>
<TxType type={ tx.txType }/>
<TxStatus status={ tx.status } errorText={ tx.errorText }/>
</HStack>
<TxAdditionalInfoButton onClick={ onOpen }/>
</Flex>
<Flex justifyContent="space-between" lineHeight="24px" mt={ 3 }>
<Flex>
<Icon
as={ transactionIcon }
boxSize="30px"
mr={ 2 }
color={ iconColor }
/>
<Address width="100%">
<AddressLink
hash={ tx.hash }
type="transaction"
fontWeight="700"
truncation="constant"
/>
</Address>
</Flex>
<Text variant="secondary" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text>
</Flex>
<Flex mt={ 3 }>
<Text as="span" whiteSpace="pre">Method </Text>
<Text
as="span"
variant="secondary"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
{ tx.method }
</Text>
</Flex>
<Box mt={ 2 }>
<Text as="span">Block </Text>
<Link href={ link('block', { id: tx.block_num.toString() }) }>{ tx.block_num }</Link>
</Box>
<Flex alignItems="center" height={ 6 } mt={ 6 }>
<Address width="calc((100%-40px)/2)">
<AddressIcon hash={ tx.address_from.hash }/>
<AddressLink
hash={ tx.address_from.hash }
alias={ tx.address_from.alias }
fontWeight="500"
ml={ 2 }
/>
</Address>
<Icon
as={ rightArrowIcon }
boxSize={ 6 }
mx={ 2 }
color="gray.500"
/>
<Address width="calc((100%-40px)/2)">
<AddressIcon hash={ tx.address_to.hash }/>
<AddressLink
hash={ tx.address_to.hash }
alias={ tx.address_to.alias }
fontWeight="500"
ml={ 2 }
/>
</Address>
</Flex>
<Box mt={ 2 }>
<Text as="span">Value { selectedNetwork?.currency } </Text>
<Text as="span" variant="secondary">{ tx.amount.value.toFixed(8) }</Text>
</Box>
<Box mt={ 2 } mb={ 3 }>
<Text as="span">Fee { selectedNetwork?.currency } </Text>
<Text as="span" variant="secondary">{ tx.fee.value.toFixed(8) }</Text>
</Box>
</Box>
<Modal isOpen={ isOpen } onClose={ onClose } size="full">
<ModalContent paddingTop={ 4 }>
<ModalCloseButton/>
<TxAdditionalInfo tx={ tx }/>
</ModalContent>
</Modal>
</>
);
};
export default TxsListItem;
import React from 'react';
import TxsContent from './TxsContent';
const TxsPending = () => {
return <TxsContent isPending/>;
};
export default TxsPending;
import { Link, Table, Thead, Tbody, Tr, Th, TableContainer, Icon } from '@chakra-ui/react';
import React from 'react';
import type { Sort } from 'types/client/txs-sort';
import type { txs as data } from 'data/txs';
import rightArrowIcon from 'icons/arrows/east.svg';
import useNetwork from 'lib/hooks/useNetwork';
import TxsTableItem from './TxsTableItem';
type Props = {
txs: typeof data;
sort: (field: 'val' | 'fee') => () => void;
sorting: Sort;
}
const TxsTable = ({ txs, sort, sorting }: Props) => {
const selectedNetwork = useNetwork();
return (
<TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="810px" size="xs">
<Thead>
<Tr>
<Th width="54px"></Th>
<Th width="20%">Type</Th>
<Th width="18%">Txn hash</Th>
<Th width="15%">Method</Th>
<Th width="11%">Block</Th>
<Th width={{ xl: '128px', base: '58px' }}>From</Th>
<Th width={{ xl: '36px', base: '0' }}></Th>
<Th width={{ xl: '128px', base: '58px' }}>To</Th>
<Th width="18%" isNumeric>
<Link onClick={ sort('val') } display="flex" justifyContent="end">
{ sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'val-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ `Value ${ selectedNetwork?.currency }` }
</Link>
</Th>
<Th width="18%" isNumeric pr={ 5 }>
<Link onClick={ sort('fee') } display="flex" justifyContent="end">
{ sorting === 'fee-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'fee-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ `Fee ${ selectedNetwork?.currency }` }
</Link>
</Th>
</Tr>
</Thead>
<Tbody>
{ txs.map((item) => (
<TxsTableItem
key={ item.hash }
tx={ item }
/>
)) }
</Tbody>
</Table>
</TableContainer>
);
};
export default TxsTable;
import {
Box,
Tr,
Td,
Tag,
Link,
Icon,
VStack,
Text,
Tooltip,
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
Portal,
useColorModeValue,
Show,
} from '@chakra-ui/react';
import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement';
import type { txs } from 'data/txs';
import rightArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs';
import useLink from 'lib/link/useLink';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxAdditionalInfoButton from 'ui/txs/TxAdditionalInfoButton';
import TxType from './TxType';
const TxsTableItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
const link = useLink();
const addressFrom = (
<Address>
<Tooltip label={ tx.address_from.type }>
<Box display="flex"><AddressIcon hash={ tx.address_from.hash }/></Box>
</Tooltip>
<AddressLink hash={ tx.address_from.hash } alias={ tx.address_from.alias } fontWeight="500" ml={ 2 }/>
</Address>
);
const addressTo = (
<Address>
<Tooltip label={ tx.address_to.type }>
<Box display="flex"> <AddressIcon hash={ tx.address_to.hash }/></Box>
</Tooltip>
<AddressLink hash={ tx.address_to.hash } alias={ tx.address_to.alias } fontWeight="500" ml={ 2 }/>
</Address>
);
const infoBorderColor = useColorModeValue('gray.200', 'whiteAlpha.200');
return (
<Tr>
<Td pl={ 4 }>
<Popover placement="right-start" openDelay={ 300 }>
{ ({ isOpen }) => (
<>
<PopoverTrigger>
<TxAdditionalInfoButton isOpen={ isOpen }/>
</PopoverTrigger>
<Portal>
<PopoverContent border="1px solid" borderColor={ infoBorderColor }>
<PopoverBody>
<TxAdditionalInfo tx={ tx }/>
</PopoverBody>
</PopoverContent>
</Portal>
</>
) }
</Popover>
</Td>
<Td>
<VStack alignItems="start">
<TxType type={ tx.txType }/>
<TxStatus status={ tx.status } errorText={ tx.errorText }/>
</VStack>
</Td>
<Td>
<VStack alignItems="start" lineHeight="24px">
<Address width="100%">
<AddressLink
hash={ tx.hash }
type="transaction"
fontWeight="700"
/>
</Address>
<Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text>
</VStack>
</Td>
<Td>
<TruncatedTextTooltip label={ tx.method }>
<Tag
colorScheme={ tx.method === 'Multicall' ? 'teal' : 'gray' }
>
{ tx.method }
</Tag>
</TruncatedTextTooltip>
</Td>
<Td>
<Link href={ link('block', { id: tx.block_num.toString() }) }>{ tx.block_num }</Link>
</Td>
<Show above="xl">
<Td>
{ addressFrom }
</Td>
<Td>
<Icon as={ rightArrowIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/>
</Td>
<Td>
{ addressTo }
</Td>
</Show>
<Show below="xl">
<Td colSpan={ 3 }>
<Box>
{ addressFrom }
<Icon
as={ rightArrowIcon }
boxSize={ 6 }
mt={ 2 }
mb={ 1 }
color="gray.500"
transform="rotate(90deg)"
/>
{ addressTo }
</Box>
</Td>
</Show>
<Td isNumeric>
{ tx.amount.value.toFixed(8) }
</Td>
<Td isNumeric>
{ tx.fee.value.toFixed(8) }
</Td>
</Tr>
);
};
export default TxsTableItem;
import React from 'react';
import TxsContent from './TxsContent';
const TxsValidated = () => {
return <TxsContent/>;
};
export default TxsValidated;
...@@ -3,17 +3,23 @@ import React, { useCallback } from 'react'; ...@@ -3,17 +3,23 @@ import React, { useCallback } from 'react';
import { Controller } from 'react-hook-form'; import { Controller } from 'react-hook-form';
import type { Path, ControllerRenderProps, FieldValues, Control } from 'react-hook-form'; import type { Path, ControllerRenderProps, FieldValues, Control } from 'react-hook-form';
import useNetwork from 'lib/hooks/useNetwork';
import CheckboxInput from 'ui/shared/CheckboxInput'; import CheckboxInput from 'ui/shared/CheckboxInput';
// does it depend on the network? // does it depend on the network?
const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721' ] as const; const NOTIFICATIONS = [ 'native', 'ERC-20', 'ERC-721' ] as const;
const NOTIFICATIONS_NAMES = [ 'xDAI', 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ];
type Props<Inputs extends FieldValues> = { type Props<Inputs extends FieldValues> = {
control: Control<Inputs>; control: Control<Inputs>;
} }
export default function AddressFormNotifications<Inputs extends FieldValues, Checkboxes extends Path<Inputs>>({ control }: Props<Inputs>) { export default function AddressFormNotifications<Inputs extends FieldValues, Checkboxes extends Path<Inputs>>({ control }: Props<Inputs>) {
const selectedNetwork = useNetwork();
const NOTIFICATIONS_NAMES = React.useMemo(() => {
return [ selectedNetwork?.currency, 'ERC-20', 'ERC-721, ERC-1155 (NFT)' ];
}, [ selectedNetwork?.currency ]);
// eslint-disable-next-line react/display-name // eslint-disable-next-line react/display-name
const renderCheckbox = useCallback((text: string) => ({ field }: {field: ControllerRenderProps<Inputs, Checkboxes>}) => ( const renderCheckbox = useCallback((text: string) => ({ field }: {field: ControllerRenderProps<Inputs, Checkboxes>}) => (
<CheckboxInput<Inputs, Checkboxes> text={ text } field={ field }/> <CheckboxInput<Inputs, Checkboxes> text={ text } field={ field }/>
......
import { HStack, VStack, Image, Text, Icon, useColorModeValue } from '@chakra-ui/react'; import { HStack, VStack, Text, Icon, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TWatchlistItem } from 'types/client/account'; import type { TWatchlistItem } from 'types/client/account';
import TokensIcon from 'icons/tokens.svg'; import TokensIcon from 'icons/tokens.svg';
// import WalletIcon from 'icons/wallet.svg'; // import WalletIcon from 'icons/wallet.svg';
import useNetwork from 'lib/hooks/useNetwork';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import AddressSnippet from 'ui/shared/AddressSnippet'; import AddressSnippet from 'ui/shared/AddressSnippet';
import TokenLogo from 'ui/shared/TokenLogo';
// now this component works only for xDAI
// for other networks later we will use config or smth
const DECIMALS = 18; const DECIMALS = 18;
const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => { const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
const mainTextColor = useColorModeValue('gray.700', 'gray.50'); const mainTextColor = useColorModeValue('gray.700', 'gray.50');
const selectedNetwork = useNetwork();
const nativeBalance = ((item.address_balance || 0) / 10 ** DECIMALS).toFixed(1); const nativeBalance = ((item.address_balance || 0) / 10 ** DECIMALS).toFixed(1);
const nativeBalanceUSD = item.exchange_rate ? `$${ Number(nativeBalance) * item.exchange_rate } USD` : 'N/A'; const nativeBalanceUSD = item.exchange_rate ? `$${ Number(nativeBalance) * item.exchange_rate } USD` : 'N/A';
...@@ -23,8 +24,8 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => { ...@@ -23,8 +24,8 @@ const WatchListAddressItem = ({ item }: {item: TWatchlistItem}) => {
<VStack spacing={ 2 } align="stretch" overflow="hidden" fontWeight={ 500 } color="gray.700"> <VStack spacing={ 2 } align="stretch" overflow="hidden" fontWeight={ 500 } color="gray.700">
<AddressSnippet address={ item.address_hash }/> <AddressSnippet address={ item.address_hash }/>
<HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }> <HStack spacing={ 0 } fontSize="sm" h={ 6 } pl={ infoItemsPaddingLeft }>
<Image src="/xdai.png" srcSet="/xdai@2x.png 2x" alt="chain-logo" marginRight="10px" w="16px" h="16px"/> { selectedNetwork && <TokenLogo hash={ selectedNetwork.nativeTokenAddress } name={ selectedNetwork.name } boxSize={ 4 } mr="10px"/> }
<Text color={ mainTextColor }>{ `xDAI balance:${ nbsp }` + nativeBalance }</Text> <Text color={ mainTextColor }>{ `${ selectedNetwork?.currency } balance:${ nbsp }` + nativeBalance }</Text>
<Text variant="secondary">{ `${ nbsp }(${ nativeBalanceUSD })` }</Text> <Text variant="secondary">{ `${ nbsp }(${ nativeBalanceUSD })` }</Text>
</HStack> </HStack>
{ item.tokens_count && ( { item.tokens_count && (
......
...@@ -584,13 +584,6 @@ ...@@ -584,13 +584,6 @@
dependencies: dependencies:
"@chakra-ui/shared-utils" "2.0.1" "@chakra-ui/shared-utils" "2.0.1"
"@chakra-ui/icons@^2.0.2":
version "2.0.9"
resolved "https://registry.yarnpkg.com/@chakra-ui/icons/-/icons-2.0.9.tgz#35edcf9c61b8e158a5d03aeda6bd4e756960dfc7"
integrity sha512-6xvV2rC8wATgfnRH+fC9mi0nLcgKjhHKO29lV1pGioVI0yWK0dqc//zjcyBhMMpW5ABnSfig7ujVBf3op/Syzg==
dependencies:
"@chakra-ui/icon" "3.0.9"
"@chakra-ui/image@2.0.10": "@chakra-ui/image@2.0.10":
version "2.0.10" version "2.0.10"
resolved "https://registry.yarnpkg.com/@chakra-ui/image/-/image-2.0.10.tgz#712c0e1c579d959225bd8316d8d8f66cbeb95bb8" resolved "https://registry.yarnpkg.com/@chakra-ui/image/-/image-2.0.10.tgz#712c0e1c579d959225bd8316d8d8f66cbeb95bb8"
......
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