Commit 671443d5 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into client-api-calls

parents 0bcef249 59b7b3bb
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
"detail": "start local dev server", "detail": "start local dev server",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "dedicated", "panel": "shared",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
"detail": "start local dev server for POA network", "detail": "start local dev server for POA network",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "dedicated", "panel": "shared",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
"detail": "start local dev server for Goerli network", "detail": "start local dev server for Goerli network",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "dedicated", "panel": "shared",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
}, },
"presentation": { "presentation": {
"reveal": "never", "reveal": "never",
"panel": "dedicated", "panel": "shared",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
"detail": "run eslint", "detail": "run eslint",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "dedicated", "panel": "shared",
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
"icon": { "icon": {
...@@ -112,7 +112,7 @@ ...@@ -112,7 +112,7 @@
"detail": "run visual components tests for current file", "detail": "run visual components tests for current file",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "dedicated", "panel": "shared",
"focus": true, "focus": true,
}, },
"icon": { "icon": {
...@@ -131,7 +131,7 @@ ...@@ -131,7 +131,7 @@
"detail": "run visual components tests for current file", "detail": "run visual components tests for current file",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "dedicated", "panel": "shared",
"focus": true, "focus": true,
}, },
"icon": { "icon": {
...@@ -150,7 +150,7 @@ ...@@ -150,7 +150,7 @@
"detail": "run visual components tests", "detail": "run visual components tests",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "dedicated", "panel": "shared",
"focus": true, "focus": true,
}, },
"icon": { "icon": {
...@@ -171,7 +171,7 @@ ...@@ -171,7 +171,7 @@
"detail": "run jest tests", "detail": "run jest tests",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "dedicated", "panel": "shared",
"focus": true, "focus": true,
}, },
"icon": { "icon": {
...@@ -190,7 +190,7 @@ ...@@ -190,7 +190,7 @@
"detail": "run jest tests in watch mode", "detail": "run jest tests in watch mode",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "dedicated", "panel": "shared",
"close": true, "close": true,
"focus": true, "focus": true,
}, },
...@@ -210,7 +210,7 @@ ...@@ -210,7 +210,7 @@
"detail": "run jest tests in watch mode for current file", "detail": "run jest tests in watch mode for current file",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "dedicated", "panel": "shared",
"focus": true, "focus": true,
}, },
"icon": { "icon": {
...@@ -230,7 +230,7 @@ ...@@ -230,7 +230,7 @@
"detail": "build docker image", "detail": "build docker image",
"presentation": { "presentation": {
"reveal": "always", "reveal": "always",
"panel": "dedicated", "panel": "shared",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
"focus": true, "focus": true,
...@@ -251,7 +251,7 @@ ...@@ -251,7 +251,7 @@
"detail": "run docker container for POA network", "detail": "run docker container for POA network",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "dedicated", "panel": "shared",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
...@@ -271,7 +271,7 @@ ...@@ -271,7 +271,7 @@
"detail": "format svg files with svgo", "detail": "format svg files with svgo",
"presentation": { "presentation": {
"reveal": "silent", "reveal": "silent",
"panel": "dedicated", "panel": "shared",
"close": true, "close": true,
"revealProblems": "onProblem", "revealProblems": "onProblem",
}, },
......
...@@ -11,6 +11,8 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout ...@@ -11,6 +11,8 @@ NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=true
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
NEXT_PUBLIC_NETWORK_LOGO=
NEXT_PUBLIC_NETWORK_SMALL_LOGO=
# api config # api config
NEXT_PUBLIC_API_HOST=blockscout.com NEXT_PUBLIC_API_HOST=blockscout.com
......
...@@ -35,7 +35,31 @@ const oldUrls = [ ...@@ -35,7 +35,31 @@ const oldUrls = [
}, },
{ {
oldPath: '/block/:id/transactions', oldPath: '/block/:id/transactions',
newPath: `${ PATHS.block }?tab=txs`, newPath: `${ PATHS.block }`,
},
{
oldPath: '/address/:id/transactions',
newPath: `${ PATHS.address_index }`,
},
{
oldPath: '/address/:id/token-transfers',
newPath: `${ PATHS.address_index }?tab=token_transfers`,
},
{
oldPath: '/address/:id/tokens',
newPath: `${ PATHS.address_index }?tab=tokens`,
},
{
oldPath: '/address/:id/internal-transactions',
newPath: `${ PATHS.address_index }?tab=internal_txns`,
},
{
oldPath: '/address/:id/coin-balances',
newPath: `${ PATHS.address_index }?tab=coin_balance_history`,
},
{
oldPath: '/address/:id/validations',
newPath: `${ PATHS.address_index }?tab=blocks_validated`,
}, },
]; ];
......
...@@ -28,6 +28,7 @@ blockscout: ...@@ -28,6 +28,7 @@ blockscout:
- 'nginx.ingress.kubernetes.io/cors-allow-credentials: "true"' - 'nginx.ingress.kubernetes.io/cors-allow-credentials: "true"'
- 'nginx.ingress.kubernetes.io/cors-allow-methods: PUT, GET, POST, OPTIONS, DELETE, PATCH' - 'nginx.ingress.kubernetes.io/cors-allow-methods: PUT, GET, POST, OPTIONS, DELETE, PATCH'
- 'nginx.ingress.kubernetes.io/enable-cors: "true"' - 'nginx.ingress.kubernetes.io/enable-cors: "true"'
- 'nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,x-csrf-token"'
host: host:
_default: blockscout.test.blockscout.aws-k8s.blockscout.com _default: blockscout.test.blockscout.aws-k8s.blockscout.com
# enable https # enable https
......
...@@ -395,4 +395,5 @@ frontend: ...@@ -395,4 +395,5 @@ frontend:
NEXT_PUBLIC_APP_HOST: NEXT_PUBLIC_APP_HOST:
_default: blockscout.com _default: blockscout.com
NEXT_PUBLIC_NETWORK_EXPLORERS: NEXT_PUBLIC_NETWORK_EXPLORERS:
_default: "[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]" _default: "[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction','address':'/ethereum/ethereum/goerli/address'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address'}}]"
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m12.18 3.765-1.944-1.944a1.674 1.674 0 0 0-1.179-.488H4.333c-.92 0-1.666.746-1.666 1.667v10c0 .92.746 1.667 1.667 1.667H11c.917 0 1.667-.75 1.667-1.667V4.943c0-.44-.175-.865-.487-1.178ZM11.417 13c0 .23-.187.417-.417.417H4.334A.417.417 0 0 1 3.917 13V3.003c0-.23.186-.416.416-.416H8.5v2.08c0 .46.373.833.833.833h2.06V13h.024ZM8.273 8.938a.365.365 0 0 0-.303.162l-1.032 1.55-.305-.456a.363.363 0 0 0-.606-.001l-1.215 1.823a.364.364 0 0 0-.018.374c.063.12.186.192.297.192h5.104a.365.365 0 0 0 .304-.566L8.554 9.1c-.044-.103-.158-.162-.28-.162ZM6 8.833a.833.833 0 1 0 0-1.666.833.833 0 0 0 0 1.666Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.667 2A.667.667 0 0 0 2 2.667V6a.667.667 0 0 0 1.333 0V3.333H6A.667.667 0 1 0 6 2H2.667Zm0 12A.667.667 0 0 1 2 13.333V10a.667.667 0 0 1 1.333 0v2.667H6A.667.667 0 1 1 6 14H2.667ZM14 2.667A.667.667 0 0 0 13.333 2H10a.667.667 0 0 0 0 1.333h2.667V6A.667.667 0 1 0 14 6V2.667ZM13.333 14a.667.667 0 0 0 .667-.667V10a.667.667 0 0 0-1.333 0v2.667H10A.667.667 0 1 0 10 14h3.333Z" fill="currentColor"/>
</svg>
...@@ -9,5 +9,5 @@ export default function getBlockTotalReward(block: Block) { ...@@ -9,5 +9,5 @@ export default function getBlockTotalReward(block: Block) {
?.map(({ reward }) => BigNumber(reward)) ?.map(({ reward }) => BigNumber(reward))
.reduce((result, item) => result.plus(item), ZERO) || ZERO; .reduce((result, item) => result.plus(item), ZERO) || ZERO;
return totalReward.div(WEI).toFixed(); return totalReward.div(WEI);
} }
import type { Types } from 'typescript-cookie'; import Cookies from 'js-cookie';
import { Cookies } from 'typescript-cookie';
import isBrowser from './isBrowser'; import isBrowser from './isBrowser';
...@@ -14,10 +13,13 @@ export function get(name?: NAMES | undefined | null, serverCookie?: string) { ...@@ -14,10 +13,13 @@ export function get(name?: NAMES | undefined | null, serverCookie?: string) {
if (!isBrowser()) { if (!isBrowser()) {
return serverCookie ? getFromCookieString(serverCookie, name) : undefined; return serverCookie ? getFromCookieString(serverCookie, name) : undefined;
} }
return Cookies.get(name);
if (name) {
return Cookies.get(name);
}
} }
export function set(name: string, value: string, attributes: Types.CookieAttributes = {}) { export function set(name: string, value: string, attributes: Cookies.CookieAttributes = {}) {
attributes.path = '/'; attributes.path = '/';
return Cookies.set(name, value, attributes); return Cookies.set(name, value, attributes);
......
...@@ -36,6 +36,7 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -36,6 +36,7 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
const fetch = useFetch(); const fetch = useFetch();
const isMounted = React.useRef(false); const isMounted = React.useRef(false);
const canGoBackwards = React.useRef(!router.query.page); const canGoBackwards = React.useRef(!router.query.page);
const [ hasPagination, setHasPagination ] = React.useState(page > 1);
const queryKey = [ queryName, ...(queryIds || []), { page, filters } ]; const queryKey = [ queryName, ...(queryIds || []), { page, filters } ];
...@@ -74,6 +75,7 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -74,6 +75,7 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
const nextPageQuery = { ...router.query }; const nextPageQuery = { ...router.query };
Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString()); Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
nextPageQuery.page = String(page + 1); nextPageQuery.page = String(page + 1);
setHasPagination(true);
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }) router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => { .then(() => {
...@@ -101,6 +103,7 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -101,6 +103,7 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
setPage(prev => prev - 1); setPage(prev => prev - 1);
page === 2 && queryClient.removeQueries({ queryKey: [ queryName ] }); page === 2 && queryClient.removeQueries({ queryKey: [ queryName ] });
}); });
setHasPagination(true);
}, [ router, page, paginationFields, pageParams, queryClient, scrollToTop, queryName ]); }, [ router, page, paginationFields, pageParams, queryClient, scrollToTop, queryName ]);
const resetPage = useCallback(() => { const resetPage = useCallback(() => {
...@@ -118,6 +121,8 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -118,6 +121,8 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
queryClient.removeQueries({ queryKey: [ queryName ], type: 'inactive' }); queryClient.removeQueries({ queryKey: [ queryName ], type: 'inactive' });
}, 100); }, 100);
}); });
setHasPagination(true);
}, [ queryClient, queryName, router, paginationFields, scrollToTop ]); }, [ queryClient, queryName, router, paginationFields, scrollToTop ]);
const onFilterChange = useCallback((newFilters: PaginationFilters<QueryName> | undefined) => { const onFilterChange = useCallback((newFilters: PaginationFilters<QueryName> | undefined) => {
...@@ -156,6 +161,8 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -156,6 +161,8 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
canGoBackwards: canGoBackwards.current, canGoBackwards: canGoBackwards.current,
}; };
const isPaginationVisible = hasPagination || (!queryResult.isLoading && !queryResult.isError && pagination.hasNextPage);
React.useEffect(() => { React.useEffect(() => {
if (page !== 1 && isMounted.current) { if (page !== 1 && isMounted.current) {
queryClient.cancelQueries({ queryKey }); queryClient.cancelQueries({ queryKey });
...@@ -171,5 +178,5 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -171,5 +178,5 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
}, 0); }, 0);
}, []); }, []);
return { ...queryResult, pagination, onFilterChange }; return { ...queryResult, pagination, onFilterChange, isPaginationVisible };
} }
...@@ -41,46 +41,48 @@ function getUpdateParams(ts: string) { ...@@ -41,46 +41,48 @@ function getUpdateParams(ts: string) {
}; };
} }
export default function useTimeAgoIncrement(ts: string, isEnabled?: boolean) { export default function useTimeAgoIncrement(ts: string | null, isEnabled?: boolean) {
const [ value, setValue ] = React.useState(dayjs(ts).fromNow()); const [ value, setValue ] = React.useState(ts ? dayjs(ts).fromNow() : null);
React.useEffect(() => { React.useEffect(() => {
const timeouts: Array<number> = []; if (ts !== null) {
const intervals: Array<number> = []; const timeouts: Array<number> = [];
const intervals: Array<number> = [];
const startIncrement = () => { const startIncrement = () => {
const { startTimeout, interval, endTimeout } = getUpdateParams(ts); const { startTimeout, interval, endTimeout } = getUpdateParams(ts);
if (!startTimeout && !endTimeout) { if (!startTimeout && !endTimeout) {
return; return;
} }
let intervalId: number; let intervalId: number;
const startTimeoutId = window.setTimeout(() => { const startTimeoutId = window.setTimeout(() => {
setValue(dayjs(ts).fromNow());
intervalId = window.setInterval(() => {
setValue(dayjs(ts).fromNow()); setValue(dayjs(ts).fromNow());
}, interval);
intervals.push(intervalId); intervalId = window.setInterval(() => {
}, startTimeout); setValue(dayjs(ts).fromNow());
}, interval);
intervals.push(intervalId);
}, startTimeout);
const endTimeoutId = window.setTimeout(() => { const endTimeoutId = window.setTimeout(() => {
window.clearInterval(intervalId); window.clearInterval(intervalId);
startIncrement(); startIncrement();
}, endTimeout); }, endTimeout);
timeouts.push(startTimeoutId); timeouts.push(startTimeoutId);
timeouts.push(endTimeoutId); timeouts.push(endTimeoutId);
}; };
isEnabled && startIncrement(); isEnabled && startIncrement();
return () => { return () => {
timeouts.forEach(window.clearTimeout); timeouts.forEach(window.clearTimeout);
intervals.forEach(window.clearInterval); intervals.forEach(window.clearInterval);
}; };
}
}, [ isEnabled, ts ]); }, [ isEnabled, ts ]);
return value; return value;
......
...@@ -5,6 +5,7 @@ import type { NewBlockSocketResponse } from 'types/api/block'; ...@@ -5,6 +5,7 @@ import type { NewBlockSocketResponse } from 'types/api/block';
export type SocketMessageParams = SocketMessage.NewBlock | export type SocketMessageParams = SocketMessage.NewBlock |
SocketMessage.BlocksIndexStatus | SocketMessage.BlocksIndexStatus |
SocketMessage.InternalTxsIndexStatus |
SocketMessage.TxStatusUpdate | SocketMessage.TxStatusUpdate |
SocketMessage.NewTx | SocketMessage.NewTx |
SocketMessage.NewPendingTx | SocketMessage.NewPendingTx |
...@@ -23,7 +24,8 @@ interface SocketMessageParamsGeneric<Event extends string | undefined, Payload e ...@@ -23,7 +24,8 @@ interface SocketMessageParamsGeneric<Event extends string | undefined, Payload e
// eslint-disable-next-line @typescript-eslint/no-namespace // eslint-disable-next-line @typescript-eslint/no-namespace
export namespace SocketMessage { export namespace SocketMessage {
export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>; export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>;
export type BlocksIndexStatus = SocketMessageParamsGeneric<'index_status', {finished: boolean; ratio: string}>; export type BlocksIndexStatus = SocketMessageParamsGeneric<'block_index_status', {finished: boolean; ratio: string}>;
export type InternalTxsIndexStatus = SocketMessageParamsGeneric<'internal_txs_index_status', {finished: boolean; ratio: string}>;
export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>; export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>;
export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>; export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>;
export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>; export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>;
......
import type { FeaturedNetwork } from 'types/networks';
const FEATURED_NETWORKS: Array<FeaturedNetwork> = [
{ title: 'Gnosis Chain', url: 'https://blockscout.com/xdai/mainnet', group: 'mainnets', type: 'xdai_mainnet' },
{ title: 'Optimism on Gnosis Chain', url: 'https://blockscout.com/xdai/optimism', group: 'mainnets', type: 'xdai_optimism' },
{ title: 'Arbitrum on xDai', url: 'https://blockscout.com/xdai/aox', group: 'mainnets' },
{ title: 'Ethereum', url: 'https://blockscout.com/eth/mainnet', group: 'mainnets', type: 'eth_mainnet' },
{ title: 'Ethereum Classic', url: 'https://blockscout.com/etx/mainnet', group: 'mainnets', type: 'etc_mainnet', icon: 'https://example.com/my-logo.png' },
{ title: 'POA', url: 'https://blockscout.com/poa/core', group: 'mainnets', type: 'poa_core' },
{ title: 'RSK', url: 'https://blockscout.com/rsk/mainnet', group: 'mainnets', type: 'rsk_mainnet' },
{ title: 'Gnosis Chain Testnet', url: 'https://blockscout.com/xdai/testnet', group: 'testnets', type: 'xdai_testnet' },
{ title: 'POA Sokol', url: 'https://blockscout.com/poa/sokol', group: 'testnets', type: 'poa_sokol' },
{ title: 'ARTIS Σ1', url: 'https://blockscout.com/artis/sigma1', group: 'other', type: 'artis_sigma1' },
{ title: 'LUKSO L14', url: 'https://blockscout.com/lukso/l14', group: 'other', type: 'lukso_l14' },
{ title: 'Astar', url: 'https://blockscout.com/astar', group: 'other', type: 'astar' },
];
export const FEATURED_NETWORKS_MOCK = JSON.stringify(FEATURED_NETWORKS).replaceAll('"', '\'');
...@@ -37,6 +37,7 @@ export const erc20: TokenTransfer = { ...@@ -37,6 +37,7 @@ export const erc20: TokenTransfer = {
}, },
tx_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193', tx_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
type: 'token_transfer', type: 'token_transfer',
timestamp: '2022-10-10T14:34:30.000000Z',
}; };
export const erc721: TokenTransfer = { export const erc721: TokenTransfer = {
...@@ -75,6 +76,7 @@ export const erc721: TokenTransfer = { ...@@ -75,6 +76,7 @@ export const erc721: TokenTransfer = {
}, },
tx_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc', tx_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc',
type: 'token_transfer', type: 'token_transfer',
timestamp: '2022-10-10T14:34:30.000000Z',
}; };
export const erc1155: TokenTransfer = { export const erc1155: TokenTransfer = {
...@@ -115,6 +117,7 @@ export const erc1155: TokenTransfer = { ...@@ -115,6 +117,7 @@ export const erc1155: TokenTransfer = {
}, },
tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746', tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746',
type: 'token_minting', type: 'token_minting',
timestamp: '2022-10-10T14:34:30.000000Z',
}; };
export const erc1155multiple: TokenTransfer = { export const erc1155multiple: TokenTransfer = {
......
export const base = {
avatar: 'https://avatars.githubusercontent.com/u/22130104',
email: 'tom@ohhhh.me',
name: 'tom goriunov',
nickname: 'tom2drum',
};
import handler from 'lib/api/handler';
const getUrl = () => '/v2/main-page/indexing-status';
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
...@@ -96,6 +96,15 @@ const config: PlaywrightTestConfig = { ...@@ -96,6 +96,15 @@ const config: PlaywrightTestConfig = {
colorScheme: 'dark', colorScheme: 'dark',
}, },
}, },
{
name: 'dark color mode desktop xl',
grep: /\+@dark-mode-xl/,
use: {
...devices['Desktop Chrome'],
viewport: { width: 1600, height: 1000 },
colorScheme: 'dark',
},
},
], ],
}; };
......
...@@ -2,6 +2,8 @@ import { ChakraProvider } from '@chakra-ui/react'; ...@@ -2,6 +2,8 @@ import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import { AppContextProvider } from 'lib/appContext';
import type { Props as PageProps } from 'lib/next/getServerSideProps';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import { PORT } from 'playwright/fixtures/socketServer'; import { PORT } from 'playwright/fixtures/socketServer';
import theme from 'theme'; import theme from 'theme';
...@@ -9,9 +11,19 @@ import theme from 'theme'; ...@@ -9,9 +11,19 @@ import theme from 'theme';
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode;
withSocket?: boolean; withSocket?: boolean;
appContext?: {
pageProps: PageProps;
};
} }
const TestApp = ({ children, withSocket }: Props) => { const defaultAppContext = {
pageProps: {
cookies: '',
referrer: '',
},
};
const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props) => {
const [ queryClient ] = React.useState(() => new QueryClient({ const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
...@@ -25,7 +37,9 @@ const TestApp = ({ children, withSocket }: Props) => { ...@@ -25,7 +37,9 @@ const TestApp = ({ children, withSocket }: Props) => {
<ChakraProvider theme={ theme }> <ChakraProvider theme={ theme }>
<QueryClientProvider client={ queryClient }> <QueryClientProvider client={ queryClient }>
<SocketProvider url={ withSocket ? `ws://localhost:${ PORT }` : undefined }> <SocketProvider url={ withSocket ? `ws://localhost:${ PORT }` : undefined }>
{ children } <AppContextProvider { ...appContext }>
{ children }
</AppContextProvider>
</SocketProvider> </SocketProvider>
</QueryClientProvider> </QueryClientProvider>
</ChakraProvider> </ChakraProvider>
......
import type { BrowserContext } from '@playwright/test';
import * as cookies from 'lib/cookies';
export default function authFixture(context: BrowserContext) {
context.addCookies([ { name: cookies.NAMES.API_TOKEN, value: 'foo', domain: 'localhost', path: '/' } ]);
}
...@@ -25,10 +25,8 @@ do ...@@ -25,10 +25,8 @@ do
configValue="$(cut -d'=' -f2- <<<"$line")"; configValue="$(cut -d'=' -f2- <<<"$line")";
# if there is a value, escape it and add line to target file # if there is a value, escape it and add line to target file
if [ -n "$configValue" ]; then escapedConfigValue=$(echo $configValue | sed s/\'/\"/g);
escapedConfigValue=$(echo $configValue | sed s/\'/\"/g); echo "window.process.env.${configName} = localStorage.getItem('${configName}') || '${escapedConfigValue}';" >> $targetFile;
echo "window.process.env.${configName} = localStorage.getItem('${configName}') || '${escapedConfigValue}';" >> $targetFile;
fi
done < $envFile done < $envFile
done done
......
...@@ -18,7 +18,7 @@ const baseStyle = definePartsStyle({ ...@@ -18,7 +18,7 @@ const baseStyle = definePartsStyle({
container: { container: {
borderRadius: 'md', borderRadius: 'md',
px: 6, px: 6,
py: 4, py: 3,
}, },
}); });
......
import { Button } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
[
{ variant: 'solid' },
{ variant: 'solid', colorScheme: 'gray', withDarkMode: true },
{ variant: 'outline', colorScheme: 'gray', withDarkMode: true },
{ variant: 'outline', colorScheme: 'gray-dark', withDarkMode: true },
{ variant: 'outline', colorScheme: 'blue', withDarkMode: true },
{ variant: 'simple', withDarkMode: true },
{ variant: 'ghost', withDarkMode: true },
{ variant: 'subtle' },
{ variant: 'subtle', colorScheme: 'gray', withDarkMode: true },
].forEach(({ variant, colorScheme, withDarkMode }) => {
test.describe(`variant ${ variant }${ colorScheme ? ` with ${ colorScheme } color scheme` : '' }${ withDarkMode ? ' +@dark-mode' : '' }`, () => {
test('base', async({ mount }) => {
const component = await mount(
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme }>Click me</Button>
</TestApp>,
);
await expect(component.locator('button')).toHaveScreenshot();
});
test('disabled', async({ mount }) => {
const component = await mount(
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme } disabled>Click me</Button>
</TestApp>,
);
await expect(component.locator('button')).toHaveScreenshot();
});
test('hovered', async({ mount }) => {
const component = await mount(
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme }>Click me</Button>
</TestApp>,
);
await component.getByText(/click/i).hover();
await expect(component.locator('button')).toHaveScreenshot();
});
test('active', async({ mount }) => {
const component = await mount(
<TestApp>
<Button variant={ variant } colorScheme={ colorScheme } isActive>Click me</Button>
</TestApp>,
);
await expect(component.locator('button')).toHaveScreenshot();
});
});
});
import { FormControl, Input, FormLabel } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
test.use({ viewport: { width: 500, height: 300 } });
test.describe('floating label size md +@dark-mode', () => {
test('empty', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value=""/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
);
await expect(component).toHaveScreenshot();
await component.locator('input').focus();
await expect(component).toHaveScreenshot();
});
test('empty error', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="" isInvalid/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
);
await expect(component).toHaveScreenshot();
await component.locator('input').focus();
await expect(component).toHaveScreenshot();
});
test('filled', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="foo"/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('filled disabled', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="foo" isDisabled/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('filled error', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="md">
<Input required value="foo" isInvalid/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
});
test.describe('floating label size lg +@dark-mode', () => {
test('empty', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="lg">
<Input required value=""/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
);
await expect(component).toHaveScreenshot();
await component.locator('input').focus();
await expect(component).toHaveScreenshot();
});
test('filled', async({ mount }) => {
const component = await mount(
<TestApp>
<FormControl variant="floating" id="name" isRequired size="lg">
<Input required value="foo"/>
<FormLabel>Smart contract / Address (0x...)</FormLabel>
</FormControl>
</TestApp>,
);
await expect(component).toHaveScreenshot();
await component.locator('input').focus();
await expect(component).toHaveScreenshot();
});
});
import { Box, Tag } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
[ 'blue', 'gray', 'orange', 'green', 'purple', 'cyan', 'teal' ].forEach((colorScheme) => {
test(`${ colorScheme } color scheme +@dark-mode`, async({ mount }) => {
const component = await mount(
<TestApp>
<Tag colorScheme={ colorScheme }>content</Tag>
</TestApp>,
);
await expect(component.getByText(/content/i)).toHaveScreenshot();
});
});
test('with long text', async({ mount }) => {
const component = await mount(
<TestApp>
<Box w="100px">
<Tag>this is very looooooooooong text</Tag>
</Box>
</TestApp>,
);
await expect(component.getByText(/this/i)).toHaveScreenshot();
});
...@@ -4,8 +4,8 @@ import { ...@@ -4,8 +4,8 @@ import {
defineStyle, defineStyle,
} from '@chakra-ui/styled-system'; } from '@chakra-ui/styled-system';
import getDefaultTransitionProps from '../utils/getDefaultTransitionProps'; import getDefaultTransitionProps from '../../utils/getDefaultTransitionProps';
import Badge from './Badge'; import Badge from '../Badge';
const transitionProps = getDefaultTransitionProps(); const transitionProps = getDefaultTransitionProps();
const { defineMultiStyleConfig, definePartsStyle } = const { defineMultiStyleConfig, definePartsStyle } =
......
import { Box, Tooltip, Icon } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
test('base view +@dark-mode', async({ mount, page }) => {
const component = await mount(
<TestApp>
<Box m={ 10 }>
<Tooltip label="Tooltip content">
trigger
</Tooltip>
</Box>
</TestApp>,
);
await component.getByText(/trigger/i).hover();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 40, width: 130, height: 64 } });
});
// was not able to reproduce in tests issue when Icon is used as trigger for tooltip
// https://github.com/chakra-ui/chakra-ui/issues/7107
test.skip('with icon', async({ mount, page }) => {
const component = await mount(
<TestApp>
<Box m={ 10 }>
<Tooltip label="Tooltip content">
<Icon viewBox="0 0 20 20" boxSize={ 5 } aria-label="Trigger">
<circle cx="10" cy="10" r="10"/>
</Icon>
</Tooltip>
</Box>
</TestApp>,
);
const tooltip = page.getByText(/tooltip content/i);
expect(await tooltip.isVisible()).toBe(false);
await component.locator('svg[aria-label="Trigger"]').hover();
expect(await tooltip.isVisible()).toBe(true);
});
...@@ -2,8 +2,6 @@ import { Tooltip as TooltipComponent } from '@chakra-ui/react'; ...@@ -2,8 +2,6 @@ import { Tooltip as TooltipComponent } from '@chakra-ui/react';
import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system'; import { defineStyle, defineStyleConfig } from '@chakra-ui/styled-system';
import { mode, cssVar } from '@chakra-ui/theme-tools'; import { mode, cssVar } from '@chakra-ui/theme-tools';
// these cause console warning about kebab-case in naming of css variables
// but the chakra-ui itself uses the same variable names, and i guess we cannot override them
const $bg = cssVar('tooltip-bg'); const $bg = cssVar('tooltip-bg');
const $fg = cssVar('tooltip-fg'); const $fg = cssVar('tooltip-fg');
const $arrowBg = cssVar('popper-arrow-bg'); const $arrowBg = cssVar('popper-arrow-bg');
...@@ -33,9 +31,9 @@ const baseStyle = defineStyle((props) => { ...@@ -33,9 +31,9 @@ const baseStyle = defineStyle((props) => {
return { return {
bg: $bg.reference, bg: $bg.reference,
color: mode('white', 'black')(props), color: $fg.reference,
[$bg.variable]: `colors.${ bg }`, [$bg.variable]: `colors.${ bg }`,
[$fg.reference]: `colors.${ fg }`, [$fg.variable]: `colors.${ fg }`,
[$arrowBg.variable]: $bg.reference, [$arrowBg.variable]: $bg.reference,
maxWidth: props.maxWidth || props.maxW || 'unset', maxWidth: props.maxWidth || props.maxW || 'unset',
}; };
......
import Alert from './Alert'; import Alert from './Alert';
import Badge from './Badge'; import Badge from './Badge';
import Button from './Button'; import Button from './Button/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';
...@@ -16,10 +16,10 @@ import Spinner from './Spinner'; ...@@ -16,10 +16,10 @@ import Spinner from './Spinner';
import Switch from './Switch'; import Switch from './Switch';
import Table from './Table'; import Table from './Table';
import Tabs from './Tabs'; import Tabs from './Tabs';
import Tag from './Tag'; import Tag from './Tag/Tag';
import Text from './Text'; import Text from './Text';
import Textarea from './Textarea'; import Textarea from './Textarea';
import Tooltip from './Tooltip'; import Tooltip from './Tooltip/Tooltip';
const components = { const components = {
Alert, Alert,
......
export type IndexingStatus = {
finished_indexing: boolean;
finished_indexing_blocks: boolean;
indexed_blocks_ratio: string;
indexed_inernal_transactions_ratio: string;
}
...@@ -7,7 +7,7 @@ export type HomeStats = { ...@@ -7,7 +7,7 @@ export type HomeStats = {
total_gas_used: string; total_gas_used: string;
transactions_today: string; transactions_today: string;
gas_used_today: string; gas_used_today: string;
gas_prices: GasPrices; gas_prices: GasPrices | null;
static_gas_price: string; static_gas_price: string;
market_cap: string; market_cap: string;
network_utilization_percentage: number; network_utilization_percentage: number;
......
...@@ -36,6 +36,7 @@ interface TokenTransferBase { ...@@ -36,6 +36,7 @@ interface TokenTransferBase {
tx_hash: string; tx_hash: string;
from: AddressParam; from: AddressParam;
to: AddressParam; to: AddressParam;
timestamp: string;
} }
export type TokenTransferPagination = { export type TokenTransferPagination = {
......
...@@ -4,6 +4,7 @@ export enum QueryKeys { ...@@ -4,6 +4,7 @@ export enum QueryKeys {
txsValidate = 'txs-validated', txsValidate = 'txs-validated',
txsPending = 'txs-pending', txsPending = 'txs-pending',
homeStats='homeStats', homeStats='homeStats',
indexingStatus='indexingStatus',
stats='stats', stats='stats',
charts='stats', charts='stats',
tx = 'tx', tx = 'tx',
......
...@@ -14,12 +14,12 @@ import useSocketMessage from 'lib/socket/useSocketMessage'; ...@@ -14,12 +14,12 @@ import useSocketMessage from 'lib/socket/useSocketMessage';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import SocketAlert from 'ui/shared/SocketAlert'; import SocketAlert from 'ui/shared/SocketAlert';
import { default as Thead } from 'ui/shared/TheadSticky'; import { default as Thead } from 'ui/shared/TheadSticky';
import AddressBlocksValidatedListItem from './blocksValidated/AddressBlocksValidatedListItem'; import AddressBlocksValidatedListItem from './blocksValidated/AddressBlocksValidatedListItem';
import AddressBlocksValidatedSkeletonMobile from './blocksValidated/AddressBlocksValidatedSkeletonMobile';
import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksValidatedTableItem'; import AddressBlocksValidatedTableItem from './blocksValidated/AddressBlocksValidatedTableItem';
interface Props { interface Props {
...@@ -79,7 +79,7 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => { ...@@ -79,7 +79,7 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => {
<SkeletonTable columns={ [ '17%', '17%', '16%', '25%', '25%' ] }/> <SkeletonTable columns={ [ '17%', '17%', '16%', '25%', '25%' ] }/>
</Hide> </Hide>
<Show below="lg"> <Show below="lg">
<AddressBlocksValidatedSkeletonMobile/> <SkeletonList/>
</Show> </Show>
</> </>
); );
...@@ -122,11 +122,9 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => { ...@@ -122,11 +122,9 @@ const AddressBlocksValidated = ({ addressQuery }: Props) => {
); );
})(); })();
const isPaginatorHidden = !query.isLoading && !query.isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
return ( return (
<Box> <Box>
{ !isPaginatorHidden && ( { query.isPaginationVisible && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
<Pagination ml="auto" { ...query.pagination }/> <Pagination ml="auto" { ...query.pagination }/>
</ActionBar> </ActionBar>
......
...@@ -35,7 +35,7 @@ const AddressInternalTxs = () => { ...@@ -35,7 +35,7 @@ const AddressInternalTxs = () => {
const queryIdArray = castArray(queryId); const queryIdArray = castArray(queryId);
const queryIdStr = queryIdArray[0]; const queryIdStr = queryIdArray[0];
const { data, isLoading, isError, pagination, onFilterChange } = useQueryWithPages({ const { data, isLoading, isError, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
apiPath: `/node-api/addresses/${ queryId }/internal-transactions`, apiPath: `/node-api/addresses/${ queryId }/internal-transactions`,
queryName: QueryKeys.addressInternalTxs, queryName: QueryKeys.addressInternalTxs,
queryIds: queryIdArray, queryIds: queryIdArray,
...@@ -43,8 +43,6 @@ const AddressInternalTxs = () => { ...@@ -43,8 +43,6 @@ const AddressInternalTxs = () => {
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET }, scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
}); });
const isPaginatorHidden = !isLoading && !isError && pagination.page === 1 && !pagination.hasNextPage;
const handleFilterChange = React.useCallback((val: string | Array<string>) => { const handleFilterChange = React.useCallback((val: string | Array<string>) => {
const newVal = getFilterValue(val); const newVal = getFilterValue(val);
...@@ -94,7 +92,7 @@ const AddressInternalTxs = () => { ...@@ -94,7 +92,7 @@ const AddressInternalTxs = () => {
onFilterChange={ handleFilterChange } onFilterChange={ handleFilterChange }
isActive={ Boolean(filterValue) } isActive={ Boolean(filterValue) }
/> />
{ !isPaginatorHidden && <Pagination ml="auto" { ...pagination }/> } { isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
</ActionBar> </ActionBar>
{ content } { content }
</Element> </Element>
......
...@@ -16,6 +16,7 @@ const AddressTokenTransfers = () => { ...@@ -16,6 +16,7 @@ const AddressTokenTransfers = () => {
queryName={ QueryKeys.addressTokenTransfers } queryName={ QueryKeys.addressTokenTransfers }
queryIds={ castArray(router.query.id) } queryIds={ castArray(router.query.id) }
baseAddress={ typeof hash === 'string' ? hash : undefined } baseAddress={ typeof hash === 'string' ? hash : undefined }
enableTimeIncrement
/> />
); );
}; };
......
...@@ -43,12 +43,6 @@ const AddressTxs = () => { ...@@ -43,12 +43,6 @@ const AddressTxs = () => {
addressTxsQuery.onFilterChange({ filter: newVal }); addressTxsQuery.onFilterChange({ filter: newVal });
}, [ addressTxsQuery ]); }, [ addressTxsQuery ]);
const isPaginatorHidden =
!addressTxsQuery.isLoading &&
!addressTxsQuery.isError &&
addressTxsQuery.pagination.page === 1 &&
!addressTxsQuery.pagination.hasNextPage;
const filter = ( const filter = (
<AddressTxsFilter <AddressTxsFilter
defaultFilter={ filterValue } defaultFilter={ filterValue }
...@@ -62,7 +56,7 @@ const AddressTxs = () => { ...@@ -62,7 +56,7 @@ const AddressTxs = () => {
{ !isMobile && ( { !isMobile && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
{ filter } { filter }
{ !isPaginatorHidden && <Pagination { ...addressTxsQuery.pagination }/> } { addressTxsQuery.isPaginationVisible && <Pagination { ...addressTxsQuery.pagination }/> }
</ActionBar> </ActionBar>
) } ) }
<TxsContent <TxsContent
...@@ -70,6 +64,7 @@ const AddressTxs = () => { ...@@ -70,6 +64,7 @@ const AddressTxs = () => {
query={ addressTxsQuery } query={ addressTxsQuery }
showSocketInfo={ false } showSocketInfo={ false }
currentAddress={ typeof router.query.id === 'string' ? router.query.id : undefined } currentAddress={ typeof router.query.id === 'string' ? router.query.id : undefined }
enableTimeIncrement
/> />
</Element> </Element>
); );
......
...@@ -8,7 +8,7 @@ import appConfig from 'configs/app/config'; ...@@ -8,7 +8,7 @@ import appConfig from 'configs/app/config';
import getBlockTotalReward from 'lib/block/getBlockTotalReward'; import getBlockTotalReward from 'lib/block/getBlockTotalReward';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
type Props = Block & { type Props = Block & {
...@@ -21,7 +21,7 @@ const AddressBlocksValidatedListItem = (props: Props) => { ...@@ -21,7 +21,7 @@ const AddressBlocksValidatedListItem = (props: Props) => {
const totalReward = getBlockTotalReward(props); const totalReward = getBlockTotalReward(props);
return ( return (
<AccountListItemMobile rowGap={ 2 }> <ListItemMobile rowGap={ 2 } isAnimated>
<Flex justifyContent="space-between" w="100%"> <Flex justifyContent="space-between" w="100%">
<Link href={ blockUrl } fontWeight="700">{ props.height }</Link> <Link href={ blockUrl } fontWeight="700">{ props.height }</Link>
<Text variant="secondary">{ timeAgo }</Text> <Text variant="secondary">{ timeAgo }</Text>
...@@ -37,9 +37,9 @@ const AddressBlocksValidatedListItem = (props: Props) => { ...@@ -37,9 +37,9 @@ const AddressBlocksValidatedListItem = (props: Props) => {
</Flex> </Flex>
<Flex columnGap={ 2 } w="100%"> <Flex columnGap={ 2 } w="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Reward { appConfig.network.currency.symbol }</Text> <Text fontWeight={ 500 } flexShrink={ 0 }>Reward { appConfig.network.currency.symbol }</Text>
<Text variant="secondary">{ totalReward }</Text> <Text variant="secondary">{ totalReward.toFixed() }</Text>
</Flex> </Flex>
</AccountListItemMobile> </ListItemMobile>
); );
}; };
......
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const AddressBlocksValidatedSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<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 justifyContent="space-between" w="100%" h={ 6 }>
<Skeleton w="100px"/>
<Skeleton w="100px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="40px"/>
<Skeleton w="40px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="70px"/>
<Skeleton w="70px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="100px"/>
<Skeleton w="120px"/>
</Flex>
</Flex>
)) }
</Box>
);
};
export default AddressBlocksValidatedSkeletonMobile;
...@@ -36,7 +36,7 @@ const AddressBlocksValidatedTableItem = (props: Props) => { ...@@ -36,7 +36,7 @@ const AddressBlocksValidatedTableItem = (props: Props) => {
</Flex> </Flex>
</Td> </Td>
<Td isNumeric display="flex" justifyContent="end"> <Td isNumeric display="flex" justifyContent="end">
{ totalReward } { totalReward.toFixed() }
</Td> </Td>
</Tr> </Tr>
); );
......
...@@ -9,23 +9,22 @@ import ActionBar from 'ui/shared/ActionBar'; ...@@ -9,23 +9,22 @@ import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import { default as Thead } from 'ui/shared/TheadSticky'; import { default as Thead } from 'ui/shared/TheadSticky';
import AddressCoinBalanceListItem from './AddressCoinBalanceListItem'; import AddressCoinBalanceListItem from './AddressCoinBalanceListItem';
import AddressCoinBalanceSkeletonMobile from './AddressCoinBalanceSkeletonMobile';
import AddressCoinBalanceTableItem from './AddressCoinBalanceTableItem'; import AddressCoinBalanceTableItem from './AddressCoinBalanceTableItem';
interface Props { interface Props {
query: UseQueryResult<AddressCoinBalanceHistoryResponse> & { query: UseQueryResult<AddressCoinBalanceHistoryResponse> & {
pagination: PaginationProps; pagination: PaginationProps;
isPaginationVisible: boolean;
}; };
} }
const AddressCoinBalanceHistory = ({ query }: Props) => { const AddressCoinBalanceHistory = ({ query }: Props) => {
const isPaginatorHidden = !query.isLoading && !query.isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
const content = (() => { const content = (() => {
if (query.isLoading) { if (query.isLoading) {
return ( return (
...@@ -34,7 +33,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => { ...@@ -34,7 +33,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
<SkeletonTable columns={ [ '25%', '25%', '25%', '25%', '120px' ] }/> <SkeletonTable columns={ [ '25%', '25%', '25%', '25%', '120px' ] }/>
</Hide> </Hide>
<Show below="lg"> <Show below="lg">
<AddressCoinBalanceSkeletonMobile/> <SkeletonList/>
</Show> </Show>
</> </>
); );
...@@ -75,7 +74,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => { ...@@ -75,7 +74,7 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
return ( return (
<Box mt={ 8 }> <Box mt={ 8 }>
{ !isPaginatorHidden && ( { query.isPaginationVisible && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
<Pagination ml="auto" { ...query.pagination }/> <Pagination ml="auto" { ...query.pagination }/>
</ActionBar> </ActionBar>
......
...@@ -8,9 +8,9 @@ import appConfig from 'configs/app/config'; ...@@ -8,9 +8,9 @@ import appConfig from 'configs/app/config';
import { WEI, ZERO } from 'lib/consts'; import { WEI, ZERO } from 'lib/consts';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
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 ListItemMobile from 'ui/shared/ListItemMobile';
type Props = AddressCoinBalanceHistoryItem & { type Props = AddressCoinBalanceHistoryItem & {
page: number; page: number;
...@@ -23,7 +23,7 @@ const AddressCoinBalanceListItem = (props: Props) => { ...@@ -23,7 +23,7 @@ const AddressCoinBalanceListItem = (props: Props) => {
const timeAgo = useTimeAgoIncrement(props.block_timestamp, props.page === 1); const timeAgo = useTimeAgoIncrement(props.block_timestamp, props.page === 1);
return ( return (
<AccountListItemMobile rowGap={ 2 }> <ListItemMobile rowGap={ 2 } isAnimated>
<Flex justifyContent="space-between" w="100%"> <Flex justifyContent="space-between" w="100%">
<Text fontWeight={ 600 }>{ BigNumber(props.value).div(WEI).toFixed(8) } { appConfig.network.currency.symbol }</Text> <Text fontWeight={ 600 }>{ BigNumber(props.value).div(WEI).toFixed(8) } { appConfig.network.currency.symbol }</Text>
<Stat flexGrow="0"> <Stat flexGrow="0">
...@@ -51,7 +51,7 @@ const AddressCoinBalanceListItem = (props: Props) => { ...@@ -51,7 +51,7 @@ const AddressCoinBalanceListItem = (props: Props) => {
<Text fontWeight={ 500 } flexShrink={ 0 }>Age</Text> <Text fontWeight={ 500 } flexShrink={ 0 }>Age</Text>
<Text variant="secondary">{ timeAgo }</Text> <Text variant="secondary">{ timeAgo }</Text>
</Flex> </Flex>
</AccountListItemMobile> </ListItemMobile>
); );
}; };
......
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const AddressCoinBalanceSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<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 justifyContent="space-between" w="100%" h={ 6 }>
<Skeleton w="170px"/>
<Skeleton w="120px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="40px"/>
<Skeleton w="80px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="40px"/>
<Skeleton w="150px"/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="30px"/>
<Skeleton w="60px"/>
</Flex>
</Flex>
)) }
</Box>
);
};
export default AddressCoinBalanceSkeletonMobile;
...@@ -8,14 +8,13 @@ const AddressDetailsSkeleton = () => { ...@@ -8,14 +8,13 @@ const AddressDetailsSkeleton = () => {
<Box> <Box>
<Flex align="center"> <Flex align="center">
<SkeletonCircle boxSize={ 6 } flexShrink={ 0 }/> <SkeletonCircle boxSize={ 6 } flexShrink={ 0 }/>
<Skeleton h={ 6 } w={{ base: '100px', lg: '420px' }} ml={ 2 }/> <Skeleton h={ 6 } w={{ base: '100px', lg: '420px' }} ml={ 2 } borderRadius="full"/>
<Skeleton h={ 6 } w="24px" ml={ 2 } flexShrink={ 0 }/> <Skeleton h={ 8 } w="36px" ml={ 3 } flexShrink={ 0 }/>
<Skeleton h={ 8 } w="48px" ml={ 3 } flexShrink={ 0 }/> <Skeleton h={ 8 } w="36px" ml={ 3 } flexShrink={ 0 }/>
<Skeleton h={ 8 } w="48px" ml={ 3 } flexShrink={ 0 }/>
</Flex> </Flex>
<Flex align="center" columnGap={ 4 } mt={ 8 }> <Flex align="center" columnGap={ 4 } mt={ 8 }>
<Skeleton h={ 6 } w="200px"/> <Skeleton h={ 6 } w="200px" borderRadius="full"/>
<Skeleton h={ 6 } w="80px"/> <Skeleton h={ 6 } w="80px" borderRadius="full"/>
</Flex> </Flex>
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '150px 1fr' }} maxW="1000px" mt={ 8 }> <Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '150px 1fr' }} maxW="1000px" mt={ 8 }>
<DetailsSkeletonRow w="30%"/> <DetailsSkeletonRow w="30%"/>
......
...@@ -8,11 +8,11 @@ import appConfig from 'configs/app/config'; ...@@ -8,11 +8,11 @@ import appConfig from 'configs/app/config';
import eastArrowIcon from 'icons/arrows/east.svg'; import eastArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import link from 'lib/link/link'; import link from 'lib/link/link';
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 InOutTag from 'ui/shared/InOutTag'; import InOutTag from 'ui/shared/InOutTag';
import ListItemMobile from 'ui/shared/ListItemMobile';
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';
...@@ -38,7 +38,7 @@ const TxInternalsListItem = ({ ...@@ -38,7 +38,7 @@ const TxInternalsListItem = ({
const isIn = Boolean(currentAddress && currentAddress === to?.hash); const isIn = Boolean(currentAddress && currentAddress === to?.hash);
return ( return (
<AccountListItemMobile rowGap={ 3 }> <ListItemMobile rowGap={ 3 }>
<Flex> <Flex>
{ typeTitle && <Tag colorScheme="cyan" mr={ 2 }>{ typeTitle }</Tag> } { typeTitle && <Tag colorScheme="cyan" mr={ 2 }>{ typeTitle }</Tag> }
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/> <TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
...@@ -71,7 +71,7 @@ const TxInternalsListItem = ({ ...@@ -71,7 +71,7 @@ const TxInternalsListItem = ({
{ BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() } { BigNumber(value).div(BigNumber(10 ** appConfig.network.currency.decimals)).toFormat() }
</Text> </Text>
</HStack> </HStack>
</AccountListItemMobile> </ListItemMobile>
); );
}; };
......
import React from 'react'; import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
const TxInternalsSkeletonDesktop = () => { const TxInternalsSkeletonDesktop = () => {
return ( return (
......
...@@ -6,7 +6,7 @@ import type { InternalTransaction } from 'types/api/internalTransaction'; ...@@ -6,7 +6,7 @@ import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link'; import link from 'lib/link/link';
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';
...@@ -36,12 +36,14 @@ const AddressIntTxsTableItem = ({ ...@@ -36,12 +36,14 @@ const AddressIntTxsTableItem = ({
const isOut = Boolean(currentAddress && currentAddress === from.hash); const isOut = Boolean(currentAddress && currentAddress === from.hash);
const isIn = Boolean(currentAddress && currentAddress === to?.hash); const isIn = Boolean(currentAddress && currentAddress === to?.hash);
const timeAgo = useTimeAgoIncrement(timestamp, true);
return ( return (
<Tr alignItems="top"> <Tr alignItems="top">
<Td verticalAlign="middle"> <Td verticalAlign="middle">
<Flex rowGap={ 3 } flexWrap="wrap"> <Flex rowGap={ 3 } flexWrap="wrap">
<AddressLink fontWeight="700" hash={ txnHash } type="transaction"/> <AddressLink fontWeight="700" hash={ txnHash } type="transaction"/>
<Text variant="secondary" fontWeight="400" fontSize="sm">{ dayjs(timestamp).fromNow() }</Text> { timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> }
</Flex> </Flex>
</Td> </Td>
<Td verticalAlign="middle"> <Td verticalAlign="middle">
......
...@@ -2,8 +2,8 @@ import React, { useCallback } from 'react'; ...@@ -2,8 +2,8 @@ import React, { useCallback } from 'react';
import type { ApiKey } from 'types/api/account'; import type { ApiKey } from 'types/api/account';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import ApiKeySnippet from 'ui/shared/ApiKeySnippet'; import ApiKeySnippet from 'ui/shared/ApiKeySnippet';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props { interface Props {
...@@ -23,10 +23,10 @@ const ApiKeyListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -23,10 +23,10 @@ const ApiKeyListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]); }, [ item, onDeleteClick ]);
return ( return (
<AccountListItemMobile> <ListItemMobile>
<ApiKeySnippet apiKey={ item.api_key } name={ item.name }/> <ApiKeySnippet apiKey={ item.api_key } name={ item.name }/>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
</AccountListItemMobile> </ListItemMobile>
); );
}; };
......
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip, Alert } from '@chakra-ui/react'; import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
...@@ -67,7 +67,7 @@ const BlockDetails = () => { ...@@ -67,7 +67,7 @@ const BlockDetails = () => {
if (isError) { if (isError) {
const is404 = error?.error?.status === 404; const is404 = error?.error?.status === 404;
return is404 ? <Alert>This block has not been processed yet.</Alert> : <DataFetchAlert/>; return is404 ? <span>This block has not been processed yet.</span> : <DataFetchAlert/>;
} }
const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>; const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>;
......
...@@ -11,13 +11,13 @@ import useIsMobile from 'lib/hooks/useIsMobile'; ...@@ -11,13 +11,13 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel'; import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage'; import useSocketMessage from 'lib/socket/useSocketMessage';
import BlocksList from 'ui/blocks/BlocksList'; import BlocksList from 'ui/blocks/BlocksList';
import BlocksSkeletonMobile from 'ui/blocks/BlocksSkeletonMobile';
import BlocksTable from 'ui/blocks/BlocksTable'; import BlocksTable from 'ui/blocks/BlocksTable';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
type QueryResult = UseQueryResult<BlocksResponse> & { type QueryResult = UseQueryResult<BlocksResponse> & {
pagination: PaginationProps; pagination: PaginationProps;
...@@ -83,7 +83,7 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -83,7 +83,7 @@ const BlocksContent = ({ type, query }: Props) => {
return ( return (
<> <>
<Show below="lg" key="skeleton-mobile" ssr={ false }> <Show below="lg" key="skeleton-mobile" ssr={ false }>
<BlocksSkeletonMobile/> <SkeletonList/>
</Show> </Show>
<Hide below="lg" key="skeleton-desktop" ssr={ false }> <Hide below="lg" key="skeleton-desktop" ssr={ false }>
<SkeletonTable columns={ [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }/> <SkeletonTable columns={ [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }/>
......
...@@ -12,9 +12,9 @@ import { WEI } from 'lib/consts'; ...@@ -12,9 +12,9 @@ import { WEI } from 'lib/consts';
import link from 'lib/link/link'; import link from 'lib/link/link';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import BlockTimestamp from 'ui/blocks/BlockTimestamp'; import BlockTimestamp from 'ui/blocks/BlockTimestamp';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio'; import GasUsedToTargetRatio from 'ui/shared/GasUsedToTargetRatio';
import ListItemMobile from 'ui/shared/ListItemMobile';
import Utilization from 'ui/shared/Utilization/Utilization'; import Utilization from 'ui/shared/Utilization/Utilization';
interface Props { interface Props {
...@@ -29,7 +29,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -29,7 +29,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
const txFees = BigNumber(data.tx_fees || 0); const txFees = BigNumber(data.tx_fees || 0);
return ( return (
<AccountListItemMobile rowGap={ 3 } key={ String(data.height) }> <ListItemMobile rowGap={ 3 } key={ String(data.height) } isAnimated>
<Flex justifyContent="space-between" w="100%"> <Flex justifyContent="space-between" w="100%">
<Flex columnGap={ 2 } alignItems="center"> <Flex columnGap={ 2 } alignItems="center">
{ isPending && <Spinner size="sm"/> } { isPending && <Spinner size="sm"/> }
...@@ -64,7 +64,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -64,7 +64,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</Box> </Box>
<Flex columnGap={ 2 }> <Flex columnGap={ 2 }>
<Text fontWeight={ 500 }>Reward { appConfig.network.currency.symbol }</Text> <Text fontWeight={ 500 }>Reward { appConfig.network.currency.symbol }</Text>
<Text variant="secondary">{ totalReward }</Text> <Text variant="secondary">{ totalReward.toFixed() }</Text>
</Flex> </Flex>
<Box> <Box>
<Text fontWeight={ 500 }>Burnt fees</Text> <Text fontWeight={ 500 }>Burnt fees</Text>
...@@ -76,7 +76,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -76,7 +76,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/> <Utilization ml={ 4 } value={ burntFees.div(txFees).toNumber() }/>
</Flex> </Flex>
</Box> </Box>
</AccountListItemMobile> </ListItemMobile>
); );
}; };
......
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const BlocksSkeletonMobile = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<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 } justifyContent="space-between">
<Skeleton w="75px"/>
<Skeleton w="90px"/>
</Flex>
<Skeleton h={ 6 } w="130px"/>
<Skeleton h={ 6 } w="180px"/>
<Skeleton h={ 6 } w="60px"/>
<Skeleton h={ 6 } w="100%"/>
<Skeleton h={ 6 } w="170px"/>
<Skeleton h={ 6 } w="100%"/>
</Flex>
)) }
</Box>
);
};
export default BlocksSkeletonMobile;
...@@ -13,9 +13,10 @@ import Pagination from 'ui/shared/Pagination'; ...@@ -13,9 +13,10 @@ import Pagination from 'ui/shared/Pagination';
interface Props { interface Props {
pagination: PaginationProps; pagination: PaginationProps;
isPaginationVisible: boolean;
} }
const BlocksTabSlot = ({ pagination }: Props) => { const BlocksTabSlot = ({ pagination, isPaginationVisible }: Props) => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const fetch = useFetch(); const fetch = useFetch();
...@@ -41,7 +42,7 @@ const BlocksTabSlot = ({ pagination }: Props) => { ...@@ -41,7 +42,7 @@ const BlocksTabSlot = ({ pagination }: Props) => {
</Text> </Text>
</Box> </Box>
) } ) }
<Pagination my={ 1 } { ...pagination }/> { isPaginationVisible && <Pagination my={ 1 } { ...pagination }/> }
</Flex> </Flex>
); );
}; };
......
...@@ -23,7 +23,7 @@ const BlocksTable = ({ data, top, page }: Props) => { ...@@ -23,7 +23,7 @@ const BlocksTable = ({ data, top, page }: Props) => {
<Thead top={ top }> <Thead top={ top }>
<Tr> <Tr>
<Th width="125px">Block</Th> <Th width="125px">Block</Th>
<Th width="120px">Size</Th> <Th width="120px">Size, bytes</Th>
<Th width="21%" minW="144px">{ capitalize(getNetworkValidatorTitle()) }</Th> <Th width="21%" minW="144px">{ capitalize(getNetworkValidatorTitle()) }</Th>
<Th width="64px" isNumeric>Txn</Th> <Th width="64px" isNumeric>Txn</Th>
<Th width="35%">Gas used</Th> <Th width="35%">Gas used</Th>
......
...@@ -48,7 +48,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -48,7 +48,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
</Flex> </Flex>
<BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/> <BlockTimestamp ts={ data.timestamp } isEnabled={ enableTimeIncrement }/>
</Td> </Td>
<Td fontSize="sm">{ data.size.toLocaleString('en') } bytes</Td> <Td fontSize="sm">{ data.size.toLocaleString('en') }</Td>
<Td fontSize="sm"> <Td fontSize="sm">
<AddressLink alias={ data.miner.name } hash={ data.miner.hash } truncation="constant" display="inline-flex" maxW="100%"/> <AddressLink alias={ data.miner.name } hash={ data.miner.hash } truncation="constant" display="inline-flex" maxW="100%"/>
</Td> </Td>
...@@ -60,7 +60,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => { ...@@ -60,7 +60,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<GasUsedToTargetRatio ml={ 2 } value={ data.gas_target_percentage || undefined }/> <GasUsedToTargetRatio ml={ 2 } value={ data.gas_target_percentage || undefined }/>
</Flex> </Flex>
</Td> </Td>
<Td fontSize="sm">{ totalReward }</Td> <Td fontSize="sm">{ totalReward.toFixed(8) }</Td>
<Td fontSize="sm"> <Td fontSize="sm">
<Flex alignItems="center" columnGap={ 1 }> <Flex alignItems="center" columnGap={ 1 }>
<Icon as={ flameIcon } boxSize={ 5 } color={ useColorModeValue('gray.500', 'inherit') }/> <Icon as={ flameIcon } boxSize={ 5 } color={ useColorModeValue('gray.500', 'inherit') }/>
......
...@@ -2,8 +2,8 @@ import React, { useCallback } from 'react'; ...@@ -2,8 +2,8 @@ import React, { useCallback } from 'react';
import type { CustomAbi } from 'types/api/account'; import type { CustomAbi } from 'types/api/account';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import AddressSnippet from 'ui/shared/AddressSnippet'; import AddressSnippet from 'ui/shared/AddressSnippet';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props { interface Props {
...@@ -23,10 +23,10 @@ const CustomAbiListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -23,10 +23,10 @@ const CustomAbiListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]); }, [ item, onDeleteClick ]);
return ( return (
<AccountListItemMobile> <ListItemMobile>
<AddressSnippet address={ item.contract_address_hash } subtitle={ item.name } isContract/> <AddressSnippet address={ item.contract_address_hash } subtitle={ item.name } isContract/>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
</AccountListItemMobile> </ListItemMobile>
); );
}; };
......
import { Alert, AlertIcon, AlertTitle, chakra } from '@chakra-ui/react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { IndexingStatus } from 'types/api/indexingStatus';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp, ndash } from 'lib/html-entities';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
const IndexingAlert = ({ className }: { className?: string }) => {
const fetch = useFetch();
const isMobile = useIsMobile();
const { data } = useQuery<unknown, unknown, IndexingStatus>(
[ QueryKeys.indexingStatus ],
async() => await fetch(`/node-api/index/indexing-status`),
);
const queryClient = useQueryClient();
const handleBlocksIndexStatus: SocketMessage.BlocksIndexStatus['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.indexingStatus ], (prevData: IndexingStatus | undefined) => {
const newData = prevData ? { ...prevData } : {} as IndexingStatus;
newData.finished_indexing_blocks = payload.finished;
newData.indexed_blocks_ratio = payload.ratio;
return newData;
});
}, [ queryClient ]);
const blockIndexingChannel = useSocketChannel({
topic: 'blocks:indexing',
isDisabled: !data || data.finished_indexing_blocks,
});
useSocketMessage({
channel: blockIndexingChannel,
event: 'block_index_status',
handler: handleBlocksIndexStatus,
});
const handleIntermalTxsIndexStatus: SocketMessage.InternalTxsIndexStatus['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.indexingStatus ], (prevData: IndexingStatus | undefined) => {
const newData = prevData ? { ...prevData } : {} as IndexingStatus;
newData.finished_indexing = payload.finished;
newData.indexed_inernal_transactions_ratio = payload.ratio;
return newData;
});
}, [ queryClient ]);
const internalTxsIndexingChannel = useSocketChannel({
topic: 'blocks:indexing_internal_transactions',
isDisabled: !data || data.finished_indexing,
});
useSocketMessage({
channel: internalTxsIndexingChannel,
event: 'internal_txs_index_status',
handler: handleIntermalTxsIndexStatus,
});
if (!data) {
return null;
}
let content;
if (data.finished_indexing_blocks === false) {
content = `${ data.indexed_blocks_ratio && `${ (Number(data.indexed_blocks_ratio) * 100).toFixed() }% Blocks Indexed${ nbsp }${ ndash } ` }
We're indexing this chain right now. Some of the counts may be inaccurate.` ;
} else if (data.finished_indexing === false) {
content = `${ data.indexed_inernal_transactions_ratio &&
`${ (Number(data.indexed_inernal_transactions_ratio) * 100).toFixed() }% Blocks With Internal Transactions Indexed${ nbsp }${ ndash } ` }
We're indexing this chain right now. Some of the counts may be inaccurate.`;
}
if (!content) {
return null;
}
return (
<Alert status="warning" variant="solid" py={ 3 } borderRadius="12px" mb={ 6 } className={ className }>
{ !isMobile && <AlertIcon/> }
<AlertTitle>
{ content }
</AlertTitle>
</Alert>
);
};
export default chakra(IndexingAlert);
...@@ -19,7 +19,7 @@ import LatestBlocksItem from './LatestBlocksItem'; ...@@ -19,7 +19,7 @@ import LatestBlocksItem from './LatestBlocksItem';
import LatestBlocksItemSkeleton from './LatestBlocksItemSkeleton'; import LatestBlocksItemSkeleton from './LatestBlocksItemSkeleton';
const BLOCK_HEIGHT = 166; const BLOCK_HEIGHT = 166;
const BLOCK_MARGIN = 24; const BLOCK_MARGIN = 12;
const LatestBlocks = () => { const LatestBlocks = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
......
...@@ -59,7 +59,7 @@ const LatestBlocksItem = ({ block, h }: Props) => { ...@@ -59,7 +59,7 @@ const LatestBlocksItem = ({ block, h }: Props) => {
<GridItem><Text variant="secondary">{ block.tx_count }</Text></GridItem> <GridItem><Text variant="secondary">{ block.tx_count }</Text></GridItem>
{ /* */ } { /* */ }
<GridItem>Reward</GridItem> <GridItem>Reward</GridItem>
<GridItem><Text variant="secondary">{ totalReward }</Text></GridItem> <GridItem><Text variant="secondary">{ totalReward.toFixed() }</Text></GridItem>
<GridItem>Miner</GridItem> <GridItem>Miner</GridItem>
<GridItem><AddressLink alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%"/></GridItem> <GridItem><AddressLink alias={ block.miner.name } hash={ block.miner.hash } truncation="constant" maxW="100%"/></GridItem>
</Grid> </Grid>
......
...@@ -12,6 +12,7 @@ import gasIcon from 'icons/gas.svg'; ...@@ -12,6 +12,7 @@ import gasIcon from 'icons/gas.svg';
import txIcon from 'icons/transactions.svg'; import txIcon from 'icons/transactions.svg';
import walletIcon from 'icons/wallet.svg'; import walletIcon from 'icons/wallet.svg';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import link from 'lib/link/link';
import StatsGasPrices from './StatsGasPrices'; import StatsGasPrices from './StatsGasPrices';
import StatsItem from './StatsItem'; import StatsItem from './StatsItem';
...@@ -45,13 +46,14 @@ const Stats = () => { ...@@ -45,13 +46,14 @@ const Stats = () => {
const lastItemTouchStyle = { gridColumn: { base: 'span 2', lg: 'unset' } }; const lastItemTouchStyle = { gridColumn: { base: 'span 2', lg: 'unset' } };
if (data) { if (data) {
const gasLabel = hasGasTracker ? <StatsGasPrices gasPrices={ data.gas_prices }/> : null; const gasLabel = hasGasTracker && data.gas_prices ? <StatsGasPrices gasPrices={ data.gas_prices }/> : null;
content = ( content = (
<> <>
<StatsItem <StatsItem
icon={ blockIcon } icon={ blockIcon }
title="Total blocks" title="Total blocks"
value={ Number(data.total_blocks).toLocaleString() } value={ Number(data.total_blocks).toLocaleString() }
url={ link('blocks') }
/> />
{ hasAvgBlockTime && ( { hasAvgBlockTime && (
<StatsItem <StatsItem
...@@ -64,6 +66,7 @@ const Stats = () => { ...@@ -64,6 +66,7 @@ const Stats = () => {
icon={ txIcon } icon={ txIcon }
title="Total transactions" title="Total transactions"
value={ Number(data.total_transactions).toLocaleString() } value={ Number(data.total_transactions).toLocaleString() }
url={ link('txs') }
/> />
<StatsItem <StatsItem
icon={ walletIcon } icon={ walletIcon }
...@@ -71,7 +74,7 @@ const Stats = () => { ...@@ -71,7 +74,7 @@ const Stats = () => {
value={ Number(data.total_addresses).toLocaleString() } value={ Number(data.total_addresses).toLocaleString() }
_last={ itemsCount % 2 ? lastItemTouchStyle : undefined } _last={ itemsCount % 2 ? lastItemTouchStyle : undefined }
/> />
{ hasGasTracker && ( { hasGasTracker && data.gas_prices && (
<StatsItem <StatsItem
icon={ gasIcon } icon={ gasIcon }
title="Gas tracker" title="Gas tracker"
......
import { Box, Flex, Icon, Text, useColorModeValue, chakra, Tooltip } from '@chakra-ui/react'; import { Box, Flex, Icon, Text, useColorModeValue, chakra, Tooltip, LightMode } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import infoIcon from 'icons/info.svg'; import infoIcon from 'icons/info.svg';
...@@ -10,11 +10,12 @@ type Props = { ...@@ -10,11 +10,12 @@ type Props = {
value: string; value: string;
className?: string; className?: string;
tooltipLabel?: React.ReactNode; tooltipLabel?: React.ReactNode;
url?: string;
} }
const LARGEST_BREAKPOINT = '1240px'; const LARGEST_BREAKPOINT = '1240px';
const StatsItem = ({ icon, title, value, className, tooltipLabel }: Props) => { const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const sxContainer = {} as any; const sxContainer = {} as any;
sxContainer[`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`] = { flexDirection: 'column' }; sxContainer[`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`] = { flexDirection: 'column' };
...@@ -38,6 +39,10 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel }: Props) => { ...@@ -38,6 +39,10 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel }: Props) => {
className={ className } className={ className }
color={ useColorModeValue('black', 'white') } color={ useColorModeValue('black', 'white') }
position="relative" position="relative"
{ ...(url ? {
as: 'a',
href: url,
} : {}) }
> >
<Icon as={ icon } boxSize={ 7 }/> <Icon as={ icon } boxSize={ 7 }/>
<Flex <Flex
...@@ -49,18 +54,22 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel }: Props) => { ...@@ -49,18 +54,22 @@ const StatsItem = ({ icon, title, value, className, tooltipLabel }: Props) => {
<Text fontWeight={ 500 } fontSize="md" color={ useColorModeValue('black', 'white') }>{ value }</Text> <Text fontWeight={ 500 } fontSize="md" color={ useColorModeValue('black', 'white') }>{ value }</Text>
</Flex> </Flex>
{ tooltipLabel && ( { tooltipLabel && (
<Tooltip label={ tooltipLabel } hasArrow={ false } borderRadius="12px" placement="bottom-end" offset={ [ 0, 0 ] }> <LightMode>
<Box <Tooltip label={ tooltipLabel } hasArrow={ false } borderRadius="12px" placement="bottom-end" offset={ [ 0, 0 ] } bgColor="blackAlpha.900">
position="absolute" <Box
top={{ base: 'calc(50% - 12px)', lg: '10px', xl: 'calc(50% - 12px)' }} position="absolute"
right="10px"> top={{ base: 'calc(50% - 12px)', lg: '10px', xl: 'calc(50% - 12px)' }}
<Icon right="10px"
as={ infoIcon } cursor="pointer"
boxSize={ 6 } >
color={ infoColor } <Icon
/> as={ infoIcon }
</Box> boxSize={ 6 }
</Tooltip> color={ infoColor }
/>
</Box>
</Tooltip>
</LightMode>
) } ) }
</Flex> </Flex>
); );
......
...@@ -40,7 +40,7 @@ const AddressPageContent = () => { ...@@ -40,7 +40,7 @@ const AddressPageContent = () => {
{ id: 'txs', title: 'Transactions', component: <AddressTxs/> }, { id: 'txs', title: 'Transactions', component: <AddressTxs/> },
{ id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers/> }, { id: 'token_transfers', title: 'Token transfers', component: <AddressTokenTransfers/> },
{ id: 'tokens', title: 'Tokens', component: null }, { id: 'tokens', title: 'Tokens', component: null },
{ id: 'internal_txn', title: 'Internal txn', component: <AddressInternalTxs/> }, { id: 'internal_txns', title: 'Internal txns', component: <AddressInternalTxs/> },
{ id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance addressQuery={ addressQuery }/> }, { id: 'coin_balance_history', title: 'Coin balance history', component: <AddressCoinBalance addressQuery={ addressQuery }/> },
// temporary show this tab in all address // temporary show this tab in all address
// later api will return info about available tabs // later api will return info about available tabs
......
...@@ -15,8 +15,8 @@ import AccountPageDescription from 'ui/shared/AccountPageDescription'; ...@@ -15,8 +15,8 @@ import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile'; import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
const DATA_LIMIT = 3; const DATA_LIMIT = 3;
...@@ -60,7 +60,7 @@ const ApiKeysPage: React.FC = () => { ...@@ -60,7 +60,7 @@ const ApiKeysPage: React.FC = () => {
const content = (() => { const content = (() => {
if (isLoading && !data) { if (isLoading && !data) {
const loader = isMobile ? <SkeletonAccountMobile/> : ( const loader = isMobile ? <SkeletonListAccount/> : (
<> <>
<SkeletonTable columns={ [ '100%', '108px' ] }/> <SkeletonTable columns={ [ '100%', '108px' ] }/>
<Skeleton height="48px" width="156px" marginTop={ 8 }/> <Skeleton height="48px" width="156px" marginTop={ 8 }/>
......
import { Flex, Icon, Link, Tooltip } from '@chakra-ui/react';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import eastArrowIcon from 'icons/arrows/east.svg';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import isBrowser from 'lib/isBrowser';
import BlockDetails from 'ui/block/BlockDetails'; import BlockDetails from 'ui/block/BlockDetails';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
...@@ -22,6 +26,8 @@ const TAB_LIST_PROPS = { ...@@ -22,6 +26,8 @@ const TAB_LIST_PROPS = {
const BlockPageContent = () => { const BlockPageContent = () => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const isInBrowser = isBrowser();
const appProps = useAppContext();
const blockTxsQuery = useQueryWithPages({ const blockTxsQuery = useQueryWithPages({
apiPath: `/node-api/blocks/${ router.query.id }/transactions`, apiPath: `/node-api/blocks/${ router.query.id }/transactions`,
...@@ -40,12 +46,23 @@ const BlockPageContent = () => { ...@@ -40,12 +46,23 @@ const BlockPageContent = () => {
{ id: 'txs', title: 'Transactions', component: <TxsContent query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> }, { id: 'txs', title: 'Transactions', component: <TxsContent query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> },
]; ];
const isPaginatorHidden = !blockTxsQuery.isLoading && !blockTxsQuery.isError && blockTxsQuery.pagination.page === 1 && !blockTxsQuery.pagination.hasNextPage; const hasPagination = !isMobile && router.query.tab === 'txs' && blockTxsQuery.isPaginationVisible;
const hasPagination = !isMobile && router.query.tab === 'txs' && !isPaginatorHidden;
const referrer = isInBrowser ? window.document.referrer : appProps.referrer;
const hasGoBackLink = referrer && referrer.includes('/blocks');
return ( return (
<Page> <Page>
<PageTitle text={ `Block #${ router.query.id }` }/> <Flex alignItems="center" columnGap={ 3 }>
{ hasGoBackLink && (
<Tooltip label="Back to blocks list">
<Link mb={ 6 } display="inline-flex" href={ referrer }>
<Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)"/>
</Link>
</Tooltip>
) }
<PageTitle text={ `Block #${ router.query.id }` }/>
</Flex>
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS } tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
......
...@@ -55,7 +55,7 @@ const BlocksPageContent = () => { ...@@ -55,7 +55,7 @@ const BlocksPageContent = () => {
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS } tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ <BlocksTabSlot pagination={ blocksQuery.pagination }/> } rightSlot={ <BlocksTabSlot pagination={ blocksQuery.pagination } isPaginationVisible={ blocksQuery.isPaginationVisible }/> }
stickyEnabled={ !isMobile } stickyEnabled={ !isMobile }
/> />
</Page> </Page>
......
...@@ -14,8 +14,8 @@ import AccountPageDescription from 'ui/shared/AccountPageDescription'; ...@@ -14,8 +14,8 @@ import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile'; import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
const CustomAbiPage: React.FC = () => { const CustomAbiPage: React.FC = () => {
const customAbiModalProps = useDisclosure(); const customAbiModalProps = useDisclosure();
...@@ -56,7 +56,7 @@ const CustomAbiPage: React.FC = () => { ...@@ -56,7 +56,7 @@ const CustomAbiPage: React.FC = () => {
const content = (() => { const content = (() => {
if (isLoading && !data) { if (isLoading && !data) {
const loader = isMobile ? <SkeletonAccountMobile/> : ( const loader = isMobile ? <SkeletonListAccount/> : (
<> <>
<SkeletonTable columns={ [ '100%', '108px' ] }/> <SkeletonTable columns={ [ '100%', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/> <Skeleton height="44px" width="156px" marginTop={ 8 }/>
......
...@@ -43,5 +43,5 @@ test('default view -@default +@desktop-xl +@mobile +@dark-mode', async({ mount, ...@@ -43,5 +43,5 @@ test('default view -@default +@desktop-xl +@mobile +@dark-mode', async({ mount,
await page.evaluate(insertAdText); await page.evaluate(insertAdText);
await expect(component).toHaveScreenshot(); await expect(component.locator('main')).toHaveScreenshot();
}); });
import { Flex, Link, Icon, Tag } from '@chakra-ui/react'; import { Flex, Link, Icon, Tag, Tooltip } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -26,7 +26,7 @@ import TxTokenTransfer from 'ui/tx/TxTokenTransfer'; ...@@ -26,7 +26,7 @@ import TxTokenTransfer from 'ui/tx/TxTokenTransfer';
const TABS: Array<RoutedTab> = [ const TABS: Array<RoutedTab> = [
{ id: 'index', title: 'Details', component: <TxDetails/> }, { id: 'index', title: 'Details', component: <TxDetails/> },
{ id: 'token_transfers', title: 'Token transfers', component: <TxTokenTransfer/> }, { id: 'token_transfers', title: 'Token transfers', component: <TxTokenTransfer/> },
{ id: 'internal', title: 'Internal txn', component: <TxInternals/> }, { id: 'internal', title: 'Internal txns', component: <TxInternals/> },
{ id: 'logs', title: 'Logs', component: <TxLogs/> }, { id: 'logs', title: 'Logs', component: <TxLogs/> },
// will be implemented later, api is not ready // will be implemented later, api is not ready
// { id: 'state', title: 'State', component: <TxState/> }, // { id: 'state', title: 'State', component: <TxState/> },
...@@ -60,14 +60,17 @@ const TransactionPageContent = () => { ...@@ -60,14 +60,17 @@ const TransactionPageContent = () => {
return ( return (
<Page> <Page>
{ hasGoBackLink && (
<Link mb={ 6 } display="inline-flex" href={ referrer }>
<Icon as={ eastArrowIcon } boxSize={ 6 } mr={ 2 } transform="rotate(180deg)"/>
Transactions
</Link>
) }
<Flex alignItems="flex-start" flexDir={{ base: 'column', lg: 'row' }}> <Flex alignItems="flex-start" flexDir={{ base: 'column', lg: 'row' }}>
<PageTitle text="Transaction details"/> <Flex alignItems="center" columnGap={ 3 }>
{ hasGoBackLink && (
<Tooltip label="Back to transactions list">
<Link display="inline-flex" href={ referrer } mb={ 6 }>
<Icon as={ eastArrowIcon } boxSize={ 6 } transform="rotate(180deg)"/>
</Link>
</Tooltip>
) }
<PageTitle text="Transaction details"/>
</Flex>
{ data?.tx_tag && <Tag my={ 2 } ml={ 3 }>{ data.tx_tag }</Tag> } { data?.tx_tag && <Tag my={ 2 } ml={ 3 }>{ data.tx_tag }</Tag> }
{ explorersLinks.length > 0 && ( { explorersLinks.length > 0 && (
<Flex <Flex
......
...@@ -43,7 +43,7 @@ const Transactions = () => { ...@@ -43,7 +43,7 @@ const Transactions = () => {
<RoutedTabs <RoutedTabs
tabs={ tabs } tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS } tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ <TxsTabSlot pagination={ txsQuery.pagination }/> } rightSlot={ <TxsTabSlot pagination={ txsQuery.pagination } isPaginationVisible={ txsQuery.isPaginationVisible && !isMobile }/> }
stickyEnabled={ !isMobile } stickyEnabled={ !isMobile }
/> />
</Box> </Box>
......
...@@ -13,8 +13,8 @@ import AccountPageDescription from 'ui/shared/AccountPageDescription'; ...@@ -13,8 +13,8 @@ import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile'; import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import AddressModal from 'ui/watchlist/AddressModal/AddressModal'; import AddressModal from 'ui/watchlist/AddressModal/AddressModal';
import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal'; import DeleteAddressModal from 'ui/watchlist/DeleteAddressModal';
import WatchListItem from 'ui/watchlist/WatchlistTable/WatchListItem'; import WatchListItem from 'ui/watchlist/WatchlistTable/WatchListItem';
...@@ -98,7 +98,7 @@ const WatchList: React.FC = () => { ...@@ -98,7 +98,7 @@ const WatchList: React.FC = () => {
let content; let content;
if (isLoading && !data) { if (isLoading && !data) {
const loader = isMobile ? <SkeletonAccountMobile showFooterSlot/> : ( const loader = isMobile ? <SkeletonListAccount showFooterSlot/> : (
<> <>
<SkeletonTable columns={ [ '70%', '30%', '160px', '108px' ] }/> <SkeletonTable columns={ [ '70%', '30%', '160px', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/> <Skeleton height="44px" width="156px" marginTop={ 8 }/>
......
...@@ -3,8 +3,8 @@ import React, { useCallback } from 'react'; ...@@ -3,8 +3,8 @@ import React, { useCallback } from 'react';
import type { AddressTag } from 'types/api/account'; import type { AddressTag } from 'types/api/account';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import AddressSnippet from 'ui/shared/AddressSnippet'; import AddressSnippet from 'ui/shared/AddressSnippet';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
interface Props { interface Props {
...@@ -23,7 +23,7 @@ const AddressTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -23,7 +23,7 @@ const AddressTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]); }, [ item, onDeleteClick ]);
return ( return (
<AccountListItemMobile> <ListItemMobile>
<Flex alignItems="flex-start" flexDirection="column" maxW="100%"> <Flex alignItems="flex-start" flexDirection="column" maxW="100%">
<AddressSnippet address={ item.address_hash }/> <AddressSnippet address={ item.address_hash }/>
<HStack spacing={ 3 } mt={ 4 }> <HStack spacing={ 3 } mt={ 4 }>
...@@ -34,7 +34,7 @@ const AddressTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -34,7 +34,7 @@ const AddressTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
</HStack> </HStack>
</Flex> </Flex>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
</AccountListItemMobile> </ListItemMobile>
); );
}; };
......
...@@ -7,8 +7,8 @@ import useApiQuery from 'lib/api/useApiQuery'; ...@@ -7,8 +7,8 @@ import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile'; import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import AddressModal from './AddressModal/AddressModal'; import AddressModal from './AddressModal/AddressModal';
import AddressTagListItem from './AddressTagTable/AddressTagListItem'; import AddressTagListItem from './AddressTagTable/AddressTagListItem';
...@@ -53,7 +53,7 @@ const PrivateAddressTags = () => { ...@@ -53,7 +53,7 @@ const PrivateAddressTags = () => {
); );
if (isLoading && !addressTagsData) { if (isLoading && !addressTagsData) {
const loader = isMobile ? <SkeletonAccountMobile/> : ( const loader = isMobile ? <SkeletonListAccount/> : (
<> <>
<SkeletonTable columns={ [ '60%', '40%', '108px' ] }/> <SkeletonTable columns={ [ '60%', '40%', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/> <Skeleton height="44px" width="156px" marginTop={ 8 }/>
......
...@@ -7,8 +7,8 @@ import useApiQuery from 'lib/api/useApiQuery'; ...@@ -7,8 +7,8 @@ import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile'; import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import DeletePrivateTagModal from './DeletePrivateTagModal'; import DeletePrivateTagModal from './DeletePrivateTagModal';
import TransactionModal from './TransactionModal/TransactionModal'; import TransactionModal from './TransactionModal/TransactionModal';
...@@ -53,7 +53,7 @@ const PrivateTransactionTags = () => { ...@@ -53,7 +53,7 @@ const PrivateTransactionTags = () => {
); );
if (isLoading && !transactionTagsData) { if (isLoading && !transactionTagsData) {
const loader = isMobile ? <SkeletonAccountMobile/> : ( const loader = isMobile ? <SkeletonListAccount/> : (
<> <>
<SkeletonTable columns={ [ '75%', '25%', '108px' ] }/> <SkeletonTable columns={ [ '75%', '25%', '108px' ] }/>
<Skeleton height="44px" width="156px" marginTop={ 8 }/> <Skeleton height="44px" width="156px" marginTop={ 8 }/>
......
...@@ -3,7 +3,7 @@ import React, { useCallback } from 'react'; ...@@ -3,7 +3,7 @@ import React, { useCallback } from 'react';
import type { TransactionTag } from 'types/api/account'; import type { TransactionTag } from 'types/api/account';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TransactionSnippet from 'ui/shared/TransactionSnippet'; import TransactionSnippet from 'ui/shared/TransactionSnippet';
...@@ -23,7 +23,7 @@ const TransactionTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => ...@@ -23,7 +23,7 @@ const TransactionTagListItem = ({ item, onEditClick, onDeleteClick }: Props) =>
}, [ item, onDeleteClick ]); }, [ item, onDeleteClick ]);
return ( return (
<AccountListItemMobile> <ListItemMobile>
<Flex alignItems="flex-start" flexDirection="column" maxW="100%"> <Flex alignItems="flex-start" flexDirection="column" maxW="100%">
<TransactionSnippet hash={ item.transaction_hash }/> <TransactionSnippet hash={ item.transaction_hash }/>
<HStack spacing={ 3 } mt={ 4 }> <HStack spacing={ 3 } mt={ 4 }>
...@@ -34,7 +34,7 @@ const TransactionTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => ...@@ -34,7 +34,7 @@ const TransactionTagListItem = ({ item, onEditClick, onDeleteClick }: Props) =>
</HStack> </HStack>
</Flex> </Flex>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
</AccountListItemMobile> </ListItemMobile>
); );
}; };
......
...@@ -3,8 +3,8 @@ import React, { useCallback } from 'react'; ...@@ -3,8 +3,8 @@ import React, { useCallback } from 'react';
import type { PublicTag } from 'types/api/account'; import type { PublicTag } from 'types/api/account';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile';
import AddressSnippet from 'ui/shared/AddressSnippet'; import AddressSnippet from 'ui/shared/AddressSnippet';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
...@@ -24,7 +24,7 @@ const PublicTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -24,7 +24,7 @@ const PublicTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ item, onDeleteClick ]); }, [ item, onDeleteClick ]);
return ( return (
<AccountListItemMobile> <ListItemMobile>
<VStack spacing={ 3 } alignItems="flex-start" maxW="100%"> <VStack spacing={ 3 } alignItems="flex-start" maxW="100%">
<VStack spacing={ 4 } alignItems="unset" maxW="100%"> <VStack spacing={ 4 } alignItems="unset" maxW="100%">
{ item.addresses.map((address) => <AddressSnippet key={ address } address={ address }/>) } { item.addresses.map((address) => <AddressSnippet key={ address } address={ address }/>) }
...@@ -49,7 +49,7 @@ const PublicTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -49,7 +49,7 @@ const PublicTagListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
</HStack> </HStack>
</VStack> </VStack>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
</AccountListItemMobile> </ListItemMobile>
); );
}; };
......
...@@ -8,8 +8,8 @@ import useIsMobile from 'lib/hooks/useIsMobile'; ...@@ -8,8 +8,8 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import PublicTagListItem from 'ui/publicTags/PublicTagTable/PublicTagListItem'; import PublicTagListItem from 'ui/publicTags/PublicTagTable/PublicTagListItem';
import AccountPageDescription from 'ui/shared/AccountPageDescription'; import AccountPageDescription from 'ui/shared/AccountPageDescription';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import SkeletonAccountMobile from 'ui/shared/SkeletonAccountMobile'; import SkeletonListAccount from 'ui/shared/skeletons/SkeletonListAccount';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import DeletePublicTagModal from './DeletePublicTagModal'; import DeletePublicTagModal from './DeletePublicTagModal';
import PublicTagTable from './PublicTagTable/PublicTagTable'; import PublicTagTable from './PublicTagTable/PublicTagTable';
...@@ -54,7 +54,7 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => { ...@@ -54,7 +54,7 @@ const PublicTagsData = ({ changeToFormScreen, onTagDelete }: Props) => {
); );
if (isLoading) { if (isLoading) {
const loader = isMobile ? <SkeletonAccountMobile/> : ( const loader = isMobile ? <SkeletonListAccount/> : (
<> <>
<SkeletonTable columns={ [ '50%', '25%', '25%', '108px' ] }/> <SkeletonTable columns={ [ '50%', '25%', '25%', '108px' ] }/>
<Skeleton height="48px" width="270px" marginTop={ 8 }/> <Skeleton height="48px" width="270px" marginTop={ 8 }/>
......
import { import {
Icon, Icon,
Center,
useColorModeValue, useColorModeValue,
chakra, chakra,
Button,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
...@@ -14,13 +14,16 @@ interface Props { ...@@ -14,13 +14,16 @@ interface Props {
onClick?: () => void; onClick?: () => void;
} }
const AdditionalInfoButton = ({ isOpen, onClick, className }: Props, ref: React.ForwardedRef<HTMLDivElement>) => { const AdditionalInfoButton = ({ isOpen, onClick, className }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const infoBgColor = useColorModeValue('blue.50', 'gray.600'); const infoBgColor = useColorModeValue('blue.50', 'gray.600');
const infoColor = useColorModeValue('blue.600', 'blue.300'); const infoColor = useColorModeValue('blue.600', 'blue.300');
return ( return (
<Center <Button
variant="unstyled"
display="inline-flex"
alignItems="center"
className={ className } className={ className }
ref={ ref } ref={ ref }
background={ isOpen ? infoBgColor : 'unset' } background={ isOpen ? infoBgColor : 'unset' }
...@@ -36,7 +39,7 @@ const AdditionalInfoButton = ({ isOpen, onClick, className }: Props, ref: React. ...@@ -36,7 +39,7 @@ const AdditionalInfoButton = ({ isOpen, onClick, className }: Props, ref: React.
color={ infoColor } color={ infoColor }
_hover={{ color: 'blue.400' }} _hover={{ color: 'blue.400' }}
/> />
</Center> </Button>
); );
}; };
......
...@@ -3,11 +3,12 @@ import React from 'react'; ...@@ -3,11 +3,12 @@ import React from 'react';
interface Props { interface Props {
hash: string; hash: string;
isTooltipDisabled?: boolean;
} }
const HashStringShorten = ({ hash }: Props) => { const HashStringShorten = ({ hash, isTooltipDisabled }: Props) => {
return ( return (
<Tooltip label={ hash }> <Tooltip label={ hash } isDisabled={ isTooltipDisabled }>
{ hash.slice(0, 4) + '...' + hash.slice(-4) } { hash.slice(0, 4) + '...' + hash.slice(-4) }
</Tooltip> </Tooltip>
); );
......
...@@ -22,9 +22,10 @@ const HEAD_MIN_LENGTH = 4; ...@@ -22,9 +22,10 @@ const HEAD_MIN_LENGTH = 4;
interface Props { interface Props {
hash: string; hash: string;
fontWeight?: string | number; fontWeight?: string | number;
isTooltipDisabled?: boolean;
} }
const HashStringShortenDynamic = ({ hash, fontWeight = '400' }: Props) => { const HashStringShortenDynamic = ({ hash, fontWeight = '400', isTooltipDisabled }: Props) => {
const elementRef = useRef<HTMLSpanElement>(null); const elementRef = useRef<HTMLSpanElement>(null);
const [ displayedString, setDisplayedString ] = React.useState(hash); const [ displayedString, setDisplayedString ] = React.useState(hash);
...@@ -90,7 +91,7 @@ const HashStringShortenDynamic = ({ hash, fontWeight = '400' }: Props) => { ...@@ -90,7 +91,7 @@ const HashStringShortenDynamic = ({ hash, fontWeight = '400' }: Props) => {
if (isTruncated) { if (isTruncated) {
return ( return (
<Tooltip label={ hash }>{ content }</Tooltip> <Tooltip label={ hash } isDisabled={ isTooltipDisabled }>{ content }</Tooltip>
); );
} }
......
...@@ -5,18 +5,17 @@ import React from 'react'; ...@@ -5,18 +5,17 @@ import React from 'react';
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
key?: string; isAnimated?: boolean;
} }
const AccountListItemMobile = ({ children, className, key }: Props) => { const ListItemMobile = ({ children, className, isAnimated }: Props) => {
return ( return (
<Flex <Flex
as={ motion.div } as={ motion.div }
initial={{ opacity: 0, scale: 0.97 }} initial={ isAnimated ? { opacity: 0, scale: 0.97 } : { opacity: 1, scale: 1 } }
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transitionDuration="normal" transitionDuration="normal"
transitionTimingFunction="linear" transitionTimingFunction="linear"
key={ key }
rowGap={ 6 } rowGap={ 6 }
alignItems="flex-start" alignItems="flex-start"
flexDirection="column" flexDirection="column"
...@@ -33,4 +32,4 @@ const AccountListItemMobile = ({ children, className, key }: Props) => { ...@@ -33,4 +32,4 @@ const AccountListItemMobile = ({ children, className, key }: Props) => {
); );
}; };
export default chakra(AccountListItemMobile); export default chakra(ListItemMobile);
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import Page from './Page';
const API_URL = '/node-api/index/indexing-status';
test('without indexing alert +@mobile', async({ mount }) => {
const component = await mount(
<TestApp>
<Page>Page Content</Page>
</TestApp>,
);
await expect(component).toHaveScreenshot();
});
test('with indexing alert +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ finished_indexing_blocks: false, indexed_blocks_ratio: 0.1 }),
}));
const component = await mount(
<TestApp>
<Page>Page Content</Page>
</TestApp>,
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import IndexingAlert from 'ui/home/IndexingAlert';
interface Props { interface Props {
children: React.ReactNode; children: React.ReactNode;
isHomePage?: boolean; isHomePage?: boolean;
...@@ -13,8 +15,9 @@ const PageContent = ({ children, isHomePage }: Props) => { ...@@ -13,8 +15,9 @@ const PageContent = ({ children, isHomePage }: Props) => {
w="100%" w="100%"
paddingX={{ base: 4, lg: 12 }} paddingX={{ base: 4, lg: 12 }}
paddingBottom={ 10 } paddingBottom={ 10 }
paddingTop={{ base: isHomePage ? '88px' : '138px', lg: isHomePage ? 9 : 0 }} paddingTop={{ base: isHomePage ? '88px' : '138px', lg: 0 }}
> >
<IndexingAlert display={{ base: 'block', lg: 'none' }}/>
{ children } { children }
</Box> </Box>
); );
......
...@@ -32,3 +32,25 @@ test('without tx info +@mobile', async({ mount, page }) => { ...@@ -32,3 +32,25 @@ test('without tx info +@mobile', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('with tx info +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(tokenTransferMock.mixTokens),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<TokenTransfer
path={ API_URL }
queryName={ QueryKeys.txTokenTransfers }
showTxInfo={ true }
/>
</TestApp>,
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
...@@ -17,11 +17,11 @@ import EmptySearchResult from 'ui/apps/EmptySearchResult'; ...@@ -17,11 +17,11 @@ import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers'; import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter'; import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList'; import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferSkeletonMobile from 'ui/shared/TokenTransfer/TokenTransferSkeletonMobile';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable'; import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
import { TOKEN_TYPE } from './helpers'; import { TOKEN_TYPE } from './helpers';
...@@ -43,15 +43,25 @@ interface Props { ...@@ -43,15 +43,25 @@ interface Props {
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
txHash?: string; txHash?: string;
enableTimeIncrement?: boolean;
} }
const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryIds, path, baseAddress, showTxInfo = true }: Props) => { const TokenTransfer = ({
isLoading: isLoadingProp,
isDisabled,
queryName,
queryIds,
path,
baseAddress,
showTxInfo = true,
enableTimeIncrement,
}: Props) => {
const router = useRouter(); const router = useRouter();
const [ filters, setFilters ] = React.useState<AddressTokenTransferFilters & TokenTransferFilters>( const [ filters, setFilters ] = React.useState<AddressTokenTransferFilters & TokenTransferFilters>(
{ type: getTokenFilterValue(router.query.type), filter: getAddressFilterValue(router.query.filter) }, { type: getTokenFilterValue(router.query.type), filter: getAddressFilterValue(router.query.filter) },
); );
const { isError, isLoading, data, pagination, onFilterChange } = useQueryWithPages({ const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
apiPath: path, apiPath: path,
queryName, queryName,
queryIds, queryIds,
...@@ -85,7 +95,7 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI ...@@ -85,7 +95,7 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI
/> />
</Hide> </Hide>
<Show below="lg"> <Show below="lg">
<TokenTransferSkeletonMobile showTxInfo={ showTxInfo }/> <SkeletonList/>
</Show> </Show>
</> </>
); );
...@@ -107,10 +117,10 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI ...@@ -107,10 +117,10 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI
return ( return (
<> <>
<Hide below="lg"> <Hide below="lg">
<TokenTransferTable data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } top={ 80 }/> <TokenTransferTable data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } top={ 80 } enableTimeIncrement={ enableTimeIncrement }/>
</Hide> </Hide>
<Show below="lg"> <Show below="lg">
<TokenTransferList data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/> <TokenTransferList data={ items } baseAddress={ baseAddress } showTxInfo={ showTxInfo } enableTimeIncrement={ enableTimeIncrement }/>
</Show> </Show>
</> </>
); );
...@@ -128,7 +138,7 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI ...@@ -128,7 +138,7 @@ const TokenTransfer = ({ isLoading: isLoadingProp, isDisabled, queryName, queryI
onAddressFilterChange={ handleAddressFilterChange } onAddressFilterChange={ handleAddressFilterChange }
defaultAddressFilter={ filters.filter } defaultAddressFilter={ filters.filter }
/> />
<Pagination ml="auto" { ...pagination }/> { isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
</ActionBar> </ActionBar>
) } ) }
{ content } { content }
......
...@@ -9,12 +9,21 @@ interface Props { ...@@ -9,12 +9,21 @@ interface Props {
data: Array<TokenTransfer>; data: Array<TokenTransfer>;
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
enableTimeIncrement?: boolean;
} }
const TokenTransferList = ({ data, baseAddress, showTxInfo }: Props) => { const TokenTransferList = ({ data, baseAddress, showTxInfo, enableTimeIncrement }: Props) => {
return ( return (
<Box> <Box>
{ data.map((item, index) => <TokenTransferListItem key={ index } { ...item } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/>) } { data.map((item, index) => (
<TokenTransferListItem
key={ index }
{ ...item }
baseAddress={ baseAddress }
showTxInfo={ showTxInfo }
enableTimeIncrement={ enableTimeIncrement }
/>
)) }
</Box> </Box>
); );
}; };
......
import { Text, Flex, Tag, Icon } from '@chakra-ui/react'; import { Text, Flex, Tag, Icon, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import eastArrowIcon from 'icons/arrows/east.svg'; import eastArrowIcon from 'icons/arrows/east.svg';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile'; import transactionIcon from 'icons/transactions.svg';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton'; import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
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 InOutTag from 'ui/shared/InOutTag'; import InOutTag from 'ui/shared/InOutTag';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet'; import TokenSnippet from 'ui/shared/TokenSnippet/TokenSnippet';
import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers'; import { getTokenTransferTypeText } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
...@@ -18,9 +20,21 @@ import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; ...@@ -18,9 +20,21 @@ import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer & { type Props = TokenTransfer & {
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
enableTimeIncrement?: boolean;
} }
const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAddress, showTxInfo, type }: Props) => { const TokenTransferListItem = ({
token,
total,
tx_hash: txHash,
from,
to,
baseAddress,
showTxInfo,
type,
timestamp,
enableTimeIncrement,
}: Props) => {
const value = (() => { const value = (() => {
if (!('value' in total)) { if (!('value' in total)) {
return null; return null;
...@@ -29,9 +43,13 @@ const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAd ...@@ -29,9 +43,13 @@ const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAd
return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat(); return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat();
})(); })();
const iconColor = useColorModeValue('blue.600', 'blue.300');
const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement);
const addressWidth = `calc((100% - ${ baseAddress ? '50px' : '0px' }) / 2)`; const addressWidth = `calc((100% - ${ baseAddress ? '50px' : '0px' }) / 2)`;
return ( return (
<AccountListItemMobile rowGap={ 3 }> <ListItemMobile rowGap={ 3 } isAnimated>
<Flex w="100%" flexWrap="wrap" rowGap={ 1 } position="relative"> <Flex w="100%" flexWrap="wrap" rowGap={ 1 } position="relative">
<TokenSnippet hash={ token.address } w="auto" maxW="calc(100% - 140px)" name={ token.name || 'Unnamed token' }/> <TokenSnippet hash={ token.address } w="auto" maxW="calc(100% - 140px)" name={ token.name || 'Unnamed token' }/>
<Tag flexShrink={ 0 } ml={ 2 } mr={ 2 }>{ token.type }</Tag> <Tag flexShrink={ 0 } ml={ 2 } mr={ 2 }>{ token.type }</Tag>
...@@ -40,11 +58,24 @@ const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAd ...@@ -40,11 +58,24 @@ const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAd
</Flex> </Flex>
{ 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> } { 'token_id' in total && <TokenTransferNft hash={ token.address } id={ total.token_id }/> }
{ showTxInfo && ( { showTxInfo && (
<Flex columnGap={ 2 } w="100%"> <Flex justifyContent="space-between" alignItems="center" lineHeight="24px" width="100%">
<Text fontWeight={ 500 } flexShrink={ 0 }>Txn hash</Text> <Flex>
<Address display="inline-flex" maxW="100%"> <Icon
<AddressLink type="transaction" hash={ txHash }/> as={ transactionIcon }
</Address> boxSize="30px"
mr={ 2 }
color={ iconColor }
/>
<Address width="100%">
<AddressLink
hash={ txHash }
type="transaction"
fontWeight="700"
truncation="constant"
/>
</Address>
</Flex>
{ timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> }
</Flex> </Flex>
) } ) }
<Flex w="100%" columnGap={ 3 }> <Flex w="100%" columnGap={ 3 }>
...@@ -67,7 +98,7 @@ const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAd ...@@ -67,7 +98,7 @@ const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAd
<Text variant="secondary">{ value }</Text> <Text variant="secondary">{ value }</Text>
</Flex> </Flex>
) } ) }
</AccountListItemMobile> </ListItemMobile>
); );
}; };
......
import { Skeleton, SkeletonCircle, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
const TokenTransferSkeletonMobile = ({ showTxInfo }: { showTxInfo?: boolean }) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<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 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton w="100px" mr={ 2 }/>
<Skeleton w="50px"/>
{ showTxInfo && <Skeleton w="24px" ml="auto"/> }
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="70px"/>
<Skeleton w="24px"/>
<Skeleton w="90px"/>
</Flex>
{ showTxInfo && (
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="70px" flexShrink={ 0 }/>
<Skeleton w="100%"/>
</Flex>
) }
<Flex h={ 6 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
<Skeleton w="50px" mr={ 3 }/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/>
</Flex>
<Flex h={ 6 } columnGap={ 2 }>
<Skeleton w="45px"/>
<Skeleton w="90px"/>
</Flex>
</Flex>
)) }
</Box>
);
};
export default TokenTransferSkeletonMobile;
...@@ -11,9 +11,10 @@ interface Props { ...@@ -11,9 +11,10 @@ interface Props {
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
top: number; top: number;
enableTimeIncrement?: boolean;
} }
const TokenTransferTable = ({ data, baseAddress, showTxInfo, top }: Props) => { const TokenTransferTable = ({ data, baseAddress, showTxInfo, top, enableTimeIncrement }: Props) => {
return ( return (
<Table variant="simple" size="sm"> <Table variant="simple" size="sm">
...@@ -31,7 +32,7 @@ const TokenTransferTable = ({ data, baseAddress, showTxInfo, top }: Props) => { ...@@ -31,7 +32,7 @@ const TokenTransferTable = ({ data, baseAddress, showTxInfo, top }: Props) => {
</Thead> </Thead>
<Tbody> <Tbody>
{ data.map((item, index) => ( { data.map((item, index) => (
<TokenTransferTableItem key={ index } { ...item } baseAddress={ baseAddress } showTxInfo={ showTxInfo }/> <TokenTransferTableItem key={ index } { ...item } baseAddress={ baseAddress } showTxInfo={ showTxInfo } enableTimeIncrement={ enableTimeIncrement }/>
)) } )) }
</Tbody> </Tbody>
</Table> </Table>
......
import { Tr, Td, Tag, Flex } from '@chakra-ui/react'; import { Tr, Td, Tag, Flex, Text } from '@chakra-ui/react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React from 'react'; import React from 'react';
import type { TokenTransfer } from 'types/api/tokenTransfer'; import type { TokenTransfer } from 'types/api/tokenTransfer';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton'; import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
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';
...@@ -16,9 +17,21 @@ import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft'; ...@@ -16,9 +17,21 @@ import TokenTransferNft from 'ui/shared/TokenTransfer/TokenTransferNft';
type Props = TokenTransfer & { type Props = TokenTransfer & {
baseAddress?: string; baseAddress?: string;
showTxInfo?: boolean; showTxInfo?: boolean;
enableTimeIncrement?: boolean;
} }
const TokenTransferTableItem = ({ token, total, tx_hash: txHash, from, to, baseAddress, showTxInfo, type }: Props) => { const TokenTransferTableItem = ({
token,
total,
tx_hash: txHash,
from,
to,
baseAddress,
showTxInfo,
type,
timestamp,
enableTimeIncrement,
}: Props) => {
const value = (() => { const value = (() => {
if (!('value' in total)) { if (!('value' in total)) {
return '-'; return '-';
...@@ -27,6 +40,8 @@ const TokenTransferTableItem = ({ token, total, tx_hash: txHash, from, to, baseA ...@@ -27,6 +40,8 @@ const TokenTransferTableItem = ({ token, total, tx_hash: txHash, from, to, baseA
return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat(); return BigNumber(total.value).div(BigNumber(10 ** Number(total.decimals))).dp(8).toFormat();
})(); })();
const timeAgo = useTimeAgoIncrement(timestamp, enableTimeIncrement);
return ( return (
<Tr alignItems="top"> <Tr alignItems="top">
{ showTxInfo && ( { showTxInfo && (
...@@ -49,6 +64,7 @@ const TokenTransferTableItem = ({ token, total, tx_hash: txHash, from, to, baseA ...@@ -49,6 +64,7 @@ const TokenTransferTableItem = ({ token, total, tx_hash: txHash, from, to, baseA
<Address display="inline-flex" maxW="100%" fontWeight={ 600 } lineHeight="30px"> <Address display="inline-flex" maxW="100%" fontWeight={ 600 } lineHeight="30px">
<AddressLink type="transaction" hash={ txHash }/> <AddressLink type="transaction" hash={ txHash }/>
</Address> </Address>
{ timestamp && <Text color="gray.500" fontWeight="400" mt="10px">{ timeAgo }</Text> }
</Td> </Td>
) } ) }
<Td> <Td>
......
...@@ -2,6 +2,7 @@ import { Link, chakra, shouldForwardProp, Tooltip, Box } from '@chakra-ui/react' ...@@ -2,6 +2,7 @@ import { Link, chakra, shouldForwardProp, Tooltip, Box } from '@chakra-ui/react'
import type { HTMLAttributeAnchorTarget } from 'react'; import type { HTMLAttributeAnchorTarget } from 'react';
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link'; import link from 'lib/link/link';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic'; import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
...@@ -18,6 +19,8 @@ interface Props { ...@@ -18,6 +19,8 @@ interface Props {
} }
const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, fontWeight, target = '_self' }: Props) => { const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, fontWeight, target = '_self' }: Props) => {
const isMobile = useIsMobile();
let url; let url;
if (type === 'transaction') { if (type === 'transaction') {
url = link('tx', { id: id || hash }); url = link('tx', { id: id || hash });
...@@ -34,16 +37,16 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id, ...@@ -34,16 +37,16 @@ const AddressLink = ({ alias, type, className, truncation = 'dynamic', hash, id,
const content = (() => { const content = (() => {
if (alias) { if (alias) {
return ( return (
<Tooltip label={ hash }> <Tooltip label={ hash } isDisabled={ isMobile }>
<Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ alias }</Box> <Box overflow="hidden" textOverflow="ellipsis" whiteSpace="nowrap">{ alias }</Box>
</Tooltip> </Tooltip>
); );
} }
switch (truncation) { switch (truncation) {
case 'constant': case 'constant':
return <HashStringShorten hash={ id || hash }/>; return <HashStringShorten hash={ id || hash } isTooltipDisabled={ isMobile }/>;
case 'dynamic': case 'dynamic':
return <HashStringShortenDynamic hash={ id || hash } fontWeight={ fontWeight }/>; return <HashStringShortenDynamic hash={ id || hash } fontWeight={ fontWeight } isTooltipDisabled={ isMobile }/>;
case 'none': case 'none':
return <span>{ id || hash }</span>; return <span>{ id || hash }</span>;
} }
......
import { Box, Grid, Icon, IconButton, Menu, MenuButton, MenuItem, MenuList, Text, Tooltip, useColorModeValue, VisuallyHidden } from '@chakra-ui/react'; import { Box, Grid, Icon, IconButton, Menu, MenuButton, MenuItem, MenuList, Text, Tooltip, useColorModeValue, VisuallyHidden } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react'; import domToImage from 'dom-to-image';
import React, { useRef, useCallback, useState } from 'react';
import type { TimeChartItem } from './types'; import type { TimeChartItem } from './types';
import imageIcon from 'icons/image.svg';
import repeatArrow from 'icons/repeat_arrow.svg'; import repeatArrow from 'icons/repeat_arrow.svg';
import scopeIcon from 'icons/scope.svg';
import dotsIcon from 'icons/vertical_dots.svg'; import dotsIcon from 'icons/vertical_dots.svg';
import ChartWidgetGraph from './ChartWidgetGraph'; import ChartWidgetGraph from './ChartWidgetGraph';
...@@ -17,7 +20,10 @@ type Props = { ...@@ -17,7 +20,10 @@ type Props = {
isLoading: boolean; isLoading: boolean;
} }
const DOWNLOAD_IMAGE_SCALE = 5;
const ChartWidget = ({ items, title, description, isLoading }: Props) => { const ChartWidget = ({ items, title, description, isLoading }: Props) => {
const ref = useRef<HTMLDivElement>(null);
const [ isFullscreen, setIsFullscreen ] = useState(false); const [ isFullscreen, setIsFullscreen ] = useState(false);
const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true); const [ isZoomResetInitial, setIsZoomResetInitial ] = React.useState(true);
...@@ -47,6 +53,30 @@ const ChartWidget = ({ items, title, description, isLoading }: Props) => { ...@@ -47,6 +53,30 @@ const ChartWidget = ({ items, title, description, isLoading }: Props) => {
} }
}, []); }, []);
const handleFileSaveClick = useCallback(() => {
if (ref.current) {
domToImage.toPng(ref.current,
{
quality: 100,
bgcolor: 'white',
width: ref.current.offsetWidth * DOWNLOAD_IMAGE_SCALE,
height: ref.current.offsetHeight * DOWNLOAD_IMAGE_SCALE,
filter: (node) => node.nodeName !== 'BUTTON',
style: {
transform: `scale(${ DOWNLOAD_IMAGE_SCALE })`,
'transform-origin': 'top left',
},
})
.then((dataUrl) => {
const link = document.createElement('a');
link.download = `${ title } (Blockscout chart).png`;
link.href = dataUrl;
link.click();
link.remove();
});
}
}, [ title ]);
if (isLoading) { if (isLoading) {
return <ChartWidgetSkeleton/>; return <ChartWidgetSkeleton/>;
} }
...@@ -55,6 +85,7 @@ const ChartWidget = ({ items, title, description, isLoading }: Props) => { ...@@ -55,6 +85,7 @@ const ChartWidget = ({ items, title, description, isLoading }: Props) => {
return ( return (
<> <>
<Box <Box
ref={ ref }
padding={{ base: 3, lg: 4 }} padding={{ base: 3, lg: 4 }}
borderRadius="md" borderRadius="md"
border="1px" border="1px"
...@@ -115,11 +146,26 @@ const ChartWidget = ({ items, title, description, isLoading }: Props) => { ...@@ -115,11 +146,26 @@ const ChartWidget = ({ items, title, description, isLoading }: Props) => {
as={ IconButton } as={ IconButton }
> >
<VisuallyHidden> <VisuallyHidden>
Open chart options menu Open chart options menu
</VisuallyHidden> </VisuallyHidden>
</MenuButton> </MenuButton>
<MenuList> <MenuList>
<MenuItem onClick={ showChartFullscreen }>View fullscreen</MenuItem> <MenuItem
display="flex"
alignItems="center"
onClick={ showChartFullscreen }
>
<Icon as={ scopeIcon } boxSize={ 4 } mr={ 3 }/>
View fullscreen
</MenuItem>
<MenuItem
display="flex"
alignItems="center"
onClick={ handleFileSaveClick }
>
<Icon as={ imageIcon } boxSize={ 4 } mr={ 3 }/>
Save as PNG
</MenuItem>
</MenuList> </MenuList>
</Menu> </Menu>
</Grid> </Grid>
......
import { Skeleton, SkeletonCircle, Flex, Box, useColorModeValue } from '@chakra-ui/react'; import { Box, Flex, Skeleton, SkeletonCircle, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
const TxInternalsSkeletonMobile = () => { const SkeletonList = () => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return ( return (
...@@ -9,38 +9,32 @@ const TxInternalsSkeletonMobile = () => { ...@@ -9,38 +9,32 @@ const TxInternalsSkeletonMobile = () => {
{ Array.from(Array(2)).map((item, index) => ( { Array.from(Array(2)).map((item, index) => (
<Flex <Flex
key={ index } key={ index }
rowGap={ 3 } rowGap={ 4 }
flexDirection="column" flexDirection="column"
paddingY={ 6 } paddingY={ 6 }
borderTopWidth="1px" borderTopWidth="1px"
borderColor={ borderColor } borderColor={ borderColor }
_last={{ _last={{
borderBottomWidth: '1px', borderBottomWidth: '0px',
}} }}
> >
<Flex h={ 6 }> <Flex h={ 4 }>
<Skeleton w="100px" mr={ 2 }/> <Skeleton w="30%" mr={ 2 } borderRadius="full"/>
<Skeleton w="90px"/> <Skeleton w="15%" borderRadius="full"/>
</Flex> </Flex>
<Flex h={ 6 }> <Flex h={ 4 }>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/> <SkeletonCircle boxSize={ 4 } mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/> <Skeleton flexGrow={ 1 } mr={ 3 } borderRadius="full"/>
<Skeleton w={ 6 } mr={ 3 }/> <Skeleton w={ 6 } mr={ 3 } borderRadius="full"/>
<SkeletonCircle size="6" mr={ 2 } flexShrink={ 0 }/> <SkeletonCircle boxSize={ 4 } mr={ 2 } flexShrink={ 0 }/>
<Skeleton flexGrow={ 1 } mr={ 3 }/> <Skeleton flexGrow={ 1 } mr={ 3 } borderRadius="full"/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="30px"/>
</Flex>
<Flex h={ 6 }>
<Skeleton w="70px" mr={ 2 }/>
<Skeleton w="60px"/>
</Flex> </Flex>
<Skeleton w="75%" h={ 4 } borderRadius="full"/>
<Skeleton w="60%" h={ 4 } borderRadius="full"/>
</Flex> </Flex>
)) } )) }
</Box> </Box>
); );
}; };
export default TxInternalsSkeletonMobile; export default SkeletonList;
...@@ -5,7 +5,7 @@ interface Props { ...@@ -5,7 +5,7 @@ interface Props {
showFooterSlot?: boolean; showFooterSlot?: boolean;
} }
const SkeletonAccountMobile = ({ showFooterSlot }: Props) => { const SkeletonListAccount = ({ showFooterSlot }: Props) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return ( return (
...@@ -18,9 +18,8 @@ const SkeletonAccountMobile = ({ showFooterSlot }: Props) => { ...@@ -18,9 +18,8 @@ const SkeletonAccountMobile = ({ showFooterSlot }: Props) => {
paddingY={ 6 } paddingY={ 6 }
borderTopWidth="1px" borderTopWidth="1px"
borderColor={ borderColor } borderColor={ borderColor }
_first={{ _last={{
borderTopWidth: '0', borderBottomWidth: '0px',
pt: '0',
}} }}
> >
<Flex columnGap={ 2 } w="100%" alignItems="center"> <Flex columnGap={ 2 } w="100%" alignItems="center">
...@@ -45,4 +44,4 @@ const SkeletonAccountMobile = ({ showFooterSlot }: Props) => { ...@@ -45,4 +44,4 @@ const SkeletonAccountMobile = ({ showFooterSlot }: Props) => {
); );
}; };
export default SkeletonAccountMobile; export default SkeletonListAccount;
import { test, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react';
import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp';
import Burger from './Burger';
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ mount, page }) => {
const component = await mount(
<TestApp>
<Burger/>
</TestApp>,
);
await component.locator('svg[aria-label="Menu button"]').click();
await expect(page).toHaveScreenshot();
await page.locator('button[aria-label="Network menu"]').click();
await expect(page).toHaveScreenshot();
});
test.describe('dark mode', () => {
test.use({ colorScheme: 'dark' });
test('base view', async({ mount, page }) => {
const component = await mount(
<TestApp>
<Burger/>
</TestApp>,
);
await component.locator('svg[aria-label="Menu button"]').click();
await expect(page).toHaveScreenshot();
await page.locator('button[aria-label="Network menu"]').click();
await expect(page).toHaveScreenshot();
});
});
test.describe('auth', () => {
const extendedTest = test.extend({
context: ({ context }, use) => {
authFixture(context);
use(context);
},
});
extendedTest('base view', async({ mount, page }) => {
const component = await mount(
<TestApp>
<Burger/>
</TestApp>,
);
await component.locator('svg[aria-label="Menu button"]').click();
await expect(page).toHaveScreenshot();
});
});
...@@ -29,6 +29,7 @@ const Burger = () => { ...@@ -29,6 +29,7 @@ const Burger = () => {
boxSize={ 6 } boxSize={ 6 }
display="block" display="block"
color={ iconColor } color={ iconColor }
aria-label="Menu button"
/> />
</Box> </Box>
<Drawer <Drawer
......
...@@ -89,6 +89,7 @@ const ColorModeToggler = forwardRef<ColorModeTogglerProps, 'input'>((props, ref) ...@@ -89,6 +89,7 @@ const ColorModeToggler = forwardRef<ColorModeTogglerProps, 'input'>((props, ref)
<chakra.div <chakra.div
{ ...getCheckboxProps() } { ...getCheckboxProps() }
__css={ trackStyles } __css={ trackStyles }
aria-label="Toggle color mode"
> >
<Icon <Icon
boxSize={ 4 } boxSize={ 4 }
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import TestApp from 'playwright/TestApp';
import Header from './Header';
test('no auth +@mobile +@dark-mode +@dark-mode-mobile', async({ mount, page }) => {
await mount(
<TestApp>
<Header/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1500, height: 150 } });
});
...@@ -2,6 +2,7 @@ import { HStack, Box, Flex, useColorModeValue } from '@chakra-ui/react'; ...@@ -2,6 +2,7 @@ import { HStack, Box, Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { useScrollDirection } from 'lib/contexts/scrollDirection'; import { useScrollDirection } from 'lib/contexts/scrollDirection';
import IndexingAlert from 'ui/home/IndexingAlert';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo'; import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop'; import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile'; import ProfileMenuMobile from 'ui/snippets/profileMenu/ProfileMenuMobile';
...@@ -44,25 +45,29 @@ const Header = ({ hideOnScrollDown, isHomePage }: Props) => { ...@@ -44,25 +45,29 @@ const Header = ({ hideOnScrollDown, isHomePage }: Props) => {
</Flex> </Flex>
{ !isHomePage && <SearchBar withShadow={ !hideOnScrollDown }/> } { !isHomePage && <SearchBar withShadow={ !hideOnScrollDown }/> }
</Box> </Box>
{ !isHomePage && ( <Box
<HStack paddingX={ 12 }
as="header" paddingTop={ 9 }
width="100%" display={{ base: 'none', lg: 'block' }}
alignItems="center" >
justifyContent="center" <IndexingAlert/>
gap={ 12 } { !isHomePage && (
display={{ base: 'none', lg: 'flex' }} <HStack
paddingX={ 12 } as="header"
paddingTop={ 9 } width="100%"
paddingBottom="52px" alignItems="center"
> justifyContent="center"
<Box width="100%"> gap={ 12 }
<SearchBar/> paddingBottom="52px"
</Box> >
<ColorModeToggler/> <Box width="100%">
<ProfileMenuDesktop/> <SearchBar/>
</HStack> </Box>
) } <ColorModeToggler/>
<ProfileMenuDesktop/>
</HStack>
) }
</Box>
</> </>
); );
}; };
......
...@@ -33,6 +33,7 @@ const NavLink = ({ text, url, icon, isCollapsed, isActive, px, isNewUi }: Props) ...@@ -33,6 +33,7 @@ const NavLink = ({ text, url, icon, isCollapsed, isActive, px, isNewUi }: Props)
_hover={{ color: isActive ? colors.text.active : colors.text.hover }} _hover={{ color: isActive ? colors.text.active : colors.text.hover }}
borderRadius="base" borderRadius="base"
whiteSpace="nowrap" whiteSpace="nowrap"
aria-label={ `${ text } link` }
{ ...getDefaultTransitionProps({ transitionProperty: 'width, padding' }) } { ...getDefaultTransitionProps({ transitionProperty: 'width, padding' }) }
> >
<Tooltip <Tooltip
......
import { Box, Flex } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as cookies from 'lib/cookies';
import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp';
import NavigationDesktop from './NavigationDesktop';
const hooksConfig = {
router: {
route: '/blocks',
query: { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' },
},
};
test('no auth +@desktop-xl +@dark-mode-xl', async({ mount }) => {
const component = await mount(
<TestApp>
<Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/>
<Box bgColor="lightpink" w="100%"/>
</Flex>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test.describe('auth', () => {
const extendedTest = test.extend({
context: ({ context }, use) => {
authFixture(context);
use(context);
},
});
extendedTest('+@desktop-xl +@dark-mode-xl', async({ mount }) => {
const component = await mount(
<TestApp>
<Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/>
<Box bgColor="lightpink" w="100%"/>
</Flex>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
});
test('with tooltips +@desktop-xl -@default', async({ mount, page }) => {
const component = await mount(
<TestApp>
<Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/>
<Box bgColor="lightpink" w="100%"/>
</Flex>
</TestApp>,
{ hooksConfig },
);
await page.locator('svg[aria-label="Expand/Collapse menu"]').click();
await page.locator('a[aria-label="Tokens link"]').hover();
await expect(component).toHaveScreenshot();
});
test.describe('cookie set to false', () => {
const extendedTest = test.extend({
context: ({ context }, use) => {
context.addCookies([ { name: cookies.NAMES.NAV_BAR_COLLAPSED, value: 'false', domain: 'localhost', path: '/' } ]);
use(context);
},
});
extendedTest('navbar is opened +@desktop-xl', async({ mount }) => {
const component = await mount(
<TestApp>
<Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/>
<Box bgColor="lightpink" w="100%"/>
</Flex>
</TestApp>,
{ hooksConfig },
);
const networkMenu = component.locator('button[aria-label="Network menu"]');
expect(await networkMenu.isVisible()).toBe(true);
});
});
test.describe('cookie set to true', () => {
const extendedTest = test.extend({
context: ({ context }, use) => {
context.addCookies([ { name: cookies.NAMES.NAV_BAR_COLLAPSED, value: 'true', domain: 'localhost', path: '/' } ]);
use(context);
},
});
extendedTest('navbar is collapsed', async({ mount }) => {
const component = await mount(
<TestApp>
<Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/>
<Box bgColor="lightpink" w="100%"/>
</Flex>
</TestApp>,
{ hooksConfig },
);
const networkMenu = component.locator('button[aria-label="Network menu"]');
expect(await networkMenu.isVisible()).toBe(false);
});
});
...@@ -77,13 +77,13 @@ const NavigationDesktop = () => { ...@@ -77,13 +77,13 @@ const NavigationDesktop = () => {
<NetworkMenu isCollapsed={ isCollapsed }/> <NetworkMenu isCollapsed={ isCollapsed }/>
</Box> </Box>
<Box as="nav" mt={ 8 }> <Box as="nav" mt={ 8 }>
<VStack as="ul" spacing="2" alignItems="flex-start"> <VStack as="ul" spacing="1" alignItems="flex-start">
{ mainNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) } { mainNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) }
</VStack> </VStack>
</Box> </Box>
{ hasAccount && ( { hasAccount && (
<Box as="nav" mt={ 12 }> <Box as="nav" mt={ 8 }>
<VStack as="ul" spacing="2" alignItems="flex-start"> <VStack as="ul" spacing="1" alignItems="flex-start">
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) } { accountNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) }
</VStack> </VStack>
</Box> </Box>
...@@ -105,6 +105,7 @@ const NavigationDesktop = () => { ...@@ -105,6 +105,7 @@ const NavigationDesktop = () => {
left={{ lg: isExpanded ? '216px' : '80px', xl: isCollapsed ? '80px' : '216px' }} left={{ lg: isExpanded ? '216px' : '80px', xl: isCollapsed ? '80px' : '216px' }}
cursor="pointer" cursor="pointer"
onClick={ handleTogglerClick } onClick={ handleTogglerClick }
aria-label="Expand/Collapse menu"
/> />
</Flex> </Flex>
); );
......
...@@ -16,13 +16,13 @@ const NavigationMobile = () => { ...@@ -16,13 +16,13 @@ const NavigationMobile = () => {
return ( return (
<> <>
<Box as="nav" mt={ 6 }> <Box as="nav" mt={ 6 }>
<VStack as="ul" spacing="2" alignItems="flex-start"> <VStack as="ul" spacing="1" alignItems="flex-start">
{ mainNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) } { mainNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) }
</VStack> </VStack>
</Box> </Box>
{ isAuth && ( { isAuth && (
<Box as="nav" mt={ 6 }> <Box as="nav" mt={ 6 }>
<VStack as="ul" spacing="2" alignItems="flex-start"> <VStack as="ul" spacing="1" alignItems="flex-start">
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) } { accountNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) }
</VStack> </VStack>
</Box> </Box>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
import NetworkLogo from './NetworkLogo';
test('fallback logo +@desktop-xl +@dark-mode +@dark-mode-xl', async({ mount }) => {
const component = await mount(
<TestApp>
<NetworkLogo/>
</TestApp>,
);
await expect(component.locator('a')).toHaveScreenshot();
});
test.describe('placeholder logo', () => {
const extendedTest = test.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_NETWORK_TYPE', value: 'unknown' },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any,
});
extendedTest('+@desktop-xl +@dark-mode +@dark-mode-xl', async({ mount }) => {
const component = await mount(
<TestApp>
<NetworkLogo/>
</TestApp>,
);
await expect(component.locator('a')).toHaveScreenshot();
});
});
test.describe('custom logo', () => {
const LOGO_URL = 'https://example.com/my-logo.png';
const SMALL_LOGO_URL = 'https://example.com/my-logo-short.png';
const extendedTest = test.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_NETWORK_LOGO', value: LOGO_URL },
{ name: 'NEXT_PUBLIC_NETWORK_SMALL_LOGO', value: SMALL_LOGO_URL },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any,
});
extendedTest('+@desktop-xl +@dark-mode +@dark-mode-xl', async({ mount, page }) => {
await page.route(LOGO_URL, (route) => {
return route.fulfill({
status: 200,
path: './playwright/giant_duck_long.jpg',
});
});
await page.route(SMALL_LOGO_URL, (route) => {
return route.fulfill({
status: 200,
path: './playwright/image_s.jpg',
});
});
const component = await mount(
<TestApp>
<NetworkLogo/>
</TestApp>,
);
await expect(component.locator('a')).toHaveScreenshot();
});
});
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { FEATURED_NETWORKS_MOCK } from 'mocks/config/network';
import contextWithEnvs from 'playwright/fixtures/contextWithEnvs';
import TestApp from 'playwright/TestApp';
import NetworkMenu from './NetworkMenu';
const extendedTest = test.extend({
context: contextWithEnvs([
{ name: 'NEXT_PUBLIC_FEATURED_NETWORKS', value: FEATURED_NETWORKS_MOCK },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
]) as any,
});
extendedTest.use({ viewport: { width: 1600, height: 1000 } });
extendedTest('base view +@dark-mode', async({ mount, page }) => {
const LOGO_URL = 'https://example.com/my-logo.png';
await page.route(LOGO_URL, (route) => {
return route.fulfill({
status: 200,
path: './playwright/image_s.jpg',
});
});
const component = await mount(
<TestApp>
<NetworkMenu/>
</TestApp>,
);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 36, height: 36 } });
await component.locator('button[aria-label="Network menu"]').hover();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 36, height: 36 } });
await component.locator('button[aria-label="Network menu"]').click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 450, height: 550 } });
await component.getByText(/optimism/i).hover();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 450, height: 550 } });
});
import { Popover, PopoverTrigger, Box } from '@chakra-ui/react'; import { Popover, PopoverTrigger } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import NetworkMenuButton from './NetworkMenuButton'; import NetworkMenuButton from './NetworkMenuButton';
...@@ -9,17 +9,16 @@ interface Props { ...@@ -9,17 +9,16 @@ interface Props {
const NetworkMenu = ({ isCollapsed }: Props) => { const NetworkMenu = ({ isCollapsed }: Props) => {
return ( return (
<Popover openDelay={ 300 } placement="right-start" gutter={ 22 } isLazy> <Popover openDelay={ 300 } placement="right-start" gutter={ 8 } isLazy>
{ ({ isOpen }) => ( { ({ isOpen }) => (
<> <>
<PopoverTrigger> <PopoverTrigger>
<Box <NetworkMenuButton
marginLeft="auto" marginLeft="auto"
overflow="hidden" overflow="hidden"
width={{ base: 'auto', lg: isCollapsed === false ? 'auto' : '0px', xl: isCollapsed ? '0px' : 'auto' }} width={{ base: 'auto', lg: isCollapsed === false ? 'auto' : '0px', xl: isCollapsed ? '0px' : 'auto' }}
> isActive={ isOpen }
<NetworkMenuButton isActive={ isOpen }/> />
</Box>
</PopoverTrigger> </PopoverTrigger>
<NetworkMenuContentDesktop/> <NetworkMenuContentDesktop/>
</> </>
......
import { Icon, useColorModeValue, Button } from '@chakra-ui/react'; import { Icon, useColorModeValue, Button, forwardRef, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import networksIcon from 'icons/networks.svg'; import networksIcon from 'icons/networks.svg';
...@@ -8,15 +8,17 @@ interface Props { ...@@ -8,15 +8,17 @@ interface Props {
isMobile?: boolean; isMobile?: boolean;
isActive?: boolean; isActive?: boolean;
onClick?: () => void; onClick?: () => void;
className?: string;
} }
const NetworkMenuButton = ({ isMobile, isActive, onClick }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => { const NetworkMenuButton = ({ isMobile, isActive, onClick, className }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const defaultIconColor = useColorModeValue('gray.600', 'gray.400'); const defaultIconColor = useColorModeValue('gray.600', 'gray.400');
const bgColorMobile = useColorModeValue('blue.50', 'gray.800'); const bgColorMobile = useColorModeValue('blue.50', 'gray.800');
const iconColorMobile = useColorModeValue('blue.700', 'blue.50'); const iconColorMobile = useColorModeValue('blue.700', 'blue.50');
return ( return (
<Button <Button
className={ className }
variant="unstyled" variant="unstyled"
display="inline-flex" display="inline-flex"
alignSelf="stretch" alignSelf="stretch"
...@@ -43,4 +45,4 @@ const NetworkMenuButton = ({ isMobile, isActive, onClick }: Props, ref: React.Fo ...@@ -43,4 +45,4 @@ const NetworkMenuButton = ({ isMobile, isActive, onClick }: Props, ref: React.Fo
); );
}; };
export default React.forwardRef(NetworkMenuButton); export default chakra(forwardRef(NetworkMenuButton));
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as profileMock from 'mocks/user/profile';
import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp';
import ProfileMenuDesktop from './ProfileMenuDesktop';
test('no auth', async({ mount, page }) => {
const hooksConfig = {
router: {
asPath: '/',
},
};
const component = await mount(
<TestApp>
<ProfileMenuDesktop/>
</TestApp>,
{ hooksConfig },
);
await component.locator('.identicon').click();
expect(page.url()).toBe('http://localhost:3100/auth/auth0?path=%2F');
});
test.describe('auth', () => {
const extendedTest = test.extend({
context: ({ context }, use) => {
authFixture(context);
use(context);
},
});
extendedTest('+@dark-mode', async({ mount, page }) => {
await page.route('/node-api/account/profile', (route) => route.fulfill({
status: 200,
body: JSON.stringify(profileMock.base),
}));
await page.route(profileMock.base.avatar, (route) => {
return route.fulfill({
status: 200,
path: './playwright/image_s.jpg',
});
});
const component = await mount(
<TestApp>
<ProfileMenuDesktop/>
</TestApp>,
);
await component.getByAltText(/Profile picture/i).click();
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 250, height: 550 } });
});
});
import { test, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react';
import * as profileMock from 'mocks/user/profile';
import authFixture from 'playwright/fixtures/auth';
import TestApp from 'playwright/TestApp';
import ProfileMenuMobile from './ProfileMenuMobile';
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('no auth', async({ mount, page }) => {
const component = await mount(
<TestApp>
<ProfileMenuMobile/>
</TestApp>,
);
await component.locator('.identicon').click();
await expect(page).toHaveScreenshot();
});
test.describe('auth', () => {
const extendedTest = test.extend({
context: ({ context }, use) => {
authFixture(context);
use(context);
},
});
extendedTest('base view', async({ mount, page }) => {
await page.route('/node-api/account/profile', (route) => route.fulfill({
status: 200,
body: JSON.stringify(profileMock.base),
}));
await page.route(profileMock.base.avatar, (route) => {
return route.fulfill({
status: 200,
path: './playwright/image_s.jpg',
});
});
const component = await mount(
<TestApp>
<ProfileMenuMobile/>
</TestApp>,
);
await component.getByAltText(/Profile picture/i).click();
await expect(page).toHaveScreenshot();
await page.locator('div[aria-label="Toggle color mode"]').click();
await expect(page).toHaveScreenshot();
});
});
import { InputGroup, Input, InputLeftAddon, InputLeftElement, Icon, chakra, useColorModeValue } from '@chakra-ui/react'; import { InputGroup, Input, InputLeftElement, Icon, chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import type { ChangeEvent, FormEvent } from 'react'; import type { ChangeEvent, FormEvent } from 'react';
...@@ -18,15 +18,15 @@ const SearchBarDesktop = ({ onChange, onSubmit, isHomepage }: Props) => { ...@@ -18,15 +18,15 @@ const SearchBarDesktop = ({ onChange, onSubmit, isHomepage }: Props) => {
display={{ base: 'none', lg: 'block' }} display={{ base: 'none', lg: 'block' }}
w="100%" w="100%"
backgroundColor={ isHomepage ? 'white' : 'none' } backgroundColor={ isHomepage ? 'white' : 'none' }
borderRadius="10px" borderRadius="base"
> >
<InputGroup> <InputGroup>
<InputLeftAddon w="111px">All filters</InputLeftAddon> <InputLeftElement w={ 6 } ml={ 4 }>
<InputLeftElement w={ 6 } ml="132px" mr={ 2.5 }>
<Icon as={ searchIcon } boxSize={ 6 } color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/> <Icon as={ searchIcon } boxSize={ 6 } color={ useColorModeValue('blackAlpha.600', 'whiteAlpha.600') }/>
</InputLeftElement> </InputLeftElement>
<Input <Input
paddingInlineStart="50px" // paddingInlineStart="50px"
pl="50px"
placeholder="Search by addresses / transactions / block / token... " placeholder="Search by addresses / transactions / block / token... "
ml="1px" ml="1px"
onChange={ onChange } onChange={ onChange }
......
...@@ -65,6 +65,21 @@ const TxDetails = () => { ...@@ -65,6 +65,21 @@ const TxDetails = () => {
...toAddress.watchlist_names || [], ...toAddress.watchlist_names || [],
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>); ].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
const executionSuccessBadge = toAddress.is_contract && data.result === 'success' ? (
<Tooltip label="Contract execution completed">
<chakra.span display="inline-flex" ml={ 2 } mr={ 1 }>
<Icon as={ successIcon } boxSize={ 4 } color="green.500" cursor="pointer"/>
</chakra.span>
</Tooltip>
) : null;
const executionFailedBadge = toAddress.is_contract && Boolean(data.status) && data.result !== 'success' ? (
<Tooltip label="Error occurred during contract execution">
<chakra.span display="inline-flex" ml={ 2 } mr={ 1 }>
<Icon as={ errorIcon } boxSize={ 4 } color="red.500" cursor="pointer"/>
</chakra.span>
</Tooltip>
) : null;
return ( return (
<Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}> <Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }}>
{ socketStatus && ( { socketStatus && (
...@@ -144,40 +159,30 @@ const TxDetails = () => { ...@@ -144,40 +159,30 @@ const TxDetails = () => {
) } ) }
</DetailsInfoItem> </DetailsInfoItem>
<DetailsInfoItem <DetailsInfoItem
title={ toAddress.is_contract ? 'Interacted with contract' : 'To' } title={ data.to?.is_contract ? 'Interacted with contract' : 'To' }
hint="Address (external or contract) receiving the transaction." hint="Address (external or contract) receiving the transaction."
flexWrap={{ base: 'wrap', lg: 'nowrap' }} flexWrap={{ base: 'wrap', lg: 'nowrap' }}
columnGap={ 3 } columnGap={ 3 }
> >
{ data.to && data.to.hash ? ( { data.to && data.to.hash ? (
<Address> <Address alignItems="center">
<AddressIcon hash={ toAddress.hash }/> <AddressIcon hash={ toAddress.hash }/>
<AddressLink ml={ 2 } hash={ toAddress.hash }/> <AddressLink ml={ 2 } hash={ toAddress.hash }/>
{ executionSuccessBadge }
{ executionFailedBadge }
<CopyToClipboard text={ toAddress.hash }/> <CopyToClipboard text={ toAddress.hash }/>
</Address> </Address>
) : ( ) : (
<Flex width={{ base: '100%', lg: 'auto' }} whiteSpace="pre"> <Flex width={{ base: '100%', lg: 'auto' }} whiteSpace="pre" alignItems="center">
<span>[Contract </span> <span>[Contract </span>
<AddressLink hash={ toAddress.hash }/> <AddressLink hash={ toAddress.hash }/>
<span> created]</span> <span> created]</span>
{ executionSuccessBadge }
{ executionFailedBadge }
<CopyToClipboard text={ toAddress.hash }/> <CopyToClipboard text={ toAddress.hash }/>
</Flex> </Flex>
) } ) }
{ toAddress.name && <Text>{ toAddress.name }</Text> } { toAddress.name && <Text>{ toAddress.name }</Text> }
{ toAddress.is_contract && data.result === 'success' && (
<Tooltip label="Contract execution completed">
<chakra.span display="inline-flex">
<Icon as={ successIcon } boxSize={ 4 } color="green.500" cursor="pointer"/>
</chakra.span>
</Tooltip>
) }
{ toAddress.is_contract && Boolean(data.status) && data.result !== 'success' && (
<Tooltip label="Error occured during contract execution">
<chakra.span display="inline-flex">
<Icon as={ errorIcon } boxSize={ 4 } color="red.500" cursor="pointer"/>
</chakra.span>
</Tooltip>
) }
{ addressToTags.length > 0 && ( { addressToTags.length > 0 && (
<Flex columnGap={ 3 }> <Flex columnGap={ 3 }>
{ addressToTags } { addressToTags }
......
...@@ -33,8 +33,5 @@ test('base view +@mobile', async({ mount, page }) => { ...@@ -33,8 +33,5 @@ test('base view +@mobile', async({ mount, page }) => {
{ hooksConfig }, { hooksConfig },
); );
await page.waitForResponse(API_URL_TX),
await page.waitForResponse(API_URL_TX_INTERNALS),
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -14,9 +14,9 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert'; ...@@ -14,9 +14,9 @@ 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 Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
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';
import TxPendingAlert from 'ui/tx/TxPendingAlert'; import TxPendingAlert from 'ui/tx/TxPendingAlert';
...@@ -75,7 +75,7 @@ const TxInternals = () => { ...@@ -75,7 +75,7 @@ const TxInternals = () => {
// 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 txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND }); const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError, pagination } = useQueryWithPages({ const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
apiPath: `/node-api/transactions/${ txInfo.data?.hash }/internal-transactions`, apiPath: `/node-api/transactions/${ txInfo.data?.hash }/internal-transactions`,
queryName: QueryKeys.txInternals, queryName: QueryKeys.txInternals,
queryIds: txInfo.data?.hash ? [ txInfo.data.hash ] : undefined, queryIds: txInfo.data?.hash ? [ txInfo.data.hash ] : undefined,
...@@ -83,7 +83,6 @@ const TxInternals = () => { ...@@ -83,7 +83,6 @@ const TxInternals = () => {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status), enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
}, },
}); });
const isPaginatorHidden = !isLoading && !isError && pagination.page === 1 && !pagination.hasNextPage;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -104,8 +103,8 @@ const TxInternals = () => { ...@@ -104,8 +103,8 @@ const TxInternals = () => {
if (isLoading || txInfo.isLoading) { if (isLoading || txInfo.isLoading) {
return ( return (
<> <>
<Show below="lg"><TxInternalsSkeletonMobile/></Show> <Show below="lg"><SkeletonList/></Show>
<Hide below="lg"><TxInternalsSkeletonDesktop/></Hide> <Hide below="lg"><SkeletonTable columns={ [ '28%', '20%', '24px', '20%', '16%', '16%' ] }/></Hide>
</> </>
); );
} }
...@@ -131,12 +130,12 @@ const TxInternals = () => { ...@@ -131,12 +130,12 @@ const TxInternals = () => {
return isMobile ? return isMobile ?
<TxInternalsList data={ filteredData }/> : <TxInternalsList data={ filteredData }/> :
<TxInternalsTable data={ filteredData } sort={ sort } onSortToggle={ handleSortToggle } top={ isPaginatorHidden ? 0 : 80 }/>; <TxInternalsTable data={ filteredData } sort={ sort } onSortToggle={ handleSortToggle } top={ isPaginationVisible ? 80 : 0 }/>;
})(); })();
return ( return (
<Box> <Box>
{ !isPaginatorHidden && ( { isPaginationVisible && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/> <Pagination ml="auto" { ...pagination }/>
</ActionBar> </ActionBar>
......
...@@ -16,7 +16,7 @@ import useFetchTxInfo from 'ui/tx/useFetchTxInfo'; ...@@ -16,7 +16,7 @@ import useFetchTxInfo from 'ui/tx/useFetchTxInfo';
const TxLogs = () => { const TxLogs = () => {
const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND }); const txInfo = useFetchTxInfo({ updateDelay: 5 * SECOND });
const { data, isLoading, isError, pagination } = useQueryWithPages({ const { data, isLoading, isError, pagination, isPaginationVisible } = useQueryWithPages({
apiPath: `/node-api/transactions/${ txInfo.data?.hash }/logs`, apiPath: `/node-api/transactions/${ txInfo.data?.hash }/logs`,
queryName: QueryKeys.txLogs, queryName: QueryKeys.txLogs,
queryIds: txInfo.data?.hash ? [ txInfo.data.hash ] : undefined, queryIds: txInfo.data?.hash ? [ txInfo.data.hash ] : undefined,
...@@ -24,7 +24,6 @@ const TxLogs = () => { ...@@ -24,7 +24,6 @@ const TxLogs = () => {
enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status), enabled: Boolean(txInfo.data?.hash) && Boolean(txInfo.data?.status),
}, },
}); });
const isPaginatorHidden = !isLoading && !isError && pagination.page === 1 && !pagination.hasNextPage;
if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) { if (!txInfo.isLoading && !txInfo.isError && !txInfo.data.status) {
return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>; return txInfo.socketStatus ? <TxSocketAlert status={ txInfo.socketStatus }/> : <TxPendingAlert/>;
...@@ -49,7 +48,7 @@ const TxLogs = () => { ...@@ -49,7 +48,7 @@ const TxLogs = () => {
return ( return (
<Box> <Box>
{ !isPaginatorHidden && ( { isPaginationVisible && (
<ActionBar mt={ -6 }> <ActionBar mt={ -6 }>
<Pagination ml="auto" { ...pagination }/> <Pagination ml="auto" { ...pagination }/>
</ActionBar> </ActionBar>
......
...@@ -46,6 +46,10 @@ const TxRawTrace = () => { ...@@ -46,6 +46,10 @@ const TxRawTrace = () => {
); );
} }
if (data.length === 0) {
return <span>There is no raw trace for this transaction.</span>;
}
const text = JSON.stringify(data, undefined, 4); const text = JSON.stringify(data, undefined, 4);
return ( return (
......
...@@ -6,10 +6,10 @@ import type { InternalTransaction } from 'types/api/internalTransaction'; ...@@ -6,10 +6,10 @@ import type { InternalTransaction } from 'types/api/internalTransaction';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import eastArrowIcon from 'icons/arrows/east.svg'; import eastArrowIcon from 'icons/arrows/east.svg';
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 ListItemMobile from 'ui/shared/ListItemMobile';
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';
...@@ -20,7 +20,7 @@ const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit: ...@@ -20,7 +20,7 @@ const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit:
const toData = to ? to : createdContract; const toData = to ? to : createdContract;
return ( return (
<AccountListItemMobile rowGap={ 3 }> <ListItemMobile rowGap={ 3 }>
<Flex> <Flex>
{ typeTitle && <Tag colorScheme="cyan" mr={ 2 }>{ typeTitle }</Tag> } { typeTitle && <Tag colorScheme="cyan" mr={ 2 }>{ typeTitle }</Tag> }
<TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/> <TxStatus status={ success ? 'ok' : 'error' } errorText={ error }/>
...@@ -46,7 +46,7 @@ const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit: ...@@ -46,7 +46,7 @@ const TxInternalsListItem = ({ type, from, to, value, success, error, gas_limit:
<Text fontSize="sm" fontWeight={ 500 }>Gas limit</Text> <Text fontSize="sm" fontWeight={ 500 }>Gas limit</Text>
<Text fontSize="sm" variant="secondary">{ BigNumber(gasLimit).toFormat() }</Text> <Text fontSize="sm" variant="secondary">{ BigNumber(gasLimit).toFormat() }</Text>
</HStack> </HStack>
</AccountListItemMobile> </ListItemMobile>
); );
}; };
......
import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable';
const TxInternalsSkeletonDesktop = () => {
return (
<SkeletonTable columns={ [ '28%', '20%', '24px', '20%', '16%', '16%' ] }/>
);
};
export default TxInternalsSkeletonDesktop;
...@@ -8,10 +8,10 @@ import appConfig from 'configs/app/config'; ...@@ -8,10 +8,10 @@ import appConfig from 'configs/app/config';
import type { data } from 'data/txState'; import type { data } from 'data/txState';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
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 ListItemMobile from 'ui/shared/ListItemMobile';
import TxStateStorageItem from './TxStateStorageItem'; import TxStateStorageItem from './TxStateStorageItem';
...@@ -22,7 +22,7 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props ...@@ -22,7 +22,7 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
const hasStorageData = Boolean(storage?.length); const hasStorageData = Boolean(storage?.length);
return ( return (
<AccountListItemMobile> <ListItemMobile>
<AccordionItem isDisabled={ !hasStorageData } border={ 0 } w="100%" display="flex" flexDirection="column"> <AccordionItem isDisabled={ !hasStorageData } border={ 0 } w="100%" display="flex" flexDirection="column">
{ ({ isExpanded }) => ( { ({ isExpanded }) => (
<> <>
...@@ -94,7 +94,7 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props ...@@ -94,7 +94,7 @@ const TxStateListItem = ({ storage, address, miner, after, before, diff }: Props
</> </>
) } ) }
</AccordionItem> </AccordionItem>
</AccountListItemMobile> </ListItemMobile>
); );
}; };
......
...@@ -18,11 +18,11 @@ const TxType = ({ types }: Props) => { ...@@ -18,11 +18,11 @@ const TxType = ({ types }: Props) => {
switch (typeToShow) { switch (typeToShow) {
case 'contract_call': case 'contract_call':
label = 'Contract call'; label = 'Contract call';
colorScheme = 'green'; colorScheme = 'blue';
break; break;
case 'contract_creation': case 'contract_creation':
label = 'Contract creation'; label = 'Contract creation';
colorScheme = 'purple'; colorScheme = 'blue';
break; break;
case 'token_transfer': case 'token_transfer':
label = 'Token transfer'; label = 'Token transfer';
...@@ -30,15 +30,15 @@ const TxType = ({ types }: Props) => { ...@@ -30,15 +30,15 @@ const TxType = ({ types }: Props) => {
break; break;
case 'token_creation': case 'token_creation':
label = 'Token creation'; label = 'Token creation';
colorScheme = 'cyan'; colorScheme = 'orange';
break; break;
case 'coin_transfer': case 'coin_transfer':
label = 'Coin transfer'; label = 'Coin transfer';
colorScheme = 'teal'; colorScheme = 'orange';
break; break;
default: default:
label = 'Transaction'; label = 'Transaction';
colorScheme = 'blue'; colorScheme = 'purple';
} }
......
...@@ -7,12 +7,12 @@ import type { TxsResponse } from 'types/api/transaction'; ...@@ -7,12 +7,12 @@ import type { TxsResponse } from 'types/api/transaction';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import TxsHeaderMobile from './TxsHeaderMobile'; import TxsHeaderMobile from './TxsHeaderMobile';
import TxsListItem from './TxsListItem'; import TxsListItem from './TxsListItem';
import TxsNewItemNotice from './TxsNewItemNotice'; import TxsNewItemNotice from './TxsNewItemNotice';
import TxsSkeletonDesktop from './TxsSkeletonDesktop';
import TxsSkeletonMobile from './TxsSkeletonMobile';
import TxsTable from './TxsTable'; import TxsTable from './TxsTable';
import useTxsSort from './useTxsSort'; import useTxsSort from './useTxsSort';
...@@ -26,9 +26,10 @@ type Props = { ...@@ -26,9 +26,10 @@ type Props = {
showSocketInfo?: boolean; showSocketInfo?: boolean;
currentAddress?: string; currentAddress?: string;
filter?: React.ReactNode; filter?: React.ReactNode;
enableTimeIncrement?: boolean;
} }
const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true, currentAddress }: Props) => { const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true, currentAddress, enableTimeIncrement }: Props) => {
const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query); const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query);
const isPaginatorHidden = !isLoading && !isError && query.pagination.page === 1 && !query.pagination.hasNextPage; const isPaginatorHidden = !isLoading && !isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -41,8 +42,13 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true ...@@ -41,8 +42,13 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true
if (isLoading) { if (isLoading) {
return ( return (
<> <>
<Show below="lg" ssr={ false }><TxsSkeletonMobile showBlockInfo={ showBlockInfo }/></Show> <Show below="lg" ssr={ false }><SkeletonList/></Show>
<Hide below="lg" ssr={ false }><TxsSkeletonDesktop showBlockInfo={ showBlockInfo }/></Hide> <Hide below="lg" ssr={ false }>
<SkeletonTable columns={ showBlockInfo ?
[ '32px', '22%', '160px', '20%', '18%', '292px', '20%', '20%' ] :
[ '32px', '22%', '160px', '20%', '292px', '20%', '20%' ]
}/>
</Hide>
</> </>
); );
} }
...@@ -62,7 +68,15 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true ...@@ -62,7 +68,15 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true
{ ({ content }) => <Box>{ content }</Box> } { ({ content }) => <Box>{ content }</Box> }
</TxsNewItemNotice> </TxsNewItemNotice>
) } ) }
{ txs.map(tx => <TxsListItem tx={ tx } key={ tx.hash } showBlockInfo={ showBlockInfo } currentAddress={ currentAddress }/>) } { txs.map(tx => (
<TxsListItem
tx={ tx }
key={ tx.hash }
showBlockInfo={ showBlockInfo }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
/>
)) }
</Box> </Box>
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
...@@ -74,6 +88,7 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true ...@@ -74,6 +88,7 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true
showSocketInfo={ showSocketInfo } showSocketInfo={ showSocketInfo }
top={ isPaginatorHidden ? 0 : 80 } top={ isPaginatorHidden ? 0 : 80 }
currentAddress={ currentAddress } currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
/> />
</Hide> </Hide>
</> </>
......
...@@ -17,8 +17,8 @@ import type { Transaction } from 'types/api/transaction'; ...@@ -17,8 +17,8 @@ import type { Transaction } from 'types/api/transaction';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import transactionIcon from 'icons/transactions.svg'; import transactionIcon from 'icons/transactions.svg';
import dayjs from 'lib/date/dayjs';
import getValueWithUnit from 'lib/getValueWithUnit'; import getValueWithUnit from 'lib/getValueWithUnit';
import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton'; import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
...@@ -33,12 +33,13 @@ type Props = { ...@@ -33,12 +33,13 @@ type Props = {
tx: Transaction; tx: Transaction;
showBlockInfo: boolean; showBlockInfo: boolean;
currentAddress?: string; currentAddress?: string;
enableTimeIncrement?: boolean;
} }
const TAG_WIDTH = 48; const TAG_WIDTH = 48;
const ARROW_WIDTH = 24; const ARROW_WIDTH = 24;
const TxsListItem = ({ tx, showBlockInfo, currentAddress }: Props) => { const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const iconColor = useColorModeValue('blue.600', 'blue.300'); const iconColor = useColorModeValue('blue.600', 'blue.300');
...@@ -48,6 +49,8 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress }: Props) => { ...@@ -48,6 +49,8 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress }: Props) => {
const isOut = Boolean(currentAddress && currentAddress === tx.from.hash); const isOut = Boolean(currentAddress && currentAddress === tx.from.hash);
const isIn = Boolean(currentAddress && currentAddress === tx.to?.hash); const isIn = Boolean(currentAddress && currentAddress === tx.to?.hash);
const timeAgo = useTimeAgoIncrement(tx.timestamp, enableTimeIncrement);
return ( return (
<> <>
<Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor }}> <Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor }}>
...@@ -58,7 +61,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress }: Props) => { ...@@ -58,7 +61,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress }: Props) => {
</HStack> </HStack>
<AdditionalInfoButton onClick={ onOpen }/> <AdditionalInfoButton onClick={ onOpen }/>
</Flex> </Flex>
<Flex justifyContent="space-between" lineHeight="24px" mt={ 3 }> <Flex justifyContent="space-between" lineHeight="24px" mt={ 3 } alignItems="center">
<Flex> <Flex>
<Icon <Icon
as={ transactionIcon } as={ transactionIcon }
...@@ -75,20 +78,22 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress }: Props) => { ...@@ -75,20 +78,22 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress }: Props) => {
/> />
</Address> </Address>
</Flex> </Flex>
<Text variant="secondary" fontWeight="400" fontSize="sm">{ dayjs(tx.timestamp).fromNow() }</Text> { tx.timestamp && <Text variant="secondary" fontWeight="400" fontSize="sm">{ timeAgo }</Text> }
</Flex>
<Flex mt={ 3 }>
<Text as="span" whiteSpace="pre">Method </Text>
<Text
as="span"
variant="secondary"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
{ tx.method }
</Text>
</Flex> </Flex>
{ tx.method && (
<Flex mt={ 3 }>
<Text as="span" whiteSpace="pre">Method </Text>
<Text
as="span"
variant="secondary"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
>
{ tx.method }
</Text>
</Flex>
) }
{ showBlockInfo && tx.block !== null && ( { showBlockInfo && tx.block !== null && (
<Box mt={ 2 }> <Box mt={ 2 }>
<Text as="span">Block </Text> <Text as="span">Block </Text>
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import SkeletonTable from 'ui/shared/SkeletonTable';
interface Props {
showBlockInfo: boolean;
}
const TxsInternalsSkeletonDesktop = ({ showBlockInfo }: Props) => {
return (
<Box mb={ 8 }>
<SkeletonTable columns={ showBlockInfo ?
[ '32px', '20%', '18%', '15%', '11%', '292px', '18%', '18%' ] :
[ '32px', '20%', '18%', '15%', '292px', '18%', '18%' ]
}/>
</Box>
);
};
export default TxsInternalsSkeletonDesktop;
import { Skeleton, Flex, Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
interface Props {
showBlockInfo: boolean;
}
const TxInternalsSkeletonMobile = ({ showBlockInfo }: Props) => {
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Box>
{ Array.from(Array(2)).map((item, index) => (
<Flex
key={ index }
flexDirection="column"
paddingBottom={ 3 }
paddingTop={ 4 }
borderTopWidth="1px"
borderColor={ borderColor }
_last={{
borderBottomWidth: '1px',
}}
>
<Flex h={ 6 }>
<Skeleton w="100px" mr={ 2 } h={ 6 }/>
<Skeleton w="100px" h={ 6 }/>
</Flex>
<Skeleton w="100%" h="30px" mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 3 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
{ showBlockInfo && <Skeleton w="100%" h={ 6 } mt={ 6 }/> }
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
<Skeleton w="50%" h={ 6 } mt={ 2 }/>
</Flex>
)) }
</Box>
);
};
export default TxInternalsSkeletonMobile;
import React from 'react'; import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import type { Props as PaginationProps } from 'ui/shared/Pagination'; import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
interface Props { interface Props {
pagination: PaginationProps; pagination: PaginationProps;
isPaginationVisible: boolean;
} }
const TxsTabSlot = ({ pagination }: Props) => { const TxsTabSlot = ({ pagination, isPaginationVisible }: Props) => {
const isMobile = useIsMobile(); if (!isPaginationVisible) {
if (isMobile) {
return null; return null;
} }
......
...@@ -19,29 +19,30 @@ type Props = { ...@@ -19,29 +19,30 @@ type Props = {
showBlockInfo: boolean; showBlockInfo: boolean;
showSocketInfo: boolean; showSocketInfo: boolean;
currentAddress?: string; currentAddress?: string;
enableTimeIncrement?: boolean;
} }
const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo, currentAddress }: Props) => { const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo, currentAddress, enableTimeIncrement }: Props) => {
return ( return (
<Table variant="simple" minWidth="950px" size="xs"> <Table variant="simple" minWidth="950px" size="xs">
<TheadSticky top={ top }> <TheadSticky top={ top }>
<Tr> <Tr>
<Th width="54px"></Th> <Th width="54px"></Th>
<Th width="18%">Txn hash</Th> <Th width="22%">Txn hash</Th>
<Th width="20%">Type</Th> <Th width="160px">Type</Th>
<Th width="15%">Method</Th> <Th width="20%">Method</Th>
{ showBlockInfo && <Th width="11%">Block</Th> } { showBlockInfo && <Th width="18%">Block</Th> }
<Th width={{ xl: '128px', base: '66px' }}>From</Th> <Th width={{ xl: '128px', base: '66px' }}>From</Th>
<Th width={{ xl: currentAddress ? '48px' : '36px', base: '0' }}></Th> <Th width={{ xl: currentAddress ? '48px' : '36px', base: '0' }}></Th>
<Th width={{ xl: '128px', base: '66px' }}>To</Th> <Th width={{ xl: '128px', base: '66px' }}>To</Th>
<Th width="18%" isNumeric> <Th width="20%" isNumeric>
<Link onClick={ sort('val') } display="flex" justifyContent="end"> <Link onClick={ sort('val') } display="flex" justifyContent="end">
{ sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> } { sorting === 'val-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'val-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> } { sorting === 'val-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
{ `Value ${ appConfig.network.currency.symbol }` } { `Value ${ appConfig.network.currency.symbol }` }
</Link> </Link>
</Th> </Th>
<Th width="18%" isNumeric pr={ 5 }> <Th width="20%" isNumeric pr={ 5 }>
<Link onClick={ sort('fee') } display="flex" justifyContent="end"> <Link onClick={ sort('fee') } display="flex" justifyContent="end">
{ sorting === 'fee-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> } { sorting === 'fee-asc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(-90deg)"/> }
{ sorting === 'fee-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> } { sorting === 'fee-desc' && <Icon boxSize={ 5 } as={ rightArrowIcon } transform="rotate(90deg)"/> }
...@@ -62,6 +63,7 @@ const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo, curr ...@@ -62,6 +63,7 @@ const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo, curr
tx={ item } tx={ item }
showBlockInfo={ showBlockInfo } showBlockInfo={ showBlockInfo }
currentAddress={ currentAddress } currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
/> />
)) } )) }
</Tbody> </Tbody>
......
...@@ -21,7 +21,7 @@ import React from 'react'; ...@@ -21,7 +21,7 @@ import React from 'react';
import type { Transaction } from 'types/api/transaction'; import type { Transaction } from 'types/api/transaction';
import rightArrowIcon from 'icons/arrows/east.svg'; import rightArrowIcon from 'icons/arrows/east.svg';
import dayjs from 'lib/date/dayjs'; import useTimeAgoIncrement from 'lib/hooks/useTimeAgoIncrement';
import link from 'lib/link/link'; import link from 'lib/link/link';
import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton'; import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
...@@ -39,12 +39,15 @@ type Props = { ...@@ -39,12 +39,15 @@ type Props = {
tx: Transaction; tx: Transaction;
showBlockInfo: boolean; showBlockInfo: boolean;
currentAddress?: string; currentAddress?: string;
enableTimeIncrement?: boolean;
} }
const TxsTableItem = ({ tx, showBlockInfo, currentAddress }: Props) => { const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }: Props) => {
const isOut = Boolean(currentAddress && currentAddress === tx.from.hash); const isOut = Boolean(currentAddress && currentAddress === tx.from.hash);
const isIn = Boolean(currentAddress && currentAddress === tx.to?.hash); const isIn = Boolean(currentAddress && currentAddress === tx.to?.hash);
const timeAgo = useTimeAgoIncrement(tx.timestamp, enableTimeIncrement);
const addressFrom = ( const addressFrom = (
<Address> <Address>
<Tooltip label={ tx.from.implementation_name }> <Tooltip label={ tx.from.implementation_name }>
...@@ -93,7 +96,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress }: Props) => { ...@@ -93,7 +96,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress }: Props) => {
fontWeight="700" fontWeight="700"
/> />
</Address> </Address>
<Text color="gray.500" fontWeight="400">{ dayjs(tx.timestamp).fromNow() }</Text> { tx.timestamp && <Text color="gray.500" fontWeight="400">{ timeAgo }</Text> }
</VStack> </VStack>
</Td> </Td>
<Td> <Td>
...@@ -150,7 +153,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress }: Props) => { ...@@ -150,7 +153,7 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress }: Props) => {
</Td> </Td>
</Hide> </Hide>
<Td isNumeric> <Td isNumeric>
<CurrencyValue value={ tx.value }/> <CurrencyValue value={ tx.value } accuracy={ 8 }/>
</Td> </Td>
<Td isNumeric> <Td isNumeric>
<CurrencyValue value={ tx.fee.value } accuracy={ 8 }/> <CurrencyValue value={ tx.fee.value } accuracy={ 8 }/>
......
...@@ -6,7 +6,7 @@ import type { TWatchlistItem } from 'types/client/account'; ...@@ -6,7 +6,7 @@ import type { TWatchlistItem } from 'types/client/account';
import useApiFetch from 'lib/api/useApiFetch'; import useApiFetch from 'lib/api/useApiFetch';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
import AccountListItemMobile from 'ui/shared/AccountListItemMobile'; import ListItemMobile from 'ui/shared/ListItemMobile';
import TableItemActionButtons from 'ui/shared/TableItemActionButtons'; import TableItemActionButtons from 'ui/shared/TableItemActionButtons';
import WatchListAddressItem from './WatchListAddressItem'; import WatchListAddressItem from './WatchListAddressItem';
...@@ -82,7 +82,7 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -82,7 +82,7 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
}, [ mutate ]); }, [ mutate ]);
return ( return (
<AccountListItemMobile> <ListItemMobile>
<Box maxW="100%"> <Box maxW="100%">
<WatchListAddressItem item={ item }/> <WatchListAddressItem item={ item }/>
<HStack spacing={ 3 } mt={ 6 }> <HStack spacing={ 3 } mt={ 6 }>
...@@ -106,7 +106,7 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => { ...@@ -106,7 +106,7 @@ const WatchListItem = ({ item, onEditClick, onDeleteClick }: Props) => {
</HStack> </HStack>
<TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/> <TableItemActionButtons onDeleteClick={ onItemDeleteClick } onEditClick={ onItemEditClick }/>
</Flex> </Flex>
</AccountListItemMobile> </ListItemMobile>
); );
}; };
......
...@@ -3476,6 +3476,11 @@ ...@@ -3476,6 +3476,11 @@
"@types/d3-transition" "*" "@types/d3-transition" "*"
"@types/d3-zoom" "*" "@types/d3-zoom" "*"
"@types/dom-to-image@^2.6.4":
version "2.6.4"
resolved "https://registry.yarnpkg.com/@types/dom-to-image/-/dom-to-image-2.6.4.tgz#008411e23903cb0ee9e51a42ab8358c609541ee8"
integrity sha512-UddUdGF1qulrSDulkz3K2Ypq527MR6ixlgAzqLbxSiQ0icx0XDlIV+h4+edmjq/1dqn0KgN0xGSe1kI9t+vGuw==
"@types/estree@^1.0.0": "@types/estree@^1.0.0":
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2"
...@@ -3537,6 +3542,11 @@ ...@@ -3537,6 +3542,11 @@
expect "^29.0.0" expect "^29.0.0"
pretty-format "^29.0.0" pretty-format "^29.0.0"
"@types/js-cookie@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.2.tgz#451eaeece64c6bdac8b2dde0caab23b085899e0d"
integrity sha512-6+0ekgfusHftJNYpihfkMu8BWdeHs9EOJuGcSofErjstGPfPGEu9yTu4t460lTzzAMl2cM5zngQJqPMHbbnvYA==
"@types/jsdom@^20.0.0": "@types/jsdom@^20.0.0":
version "20.0.1" version "20.0.1"
resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-20.0.1.tgz#07c14bc19bd2f918c1929541cdaacae894744808" resolved "https://registry.yarnpkg.com/@types/jsdom/-/jsdom-20.0.1.tgz#07c14bc19bd2f918c1929541cdaacae894744808"
...@@ -5045,6 +5055,11 @@ dom-serializer@^1.0.1: ...@@ -5045,6 +5055,11 @@ dom-serializer@^1.0.1:
domhandler "^4.2.0" domhandler "^4.2.0"
entities "^2.0.0" entities "^2.0.0"
dom-to-image@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/dom-to-image/-/dom-to-image-2.6.0.tgz#8a503608088c87b1c22f9034ae032e1898955867"
integrity sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==
domelementtype@^2.0.1, domelementtype@^2.2.0: domelementtype@^2.0.1, domelementtype@^2.2.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
...@@ -6975,6 +6990,11 @@ joycon@^3.1.1: ...@@ -6975,6 +6990,11 @@ joycon@^3.1.1:
resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03"
integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==
js-cookie@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414"
integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==
js-sdsl@^4.1.4: js-sdsl@^4.1.4:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0"
...@@ -9004,11 +9024,6 @@ type-fest@^0.21.3: ...@@ -9004,11 +9024,6 @@ type-fest@^0.21.3:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
typescript-cookie@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/typescript-cookie/-/typescript-cookie-1.0.4.tgz#22715aa394275e967fd734fa1417e24870504985"
integrity sha512-vZo252VmoEleD/dbE9Wb2lMK63V3M/8aqFbp2Pdb4Oxq8YqqADJ7iMh8THZenFXN+uZJPE8RXkztEaHkOptH4w==
typescript@4.7.2: typescript@4.7.2:
version "4.7.2" version "4.7.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.2.tgz#1f9aa2ceb9af87cca227813b4310fff0b51593c4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.2.tgz#1f9aa2ceb9af87cca227813b4310fff0b51593c4"
......
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