Commit 8a487831 authored by tom's avatar tom

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

parents 85446f14 33b1ad24
...@@ -10,4 +10,4 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout ...@@ -10,4 +10,4 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_SUPPORTED_NETWORKS=[{"name":"Gnosis Chain","type":"xdai","subType":"mainnet","group":"mainnets","isAccountSupported":true},{"name":"Optimism on Gnosis Chain","shortName":"OoG","type":"xdai","subType":"optimism","group":"mainnets","icon":"https://www.fillmurray.com/60/60"},{"name":"Arbitrum on xDai","type":"xdai","subType":"aox","group":"mainnets"},{"name":"Ethereum","shortName":"ETH","type":"eth","subType":"mainnet","group":"mainnets"},{"name":"Ethereum Classic","shortName":"ETC","type":"etc","subType":"mainnet","group":"mainnets"},{"name":"POA","shortName":"POA","type":"poa","subType":"core","group":"mainnets","isAccountSupported":true},{"name":"RSK","shortName":"RBTC","type":"rsk","subType":"mainnet","group":"mainnets"},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets"},{"name":"POA Sokol","shortName":"POA","type":"poa","subType":"sokol","group":"testnets"},{"name":"ARTIS Σ1","type":"artis","subType":"sigma1","group":"other"},{"name":"LUKSO L14","shortName":"POA","type":"lukso","subType":"l14","group":"other"}] 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", "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","isAccountSupported":true, "chainId": 99},{"name":"RSK","shortName":"RBTC","type":"rsk","subType":"mainnet","group":"mainnets", "chainId": 30},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets"},{"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}]
\ No newline at end of file
#!/usr/bin/env sh #!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/_/husky.sh"
npx lint-staged # lint js/ts files
\ No newline at end of file echo 🧿 Running file linter...
npx lint-staged
# format svg
echo 🧿 Running svg formatter...
for file in `git diff --diff-filter=ACMRT --cached --name-only | grep ".svg\$"`
do
echo "Formatting $file"
./node_modules/.bin/svgo -q $file
git add $file
done
echo ✅ All pre-commit jobs are done
\ No newline at end of file
...@@ -27,18 +27,19 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -27,18 +27,19 @@ The app instance could be customized by passing following variables to NodeJS en
| Variable | Type | Description | Default value | Variable | Type | Description | Default value
| --- | --- | --- | --- | | --- | --- | --- | --- |
| NEXT_PUBLIC_BLOCKSCOUT_VERSION | `string` | Current running version of Blockscout (used to display link to release in the footer) | | NEXT_PUBLIC_BLOCKSCOUT_VERSION | `string` | Current running version of Blockscout (used to display link to release in the footer) |
| NEXT_PUBLIC_FOOTER_GITHUB_LINK | `string` | Link to Github in the footer | `https://github.com/blockscout/blockscout` | | NEXT_PUBLIC_FOOTER_GITHUB_LINK | `string` | Link to Github in the footer | `https://github.com/blockscout/blockscout` |
| NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` | | NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` |
| NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` | Link to Telegram in the footer | `https://t.me/poa_network` | | NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` | Link to Telegram in the footer | `https://t.me/poa_network` |
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` | | NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` | 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},{"name":"Optimism on Gnosis Chain","type":"xdai","subType":"optimism","group":"mainnets"},{"name":"Arbitrum on xDai","type":"xdai","subType":"aox","group":"mainnets"},{"name":"Ethereum","type":"eth","subType":"mainnet","group":"mainnets"},{"name":"Ethereum Classic","type":"etc","subType":"mainnet","group":"mainnets"},{"name":"POA","type":"poa","subType":"core","group":"mainnets"},{"name":"RSK","type":"rsk","subType":"mainnet","group":"mainnets"},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets","isAccountSupported":true},{"name":"POA Sokol","type":"poa","subType":"sokol","group":"testnets"},{"name":"ARTIS Σ1","type":"artis","subType":"sigma1","group":"other"},{"name":"LUKSO L14","type":"lukso","subType":"l14","group":"other"}]` | | 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},{"name":"Optimism on Gnosis Chain","shortName":"OoG","type":"xdai","subType":"optimism","group":"mainnets","icon":"https://www.fillmurray.com/60/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","isAccountSupported":true, "chainId": 99},{"name":"RSK","shortName":"RBTC","type":"rsk","subType":"mainnet","group":"mainnets", "chainId": 30},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets"},{"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}]` |
### Network configuration properties ### Network configuration properties
| Property | Type | Description | Example value | Property | Type | Description | Example value
| --- | --- | --- | --- | | --- | --- | --- | --- |
| name | `string` | Displayed name of the network | `"Gnosis Chain"` | | name | `string` | Displayed name of the network | `"Gnosis Chain"` |
| chainId | `number` | Id of the network. Could be seen there – [https://chainlist.org/](https://chainlist.org/) | `1` |
| type | `string` | Network type (used as first part of the base path) | `"xdai"` | | type | `string` | Network type (used as first part of the base path) | `"xdai"` |
| subType | `string` | Network subtype (used as second part of the base path) | `"mainnet"` | | subType | `string` | Network subtype (used as second part of the base path) | `"mainnet"` |
| group | `mainnets \| testnets \| other` | Indicates in which tab network appears in the menu | `"mainnets"` | | group | `mainnets \| testnets \| other` | Indicates in which tab network appears in the menu | `"mainnets"` |
...@@ -46,4 +47,4 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -46,4 +47,4 @@ The app instance could be customized by passing following variables to NodeJS en
| icon | `string` *(optional)* | Network icon; if not provided, will fallback to icon predefined in the project; if the project doesn't have icon for such network then the common placeholder will be shown; *Note* that icon size should be 30px by 30px | `"https://www.fillmurray.com/60/60"` | | icon | `string` *(optional)* | Network icon; if not provided, will fallback to icon predefined in the project; if the project doesn't have icon for such network then the common placeholder will be shown; *Note* that icon size should be 30px by 30px | `"https://www.fillmurray.com/60/60"` |
| logo | `string` *(optional)* | Network logo; if not provided, will fallback to logo predefined in the project; if the project doesn't have logo for such network then the common placeholder will be shown; *Note* that logo height should be 20px and width less than 120px | `"https://www.fillmurray.com/240/40"` | | logo | `string` *(optional)* | Network logo; if not provided, will fallback to logo predefined in the project; if the project doesn't have logo for such network then the common placeholder will be shown; *Note* that logo height should be 20px and width less than 120px | `"https://www.fillmurray.com/240/40"` |
*Note* the base path for the network is built up from its `type` and `subType` like so `https://blockscout.com/<type>/<subType>` *Note* the base path for the network is built up from its `type` and `subType` like so `https://blockscout.com/<type>/<subType>`
\ No newline at end of file
This diff is collapsed.
<svg viewBox="0 0 240 192" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M82.667 4H56a4 4 0 0 0-4 4v26.667a4 4 0 0 0 4 4h26.667a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4ZM56 0a8 8 0 0 0-8 8v26.667a8 8 0 0 0 8 8h26.667a8 8 0 0 0 8-8V8a8 8 0 0 0-8-8H56Zm122.667 4H152a4 4 0 0 0-4 4v26.667a4 4 0 0 0 4 4h26.667a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4ZM152 0a8 8 0 0 0-8 8v26.667a8 8 0 0 0 8 8h26.667a8 8 0 0 0 8-8V8a8 8 0 0 0-8-8H152ZM56 52h26.667a4 4 0 0 1 4 4v26.667a4 4 0 0 1-4 4H56a4 4 0 0 1-4-4V56a4 4 0 0 1 4-4Zm-8 4a8 8 0 0 1 8-8h26.667a8 8 0 0 1 8 8v26.667a8 8 0 0 1-8 8H56a8 8 0 0 1-8-8V56Zm34.667 44H56a4 4 0 0 0-4 4v26.667a4 4 0 0 0 4 4h26.667a4 4 0 0 0 4-4V104a4 4 0 0 0-4-4ZM56 96a8 8 0 0 0-8 8v26.667a8 8 0 0 0 8 8h26.667a8 8 0 0 0 8-8V104a8 8 0 0 0-8-8H56Zm0 52h26.667a4 4 0 0 1 4 4v26.667a4 4 0 0 1-4 4H56a4 4 0 0 1-4-4V152a4 4 0 0 1 4-4Zm-8 4a8 8 0 0 1 8-8h26.667a8 8 0 0 1 8 8v26.667a8 8 0 0 1-8 8H56a8 8 0 0 1-8-8V152ZM178.667 52H152a4 4 0 0 0-4 4v26.667a4 4 0 0 0 4 4h26.667a4 4 0 0 0 4-4V56a4 4 0 0 0-4-4ZM152 48a8 8 0 0 0-8 8v26.667a8 8 0 0 0 8 8h26.667a8 8 0 0 0 8-8V56a8 8 0 0 0-8-8H152Zm0 52h26.667a4 4 0 0 1 4 4v26.667a4 4 0 0 1-4 4H152a4 4 0 0 1-4-4V104a4 4 0 0 1 4-4Zm-8 4a8 8 0 0 1 8-8h26.667a8 8 0 0 1 8 8v26.667a8 8 0 0 1-8 8H152a8 8 0 0 1-8-8V104Zm34.667 44H152a4 4 0 0 0-4 4v26.667a4 4 0 0 0 4 4h26.667a4 4 0 0 0 4-4V152a4 4 0 0 0-4-4ZM152 144a8 8 0 0 0-8 8v26.667a8 8 0 0 0 8 8h26.667a8 8 0 0 0 8-8V152a8 8 0 0 0-8-8H152ZM8 28h26.667a4 4 0 0 1 4 4v26.667a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V32a4 4 0 0 1 4-4Zm-8 4a8 8 0 0 1 8-8h26.667a8 8 0 0 1 8 8v26.667a8 8 0 0 1-8 8H8a8 8 0 0 1-8-8V32Zm104-4h26.667a4 4 0 0 1 4 4v26.667a4 4 0 0 1-4 4H104a4 4 0 0 1-4-4V32a4 4 0 0 1 4-4Zm-8 4a8 8 0 0 1 8-8h26.667a8 8 0 0 1 8 8v26.667a8 8 0 0 1-8 8H104a8 8 0 0 1-8-8V32Zm130.667-4H200a4 4 0 0 0-4 4v26.667a4 4 0 0 0 4 4h26.667a4 4 0 0 0 4-4V32a4 4 0 0 0-4-4ZM200 24a8 8 0 0 0-8 8v26.667a8 8 0 0 0 8 8h26.667a8 8 0 0 0 8-8V32a8 8 0 0 0-8-8H200Zm-96 52h26.667a4 4 0 0 1 4 4v26.667a4 4 0 0 1-4 4H104a4 4 0 0 1-4-4V80a4 4 0 0 1 4-4Zm-8 4a8 8 0 0 1 8-8h26.667a8 8 0 0 1 8 8v26.667a8 8 0 0 1-8 8H104a8 8 0 0 1-8-8V80Zm34.667 44H104a4 4 0 0 0-4 4v26.667a4 4 0 0 0 4 4h26.667a4 4 0 0 0 4-4V128a4 4 0 0 0-4-4ZM104 120a8 8 0 0 0-8 8v26.667a8 8 0 0 0 8 8h26.667a8 8 0 0 0 8-8V128a8 8 0 0 0-8-8H104Zm96-44h26.667a4 4 0 0 1 4 4v26.667a4 4 0 0 1-4 4H200a4 4 0 0 1-4-4V80a4 4 0 0 1 4-4Zm-8 4a8 8 0 0 1 8-8h26.667a8 8 0 0 1 8 8v26.667a8 8 0 0 1-8 8H200a8 8 0 0 1-8-8V80Zm34.667 44H200a4 4 0 0 0-4 4v26.667a4 4 0 0 0 4 4h26.667a4 4 0 0 0 4-4V128a4 4 0 0 0-4-4ZM200 120a8 8 0 0 0-8 8v26.667a8 8 0 0 0 8 8h26.667a8 8 0 0 0 8-8V128a8 8 0 0 0-8-8H200ZM8 76h26.667a4 4 0 0 1 4 4v26.667a4 4 0 0 1-4 4H8a4 4 0 0 1-4-4V80a4 4 0 0 1 4-4Zm-8 4a8 8 0 0 1 8-8h26.667a8 8 0 0 1 8 8v26.667a8 8 0 0 1-8 8H8a8 8 0 0 1-8-8V80Zm34.667 44H8a4 4 0 0 0-4 4v26.667a4 4 0 0 0 4 4h26.667a4 4 0 0 0 4-4V128a4 4 0 0 0-4-4ZM8 120a8 8 0 0 0-8 8v26.667a8 8 0 0 0 8 8h26.667a8 8 0 0 0 8-8V128a8 8 0 0 0-8-8H8Z" fill="url(#empty_search_result_svg__a)"/>
<path d="m135.337 94.376 11.182 11.242-3.694 3.715-11.18-11.245a23.31 23.31 0 0 1-14.666 5.17c-12.971 0-23.498-10.586-23.498-23.629C93.48 66.586 104.008 56 116.979 56c12.971 0 23.499 10.586 23.499 23.629a23.614 23.614 0 0 1-5.141 14.747Zm-5.238-1.948a18.372 18.372 0 0 0 5.157-12.799c0-10.155-8.18-18.378-18.277-18.378-10.099 0-18.276 8.223-18.276 18.378 0 10.153 8.177 18.378 18.276 18.378a18.166 18.166 0 0 0 12.729-5.185l.391-.394Z" fill="#A0AEC0"/>
<defs>
<radialGradient id="empty_search_result_svg__a" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="rotate(103.716 25.035 85.404) scale(58.3301 137.703)">
<stop offset=".081" stop-color="#CBD5E0" stop-opacity="0"/>
<stop offset=".563" stop-color="#CBD5E0" stop-opacity=".54"/>
<stop offset="1" stop-color="#CBD5E0" stop-opacity="0"/>
</radialGradient>
</defs>
</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"/>
</svg>
...@@ -18,3 +18,4 @@ export const disk = String.fromCharCode(8226); // диск • ...@@ -18,3 +18,4 @@ export const disk = String.fromCharCode(8226); // диск •
export const minus = String.fromCharCode(8722); // минус − export const minus = String.fromCharCode(8722); // минус −
export const leftLineArrow = String.fromCharCode(8592); // стрелка ← export const leftLineArrow = String.fromCharCode(8592); // стрелка ←
export const rightLineArrow = String.fromCharCode(8594); // стрелка → export const rightLineArrow = String.fromCharCode(8594); // стрелка →
export const apos = String.fromCharCode(39); // апостроф '
...@@ -60,6 +60,7 @@ export default NETWORKS; ...@@ -60,6 +60,7 @@ export default NETWORKS;
// subType: 'mainnet', // subType: 'mainnet',
// group: 'mainnets', // group: 'mainnets',
// isAccountSupported: true, // isAccountSupported: true,
// chainId: 100
// }, // },
// { // {
// name: 'Optimism on Gnosis Chain', // name: 'Optimism on Gnosis Chain',
...@@ -68,12 +69,14 @@ export default NETWORKS; ...@@ -68,12 +69,14 @@ export default NETWORKS;
// subType: 'optimism', // subType: 'optimism',
// group: 'mainnets', // group: 'mainnets',
// icon: 'https://www.fillmurray.com/60/60' // icon: 'https://www.fillmurray.com/60/60'
// chainId: 300
// }, // },
// { // {
// name: 'Arbitrum on xDai', // name: 'Arbitrum on xDai',
// type: 'xdai', // type: 'xdai',
// subType: 'aox', // subType: 'aox',
// group: 'mainnets', // group: 'mainnets',
// chainId: 200
// }, // },
// { // {
// name: 'Ethereum', // name: 'Ethereum',
...@@ -81,6 +84,7 @@ export default NETWORKS; ...@@ -81,6 +84,7 @@ export default NETWORKS;
// type: 'eth', // type: 'eth',
// subType: 'mainnet', // subType: 'mainnet',
// group: 'mainnets', // group: 'mainnets',
// chainId: 1
// }, // },
// { // {
// name: 'Ethereum Classic', // name: 'Ethereum Classic',
...@@ -88,6 +92,7 @@ export default NETWORKS; ...@@ -88,6 +92,7 @@ export default NETWORKS;
// type: 'etc', // type: 'etc',
// subType: 'mainnet', // subType: 'mainnet',
// group: 'mainnets', // group: 'mainnets',
// chainId: 61
// }, // },
// { // {
// name: 'POA', // name: 'POA',
...@@ -95,6 +100,7 @@ export default NETWORKS; ...@@ -95,6 +100,7 @@ export default NETWORKS;
// type: 'poa', // type: 'poa',
// subType: 'core', // subType: 'core',
// group: 'mainnets', // group: 'mainnets',
// chainId: 99
// }, // },
// { // {
// name: 'RSK', // name: 'RSK',
...@@ -102,6 +108,7 @@ export default NETWORKS; ...@@ -102,6 +108,7 @@ export default NETWORKS;
// type: 'rsk', // type: 'rsk',
// subType: 'mainnet', // subType: 'mainnet',
// group: 'mainnets', // group: 'mainnets',
// chainId: 30
// }, // },
// { // {
// name: 'Gnosis Chain Testnet', // name: 'Gnosis Chain Testnet',
...@@ -116,12 +123,14 @@ export default NETWORKS; ...@@ -116,12 +123,14 @@ export default NETWORKS;
// type: 'poa', // type: 'poa',
// subType: 'sokol', // subType: 'sokol',
// group: 'testnets', // group: 'testnets',
// chainId: 77
// }, // },
// { // {
// name: 'ARTIS Σ1', // name: 'ARTIS Σ1',
// type: 'artis', // type: 'artis',
// subType: 'sigma1', // subType: 'sigma1',
// group: 'other', // group: 'other',
// chainId: 246529
// }, // },
// { // {
// name: 'LUKSO L14', // name: 'LUKSO L14',
...@@ -129,5 +138,6 @@ export default NETWORKS; ...@@ -129,5 +138,6 @@ export default NETWORKS;
// type: 'lukso', // type: 'lukso',
// subType: 'l14', // subType: 'l14',
// group: 'other', // group: 'other',
// chainId: 22
// }, // },
// ]; // ];
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';
const AppsPage = () => {
return (
<Page>
<PageHeader text="Apps"/>
<Head><title>Apps</title></Head>
<Apps/>
</Page>
);
};
export default AppsPage;
...@@ -23,6 +23,7 @@ const sizes = { ...@@ -23,6 +23,7 @@ const sizes = {
minH: 6, minH: 6,
minW: 6, minW: 6,
fontSize: 'sm', fontSize: 'sm',
lineHeight: 'sm',
px: 2, px: 2,
py: '2px', py: '2px',
}, },
...@@ -33,7 +34,7 @@ const sizes = { ...@@ -33,7 +34,7 @@ const sizes = {
}; };
const baseStyleContainer = defineStyle({ const baseStyleContainer = defineStyle({
display: 'inline-flex', display: 'inline-block',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
borderRadius: 'sm', borderRadius: 'sm',
......
const breakpoints = { const breakpoints = {
// maybe we need them in future // maybe we need them in future
// sm: '320px', sm: '414px',
// md: '768px', // md: '768px',
lg: '1000px', lg: '1000px',
xl: '1440px', xl: '1440px',
// these breakpoint are needed just to make others work // these breakpoint are needed just to make others work
......
...@@ -15,9 +15,6 @@ ...@@ -15,9 +15,6 @@
"jsx": "preserve", "jsx": "preserve",
"incremental": true, "incremental": true,
"baseUrl": ".", "baseUrl": ".",
"paths": {
"~/*": ["./*"]
},
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "additional.d.ts", "decs.d.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "additional.d.ts", "decs.d.ts"],
"exclude": ["node_modules"], "exclude": ["node_modules"],
......
export type AppCategory = {
id: string;
name: string;
}
export type AppItemPreview = {
id: string;
title: string;
logo: string;
shortDescription: string;
categories: Array<AppCategory>;
}
export type AppItemOverview = AppItemPreview & {
author: string;
url: string;
description: string;
site?: string;
twitter?: string;
telegram?: string;
github?: string;
}
...@@ -4,6 +4,8 @@ export type NetworkGroup = 'mainnets' | 'testnets' | 'other'; ...@@ -4,6 +4,8 @@ export type NetworkGroup = 'mainnets' | 'testnets' | 'other';
export interface Network { export interface Network {
name: string; name: string;
// https://chainlist.org/
chainId?: number;
shortName?: string; shortName?: string;
// basePath = /<type>/<subType>, e.g. /xdai/mainnet // basePath = /<type>/<subType>, e.g. /xdai/mainnet
type: string; type: string;
......
import { Box, Heading, Image, Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { AppItemPreview } from 'types/client/apps';
const AppCard = ({ title, logo, shortDescription, categories }: AppItemPreview) => {
const categoriesLabel = categories.map(c => c.name).join(', ');
return (
<Box
borderRadius={{ base: 'none', sm: 'md' }}
height="100%"
padding={{ base: '16px', sm: '20px' }}
boxShadow={ `0 0 0 1px ${ useColorModeValue('var(--chakra-colors-gray-200)', 'var(--chakra-colors-gray-600)') }` }
>
<Box overflow="hidden" height="100%">
<Box
marginBottom={ 4 }
w={{ base: '64px', sm: '96px' }}
h={{ base: '64px', sm: '96px' }}
>
<Image
src={ logo }
alt={ `${ title } app icon` }
/>
</Box>
<Heading
as="h3"
marginBottom={ 2 }
fontSize={{ base: 'sm', sm: 'lg' }}
fontWeight="semibold"
>
{ title }
</Heading>
<Text
marginBottom={ 2 }
variant="secondary"
fontSize="xs"
>
{ categoriesLabel }
</Text>
<Text
fontSize={{ base: 'xs', sm: 'sm' }}
lineHeight="20px"
noOfLines={ 4 }
>
{ shortDescription }
</Text>
</Box>
</Box>
);
};
export default AppCard;
import { Grid, GridItem, VisuallyHidden, Heading } from '@chakra-ui/react';
import React from 'react';
import type { AppItemPreview } from 'types/client/apps';
import AppCard from 'ui/apps/AppCard';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
type Props = {
apps: Array<AppItemPreview>;
}
const AppList = ({ apps }: Props) => {
return (
<>
<VisuallyHidden>
<Heading as="h2">App list</Heading>
</VisuallyHidden>
{ apps.length > 0 ? (
<Grid
templateColumns={{
base: 'repeat(auto-fill, minmax(160px, 1fr))',
sm: 'repeat(auto-fill, minmax(200px, 1fr))',
lg: 'repeat(auto-fill, minmax(260px, 1fr))',
}}
autoRows="1fr"
gap={{ base: '1px', sm: '24px' }}
>
{ apps.map((app) => (
<GridItem
key={ app.id }
>
<AppCard
id={ app.id }
title={ app.title }
logo={ app.logo }
shortDescription={ app.shortDescription }
categories={ app.categories }
/>
</GridItem>
)) }
</Grid>
) : (
<EmptySearchResult/>
) }
</>
);
};
export default 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,
} from '@chakra-ui/react';
import type { FunctionComponent } from 'react';
import React, { useCallback } from 'react';
import type { AppCategory, AppItemOverview } from 'types/client/apps';
import { TEMPORARY_DEMO_APPS } from 'data/apps';
import ghIcon from 'icons/social/git.svg';
import tgIcon from 'icons/social/telega.svg';
import twIcon from 'icons/social/tweet.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import { nbsp } from 'lib/html-entities';
type Props = {
id: string | null;
onClose: () => void;
}
const AppModal = ({
id,
onClose,
}: Props) => {
const handleFavorite = useCallback(() => {
// TODO: implement
}, []);
if (!id) {
return null;
}
const {
title,
author,
description,
url,
site,
github,
telegram,
twitter,
logo,
categories,
} = TEMPORARY_DEMO_APPS.find(app => app.id === id) as AppItemOverview;
const isFavorite = false;
const socialLinks = [
Boolean(telegram) && {
icon: tgIcon,
url: telegram,
},
Boolean(twitter) && {
icon: twIcon,
url: twitter,
},
Boolean(github) && {
icon: ghIcon,
url: github,
},
].filter(Boolean) as Array<{ icon: FunctionComponent; url: string }>;
return (
<Modal
isOpen={ Boolean(id) }
onClose={ onClose }
size={{ base: 'full', lg: 'md' }}
isCentered
>
<ModalOverlay/>
<ModalContent>
<ModalHeader
display="grid"
gridTemplateColumns={{ base: 'auto 1fr' }}
paddingRight={{ sm: 12 }}
>
<Box
w={{ base: '72px', sm: '144px' }}
h={{ base: '72px', sm: '144px' }}
marginRight={{ base: 6, sm: 8 }}
gridRow={{ base: '1 / 3', sm: '1 / 4' }}
>
<Image
src={ logo }
alt={ `${ title } app icon` }
/>
</Box>
<Heading
as="h2"
gridColumn={ 2 }
fontSize={{ base: '2xl', sm: '3xl' }}
fontWeight="medium"
lineHeight={ 1 }
color="blue.600"
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
>
{ title }
</Heading>
<Text
variant="secondary"
gridColumn={ 2 }
fontSize="sm"
fontWeight="normal"
lineHeight={ 1 }
>
By{ nbsp }{ author }
</Text>
<Box
gridColumn={{ base: '1 / 3', sm: 2 }}
marginTop={{ base: 6, sm: 0 }}
>
<Box display="flex">
<Button
href={ url }
as="a"
size="sm"
marginRight={ 2 }
width={{ base: '100%', sm: 'auto' }}
>
Launch app
</Button>
<IconButton
aria-label="Mark as favorite"
title="Mark as favorite"
variant="outline"
colorScheme="gray"
w={ 9 }
h={ 8 }
onClick={ handleFavorite }
icon={ isFavorite ?
<Icon as={ StarIcon } w={ 4 } h={ 4 } color="yellow.400"/> :
<Icon as={ starOutlineIcon } w={ 4 } h={ 4 }/> }
/>
</Box>
</Box>
</ModalHeader>
<ModalCloseButton/>
<ModalBody>
<Heading
as="h3"
fontSize="2xl"
marginBottom={ 4 }
>
Overview
</Heading>
<Box marginBottom={ 2 }>
{ categories.map((category: AppCategory) => (
<Tag
colorScheme="blue"
marginRight={ 2 }
marginBottom={ 2 }
key={ category.id }
>
{ category.name }
</Tag>
)) }
</Box>
<Text>{ description }</Text>
</ModalBody>
<ModalFooter
display="flex"
flexDirection={{ base: 'column', sm: 'row' }}
alignItems={{ base: 'flex-start', sm: 'center' }}
>
{ site && (
<Link
isExternal
href={ site }
display="flex"
alignItems="center"
paddingRight={{ sm: 2 }}
marginBottom={{ base: 3, sm: 0 }}
maxW="100%"
overflow="hidden"
>
<Icon
as={ LinkIcon }
display="inline"
verticalAlign="baseline"
boxSize={ 3 }
marginRight={ 2 }
/>
<Text
color="inherit"
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
>
{ site }
</Text>
</Link>
) }
{ socialLinks.length && (
<List
marginLeft={{ sm: 'auto' }}
display="grid"
gridAutoFlow="column"
columnGap={ 2 }
>
{ socialLinks.map(({ icon, url }) => (
<Link
aria-label={ `Link to ${ url }` }
title={ url }
key={ url }
href={ url }
display="flex"
alignItems="center"
justifyContent="center"
isExternal
w={ 10 }
h={ 10 }
>
<Icon
as={ icon }
w="20px"
h="20px"
display="block"
/>
</Link>
)) }
</List>
) }
</ModalFooter>
</ModalContent>
</Modal>
);
};
export default AppModal;
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 = () => {
return (
<Box
display="flex"
flexDirection="column"
alignItems="center"
>
<Icon
as={ emptyIcon }
boxSize={ 60 }
display="block"
/>
<Heading
as="h3"
marginBottom={ 2 }
fontSize={{ base: '2xl', sm: '3xl' }}
fontWeight="semibold"
>
No results
</Heading>
<Text
fontSize={{ base: 'sm' }}
variant="secondary"
align="center"
>
Couldn{ apos }t find an app that matches your filter query.
</Text>
</Box>
);
};
export default EmptySearchResult;
import { SearchIcon } from '@chakra-ui/icons';
import { Input, InputGroup, InputLeftElement, useColorModeValue } from '@chakra-ui/react';
import type { ChangeEvent } from 'react';
import React, { useCallback, useState } from 'react';
type Props = {
onChange: (q: string) => void;
}
const FilterInput = ({ onChange }: Props) => {
const [ filterQuery, setFilterQuery ] = useState('');
const handleFilterQueryChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setFilterQuery(value);
onChange(value);
}, [ onChange ]);
return (
<InputGroup
size="sm"
>
<InputLeftElement
pointerEvents="none"
>
<SearchIcon color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/>
</InputLeftElement>
<Input
size="sm"
value={ filterQuery }
onChange={ handleFilterQueryChange }
marginBottom={{ base: '4', lg: '6' }}
/>
</InputGroup>
);
};
export default FilterInput;
import type { AppCategory } from 'types/client/apps';
export const APP_CATEGORIES: Array<AppCategory> = [
{
id: 'defi',
name: 'DeFi',
},
{
id: 'exchanges',
name: 'Exchanges',
},
{
id: 'finance',
name: 'Finance',
},
{
id: 'games',
name: 'Games',
},
{
id: 'marketplaces',
name: 'Marketplaces',
},
{
id: 'nft',
name: 'NFT',
},
{
id: 'security',
name: 'Security',
},
{
id: 'social',
name: 'Social',
},
{
id: 'tools',
name: 'Tools',
},
{
id: 'Yield-farming',
name: 'yield-farming',
},
];
import { Link, Icon, Text, HStack, Tooltip } from '@chakra-ui/react'; import { Link, Icon, Text, HStack, Tooltip, Box } from '@chakra-ui/react';
import NextLink from 'next/link'; import NextLink from 'next/link';
import React from 'react'; import React from 'react';
...@@ -28,36 +28,37 @@ const NavLink = ({ text, url, icon, isCollapsed, isActive, px }: Props) => { ...@@ -28,36 +28,37 @@ const NavLink = ({ text, url, icon, isCollapsed, isActive, px }: Props) => {
})(); })();
return ( return (
<NextLink href={ url } passHref> <Box as="li" listStyleType="none" w="100%">
<Link <NextLink href={ url } passHref>
as="li" <Link
listStyleType="none" w={ width }
w={ width } px={ px || (isCollapsed ? '15px' : 3) }
px={ px || (isCollapsed ? '15px' : 3) } py={ 2.5 }
py={ 2.5 } display="flex"
color={ isActive ? colors.text.active : colors.text.default } color={ isActive ? colors.text.active : colors.text.default }
bgColor={ isActive ? colors.bg.active : colors.bg.default } bgColor={ isActive ? colors.bg.active : colors.bg.default }
_hover={{ color: isActive ? colors.text.active : colors.text.hover }} _hover={{ color: isActive ? colors.text.active : colors.text.hover }}
borderRadius="base" borderRadius="base"
whiteSpace="nowrap" whiteSpace="nowrap"
{ ...getDefaultTransitionProps({ transitionProperty: 'width, padding' }) } { ...getDefaultTransitionProps({ transitionProperty: 'width, padding' }) }
>
<Tooltip
label={ text }
hasArrow={ false }
isDisabled={ !isCollapsed }
placement="right"
variant="nav"
gutter={ 15 }
color={ isActive ? colors.text.active : colors.text.hover }
> >
<HStack spacing={ 3 }> <Tooltip
<Icon as={ icon } boxSize="30px"/> label={ text }
{ !isCollapsed && <Text variant="inherit" fontSize="sm" lineHeight="20px">{ text }</Text> } hasArrow={ false }
</HStack> isDisabled={ !isCollapsed }
</Tooltip> placement="right"
</Link> variant="nav"
</NextLink> gutter={ 15 }
color={ isActive ? colors.text.active : colors.text.hover }
>
<HStack spacing={ 3 }>
<Icon as={ icon } boxSize="30px"/>
{ !isCollapsed && <Text variant="inherit" fontSize="sm" lineHeight="20px">{ text }</Text> }
</HStack>
</Tooltip>
</Link>
</NextLink>
</Box>
); );
}; };
......
import debounce from 'lodash/debounce';
import React, { useCallback, useState } from 'react';
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';
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 filterApps = (q: string) => {
const apps = displayedApps
.filter(app => app.title.toLowerCase().includes(q.toLowerCase()));
setDisplayedApps(apps);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
const debounceFilterApps = useCallback(debounce(q => filterApps(q), 500), []);
const clearDisplayedAppId = useCallback(() => setDisplayedAppId(null), []);
return (
<>
<FilterInput onChange={ debounceFilterApps }/>
<AppList apps={ displayedApps }/>
<AppModal
id={ displayedAppId }
onClose={ clearDisplayedAppId }
/>
</>
);
};
export default Apps;
import { Box } from '@chakra-ui/react';
import React from 'react';
import Jazzicon, { jsNumberForAddress } from 'react-jazzicon';
const AddressIcon = ({ address }: {address: string}) => {
return (
<Box width="24px" display="inline-flex">
<Jazzicon diameter={ 24 } seed={ jsNumberForAddress(address) }/>
</Box>
);
};
export default AddressIcon;
import { Flex, Link } from '@chakra-ui/react';
import type { HTMLChakraProps } from '@chakra-ui/system';
import React from 'react';
import useLink from 'lib/link/useLink';
import AddressWithDots from './AddressWithDots';
import CopyToClipboard from './CopyToClipboard';
const FONT_WEIGHT = '600';
interface Props extends HTMLChakraProps<'div'> {
address: string;
type?: 'address' | 'transaction' | 'token';
fontWeight?: string;
truncated?: boolean;
withCopy?: boolean;
}
const AddressLinkWithTooltip = ({ address, type = 'address', truncated, withCopy = true, ...styles }: Props) => {
const link = useLink();
let url;
if (type === 'transaction') {
url = link('tx_index', { id: address });
} else if (type === 'token') {
url = link('token_index', { id: address });
} else {
url = link('address_index', { id: address });
}
return (
<Flex columnGap={ 2 } alignItems="center" overflow="hidden" maxW="100%" { ...styles }>
<Link
href={ url }
target="_blank"
overflow="hidden"
fontWeight={ styles.fontWeight || FONT_WEIGHT }
lineHeight="24px"
whiteSpace="nowrap"
>
<AddressWithDots address={ address } fontWeight={ styles.fontWeight || FONT_WEIGHT } truncated={ truncated }/>
</Link>
{ withCopy && <CopyToClipboard text={ address }/> }
</Flex>
);
};
export default React.memo(AddressLinkWithTooltip);
import { Box, HStack, Text } from '@chakra-ui/react'; import { Text, Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import AddressIcon from 'ui/shared/AddressIcon'; import Address from 'ui/shared/address/Address';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props { interface Props {
address: string; address: string;
...@@ -11,13 +13,14 @@ interface Props { ...@@ -11,13 +13,14 @@ interface Props {
const AddressSnippet = ({ address, subtitle }: Props) => { const AddressSnippet = ({ address, subtitle }: Props) => {
return ( return (
<HStack spacing={ 4 } key={ address } overflow="hidden" alignItems="start" maxW="100%"> <Box maxW="100%">
<AddressIcon address={ address }/> <Address>
<Box overflow="hidden"> <AddressIcon hash={ address }/>
<AddressLinkWithTooltip address={ address }/> <AddressLink hash={ address } fontWeight="600" ml={ 2 }/>
{ subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 }>{ subtitle }</Text> } <CopyToClipboard text={ address } ml={ 1 }/>
</Box> </Address>
</HStack> { subtitle && <Text fontSize="sm" variant="secondary" mt={ 0.5 } ml={{ base: 0, lg: 8 }}>{ subtitle }</Text> }
</Box>
); );
}; };
......
import { IconButton, Tooltip, useClipboard } from '@chakra-ui/react'; import { IconButton, Tooltip, useClipboard, chakra } from '@chakra-ui/react';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import CopyIcon from 'icons/copy.svg'; import CopyIcon from 'icons/copy.svg';
const CopyToClipboard = ({ text }: {text: string}) => { const CopyToClipboard = ({ text, className }: {text: string; className?: string}) => {
const { hasCopied, onCopy } = useClipboard(text, 3000); const { hasCopied, onCopy } = useClipboard(text, 3000);
const [ copied, setCopied ] = useState(false); const [ copied, setCopied ] = useState(false);
...@@ -26,9 +26,10 @@ const CopyToClipboard = ({ text }: {text: string}) => { ...@@ -26,9 +26,10 @@ const CopyToClipboard = ({ text }: {text: string}) => {
display="inline-block" display="inline-block"
flexShrink={ 0 } flexShrink={ 0 }
onClick={ onCopy } onClick={ onCopy }
className={ className }
/> />
</Tooltip> </Tooltip>
); );
}; };
export default CopyToClipboard; export default chakra(CopyToClipboard);
import { Tooltip } from '@chakra-ui/react';
import React from 'react';
interface Props {
hash: string;
}
const HashStringShorten = ({ hash }: Props) => {
return (
<Tooltip label={ hash }>
{ hash.slice(0, 4) + '...' + hash.slice(-4) }
</Tooltip>
);
};
export default HashStringShorten;
// this component trims address like 0x123...4567 (always 4 chars after dots) // this component trims hash string like 0x123...4567 (always 4 chars after dots)
// or shows full address, if fits // or shows full hash string, if fits
// i can't do this with pure css. if you can, feel free to replace it // i can't do this with pure css. if you can, feel free to replace it
...@@ -20,28 +20,20 @@ const TAIL_LENGTH = 4; ...@@ -20,28 +20,20 @@ const TAIL_LENGTH = 4;
const HEAD_MIN_LENGTH = 4; const HEAD_MIN_LENGTH = 4;
interface Props { interface Props {
address: string; hash: string;
fontWeight: string | number; fontWeight?: string | number;
truncated?: boolean;
} }
const shortenAddress = (address: string) => address.slice(0, 4) + '...' + address.slice(-4); const HashStringShortenDynamic = ({ hash, fontWeight = '400' }: Props) => {
const elementRef = useRef<HTMLSpanElement>(null);
const AddressWithDots = ({ address, fontWeight, truncated }: Props) => { const [ displayedString, setDisplayedString ] = React.useState(hash);
const addressRef = useRef<HTMLSpanElement>(null);
const [ displayedAddress, setAddress ] = React.useState(truncated ? shortenAddress(address) : address);
const isFontFaceLoaded = useFontFaceObserver([ const isFontFaceLoaded = useFontFaceObserver([
{ family: BODY_TYPEFACE, weight: String(fontWeight) as FontFace['weight'] }, { family: BODY_TYPEFACE, weight: String(fontWeight) as FontFace['weight'] },
]); ]);
const calculateString = useCallback(() => { const calculateString = useCallback(() => {
const addressEl = addressRef.current; const parent = elementRef?.current?.parentNode as HTMLElement;
if (!addressEl) {
return;
}
const parent = addressRef?.current?.parentNode as HTMLElement;
if (!parent) { if (!parent) {
return; return;
} }
...@@ -49,51 +41,49 @@ const AddressWithDots = ({ address, fontWeight, truncated }: Props) => { ...@@ -49,51 +41,49 @@ const AddressWithDots = ({ address, fontWeight, truncated }: Props) => {
const shadowEl = document.createElement('span'); const shadowEl = document.createElement('span');
shadowEl.style.opacity = '0'; shadowEl.style.opacity = '0';
parent.appendChild(shadowEl); parent.appendChild(shadowEl);
shadowEl.textContent = address; shadowEl.textContent = hash;
const parentWidth = getWidth(parent); const parentWidth = getWidth(parent);
if (getWidth(shadowEl) > parentWidth) { if (getWidth(shadowEl) > parentWidth) {
for (let i = 1; i <= address.length - TAIL_LENGTH - HEAD_MIN_LENGTH; i++) { for (let i = 1; i <= hash.length - TAIL_LENGTH - HEAD_MIN_LENGTH; i++) {
const res = address.slice(0, address.length - i - TAIL_LENGTH) + '...' + address.slice(-TAIL_LENGTH); const res = hash.slice(0, hash.length - i - TAIL_LENGTH) + '...' + hash.slice(-TAIL_LENGTH);
shadowEl.textContent = res; shadowEl.textContent = res;
if (getWidth(shadowEl) < parentWidth || i === address.length - TAIL_LENGTH - HEAD_MIN_LENGTH) { if (getWidth(shadowEl) < parentWidth || i === hash.length - TAIL_LENGTH - HEAD_MIN_LENGTH) {
setAddress(res); setDisplayedString(res);
break; break;
} }
} }
} else { } else {
setAddress(address); setDisplayedString(hash);
} }
parent.removeChild(shadowEl); parent.removeChild(shadowEl);
}, [ address ]); }, [ hash ]);
// we want to do recalculation when isFontFaceLoaded flag is changed // we want to do recalculation when isFontFaceLoaded flag is changed
// but we don't want to create more resize event listeners // but we don't want to create more resize event listeners
// that's why there are separate useEffect hooks // that's why there are separate useEffect hooks
useEffect(() => { useEffect(() => {
!truncated && calculateString(); calculateString();
}, [ calculateString, isFontFaceLoaded, truncated ]); }, [ calculateString, isFontFaceLoaded ]);
useEffect(() => { useEffect(() => {
if (!truncated) { const resizeHandler = _debounce(calculateString, 100);
const resizeHandler = _debounce(calculateString, 100); const resizeObserver = new ResizeObserver(resizeHandler);
const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(document.body);
resizeObserver.observe(document.body); return function cleanup() {
return function cleanup() { resizeObserver.unobserve(document.body);
resizeObserver.unobserve(document.body); };
}; }, [ calculateString ]);
}
}, [ calculateString, truncated ]);
const content = <span ref={ addressRef }>{ displayedAddress }</span>; const content = <span ref={ elementRef }>{ displayedString }</span>;
const isTruncated = truncated || address.length !== displayedAddress.length; const isTruncated = hash.length !== displayedString.length;
if (isTruncated) { if (isTruncated) {
return ( return (
<Tooltip label={ address }>{ content }</Tooltip> <Tooltip label={ hash }>{ content }</Tooltip>
); );
} }
...@@ -104,4 +94,4 @@ function getWidth(el: HTMLElement) { ...@@ -104,4 +94,4 @@ function getWidth(el: HTMLElement) {
return el.getBoundingClientRect().width; return el.getBoundingClientRect().width;
} }
export default React.memo(AddressWithDots); export default React.memo(HashStringShortenDynamic);
...@@ -43,9 +43,7 @@ const Page = ({ children }: Props) => { ...@@ -43,9 +43,7 @@ const Page = ({ children }: Props) => {
<Header/> <Header/>
<Box <Box
as="main" as="main"
borderRadius="base"
w="100%" w="100%"
overflow="hidden"
paddingTop={ isMobile ? '138px' : '52px' } paddingTop={ isMobile ? '138px' : '52px' }
> >
{ children } { children }
......
import { Box, HStack, Icon, useColorModeValue } from '@chakra-ui/react'; import { Icon, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import transactionIcon from 'icons/transactions.svg'; import transactionIcon from 'icons/transactions.svg';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props { interface Props {
hash: string; hash: string;
...@@ -10,12 +12,11 @@ interface Props { ...@@ -10,12 +12,11 @@ interface Props {
const TransactionSnippet = ({ hash }: Props) => { const TransactionSnippet = ({ hash }: Props) => {
return ( return (
<HStack spacing={ 2 } overflow="hidden" alignItems="start" maxW="100%"> <Address maxW="100%">
<Icon as={ transactionIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') }/> <Icon as={ transactionIcon } boxSize={ 6 } color={ useColorModeValue('gray.500', 'gray.400') }/>
<Box overflow="hidden"> <AddressLink hash={ hash } fontWeight="600" type="transaction" ml={ 2 }/>
<AddressLinkWithTooltip address={ hash } type="transaction"/> <CopyToClipboard text={ hash } ml={ 1 }/>
</Box> </Address>
</HStack>
); );
}; };
......
import { Flex, chakra } from '@chakra-ui/react';
import React from 'react';
interface Props {
className?: string;
children: React.ReactNode;
}
const Address = ({ children, className }: Props) => {
return <Flex alignItems="center" overflow="hidden" className={ className }>{ children }</Flex>;
};
const AddressChakra = chakra(Address);
export default React.memo(AddressChakra);
import { Box, chakra } from '@chakra-ui/react';
import React from 'react';
import Jazzicon, { jsNumberForAddress } from 'react-jazzicon';
const AddressIcon = ({ hash, className }: {hash: string; className?: string}) => {
return (
<Box className={ className } width="24px" display="inline-flex">
<Jazzicon diameter={ 24 } seed={ jsNumberForAddress(hash) }/>
</Box>
);
};
export default chakra(AddressIcon);
import { Link, chakra, shouldForwardProp } from '@chakra-ui/react';
import React from 'react';
import useLink from 'lib/link/useLink';
import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
interface Props {
type?: 'address' | 'transaction' | 'token';
className?: string;
hash: string;
truncation?: 'constant' | 'dynamic'| 'none';
fontWeight?: string;
}
const AddressLink = ({ type, className, truncation = 'dynamic', hash, fontWeight }: Props) => {
const link = useLink();
let url;
if (type === 'transaction') {
url = link('tx_index', { id: hash });
} else if (type === 'token') {
url = link('token_index', { id: hash });
} else {
url = link('address_index', { id: hash });
}
const content = (() => {
switch (truncation) {
case 'constant':
return <HashStringShorten hash={ hash }/>;
case 'dynamic':
return <HashStringShortenDynamic hash={ hash } fontWeight={ fontWeight }/>;
case 'none':
return <span>{ hash }</span>;
}
})();
return (
<Link
className={ className }
href={ url }
target="_blank"
overflow="hidden"
whiteSpace="nowrap"
>
{ content }
</Link>
);
};
const AddressLinkChakra = chakra(AddressLink, {
shouldForwardProp: (prop) => {
const isChakraProp = !shouldForwardProp(prop);
// forward fontWeight to the AddressLink since it's needed for underlying HashStringShortenDynamic component
if (isChakraProp && prop !== 'fontWeight') {
return false;
}
return true;
},
});
export default React.memo(AddressLinkChakra);
...@@ -3,7 +3,7 @@ import React from 'react'; ...@@ -3,7 +3,7 @@ import React from 'react';
import rightArrowIcon from 'icons/arrows/right.svg'; import rightArrowIcon from 'icons/arrows/right.svg';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; import AddressLink from 'ui/shared/address/AddressLink';
import Token from 'ui/shared/Token'; import Token from 'ui/shared/Token';
interface Props { interface Props {
...@@ -17,9 +17,9 @@ interface Props { ...@@ -17,9 +17,9 @@ interface Props {
const TokenTransfer = ({ from, to, amount, usd, token }: Props) => { const TokenTransfer = ({ from, to, amount, usd, token }: Props) => {
return ( return (
<Center> <Center>
<AddressLinkWithTooltip address={ from } fontWeight="500" truncated withCopy={ false }/> <AddressLink fontWeight="500" hash={ from } truncation="constant"/>
<Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } color="gray.500"/> <Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } color="gray.500"/>
<AddressLinkWithTooltip address={ to } fontWeight="500" truncated withCopy={ false }/> <AddressLink fontWeight="500" hash={ to } truncation="constant"/>
<Text fontWeight={ 500 } as="span" ml={ 4 }>For:{ space } <Text fontWeight={ 500 } as="span" ml={ 4 }>For:{ space }
<Text fontWeight={ 600 } as="span">{ amount }</Text>{ space } <Text fontWeight={ 600 } as="span">{ amount }</Text>{ space }
<Text fontWeight={ 400 } variant="secondary" as="span">(${ usd.toFixed(2) })</Text> <Text fontWeight={ 400 } variant="secondary" as="span">(${ usd.toFixed(2) })</Text>
......
import { Flex, Text, Grid, GridItem, useColorModeValue } from '@chakra-ui/react'; import { Flex, Text, Grid, GridItem, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface RowProps { interface RowProps {
...@@ -109,10 +110,16 @@ const TxDecodedInputData = () => { ...@@ -109,10 +110,16 @@ const TxDecodedInputData = () => {
Data Data
</GridItem> </GridItem>
<TableRow name="from" type="address"> <TableRow name="from" type="address">
<AddressLinkWithTooltip address="0x0000000000000000000000000000000000000000" columnGap={ 0 } justifyContent="space-between" fontWeight="400"/> <Address justifyContent="space-between">
<AddressLink hash="0x0000000000000000000000000000000000000000"/>
<CopyToClipboard text="0x0000000000000000000000000000000000000000"/>
</Address>
</TableRow> </TableRow>
<TableRow name="from" type="address"> <TableRow name="from" type="address">
<AddressLinkWithTooltip address="0xcf0c50b7ea8af37d57380a0ac199d55b0782c718" columnGap={ 0 } justifyContent="space-between" fontWeight="400"/> <Address justifyContent="space-between">
<AddressLink hash="0xcf0c50b7ea8af37d57380a0ac199d55b0782c718"/>
<CopyToClipboard text="0xcf0c50b7ea8af37d57380a0ac199d55b0782c718"/>
</Address>
</TableRow> </TableRow>
<TableRow name="tokenId" type="uint256" isLast> <TableRow name="tokenId" type="uint256" isLast>
<Flex alignItems="center" justifyContent="space-between"> <Flex alignItems="center" justifyContent="space-between">
......
...@@ -7,8 +7,9 @@ import clockIcon from 'icons/clock.svg'; ...@@ -7,8 +7,9 @@ import clockIcon from 'icons/clock.svg';
import flameIcon from 'icons/flame.svg'; import flameIcon from 'icons/flame.svg';
import successIcon from 'icons/status/success.svg'; import successIcon from 'icons/status/success.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import AddressIcon from 'ui/shared/AddressIcon'; import Address from 'ui/shared/address/Address';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import RawInputData from 'ui/shared/RawInputData'; import RawInputData from 'ui/shared/RawInputData';
...@@ -77,15 +78,21 @@ const TxDetails = () => { ...@@ -77,15 +78,21 @@ const TxDetails = () => {
hint="Address (external or contract) sending the transaction." hint="Address (external or contract) sending the transaction."
mt={ 8 } mt={ 8 }
> >
<AddressIcon address={ tx.address_from }/> <Address>
<AddressLinkWithTooltip address={ tx.address_from } columnGap={ 0 } ml={ 2 } fontWeight="400"/> <AddressIcon hash={ tx.address_from }/>
<AddressLink ml={ 2 } hash={ tx.address_from }/>
<CopyToClipboard text={ tx.address_from }/>
</Address>
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title="Interacted with contract" title="Interacted with contract"
hint="Address (external or contract) receiving the transaction." hint="Address (external or contract) receiving the transaction."
> >
<AddressIcon address={ tx.address_to }/> <Address>
<AddressLinkWithTooltip address={ tx.address_to } columnGap={ 0 } ml={ 2 } fontWeight="400"/> <AddressIcon hash={ tx.address_to }/>
<AddressLink ml={ 2 } hash={ tx.address_to }/>
<CopyToClipboard text={ tx.address_to }/>
</Address>
<Tag colorScheme="orange" variant="solid" ml={ 3 }>SANA</Tag> <Tag colorScheme="orange" variant="solid" ml={ 3 }>SANA</Tag>
<Icon as={ successIcon } boxSize={ 4 } ml={ 2 } color="green.500"/> <Icon as={ successIcon } boxSize={ 4 } ml={ 2 } color="green.500"/>
<Token symbol="USDT" ml={ 3 }/> <Token symbol="USDT" ml={ 3 }/>
......
...@@ -14,7 +14,7 @@ const TxStatus = ({ status }: Props) => { ...@@ -14,7 +14,7 @@ const TxStatus = ({ status }: Props) => {
const colorScheme = status === 'success' ? 'green' : 'red'; const colorScheme = status === 'success' ? 'green' : 'red';
return ( return (
<Tag colorScheme={ colorScheme }> <Tag colorScheme={ colorScheme } display="inline-flex">
<TagLeftIcon boxSize={ 2.5 } as={ icon }/> <TagLeftIcon boxSize={ 2.5 } as={ icon }/>
<TagLabel>{ label }</TagLabel> <TagLabel>{ label }</TagLabel>
</Tag> </Tag>
......
import { Tr, Td, Tag, Flex, Icon } from '@chakra-ui/react'; import { Tr, Td, Tag, Icon } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
import rightArrowIcon from 'icons/arrows/right.svg'; import rightArrowIcon from 'icons/arrows/right.svg';
import AddressIcon from 'ui/shared/AddressIcon'; import Address from 'ui/shared/address/Address';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/tx/TxStatus'; import TxStatus from 'ui/tx/TxStatus';
interface Props { interface Props {
...@@ -24,17 +25,17 @@ const TxInternalTableItem = ({ type, status, from, to, value, gasLimit }: Props) ...@@ -24,17 +25,17 @@ const TxInternalTableItem = ({ type, status, from, to, value, gasLimit }: Props)
<TxStatus status={ status }/> <TxStatus status={ status }/>
</Td> </Td>
<Td pr="0"> <Td pr="0">
<Flex alignItems="center"> <Address>
<AddressIcon address={ from }/> <AddressIcon hash={ from }/>
<AddressLinkWithTooltip address={ from } fontWeight="500" withCopy={ false } ml={ 2 }/> <AddressLink ml={ 2 } fontWeight="500" hash={ from }/>
<Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } color="gray.500"/> <Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } flexShrink={ 0 } color="gray.500"/>
</Flex> </Address>
</Td> </Td>
<Td pl="0"> <Td pl="0">
<Flex alignItems="center"> <Address>
<AddressIcon address={ to }/> <AddressIcon hash={ to }/>
<AddressLinkWithTooltip address={ to } fontWeight="500" withCopy={ false } ml={ 2 }/> <AddressLink ml={ 2 } fontWeight="500" hash={ to }/>
</Flex> </Address>
</Td> </Td>
<Td isNumeric> <Td isNumeric>
{ value } { value }
......
...@@ -2,8 +2,9 @@ import { SearchIcon } from '@chakra-ui/icons'; ...@@ -2,8 +2,9 @@ import { SearchIcon } from '@chakra-ui/icons';
import { Text, Grid, GridItem, Link, Tooltip, Button, useColorModeValue } from '@chakra-ui/react'; import { Text, Grid, GridItem, Link, Tooltip, Button, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import AddressIcon from 'ui/shared/AddressIcon'; import Address from 'ui/shared/address/Address';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxLogTopic from 'ui/tx/logs/TxLogTopic'; import TxLogTopic from 'ui/tx/logs/TxLogTopic';
import DecodedInputData from 'ui/tx/TxDecodedInputData'; import DecodedInputData from 'ui/tx/TxDecodedInputData';
...@@ -24,8 +25,10 @@ const TxLogItem = ({ address, index, topics, data }: Props) => { ...@@ -24,8 +25,10 @@ const TxLogItem = ({ address, index, topics, data }: Props) => {
<Grid gridTemplateColumns="200px 1fr" gap={ 8 } py={ 8 } _notFirst={{ borderTopWidth: '1px', borderTopColor: borderColor }}> <Grid gridTemplateColumns="200px 1fr" gap={ 8 } py={ 8 } _notFirst={{ borderTopWidth: '1px', borderTopColor: borderColor }}>
<RowHeader>Address</RowHeader> <RowHeader>Address</RowHeader>
<GridItem display="flex" alignItems="center"> <GridItem display="flex" alignItems="center">
<AddressIcon address={ address }/> <Address>
<AddressLinkWithTooltip address={ address } columnGap={ 0 } ml={ 2 } fontWeight="400" withCopy={ false }/> <AddressIcon hash={ address }/>
<AddressLink hash={ address } ml={ 2 }/>
</Address>
<Tooltip label="Find matches topic"> <Tooltip label="Find matches topic">
<Link ml={ 2 }> <Link ml={ 2 }>
<SearchIcon w={ 5 } h={ 5 }/> <SearchIcon w={ 5 } h={ 5 }/>
......
...@@ -7,7 +7,6 @@ import { ...@@ -7,7 +7,6 @@ import {
Box, Box,
Tr, Tr,
Td, Td,
Flex,
Stat, Stat,
StatArrow, StatArrow,
Portal, Portal,
...@@ -18,8 +17,9 @@ import React, { useRef } from 'react'; ...@@ -18,8 +17,9 @@ import React, { useRef } from 'react';
import type { TTxStateItem } from 'data/txState'; import type { TTxStateItem } from 'data/txState';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import AddressIcon from 'ui/shared/AddressIcon'; import Address from 'ui/shared/address/Address';
import AddressLinkWithTooltip from 'ui/shared/AddressLinkWithTooltip'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import TxStateStorageItem from './TxStateStorageItem'; import TxStateStorageItem from './TxStateStorageItem';
...@@ -57,10 +57,10 @@ const TxStateTableItem = ({ txStateItem }: { txStateItem: TTxStateItem }) => { ...@@ -57,10 +57,10 @@ const TxStateTableItem = ({ txStateItem }: { txStateItem: TTxStateItem }) => {
</AccordionButton> </AccordionButton>
</Td> </Td>
<Td border={ 0 }> <Td border={ 0 }>
<Flex height="30px" alignItems="center"> <Address height="30px">
<AddressIcon address={ txStateItem.address }/> <AddressIcon hash={ txStateItem.address }/>
<AddressLinkWithTooltip address={ txStateItem.address } fontWeight="500" truncated withCopy={ false } ml={ 2 }/> <AddressLink hash={ txStateItem.address } fontWeight="500" truncation="constant" ml={ 2 }/>
</Flex> </Address>
</Td> </Td>
<Td border={ 0 } lineHeight="30px"><Link>{ txStateItem.miner }</Link></Td> <Td border={ 0 } lineHeight="30px"><Link>{ txStateItem.miner }</Link></Td>
<Td border={ 0 } isNumeric lineHeight="30px"> <Td border={ 0 } isNumeric lineHeight="30px">
......
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