Commit 10d5794c authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge branch 'main' into tab-routing

parents 3a5184ea 11eb4feb
__snapshots__/** filter=lfs diff=lfs merge=lfs -text
name: playwright
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.27.0-focal
steps:
- run: apt-get update && apt-get install git-lfs
- uses: actions/checkout@v3
with:
lfs: 'true'
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: yarn
- name: Run your tests
run: HOME=/root yarn test-ct
- name: Upload test results
if: always()
uses: actions/upload-artifact@v2
with:
name: playwright-report
path: playwright-report
\ No newline at end of file
...@@ -28,6 +28,7 @@ yarn-error.log* ...@@ -28,6 +28,7 @@ yarn-error.log*
# local env files # local env files
.env*.local .env*.local
/configs/envs/.env.secrets
# vercel # vercel
.vercel .vercel
...@@ -39,3 +40,6 @@ yarn-error.log* ...@@ -39,3 +40,6 @@ yarn-error.log*
# Sentry # Sentry
.sentryclirc .sentryclirc
/test-results/
/playwright-report/
/playwright/.cache/
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-checkout'.\n"; exit 2; }
git lfs post-checkout "$@"
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-commit'.\n"; exit 2; }
git lfs post-commit "$@"
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/post-merge'.\n"; exit 2; }
git lfs post-merge "$@"
#!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting '.git/hooks/pre-push'.\n"; exit 2; }
git lfs pre-push "$@"
{
"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
...@@ -9,6 +9,7 @@ Core technologies what used in the project are ...@@ -9,6 +9,7 @@ Core technologies what used in the project are
- [Next.js](https://nextjs.org/) as application framework - [Next.js](https://nextjs.org/) as application framework
- [Chakra](https://chakra-ui.com/) as component library; our theme customization could be found in `/theme` folder - [Chakra](https://chakra-ui.com/) as component library; our theme customization could be found in `/theme` folder
- [css-modules](https://github.com/css-modules/css-modules) as lib for styling custom components - [css-modules](https://github.com/css-modules/css-modules) as lib for styling custom components
- [playwright](https://playwright.dev/) as a tool for components visual testing
And of course our premier language is [Typescript](https://www.typescriptlang.org/) And of course our premier language is [Typescript](https://www.typescriptlang.org/)
...@@ -21,6 +22,11 @@ For local development please follow next steps: ...@@ -21,6 +22,11 @@ For local development please follow next steps:
- create local env file `.env.local` according to `.env.example` snapshot (see list of used environment variables [below](#environment-variables)) - create local env file `.env.local` according to `.env.example` snapshot (see list of used environment variables [below](#environment-variables))
- run `yarn dev` to spin up local dev server and navigate to the host from logs output - run `yarn dev` to spin up local dev server and navigate to the host from logs output
## Components visual testing
We use [playwright experimental components testing](https://playwright.dev/docs/test-components) for visual (screenshots) CI check. Test renders a single component in headless browser in docker, generates screenshots and then compares this screenshot with a reference one.
To perform testing locally you need to install docker and run `yarn test-docker`
## Environment variables ## Environment variables
### Variables list ### Variables list
The app instance could be customized by passing following variables to NodeJS environment. The app instance could be customized by passing following variables to NodeJS environment.
......
[ [
{ {
"chainId": 1, "chainIds": [
1
],
"author": "xDaichain", "author": "xDaichain",
"id": "easy-staking", "id": "easy-staking",
"title": "Easy Staking Revoke.cash", "title": "Easy Staking Revoke.cash",
...@@ -18,7 +20,9 @@ ...@@ -18,7 +20,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 99, "chainIds": [
99
],
"author": "xDaichain", "author": "xDaichain",
"id": "curve", "id": "curve",
"title": "Curve Revoke.cash", "title": "Curve Revoke.cash",
...@@ -36,7 +40,9 @@ ...@@ -36,7 +40,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 1, "chainIds": [
1
],
"author": "xDaichain", "author": "xDaichain",
"id": "honwyswap", "id": "honwyswap",
"title": "HonwySwap Revoke.cash", "title": "HonwySwap Revoke.cash",
...@@ -54,7 +60,9 @@ ...@@ -54,7 +60,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 99, "chainIds": [
99
],
"author": "xDaichain", "author": "xDaichain",
"id": "sushi", "id": "sushi",
"title": "Sushi Revoke.cash", "title": "Sushi Revoke.cash",
...@@ -72,7 +80,9 @@ ...@@ -72,7 +80,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 1, "chainIds": [
100
],
"author": "xDaichain", "author": "xDaichain",
"id": "bao-finance", "id": "bao-finance",
"title": "Bao Finance", "title": "Bao Finance",
...@@ -90,7 +100,9 @@ ...@@ -90,7 +100,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 99, "chainIds": [
99
],
"author": "xDaichain", "author": "xDaichain",
"id": "component", "id": "component",
"title": "Component", "title": "Component",
...@@ -108,7 +120,9 @@ ...@@ -108,7 +120,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 1, "chainIds": [
1
],
"author": "xDaichain", "author": "xDaichain",
"id": "pooltogether", "id": "pooltogether",
"title": "PoolTogether", "title": "PoolTogether",
...@@ -126,7 +140,9 @@ ...@@ -126,7 +140,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 99, "chainIds": [
99
],
"author": "xDaichain", "author": "xDaichain",
"id": "swapr", "id": "swapr",
"title": "Swapr", "title": "Swapr",
...@@ -144,7 +160,9 @@ ...@@ -144,7 +160,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 1, "chainIds": [
1
],
"author": "xDaichain", "author": "xDaichain",
"id": "levinswap", "id": "levinswap",
"title": "Levinswap", "title": "Levinswap",
...@@ -162,7 +180,9 @@ ...@@ -162,7 +180,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 99, "chainIds": [
99
],
"author": "xDaichain", "author": "xDaichain",
"id": "omen", "id": "omen",
"title": "Omen", "title": "Omen",
...@@ -180,7 +200,9 @@ ...@@ -180,7 +200,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 1, "chainIds": [
1
],
"author": "xDaichain", "author": "xDaichain",
"id": "nifty-ink", "id": "nifty-ink",
"title": "Nifty Ink", "title": "Nifty Ink",
...@@ -198,7 +220,9 @@ ...@@ -198,7 +220,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 99, "chainIds": [
99
],
"author": "xDaichain", "author": "xDaichain",
"id": "treasure-chess", "id": "treasure-chess",
"title": "Treasure Chess", "title": "Treasure Chess",
...@@ -216,7 +240,9 @@ ...@@ -216,7 +240,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 1, "chainIds": [
1
],
"author": "xDaichain", "author": "xDaichain",
"id": "unique-one", "id": "unique-one",
"title": "Unique.One", "title": "Unique.One",
...@@ -234,7 +260,9 @@ ...@@ -234,7 +260,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 99, "chainIds": [
99
],
"author": "xDaichain", "author": "xDaichain",
"id": "cold-truth-culture", "id": "cold-truth-culture",
"title": "Cold Truth Culture", "title": "Cold Truth Culture",
...@@ -252,7 +280,9 @@ ...@@ -252,7 +280,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 1, "chainIds": [
1
],
"author": "xDaichain", "author": "xDaichain",
"id": "xdai-bridge", "id": "xdai-bridge",
"title": "xDai Bridge", "title": "xDai Bridge",
...@@ -270,7 +300,9 @@ ...@@ -270,7 +300,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 99, "chainIds": [
99
],
"author": "xDaichain", "author": "xDaichain",
"id": "omni-bridge", "id": "omni-bridge",
"title": "OmniBridge", "title": "OmniBridge",
...@@ -288,7 +320,9 @@ ...@@ -288,7 +320,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 1, "chainIds": [
1
],
"author": "xDaichain", "author": "xDaichain",
"id": "gnosis-safe", "id": "gnosis-safe",
"title": "Gnosis Safe", "title": "Gnosis Safe",
...@@ -306,7 +340,9 @@ ...@@ -306,7 +340,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 99, "chainIds": [
99
],
"author": "xDaichain", "author": "xDaichain",
"id": "multisender", "id": "multisender",
"title": "Multisender", "title": "Multisender",
...@@ -324,7 +360,9 @@ ...@@ -324,7 +360,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 1, "chainIds": [
1
],
"author": "xDaichain", "author": "xDaichain",
"id": "disperse", "id": "disperse",
"title": "Disperse", "title": "Disperse",
...@@ -342,7 +380,9 @@ ...@@ -342,7 +380,9 @@
"github": "https://github.com/mikhin" "github": "https://github.com/mikhin"
}, },
{ {
"chainId": 99, "chainIds": [
99
],
"author": "xDaichain", "author": "xDaichain",
"id": "symmetric", "id": "symmetric",
"title": "Symmetric", "title": "Symmetric",
......
/* 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,
...@@ -52,4 +52,4 @@ export const tx = { ...@@ -52,4 +52,4 @@ export const tx = {
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: {
......
import BigNumber from 'bignumber.js';
export const WEI = new BigNumber(10 ** 18);
export const GWEI = new BigNumber(10 ** 9);
export const WEI_IN_GWEI = WEI.dividedBy(GWEI);
...@@ -22,7 +22,7 @@ export default function useNavItems() { ...@@ -22,7 +22,7 @@ export default function useNavItems() {
const selectedNetwork = useNetwork(); const selectedNetwork = useNetwork();
const isMarketplaceFilled = useMemo(() => const isMarketplaceFilled = useMemo(() =>
marketplaceApps.filter(item => item.chainId === selectedNetwork?.chainId), marketplaceApps.filter(item => item.chainIds.includes(selectedNetwork?.chainId)),
[ selectedNetwork?.chainId ]) [ selectedNetwork?.chainId ])
.length > 0; .length > 0;
......
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`;
}
...@@ -13,7 +13,9 @@ ...@@ -13,7 +13,9 @@
"start": "next start", "start": "next start",
"lint:eslint": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx", "lint:eslint": "./node_modules/.bin/eslint . --ext .js,.jsx,.ts,.tsx",
"prepare": "husky install", "prepare": "husky install",
"format-svg": "./node_modules/.bin/svgo -r ./icons" "format-svg": "./node_modules/.bin/svgo -r ./icons",
"test-ct": "playwright test -c playwright-ct.config.ts",
"test-docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.27.0-focal ./run-tests.sh"
}, },
"dependencies": { "dependencies": {
"@chakra-ui/react": "2.3.1", "@chakra-ui/react": "2.3.1",
...@@ -24,7 +26,9 @@ ...@@ -24,7 +26,9 @@
"@tanstack/react-query": "^4.0.10", "@tanstack/react-query": "^4.0.10",
"@tanstack/react-query-devtools": "^4.0.10", "@tanstack/react-query-devtools": "^4.0.10",
"@types/react-scroll": "^1.8.4", "@types/react-scroll": "^1.8.4",
"bignumber.js": "^9.1.0",
"dayjs": "^1.11.5", "dayjs": "^1.11.5",
"ethers": "^5.7.1",
"framer-motion": "^6", "framer-motion": "^6",
"lodash": "^4.0.0", "lodash": "^4.0.0",
"next": "12.2.5", "next": "12.2.5",
...@@ -40,6 +44,7 @@ ...@@ -40,6 +44,7 @@
"use-font-face-observer": "^1.2.1" "use-font-face-observer": "^1.2.1"
}, },
"devDependencies": { "devDependencies": {
"@playwright/experimental-ct-react": "^1.26.1",
"@types/node": "17.0.36", "@types/node": "17.0.36",
"@types/react": "18.0.9", "@types/react": "18.0.9",
"@types/react-dom": "18.0.5", "@types/react-dom": "18.0.5",
...@@ -51,6 +56,7 @@ ...@@ -51,6 +56,7 @@
"eslint-plugin-regexp": "^1.7.0", "eslint-plugin-regexp": "^1.7.0",
"husky": "^8.0.0", "husky": "^8.0.0",
"lint-staged": ">=10", "lint-staged": ">=10",
"playwright": "^1.26.1",
"svgo": "^2.8.0", "svgo": "^2.8.0",
"typescript": "4.7.2" "typescript": "4.7.2"
}, },
......
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 type { PlaywrightTestConfig } from '@playwright/experimental-ct-react';
import { devices } from '@playwright/experimental-ct-react';
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: './',
testMatch: /.*\.pw\.tsx/,
/* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */
snapshotDir: './__snapshots__',
/* Maximum time one test can run for. */
timeout: 10 * 1000,
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: Boolean(process.env.CI),
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
/* Port to use for Playwright component endpoint. */
ctPort: 3100,
headless: true,
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
],
};
export default config;
import { ChakraProvider } from '@chakra-ui/react';
import type { ColorMode } from '@chakra-ui/react';
import React from 'react';
import theme from '../theme';
type Props = {
children: React.ReactNode;
colorMode?: ColorMode;
}
const RenderWithChakra = ({ children, colorMode = 'light' }: Props) => {
return (
<ChakraProvider theme={{ ...theme, config: { ...theme.config, initialColorMode: colorMode } }}>
{ children }
</ChakraProvider>
);
};
export default RenderWithChakra;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Testing Page</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/playwright/index.ts"></script>
</body>
</html>
// Import styles, initialize component theme here.
// import '../src/common.css';
export {};
#!/bin/sh
yarn install
yarn test-ct
\ No newline at end of file
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': {
return {
...baseStyles,
padding: '16px 24px 2px 24px',
};
}
}
};
const getDefaultLabelStyles = (size: 'md' | 'lg') => {
switch (size) {
case 'md': {
return {
fontSize: 'md',
lineHeight: '20px',
padding: '18px 16px',
right: '18px',
};
}
case 'lg': {
return {
fontSize: 'md',
lineHeight: '24px',
padding: '28px 24px',
right: '26px',
};
}
}
};
const getPaddingX = (size: 'md' | 'lg') => {
switch (size) {
case 'md': {
return '16px';
}
case 'lg': { const activeLabelStyles = {
return '24px'; ...FormLabel.variants?.floating?.(props)._focusWithin,
...FormLabel.sizes?.[size](props)._focusWithin,
} || {};
const activeInputStyles = (() => {
switch (size) {
case 'md': {
return {
paddingTop: '26px',
paddingBottom: '10px',
};
}
case 'lg': {
return {
paddingTop: '38px',
paddingBottom: '18px',
};
}
} }
} })();
};
const getActiveInputStyles = (size: 'md' | 'lg') => { const inputPx = (() => {
switch (size) { switch (size) {
case 'md': { case 'md': {
return { return '16px';
paddingTop: '26px', }
paddingBottom: '10px',
};
}
case 'lg': { case 'lg': {
return { return '24px';
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,
......
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;
};
}
...@@ -8,4 +8,5 @@ export interface DecodedInputParams { ...@@ -8,4 +8,5 @@ export interface DecodedInputParams {
name: string; name: string;
type: string; type: string;
value: string; value: string;
indexed?: boolean;
} }
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
export type TxInternalsType = 'call' | 'delegatecall' | 'staticcall' | 'create' | 'create2' | 'selfdestruct' | 'reward'
export interface InternalTransaction { export interface InternalTransaction {
error?: string; error: string | null;
success: boolean; success: boolean;
type: string; type: TxInternalsType;
transaction_hash: string; transaction_hash: string;
from: AddressParam; from: AddressParam;
to: AddressParam; to: AddressParam;
......
...@@ -6,7 +6,7 @@ export interface Log { ...@@ -6,7 +6,7 @@ export interface Log {
topics: Array<string>; topics: Array<string>;
data: string; data: string;
index: number; index: number;
decoded?: DecodedInput; decoded: DecodedInput | null;
} }
export interface LogsResponse { export interface LogsResponse {
......
...@@ -11,7 +11,7 @@ export interface RawTrace { ...@@ -11,7 +11,7 @@ export interface RawTrace {
gasUsed: string; gasUsed: string;
output: string; output: string;
}; };
error?: string; error: string | null;
subtraces: number; subtraces: number;
traceAddress: Array<number>; traceAddress: Array<number>;
type: string; type: string;
......
export interface Reward {
reward: number;
type: 'Miner Reward' | 'Emission Reward' | 'Chore Reward' | 'Uncle Reward';
}
...@@ -11,5 +11,5 @@ export interface TokenTransfer { ...@@ -11,5 +11,5 @@ export interface TokenTransfer {
total: { total: {
value: string; value: string;
}; };
exchangeRate: number; 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;
......
...@@ -7,7 +7,7 @@ export interface Transaction { ...@@ -7,7 +7,7 @@ export interface Transaction {
hash: string; hash: string;
result: string; result: string;
confirmations: number; confirmations: number;
status: string; status: 'ok' | 'error' | null;
block: number; block: number;
timestamp: string; timestamp: string;
confirmation_duration: Array<number>; confirmation_duration: Array<number>;
...@@ -18,24 +18,24 @@ export interface Transaction { ...@@ -18,24 +18,24 @@ export interface Transaction {
fee: Fee; fee: Fee;
gas_price: number; gas_price: number;
type: number; type: number;
gas_used: number; gas_used: string;
gas_limit: number; gas_limit: string;
max_fee_per_gas?: number; max_fee_per_gas: number | null;
max_priority_fee_per_gas?: number; max_priority_fee_per_gas: number | null;
priority_fee?: number; priority_fee: number | null;
base_fee_per_gas?: number; base_fee_per_gas: number | null;
tx_burnt_fee?: number; tx_burnt_fee: number | null;
nonce: number; nonce: number;
position: number; position: number;
revert_reason?: { revert_reason: {
raw: string; raw: string;
decoded: string; decoded: string;
}; } | null;
raw_input: string; raw_input: string;
decoded_input?: DecodedInput; decoded_input: DecodedInput | null;
token_transfers?: Array<TokenTransfer>; token_transfers: Array<TokenTransfer> | null;
token_transfers_overflow: boolean; token_transfers_overflow: boolean;
exchange_rate: number; exchange_rate: string;
} }
export interface TransactionsResponse { export interface TransactionsResponse {
......
export type TxInternalsType = 'call' | 'delegate_call' | 'static_call' | 'create' | 'create2' | 'self_destruct' | 'reward'
export enum MarketplaceCategoryNames { export enum MarketplaceCategoryId {
'all',
'favorites',
'defi', 'defi',
'exchanges', 'exchanges',
'finance', 'finance',
...@@ -11,16 +13,20 @@ export enum MarketplaceCategoryNames { ...@@ -11,16 +13,20 @@ export enum MarketplaceCategoryNames {
'yieldFarming', '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<keyof typeof MarketplaceCategoryNames>; categories: Array<MarketplaceCategoriesIds>;
} }
export type AppItemOverview = AppItemPreview & { export type AppItemOverview = AppItemPreview & {
chainId: number; chainIds: Array<number>;
author: string; author: string;
url: string; url: string;
description: string; description: string;
......
...@@ -28,7 +28,6 @@ const AppCard = ({ id, ...@@ -28,7 +28,6 @@ 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 => APP_CATEGORIES[c]).filter(notEmpty).join(', ');
const handleInfoClick = useCallback((event: MouseEvent) => { const handleInfoClick = useCallback((event: MouseEvent) => {
...@@ -55,6 +54,7 @@ const AppCard = ({ id, ...@@ -55,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' }}
...@@ -71,6 +71,7 @@ const AppCard = ({ id, ...@@ -71,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` }
/> />
...@@ -110,9 +111,8 @@ const AppCard = ({ id, ...@@ -110,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' }}
...@@ -134,9 +134,11 @@ const AppCard = ({ id, ...@@ -134,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 { 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 }
/> />
) } ) }
</> </>
......
...@@ -5,7 +5,7 @@ import { AppCardSkeleton } from 'ui/apps/AppCardSkeleton'; ...@@ -5,7 +5,7 @@ import { AppCardSkeleton } from 'ui/apps/AppCardSkeleton';
const applicationStubs = [ ...Array(12) ]; const applicationStubs = [ ...Array(12) ];
export const AppListSkeleton = () => { const AppListSkeleton = () => {
return ( return (
<Grid <Grid
templateColumns={{ templateColumns={{
...@@ -25,3 +25,5 @@ export const AppListSkeleton = () => { ...@@ -25,3 +25,5 @@ export const AppListSkeleton = () => {
</Grid> </Grid>
); );
}; };
export default AppListSkeleton;
...@@ -5,7 +5,7 @@ import { ...@@ -5,7 +5,7 @@ import {
import NextLink from 'next/link'; import NextLink from 'next/link';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import type { AppItemOverview, MarketplaceCategoryNames } from 'types/client/apps'; import type { AppItemOverview, MarketplaceCategoriesIds } from 'types/client/apps';
import marketplaceApps from 'data/marketplaceApps.json'; import marketplaceApps from 'data/marketplaceApps.json';
import linkIcon from 'icons/link.svg'; import linkIcon from 'icons/link.svg';
...@@ -100,9 +100,6 @@ const AppModal = ({ ...@@ -100,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>
...@@ -161,7 +158,7 @@ const AppModal = ({ ...@@ -161,7 +158,7 @@ const AppModal = ({
</Heading> </Heading>
<Box marginBottom={ 2 }> <Box marginBottom={ 2 }>
{ categories.map((category: keyof typeof MarketplaceCategoryNames) => APP_CATEGORIES[category] && ( { categories.map((category: MarketplaceCategoriesIds) => APP_CATEGORIES[category] && (
<Tag <Tag
colorScheme="blue" colorScheme="blue"
marginRight={ 2 } marginRight={ 2 }
......
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 { MarketplaceCategoryNames } from 'types/client/apps'; import type { MarketplaceCategoriesIds } from 'types/client/apps';
export const APP_CATEGORIES: {[key in keyof typeof MarketplaceCategoryNames]: string} = { export const APP_CATEGORIES: {[key in MarketplaceCategoriesIds]: string} = {
favorites: 'Favorites',
all: 'All apps',
defi: 'DeFi', defi: 'DeFi',
exchanges: 'Exchanges', exchanges: 'Exchanges',
finance: 'Finance', finance: 'Finance',
......
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';
import useNetwork from 'lib/hooks/useNetwork';
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 selectedNetwork = useNetwork();
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(() => {
if (!selectedNetwork) {
return;
}
const defaultDisplayedApps = [ ...marketplaceApps ]
.filter(item => item.chainIds.includes(selectedNetwork?.chainId))
.sort((a, b) => a.title.localeCompare(b.title));
setDefaultAppList(defaultDisplayedApps);
setDisplayedApps(defaultDisplayedApps);
setIsLoading(false);
}, [ selectedNetwork ]);
return React.useMemo(() => ({
category,
handleCategoryChange,
debounceFilterApps,
isLoading,
displayedApps,
showAppInfo,
displayedAppId,
clearDisplayedAppId,
favoriteApps,
handleFavoriteClick,
}), [ category,
clearDisplayedAppId,
debounceFilterApps,
displayedAppId, displayedApps,
favoriteApps,
handleCategoryChange,
handleFavoriteClick,
isLoading,
showAppInfo,
]);
}
import { Icon, Link } from '@chakra-ui/react'; import { Box, Icon, Link } from '@chakra-ui/react';
import debounce from 'lodash/debounce'; import React from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps';
import marketplaceApps from 'data/marketplaceApps.json';
import PlusIcon from 'icons/plus.svg'; import PlusIcon from 'icons/plus.svg';
import useNetwork from 'lib/hooks/useNetwork';
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';
import { AppListSkeleton } from '../apps/AppListSkeleton'; import useMarketplaceApps from '../apps/useMarkeplaceApps';
const Apps = () => { const Apps = () => {
const selectedNetwork = useNetwork(); const {
const [ isLoading, setIsLoading ] = useState(true); isLoading,
const [ defaultAppList, setDefaultAppList ] = useState<Array<AppItemOverview>>(); category,
const [ displayedApps, setDisplayedApps ] = useState<Array<AppItemOverview>>([]); handleCategoryChange,
const [ displayedAppId, setDisplayedAppId ] = useState<string | null>(null); debounceFilterApps,
showAppInfo,
const showAppInfo = useCallback((id: string) => { displayedApps,
setDisplayedAppId(id); displayedAppId,
}, []); clearDisplayedAppId,
// eslint-disable-next-line react-hooks/exhaustive-deps favoriteApps,
const debounceFilterApps = useCallback(debounce(q => filterApps(q), 500), []); handleFavoriteClick,
const clearDisplayedAppId = useCallback(() => setDisplayedAppId(null), []); } = useMarketplaceApps();
function filterApps(q: string) {
const apps = defaultAppList
?.filter(app => app.title.toLowerCase().includes(q.toLowerCase()));
setDisplayedApps(apps || []);
}
useEffect(() => {
if (!selectedNetwork) {
return;
}
const defaultDisplayedApps = [ ...marketplaceApps ]
.filter(item => item.chainId === selectedNetwork?.chainId)
.sort((a, b) => a.title.localeCompare(b.title));
setDefaultAppList(defaultDisplayedApps);
setDisplayedApps(defaultDisplayedApps);
setIsLoading(false);
}, [ selectedNetwork ]);
return ( return (
<> <>
<FilterInput onChange={ debounceFilterApps } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/> <Box
display="flex"
flexDirection={{ base: 'column', sm: 'row' }}
>
<CategoriesMenu
selectedCategoryId={ category }
onSelect={ handleCategoryChange }
/>
<FilterInput onChange={ debounceFilterApps } marginBottom={{ base: '4', lg: '6' }} placeholder="Find app"/>
</Box>
{ isLoading ? <AppListSkeleton/> : ( { isLoading ? <AppListSkeleton/> : (
<AppList <AppList
...@@ -57,6 +43,8 @@ const Apps = () => { ...@@ -57,6 +43,8 @@ const Apps = () => {
onAppClick={ showAppInfo } onAppClick={ showAppInfo }
displayedAppId={ displayedAppId } displayedAppId={ displayedAppId }
onModalClose={ clearDisplayedAppId } onModalClose={ clearDisplayedAppId }
favoriteApps={ favoriteApps }
onFavoriteClick={ handleFavoriteClick }
/> />
) } ) }
......
...@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; ...@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
import type { AppItemOverview } from 'types/client/apps'; import type { AppItemOverview } from 'types/client/apps';
import useNetwork from 'lib/hooks/useNetwork'; import useNetwork from 'lib/hooks/useNetwork';
import ContentLoader from 'ui/shared/ContentLoader';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
type Props = { type Props = {
...@@ -24,13 +25,15 @@ const MarketplaceApp = ({ app, isLoading }: Props) => { ...@@ -24,13 +25,15 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
const sandboxAttributeValue = 'allow-forms allow-orientation-lock ' + const sandboxAttributeValue = 'allow-forms allow-orientation-lock ' +
'allow-pointer-lock allow-popups-to-escape-sandbox ' + 'allow-pointer-lock allow-popups-to-escape-sandbox ' +
'allow-same-origin allow-scripts ' + 'allow-same-origin allow-scripts ' +
'allow-top-navigation-by-user-activation'; 'allow-top-navigation-by-user-activation allow-popups';
const allowAttributeValue = 'clipboard-read; clipboard-write;';
useEffect(() => { useEffect(() => {
if (app) { if (app && !isFrameLoading) {
ref?.current?.contentWindow?.postMessage({ colorMode, chaindId: network?.chainId }, app.url); ref?.current?.contentWindow?.postMessage({ blockscoutColorMode: colorMode, blockscoutChainId: network?.chainId }, app.url);
} }
}, [ app, colorMode, network, ref ]); }, [ isFrameLoading, app, colorMode, network, ref ]);
return ( return (
<Page wrapChildren={ false }> <Page wrapChildren={ false }>
...@@ -40,16 +43,12 @@ const MarketplaceApp = ({ app, isLoading }: Props) => { ...@@ -40,16 +43,12 @@ const MarketplaceApp = ({ app, isLoading }: Props) => {
paddingTop={{ base: '138px', lg: 0 }} paddingTop={{ base: '138px', lg: 0 }}
> >
{ (isFrameLoading) && ( { (isFrameLoading) && (
<Center <ContentLoader/>
h="100%"
w="100%"
>
Loading...
</Center>
) } ) }
{ app && ( { app && (
<Box <Box
allow={ allowAttributeValue }
ref={ ref } ref={ ref }
sandbox={ sandboxAttributeValue } sandbox={ sandboxAttributeValue }
as="iframe" as="iframe"
......
...@@ -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> = [
{ id: 'index', title: 'Details', component: <TxDetails/> }, { id: 'index', title: 'Details', component: <TxDetails/> },
{ id: 'internal', title: 'Internal txn', component: <TxInternals/> }, { id: 'internal', title: 'Internal txn', component: <TxInternals/> },
{ id: 'logs', title: 'Logs', component: <TxLogs/> }, { id: 'logs', title: 'Logs', component: <TxLogs/> },
{ id: 'state', title: 'State', component: <TxState/> }, // will be implemented later, api is not ready
// { id: 'state', title: 'State', component: <TxState/> },
{ id: 'raw_trace', title: 'Raw trace', component: <TxRawTrace/> }, { id: 'raw_trace', title: 'Raw trace', component: <TxRawTrace/> },
]; ];
......
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 BigNumber from 'bignumber.js';
import React from 'react';
import { WEI, GWEI } from 'lib/consts';
interface Props {
value: string;
unit?: 'wei' | 'gwei' | 'ether';
currency?: string;
exchangeRate?: string;
className?: string;
accuracy?: number;
accuracyUsd?: number;
}
const CurrencyValue = ({ value, currency = '', unit = 'wei', exchangeRate, className, accuracy, accuracyUsd }: Props) => {
let unitBn: BigNumber.Value;
switch (unit) {
case 'wei':
unitBn = WEI;
break;
case 'gwei':
unitBn = GWEI;
break;
default:
unitBn = new BigNumber(1);
}
const valueBn = new BigNumber(value);
const valueCurr = valueBn.dividedBy(unitBn);
const exchangeRateBn = new BigNumber(exchangeRate || 0);
const usdBn = valueCurr.times(exchangeRateBn);
return (
<Box as="span" className={ className }>
<Text as="span">
{ accuracy ? valueCurr.toFixed(accuracy) : valueCurr.toFixed() }{ currency ? ` ${ currency }` : '' }
</Text>
{ exchangeRate !== undefined && exchangeRate !== null &&
// TODO: mb need to implement rounding to the first significant digit
<Text as="span" variant="secondary" whiteSpace="pre" fontWeight={ 400 }> (${ accuracyUsd ? usdBn.toFixed(accuracyUsd) : usdBn.toFixed() })</Text>
}
</Box>
);
};
export default React.memo(chakra(CurrencyValue));
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 { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import RenderWithChakra from '../../playwright/RenderWithChakra';
import Utilization from './Utilization';
test.use({ viewport: { width: 100, height: 50 } });
test('Utilization light', async({ mount }) => {
const component = await mount(
<RenderWithChakra>
<Utilization value={ 0.1 }/>
</RenderWithChakra>,
);
await expect(component).toHaveScreenshot();
});
test('Utilization dark', async({ mount }) => {
const component = await mount(
<RenderWithChakra colorMode="dark">
<Utilization value={ 0.1 }/>
</RenderWithChakra>,
);
await expect(component).toHaveScreenshot();
});
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,14 +41,16 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => { ...@@ -39,14 +41,16 @@ const TableRow = ({ isLast, name, type, children, indexed }: RowProps) => {
> >
{ type } { type }
</GridItem> </GridItem>
<GridItem { indexed !== undefined && (
pr={ GAP } <GridItem
pt={ GAP } pr={ GAP }
pb={ isLast ? PADDING : 0 } pt={ GAP }
bgColor={ bgColor } pb={ isLast ? PADDING : 0 }
> bgColor={ bgColor }
{ indexed ? 'true' : 'false' } >
</GridItem> { indexed ? 'true' : 'false' }
</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,15 +130,17 @@ const TxDecodedInputData = () => { ...@@ -112,15 +130,17 @@ const TxDecodedInputData = () => {
> >
Type Type
</GridItem> </GridItem>
<GridItem { hasIndexed && (
pr={ GAP } <GridItem
pt={ PADDING } pr={ GAP }
pb={ 1 } pt={ PADDING }
bgColor={ bgColor } pb={ 1 }
fontWeight={ 600 } bgColor={ bgColor }
> fontWeight={ 600 }
Inde<wbr/>xed? >
</GridItem> Inde<wbr/>xed?
</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> <Address justifyContent="space-between">
</TableRow> <AddressLink hash={ value }/>
<TableRow name="to" type="address" indexed> <CopyToClipboard text={ value }/>
<Address justifyContent="space-between"> </Address>
<AddressLink hash="0xcf0c50b7ea8af37d57380a0ac199d55b0782c718"/> ) : (
<CopyToClipboard text="0xcf0c50b7ea8af37d57380a0ac199d55b0782c718"/> <Flex alignItems="flex-start" justifyContent="space-between" whiteSpace="normal" wordBreak="break-all">
</Address> <Text>{ value }</Text>
</TableRow> <CopyToClipboard text={ value }/>
<TableRow name="tokenId" type="uint256" isLast> </Flex>
<Flex alignItems="center" justifyContent="space-between"> ) }
<Text>116842</Text> </TableRow>
<CopyToClipboard text="116842"/> );
</Flex> }) }
</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'; import type { Sort, SortField } from 'ui/tx/internals/utils';
...@@ -26,7 +30,7 @@ const getNextSortValue = (field: SortField) => (prevValue: Sort | undefined) => ...@@ -26,7 +30,7 @@ const getNextSortValue = (field: SortField) => (prevValue: Sort | undefined) =>
return sequence[nextIndex]; return sequence[nextIndex];
}; };
const sortFn = (sort: Sort | undefined) => (a: ArrayElement<typeof data>, b: ArrayElement<typeof data>) => { const sortFn = (sort: Sort | undefined) => (a: InternalTransaction, b: InternalTransaction) => {
switch (sort) { switch (sort) {
case 'value-desc': { case 'value-desc': {
const result = a.value > b.value ? -1 : 1; const result = a.value > b.value ? -1 : 1;
...@@ -38,22 +42,23 @@ const sortFn = (sort: Sort | undefined) => (a: ArrayElement<typeof data>, b: Arr ...@@ -38,22 +42,23 @@ const sortFn = (sort: Sort | undefined) => (a: ArrayElement<typeof data>, b: Arr
return a.value === b.value ? 0 : result; return a.value === b.value ? 0 : result;
} }
case 'gas-limit-desc': { // no gas limit in api yet
const result = a.gasLimit > b.gasLimit ? -1 : 1; // case 'gas-limit-desc': {
return a.gasLimit === b.gasLimit ? 0 : result; // const result = a.gasLimit > b.gasLimit ? -1 : 1;
} // return a.gasLimit === b.gasLimit ? 0 : result;
// }
case 'gas-limit-asc': { // case 'gas-limit-asc': {
const result = a.gasLimit > b.gasLimit ? 1 : -1; // const result = a.gasLimit > b.gasLimit ? 1 : -1;
return a.gasLimit === b.gasLimit ? 0 : result; // return a.gasLimit === b.gasLimit ? 0 : result;
} // }
default: default:
return 0; return 0;
} }
}; };
const searchFn = (searchTerm: string) => (item: ArrayElement<typeof data>): boolean => { 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) ||
...@@ -61,9 +66,19 @@ const searchFn = (searchTerm: string) => (item: ArrayElement<typeof data>): bool ...@@ -61,9 +66,19 @@ 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 [ 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();
...@@ -77,8 +92,25 @@ const TxInternals = () => { ...@@ -77,8 +92,25 @@ const TxInternals = () => {
}; };
}, []); }, []);
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)); .sort(sortFn(sort));
......
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', enabled: Boolean(router.query.id),
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: {
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 useNetwork from 'lib/hooks/useNetwork'; import useNetwork from 'lib/hooks/useNetwork';
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>}) => {
const selectedNetwork = useNetwork(); const selectedNetwork = useNetwork();
return ( return (
<Box mt={ 6 }> <Box mt={ 6 }>
{ data.map((item) => <TxInternalsListItem key={ item.id } { ...item } currency={ selectedNetwork?.currency }/>) } { data.map((item) => <TxInternalsListItem key={ item.transaction_hash } { ...item } currency={ selectedNetwork?.currency }/>) }
</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 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> & { currency?: string }; type Props = InternalTransaction & { currency?: string };
const TxInternalsListItem = ({ type, from, to, value, currency, success, error }: Props) => {
const typeTitle = TX_INTERNALS_ITEMS.find(({ id }) => id === type)?.title;
const TxInternalsListItem = ({ type, status, from, to, value, gasLimit, currency }: 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)">
...@@ -36,10 +37,11 @@ const TxInternalsListItem = ({ type, status, from, to, value, gasLimit, currency ...@@ -36,10 +37,11 @@ const TxInternalsListItem = ({ type, status, from, to, value, gasLimit, currency
<Text fontSize="sm" fontWeight={ 500 }>Value { currency }</Text> <Text fontSize="sm" fontWeight={ 500 }>Value { 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, Link, Icon } from '@chakra-ui/react'; import { Table, Thead, Tbody, Tr, Th, TableContainer, Link, Icon } 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 arrowIcon from 'icons/arrows/east.svg'; import arrowIcon from 'icons/arrows/east.svg';
import useNetwork from 'lib/hooks/useNetwork'; import useNetwork from 'lib/hooks/useNetwork';
import TxInternalsTableItem from 'ui/tx/internals/TxInternalsTableItem'; import TxInternalsTableItem from 'ui/tx/internals/TxInternalsTableItem';
import type { Sort, SortField } from 'ui/tx/internals/utils'; import type { Sort, SortField } from 'ui/tx/internals/utils';
interface Props { interface Props {
data: typeof txData; data: Array<InternalTransaction>;
sort: Sort | undefined; sort: Sort | undefined;
onSortToggle: (field: SortField) => () => void; onSortToggle: (field: SortField) => () => void;
} }
...@@ -23,26 +24,27 @@ const TxInternalsTable = ({ data, sort, onSortToggle }: Props) => { ...@@ -23,26 +24,27 @@ const TxInternalsTable = ({ data, sort, onSortToggle }: Props) => {
<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> <Th width="16%" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('value') } columnGap={ 1 }> <Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('value') } columnGap={ 1 }>
{ sort?.includes('value') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> } { sort?.includes('value') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Value { selectedNetwork?.currency } Value { selectedNetwork?.currency }
</Link> </Link>
</Th> </Th>
<Th width="16%" isNumeric> { /* no gas limit in api yet */ }
{ /* <Th width="16%" isNumeric>
<Link display="flex" alignItems="center" justifyContent="flex-end" onClick={ onSortToggle('gas-limit') } columnGap={ 1 }> <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 }/> } { sort?.includes('gas-limit') && <Icon as={ arrowIcon } boxSize={ 4 } transform={ sortIconTransform }/> }
Gas limit Gas limit
</Link> </Link>
</Th> </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 Sort = 'value-asc' | 'value-desc' | 'gas-limit-asc' | 'gas-limit-desc';
export type SortField = 'value' | 'gas-limit'; export type SortField = 'value' | 'gas-limit';
...@@ -10,10 +10,10 @@ interface TxInternalsTypeItem { ...@@ -10,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>
<RowHeader>Decode input data</RowHeader> { decoded && (
<GridItem> <>
<DecodedInputData/> <RowHeader>Decode input data</RowHeader>
</GridItem> <GridItem>
<DecodedInputData data={ decoded }/>
</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,22 +23,18 @@ const TxLogTopic = ({ hex, index }: Props) => { ...@@ -23,22 +23,18 @@ 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 */ } <Select
{ /* delete when ther will be real data */ } size="sm"
{ index > 0 && ( borderRadius="base"
<Select value={ selectedDataType }
size="sm" onChange={ handleSelectChange }
borderRadius="base" focusBorderColor="none"
value={ selectedDataType } w="75px"
onChange={ handleSelectChange } mr={ 3 }
focusBorderColor="none" flexShrink={ 0 }
w="75px" >
mr={ 3 } { OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
flexShrink={ 0 } </Select>
>
{ OPTIONS.map((option) => <option key={ option } value={ option }>{ option }</option>) }
</Select>
) }
<Box overflow="hidden" whiteSpace="nowrap"> <Box overflow="hidden" whiteSpace="nowrap">
<HashStringShortenDynamic hash={ hex }/> <HashStringShortenDynamic hash={ hex }/>
</Box> </Box>
......
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