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
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_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 = {
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 = {
......
......@@ -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_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_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
| Property | Type | Description | Example value
| --- | --- | --- | --- |
| 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` |
| type | `string` | Network type (used as first part of the base path) | `'xdai'` |
| name | `string` | Displayed name of the network | `"Gnosis Chain"` |
| shortName | `string` | Used for SEO attributes (page title and description) | `"OoG"` |
| 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"` |
| 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` |
| 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'` |
| 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"` |
| 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>`
......
/* eslint-disable max-len */
export const tx = {
hash: '0x1ea365d2144796f793883534aa51bf20d23292b19478994eede23dfc599e7c34',
status: 'success',
status: 'success' as TxStatus,
block_num: 15006918,
confirmation_num: 283,
confirmation_duration: 30,
timestamp: 1662623567695,
address_from: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830',
address_to: '0x35317007D203b8a86CA727ad44E473E40450E378',
address_from: {
hash: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830',
type: 'Address',
alias: '',
},
address_to: {
hash: '0x35317007D203b8a86CA727ad44E473E40450E378',
type: 'Contract',
alias: '',
},
amount: {
value: 0.03,
value_usd: 35.5,
......@@ -36,7 +44,12 @@ export const tx = {
position: 342,
input_hex: '0x42842e0e0000000000000000000000007767dac225a233ea1055d79fb227b1696d538b75000000000000000000000000fc3017c31fe752fc48e904050ea5d6edfc38a1b00000000000000000000000000000000000000000000000000000000000000e3b',
transferred_tokens: [
{ from: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', to: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e', token: 'USDT', amount: 192.7, usd: 194.05 },
{ from: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', to: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', token: 'TOKE', amount: 76.1851851851846, 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: { 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 = [
{
id: 1,
type: 'call',
type: 'call' as TxInternalsType,
status: 'success' as const,
from: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01',
to: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e',
from: { hash: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01' },
to: { hash: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e' },
value: 0.25207646303,
gasLimit: 369472,
},
{
id: 2,
type: 'delegate call',
status: 'error' as const,
from: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45',
to: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01',
type: 'delegate_call' as TxInternalsType,
status: 'success' as const,
from: { hash: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45' },
to: { hash: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01' },
value: 0.5633333,
gasLimit: 340022,
},
{
id: 3,
type: 'static call',
status: 'success' as const,
from: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830',
to: '0x35317007D203b8a86CA727ad44E473E40450E378',
type: 'static_call' as TxInternalsType,
status: 'failed' as const,
from: { hash: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830' },
to: { hash: '0x35317007D203b8a86CA727ad44E473E40450E378' },
value: 0.421152366,
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">
<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 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 {
export function get(name?: string | undefined | null) {
if (!isBrowser()) {
return () => {};
return undefined;
}
return Cookies.get(name);
}
......
// eslint-disable-next-line no-restricted-imports
import dayjs from 'dayjs';
import duration from 'dayjs/plugin/duration';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
import updateLocale from 'dayjs/plugin/updateLocale';
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(localizedFormat);
dayjs.extend(duration);
dayjs.updateLocale('en', {
formats: {
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;
......@@ -4,7 +4,7 @@ import abiIcon from 'icons/ABI.svg';
import apiKeysIcon from 'icons/API.svg';
import appsIcon from 'icons/apps.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 profileIcon from 'icons/profile.svg';
import publicTagIcon from 'icons/publictags.svg';
......@@ -21,10 +21,13 @@ export default function useNavItems() {
return React.useMemo(() => {
const mainNavItems = [
{ 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: '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 = [
......
......@@ -6,15 +6,15 @@ import type { RouteName } from './routes';
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];
if (!route) {
return '';
}
const network = findNetwork({
network_type: urlParams?.network_type || '',
network_sub_type: urlParams?.network_sub_type,
network_type: typeof urlParams?.network_type === 'string' ? urlParams?.network_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) => {
......@@ -22,7 +22,13 @@ export function link(routeName: RouteName, urlParams?: Record<string, string | u
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 }` : '';
});
......
......@@ -45,10 +45,14 @@ export const ROUTES = {
},
// TRANSACTIONS
txs: {
txs_validated: {
pattern: `${ BASE_PATH }/txs`,
crossNetworkNavigation: true,
},
txs_pending: {
pattern: `${ BASE_PATH }/pending-transactions`,
crossNetworkNavigation: true,
},
tx_index: {
pattern: `${ BASE_PATH }/tx/[id]`,
},
......@@ -70,6 +74,9 @@ export const ROUTES = {
pattern: `${ BASE_PATH }/blocks`,
crossNetworkNavigation: true,
},
block: {
pattern: `${ BASE_PATH }/block/[id]`,
},
// TOKENS
tokens: {
......
......@@ -11,10 +11,6 @@ export default function useLink() {
const networkSubType = router.query.network_sub_type;
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]);
}, [ networkType, networkSubType ]);
}
......@@ -61,6 +61,7 @@ export default NETWORKS;
// group: 'mainnets',
// isAccountSupported: true,
// chainId: 100,
// currency: 'xDAI',
// },
// {
// name: 'Optimism on Gnosis Chain',
......@@ -71,6 +72,7 @@ export default NETWORKS;
// icon: 'https://www.fillmurray.com/60/60',
// logo: 'https://www.fillmurray.com/240/60',
// chainId: 300,
// currency: 'xDAI',
// },
// {
// name: 'Arbitrum on xDai',
......@@ -78,6 +80,7 @@ export default NETWORKS;
// subType: 'aox',
// group: 'mainnets',
// chainId: 200,
// currency: 'xDAI',
// },
// {
// name: 'Ethereum',
......@@ -86,6 +89,7 @@ export default NETWORKS;
// subType: 'mainnet',
// group: 'mainnets',
// chainId: 1,
// currency: 'ETH',
// },
// {
// name: 'Ethereum Classic',
......@@ -94,6 +98,7 @@ export default NETWORKS;
// subType: 'mainnet',
// group: 'mainnets',
// chainId: 61,
// currency: 'ETC',
// },
// {
// name: 'POA',
......@@ -102,6 +107,9 @@ export default NETWORKS;
// subType: 'core',
// group: 'mainnets',
// chainId: 99,
// currency: 'POA',
// isAccountSupported: true,
// nativeTokenAddress: '0x029a799563238d0e75e20be2f4bda0ea68d00172',
// },
// {
// name: 'RSK',
......@@ -110,6 +118,7 @@ export default NETWORKS;
// subType: 'mainnet',
// group: 'mainnets',
// chainId: 30,
// currency: 'RBTC',
// },
// {
// name: 'Gnosis Chain Testnet',
......@@ -117,6 +126,7 @@ export default NETWORKS;
// subType: 'testnet',
// group: 'testnets',
// isAccountSupported: true,
// currency: 'xDAI',
// },
// {
// name: 'POA Sokol',
......@@ -125,6 +135,7 @@ export default NETWORKS;
// subType: 'sokol',
// group: 'testnets',
// chainId: 77,
// currency: 'SPOA',
// },
// {
// name: 'ARTIS Σ1',
......@@ -132,6 +143,7 @@ export default NETWORKS;
// subType: 'sigma1',
// group: 'other',
// chainId: 246529,
// currency: 'ATS',
// },
// {
// name: 'LUKSO L14',
......@@ -140,5 +152,13 @@ export default NETWORKS;
// subType: 'l14',
// group: 'other',
// 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 @@
"format-svg": "./node_modules/.bin/svgo -r ./icons"
},
"dependencies": {
"@chakra-ui/icons": "^2.0.2",
"@chakra-ui/react": "2.3.1",
"@chakra-ui/theme-tools": "^2.0.2",
"@emotion/react": "^11",
......
......@@ -19,7 +19,7 @@ const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
return (
<>
<Head><title>{ title }</title></Head>
<PrivateTags tab="address"/>
<PrivateTags tab="private_tags_address"/>
</>
);
};
......
......@@ -19,7 +19,7 @@ const TransactionTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
return (
<>
<Head><title>{ title }</title></Head>
<PrivateTags tab="transaction"/>
<PrivateTags tab="private_tags_tx"/>
</>
);
};
......
......@@ -2,13 +2,13 @@ import Head from 'next/head';
import React from 'react';
import Apps from 'ui/pages/Apps';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const AppsPage = () => {
return (
<Page>
<PageHeader text="Apps"/>
<PageTitle text="Apps"/>
<Head><title>Apps</title></Head>
<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 { useRouter } from 'next/router';
import type { ChangeEvent } from 'react';
import Head from 'next/head';
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';
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();
import Home from 'ui/pages/Home';
const HomePage: NextPage = () => {
return (
<Page>
<VStack gap={ 4 } alignItems="flex-start" maxW="800px">
<PageHeader 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>
<>
<Head><title>Home Page</title></Head>
<Home/>
</>
);
};
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';
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 = {
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return (
<TransactionNextPage tab="details" pageParams={ pageParams }/>
<TransactionNextPage tab="tx_index" pageParams={ pageParams }/>
);
};
......
......@@ -10,7 +10,7 @@ type Props = {
}
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return <TransactionNextPage pageParams={ pageParams } tab="internal_txn"/>;
return <TransactionNextPage pageParams={ pageParams } tab="tx_internal"/>;
};
export default TransactionPage;
......
......@@ -10,7 +10,7 @@ type Props = {
}
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return <TransactionNextPage pageParams={ pageParams } tab="logs"/>;
return <TransactionNextPage pageParams={ pageParams } tab="tx_logs"/>;
};
export default TransactionPage;
......
......@@ -10,7 +10,7 @@ type 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;
......
......@@ -10,7 +10,7 @@ type Props = {
}
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
return <TransactionNextPage pageParams={ pageParams } tab="state"/>;
return <TransactionNextPage pageParams={ pageParams } tab="tx_state"/>;
};
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) => {
const isGrayTheme = c === 'gray' || c === 'gray-dark';
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 activeColor = (() => {
if (c === 'gray') {
......@@ -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 = {
solid: variantSolid,
outline: variantOutline,
simple: variantSimple,
ghost: variantGhost,
subtle: variantSubtle,
};
const baseStyle = defineStyle({
fontWeight: 600,
borderRadius: 'base',
overflow: 'hidden',
});
const sizes = {
......
......@@ -53,6 +53,19 @@ const sizes = {
fontWeight: 500,
},
}),
xs: definePartsStyle({
th: {
px: '6px',
py: '10px',
fontSize: 'sm',
},
td: {
px: '6px',
py: 6,
fontSize: 'sm',
fontWeight: 500,
},
}),
};
const variants = {
......
......@@ -23,11 +23,8 @@ const sizes = {
minH: 6,
minW: 6,
fontSize: 'sm',
lineHeight: 'sm',
px: 2,
py: '2px',
},
label: {
lineHeight: 5,
},
}),
......
......@@ -7,6 +7,7 @@ const global = (props: StyleFunctionProps) => ({
body: {
bg: mode('white', 'black')(props),
...getDefaultTransitionProps(),
'-webkit-tap-highlight-color': 'transparent',
},
form: {
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';
export interface Network {
name: string;
// https://chainlist.org/
chainId?: number;
chainId: number; // https://chainlist.org/
currency: string;
nativeTokenAddress: string;
shortName?: string;
// basePath = /<type>/<subType>, e.g. /xdai/mainnet
type: string;
......@@ -14,4 +15,5 @@ export interface Network {
icon?: FunctionComponent<SVGAttributes<SVGElement>> | string;
logo?: FunctionComponent<SVGAttributes<SVGElement>> | string;
isAccountSupported?: boolean;
assetsNamePath?: string;
}
import { Box, Heading, Image, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, LinkOverlay, Text, useColorModeValue } from '@chakra-ui/react';
import type { MouseEvent } from 'react';
import React, { useCallback } from 'react';
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 handleInfoClick = useCallback((event: MouseEvent) => {
event.preventDefault();
onInfoClick(id);
}, [ onInfoClick, id ]);
const handleFavoriteClick = useCallback(() => {
onFavoriteClick(id, isFavorite);
}, [ onFavoriteClick, id, isFavorite ]);
return (
<Box
borderRadius={{ base: 'none', sm: 'md' }}
<LinkBox
_hover={{
boxShadow: 'md',
}}
_focusWithin={{
boxShadow: 'md',
}}
borderRadius="md"
height="100%"
padding={{ base: '16px', sm: '20px' }}
boxShadow={ `0 0 0 1px ${ useColorModeValue('var(--chakra-colors-gray-200)', 'var(--chakra-colors-gray-600)') }` }
padding={{ base: 3, sm: '20px' }}
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
gridRow={{ base: '1 / 4', sm: 'auto' }}
marginBottom={ 4 }
w={{ base: '64px', sm: '96px' }}
h={{ base: '64px', sm: '96px' }}
......@@ -26,12 +70,17 @@ const AppCard = ({ title, logo, shortDescription, categories }: AppItemPreview)
</Box>
<Heading
gridColumn={{ base: 2, sm: 'auto' }}
as="h3"
marginBottom={ 2 }
fontSize={{ base: 'sm', sm: 'lg' }}
fontWeight="semibold"
>
{ title }
<LinkOverlay
href="#"
>
{ title }
</LinkOverlay>
</Heading>
<Text
......@@ -49,9 +98,53 @@ const AppCard = ({ title, logo, shortDescription, categories }: AppItemPreview)
>
{ shortDescription }
</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>
</LinkBox>
);
};
export default AppCard;
export default React.memo(AppCard);
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 { apos } from 'lib/html-entities';
import AppCard from 'ui/apps/AppCard';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import AppModal from './AppModal';
type Props = {
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 (
<>
<VisuallyHidden>
......@@ -20,32 +54,43 @@ const AppList = ({ apps }: Props) => {
{ apps.length > 0 ? (
<Grid
templateColumns={{
base: 'repeat(auto-fill, minmax(160px, 1fr))',
sm: 'repeat(auto-fill, minmax(200px, 1fr))',
sm: 'repeat(auto-fill, minmax(170px, 1fr))',
lg: 'repeat(auto-fill, minmax(260px, 1fr))',
}}
autoRows="1fr"
gap={{ base: '1px', sm: '24px' }}
gap={{ base: '16px', sm: '24px' }}
>
{ apps.map((app) => (
<GridItem
key={ app.id }
>
<AppCard
onInfoClick={ onAppClick }
id={ app.id }
title={ app.title }
logo={ app.logo }
shortDescription={ app.shortDescription }
categories={ app.categories }
isFavorite={ favoriteApps.includes(app.id) }
onFavoriteClick={ handleFavoriteClick }
/>
</GridItem>
)) }
</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 {
Box, Button, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody,
ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text,
......@@ -9,29 +8,27 @@ import React, { useCallback } from 'react';
import type { AppCategory, AppItemOverview } from 'types/client/apps';
import { TEMPORARY_DEMO_APPS } from 'data/apps';
import linkIcon from 'icons/link.svg';
import ghIcon from 'icons/social/git.svg';
import tgIcon from 'icons/social/telega.svg';
import twIcon from 'icons/social/tweet.svg';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import { nbsp } from 'lib/html-entities';
type Props = {
id: string | null;
id: string;
onClose: () => void;
isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
}
const AppModal = ({
id,
onClose,
isFavorite,
onFavoriteClick,
}: Props) => {
const handleFavorite = useCallback(() => {
// TODO: implement
}, []);
if (!id) {
return null;
}
const {
title,
author,
......@@ -45,8 +42,6 @@ const AppModal = ({
categories,
} = TEMPORARY_DEMO_APPS.find(app => app.id === id) as AppItemOverview;
const isFavorite = false;
const socialLinks = [
Boolean(telegram) && {
icon: tgIcon,
......@@ -62,6 +57,10 @@ const AppModal = ({
},
].filter(Boolean) as Array<{ icon: FunctionComponent; url: string }>;
const handleFavoriteClick = useCallback(() => {
onFavoriteClick(id, isFavorite);
}, [ onFavoriteClick, id, isFavorite ]);
return (
<Modal
isOpen={ Boolean(id) }
......@@ -135,10 +134,10 @@ const AppModal = ({
colorScheme="gray"
w={ 9 }
h={ 8 }
onClick={ handleFavorite }
onClick={ handleFavoriteClick }
icon={ isFavorite ?
<Icon as={ StarIcon } w={ 4 } h={ 4 } color="yellow.400"/> :
<Icon as={ starOutlineIcon } w={ 4 } h={ 4 }/> }
<Icon as={ starFilledIcon } w={ 4 } h={ 4 } color="yellow.400"/> :
<Icon as={ starOutlineIcon } w={ 4 } h={ 4 } color="gray.300"/> }
/>
</Box>
</Box>
......@@ -188,10 +187,10 @@ const AppModal = ({
overflow="hidden"
>
<Icon
as={ LinkIcon }
as={ linkIcon }
display="inline"
verticalAlign="baseline"
boxSize={ 3 }
boxSize="18px"
marginRight={ 2 }
/>
......
......@@ -2,9 +2,12 @@ import { Box, Heading, Icon, Text } from '@chakra-ui/react';
import React from 'react';
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 (
<Box
display="flex"
......@@ -31,7 +34,7 @@ const EmptySearchResult = () => {
variant="secondary"
align="center"
>
Couldn{ apos }t find an app that matches your filter query.
{ text }
</Text>
</Box>
);
......
......@@ -12,8 +12,8 @@ import ApiKeyListItem from 'ui/apiKey/ApiKeyTable/ApiKeyListItem';
import ApiKeyTable from 'ui/apiKey/ApiKeyTable/ApiKeyTable';
import DeleteApiKeyModal from 'ui/apiKey/DeleteApiKeyModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable';
......@@ -128,7 +128,7 @@ const ApiKeysPage: React.FC = () => {
return (
<Page>
<Box h="100%">
<PageHeader text="API keys"/>
<PageTitle text="API keys"/>
{ content }
</Box>
</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';
import { TEMPORARY_DEMO_APPS } from 'data/apps';
import AppList from 'ui/apps/AppList';
import AppModal from 'ui/apps/AppModal';
import FilterInput from 'ui/apps/FilterInput';
import FilterInput from 'ui/shared/FilterInput';
const defaultDisplayedApps = [ ...TEMPORARY_DEMO_APPS ]
.sort((a, b) => a.title.localeCompare(b.title));
const Apps = () => {
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 apps = displayedApps
......@@ -29,11 +32,12 @@ const Apps = () => {
return (
<>
<FilterInput onChange={ debounceFilterApps }/>
<AppList apps={ displayedApps }/>
<AppModal
id={ displayedAppId }
onClose={ clearDisplayedAppId }
<FilterInput onChange={ debounceFilterApps } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/>
<AppList
apps={ displayedApps }
onAppClick={ showAppInfo }
displayedAppId={ displayedAppId }
onModalClose={ clearDisplayedAppId }
/>
</>
);
......
......@@ -11,8 +11,8 @@ import CustomAbiListItem from 'ui/customAbi/CustomAbiTable/CustomAbiListItem';
import CustomAbiTable from 'ui/customAbi/CustomAbiTable/CustomAbiTable';
import DeleteCustomAbiModal from 'ui/customAbi/DeleteCustomAbiModal';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable';
......@@ -116,7 +116,7 @@ const CustomAbiPage: React.FC = () => {
return (
<Page>
<Box h="100%">
<PageHeader text="Custom ABI"/>
<PageTitle text="Custom ABI"/>
{ content }
</Box>
</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';
import useFetchProfileInfo from 'lib/hooks/useFetchProfileInfo';
import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import UserAvatar from 'ui/shared/UserAvatar';
const MyProfile = () => {
......@@ -56,7 +56,7 @@ const MyProfile = () => {
return (
<Page>
<PageHeader text="My profile"/>
<PageTitle text="My profile"/>
{ content }
</Page>
);
......
import {
Box,
Tab,
Tabs,
TabList,
TabPanel,
TabPanels,
} from '@chakra-ui/react';
import React, { useCallback, useState } from 'react';
import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useLink from 'lib/link/useLink';
import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags';
import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
const TABS = [ 'address', 'transaction' ] as const;
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
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 = {
tab: TabName;
tab: RoutedTab['routeName'];
}
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 (
<Page>
<Box h="100%">
<PageHeader text="Private tags"/>
<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>
<PageTitle text="Private tags"/>
<RoutedTabs tabs={ TABS } defaultActiveTab={ tab }/>
</Page>
);
};
......
import { ArrowBackIcon } from '@chakra-ui/icons';
import { Box, Link, Text } from '@chakra-ui/react';
import { Link, Text, Icon } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react';
import { animateScroll } from 'react-scroll';
import type { PublicTag } from 'types/api/account';
import eastArrowIcon from 'icons/arrows/east.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import useToast from 'lib/hooks/useToast';
import PublicTagsData from 'ui/publicTags/PublicTagsData';
import PublicTagsForm from 'ui/publicTags/PublicTagsForm/PublicTagsForm';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
type TScreen = 'data' | 'form';
......@@ -77,16 +77,14 @@ const PublicTagsComponent: React.FC = () => {
return (
<Page>
<Box h="100%">
{ isMobile && screen === 'form' && (
<Link display="inline-flex" alignItems="center" mb={ 6 } onClick={ onGoBack }>
<ArrowBackIcon w={ 6 } h={ 6 }/>
<Text variant="inherit" fontSize="sm" ml={ 2 }>Public tags</Text>
</Link>
) }
<PageHeader text={ header }/>
{ content }
</Box>
{ isMobile && screen === 'form' && (
<Link display="inline-flex" alignItems="center" mb={ 6 } onClick={ onGoBack }>
<Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)"/>
<Text variant="inherit" fontSize="sm" ml={ 2 }>Public tags</Text>
</Link>
) }
<PageTitle text={ header }/>
{ content }
</Page>
);
};
......
import {
Tab,
Tabs,
TabList,
TabPanel,
TabPanels,
} from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { Flex, Link, Icon } from '@chakra-ui/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 Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
import ExternalLink from 'ui/shared/ExternalLink';
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 TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs';
import TxRawTrace from 'ui/tx/TxRawTrace';
import TxState from 'ui/tx/TxState';
interface Tab {
type: 'details' | 'internal_txn' | 'logs' | 'raw_trace' | 'state';
name: string;
path?: string;
component?: React.ReactNode;
routeName: RouteName;
}
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/> },
const TABS: Array<RoutedTab> = [
{ routeName: 'tx_index', title: 'Details', component: <TxDetails/> },
{ routeName: 'tx_internal', title: 'Internal txn', component: <TxInternals/> },
{ routeName: 'tx_logs', title: 'Logs', component: <TxLogs/> },
{ routeName: 'tx_state', title: 'State', component: <TxState/> },
{ routeName: 'tx_raw_trace', title: 'Raw trace', component: <TxRawTrace/> },
];
export interface Props {
tab: Tab['type'];
tab: RoutedTab['routeName'];
}
const TransactionPageContent = ({ tab }: Props) => {
const [ , setActiveTab ] = React.useState<Tab['type']>(tab);
const router = useRouter();
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 (
<Page>
<PageHeader text="Transaction details"/>
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ handleTabChange } defaultIndex={ defaultIndex }>
<TabList marginBottom={{ base: 6, lg: 8 }} flexWrap="wrap">
{ TABS.map((tab) => <Tab key={ tab.type }>{ tab.name }</Tab>) }
</TabList>
<TabPanels>
{ TABS.map((tab) => <TabPanel padding={ 0 } key={ tab.type }>{ tab.component || tab.name }</TabPanel>) }
</TabPanels>
</Tabs>
{ /* TODO should be shown only when navigating from txs list */ }
<Link mb={ 6 } display="inline-flex" href={ link('txs_validated') }>
<Icon as={ eastArrowIcon } boxSize={ 6 } mr={ 2 } transform="rotate(180deg)"/>
Transactions
</Link>
<PageTitle text="Transaction details"/>
<Flex marginLeft="auto" alignItems="center" flexWrap="wrap" columnGap={ 6 } rowGap={ 3 } mb={ 6 }>
<ExternalLink title="Open in Tenderly" href="#"/>
<ExternalLink title="Open in Blockchair" href="#"/>
<ExternalLink title="Open in Etherscan" href="#"/>
</Flex>
<RoutedTabs
tabs={ TABS }
defaultActiveTab={ tab }
/>
</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';
import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile';
import SkeletonTable from 'ui/shared/SkeletonTable';
import AddressModal from 'ui/watchlist/AddressModal/AddressModal';
......@@ -113,7 +113,7 @@ const WatchList: React.FC = () => {
return (
<Page>
<Box h="100%">
<PageHeader text="Watch list"/>
<PageTitle text="Watch list"/>
{ content }
</Box>
</Page>
......
import { VStack, useColorModeValue } from '@chakra-ui/react';
import { Flex, useColorModeValue, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
children: React.ReactNode;
className?: string;
}
const AccountListItemMobile = ({ children }: Props) => {
const AccountListItemMobile = ({ children, className }: Props) => {
return (
<VStack
gap={ 4 }
<Flex
rowGap={ 6 }
alignItems="flex-start"
flexDirection="column"
paddingY={ 6 }
borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderTopWidth="1px"
_last={{
borderBottomWidth: '1px',
}}
className={ className }
>
{ children }
</VStack>
</Flex>
);
};
export default AccountListItemMobile;
export default chakra(AccountListItemMobile);
......@@ -19,7 +19,7 @@ const AddressSnippet = ({ address, subtitle }: Props) => {
<AddressLink hash={ address } fontWeight="600" ml={ 2 }/>
<CopyToClipboard text={ address } ml={ 1 }/>
</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>
);
};
......
......@@ -13,7 +13,7 @@ interface Props extends HTMLChakraProps<'div'> {
const DetailsInfoItem = ({ title, hint, children, ...styles }: Props) => {
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">
<Tooltip
label={ hint }
......@@ -24,10 +24,20 @@ const DetailsInfoItem = ({ title, hint, children, ...styles }: Props) => {
<Icon as={ infoIcon } boxSize={ 5 }/>
</Box>
</Tooltip>
<Text fontWeight={ 500 }>{ title }</Text>
<Text fontWeight={{ base: 700, lg: 500 }}>{ title }</Text>
</Flex>
</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 }
</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, useColorModeValue } from '@chakra-ui/react';
import { Input, InputGroup, InputLeftElement, Icon, useColorModeValue, chakra } from '@chakra-ui/react';
import type { ChangeEvent } from 'react';
import React, { useCallback, useState } from 'react';
import searchIcon from 'icons/search.svg';
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 handleFilterQueryChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
......@@ -19,22 +23,25 @@ const FilterInput = ({ onChange }: Props) => {
return (
<InputGroup
size="sm"
size={ size }
className={ className }
>
<InputLeftElement
pointerEvents="none"
>
<SearchIcon color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/>
<Icon as={ searchIcon } color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/>
</InputLeftElement>
<Input
size="sm"
size={ size }
value={ filterQuery }
onChange={ handleFilterQueryChange }
marginBottom={{ base: '4', lg: '6' }}
placeholder={ placeholder }
borderWidth="2px"
textOverflow="ellipsis"
/>
</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 { useRouter } from 'next/router';
import React from 'react';
import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import Header from 'ui/blocks/header/Header';
import NavigationDesktop from 'ui/blocks/navigation/NavigationDesktop';
import PageContent from 'ui/shared/Page/PageContent';
import Header from 'ui/snippets/header/Header';
import NavigationDesktop from 'ui/snippets/navigation/NavigationDesktop';
interface Props {
children: React.ReactNode;
wrapChildren?: boolean;
}
const Page = ({ children }: Props) => {
const isMobile = useIsMobile();
const Page = ({ children, wrapChildren = true }: Props) => {
const router = useRouter();
const fetch = useFetch();
......@@ -32,24 +32,18 @@ const Page = ({ children }: Props) => {
}
}, [ networkType, networkSubType ]);
const renderedChildren = wrapChildren ? (
<PageContent>{ children }</PageContent>
) : children;
return (
<HStack
w="100%"
minH="100vh"
alignItems="stretch"
>
{ !isMobile && <NavigationDesktop/> }
<VStack width="100%" paddingX={ isMobile ? 4 : 8 } paddingTop={ isMobile ? 0 : 9 } paddingBottom={ 10 } spacing={ 0 }>
<Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/>
<Flex flexDir="column" width="100%">
<Header/>
<Box
as="main"
w="100%"
paddingTop={ isMobile ? '138px' : '52px' }
>
{ children }
</Box>
</VStack>
</HStack>
{ renderedChildren }
</Flex>
</Flex>
);
};
......
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 React from 'react';
const PageHeader = ({ text }: {text: string}) => {
const PageTitle = ({ text }: {text: string}) => {
return (
<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';
interface Props {
className?: string;
value: number;
colorScheme?: 'green' | 'gray';
}
const WIDTH = 50;
const Utilization = ({ className, value }: Props) => {
const Utilization = ({ className, value, colorScheme = 'green' }: Props) => {
const valueString = (value * 100).toFixed(2) + '%';
const colorGrayScheme = useColorModeValue('gray.500', 'gray.500');
const color = colorScheme === 'gray' ? colorGrayScheme : 'green.500';
return (
<Flex className={ className } alignItems="center">
<Box bg="gray.100" w={ `${ WIDTH }px` } h="4px" borderRadius="full" overflow="hidden">
<Box bg="green.500" w={ valueString } h="100%"/>
<Box bg={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } w={ `${ WIDTH }px` } h="4px" borderRadius="full" overflow="hidden">
<Box bg={ color } w={ valueString } h="100%"/>
</Box>
<Text color="green.500" ml="10px" fontWeight="bold">{ valueString }</Text>
<Text color={ color } ml="10px" fontWeight="bold">{ valueString }</Text>
</Flex>
);
};
......
import { Link, chakra, shouldForwardProp } from '@chakra-ui/react';
import { Link, chakra, shouldForwardProp, Tooltip, Box } from '@chakra-ui/react';
import React from 'react';
import useLink from 'lib/link/useLink';
......@@ -6,25 +6,36 @@ import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
type?: 'address' | 'transaction' | 'token';
type?: 'address' | 'transaction' | 'token' | 'block';
alias?: string;
className?: string;
hash: string;
truncation?: 'constant' | 'dynamic'| 'none';
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();
let url;
if (type === 'transaction') {
url = link('tx_index', { id: hash });
url = link('tx_index', { id: id || hash });
} 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 {
url = link('address_index', { id: hash });
url = link('address_index', { id: id || hash });
}
const content = (() => {
if (alias) {
return (
<Tooltip label={ hash }>
<Box overflow="hidden" textOverflow="ellipsis">{ alias }</Box>
</Tooltip>
);
}
switch (truncation) {
case 'constant':
return <HashStringShorten hash={ hash }/>;
......
......@@ -2,10 +2,10 @@ import { Icon, Box, Flex, Drawer, DrawerOverlay, DrawerContent, DrawerBody, useC
import React from 'react';
import burgerIcon from 'icons/burger.svg';
import NavigationMobile from 'ui/blocks/navigation/NavigationMobile';
import NetworkLogo from 'ui/blocks/networkMenu/NetworkLogo';
import NetworkMenuButton from 'ui/blocks/networkMenu/NetworkMenuButton';
import NetworkMenuContentMobile from 'ui/blocks/networkMenu/NetworkMenuContentMobile';
import NavigationMobile from 'ui/snippets/navigation/NavigationMobile';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import NetworkMenuButton from 'ui/snippets/networkMenu/NetworkMenuButton';
import NetworkMenuContentMobile from 'ui/snippets/networkMenu/NetworkMenuContentMobile';
const Burger = () => {
const iconColor = useColorModeValue('gray.600', 'white');
......
import type { UseCheckboxProps } 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 type {
SystemStyleObject,
......@@ -16,6 +15,7 @@ import { dataAttr, __DEV__ } from '@chakra-ui/utils';
import * as React from 'react';
import moonIcon from 'icons/moon.svg';
import sunIcon from 'icons/sun.svg';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
export interface ColorModeTogglerProps
......@@ -101,10 +101,11 @@ const ColorModeToggler = forwardRef<ColorModeTogglerProps, 'input'>((props, ref)
data-hover={ dataAttr(state.isHovered) }
__css={ thumbStyles }
/>
<SunIcon
boxSize={ 4 }
margin={ 2 }
<Icon
boxSize={ 5 }
margin={ 1.5 }
zIndex="docked"
as={ sunIcon }
color={ useColorModeValue('gray.500', 'blue.600') }
{ ...transitionProps }
/>
......
import { HStack, Box, Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import NetworkLogo from 'ui/blocks/networkMenu/NetworkLogo';
import ProfileMenuDesktop from 'ui/blocks/profileMenu/ProfileMenuDesktop';
import ProfileMenuMobile from 'ui/blocks/profileMenu/ProfileMenuMobile';
import SearchBar from 'ui/blocks/searchBar/SearchBar';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile';
import SearchBar from 'ui/snippets/searchBar/SearchBar';
import Burger from './Burger';
import ColorModeToggler from './ColorModeToggler';
const Header = () => {
const isMobile = useIsMobile();
const bgColor = useColorModeValue('white', 'black');
if (isMobile) {
return (
<Box bgColor={ bgColor }>
return (
<>
<Box bgColor={ bgColor } display={{ base: 'block', lg: 'none' }}>
<Flex
as="header"
position="fixed"
......@@ -36,21 +33,22 @@ const Header = () => {
</Flex>
<SearchBar/>
</Box>
);
}
return (
<HStack
as="header"
width="100%"
alignItems="center"
justifyContent="center"
gap={ 12 }
>
<SearchBar/>
<ColorModeToggler/>
<ProfileMenuDesktop/>
</HStack>
<HStack
as="header"
width="100%"
alignItems="center"
justifyContent="center"
gap={ 12 }
display={{ base: 'none', lg: 'flex' }}
paddingX={ 12 }
paddingTop={ 9 }
paddingBottom="52px"
>
<SearchBar/>
<ColorModeToggler/>
<ProfileMenuDesktop/>
</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 ghIcon from 'icons/social/git.svg';
import statsIcon from 'icons/social/stats.svg';
import tgIcon from 'icons/social/telega.svg';
import twIcon from 'icons/social/tweet.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
const SOCIAL_LINKS = [
......@@ -24,22 +23,13 @@ interface Props {
}
const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
const isMobile = useIsMobile();
const width = (() => {
if (isMobile) {
return '100%';
}
return isCollapsed ? '20px' : '180px';
})();
const isExpanded = isCollapsed === false;
const marginTop = (() => {
if (!hasAccount) {
return 'auto';
}
return isMobile ? 6 : 20;
return { base: 6, lg: 20 };
})();
return (
......@@ -48,8 +38,8 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
spacing={ 8 }
borderTop="1px solid"
borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
width={ width }
paddingTop={ isMobile ? 6 : 8 }
width={{ base: '100%', lg: isExpanded ? '180px' : '20px', xl: isCollapsed ? '20px' : '180px' }}
paddingTop={{ base: 6, lg: 8 }}
marginTop={ marginTop }
alignItems="flex-start"
alignSelf="center"
......@@ -57,7 +47,7 @@ const NavFooter = ({ isCollapsed, hasAccount }: Props) => {
fontSize="xs"
{ ...getDefaultTransitionProps({ transitionProperty: 'width' }) }
>
<Stack direction={ isCollapsed ? 'column' : 'row' }>
<Stack direction={{ base: 'row', lg: isExpanded ? 'row' : 'column', xl: isCollapsed ? 'column' : 'row' }}>
{ SOCIAL_LINKS.map(sl => {
return (
<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) => {
);
}) }
</Stack>
{ !isCollapsed && (
<>
<Text variant="secondary">
<Box display={{ base: 'block', lg: isExpanded ? 'block' : 'none', xl: isCollapsed ? 'none' : 'block' }}>
<Text variant="secondary" mb={ 8 }>
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text>
<Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ BLOCKSCOUT_VERSION }</Link></Text>
</>
) }
</Text>
<Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ BLOCKSCOUT_VERSION }</Link></Text>
</Box>
</VStack>
);
};
......
......@@ -2,7 +2,6 @@ import { Link, Icon, Text, HStack, Tooltip, Box } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import useColors from './useColors';
......@@ -18,21 +17,15 @@ interface Props {
const NavLink = ({ text, url, icon, isCollapsed, isActive, px }: Props) => {
const colors = useColors();
const isMobile = useIsMobile();
const width = (() => {
if (isMobile) {
return '100%';
}
return isCollapsed ? '60px' : '180px';
})();
const isExpanded = isCollapsed === false;
return (
<Box as="li" listStyleType="none" w="100%">
<NextLink href={ url } passHref>
<Link
w={ width }
px={ px || (isCollapsed ? '15px' : 3) }
w={{ base: '100%', lg: isExpanded ? '180px' : '60px', xl: isCollapsed ? '60px' : '180px' }}
px={ px || { base: 3, lg: isExpanded ? 3 : '15px', xl: isCollapsed ? '15px' : 3 } }
py={ 2.5 }
display="flex"
color={ isActive ? colors.text.active : colors.text.default }
......@@ -53,7 +46,14 @@ const NavLink = ({ text, url, icon, isCollapsed, isActive, px }: Props) => {
>
<HStack spacing={ 3 }>
<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>
</Tooltip>
</Link>
......
import { ChevronLeftIcon } from '@chakra-ui/icons';
import { Flex, Box, VStack, useColorModeValue, useBreakpointValue } from '@chakra-ui/react';
import { Flex, Box, VStack, Icon, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import chevronIcon from 'icons/arrows/east-mini.svg';
import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork';
import isBrowser from 'lib/isBrowser';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NetworkLogo from 'ui/blocks/networkMenu/NetworkLogo';
import NetworkMenu from 'ui/blocks/networkMenu/NetworkMenu';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import NetworkMenu from 'ui/snippets/networkMenu/NetworkMenu';
import NavFooter from './NavFooter';
import NavLink from './NavLink';
......@@ -15,18 +16,24 @@ import NavLink from './NavLink';
const NavigationDesktop = () => {
const { mainNavItems, accountNavItems } = useNavItems();
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(() => {
if (!navBarCollapsedCookie) {
setCollapsedState(!isLargeScreen);
const navBarCollapsedCookie = cookies.get(cookies.NAMES.NAV_BAR_COLLAPSED);
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(() => {
setCollapsedState((flag) => !flag);
......@@ -40,16 +47,19 @@ const NavigationDesktop = () => {
borderColor: useColorModeValue('blackAlpha.200', 'whiteAlpha.200'),
};
const isExpanded = isCollapsed === false;
return (
<Flex
display={{ base: 'none', lg: 'flex' }}
position="relative"
flexDirection="column"
alignItems="flex-start"
borderRight="1px solid"
borderColor={ containerBorderColor }
px={ isCollapsed ? 4 : 6 }
px={{ lg: isExpanded ? 6 : 4, xl: isCollapsed ? 4 : 6 }}
py={ 12 }
width={ isCollapsed ? '92px' : '229px' }
width={{ lg: isExpanded ? '229px' : '92px', xl: isCollapsed ? '92px' : '229px' }}
{ ...getDefaultTransitionProps({ transitionProperty: 'width, padding' }) }
>
<Box
......@@ -78,19 +88,20 @@ const NavigationDesktop = () => {
</Box>
) }
<NavFooter isCollapsed={ isCollapsed } hasAccount={ hasAccount }/>
<ChevronLeftIcon
<Icon
as={ chevronIcon }
width={ 6 }
height={ 6 }
border="1px"
_hover={{ color: 'blue.400' }}
borderRadius="base"
{ ...chevronIconStyles }
transform={ isCollapsed ? 'rotate(180deg)' : 'rotate(0)' }
transform={{ lg: isExpanded ? 'rotate(0)' : 'rotate(180deg)', xl: isCollapsed ? 'rotate(180deg)' : 'rotate(0)' }}
{ ...getDefaultTransitionProps({ transitionProperty: 'transform, left' }) }
transformOrigin="center"
position="fixed"
position="absolute"
top="104px"
left={ isCollapsed ? '80px' : '216px' }
left={{ lg: isExpanded ? '216px' : '80px', xl: isCollapsed ? '80px' : '216px' }}
cursor="pointer"
onClick={ handleTogglerClick }
/>
......
......@@ -4,8 +4,8 @@ import React from 'react';
import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems';
import useNetwork from 'lib/hooks/useNetwork';
import NavFooter from 'ui/blocks/navigation/NavFooter';
import NavLink from 'ui/blocks/navigation/NavLink';
import NavFooter from 'ui/snippets/navigation/NavFooter';
import NavLink from 'ui/snippets/navigation/NavLink';
const NavigationMobile = () => {
const { mainNavItems, accountNavItems } = useNavItems();
......
......@@ -57,7 +57,7 @@ const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
<NextLink href={ href } passHref>
<Box
as="a"
width={ isCollapsed ? '0' : '113px' }
width={{ base: '113px', lg: isCollapsed === false ? '113px' : 0, xl: isCollapsed ? '0' : '113px' }}
display="inline-flex"
overflow="hidden"
onClick={ onClick }
......
......@@ -4,7 +4,7 @@ import React from 'react';
import NetworkMenuButton from './NetworkMenuButton';
import NetworkMenuContentDesktop from './NetworkMenuContentDesktop';
interface Props {
isCollapsed: boolean;
isCollapsed?: boolean;
}
const NetworkMenu = ({ isCollapsed }: Props) => {
......@@ -13,7 +13,7 @@ const NetworkMenu = ({ isCollapsed }: Props) => {
{ ({ isOpen }) => (
<>
<PopoverTrigger>
<Box marginLeft={ isCollapsed ? '0px' : '7px' }>
<Box marginLeft={{ base: '7px', lg: isCollapsed === false ? '7px' : '0px', xl: isCollapsed ? '0px' : '7px' }}>
<NetworkMenuButton isActive={ isOpen }/>
</Box>
</PopoverTrigger>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment