Commit ea3d83c9 authored by tom's avatar tom

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

parents 58eeb850 1cc50d40
...@@ -4,6 +4,8 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_GITHUB_LINK ...@@ -4,6 +4,8 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_GITHUB_LINK
NEXT_PUBLIC_FOOTER_TWITTER_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_TWITTER_LINK NEXT_PUBLIC_FOOTER_TWITTER_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_TWITTER_LINK
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_TELEGRAM_LINK NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_TELEGRAM_LINK
NEXT_PUBLIC_FOOTER_STAKING_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_STAKING_LINK NEXT_PUBLIC_FOOTER_STAKING_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_STAKING_LINK
NEXT_PUBLIC_FOOTER_STAKING_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_STAKING_LINK
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=APP_NEXT_NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM
NEXT_PUBLIC_SENTRY_DSN=APP_NEXT_NEXT_PUBLIC_SENTRY_DSN NEXT_PUBLIC_SENTRY_DSN=APP_NEXT_NEXT_PUBLIC_SENTRY_DSN
NEXT_PUBLIC_APP_INSTANCE=APP_NEXT_NEXT_PUBLIC_APP_INSTANCE NEXT_PUBLIC_APP_INSTANCE=APP_NEXT_NEXT_PUBLIC_APP_INSTANCE
NEXT_PUBLIC_NETWORK_NAME=APP_NEXT_NEXT_PUBLIC_NETWORK_NAME NEXT_PUBLIC_NETWORK_NAME=APP_NEXT_NEXT_PUBLIC_NETWORK_NAME
......
{
"recommendations": [
"formulahendry.auto-close-tag",
"formulahendry.auto-rename-tag",
"dbaeumer.vscode-eslint",
"eamodio.gitlens",
]
}
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "dev",
"problemMatcher": [],
"label": "dev server",
"detail": "start local dev server",
"presentation": {
"reveal": "silent",
"panel": "new",
"close": true,
"revealProblems": "onProblem",
},
"icon": {
"color": "terminal.ansiGreen",
"id": "server-process"
},
"runOptions": {
"instanceLimit": 1
}
},
{
"type": "typescript",
"label": "tsc build",
"detail": "run ts typechecking",
"tsconfig": "tsconfig.json",
"problemMatcher": [
"$tsc"
],
"icon": {
"color": "terminal.ansiCyan",
"id": "symbol-type-parameter"
},
"presentation": {
"reveal": "never",
"panel": "new",
"close": true,
"revealProblems": "onProblem",
},
"group": "build",
}
]
}
\ No newline at end of file
...@@ -20,7 +20,7 @@ And of course our premier language is [Typescript](https://www.typescriptlang.or ...@@ -20,7 +20,7 @@ And of course our premier language is [Typescript](https://www.typescriptlang.or
For local development please follow next steps: For local development please follow next steps:
- clone repo - clone repo
- install dependencies with `yarn` - install dependencies with `yarn`
- clone `env.example` into `configs/envs/env.secrets` and fill it with necessary secret values (see description [below](#environment-variables)) - clone `.env.example` into `configs/envs/.env.secrets` and fill it with necessary secret values (see description [below](#environment-variables))
- to spin up local dev server - to spin up local dev server
- for predefined networks configs (see full available list in `package.json`) you can just run `yarn dev:<app_name>` - for predefined networks configs (see full available list in `package.json`) you can just run `yarn dev:<app_name>`
- for custom network setup create `.env.local` file with all required environment variables from the [list](#environment-variables) and run `yarn dev` - for custom network setup create `.env.local` file with all required environment variables from the [list](#environment-variables) and run `yarn dev`
...@@ -50,6 +50,7 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -50,6 +50,7 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` *(optional)* | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` | | NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` *(optional)* | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` |
| NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` *(optional)* | Link to Telegram in the footer | `https://t.me/poa_network` | | NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` *(optional)* | Link to Telegram in the footer | `https://t.me/poa_network` |
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` | | NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` *(optional)* | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` |
| NEXT_PUBLIC_APP_INSTANCE | `string` *(optional)* | Name of app instance | `wonderful_kepler` | | NEXT_PUBLIC_APP_INSTANCE | `string` *(optional)* | Name of app instance | `wonderful_kepler` |
| NEXT_PUBLIC_APP_PROTOCOL | `http \| https` *(optional)* | App protocol (`https` used as default value) | `https` | | NEXT_PUBLIC_APP_PROTOCOL | `http \| https` *(optional)* | App protocol (`https` used as default value) | `https` |
| NEXT_PUBLIC_APP_HOST | `string` | App host | `blockscout.com` | | NEXT_PUBLIC_APP_HOST | `string` | App host | `blockscout.com` |
......
...@@ -36,6 +36,7 @@ const config = Object.freeze({ ...@@ -36,6 +36,7 @@ const config = Object.freeze({
featuredNetworks: process.env.NEXT_PUBLIC_FEATURED_NETWORKS?.replaceAll('\'', '"'), featuredNetworks: process.env.NEXT_PUBLIC_FEATURED_NETWORKS?.replaceAll('\'', '"'),
blockScoutVersion: process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION, blockScoutVersion: process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION,
isAccountSupported: process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED?.replaceAll('\'', '"') === 'true', isAccountSupported: process.env.NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED?.replaceAll('\'', '"') === 'true',
marketplaceSubmitForm: process.env.NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM,
protocol: process.env.NEXT_PUBLIC_APP_PROTOCOL, protocol: process.env.NEXT_PUBLIC_APP_PROTOCOL,
host: process.env.NEXT_PUBLIC_APP_HOST, host: process.env.NEXT_PUBLIC_APP_HOST,
port: process.env.NEXT_PUBLIC_APP_PORT, port: process.env.NEXT_PUBLIC_APP_PORT,
......
...@@ -5,4 +5,5 @@ NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta ...@@ -5,4 +5,5 @@ NEXT_PUBLIC_BLOCKSCOUT_VERSION=v4.1.7-beta
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout 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_APP_INSTANCE=local NEXT_PUBLIC_APP_INSTANCE=local
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM=https://airtable.com/shrqUAcjgGJ4jU88C
NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'}] NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','basePath':'/xdai/mainnet','group':'mainnets'}]
This diff is collapsed.
This diff is collapsed.
/* eslint-disable max-len */ /* eslint-disable max-len */
export const tx = { export const tx = {
hash: '0x1ea365d2144796f793883534aa51bf20d23292b19478994eede23dfc599e7c34', hash: '0x1ea365d2144796f793883534aa51bf20d23292b19478994eede23dfc599e7c34',
status: 'success' as TxStatus, status: 'ok' as Transaction['status'],
block_num: 15006918, block_num: 15006918,
confirmation_num: 283, confirmation_num: 283,
confirmation_duration: 30, confirmation_duration: 30,
...@@ -45,11 +45,11 @@ export const tx = { ...@@ -45,11 +45,11 @@ export const tx = {
input_hex: '0x42842e0e0000000000000000000000007767dac225a233ea1055d79fb227b1696d538b75000000000000000000000000fc3017c31fe752fc48e904050ea5d6edfc38a1b00000000000000000000000000000000000000000000000000000000000000e3b', input_hex: '0x42842e0e0000000000000000000000007767dac225a233ea1055d79fb227b1696d538b75000000000000000000000000fc3017c31fe752fc48e904050ea5d6edfc38a1b00000000000000000000000000000000000000000000000000000000000000e3b',
transferred_tokens: [ transferred_tokens: [
{ from: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', to: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e', token: { symbol: 'VIK', hash: '0xADFE00d92e5A16e773891F59780e6e54f40B532e', name: 'Viktor Coin' }, amount: 192.7, usd: 194.05 }, { from: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', to: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e', token: { symbol: 'VIK', hash: '0xADFE00d92e5A16e773891F59780e6e54f40B532e', name: 'Viktor Coin' }, amount: 192.7, usd: 194.05 },
{ from: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', to: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', token: { symbol: 'PAO', hash: '0xC98a06220239818B086CD96756d4E3bC41EC848E', name: 'POA Candy' }, amount: 76.1851851851846, usd: 194.05 }, { from: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', to: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01', token: { symbol: 'PAO', hash: '0xC98a06220239818B086CD96756d4E3bC41EC848f', name: 'POA Candy' }, amount: 76.1851851851846, usd: 194.05 },
], ],
txType: 'transaction' as TxType, txType: 'transaction' as TxType,
}; };
export type TxType = 'contract-call' | 'transaction' | 'token-transfer' | 'internal-tx' | 'multicall'; export type TxType = 'contract-call' | 'transaction' | 'token-transfer' | 'internal-tx' | 'multicall';
export type TxStatus = 'success' | 'failed' | 'pending'; import type { Transaction } from 'types/api/transaction';
import type { TxInternalsType } from 'types/api/tx';
export const data = [
{
id: 1,
type: 'call' as TxInternalsType,
status: 'success' as const,
from: { hash: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01' },
to: { hash: '0xF7A558692dFB5F456e291791da7FAE8Dd046574e' },
value: 0.25207646303,
gasLimit: 369472,
},
{
id: 2,
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' as TxInternalsType,
status: 'failed' as const,
from: { hash: '0x97Aa2EfcF35c0f4c9AaDDCa8c2330fa7A9533830' },
to: { hash: '0x35317007D203b8a86CA727ad44E473E40450E378' },
value: 0.421152366,
gasLimit: 509333,
},
];
export const data = [
{
address: '0x12E80C27BfFBB76b4A8d26FF2bfd3C9f310FFA01',
topics: [
{ hex: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' },
{ hex: '0x000000000000000000000000def171fe48cf0115b1d80b88dc8eab59176fee57' },
{ hex: '0x000000000000000000000000c465c0a16228ef6fe1bf29c04fdb04bb797fd537' },
],
data: '0x000000000000000000000000000000000000000000000000019faae14eb88000',
},
{
address: '0x73968b9a57c6e53d41345fd57a6e6ae27d6cdb2f',
topics: [
{ hex: '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' },
{ hex: '0x000000000000000000000000c465c0a16228ef6fe1bf29c04fdb04bb797fd537' },
{ hex: '0x0000000000000000000000008453d9385af5f49edad9905345cd2411b5c5831b' },
],
data: '0x000000000000000000000000000000000000000000000013b6ee62022c95ced4',
},
];
import type { Transaction } from 'types/api/transaction';
import { tx } from './tx'; import { tx } from './tx';
import type { TxType, TxStatus } from './tx'; import type { TxType } from './tx';
export const txs = [ export const txs = [
{ {
...@@ -10,7 +12,7 @@ export const txs = [ ...@@ -10,7 +12,7 @@ export const txs = [
}, },
{ {
...tx, ...tx,
status: 'failed' as TxStatus, status: 'error' as Transaction['status'],
errorText: 'Error: (Awaiting internal transactions for reason)', errorText: 'Error: (Awaiting internal transactions for reason)',
txType: 'contract-call' as TxType, txType: 'contract-call' as TxType,
method: 'CommitHash CommitHash CommitHash CommitHash', method: 'CommitHash CommitHash CommitHash CommitHash',
...@@ -25,7 +27,7 @@ export const txs = [ ...@@ -25,7 +27,7 @@ export const txs = [
}, },
{ {
...tx, ...tx,
status: 'pending' as TxStatus, status: null,
txType: 'token-transfer' as TxType, txType: 'token-transfer' as TxType,
method: 'Multicall', method: 'Multicall',
address_from: { address_from: {
......
declare module 'react-identicons' declare module 'react-identicons'
declare module 'data/marketplaceApps.json' {
import type { AppItemOverview } from './types/client/apps';
const value: Array<AppItemOverview>;
export default value;
}
export const WEI = BigInt(10 ** 18);
export const GWEI = BigInt(10 ** 9);
...@@ -2,6 +2,8 @@ import appConfig from 'configs/app/config'; ...@@ -2,6 +2,8 @@ import appConfig from 'configs/app/config';
import featuredNetworks from 'lib/networks/featuredNetworks'; import featuredNetworks from 'lib/networks/featuredNetworks';
import getMarketplaceApps from '../getMarketplaceApps';
const KEY_WORDS = { const KEY_WORDS = {
BLOB: 'blob:', BLOB: 'blob:',
DATA: 'data:', DATA: 'data:',
...@@ -27,6 +29,14 @@ function getNetworksExternalAssets() { ...@@ -27,6 +29,14 @@ function getNetworksExternalAssets() {
return logo ? icons.concat(logo) : icons; return logo ? icons.concat(logo) : icons;
} }
function getMarketplaceAppsOrigins() {
return getMarketplaceApps().map(({ url }) => url);
}
function getMarketplaceAppsLogosOrigins() {
return getMarketplaceApps().map(({ logo }) => logo);
}
function makePolicyMap() { function makePolicyMap() {
const networkExternalAssets = getNetworksExternalAssets(); const networkExternalAssets = getNetworksExternalAssets();
...@@ -84,6 +94,9 @@ function makePolicyMap() { ...@@ -84,6 +94,9 @@ function makePolicyMap() {
// network assets // network assets
...networkExternalAssets.map((url) => url.host), ...networkExternalAssets.map((url) => url.host),
// marketplace apps logos
...getMarketplaceAppsLogosOrigins(),
], ],
'font-src': [ 'font-src': [
...@@ -102,12 +115,13 @@ function makePolicyMap() { ...@@ -102,12 +115,13 @@ function makePolicyMap() {
KEY_WORDS.NONE, KEY_WORDS.NONE,
], ],
'frame-src': getMarketplaceAppsOrigins(),
...(REPORT_URI ? { ...(REPORT_URI ? {
'report-uri': [ 'report-uri': [
REPORT_URI, REPORT_URI,
], ],
} : {}), } : {}),
}; };
} }
......
...@@ -36,37 +36,16 @@ dayjs.updateLocale('en', { ...@@ -36,37 +36,16 @@ dayjs.updateLocale('en', {
ss: '%d secs', ss: '%d secs',
future: 'in %s', future: 'in %s',
past: '%s ago', past: '%s ago',
m: 'a min', m: '1 min',
mm: '%d mins', mm: '%d mins',
h: 'an hour', h: '1 h',
hh: '%d hours', hh: '%d h',
d: 'a day', d: '1 d',
dd: '%d days', dd: '%d d',
M: 'a month', M: '1 mo',
MM: '%d months', MM: '%d mo',
y: 'a year', y: '1 y',
yy: '%d years', yy: '%d y',
},
});
dayjs.locale('en-short', {
name: 'en-short',
relativeTime: {
s: '1s',
future: 'in %s',
past: '%s ago',
m: '1m',
mm: '%dm',
h: '1h',
hh: '%dh',
d: '1d',
dd: '%dd',
M: '1mo',
MM: '%dmo',
y: '1y',
yy: '%dy',
// have to trick typescript 🎩 🐇
...{ ss: '%ds' },
}, },
}); });
......
import data from 'data/marketplaceApps.json';
export default function getMarketplaceApps() {
return data;
}
import React from 'react'; import appConfig from 'configs/app/config';
import React, { useMemo } from 'react';
import marketplaceApps from 'data/marketplaceApps.json';
import abiIcon from 'icons/ABI.svg'; import abiIcon from 'icons/ABI.svg';
import apiKeysIcon from 'icons/API.svg'; import apiKeysIcon from 'icons/API.svg';
import appsIcon from 'icons/apps.svg'; import appsIcon from 'icons/apps.svg';
...@@ -13,8 +15,14 @@ import transactionsIcon from 'icons/transactions.svg'; ...@@ -13,8 +15,14 @@ import transactionsIcon from 'icons/transactions.svg';
import watchlistIcon from 'icons/watchlist.svg'; import watchlistIcon from 'icons/watchlist.svg';
import useCurrentRoute from 'lib/link/useCurrentRoute'; import useCurrentRoute from 'lib/link/useCurrentRoute';
import useLink from 'lib/link/useLink'; import useLink from 'lib/link/useLink';
import notEmpty from 'lib/notEmpty';
export default function useNavItems() { export default function useNavItems() {
const isMarketplaceFilled = useMemo(() =>
marketplaceApps.filter(item => item.chainIds.includes(appConfig.network.id)),
[ ])
.length > 0;
const link = useLink(); const link = useLink();
const currentRoute = useCurrentRoute()(); const currentRoute = useCurrentRoute()();
...@@ -23,12 +31,13 @@ export default function useNavItems() { ...@@ -23,12 +31,13 @@ export default function useNavItems() {
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block') }, { text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block') },
{ text: 'Transactions', url: link('txs_validated'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx') }, { text: 'Transactions', url: link('txs_validated'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx') },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens' }, { text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens' },
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps' }, isMarketplaceFilled ?
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps' } : null,
// there should be custom site sections like Stats, Faucet, More, etc but never an '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/ // 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 // at this stage custom menu items is under development, we will implement it later
// { text: 'Other', url: link('other'), icon: gearIcon, isActive: currentRoute === 'other' }, // { text: 'Other', url: link('other'), icon: gearIcon, isActive: currentRoute === 'other' },
]; ].filter(notEmpty);
const accountNavItems = [ const accountNavItems = [
{ text: 'Watchlist', url: link('watchlist'), icon: watchlistIcon, isActive: currentRoute === 'watchlist' }, { text: 'Watchlist', url: link('watchlist'), icon: watchlistIcon, isActive: currentRoute === 'watchlist' },
...@@ -41,5 +50,5 @@ export default function useNavItems() { ...@@ -41,5 +50,5 @@ export default function useNavItems() {
const profileItem = { text: 'My profile', url: link('profile'), icon: profileIcon, isActive: currentRoute === 'profile' }; const profileItem = { text: 'My profile', url: link('profile'), icon: profileIcon, isActive: currentRoute === 'profile' };
return { mainNavItems, accountNavItems, profileItem }; return { mainNavItems, accountNavItems, profileItem };
}, [ link, currentRoute ]); }, [ isMarketplaceFilled, link, currentRoute ]);
} }
...@@ -104,6 +104,9 @@ export const ROUTES = { ...@@ -104,6 +104,9 @@ export const ROUTES = {
apps: { apps: {
pattern: `${ BASE_PATH }/apps`, pattern: `${ BASE_PATH }/apps`,
}, },
app_index: {
pattern: `${ BASE_PATH }/apps/[id]`,
},
// SEARCH // SEARCH
search_results: { search_results: {
......
export default function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
return value !== null && value !== undefined;
}
export default function getConfirmationString(durations: Array<number>) {
if (durations.length === 0) {
return '';
}
const [ lower, upper ] = durations.map((time) => time / 1_000);
if (!upper) {
return `Confirmed within ${ lower } secs`;
}
if (lower === 0) {
return `Confirmed within <= ${ upper } secs`;
}
return `Confirmed within ${ lower } - ${ upper } secs`;
}
import type { NextPage } from 'next'; import type { NextPage } from 'next';
import Head from 'next/head'; import Head from 'next/head';
import React from 'react'; import { useRouter } from 'next/router';
import React, { useEffect, useState } from 'react';
import App from 'ui/pages/App'; import type { AppItemOverview } from 'types/client/apps';
import marketplaceApps from 'data/marketplaceApps.json';
import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import MarketplaceApp from 'ui/pages/MarketplaceApp';
import Page from 'ui/shared/Page/Page';
const AppPage: NextPage = () => { const AppPage: NextPage = () => {
const router = useRouter();
const [ isLoading, setIsLoading ] = useState(true);
const [ app, setApp ] = useState<AppItemOverview | undefined>(undefined);
const { id }: { id?: string } = router.query;
useEffect(() => {
if (!id) {
return;
}
const app = marketplaceApps.find((app) => app.id === id);
setApp(app);
setIsLoading(false);
}, [ id ]);
if (app || isLoading) {
return ( return (
<> <>
<Head><title>App Card Page</title></Head> <Head><title>{ app ? `Blockscout | ${ app.title }` : 'Loading app..' }</title></Head>
<App/> <MarketplaceApp app={ app } isLoading={ isLoading }/>
</> </>
); );
}
return (
<Page>
<Head><title>Blockscout | No app found</title></Head>
<EmptySearchResult text={ `Couldn${ apos }t find an app.` }/>
</Page>
);
}; };
export default AppPage; export default AppPage;
......
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/transactions/${ req.query.id }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/transactions/${ req.query.id }/internal-transactions`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/transactions/${ req.query.id }/logs`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
return `/v2/transactions/${ req.query.id }/raw-trace`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import { formAnatomy as parts } from '@chakra-ui/anatomy'; import { formAnatomy as parts } from '@chakra-ui/anatomy';
import { import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
createMultiStyleConfigHelpers, import type { StyleFunctionProps } from '@chakra-ui/theme-tools';
} from '@chakra-ui/styled-system';
import { getColor, mode } from '@chakra-ui/theme-tools'; import { getColor, mode } from '@chakra-ui/theme-tools';
import type { Dict } from '@chakra-ui/utils';
import getDefaultFormColors from '../utils/getDefaultFormColors'; import getDefaultFormColors from '../utils/getDefaultFormColors';
import FormLabel from './FormLabel';
import Input from './Input';
import Textarea from './Textarea';
const { definePartsStyle, defineMultiStyleConfig } = const { definePartsStyle, defineMultiStyleConfig } =
createMultiStyleConfigHelpers(parts.keys); createMultiStyleConfigHelpers(parts.keys);
const getActiveLabelStyles = (theme: Dict, fc: string, bc: string, size: 'md' | 'lg') => { function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunctionProps) {
const baseStyles = { const { theme } = props;
backgroundColor: bc, const { focusColor: fc, errorColor: ec } = getDefaultFormColors(props);
color: getColor(theme, fc),
fontSize: 'xs',
lineHeight: '16px',
borderTopRightRadius: 'none',
};
switch (size) {
case 'md': {
return {
...baseStyles,
padding: '10px 16px 2px 16px',
};
}
case 'lg': { const activeLabelStyles = {
return { ...FormLabel.variants?.floating?.(props)._focusWithin,
...baseStyles, ...FormLabel.sizes?.[size](props)._focusWithin,
padding: '16px 24px 2px 24px', } || {};
};
}
}
};
const getDefaultLabelStyles = (size: 'md' | 'lg') => { const activeInputStyles = (() => {
switch (size) { switch (size) {
case 'md': { case 'md': {
return { return {
fontSize: 'md', paddingTop: '26px',
lineHeight: '20px', paddingBottom: '10px',
padding: '18px 16px',
right: '18px',
}; };
} }
case 'lg': { case 'lg': {
return { return {
fontSize: 'md', paddingTop: '38px',
lineHeight: '24px', paddingBottom: '18px',
padding: '28px 24px',
right: '26px',
}; };
} }
} }
}; })();
const getPaddingX = (size: 'md' | 'lg') => { const inputPx = (() => {
switch (size) { switch (size) {
case 'md': { case 'md': {
return '16px'; return '16px';
...@@ -68,84 +48,35 @@ const getPaddingX = (size: 'md' | 'lg') => { ...@@ -68,84 +48,35 @@ const getPaddingX = (size: 'md' | 'lg') => {
return '24px'; return '24px';
} }
} }
}; })();
const getActiveInputStyles = (size: 'md' | 'lg') => {
switch (size) {
case 'md': {
return {
paddingTop: '26px',
paddingBottom: '10px',
};
}
case 'lg': {
return {
paddingTop: '38px',
paddingBottom: '18px',
};
}
}
};
const variantFloating = definePartsStyle((props) => {
const { theme, backgroundColor, size = 'md' } = props;
const { focusColor: fc, errorColor: ec } = getDefaultFormColors(props);
const bc = backgroundColor || mode('white', 'black')(props);
const px = getPaddingX(size);
const activeInputStyles = getActiveInputStyles(size);
const activeLabelStyles = getActiveLabelStyles(theme, fc, bc, size);
return { return {
container: { container: {
// active styles
_focusWithin: { _focusWithin: {
label: { label: activeLabelStyles,
...activeLabelStyles, 'input, textarea': activeInputStyles,
},
'input, textarea': {
...activeInputStyles,
},
'label .chakra-form__required-indicator': {
color: getColor(theme, fc),
},
},
// label's styles
label: {
...getDefaultLabelStyles(size),
left: '2px',
top: '2px',
zIndex: 2,
position: 'absolute',
borderRadius: 'base',
boxSizing: 'border-box',
color: 'gray.500',
backgroundColor: 'transparent',
pointerEvents: 'none',
margin: 0,
transformOrigin: 'top left',
transitionProperty: 'font-size, line-height, padding, top, background-color',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
},
'input:not(:placeholder-shown) + label, textarea:not(:placeholder-shown) + label': {
...activeLabelStyles,
}, },
// label styles
label: FormLabel.sizes?.[size](props) || {},
'input:not(:placeholder-shown) + label, textarea:not(:placeholder-shown) + label': activeLabelStyles,
'input[aria-invalid=true] + label, textarea[aria-invalid=true] + label': { 'input[aria-invalid=true] + label, textarea[aria-invalid=true] + label': {
color: getColor(theme, ec), color: getColor(theme, ec),
}, },
// input's styles
// input styles
input: Input.sizes?.[size].field,
textarea: Textarea.sizes?.[size],
'input, textarea': { 'input, textarea': {
padding: px, padding: inputPx,
}, },
'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': activeInputStyles,
'input[disabled] + label, textarea[disabled] + label': { 'input[disabled] + label, textarea[disabled] + label': {
backgroundColor: 'transparent', backgroundColor: 'transparent',
}, },
'input:not(:placeholder-shown), textarea:not(:placeholder-shown)': {
...activeInputStyles, // indicator styles
},
// indicator's styles
'input:not(:placeholder-shown) + label .chakra-form__required-indicator, textarea:not(:placeholder-shown) + label .chakra-form__required-indicator': { 'input:not(:placeholder-shown) + label .chakra-form__required-indicator, textarea:not(:placeholder-shown) + label .chakra-form__required-indicator': {
color: getColor(theme, fc), color: getColor(theme, fc),
}, },
...@@ -153,6 +84,11 @@ const variantFloating = definePartsStyle((props) => { ...@@ -153,6 +84,11 @@ const variantFloating = definePartsStyle((props) => {
color: getColor(theme, ec), color: getColor(theme, ec),
}, },
}, },
};
}
const baseStyle = definePartsStyle((props) => {
return {
requiredIndicator: { requiredIndicator: {
marginStart: 0, marginStart: 0,
color: mode('gray.500', 'whiteAlpha.400')(props), color: mode('gray.500', 'whiteAlpha.400')(props),
...@@ -160,12 +96,42 @@ const variantFloating = definePartsStyle((props) => { ...@@ -160,12 +96,42 @@ const variantFloating = definePartsStyle((props) => {
}; };
}); });
const variantFloating = definePartsStyle((props) => {
return {
container: {
label: FormLabel.variants?.floating(props) || {},
},
};
});
const sizes = {
lg: definePartsStyle((props) => {
if (props.variant === 'floating') {
return getFloatingVariantStylesForSize('lg', props);
}
return {};
}),
md: definePartsStyle((props) => {
if (props.variant === 'floating') {
return getFloatingVariantStylesForSize('md', props);
}
return {};
}),
};
const variants = { const variants = {
floating: variantFloating, floating: variantFloating,
}; };
const Form = defineMultiStyleConfig({ const Form = defineMultiStyleConfig({
baseStyle,
variants, variants,
sizes,
defaultProps: {
size: 'md',
},
}); });
export default Form; export default Form;
import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system';
import { getColor, mode } from '@chakra-ui/theme-tools';
import getDefaultFormColors from '../utils/getDefaultFormColors';
const baseStyle = defineStyle({
fontSize: 'md',
marginEnd: '3',
mb: '2',
fontWeight: 'medium',
transitionProperty: 'common',
transitionDuration: 'normal',
opacity: 1,
_disabled: {
opacity: 0.4,
},
});
const variantFloating = defineStyle((props) => {
const { theme, backgroundColor } = props;
const { focusColor: fc } = getDefaultFormColors(props);
const bc = backgroundColor || mode('white', 'black')(props);
return {
left: '2px',
top: '2px',
zIndex: 2,
position: 'absolute',
borderRadius: 'base',
boxSizing: 'border-box',
color: 'gray.500',
backgroundColor: 'transparent',
pointerEvents: 'none',
margin: 0,
transformOrigin: 'top left',
transitionProperty: 'font-size, line-height, padding, top, background-color',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
_focusWithin: {
backgroundColor: bc,
color: getColor(theme, fc),
fontSize: 'xs',
lineHeight: '16px',
borderTopRightRadius: 'none',
},
};
});
const variants = {
floating: variantFloating,
};
const sizes = {
lg: defineStyle((props) => {
if (props.variant === 'floating') {
return {
fontSize: 'md',
lineHeight: '24px',
padding: '28px 24px',
right: '26px',
_focusWithin: {
padding: '16px 24px 2px 24px',
},
};
}
return {};
}),
md: defineStyle((props) => {
if (props.variant === 'floating') {
return {
fontSize: 'md',
lineHeight: '20px',
padding: '18px 16px',
right: '18px',
_focusWithin: {
padding: '10px 16px 2px 16px',
},
};
}
return {};
}),
};
const FormLabel = defineStyleConfig({
variants,
baseStyle,
sizes,
});
export default FormLabel;
...@@ -4,6 +4,7 @@ import Button from './Button'; ...@@ -4,6 +4,7 @@ import Button from './Button';
import Checkbox from './Checkbox'; import Checkbox from './Checkbox';
import Drawer from './Drawer'; import Drawer from './Drawer';
import Form from './Form'; import Form from './Form';
import FormLabel from './FormLabel';
import Heading from './Heading'; import Heading from './Heading';
import Input from './Input'; import Input from './Input';
import Link from './Link'; import Link from './Link';
...@@ -27,6 +28,7 @@ const components = { ...@@ -27,6 +28,7 @@ const components = {
Heading, Heading,
Input, Input,
Form, Form,
FormLabel,
Link, Link,
Modal, Modal,
Popover, Popover,
......
export interface AddressParam {
hash: string;
implementation_name: string;
name: string;
is_contract: boolean;
}
import type { AddressParam } from 'types/api/addressParams';
import type { Reward } from 'types/api/reward';
export interface Block {
height: number;
timestamp: string;
tx_count: number;
miner: AddressParam;
size: number;
hash: string;
parent_hash: string;
difficulty: number;
total_difficulty: number;
gas_used: number;
gas_limit: number;
nonce: number;
base_fee_per_gas: number | null;
burnt_fees: number | null;
priority_fee: number | null;
extra_data: string | null;
state_root: string | null;
rewards?: Array<Reward>;
gas_target_percentage: number | null;
gas_used_percentage: number | null;
burnt_fees_percentage: number | null;
type: 'block' | 'reorg' | 'uncle';
tx_fees: string | null;
uncles_hashes: Array<string>;
}
export interface BlockResponse {
items: Array<Block>;
next_page_params: {
block_number: number;
items_count: number;
};
}
export interface DecodedInput {
method_call: string;
method_id: string;
parameters: Array<DecodedInputParams>;
}
export interface DecodedInputParams {
name: string;
type: string;
value: string;
indexed?: boolean;
}
export interface Fee {
type: string;
value: string;
}
import type { AddressParam } from './addressParams';
export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward'
export interface InternalTransaction {
error: string | null;
success: boolean;
type: TxInternalsType;
transaction_hash: string;
from: AddressParam;
to: AddressParam;
created_contract: AddressParam;
value: number;
index: number;
block: number;
timestamp: string;
}
export interface InternalTransactionsResponse {
items: Array<InternalTransaction>;
next_page_params: {
block_number: number;
index: number;
items_count: number;
transaction_hash: string;
transaction_index: number;
};
}
import type { AddressParam } from './addressParams';
import type { DecodedInput } from './decodedInput';
export interface Log {
address: AddressParam;
topics: Array<string>;
data: string;
index: number;
decoded: DecodedInput | null;
}
export interface LogsResponse {
items: Array<Log>;
next_page_params: {
index: number;
items_count: number;
transaction_hash: string;
};
}
export interface RawTrace {
action: {
callType: string;
from: string;
gas: string;
input: string;
to: string;
value: string;
};
result: {
gasUsed: string;
output: string;
};
error: string | null;
subtraces: number;
traceAddress: Array<number>;
type: string;
}
export type RawTracesResponse = Array<RawTrace>;
export interface Reward {
reward: number;
type: 'Miner Reward' | 'Emission Reward' | 'Chore Reward' | 'Uncle Reward';
}
import type { AddressParam } from './addressParams';
export interface TokenTransfer {
type: string;
txHash: string;
from: AddressParam;
to: AddressParam;
token_address: string;
token_symbol: string;
token_type: string;
total: {
value: string;
};
exchange_rate: string;
}
...@@ -6,7 +6,7 @@ export type Tokenlist = { ...@@ -6,7 +6,7 @@ export type Tokenlist = {
export type TokenlistItem = { export type TokenlistItem = {
balance: number; balance: number;
contractAddress: string; contractAddress: string;
decimals?: number; decimals: number | null;
id: number; id: number;
name: string; name: string;
symbol: string; symbol: string;
......
import type { AddressParam } from './addressParams';
import type { DecodedInput } from './decodedInput';
import type { Fee } from './fee';
import type { TokenTransfer } from './tokenTransfer';
export interface Transaction {
hash: string;
result: string;
confirmations: number;
status: 'ok' | 'error' | null;
block: number;
timestamp: string;
confirmation_duration: Array<number>;
from: AddressParam;
to: AddressParam;
created_contract: AddressParam;
value: number;
fee: Fee;
gas_price: number;
type: number;
gas_used: string;
gas_limit: string;
max_fee_per_gas: number | null;
max_priority_fee_per_gas: number | null;
priority_fee: number | null;
base_fee_per_gas: number | null;
tx_burnt_fee: number | null;
nonce: number;
position: number;
revert_reason: {
raw: string;
decoded: string;
} | null;
raw_input: string;
decoded_input: DecodedInput | null;
token_transfers: Array<TokenTransfer> | null;
token_transfers_overflow: boolean;
exchange_rate: string;
}
export interface TransactionsResponse {
items: Array<Transaction>;
next_page_params: {
block_number: number;
index: number;
items_count: number;
};
}
export type TxInternalsType = 'call' | 'delegate_call' | 'static_call' | 'create' | 'create2' | 'self_destruct' | 'reward'
export type AppCategory = { export enum MarketplaceCategoryId {
id: string; 'all',
name: string; 'favorites',
'defi',
'exchanges',
'finance',
'games',
'marketplaces',
'nft',
'security',
'social',
'tools',
'yieldFarming',
} }
export type MarketplaceCategoriesIds = keyof typeof MarketplaceCategoryId;
export type MarketplaceCategory = { id: MarketplaceCategoriesIds; name: string }
export type AppItemPreview = { export type AppItemPreview = {
id: string; id: string;
title: string; title: string;
logo: string; logo: string;
shortDescription: string; shortDescription: string;
categories: Array<AppCategory>; categories: Array<MarketplaceCategoriesIds>;
} }
export type AppItemOverview = AppItemPreview & { export type AppItemOverview = AppItemPreview & {
chainIds: Array<number>;
author: string; author: string;
url: string; url: string;
description: string; description: string;
......
import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, LinkOverlay, Text, useColorModeValue } from '@chakra-ui/react'; import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, LinkOverlay, Text, useColorModeValue } from '@chakra-ui/react';
import NextLink from 'next/link';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
...@@ -7,6 +8,10 @@ import type { AppItemPreview } from 'types/client/apps'; ...@@ -7,6 +8,10 @@ import type { AppItemPreview } from 'types/client/apps';
import northEastIcon from 'icons/arrows/north-east.svg'; import northEastIcon from 'icons/arrows/north-east.svg';
import starFilledIcon from 'icons/star_filled.svg'; import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg'; import starOutlineIcon from 'icons/star_outline.svg';
import useLink from 'lib/link/useLink';
import notEmpty from 'lib/notEmpty';
import { APP_CATEGORIES } from './constants';
interface Props extends AppItemPreview { interface Props extends AppItemPreview {
onInfoClick: (id: string) => void; onInfoClick: (id: string) => void;
...@@ -23,8 +28,7 @@ const AppCard = ({ id, ...@@ -23,8 +28,7 @@ const AppCard = ({ id,
isFavorite, isFavorite,
onFavoriteClick, onFavoriteClick,
}: Props) => { }: Props) => {
const categoriesLabel = categories.map(c => APP_CATEGORIES[c]).filter(notEmpty).join(', ');
const categoriesLabel = categories.map(c => c.name).join(', ');
const handleInfoClick = useCallback((event: MouseEvent) => { const handleInfoClick = useCallback((event: MouseEvent) => {
event.preventDefault(); event.preventDefault();
...@@ -35,6 +39,8 @@ const AppCard = ({ id, ...@@ -35,6 +39,8 @@ const AppCard = ({ id,
onFavoriteClick(id, isFavorite); onFavoriteClick(id, isFavorite);
}, [ onFavoriteClick, id, isFavorite ]); }, [ onFavoriteClick, id, isFavorite ]);
const link = useLink();
return ( return (
<LinkBox <LinkBox
_hover={{ _hover={{
...@@ -48,6 +54,7 @@ const AppCard = ({ id, ...@@ -48,6 +54,7 @@ const AppCard = ({ id,
padding={{ base: 3, sm: '20px' }} padding={{ base: 3, sm: '20px' }}
border="1px" border="1px"
borderColor={ useColorModeValue('gray.200', 'gray.600') } borderColor={ useColorModeValue('gray.200', 'gray.600') }
role="group"
> >
<Box <Box
display={{ base: 'grid', sm: 'block' }} display={{ base: 'grid', sm: 'block' }}
...@@ -64,6 +71,7 @@ const AppCard = ({ id, ...@@ -64,6 +71,7 @@ const AppCard = ({ id,
h={{ base: '64px', sm: '96px' }} h={{ base: '64px', sm: '96px' }}
> >
<Image <Image
borderRadius={ 8 }
src={ logo } src={ logo }
alt={ `${ title } app icon` } alt={ `${ title } app icon` }
/> />
...@@ -76,11 +84,11 @@ const AppCard = ({ id, ...@@ -76,11 +84,11 @@ const AppCard = ({ id,
fontSize={{ base: 'sm', sm: 'lg' }} fontSize={{ base: 'sm', sm: 'lg' }}
fontWeight="semibold" fontWeight="semibold"
> >
<LinkOverlay <NextLink href={ link('app_index', { id: id }) } passHref>
href="#" <LinkOverlay>
>
{ title } { title }
</LinkOverlay> </LinkOverlay>
</NextLink>
</Heading> </Heading>
<Text <Text
...@@ -103,9 +111,8 @@ const AppCard = ({ id, ...@@ -103,9 +111,8 @@ const AppCard = ({ id,
position="absolute" position="absolute"
right={{ base: 3, sm: '20px' }} right={{ base: 3, sm: '20px' }}
bottom={{ base: 3, sm: '20px' }} bottom={{ base: 3, sm: '20px' }}
paddingTop={ 1 }
paddingLeft={ 8 } paddingLeft={ 8 }
bgGradient={ `linear(to-r, transparent, ${ useColorModeValue('white', 'black') } 20%)` } bgGradient={ `linear(to-r, ${ useColorModeValue('whiteAlpha.50', 'blackAlpha.50') }, ${ useColorModeValue('white', 'black') } 20%)` }
> >
<Link <Link
fontSize={{ base: 'xs', sm: 'sm' }} fontSize={{ base: 'xs', sm: 'sm' }}
...@@ -127,9 +134,11 @@ const AppCard = ({ id, ...@@ -127,9 +134,11 @@ const AppCard = ({ id,
</Box> </Box>
<IconButton <IconButton
display={{ base: 'block', sm: isFavorite ? 'block' : 'none' }}
_groupHover={{ display: 'block' }}
position="absolute" position="absolute"
right={{ base: 3, sm: '20px' }} right={{ base: 3, sm: '10px' }}
top={{ base: 3, sm: '20px' }} top={{ base: 3, sm: '14px' }}
aria-label="Mark as favorite" aria-label="Mark as favorite"
title="Mark as favorite" title="Mark as favorite"
variant="ghost" variant="ghost"
......
import { Box, Heading, Skeleton, SkeletonCircle, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
export const AppCardSkeleton = () => {
return (
<Box
borderRadius="md"
height="100%"
padding={{ base: 3, sm: '20px' }}
border="1px"
borderColor={ useColorModeValue('gray.200', 'gray.600') }
>
<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' }}
>
<SkeletonCircle w="100%" h="100%"/>
</Box>
<Heading
gridColumn={{ base: 2, sm: 'auto' }}
marginBottom={ 2 }
>
<Skeleton h={ 4 } w="50%"/>
</Heading>
<Box>
<Skeleton h={ 4 } mb={ 1 }/>
<Skeleton h={ 4 } mb={ 1 }/>
<Skeleton h={ 4 } w="50%"/>
</Box>
</Box>
</Box>
);
};
import { Grid, GridItem, VisuallyHidden, Heading } from '@chakra-ui/react'; import { Grid, GridItem, Heading, VisuallyHidden } from '@chakra-ui/react';
import React, { useCallback, useEffect, useState } from 'react'; import React from 'react';
import type { AppItemPreview } from 'types/client/apps'; import type { AppItemPreview } from 'types/client/apps';
...@@ -14,37 +14,11 @@ type Props = { ...@@ -14,37 +14,11 @@ type Props = {
onAppClick: (id: string) => void; onAppClick: (id: string) => void;
displayedAppId: string | null; displayedAppId: string | null;
onModalClose: () => void; onModalClose: () => void;
favoriteApps: Array<string>;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
} }
function getFavoriteApps() { const AppList = ({ apps, onAppClick, displayedAppId, onModalClose, favoriteApps, onFavoriteClick }: Props) => {
try {
return JSON.parse(localStorage.getItem('favoriteApps') || '[]');
} catch (e) {
return [];
}
}
const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => {
const [ favoriteApps, setFavoriteApps ] = useState<Array<string>>([]);
const handleFavoriteClick = useCallback((id: string, isFavorite: boolean) => {
const favoriteApps = getFavoriteApps();
if (isFavorite) {
const result = favoriteApps.filter((appId: string) => appId !== id);
setFavoriteApps(result);
localStorage.setItem('favoriteApps', JSON.stringify(result));
} else {
favoriteApps.push(id);
localStorage.setItem('favoriteApps', JSON.stringify(favoriteApps));
setFavoriteApps(favoriteApps);
}
}, [ ]);
useEffect(() => {
setFavoriteApps(getFavoriteApps());
}, [ ]);
return ( return (
<> <>
<VisuallyHidden> <VisuallyHidden>
...@@ -54,7 +28,7 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => { ...@@ -54,7 +28,7 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => {
{ apps.length > 0 ? ( { apps.length > 0 ? (
<Grid <Grid
templateColumns={{ templateColumns={{
sm: 'repeat(auto-fill, minmax(170px, 1fr))', sm: 'repeat(auto-fill, minmax(178px, 1fr))',
lg: 'repeat(auto-fill, minmax(260px, 1fr))', lg: 'repeat(auto-fill, minmax(260px, 1fr))',
}} }}
autoRows="1fr" autoRows="1fr"
...@@ -72,7 +46,7 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => { ...@@ -72,7 +46,7 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => {
shortDescription={ app.shortDescription } shortDescription={ app.shortDescription }
categories={ app.categories } categories={ app.categories }
isFavorite={ favoriteApps.includes(app.id) } isFavorite={ favoriteApps.includes(app.id) }
onFavoriteClick={ handleFavoriteClick } onFavoriteClick={ onFavoriteClick }
/> />
</GridItem> </GridItem>
)) } )) }
...@@ -86,7 +60,7 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => { ...@@ -86,7 +60,7 @@ const AppList = ({ apps, onAppClick, displayedAppId, onModalClose }: Props) => {
id={ displayedAppId } id={ displayedAppId }
onClose={ onModalClose } onClose={ onModalClose }
isFavorite={ favoriteApps.includes(displayedAppId) } isFavorite={ favoriteApps.includes(displayedAppId) }
onFavoriteClick={ handleFavoriteClick } onFavoriteClick={ onFavoriteClick }
/> />
) } ) }
</> </>
......
import { Grid, GridItem } from '@chakra-ui/react';
import React from 'react';
import { AppCardSkeleton } from 'ui/apps/AppCardSkeleton';
const applicationStubs = [ ...Array(12) ];
const AppListSkeleton = () => {
return (
<Grid
templateColumns={{
sm: 'repeat(auto-fill, minmax(170px, 1fr))',
lg: 'repeat(auto-fill, minmax(260px, 1fr))',
}}
autoRows="1fr"
gap={{ base: '16px', sm: '24px' }}
>
{ applicationStubs.map((app, index) => (
<GridItem
key={ index }
>
<AppCardSkeleton/>
</GridItem>
)) }
</Grid>
);
};
export default AppListSkeleton;
...@@ -2,12 +2,12 @@ import { ...@@ -2,12 +2,12 @@ import {
Box, Button, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody, Box, Button, Heading, Icon, IconButton, Image, Link, List, Modal, ModalBody,
ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Tag, Text,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import type { FunctionComponent } from 'react'; import NextLink from 'next/link';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { AppCategory, AppItemOverview } from 'types/client/apps'; import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
import { TEMPORARY_DEMO_APPS } from 'data/apps'; import marketplaceApps from 'data/marketplaceApps.json';
import linkIcon from 'icons/link.svg'; import linkIcon from 'icons/link.svg';
import ghIcon from 'icons/social/git.svg'; import ghIcon from 'icons/social/git.svg';
import tgIcon from 'icons/social/telega.svg'; import tgIcon from 'icons/social/telega.svg';
...@@ -15,6 +15,10 @@ import twIcon from 'icons/social/tweet.svg'; ...@@ -15,6 +15,10 @@ import twIcon from 'icons/social/tweet.svg';
import starFilledIcon from 'icons/star_filled.svg'; import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg'; import starOutlineIcon from 'icons/star_outline.svg';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import useLink from 'lib/link/useLink';
import notEmpty from 'lib/notEmpty';
import { APP_CATEGORIES } from './constants';
type Props = { type Props = {
id: string; id: string;
...@@ -33,29 +37,30 @@ const AppModal = ({ ...@@ -33,29 +37,30 @@ const AppModal = ({
title, title,
author, author,
description, description,
url,
site, site,
github, github,
telegram, telegram,
twitter, twitter,
logo, logo,
categories, categories,
} = TEMPORARY_DEMO_APPS.find(app => app.id === id) as AppItemOverview; } = marketplaceApps.find(app => app.id === id) as AppItemOverview;
const link = useLink();
const socialLinks = [ const socialLinks = [
Boolean(telegram) && { telegram ? {
icon: tgIcon, icon: tgIcon,
url: telegram, url: telegram,
}, } : null,
Boolean(twitter) && { twitter ? {
icon: twIcon, icon: twIcon,
url: twitter, url: twitter,
}, } : null,
Boolean(github) && { github ? {
icon: ghIcon, icon: ghIcon,
url: github, url: github,
}, } : null,
].filter(Boolean) as Array<{ icon: FunctionComponent; url: string }>; ].filter(notEmpty);
const handleFavoriteClick = useCallback(() => { const handleFavoriteClick = useCallback(() => {
onFavoriteClick(id, isFavorite); onFavoriteClick(id, isFavorite);
...@@ -95,9 +100,6 @@ const AppModal = ({ ...@@ -95,9 +100,6 @@ const AppModal = ({
fontWeight="medium" fontWeight="medium"
lineHeight={ 1 } lineHeight={ 1 }
color="blue.600" color="blue.600"
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
> >
{ title } { title }
</Heading> </Heading>
...@@ -117,8 +119,8 @@ const AppModal = ({ ...@@ -117,8 +119,8 @@ const AppModal = ({
marginTop={{ base: 6, sm: 0 }} marginTop={{ base: 6, sm: 0 }}
> >
<Box display="flex"> <Box display="flex">
<NextLink href={ link('app_index', { id: id }) } passHref>
<Button <Button
href={ url }
as="a" as="a"
size="sm" size="sm"
marginRight={ 2 } marginRight={ 2 }
...@@ -126,6 +128,7 @@ const AppModal = ({ ...@@ -126,6 +128,7 @@ const AppModal = ({
> >
Launch app Launch app
</Button> </Button>
</NextLink>
<IconButton <IconButton
aria-label="Mark as favorite" aria-label="Mark as favorite"
...@@ -155,14 +158,14 @@ const AppModal = ({ ...@@ -155,14 +158,14 @@ const AppModal = ({
</Heading> </Heading>
<Box marginBottom={ 2 }> <Box marginBottom={ 2 }>
{ categories.map((category: AppCategory) => ( { categories.map((category: MarketplaceCategoriesIds) => APP_CATEGORIES[category] && (
<Tag <Tag
colorScheme="blue" colorScheme="blue"
marginRight={ 2 } marginRight={ 2 }
marginBottom={ 2 } marginBottom={ 2 }
key={ category.id } key={ category }
> >
{ category.name } { APP_CATEGORIES[category] }
</Tag> </Tag>
)) } )) }
</Box> </Box>
......
import { Box, Button, Icon, Menu, MenuButton, MenuList } from '@chakra-ui/react';
import React from 'react';
import type { MarketplaceCategoriesIds, MarketplaceCategory } from 'types/client/apps';
import eastMiniArrowIcon from 'icons/arrows/east-mini.svg';
import CategoriesMenuItem from './CategoriesMenuItem';
import { APP_CATEGORIES } from './constants';
const categoriesList = Object.keys(APP_CATEGORIES).map((id: string) => ({
id: id,
name: APP_CATEGORIES[id as MarketplaceCategoriesIds],
})) as Array<MarketplaceCategory>;
type Props = {
selectedCategoryId: MarketplaceCategoriesIds;
onSelect: (category: MarketplaceCategoriesIds) => void;
}
const CategoriesMenu = ({ selectedCategoryId, onSelect }: Props) => {
const selectedCategory = categoriesList.find(category => category.id === selectedCategoryId);
return (
<Menu>
<MenuButton
as={ Button }
mb={{ base: 2, sm: 0 }}
mr={{ base: 0, sm: 2 }}
size="md"
variant="outline"
colorScheme="gray"
flexShrink={ 0 }
>
<Box
as="span"
display="flex"
alignItems="center"
>
{ selectedCategory?.name }
<Icon transform="rotate(-90deg)" ml={{ base: 'auto', sm: 1 }} as={ eastMiniArrowIcon } w={ 5 } h={ 5 }/>
</Box>
</MenuButton>
<MenuList zIndex={ 3 }>
{ categoriesList.map((category: MarketplaceCategory) => (
<CategoriesMenuItem
key={ category.id }
id={ category.id }
name={ category.name }
onClick={ onSelect }
/>
)) }
</MenuList>
</Menu>
);
};
export default React.memo(CategoriesMenu);
import { Icon, MenuItem } from '@chakra-ui/react';
import type { FunctionComponent, SVGAttributes } from 'react';
import React, { useCallback } from 'react';
import type { MarketplaceCategoriesIds } from 'types/client/apps';
import starFilledIcon from 'icons/star_filled.svg';
type Props = {
id: MarketplaceCategoriesIds;
name: string;
onClick: (category: MarketplaceCategoriesIds) => void;
}
const ICONS = {
favorites: starFilledIcon,
} as { [key in MarketplaceCategoriesIds]: FunctionComponent<SVGAttributes<SVGElement>> };
const CategoriesMenuItem = ({ id, name, onClick }: Props) => {
const handleSelection = useCallback(() => {
onClick(id);
}, [ id, onClick ]);
return (
<MenuItem
key={ id }
onClick={ handleSelection }
display="flex"
alignItems="center"
>
{ id in ICONS && (
<Icon mr={ 3 } as={ ICONS[id] } w={ 4 } h={ 4 } color="blackAlpha.800"/>
) }
{ name }
</MenuItem>
);
};
export default CategoriesMenuItem;
import type { AppCategory } from 'types/client/apps'; import type { MarketplaceCategoriesIds } from 'types/client/apps';
export const APP_CATEGORIES: Array<AppCategory> = [ export const APP_CATEGORIES: {[key in MarketplaceCategoriesIds]: string} = {
{ favorites: 'Favorites',
id: 'defi', all: 'All apps',
name: 'DeFi', defi: 'DeFi',
}, exchanges: 'Exchanges',
{ finance: 'Finance',
id: 'exchanges', games: 'Games',
name: 'Exchanges', marketplaces: 'Marketplaces',
}, nft: 'NFT',
{ security: 'Security',
id: 'finance', social: 'Social',
name: 'Finance', tools: 'Tools',
}, yieldFarming: 'Yield farming',
{ };
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 appConfig from 'configs/app/config';
import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useState } from 'react';
import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
import marketplaceApps from 'data/marketplaceApps.json';
const favoriteAppsLocalStorageKey = 'favoriteApps';
function getFavoriteApps() {
try {
return JSON.parse(localStorage.getItem(favoriteAppsLocalStorageKey) || '[]');
} catch (e) {
return [];
}
}
function isAppNameMatches(q: string, app: AppItemOverview) {
return app.title.toLowerCase().includes(q.toLowerCase());
}
function isAppCategoryMatches(category: MarketplaceCategoriesIds, app: AppItemOverview, favoriteApps: Array<string>) {
return category === 'all' ||
(category === 'favorites' && favoriteApps.includes(app.id)) ||
app.categories.includes(category);
}
export default function useMarketplaceApps() {
const [ isLoading, setIsLoading ] = useState(true);
const [ defaultAppList, setDefaultAppList ] = useState<Array<AppItemOverview>>();
const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>([]);
const [ displayedAppId, setDisplayedAppId ] = useState<string | null>(null);
const [ category, setCategory ] = useState<MarketplaceCategoriesIds>('all');
const [ filterQuery, setFilterQuery ] = useState('');
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(favoriteAppsLocalStorageKey, JSON.stringify(result));
} else {
favoriteApps.push(id);
localStorage.setItem(favoriteAppsLocalStorageKey, JSON.stringify(favoriteApps));
setFavoriteApps(favoriteApps);
}
}, [ ]);
const showAppInfo = useCallback((id: string) => {
setDisplayedAppId(id);
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
const debounceFilterApps = useCallback(debounce(q => setFilterQuery(q), 500), []);
const clearDisplayedAppId = useCallback(() => setDisplayedAppId(null), []);
const filterApps = useCallback((q: string, category: MarketplaceCategoriesIds) => {
const apps = defaultAppList
?.filter(app => {
return isAppNameMatches(q, app) && isAppCategoryMatches(category, app, favoriteApps);
});
setDisplayedApps(apps || []);
}, [ defaultAppList, favoriteApps ]);
const handleCategoryChange = useCallback((newCategory: MarketplaceCategoriesIds) => {
setCategory(newCategory);
}, []);
useEffect(() => {
setFavoriteApps(getFavoriteApps());
}, [ ]);
useEffect(() => {
filterApps(filterQuery, category);
}, [ filterQuery, category, filterApps ]);
useEffect(() => {
const defaultDisplayedApps = [ ...marketplaceApps ]
.filter(item => item.chainIds.includes(appConfig.network.id))
.sort((a, b) => a.title.localeCompare(b.title));
setDefaultAppList(defaultDisplayedApps);
setDisplayedApps(defaultDisplayedApps);
setIsLoading(false);
}, [ ]);
return React.useMemo(() => ({
category,
handleCategoryChange,
debounceFilterApps,
isLoading,
displayedApps,
showAppInfo,
displayedAppId,
clearDisplayedAppId,
favoriteApps,
handleFavoriteClick,
}), [ category,
clearDisplayedAppId,
debounceFilterApps,
displayedAppId, displayedApps,
favoriteApps,
handleCategoryChange,
handleFavoriteClick,
isLoading,
showAppInfo,
]);
}
...@@ -34,7 +34,7 @@ const BlocksListItem = ({ data, isPending }: Props) => { ...@@ -34,7 +34,7 @@ const BlocksListItem = ({ data, isPending }: Props) => {
{ data.height } { data.height }
</Link> </Link>
</Flex> </Flex>
<Text variant="secondary"fontWeight={ 400 }>{ dayjs(data.timestamp).locale('en-short').fromNow() }</Text> <Text variant="secondary"fontWeight={ 400 }>{ dayjs(data.timestamp).fromNow() }</Text>
</Flex> </Flex>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Size</Text> <Text fontWeight={ 500 }>Size</Text>
......
...@@ -33,7 +33,7 @@ const BlocksTableItem = ({ data, isPending }: Props) => { ...@@ -33,7 +33,7 @@ const BlocksTableItem = ({ data, isPending }: Props) => {
{ data.height } { data.height }
</Link> </Link>
</Flex> </Flex>
<Text variant="secondary" mt={ 2 } fontWeight={ 400 }>{ dayjs(data.timestamp).locale('en-short').fromNow() }</Text> <Text variant="secondary" mt={ 2 } fontWeight={ 400 }>{ dayjs(data.timestamp).fromNow() }</Text>
</Td> </Td>
<Td fontSize="sm">{ data.size.toLocaleString('en') } bytes</Td> <Td fontSize="sm">{ data.size.toLocaleString('en') } bytes</Td>
<Td fontSize="sm"> <Td fontSize="sm">
......
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;
import debounce from 'lodash/debounce'; import { Box, Icon, Link } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react'; import config from 'configs/app/config';
import React from 'react';
import type { AppItemOverview } from 'types/client/apps'; import PlusIcon from 'icons/plus.svg';
import { TEMPORARY_DEMO_APPS } from 'data/apps';
import AppList from 'ui/apps/AppList'; import AppList from 'ui/apps/AppList';
import AppListSkeleton from 'ui/apps/AppListSkeleton';
import CategoriesMenu from 'ui/apps/CategoriesMenu';
import FilterInput from 'ui/shared/FilterInput'; import FilterInput from 'ui/shared/FilterInput';
const defaultDisplayedApps = [ ...TEMPORARY_DEMO_APPS ] import useMarketplaceApps from '../apps/useMarkeplaceApps';
.sort((a, b) => a.title.localeCompare(b.title));
const Apps = () => { const Apps = () => {
const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>(defaultDisplayedApps); const {
const [ displayedAppId, setDisplayedAppId ] = useState<string | null>(null); isLoading,
category,
const showAppInfo = useCallback((id: string) => { handleCategoryChange,
setDisplayedAppId(id); debounceFilterApps,
}, []); showAppInfo,
displayedApps,
const filterApps = (q: string) => { displayedAppId,
const apps = displayedApps clearDisplayedAppId,
.filter(app => app.title.toLowerCase().includes(q.toLowerCase())); favoriteApps,
handleFavoriteClick,
setDisplayedApps(apps); } = useMarketplaceApps();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
const debounceFilterApps = useCallback(debounce(q => filterApps(q), 500), []);
const clearDisplayedAppId = useCallback(() => setDisplayedAppId(null), []);
return ( return (
<> <>
<Box
display="flex"
flexDirection={{ base: 'column', sm: 'row' }}
>
<CategoriesMenu
selectedCategoryId={ category }
onSelect={ handleCategoryChange }
/>
<FilterInput onChange={ debounceFilterApps } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/> <FilterInput onChange={ debounceFilterApps } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/>
</Box>
{ isLoading ? <AppListSkeleton/> : (
<AppList <AppList
apps={ displayedApps } apps={ displayedApps }
onAppClick={ showAppInfo } onAppClick={ showAppInfo }
displayedAppId={ displayedAppId } displayedAppId={ displayedAppId }
onModalClose={ clearDisplayedAppId } onModalClose={ clearDisplayedAppId }
favoriteApps={ favoriteApps }
onFavoriteClick={ handleFavoriteClick }
/> />
) }
{ config.marketplaceSubmitForm && (
<Link
fontWeight="bold"
display="inline-flex"
alignItems="baseline"
marginTop={{ base: 8, sm: 16 }}
href={ config.marketplaceSubmitForm }
isExternal
>
<Icon
as={ PlusIcon }
w={ 3 }
h={ 3 }
mr={ 2 }
/>
Submit an App
</Link>
) }
</> </>
); );
}; };
......
import { Box, Center, useColorMode } from '@chakra-ui/react';
import appConfig from 'configs/app/config';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps';
import ContentLoader from 'ui/shared/ContentLoader';
import Page from 'ui/shared/Page/Page';
type Props = {
app?: AppItemOverview;
isLoading: boolean;
}
const MarketplaceApp = ({ app, isLoading }: Props) => {
const [ isFrameLoading, setIsFrameLoading ] = useState(isLoading);
const ref = useRef<HTMLIFrameElement>(null);
const { colorMode } = useColorMode();
const handleIframeLoad = useCallback(() => {
setIsFrameLoading(false);
}, []);
const sandboxAttributeValue = 'allow-forms allow-orientation-lock ' +
'allow-pointer-lock allow-popups-to-escape-sandbox ' +
'allow-same-origin allow-scripts ' +
'allow-top-navigation-by-user-activation allow-popups';
const allowAttributeValue = 'clipboard-read; clipboard-write;';
useEffect(() => {
if (app && !isFrameLoading) {
ref?.current?.contentWindow?.postMessage({ blockscoutColorMode: colorMode, blockscoutChainId: appConfig.network.id }, app.url);
}
}, [ isFrameLoading, app, colorMode, ref ]);
return (
<Page wrapChildren={ false }>
<Center
as="main"
h="100%"
paddingTop={{ base: '138px', lg: 0 }}
>
{ (isFrameLoading) && (
<ContentLoader/>
) }
{ app && (
<Box
allow={ allowAttributeValue }
ref={ ref }
sandbox={ sandboxAttributeValue }
as="iframe"
h="100%"
w="100%"
display={ isFrameLoading ? 'none' : 'block' }
src={ app.url }
title={ app.title }
onLoad={ handleIframeLoad }
/>
) }
</Center>
</Page>
);
};
export default MarketplaceApp;
...@@ -25,7 +25,6 @@ const MyProfile = () => { ...@@ -25,7 +25,6 @@ const MyProfile = () => {
<UserAvatar size={ 64 } data={ data }/> <UserAvatar size={ 64 } data={ data }/>
<FormControl variant="floating" id="name" isRequired size="lg"> <FormControl variant="floating" id="name" isRequired size="lg">
<Input <Input
size="lg"
required required
disabled disabled
value={ data.name || '' } value={ data.name || '' }
...@@ -34,7 +33,6 @@ const MyProfile = () => { ...@@ -34,7 +33,6 @@ const MyProfile = () => {
</FormControl> </FormControl>
<FormControl variant="floating" id="nickname" isRequired size="lg"> <FormControl variant="floating" id="nickname" isRequired size="lg">
<Input <Input
size="lg"
required required
disabled disabled
value={ data.nickname || '' } value={ data.nickname || '' }
...@@ -43,7 +41,6 @@ const MyProfile = () => { ...@@ -43,7 +41,6 @@ const MyProfile = () => {
</FormControl> </FormControl>
<FormControl variant="floating" id="email" isRequired size="lg"> <FormControl variant="floating" id="email" isRequired size="lg">
<Input <Input
size="lg"
required required
disabled disabled
value={ data.email } value={ data.email }
......
...@@ -13,13 +13,14 @@ import TxDetails from 'ui/tx/TxDetails'; ...@@ -13,13 +13,14 @@ import TxDetails from 'ui/tx/TxDetails';
import TxInternals from 'ui/tx/TxInternals'; import TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs'; import TxLogs from 'ui/tx/TxLogs';
import TxRawTrace from 'ui/tx/TxRawTrace'; import TxRawTrace from 'ui/tx/TxRawTrace';
import TxState from 'ui/tx/TxState'; // import TxState from 'ui/tx/TxState';
const TABS: Array<RoutedTab> = [ const TABS: Array<RoutedTab> = [
{ routeName: 'tx_index', title: 'Details', component: <TxDetails/> }, { routeName: 'tx_index', title: 'Details', component: <TxDetails/> },
{ routeName: 'tx_internal', title: 'Internal txn', component: <TxInternals/> }, { routeName: 'tx_internal', title: 'Internal txn', component: <TxInternals/> },
{ routeName: 'tx_logs', title: 'Logs', component: <TxLogs/> }, { routeName: 'tx_logs', title: 'Logs', component: <TxLogs/> },
{ routeName: 'tx_state', title: 'State', component: <TxState/> }, // will be implemented later, api is not ready
// { routeName: 'tx_state', title: 'State', component: <TxState/> },
{ routeName: 'tx_raw_trace', title: 'Raw trace', component: <TxRawTrace/> }, { routeName: 'tx_raw_trace', title: 'Raw trace', component: <TxRawTrace/> },
]; ];
...@@ -37,12 +38,22 @@ const TransactionPageContent = ({ tab }: Props) => { ...@@ -37,12 +38,22 @@ const TransactionPageContent = ({ tab }: Props) => {
<Icon as={ eastArrowIcon } boxSize={ 6 } mr={ 2 } transform="rotate(180deg)"/> <Icon as={ eastArrowIcon } boxSize={ 6 } mr={ 2 } transform="rotate(180deg)"/>
Transactions Transactions
</Link> </Link>
<Flex alignItems="flex-start" flexDir={{ base: 'column', lg: 'row' }}>
<PageTitle text="Transaction details"/> <PageTitle text="Transaction details"/>
<Flex marginLeft="auto" alignItems="center" flexWrap="wrap" columnGap={ 6 } rowGap={ 3 } mb={ 6 }> <Flex
alignItems="center"
flexWrap="wrap"
columnGap={ 6 }
rowGap={ 3 }
ml={{ base: 'initial', lg: 'auto' }}
mb={{ base: 6, lg: 'initial' }}
py={ 2.5 }
>
<ExternalLink title="Open in Tenderly" href="#"/> <ExternalLink title="Open in Tenderly" href="#"/>
<ExternalLink title="Open in Blockchair" href="#"/> <ExternalLink title="Open in Blockchair" href="#"/>
<ExternalLink title="Open in Etherscan" href="#"/> <ExternalLink title="Open in Etherscan" href="#"/>
</Flex> </Flex>
</Flex>
<RoutedTabs <RoutedTabs
tabs={ TABS } tabs={ TABS }
defaultActiveTab={ tab } defaultActiveTab={ tab }
......
import type { InputProps } from '@chakra-ui/react';
import { IconButton, Icon, Flex } from '@chakra-ui/react'; import { IconButton, Icon, Flex } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { ControllerRenderProps, Control, FieldError } from 'react-hook-form'; import type { ControllerRenderProps, Control, FieldError } from 'react-hook-form';
...@@ -17,7 +18,7 @@ interface Props { ...@@ -17,7 +18,7 @@ interface Props {
error?: FieldError; error?: FieldError;
onAddFieldClick: (e: React.SyntheticEvent) => void; onAddFieldClick: (e: React.SyntheticEvent) => void;
onRemoveFieldClick: (index: number) => (e: React.SyntheticEvent) => void; onRemoveFieldClick: (index: number) => (e: React.SyntheticEvent) => void;
size?: string; size?: InputProps['size'];
} }
const MAX_INPUTS_NUM = 10; const MAX_INPUTS_NUM = 10;
......
import type { InputProps } from '@chakra-ui/react';
import { FormControl, FormLabel, Textarea } from '@chakra-ui/react'; import { FormControl, FormLabel, Textarea } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { ControllerRenderProps, Control, FieldError } from 'react-hook-form'; import type { ControllerRenderProps, Control, FieldError } from 'react-hook-form';
...@@ -12,7 +13,7 @@ const TEXT_INPUT_MAX_LENGTH = 255; ...@@ -12,7 +13,7 @@ const TEXT_INPUT_MAX_LENGTH = 255;
interface Props { interface Props {
control: Control<Inputs>; control: Control<Inputs>;
error?: FieldError; error?: FieldError;
size?: string; size?: InputProps['size'];
} }
export default function PublicTagFormComment({ control, error, size }: Props) { export default function PublicTagFormComment({ control, error, size }: Props) {
...@@ -22,7 +23,6 @@ export default function PublicTagFormComment({ control, error, size }: Props) { ...@@ -22,7 +23,6 @@ export default function PublicTagFormComment({ control, error, size }: Props) {
<Textarea <Textarea
{ ...field } { ...field }
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
size={ size }
/> />
<FormLabel> <FormLabel>
{ getPlaceholderWithError('Specify the reason for adding tags and color preference(s)', error?.message) } { getPlaceholderWithError('Specify the reason for adding tags and color preference(s)', error?.message) }
......
...@@ -16,7 +16,6 @@ import type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } from 'types ...@@ -16,7 +16,6 @@ import type { PublicTags, PublicTag, PublicTagNew, PublicTagErrors } from 'types
import getErrorMessage from 'lib/getErrorMessage'; import getErrorMessage from 'lib/getErrorMessage';
import type { ErrorType } from 'lib/hooks/useFetch'; import type { ErrorType } from 'lib/hooks/useFetch';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import { EMAIL_REGEXP } from 'lib/validations/email'; import { EMAIL_REGEXP } from 'lib/validations/email';
import FormSubmitAlert from 'ui/shared/FormSubmitAlert'; import FormSubmitAlert from 'ui/shared/FormSubmitAlert';
...@@ -57,9 +56,8 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 100; ...@@ -57,9 +56,8 @@ const ADDRESS_INPUT_BUTTONS_WIDTH = 100;
const PublicTagsForm = ({ changeToDataScreen, data }: Props) => { const PublicTagsForm = ({ changeToDataScreen, data }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile();
const fetch = useFetch(); const fetch = useFetch();
const inputSize = isMobile ? 'md' : 'lg'; const inputSize = { base: 'md', lg: 'lg' };
const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({ const { control, handleSubmit, formState: { errors, isValid }, setError } = useForm<Inputs>({
defaultValues: { defaultValues: {
......
import type { InputProps } from '@chakra-ui/react';
import { FormControl, FormLabel, Input } from '@chakra-ui/react'; import { FormControl, FormLabel, Input } from '@chakra-ui/react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { ControllerRenderProps, FieldError, FieldValues, Path, Control } from 'react-hook-form'; import type { ControllerRenderProps, FieldError, FieldValues, Path, Control } from 'react-hook-form';
...@@ -14,7 +15,7 @@ interface Props<TInputs extends FieldValues> { ...@@ -14,7 +15,7 @@ interface Props<TInputs extends FieldValues> {
control: Control<TInputs, object>; control: Control<TInputs, object>;
pattern?: RegExp; pattern?: RegExp;
error?: FieldError; error?: FieldError;
size?: string; size?: InputProps['size'];
} }
export default function PublicTagsFormInput<Inputs extends FieldValues>({ export default function PublicTagsFormInput<Inputs extends FieldValues>({
...@@ -31,7 +32,6 @@ export default function PublicTagsFormInput<Inputs extends FieldValues>({ ...@@ -31,7 +32,6 @@ export default function PublicTagsFormInput<Inputs extends FieldValues>({
<FormControl variant="floating" id={ field.name } isRequired={ required } size={ size }> <FormControl variant="floating" id={ field.name } isRequired={ required } size={ size }>
<Input <Input
{ ...field } { ...field }
size={ size }
required={ required } required={ required }
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
maxLength={ TEXT_INPUT_MAX_LENGTH } maxLength={ TEXT_INPUT_MAX_LENGTH }
......
import type { InputProps } from '@chakra-ui/react';
import { import {
Input, Input,
FormControl, FormControl,
...@@ -11,7 +12,7 @@ import { ADDRESS_LENGTH } from 'lib/validations/address'; ...@@ -11,7 +12,7 @@ import { ADDRESS_LENGTH } from 'lib/validations/address';
type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = { type Props<TInputs extends FieldValues, TInputName extends Path<TInputs>> = {
field: ControllerRenderProps<TInputs, TInputName>; field: ControllerRenderProps<TInputs, TInputName>;
size?: string; size?: InputProps['size'];
placeholder?: string; placeholder?: string;
backgroundColor?: string; backgroundColor?: string;
error?: FieldError; error?: FieldError;
...@@ -31,7 +32,6 @@ export default function AddressInput<Inputs extends FieldValues, Name extends Pa ...@@ -31,7 +32,6 @@ export default function AddressInput<Inputs extends FieldValues, Name extends Pa
{ ...field } { ...field }
isInvalid={ Boolean(error) } isInvalid={ Boolean(error) }
maxLength={ ADDRESS_LENGTH } maxLength={ ADDRESS_LENGTH }
size={ size }
/> />
<FormLabel>{ getPlaceholderWithError(placeholder, error?.message) }</FormLabel> <FormLabel>{ getPlaceholderWithError(placeholder, error?.message) }</FormLabel>
</FormControl> </FormControl>
......
import { Box, Text, chakra } from '@chakra-ui/react';
import { utils, constants } from 'ethers';
import React from 'react';
interface Props {
value: string;
unit?: 'wei' | 'gwei' | 'ether';
currency?: string;
exchangeRate?: string;
className?: string;
}
const CurrencyValue = ({ value, currency = '', unit = 'wei', exchangeRate, className }: Props) => {
const valueBn = utils.parseUnits(value, unit);
const exchangeRateBn = utils.parseUnits(exchangeRate || '0', 'ether');
const usdBn = valueBn.mul(exchangeRateBn).div(constants.WeiPerEther);
return (
<Box as="span" className={ className }>
<Text as="span">
{ Number(utils.formatUnits(valueBn)).toLocaleString() }{ currency ? ` ${ currency }` : '' }</Text>
{ exchangeRate !== undefined && exchangeRate !== null &&
<Text as="span" variant="secondary" whiteSpace="pre" fontWeight={ 400 }> (${ utils.formatUnits(usdBn) })</Text> }
</Box>
);
};
export default React.memo(chakra(CurrencyValue));
...@@ -49,10 +49,10 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole ...@@ -49,10 +49,10 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole
}, [ tabs, disabled ]); }, [ tabs, disabled ]);
React.useEffect(() => { React.useEffect(() => {
!disabled && setTabsRefs(tabsList.map((_, index) => tabsRefs[index] || React.createRef())); setTabsRefs(disabled ? [] : tabsList.map((_, index) => tabsRefs[index] || React.createRef()));
// imitate componentDidMount // update refs only when disabled prop changes
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [ disabled ]);
React.useEffect(() => { React.useEffect(() => {
if (tabsRefs.length > 0) { if (tabsRefs.length > 0) {
......
import { Tag, TagLabel, TagLeftIcon, Tooltip } from '@chakra-ui/react'; import { Tag, TagLabel, TagLeftIcon, Tooltip } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { Transaction } from 'types/api/transaction';
import errorIcon from 'icons/status/error.svg'; import errorIcon from 'icons/status/error.svg';
import pendingIcon from 'icons/status/pending.svg'; import pendingIcon from 'icons/status/pending.svg';
import successIcon from 'icons/status/success.svg'; import successIcon from 'icons/status/success.svg';
export interface Props { export interface Props {
status: 'success' | 'failed' | 'pending'; status: Transaction['status'];
errorText?: string; errorText?: string | null;
} }
const TxStatus = ({ status, errorText }: Props) => { const TxStatus = ({ status, errorText }: Props) => {
...@@ -16,17 +18,17 @@ const TxStatus = ({ status, errorText }: Props) => { ...@@ -16,17 +18,17 @@ const TxStatus = ({ status, errorText }: Props) => {
let colorScheme; let colorScheme;
switch (status) { switch (status) {
case 'success': case 'ok':
label = 'Success'; label = 'Success';
icon = successIcon; icon = successIcon;
colorScheme = 'green'; colorScheme = 'green';
break; break;
case 'failed': case 'error':
label = 'Failed'; label = 'Failed';
icon = errorIcon; icon = errorIcon;
colorScheme = 'red'; colorScheme = 'red';
break; break;
case 'pending': case null:
label = 'Pending'; label = 'Pending';
icon = pendingIcon; icon = pendingIcon;
// FIXME: it's not gray on mockups // FIXME: it's not gray on mockups
......
import { Flex, Icon, Text } from '@chakra-ui/react'; import { Flex, Icon, Text } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TokenTransfer as TTokenTransfer } from 'types/api/tokenTransfer';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CurrencyValue from 'ui/shared/CurrencyValue';
import TokenSnippet from 'ui/shared/TokenSnippet'; import TokenSnippet from 'ui/shared/TokenSnippet';
interface Props { type Props = TTokenTransfer
from: string;
to: string;
amount: number;
usd: number;
token: {
symbol: string;
hash: string;
name: string;
};
}
const TokenTransfer = ({ from, to, amount, usd, token }: Props) => { const TokenTransfer = ({ from, to, total, exchange_rate: exchangeRate, ...token }: Props) => {
return ( return (
<Flex alignItems="center" flexWrap="wrap" columnGap={ 3 } rowGap={ 3 }> <Flex alignItems="center" flexWrap="wrap" columnGap={ 3 } rowGap={ 3 }>
<Flex alignItems="center"> <Flex alignItems="center">
<AddressLink fontWeight="500" hash={ from } truncation="constant"/> <AddressLink fontWeight="500" hash={ from.hash } truncation="constant"/>
<Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } color="gray.500"/> <Icon as={ rightArrowIcon } boxSize={ 6 } mx={ 2 } color="gray.500"/>
<AddressLink fontWeight="500" hash={ to } truncation="constant"/> <AddressLink fontWeight="500" hash={ to.hash } truncation="constant"/>
</Flex> </Flex>
<Text fontWeight={ 500 } as="span">For:{ space } <Text fontWeight={ 500 } as="span">For:{ space }
<Text fontWeight={ 600 } as="span">{ amount }</Text>{ space } <CurrencyValue value={ total.value.replaceAll(',', '') } unit="ether" exchangeRate={ exchangeRate } fontWeight={ 600 }/>
<Text fontWeight={ 400 } variant="secondary" as="span">(${ usd.toFixed(2) })</Text>
</Text> </Text>
<TokenSnippet { ...token }/> <TokenSnippet symbol={ token.token_symbol } hash={ token.token_address } name="Foo"/>
</Flex> </Flex>
); );
}; };
......
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 type { DecodedInput } from 'types/api/decodedInput';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
...@@ -39,6 +41,7 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => { ...@@ -39,6 +41,7 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => {
> >
{ type } { type }
</GridItem> </GridItem>
{ indexed !== undefined && (
<GridItem <GridItem
pr={ GAP } pr={ GAP }
pt={ GAP } pt={ GAP }
...@@ -47,6 +50,7 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => { ...@@ -47,6 +50,7 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => {
> >
{ indexed ? 'true' : 'false' } { indexed ? 'true' : 'false' }
</GridItem> </GridItem>
) }
<GridItem <GridItem
pr={ PADDING } pr={ PADDING }
pt={ GAP } pt={ GAP }
...@@ -60,14 +64,28 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => { ...@@ -60,14 +64,28 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => {
); );
}; };
const TxDecodedInputData = () => { interface Props {
data: DecodedInput;
}
const TxDecodedInputData = ({ data }: Props) => {
const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const bgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
const hasIndexed = data.parameters.some(({ indexed }) => indexed !== undefined);
const gridTemplateColumns = hasIndexed ?
'minmax(80px, auto) minmax(80px, auto) minmax(80px, auto) minmax(0, 1fr)' :
'minmax(80px, auto) minmax(80px, auto) minmax(0, 1fr)';
const colNumber = hasIndexed ? 4 : 3;
return ( return (
<Grid gridTemplateColumns="minmax(80px, auto) minmax(80px, auto) minmax(80px, auto) minmax(0, 1fr)" fontSize="sm" lineHeight={ 5 } w="100%"> <Grid gridTemplateColumns={ gridTemplateColumns } fontSize="sm" lineHeight={ 5 } w="100%">
{ /* FIRST PART OF BLOCK */ } { /* FIRST PART OF BLOCK */ }
<GridItem fontWeight={ 600 } pl={{ base: 0, lg: PADDING }} pr={{ base: 0, lg: GAP }} colSpan={{ base: 4, lg: undefined }}>Method Id</GridItem> <GridItem fontWeight={ 600 } pl={{ base: 0, lg: PADDING }} pr={{ base: 0, lg: GAP }} colSpan={{ base: colNumber, lg: undefined }}>
<GridItem colSpan={{ base: 4, lg: 3 }} pr={{ base: 0, lg: PADDING }} mt={{ base: 2, lg: 0 }}>0xddf252ad</GridItem> Method Id
</GridItem>
<GridItem colSpan={{ base: colNumber, lg: colNumber - 1 }} pr={{ base: 0, lg: PADDING }} mt={{ base: 2, lg: 0 }}>
{ data.method_id }
</GridItem>
<GridItem <GridItem
py={ 2 } py={ 2 }
mt={ 2 } mt={ 2 }
...@@ -76,7 +94,7 @@ const TxDecodedInputData = () => { ...@@ -76,7 +94,7 @@ const TxDecodedInputData = () => {
fontWeight={ 600 } fontWeight={ 600 }
borderTopColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } borderTopColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderTopWidth="1px" borderTopWidth="1px"
colSpan={{ base: 4, lg: undefined }} colSpan={{ base: colNumber, lg: undefined }}
> >
Call Call
</GridItem> </GridItem>
...@@ -84,13 +102,13 @@ const TxDecodedInputData = () => { ...@@ -84,13 +102,13 @@ const TxDecodedInputData = () => {
py={{ base: 0, lg: 2 }} py={{ base: 0, lg: 2 }}
mt={{ base: 0, lg: 2 }} mt={{ base: 0, lg: 2 }}
mb={{ base: 2, lg: 0 }} mb={{ base: 2, lg: 0 }}
colSpan={{ base: 4, lg: 3 }} colSpan={{ base: colNumber, lg: colNumber - 1 }}
pr={{ base: 0, lg: PADDING }} pr={{ base: 0, lg: PADDING }}
borderTopColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') } borderTopColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderTopWidth={{ base: '0px', lg: '1px' }} borderTopWidth={{ base: '0px', lg: '1px' }}
whiteSpace="normal" whiteSpace="normal"
> >
Transfer(address indexed from, address indexed to, uint256 indexed tokenId) { data.method_call }
</GridItem> </GridItem>
{ /* TABLE INSIDE OF BLOCK */ } { /* TABLE INSIDE OF BLOCK */ }
<GridItem <GridItem
...@@ -112,6 +130,7 @@ const TxDecodedInputData = () => { ...@@ -112,6 +130,7 @@ const TxDecodedInputData = () => {
> >
Type Type
</GridItem> </GridItem>
{ hasIndexed && (
<GridItem <GridItem
pr={ GAP } pr={ GAP }
pt={ PADDING } pt={ PADDING }
...@@ -121,6 +140,7 @@ const TxDecodedInputData = () => { ...@@ -121,6 +140,7 @@ const TxDecodedInputData = () => {
> >
Inde<wbr/>xed? Inde<wbr/>xed?
</GridItem> </GridItem>
) }
<GridItem <GridItem
pr={ PADDING } pr={ PADDING }
pt={ PADDING } pt={ PADDING }
...@@ -130,24 +150,23 @@ const TxDecodedInputData = () => { ...@@ -130,24 +150,23 @@ const TxDecodedInputData = () => {
> >
Data Data
</GridItem> </GridItem>
<TableRow name="from" type="address"> { data.parameters.map(({ name, type, value, indexed }, index) => {
<Address justifyContent="space-between"> return (
<AddressLink hash="0x0000000000000000000000000000000000000000"/> <TableRow key={ name } name={ name } type={ type } isLast={ index === data.parameters.length - 1 } indexed={ indexed }>
<CopyToClipboard text="0x0000000000000000000000000000000000000000"/> { type === 'address' ? (
</Address>
</TableRow>
<TableRow name="to" type="address" indexed>
<Address justifyContent="space-between"> <Address justifyContent="space-between">
<AddressLink hash="0xcf0c50b7ea8af37d57380a0ac199d55b0782c718"/> <AddressLink hash={ value }/>
<CopyToClipboard text="0xcf0c50b7ea8af37d57380a0ac199d55b0782c718"/> <CopyToClipboard text={ value }/>
</Address> </Address>
</TableRow> ) : (
<TableRow name="tokenId" type="uint256" isLast> <Flex alignItems="flex-start" justifyContent="space-between" whiteSpace="normal" wordBreak="break-all">
<Flex alignItems="center" justifyContent="space-between"> <Text>{ value }</Text>
<Text>116842</Text> <CopyToClipboard text={ value }/>
<CopyToClipboard text="116842"/>
</Flex> </Flex>
) }
</TableRow> </TableRow>
);
}) }
</Grid> </Grid>
); );
}; };
......
This diff is collapsed.
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex, Alert, Show } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { TxInternalsType } from 'types/api/tx'; import type { InternalTransactionsResponse, TxInternalsType, InternalTransaction } from 'types/api/internalTransaction';
import type ArrayElement from 'types/utils/ArrayElement';
import { data } from 'data/txInternal'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { apos } from 'lib/html-entities'; import { apos } from 'lib/html-entities';
import EmptySearchResult from 'ui/apps/EmptySearchResult'; import EmptySearchResult from 'ui/apps/EmptySearchResult';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import FilterInput from 'ui/shared/FilterInput'; import FilterInput from 'ui/shared/FilterInput';
import TxInternalsFilter from 'ui/tx/internals/TxInternalsFilter'; import TxInternalsFilter from 'ui/tx/internals/TxInternalsFilter';
import TxInternalsList from 'ui/tx/internals/TxInternalsList'; import TxInternalsList from 'ui/tx/internals/TxInternalsList';
import TxInternalsSkeletonDesktop from 'ui/tx/internals/TxInternalsSkeletonDesktop';
import TxInternalsSkeletonMobile from 'ui/tx/internals/TxInternalsSkeletonMobile';
import TxInternalsTable from 'ui/tx/internals/TxInternalsTable'; import TxInternalsTable from 'ui/tx/internals/TxInternalsTable';
import type { Sort, SortField } from 'ui/tx/internals/utils';
const searchFn = (searchTerm: string) => (item: ArrayElement<typeof data>): boolean => { const SORT_SEQUENCE: Record<SortField, Array<Sort | undefined>> = {
value: [ 'value-desc', 'value-asc', undefined ],
'gas-limit': [ 'gas-limit-desc', 'gas-limit-asc', undefined ],
};
const getNextSortValue = (field: SortField) => (prevValue: Sort | undefined) => {
const sequence = SORT_SEQUENCE[field];
const curIndex = sequence.findIndex((sort) => sort === prevValue);
const nextIndex = curIndex + 1 > sequence.length - 1 ? 0 : curIndex + 1;
return sequence[nextIndex];
};
const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalTransaction) => {
switch (sort) {
case 'value-desc': {
const result = a.value > b.value ? -1 : 1;
return a.value === b.value ? 0 : result;
}
case 'value-asc': {
const result = a.value > b.value ? 1 : -1;
return a.value === b.value ? 0 : result;
}
// no gas limit in api yet
// case 'gas-limit-desc': {
// const result = a.gasLimit > b.gasLimit ? -1 : 1;
// return a.gasLimit === b.gasLimit ? 0 : result;
// }
// case 'gas-limit-asc': {
// const result = a.gasLimit > b.gasLimit ? 1 : -1;
// return a.gasLimit === b.gasLimit ? 0 : result;
// }
default:
return 0;
}
};
const searchFn = (searchTerm: string) => (item: InternalTransaction): boolean => {
const formattedSearchTerm = searchTerm.toLowerCase(); const formattedSearchTerm = searchTerm.toLowerCase();
return item.type.toLowerCase().includes(formattedSearchTerm) || return item.type.toLowerCase().includes(formattedSearchTerm) ||
item.from.hash.toLowerCase().includes(formattedSearchTerm) || item.from.hash.toLowerCase().includes(formattedSearchTerm) ||
...@@ -21,24 +66,62 @@ const searchFn = (searchTerm: string) => (item: ArrayElement<typeof data>): bool ...@@ -21,24 +66,62 @@ const searchFn = (searchTerm: string) => (item: ArrayElement<typeof data>): bool
}; };
const TxInternals = () => { const TxInternals = () => {
const router = useRouter();
const fetch = useFetch();
const [ filters, setFilters ] = React.useState<Array<TxInternalsType>>([]); const [ filters, setFilters ] = React.useState<Array<TxInternalsType>>([]);
const [ searchTerm, setSearchTerm ] = React.useState<string>(''); const [ searchTerm, setSearchTerm ] = React.useState<string>('');
const [ sort, setSort ] = React.useState<Sort>();
const { data, isLoading, isError } = useQuery<unknown, unknown, InternalTransactionsResponse>(
[ 'tx-internals', router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }/internal-transactions`),
{
enabled: Boolean(router.query.id),
},
);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const handleFilterChange = React.useCallback((nextValue: Array<TxInternalsType>) => { const handleFilterChange = React.useCallback((nextValue: Array<TxInternalsType>) => {
setFilters(nextValue); setFilters(nextValue);
}, []); }, []);
const handleSortToggle = React.useCallback((field: SortField) => {
return () => {
setSort(getNextSortValue(field));
};
}, []);
if (isLoading) {
return (
<>
<Show below="lg"><TxInternalsSkeletonMobile/></Show>
<Show above="lg"><TxInternalsSkeletonDesktop/></Show>
</>
);
}
if (isError) {
return <DataFetchAlert/>;
}
if (data.items.length === 0) {
return <Alert>There are no internal transactions for this transaction.</Alert>;
}
const content = (() => { const content = (() => {
const filteredData = data const filteredData = data.items
.filter(({ type }) => filters.length > 0 ? filters.includes(type) : true) .filter(({ type }) => filters.length > 0 ? filters.includes(type) : true)
.filter(searchFn(searchTerm)); .filter(searchFn(searchTerm))
.sort(sortFn(sort));
if (filteredData.length === 0) { if (filteredData.length === 0) {
return <EmptySearchResult text={ `Couldn${ apos }t find any transaction that matches your query.` }/>; return <EmptySearchResult text={ `Couldn${ apos }t find any transaction that matches your query.` }/>;
} }
return isMobile ? <TxInternalsList data={ filteredData }/> : <TxInternalsTable data={ filteredData }/>; return isMobile ?
<TxInternalsList data={ filteredData }/> :
<TxInternalsTable data={ filteredData } sort={ sort } onSortToggle={ handleSortToggle }/>;
})(); })();
return ( return (
......
import { Box } from '@chakra-ui/react'; import { Box, Alert } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { data } from 'data/txLogs'; import type { LogsResponse } from 'types/api/log';
import useFetch from 'lib/hooks/useFetch';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import TxLogItem from 'ui/tx/logs/TxLogItem'; import TxLogItem from 'ui/tx/logs/TxLogItem';
import TxLogSkeleton from 'ui/tx/logs/TxLogSkeleton';
const TxLogs = () => { const TxLogs = () => {
const router = useRouter();
const fetch = useFetch();
const { data, isLoading, isError } = useQuery<unknown, unknown, LogsResponse>(
[ 'tx-log', router.query.id ],
async() => await fetch(`/api/transactions/${ router.query.id }/logs`),
{
enabled: Boolean(router.query.id),
},
);
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<Box>
<TxLogSkeleton/>
<TxLogSkeleton/>
</Box>
);
}
if (data.items.length === 0) {
return <Alert>There are no logs for this transaction.</Alert>;
}
return ( return (
<Box> <Box>
{ data.map((item, index) => <TxLogItem key={ index } { ...item } index={ index }/>) } { data.items.map((item, index) => <TxLogItem key={ index } { ...item }/>) }
</Box> </Box>
); );
}; };
......
import { Flex, Textarea } from '@chakra-ui/react'; import { Flex, Textarea, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { RawTracesResponse } from 'types/api/rawTrace';
import useFetch from 'lib/hooks/useFetch';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
const data = [ const TxRawTrace = () => {
{ const router = useRouter();
action: { const fetch = useFetch();
callType: 'delegatecall',
from: '0x296033cb983747b68911244ec1a3f01d7708851b', const { data, isLoading, isError } = useQuery<unknown, unknown, RawTracesResponse>(
gas: '0x1AB35C9', [ 'tx-raw-trace', router.query.id ],
// eslint-disable-next-line max-len async() => await fetch(`/api/transactions/${ router.query.id }/raw-trace`),
input: '0x6a76120200000000000000000000000099759357a9923bb164a7ae8b85703a6882cb84ea0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000014466d2a64d000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000013ef0000000000000000000000000000000000000000000000000000000000001bfb000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000186b900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041000000000000000000000000f4e5b62da2eee3b5811dae1fae480f7623bd4cd000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000',
to: '0x3e5c63644e683549055b9be8653de26e0b4cd36e',
value: '0x0',
},
result: {
blockHash: '0x43dd926aa138a58d3f4740dae387bcff3c7bc525db2d0a449f323f8b8f92a229',
blockNumber: '0xa4f285',
from: '0xea8a7ef30f894bce23b42314613458d13f9d43ea',
gas: '0x30d40',
gasPrice: '0x2e90edd000',
hash: '0x72ee43a3784cc6749f64fad1ecf0bbd51a54dd6892ae0573f211566809e0d511',
input: '0x',
nonce: '0x1e7',
to: '0xbd064928cdd4fd67fb99917c880e6560978d7ca1',
transactionIndex: '0x0',
value: '0xde0b6b3a7640000',
v: '0x25',
r: '0x7e833413ead52b8c538001b12ab5a85bac88db0b34b61251bb0fc81573ca093f',
s: '0x49634f1e439e3760265888434a2f9782928362412030db1429458ddc9dcee995',
},
},
{ {
action: { enabled: Boolean(router.query.id),
callType: 'delegatecall',
from: '0x296033cb983747b68911244ec1a3f01d7708851b',
gas: '0x1AB35C9',
// eslint-disable-next-line max-len
input: '0x6a76120200000000000000000000000099759357a9923bb164a7ae8b85703a6882cb84ea0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000014466d2a64d000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000013ef0000000000000000000000000000000000000000000000000000000000001bfb000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000186b900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041000000000000000000000000f4e5b62da2eee3b5811dae1fae480f7623bd4cd000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000',
to: '0x3e5c63644e683549055b9be8653de26e0b4cd36e',
value: '0x0',
},
result: {
blockHash: '0x43dd926aa138a58d3f4740dae387bcff3c7bc525db2d0a449f323f8b8f92a229',
blockNumber: '0xa4f285',
from: '0xea8a7ef30f894bce23b42314613458d13f9d43ea',
gas: '0x30d40',
gasPrice: '0x2e90edd000',
hash: '0x72ee43a3784cc6749f64fad1ecf0bbd51a54dd6892ae0573f211566809e0d511',
input: '0x',
nonce: '0x1e7',
to: '0xbd064928cdd4fd67fb99917c880e6560978d7ca1',
transactionIndex: '0x0',
value: '0xde0b6b3a7640000',
v: '0x25',
r: '0x7e833413ead52b8c538001b12ab5a85bac88db0b34b61251bb0fc81573ca093f',
s: '0x49634f1e439e3760265888434a2f9782928362412030db1429458ddc9dcee995',
}, },
}, );
];
if (isError) {
return <DataFetchAlert/>;
}
if (isLoading) {
return (
<>
<Flex justifyContent="end" mb={ 2 }>
<Skeleton w={ 5 } h={ 5 }/>
</Flex>
<Skeleton w="100%" h="500px"/>
</>
);
}
const TxRawTrace = () => {
const text = JSON.stringify(data, undefined, 4); const text = JSON.stringify(data, undefined, 4);
return ( return (
<> <>
<Flex justifyContent="end" mb={ 2 }> <Flex justifyContent="end" mb={ 2 }>
...@@ -69,7 +45,7 @@ const TxRawTrace = () => { ...@@ -69,7 +45,7 @@ const TxRawTrace = () => {
</Flex> </Flex>
<Textarea <Textarea
variant="filledInactive" variant="filledInactive"
height="570px" minHeight="500px"
p={ 4 } p={ 4 }
value={ text } value={ text }
/> />
......
import { Grid, GridItem, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react';
const SkeletonRow = ({ w = '100%' }: { w?: string }) => (
<>
<GridItem display="flex" columnGap={ 2 } w={{ base: '50%', lg: 'auto' }} _notFirst={{ mt: { base: 3, lg: 0 } }}>
<SkeletonCircle h={ 5 } w={ 5 }/>
<Skeleton flexGrow={ 1 } h={ 5 } borderRadius="full"/>
</GridItem>
<GridItem pl={{ base: 7, lg: 0 }}>
<Skeleton h={ 5 } borderRadius="full" w={{ base: '100%', lg: w }}/>
</GridItem>
</>
);
const TxDetailsSkeleton = () => {
const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>;
return (
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px">
<SkeletonRow/>
<SkeletonRow w="20%"/>
<SkeletonRow w="50%"/>
<SkeletonRow/>
<SkeletonRow w="70%"/>
<SkeletonRow w="70%"/>
{ sectionGap }
<SkeletonRow w="40%"/>
<SkeletonRow w="40%"/>
<SkeletonRow w="40%"/>
<SkeletonRow w="40%"/>
<SkeletonRow w="40%"/>
{ sectionGap }
<GridItem colSpan={{ base: undefined, lg: 2 }}>
<Skeleton h={ 5 } borderRadius="full" w="100px"/>
</GridItem>
</Grid>
);
};
export default TxDetailsSkeleton;
import { Popover, PopoverTrigger, PopoverContent, PopoverBody, CheckboxGroup, Checkbox, Text, useDisclosure } from '@chakra-ui/react'; import { Popover, PopoverTrigger, PopoverContent, PopoverBody, CheckboxGroup, Checkbox, Text, useDisclosure } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { TxInternalsType } from 'types/api/tx'; import type { TxInternalsType } from 'types/api/internalTransaction';
import FilterButton from 'ui/shared/FilterButton'; import FilterButton from 'ui/shared/FilterButton';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils'; import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { data as txData } from 'data/txInternal'; import type { InternalTransaction } from 'types/api/internalTransaction';
import TxInternalsListItem from 'ui/tx/internals/TxInternalsListItem'; import TxInternalsListItem from 'ui/tx/internals/TxInternalsListItem';
const TxInternalsList = ({ data }: { data: typeof txData}) => { const TxInternalsList = ({ data }: { data: Array<InternalTransaction>}) => {
return ( return (
<Box mt={ 6 }> <Box mt={ 6 }>
{ data.map((item) => <TxInternalsListItem key={ item.id } { ...item }/>) } { data.map((item) => <TxInternalsListItem key={ item.transaction_hash } { ...item }/>) }
</Box> </Box>
); );
}; };
......
import { Flex, Tag, Icon, Box, HStack, Text } from '@chakra-ui/react'; import { Flex, Tag, Icon, Box, HStack, Text } from '@chakra-ui/react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import capitalize from 'lodash/capitalize';
import React from 'react'; import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement'; import type { InternalTransaction } from 'types/api/internalTransaction';
import type { data } from 'data/txInternal';
import eastArrowIcon from 'icons/arrows/east.svg'; import eastArrowIcon from 'icons/arrows/east.svg';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile'; import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
type Props = ArrayElement<typeof data>; type Props = InternalTransaction;
const TxInternalsListItem = ({ type, from, to, value, success, error }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const TxInternalsListItem = ({ type, status, from, to, value, gasLimit }: Props) => {
return ( return (
<AccountListItemMobile rowGap={ 3 }> <AccountListItemMobile rowGap={ 3 }>
<Flex> <Flex>
<Tag colorScheme="cyan" mr={ 2 }>{ capitalize(type) }</Tag> <Tag colorScheme="cyan" mr={ 2 }>{ typeTitle }</Tag>
<TxStatus status={ status }/> <TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
</Flex> </Flex>
<Box w="100%" display="flex" columnGap={ 3 }> <Box w="100%" display="flex" columnGap={ 3 }>
<Address width="calc((100% - 48px) / 2)"> <Address width="calc((100% - 48px) / 2)">
...@@ -37,10 +38,11 @@ const TxInternalsListItem = ({ type, status, from, to, value, gasLimit }: Props) ...@@ -37,10 +38,11 @@ const TxInternalsListItem = ({ type, status, from, to, value, gasLimit }: Props)
<Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency }</Text> <Text fontSize="sm" fontWeight={ 500 }>Value { appConfig.network.currency }</Text>
<Text fontSize="sm" variant="secondary">{ value }</Text> <Text fontSize="sm" variant="secondary">{ value }</Text>
</HStack> </HStack>
<HStack spacing={ 3 }> { /* no gas limit in api yet */ }
{ /* <HStack spacing={ 3 }>
<Text fontSize="sm" fontWeight={ 500 }>Gas limit</Text> <Text fontSize="sm" fontWeight={ 500 }>Gas limit</Text>
<Text fontSize="sm" variant="secondary">{ gasLimit.toLocaleString('en') }</Text> <Text fontSize="sm" variant="secondary">{ gasLimit.toLocaleString('en') }</Text>
</HStack> </HStack> */ }
</AccountListItemMobile> </AccountListItemMobile>
); );
}; };
......
import { Skeleton, Flex } from '@chakra-ui/react';
import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable';
const TxInternalsSkeletonDesktop = () => {
return (
<>
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="78px"/>
<Skeleton w="360px"/>
</Flex>
<SkeletonTable columns={ [ '28%', '28%', '24px', '28%', '16%' ] }/>
</>
);
};
export default TxInternalsSkeletonDesktop;
import { Skeleton, SkeletonCircle, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TxInternalsSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<>
<Flex columnGap={ 3 } h={ 8 } mb={ 6 }>
<Skeleton w="36px" flexShrink={ 0 }/>
<Skeleton w="100%"/>
</Flex>
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
rowGap={ 3 }
flexDirection="column"
paddingY={ 6 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<Skeleton w="100px" mr={ 2 }/>
<Skeleton w="90px"/>
</Flex>
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
<Skeleton w={ 6 } mr={ 3 }/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="30px"/>
</Flex>
</Flex>
)) }
</Box>
</>
);
};
export default TxInternalsSkeletonMobile;
import { Table, Thead, Tbody, Tr, Th, TableContainer } from '@chakra-ui/react'; import { Table, Thead, Tbody, Tr, Th, TableContainer, Link, Icon } from '@chakra-ui/react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import React from 'react'; import React from 'react';
import type { data as txData } from 'data/txInternal'; import type { InternalTransaction } from 'types/api/internalTransaction';
import arrowIcon from 'icons/arrows/east.svg';
import TxInternalsTableItem from 'ui/tx/internals/TxInternalsTableItem'; import TxInternalsTableItem from 'ui/tx/internals/TxInternalsTableItem';
import type { Sort, SortField } from 'ui/tx/internals/utils';
interface Props {
data: Array<InternalTransaction>;
sort: Sort | undefined;
onSortToggle: (field: SortField) => () => void;
}
const TxInternalsTable = ({ data }: { data: typeof txData}) => { const TxInternalsTable = ({ data, sort, onSortToggle }: Props) => {
const sortIconTransform = sort?.includes('asc') ? 'rotate(-90deg)' : 'rotate(90deg)';
return ( return (
<TableContainer width="100%" mt={ 6 }> <TableContainer width="100%" mt={ 6 }>
...@@ -13,16 +23,27 @@ const TxInternalsTable = ({ data }: { data: typeof txData}) => { ...@@ -13,16 +23,27 @@ const TxInternalsTable = ({ data }: { data: typeof txData}) => {
<Thead> <Thead>
<Tr> <Tr>
<Th width="28%">Type</Th> <Th width="28%">Type</Th>
<Th width="20%">From</Th> <Th width="28%">From</Th>
<Th width="24px" px={ 0 }/> <Th width="24px" px={ 0 }/>
<Th width="20%">To</Th> <Th width="28%">To</Th>
<Th width="16%" isNumeric>Value { appConfig.network.currency }</Th> <Th width="16%" isNumeric>
<Th width="16%" isNumeric>Gas limit</Th> <Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('value') } columnGap={ 1 }>
{ sort?.includes('value') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Value { appConfig.network.currency }
</Link>
</Th>
{ /* no gas limit in api yet */ }
{ /* <Th width="16%" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('gas-limit') } columnGap={ 1 }>
{ sort?.includes('gas-limit') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Gas limit
</Link>
</Th> */ }
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item) => ( { data.map((item) => (
<TxInternalsTableItem key={ item.id } { ...item }/> <TxInternalsTableItem key={ item.transaction_hash } { ...item }/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Tr, Td, Tag, Icon, Box } from '@chakra-ui/react'; import { Tr, Td, Tag, Icon, Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { InternalTransaction } from 'types/api/internalTransaction';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
...@@ -8,16 +10,9 @@ import AddressLink from 'ui/shared/address/AddressLink'; ...@@ -8,16 +10,9 @@ import AddressLink from 'ui/shared/address/AddressLink';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils'; import { TX_INTERNALS_ITEMS } from 'ui/tx/internals/utils';
interface Props { type Props = InternalTransaction
type: string;
status: 'success' | 'failed' | 'pending';
from: { hash: string; alias?: string};
to: { hash: string; alias?: string};
value: number;
gasLimit: number;
}
const TxInternalTableItem = ({ type, status, from, to, value, gasLimit }: Props) => { const TxInternalTableItem = ({ type, from, to, value, success, error }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title; const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
return ( return (
...@@ -28,12 +23,12 @@ const TxInternalTableItem = ({ type, status, from, to, value, gasLimit }: Props) ...@@ -28,12 +23,12 @@ const TxInternalTableItem = ({ type, status, from, to, value, gasLimit }: Props)
<Tag colorScheme="cyan" mr={ 5 }>{ typeTitle }</Tag> <Tag colorScheme="cyan" mr={ 5 }>{ typeTitle }</Tag>
</Box> </Box>
) } ) }
<TxStatus status={ status }/> <TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
</Td> </Td>
<Td> <Td>
<Address> <Address>
<AddressIcon hash={ from.hash }/> <AddressIcon hash={ from.hash }/>
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.alias } flexGrow={ 1 }/> <AddressLink ml={ 2 } fontWeight="500" hash={ from.hash } alias={ from.name } flexGrow={ 1 }/>
</Address> </Address>
</Td> </Td>
<Td px={ 0 }> <Td px={ 0 }>
...@@ -42,15 +37,16 @@ const TxInternalTableItem = ({ type, status, from, to, value, gasLimit }: Props) ...@@ -42,15 +37,16 @@ const TxInternalTableItem = ({ type, status, from, to, value, gasLimit }: Props)
<Td> <Td>
<Address> <Address>
<AddressIcon hash={ to.hash }/> <AddressIcon hash={ to.hash }/>
<AddressLink hash={ to.hash } alias={ to.alias } fontWeight="500" ml={ 2 }/> <AddressLink hash={ to.hash } alias={ to.name } fontWeight="500" ml={ 2 }/>
</Address> </Address>
</Td> </Td>
<Td isNumeric> <Td isNumeric>
{ value } { value }
</Td> </Td>
<Td isNumeric> { /* no gas limit in api yet */ }
{ /* <Td isNumeric>
{ gasLimit.toLocaleString('en') } { gasLimit.toLocaleString('en') }
</Td> </Td> */ }
</Tr> </Tr>
); );
}; };
......
import type { TxInternalsType } from 'types/api/tx'; import type { TxInternalsType } from 'types/api/internalTransaction';
export type Sort = 'value-asc' | 'value-desc' | 'gas-limit-asc' | 'gas-limit-desc';
export type SortField = 'value' | 'gas-limit';
interface TxInternalsTypeItem { interface TxInternalsTypeItem {
title: string; title: string;
...@@ -7,10 +10,10 @@ interface TxInternalsTypeItem { ...@@ -7,10 +10,10 @@ interface TxInternalsTypeItem {
export const TX_INTERNALS_ITEMS: Array<TxInternalsTypeItem> = [ export const TX_INTERNALS_ITEMS: Array<TxInternalsTypeItem> = [
{ title: 'Call', id: 'call' }, { title: 'Call', id: 'call' },
{ title: 'Delegate call', id: 'delegate_call' }, { title: 'Delegate call', id: 'delegatecall' },
{ title: 'Static call', id: 'static_call' }, { title: 'Static call', id: 'staticcall' },
{ title: 'Create', id: 'create' }, { title: 'Create', id: 'create' },
{ title: 'Create2', id: 'create2' }, { title: 'Create2', id: 'create2' },
{ title: 'Self-destruct', id: 'self_destruct' }, { title: 'Self-destruct', id: 'selfdestruct' },
{ title: 'Reward', id: 'reward' }, { title: 'Reward', id: 'reward' },
]; ];
import { Text, Grid, GridItem, Link, Tooltip, Button, Icon, useColorModeValue } from '@chakra-ui/react'; import { Text, Grid, GridItem, Link, Tooltip, Button, Icon, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { Log } from 'types/api/log';
import searchIcon from 'icons/search.svg'; import searchIcon from 'icons/search.svg';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
...@@ -8,12 +10,7 @@ import AddressLink from 'ui/shared/address/AddressLink'; ...@@ -8,12 +10,7 @@ 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';
interface Props { type Props = Log;
address: string;
topics: Array<{ hex: string }>;
data: string;
index: number;
}
const RowHeader = ({ children }: { children: React.ReactNode }) => ( const RowHeader = ({ children }: { children: React.ReactNode }) => (
<GridItem _notFirst={{ my: { base: 4, lg: 0 } }}> <GridItem _notFirst={{ my: { base: 4, lg: 0 } }}>
...@@ -21,7 +18,8 @@ const RowHeader = ({ children }: { children: React.ReactNode }) => ( ...@@ -21,7 +18,8 @@ const RowHeader = ({ children }: { children: React.ReactNode }) => (
</GridItem> </GridItem>
); );
const TxLogItem = ({ address, index, topics, data }: Props) => { const TxLogItem = ({ address, index, topics, data, decoded }: Props) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const dataBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50'); const dataBgColor = useColorModeValue('blackAlpha.50', 'whiteAlpha.50');
...@@ -41,27 +39,31 @@ const TxLogItem = ({ address, index, topics, data }: Props) => { ...@@ -41,27 +39,31 @@ const TxLogItem = ({ address, index, topics, data }: Props) => {
<RowHeader>Address</RowHeader> <RowHeader>Address</RowHeader>
<GridItem display="flex" alignItems="center"> <GridItem display="flex" alignItems="center">
<Address> <Address>
<AddressIcon hash={ address }/> <AddressIcon hash={ address.hash }/>
<AddressLink hash={ address } ml={ 2 }/> <AddressLink hash={ address.hash } alias={ address.name } ml={ 2 }/>
</Address> </Address>
<Tooltip label="Find matches topic"> <Tooltip label="Find matches topic">
<Link ml={ 2 } display="inline-flex"> <Link ml={ 2 } mr={{ base: 9, lg: 0 }} display="inline-flex">
<Icon as={ searchIcon } boxSize={ 5 }/> <Icon as={ searchIcon } boxSize={ 5 }/>
</Link> </Link>
</Tooltip> </Tooltip>
<Tooltip label="Log index"> <Tooltip label="Log index">
<Button variant="outline" colorScheme="gray" isActive ml={{ base: 9, lg: 'auto' }} size="sm" fontWeight={ 400 }> <Button variant="outline" colorScheme="gray" isActive ml="auto" size="sm" fontWeight={ 400 }>
{ index } { index }
</Button> </Button>
</Tooltip> </Tooltip>
</GridItem> </GridItem>
{ decoded && (
<>
<RowHeader>Decode input data</RowHeader> <RowHeader>Decode input data</RowHeader>
<GridItem> <GridItem>
<DecodedInputData/> <DecodedInputData data={ decoded }/>
</GridItem> </GridItem>
</>
) }
<RowHeader>Topics</RowHeader> <RowHeader>Topics</RowHeader>
<GridItem> <GridItem>
{ topics.map((item, index) => <TxLogTopic key={ index } { ...item } index={ index }/>) } { topics.filter(Boolean).map((item, index) => <TxLogTopic key={ index } hex={ item } index={ index }/>) }
</GridItem> </GridItem>
<RowHeader>Data</RowHeader> <RowHeader>Data</RowHeader>
<GridItem p={ 4 } fontSize="sm" borderRadius="md" bgColor={ dataBgColor }> <GridItem p={ 4 } fontSize="sm" borderRadius="md" bgColor={ dataBgColor }>
......
import { Flex, Grid, GridItem, Skeleton, SkeletonCircle, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const RowHeader = () => (
<GridItem _notFirst={{ my: { base: 4, lg: 0 } }} _first={{ alignSelf: 'center' }}>
<Skeleton h={ 6 } borderRadius="full" w="150px"/>
</GridItem>
);
const TopicRow = () => (
<Flex columnGap={ 3 }>
<SkeletonCircle size="6"/>
<Skeleton h={ 6 } w="70px" borderRadius="full"/>
<Skeleton h={ 6 } flexGrow={ 1 } borderRadius="full"/>
</Flex>
);
const TxLogSkeleton = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Grid
gap={{ base: 2, lg: 8 }}
templateColumns={{ base: '1fr', lg: '200px 1fr' }}
py={ 8 }
_notFirst={{
borderTopWidth: '1px',
borderTopColor: borderColor,
}}
_first={{
pt: 0,
}}
>
<RowHeader/>
<GridItem display="flex" alignItems="center">
<SkeletonCircle size="6"/>
<Skeleton h={ 6 } flexGrow={ 1 } borderRadius="full" ml={ 2 } mr={ 9 }/>
<Skeleton h={ 8 } w={ 8 } borderRadius="base"/>
</GridItem>
<RowHeader/>
<GridItem>
<Skeleton h="150px" w="100%" borderRadius="base"/>
</GridItem>
<RowHeader/>
<GridItem display="flex" flexDir="column" rowGap={ 3 }>
<TopicRow/>
<TopicRow/>
<TopicRow/>
</GridItem>
<RowHeader/>
<GridItem>
<Skeleton h="60px" w="100%" borderRadius="base"/>
</GridItem>
</Grid>
);
};
export default TxLogSkeleton;
...@@ -23,9 +23,6 @@ const TxLogTopic = ({ hex, index }: Props) => { ...@@ -23,9 +23,6 @@ const TxLogTopic = ({ hex, index }: Props) => {
<Button variant="outline" colorScheme="gray" isActive size="xs" fontWeight={ 400 } mr={ 3 } w={ 6 }> <Button variant="outline" colorScheme="gray" isActive size="xs" fontWeight={ 400 } mr={ 3 } w={ 6 }>
{ index } { index }
</Button> </Button>
{ /* temporary condition juse to show different states of the component */ }
{ /* delete when ther will be real data */ }
{ index > 0 && (
<Select <Select
size="sm" size="sm"
borderRadius="base" borderRadius="base"
...@@ -38,7 +35,6 @@ const TxLogTopic = ({ hex, index }: Props) => { ...@@ -38,7 +35,6 @@ const TxLogTopic = ({ hex, index }: Props) => {
> >
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) } { OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
</Select> </Select>
) }
<Box overflow="hidden" whiteSpace="nowrap"> <Box overflow="hidden" whiteSpace="nowrap">
<HashStringShortenDynamic hash={ hex }/> <HashStringShortenDynamic hash={ hex }/>
</Box> </Box>
......
import { AccordionItem, AccordionButton, AccordionIcon, Button, Flex, Text, Link, StatArrow, Stat, AccordionPanel } from '@chakra-ui/react'; import { AccordionItem, AccordionButton, AccordionIcon, Button, Box, Flex, Text, Link, StatArrow, Stat, AccordionPanel } from '@chakra-ui/react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import React from 'react'; import React from 'react';
...@@ -10,7 +10,6 @@ import AccountListItemMobile from 'ui/shared/AccountListItemMobile'; ...@@ -10,7 +10,6 @@ import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import TextSeparator from 'ui/shared/TextSeparator';
import TxStateStorageItem from './TxStateStorageItem'; import TxStateStorageItem from './TxStateStorageItem';
...@@ -22,18 +21,14 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props ...@@ -22,18 +21,14 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
return ( return (
<AccountListItemMobile> <AccountListItemMobile>
<AccordionItem isDisabled={ !hasStorageData } border={ 0 } w="100%" display="flex" flexDirection="column" rowGap={ 3 }> <AccordionItem isDisabled={ !hasStorageData } border={ 0 } w="100%" display="flex" flexDirection="column">
{ ({ isExpanded }) => ( { ({ isExpanded }) => (
<> <>
<Flex> <Flex mb={ 6 }>
<Address flexGrow={ 1 }>
<AddressIcon hash={ address }/>
<AddressLink hash={ address } fontWeight="500" ml={ 2 }/>
</Address>
<AccordionButton <AccordionButton
_hover={{ background: 'unset' }} _hover={{ background: 'unset' }}
padding="0" padding="0"
ml={ 4 } mr={ 5 }
w="auto" w="auto"
> >
<Button <Button
...@@ -53,36 +48,47 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props ...@@ -53,36 +48,47 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
</Button> </Button>
<AccordionIcon color="blue.600" width="30px"/> <AccordionIcon color="blue.600" width="30px"/>
</AccordionButton> </AccordionButton>
</Flex> <Address flexGrow={ 1 }>
<Flex rowGap={ 2 } flexDir="column" fontSize="sm"> <AddressIcon hash={ address }/>
<Text fontWeight={ 600 }>Miner</Text> <AddressLink hash={ address } ml={ 2 }/>
<Link>{ miner }</Link> </Address>
</Flex>
<Flex rowGap={ 2 } flexDir="column" fontSize="sm">
<Text fontWeight={ 600 }>Before</Text>
<Flex>
<Text>{ before.balance } { appConfig.network.currency }</Text>
<TextSeparator/>
{ typeof before.nonce !== 'undefined' && <Text>Nonce:{ nbsp }{ before.nonce }</Text> }
</Flex>
</Flex>
<Flex rowGap={ 2 } flexDir="column" fontSize="sm">
<Text fontWeight={ 600 }>After</Text>
<Text>{ after.balance } { appConfig.network.currency }</Text>
{ typeof after.nonce !== 'undefined' && <Text>Nonce:{ nbsp }{ after.nonce }</Text> }
</Flex>
<Flex rowGap={ 2 } flexDir="column" fontSize="sm">
<Text fontWeight={ 600 }>State difference</Text>
<Stat>
{ diff } { appConfig.network.currency }
<StatArrow ml={ 2 } type={ Number(diff) > 0 ? 'increase' : 'decrease' }/>
</Stat>
</Flex> </Flex>
{ hasStorageData && ( { hasStorageData && (
<AccordionPanel fontWeight={ 500 } p={ 0 }> <AccordionPanel fontWeight={ 500 } p={ 0 }>
{ storage?.map((storageItem, index) => <TxStateStorageItem key={ index } storageItem={ storageItem }/>) } { storage?.map((storageItem, index) => <TxStateStorageItem key={ index } storageItem={ storageItem }/>) }
</AccordionPanel> </AccordionPanel>
) } ) }
<Flex rowGap={ 2 } flexDir="column" fontSize="sm" whiteSpace="pre" fontWeight={ 500 }>
<Box>
<Text as="span">Miner </Text>
<Link>{ miner }</Link>
</Box>
<Box>
<Text as="span">Before { appConfig.network.currency } </Text>
<Text as="span" variant="secondary">{ before.balance }</Text>
</Box>
{ typeof before.nonce !== 'undefined' && (
<Box>
<Text as="span">Nonce:</Text>
<Text as="span" fontWeight={ 600 }>{ nbsp }{ before.nonce }</Text>
</Box>
) }
<Box>
<Text as="span">After { appConfig.network.currency } </Text>
<Text as="span" variant="secondary">{ after.balance }</Text>
</Box>
{ typeof after.nonce !== 'undefined' && (
<Box>
<Text as="span">Nonce:</Text>
<Text as="span" fontWeight={ 600 }>{ nbsp }{ after.nonce }</Text>
</Box>
) }
<Text>State difference { appConfig.network.currency }</Text>
<Stat>
{ diff }
<StatArrow ml={ 2 } type={ Number(diff) > 0 ? 'increase' : 'decrease' }/>
</Stat>
</Flex>
</> </>
) } ) }
</AccordionItem> </AccordionItem>
......
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
import React from 'react'; import React from 'react';
import type { TTxStateItemStorage } from 'data/txState'; import type { TTxStateItemStorage } from 'data/txState';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage}) => { const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage}) => {
const gridData = [ const gridData = [
...@@ -21,20 +22,27 @@ const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage}) ...@@ -21,20 +22,27 @@ const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage})
const OPTIONS = [ 'Hex', 'Number', 'Text', 'Address' ]; const OPTIONS = [ 'Hex', 'Number', 'Text', 'Address' ];
return ( return (
<Grid <Grid
gridTemplateColumns={{ base: '70px minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} gridTemplateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}
columnGap={ 3 } columnGap={ 3 }
rowGap={ 4 } rowGap={{ base: 2.5, lg: 4 }}
px={ 6 } px={{ base: 3, lg: 6 }}
py={ 4 } py={{ base: 3, lg: 4 }}
background="blackAlpha.50" backgroundColor={ useColorModeValue('blackAlpha.50', 'whiteAlpha.100') }
borderRadius="12px" borderRadius="12px"
mb={ 4 } mb={ 4 }
fontSize="sm" fontSize="sm"
> >
{ gridData.map((item) => ( { gridData.map((item) => (
<React.Fragment key={ item.name }> <React.Fragment key={ item.name }>
<GridItem alignSelf={{ base: 'start', lg: 'center' }} fontWeight={{ base: 500, lg: 600 }} textAlign="end">{ item.name }</GridItem> <GridItem
<GridItem display="flex" flexDir={{ base: 'column', lg: 'row' }} rowGap={ 2 } alignItems={{ base: 'flex-start', lg: 'center' }} > alignSelf="center"
fontWeight={ 600 }
textAlign={{ base: 'start', lg: 'end' }}
_notFirst={{ mt: { base: 0.5, lg: 0 } }}
>
{ item.name }
</GridItem>
<GridItem display="flex" flexDir="row" columnGap={ 3 } alignItems="center" >
{ item.select && ( { item.select && (
<Select <Select
size="sm" size="sm"
...@@ -42,14 +50,14 @@ const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage}) ...@@ -42,14 +50,14 @@ const TxStateStorageItem = ({ storageItem }: {storageItem: TTxStateItemStorage})
focusBorderColor="none" focusBorderColor="none"
display="inline-block" display="inline-block"
w="auto" w="auto"
mr={ 3 } flexShrink={ 0 }
background={ backgroundColor } background={ backgroundColor }
> >
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) } { OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
</Select> </Select>
) } ) }
<Box fontWeight={{ base: 400, lg: 500 }} maxW="100%"> <Box fontWeight={ 500 } whiteSpace="nowrap" overflow="hidden">
{ item.value } <HashStringShortenDynamic fontWeight="500" hash={ item.value }/>
</Box> </Box>
</GridItem> </GridItem>
</React.Fragment> </React.Fragment>
......
...@@ -15,7 +15,7 @@ import TxStateTableItem from 'ui/tx/state/TxStateTableItem'; ...@@ -15,7 +15,7 @@ import TxStateTableItem from 'ui/tx/state/TxStateTableItem';
const TxStateTable = () => { const TxStateTable = () => {
return ( return (
<TableContainer width="100%" mt={ 6 }> <TableContainer width="100%" mt={ 6 }>
<Table variant="simple" minWidth="950px" size="sm"> <Table variant="simple" minWidth="950px" size="sm" w="auto">
<Thead> <Thead>
<Tr> <Tr>
<Th width="92px">Storage</Th> <Th width="92px">Storage</Th>
......
...@@ -5,6 +5,7 @@ import React from 'react'; ...@@ -5,6 +5,7 @@ import React from 'react';
import type ArrayElement from 'types/utils/ArrayElement'; import type ArrayElement from 'types/utils/ArrayElement';
import type { txs } from 'data/txs'; import type { txs } from 'data/txs';
import { nbsp } from 'lib/html-entities';
import useLink from 'lib/link/useLink'; import useLink from 'lib/link/useLink';
import TextSeparator from 'ui/shared/TextSeparator'; import TextSeparator from 'ui/shared/TextSeparator';
import Utilization from 'ui/shared/Utilization'; import Utilization from 'ui/shared/Utilization';
...@@ -21,6 +22,7 @@ const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => { ...@@ -21,6 +22,7 @@ const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => {
color: 'gray.500', color: 'gray.500',
fontWeight: 600, fontWeight: 600,
marginBottom: 3, marginBottom: 3,
fontSize: 'sm',
}; };
const link = useLink(); const link = useLink();
...@@ -31,7 +33,7 @@ const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => { ...@@ -31,7 +33,7 @@ const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => {
<Box { ...sectionProps } mb={ 4 }> <Box { ...sectionProps } mb={ 4 }>
<Text { ...sectionTitleProps }>Transaction fee</Text> <Text { ...sectionTitleProps }>Transaction fee</Text>
<Flex> <Flex>
<Text>{ tx.fee.value } { appConfig.network.currency }</Text> <Text>{ tx.fee.value }{ nbsp }{ appConfig.network.currency }</Text>
<Text variant="secondary" ml={ 1 }>(${ tx.fee.value_usd.toFixed(2) })</Text> <Text variant="secondary" ml={ 1 }>(${ tx.fee.value_usd.toFixed(2) })</Text>
</Flex> </Flex>
</Box> </Box>
...@@ -64,7 +66,7 @@ const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => { ...@@ -64,7 +66,7 @@ const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => {
<Box> <Box>
<Text as="span" fontWeight="500">Txn type: </Text> <Text as="span" fontWeight="500">Txn type: </Text>
<Text fontWeight="600" as="span">{ tx.type.value }</Text> <Text fontWeight="600" as="span">{ tx.type.value }</Text>
<Text fontWeight="400" as="span" ml={ 1 }>({ tx.type.eip })</Text> <Text fontWeight="400" as="span" ml={ 1 } color="gray.500">({ tx.type.eip })</Text>
</Box> </Box>
<Box> <Box>
<Text as="span" fontWeight="500">Nonce: </Text> <Text as="span" fontWeight="500">Nonce: </Text>
...@@ -75,7 +77,7 @@ const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => { ...@@ -75,7 +77,7 @@ const TxAdditionalInfo = ({ tx }: { tx: ArrayElement<typeof txs> }) => {
<Text fontWeight="600" as="span">{ tx.position }</Text> <Text fontWeight="600" as="span">{ tx.position }</Text>
</Box> </Box>
</Box> </Box>
<Link href={ link('tx_index', { id: tx.hash }) }>More details</Link> <Link fontSize="sm" href={ link('tx_index', { id: tx.hash }) }>More details</Link>
</> </>
); );
}; };
......
...@@ -13,7 +13,7 @@ const TxAdditionalInfoButton = ({ isOpen, onClick }: {isOpen?: boolean; onClick? ...@@ -13,7 +13,7 @@ const TxAdditionalInfoButton = ({ isOpen, onClick }: {isOpen?: boolean; onClick?
const infoColor = useColorModeValue('blue.600', 'blue.300'); const infoColor = useColorModeValue('blue.600', 'blue.300');
return ( return (
<Center ref={ ref } background={ isOpen ? infoBgColor : 'unset' } borderRadius="8px" w="30px" h="30px" onClick={ onClick }> <Center ref={ ref } background={ isOpen ? infoBgColor : 'unset' } borderRadius="8px" w="24px" h="24px" onClick={ onClick } cursor="pointer">
<Icon <Icon
as={ infoIcon } as={ infoIcon }
boxSize={ 5 } boxSize={ 5 }
......
...@@ -62,7 +62,7 @@ const TxsListItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => { ...@@ -62,7 +62,7 @@ const TxsListItem = ({ tx }: {tx: ArrayElement<typeof txs>}) => {
/> />
</Address> </Address>
</Flex> </Flex>
<Text variant="secondary" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text> <Text variant="secondary" fontWeight="400" fontSize="sm">{ dayjs(tx.timestamp).fromNow() }</Text>
</Flex> </Flex>
<Flex mt={ 3 }> <Flex mt={ 3 }>
<Text as="span" whiteSpace="pre">Method </Text> <Text as="span" whiteSpace="pre">Method </Text>
......
...@@ -26,9 +26,9 @@ const TxsTable = ({ txs, sort, sorting }: Props) => { ...@@ -26,9 +26,9 @@ const TxsTable = ({ txs, sort, sorting }: Props) => {
<Th width="18%">Txn hash</Th> <Th width="18%">Txn hash</Th>
<Th width="15%">Method</Th> <Th width="15%">Method</Th>
<Th width="11%">Block</Th> <Th width="11%">Block</Th>
<Th width={{ xl: '128px', base: '58px' }}>From</Th> <Th width={{ xl: '128px', base: '66px' }}>From</Th>
<Th width={{ xl: '36px', base: '0' }}></Th> <Th width={{ xl: '36px', base: '0' }}></Th>
<Th width={{ xl: '128px', base: '58px' }}>To</Th> <Th width={{ xl: '128px', base: '66px' }}>To</Th>
<Th width="18%" isNumeric> <Th width="18%" isNumeric>
<Link onClick={ sort('val') } display="flex" justifyContent="end"> <Link onClick={ sort('val') } display="flex" justifyContent="end">
{ sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> } { sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
......
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