Commit 26e3e22b authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into home-fixes

parents a0940fab 1193c911
...@@ -4,6 +4,7 @@ NEXT_PUBLIC_APP_INSTANCE=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_INSTANCE__ ...@@ -4,6 +4,7 @@ NEXT_PUBLIC_APP_INSTANCE=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_INSTANCE__
NEXT_PUBLIC_APP_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_PROTOCOL__ NEXT_PUBLIC_APP_PROTOCOL=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_PROTOCOL__
NEXT_PUBLIC_APP_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_HOST__ NEXT_PUBLIC_APP_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_HOST__
NEXT_PUBLIC_APP_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_PORT__ NEXT_PUBLIC_APP_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_APP_PORT__
NEXT_PUBLIC_AUTH_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_AUTH_URL__
# network config # network config
NEXT_PUBLIC_NETWORK_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_NAME__ NEXT_PUBLIC_NETWORK_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_NAME__
......
...@@ -68,6 +68,7 @@ The app instance could be customized by passing following variables to NodeJS en ...@@ -68,6 +68,7 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` | | NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM | `string` | Link to form where authors can submit their dapps to the marketplace | `https://airtable.com/shrqUAcjgGJ4jU88C` |
| NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` | | NEXT_PUBLIC_NETWORK_EXPLORERS | `Array<NetworkExplorer>` where `NetworkExplorer` can have following [properties](#network-explorer-configuration-properties) | Used to build up links to transactions, blocks, addresses in other chain explorers. | `[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/tx'}}]` |
| NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` *(optional)* | Verification type in the network | `mining` | | NEXT_PUBLIC_NETWORK_VERIFICATION_TYPE | `validation` or `mining` *(optional)* | Verification type in the network | `mining` |
| NEXT_PUBLIC_AUTH_URL | `string` *(optional)* | Account auth base url; the login path (`/auth/auth0`) will be added to it at execution time | `https://blockscout.com` |
| NEXT_PUBLIC_LOGOUT_URL | `string` *(optional)* | Account logout url | `https://blockscoutcom.us.auth0.com/v2/logout` | | NEXT_PUBLIC_LOGOUT_URL | `string` *(optional)* | Account logout url | `https://blockscoutcom.us.auth0.com/v2/logout` |
| NEXT_PUBLIC_LOGOUT_RETURN_URL | `string` *(optional)* | Account logout return url | `https://blockscout.com/poa/core/auth/logout` | | NEXT_PUBLIC_LOGOUT_RETURN_URL | `string` *(optional)* | Account logout return url | `https://blockscout.com/poa/core/auth/logout` |
| NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cup'>` *(optional)* | List of charts displayed on the home page | `['daily_txs','coin_price','market_cup']` | | NEXT_PUBLIC_HOMEPAGE_CHARTS | `Array<'daily_txs' \| 'coin_price' \| 'market_cup'>` *(optional)* | List of charts displayed on the home page | `['daily_txs','coin_price','market_cup']` |
......
...@@ -25,6 +25,7 @@ const baseUrl = [ ...@@ -25,6 +25,7 @@ const baseUrl = [
process.env.NEXT_PUBLIC_VERCEL_URL || appHost, process.env.NEXT_PUBLIC_VERCEL_URL || appHost,
appPort && ':' + appPort, appPort && ':' + appPort,
].filter(Boolean).join(''); ].filter(Boolean).join('');
const authUrl = getEnvValue(process.env.NEXT_PUBLIC_AUTH_URL) || baseUrl;
const apiHost = getEnvValue(process.env.NEXT_PUBLIC_API_HOST); const apiHost = getEnvValue(process.env.NEXT_PUBLIC_API_HOST);
const logoutUrl = (() => { const logoutUrl = (() => {
...@@ -81,6 +82,7 @@ const config = Object.freeze({ ...@@ -81,6 +82,7 @@ const config = Object.freeze({
host: appHost, host: appHost,
port: appPort, port: appPort,
baseUrl, baseUrl,
authUrl,
logoutUrl, logoutUrl,
api: { api: {
endpoint: apiHost ? `https://${ apiHost }` : 'https://blockscout.com', endpoint: apiHost ? `https://${ apiHost }` : 'https://blockscout.com',
......
# ui config # ui config
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_FEATURED_NETWORKS=[{'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','icon':'https://www.fillmurray.com/60/60','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'},{'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'}] NEXT_PUBLIC_FEATURED_NETWORKS=[{'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'},{'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'}]
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}] NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup'] NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup']
NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=radial-gradient(at 12% 37%, hsla(324,73%,67%,1) 0px, transparent 50%), radial-gradient(at 62% 14%, hsla(256,87%,73%,1) 0px, transparent 50%), radial-gradient(at 84% 80%, hsla(128,75%,73%,1) 0px, transparent 50%), radial-gradient(at 57% 46%, hsla(285,63%,72%,1) 0px, transparent 50%), radial-gradient(at 37% 30%, hsla(174,70%,61%,1) 0px, transparent 50%), radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 67% 57%, hsla(14,95%,76%,1) 0px, transparent 50%) NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=radial-gradient(at 12% 37%, hsla(324,73%,67%,1) 0px, transparent 50%), radial-gradient(at 62% 14%, hsla(256,87%,73%,1) 0px, transparent 50%), radial-gradient(at 84% 80%, hsla(128,75%,73%,1) 0px, transparent 50%), radial-gradient(at 57% 46%, hsla(285,63%,72%,1) 0px, transparent 50%), radial-gradient(at 37% 30%, hsla(174,70%,61%,1) 0px, transparent 50%), radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 67% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)
......
...@@ -472,7 +472,7 @@ frontend: ...@@ -472,7 +472,7 @@ frontend:
environment: environment:
# ui config # ui config
NEXT_PUBLIC_FEATURED_NETWORKS: NEXT_PUBLIC_FEATURED_NETWORKS:
_default: "[{'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','icon':'https://www.fillmurray.com/60/60','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'},{'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'}]" _default: "[{'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'},{'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'}]"
NEXT_PUBLIC_NETWORK_EXPLORERS: NEXT_PUBLIC_NETWORK_EXPLORERS:
_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'}}]" _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'}}]"
# network config # network config
......
...@@ -367,10 +367,12 @@ frontend: ...@@ -367,10 +367,12 @@ frontend:
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED: NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED:
_default: 'true' _default: 'true'
NEXT_PUBLIC_FEATURED_NETWORKS: NEXT_PUBLIC_FEATURED_NETWORKS:
_default: "[{'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','icon':'https://www.fillmurray.com/60/60','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'},{'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'}]" _default: "[{'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'},{'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'}]"
NEXT_PUBLIC_API_HOST: NEXT_PUBLIC_API_HOST:
_default: blockscout.com _default: blockscout.com
review: blockscout-main.test.aws-k8s.blockscout.com review: blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_AUTH_URL:
_default: https://blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_API_BASE_PATH: NEXT_PUBLIC_API_BASE_PATH:
_default: / _default: /
NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM: NEXT_PUBLIC_MARKETPLACE_SUBMIT_FORM:
......
...@@ -2,22 +2,27 @@ import clamp from 'lodash/clamp'; ...@@ -2,22 +2,27 @@ import clamp from 'lodash/clamp';
import throttle from 'lodash/throttle'; import throttle from 'lodash/throttle';
import React from 'react'; import React from 'react';
const ScrollDirectionContext = React.createContext<'up' | 'down' | null>(null);
import isBrowser from 'lib/isBrowser'; import isBrowser from 'lib/isBrowser';
const SCROLL_DIFF_THRESHOLD = 20; const SCROLL_DIFF_THRESHOLD = 20;
type Directions = 'up' | 'down'; type Directions = 'up' | 'down';
export default function useScrollDirection() { interface Props {
children: React.ReactNode;
}
export function ScrollDirectionProvider({ children }: Props) {
const prevScrollPosition = React.useRef(isBrowser() ? window.pageYOffset : 0); const prevScrollPosition = React.useRef(isBrowser() ? window.pageYOffset : 0);
const [ scrollDirection, setDirection ] = React.useState<Directions>(); const [ scrollDirection, setDirection ] = React.useState<Directions | null>(null);
const handleScroll = React.useCallback(() => { const handleScroll = React.useCallback(() => {
const currentScrollPosition = clamp(window.pageYOffset, 0, window.document.body.scrollHeight - window.innerHeight); const currentScrollPosition = clamp(window.pageYOffset, 0, window.document.body.scrollHeight - window.innerHeight);
const scrollDiff = currentScrollPosition - prevScrollPosition.current; const scrollDiff = currentScrollPosition - prevScrollPosition.current;
if (window.pageYOffset === 0) { if (window.pageYOffset === 0) {
setDirection(undefined); setDirection(null);
} else if (Math.abs(scrollDiff) > SCROLL_DIFF_THRESHOLD) { } else if (Math.abs(scrollDiff) > SCROLL_DIFF_THRESHOLD) {
setDirection(scrollDiff < 0 ? 'up' : 'down'); setDirection(scrollDiff < 0 ? 'up' : 'down');
} }
...@@ -33,9 +38,21 @@ export default function useScrollDirection() { ...@@ -33,9 +38,21 @@ export default function useScrollDirection() {
return () => { return () => {
window.removeEventListener('scroll', throttledHandleScroll); window.removeEventListener('scroll', throttledHandleScroll);
}; };
// replicate componentDidMount // replicate componentDidMount
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
return scrollDirection; return (
<ScrollDirectionContext.Provider value={ scrollDirection }>
{ children }
</ScrollDirectionContext.Provider>
);
}
export function useScrollDirection() {
const context = React.useContext(ScrollDirectionContext);
if (context === undefined) {
throw new Error('useScrollDirection must be used within a ScrollDirectionProvider');
}
return context;
} }
import _clamp from 'lodash/clamp';
import React from 'react'; import React from 'react';
const MAX_DELAY = 500; const DURATION = 300;
const MIN_DELAY = 50;
export default function useGradualIncrement(initialValue: number): [number, (inc: number) => void] { export default function useGradualIncrement(initialValue: number): [number, (inc: number) => void] {
const [ num, setNum ] = React.useState(initialValue); const [ num, setNum ] = React.useState(initialValue);
const queue = React.useRef<number>(0); const queue = React.useRef<number>(0);
const timeoutId = React.useRef(0); const timeoutId = React.useRef(0);
const delay = React.useRef(0);
const incrementDelayed = React.useCallback(() => { const incrementDelayed = React.useCallback(() => {
if (queue.current === 0) { if (queue.current === 0) {
...@@ -33,8 +32,13 @@ export default function useGradualIncrement(initialValue: number): [number, (inc ...@@ -33,8 +32,13 @@ export default function useGradualIncrement(initialValue: number): [number, (inc
React.useEffect(() => { React.useEffect(() => {
if (queue.current > 0 && !timeoutId.current) { if (queue.current > 0 && !timeoutId.current) {
const delay = _clamp(MAX_DELAY / queue.current * 1.5, MIN_DELAY, MAX_DELAY); if (!delay.current) {
timeoutId.current = window.setTimeout(incrementDelayed, delay); delay.current = DURATION / queue.current;
} else if (delay.current > DURATION / queue.current) {
// in case if queue size is increased since last DOM update
delay.current = DURATION / queue.current;
}
timeoutId.current = window.setTimeout(incrementDelayed, delay.current);
} }
}, [ incrementDelayed, num ]); }, [ incrementDelayed, num ]);
......
import throttle from 'lodash/throttle';
import React from 'react';
export default function useIsSticky(ref: React.RefObject<HTMLDivElement>, offset = 0, isEnabled = true) {
const [ isSticky, setIsSticky ] = React.useState(false);
const handleScroll = React.useCallback(() => {
if (
Number(ref.current?.getBoundingClientRect().y) < offset
) {
setIsSticky(true);
} else {
setIsSticky(false);
}
}, [ ref, offset ]);
React.useEffect(() => {
if (!isEnabled) {
return;
}
const throttledHandleScroll = throttle(handleScroll, 300);
window.addEventListener('scroll', throttledHandleScroll);
return () => {
window.removeEventListener('scroll', throttledHandleScroll);
};
// replicate componentDidMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ isEnabled ]);
return isSticky;
}
...@@ -52,11 +52,11 @@ export default function useNewTxsSocket() { ...@@ -52,11 +52,11 @@ export default function useNewTxsSocket() {
}, [ setNum ]); }, [ setNum ]);
const handleSocketClose = React.useCallback(() => { const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please click here to load new transactions.'); setSocketAlert('Connection is lost. Please reload page.');
}, []); }, []);
const handleSocketError = React.useCallback(() => { const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new transactions. Please click here to refresh the page.'); setSocketAlert('An error has occurred while fetching new transactions. Please reload page.');
}, []); }, []);
const channel = useSocketChannel({ const channel = useSocketChannel({
......
...@@ -22,13 +22,17 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -22,13 +22,17 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
const paginationFields = PAGINATION_FIELDS[queryName]; const paginationFields = PAGINATION_FIELDS[queryName];
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
const [ page, setPage ] = React.useState(1); const [ page, setPage ] = React.useState<number>(router.query.page && !Array.isArray(router.query.page) ? Number(router.query.page) : 1);
const currPageParams = pick(router.query, paginationFields); const currPageParams = pick(router.query, paginationFields);
const [ pageParams, setPageParams ] = React.useState<Array<PaginationParams<QueryName>>>([ ]); const [ pageParams, setPageParams ] = React.useState<Array<PaginationParams<QueryName>>>([ ]);
const fetch = useFetch(); const fetch = useFetch();
const isMounted = React.useRef(false);
const canGoBackwards = React.useRef(!router.query.page);
const queryKey = [ queryName, ...(queryIds || []), { page, filters } ];
const queryResult = useQuery<unknown, unknown, PaginatedResponse<QueryName>>( const queryResult = useQuery<unknown, unknown, PaginatedResponse<QueryName>>(
[ queryName, ...(queryIds || []), { page, filters } ], queryKey,
async() => { async() => {
const params: Array<string> = []; const params: Array<string> = [];
...@@ -42,7 +46,7 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -42,7 +46,7 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
return fetch(`${ apiPath }${ params.length ? '?' + params.join('&') : '' }`); return fetch(`${ apiPath }${ params.length ? '?' + params.join('&') : '' }`);
}, },
{ staleTime: Infinity, ...options }, { staleTime: page === 1 ? 0 : Infinity, ...options },
); );
const { data } = queryResult; const { data } = queryResult;
...@@ -57,6 +61,8 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -57,6 +61,8 @@ 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);
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }) router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => { .then(() => {
animateScroll.scrollToTop({ duration: 0 }); animateScroll.scrollToTop({ duration: 0 });
...@@ -67,13 +73,14 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -67,13 +73,14 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
const onPrevPageClick = useCallback(() => { const onPrevPageClick = useCallback(() => {
// returning to the first page // returning to the first page
// we dont have pagination params for the first page // we dont have pagination params for the first page
let nextPageQuery: typeof router.query; let nextPageQuery: typeof router.query = {};
if (page === 2) { if (page === 2) {
nextPageQuery = omit(router.query, paginationFields); nextPageQuery = omit(router.query, paginationFields, 'page');
canGoBackwards.current = true;
} else { } else {
const nextPageParams = { ...pageParams[page - 2] }; const nextPageParams = { ...pageParams[page - 2] };
nextPageQuery = { ...router.query };
nextPageParams && Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString()); nextPageParams && Object.entries(nextPageParams).forEach(([ key, val ]) => nextPageQuery[key] = val.toString());
nextPageQuery.page = String(page - 1);
} }
router.query = nextPageQuery; router.query = nextPageQuery;
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }) router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
...@@ -86,10 +93,11 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -86,10 +93,11 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
const resetPage = useCallback(() => { const resetPage = useCallback(() => {
queryClient.clear(); queryClient.clear();
router.push({ pathname: router.pathname, query: omit(router.query, paginationFields) }, undefined, { shallow: true }).then(() => { router.push({ pathname: router.pathname, query: omit(router.query, paginationFields, 'page') }, undefined, { shallow: true }).then(() => {
animateScroll.scrollToTop({ duration: 0 }); animateScroll.scrollToTop({ duration: 0 });
setPage(1); setPage(1);
setPageParams([ ]); setPageParams([ ]);
canGoBackwards.current = true;
}); });
}, [ queryClient, router, paginationFields ]); }, [ queryClient, router, paginationFields ]);
...@@ -103,7 +111,23 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -103,7 +111,23 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
hasPaginationParams, hasPaginationParams,
resetPage, resetPage,
hasNextPage: nextPageParams ? Object.keys(nextPageParams).length > 0 : false, hasNextPage: nextPageParams ? Object.keys(nextPageParams).length > 0 : false,
canGoBackwards: canGoBackwards.current,
}; };
React.useEffect(() => {
if (page !== 1 && isMounted.current) {
queryClient.cancelQueries({ queryKey });
setPage(1);
}
// hook should run only when queryName has changed
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ queryName ]);
React.useEffect(() => {
window.setTimeout(() => {
isMounted.current = true;
}, 0);
}, []);
return { ...queryResult, pagination }; return { ...queryResult, pagination };
} }
...@@ -25,8 +25,9 @@ export default function link( ...@@ -25,8 +25,9 @@ export default function link(
return paramValue ? `/${ paramValue }` : ''; return paramValue ? `/${ paramValue }` : '';
}); });
const baseUrl = routeName === 'auth' ? appConfig.authUrl : appConfig.baseUrl;
const url = new URL(path, appConfig.baseUrl); const url = new URL(path, baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => { queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
url.searchParams.append(key, value); url.searchParams.append(key, value);
......
...@@ -7,19 +7,16 @@ import React, { useState } from 'react'; ...@@ -7,19 +7,16 @@ import React, { useState } from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import { AppContextProvider } from 'lib/appContext'; import { AppContextProvider } from 'lib/appContext';
import { Chakra } from 'lib/Chakra'; import { Chakra } from 'lib/Chakra';
import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection';
import useConfigSentry from 'lib/hooks/useConfigSentry'; import useConfigSentry from 'lib/hooks/useConfigSentry';
import type { ErrorType } from 'lib/hooks/useFetch'; import type { ErrorType } from 'lib/hooks/useFetch';
import useScrollDirection from 'lib/hooks/useScrollDirection';
import { SocketProvider } from 'lib/socket/context'; import { SocketProvider } from 'lib/socket/context';
import theme from 'theme'; import theme from 'theme';
import ScrollDirectionContext from 'ui/ScrollDirectionContext';
import AppError from 'ui/shared/AppError/AppError'; import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary'; import ErrorBoundary from 'ui/shared/ErrorBoundary';
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
useConfigSentry(); useConfigSentry();
const directionContext = useScrollDirection();
const [ queryClient ] = useState(() => new QueryClient({ const [ queryClient ] = useState(() => new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
...@@ -62,11 +59,11 @@ function MyApp({ Component, pageProps }: AppProps) { ...@@ -62,11 +59,11 @@ function MyApp({ Component, pageProps }: AppProps) {
<ErrorBoundary renderErrorScreen={ renderErrorScreen } onError={ handleError }> <ErrorBoundary renderErrorScreen={ renderErrorScreen } onError={ handleError }>
<AppContextProvider pageProps={ pageProps }> <AppContextProvider pageProps={ pageProps }>
<QueryClientProvider client={ queryClient }> <QueryClientProvider client={ queryClient }>
<ScrollDirectionContext.Provider value={ directionContext }> <ScrollDirectionProvider>
<SocketProvider url={ `${ appConfig.api.socket }${ appConfig.api.basePath }/socket/v2` }> <SocketProvider url={ `${ appConfig.api.socket }${ appConfig.api.basePath }/socket/v2` }>
<Component { ...pageProps }/> <Component { ...pageProps }/>
</SocketProvider> </SocketProvider>
</ScrollDirectionContext.Provider> </ScrollDirectionProvider>
<ReactQueryDevtools/> <ReactQueryDevtools/>
</QueryClientProvider> </QueryClientProvider>
</AppContextProvider> </AppContextProvider>
......
...@@ -56,6 +56,9 @@ const variantOutline = defineStyle((props) => { ...@@ -56,6 +56,9 @@ const variantOutline = defineStyle((props) => {
if (c === 'gray-dark') { if (c === 'gray-dark') {
return mode('blue.700', 'gray.50')(props); return mode('blue.700', 'gray.50')(props);
} }
if (c === 'blue') {
return mode('blue.400', 'gray.50')(props);
}
return 'blue.400'; return 'blue.400';
})(); })();
......
...@@ -44,6 +44,7 @@ export interface BlockTransactionsResponse { ...@@ -44,6 +44,7 @@ export interface BlockTransactionsResponse {
next_page_params: { next_page_params: {
block_number: number; block_number: number;
items_count: number; items_count: number;
index: number;
} | null; } | null;
} }
......
...@@ -9,6 +9,8 @@ import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull'; ...@@ -9,6 +9,8 @@ import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull';
export type PaginatedQueryKeys = export type PaginatedQueryKeys =
QueryKeys.blocks | QueryKeys.blocks |
QueryKeys.blocksReorgs |
QueryKeys.blocksUncles |
QueryKeys.blockTxs | QueryKeys.blockTxs |
QueryKeys.txsValidate | QueryKeys.txsValidate |
QueryKeys.txsPending | QueryKeys.txsPending |
...@@ -17,7 +19,7 @@ export type PaginatedQueryKeys = ...@@ -17,7 +19,7 @@ export type PaginatedQueryKeys =
QueryKeys.txTokenTransfers; QueryKeys.txTokenTransfers;
export type PaginatedResponse<Q extends PaginatedQueryKeys> = export type PaginatedResponse<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.blocks ? BlocksResponse : Q extends (QueryKeys.blocks | QueryKeys.blocksReorgs | QueryKeys.blocksUncles) ? BlocksResponse :
Q extends QueryKeys.blockTxs ? BlockTransactionsResponse : Q extends QueryKeys.blockTxs ? BlockTransactionsResponse :
Q extends QueryKeys.txsValidate ? TransactionsResponseValidated : Q extends QueryKeys.txsValidate ? TransactionsResponseValidated :
Q extends QueryKeys.txsPending ? TransactionsResponsePending : Q extends QueryKeys.txsPending ? TransactionsResponsePending :
...@@ -41,7 +43,9 @@ type PaginationFields = { ...@@ -41,7 +43,9 @@ type PaginationFields = {
export const PAGINATION_FIELDS: PaginationFields = { export const PAGINATION_FIELDS: PaginationFields = {
[QueryKeys.blocks]: [ 'block_number', 'items_count' ], [QueryKeys.blocks]: [ 'block_number', 'items_count' ],
[QueryKeys.blockTxs]: [ 'block_number', 'items_count' ], [QueryKeys.blocksReorgs]: [ 'block_number', 'items_count' ],
[QueryKeys.blocksUncles]: [ 'block_number', 'items_count' ],
[QueryKeys.blockTxs]: [ 'block_number', 'items_count', 'index' ],
[QueryKeys.txsValidate]: [ 'block_number', 'items_count', 'filter', 'index' ], [QueryKeys.txsValidate]: [ 'block_number', 'items_count', 'filter', 'index' ],
[QueryKeys.txsPending]: [ 'filter', 'hash', 'inserted_at' ], [QueryKeys.txsPending]: [ 'filter', 'hash', 'inserted_at' ],
[QueryKeys.txInternals]: [ 'block_number', 'items_count', 'transaction_hash', 'index', 'transaction_index' ], [QueryKeys.txInternals]: [ 'block_number', 'items_count', 'transaction_hash', 'index', 'transaction_index' ],
......
import type { AddressParam } from './addressParams'; import type { AddressParam } from './addressParams';
import type { BlockTransactionsResponse } from './block';
import type { DecodedInput } from './decodedInput'; import type { DecodedInput } from './decodedInput';
import type { Fee } from './fee'; import type { Fee } from './fee';
import type { TokenTransfer } from './tokenTransfer'; import type { TokenTransfer } from './tokenTransfer';
...@@ -71,3 +72,5 @@ export interface TransactionsResponsePending { ...@@ -71,3 +72,5 @@ export interface TransactionsResponsePending {
} }
export type TransactionType = 'token_transfer' | 'contract_creation' | 'contract_call' | 'token_creation' | 'coin_transfer' export type TransactionType = 'token_transfer' | 'contract_creation' | 'contract_call' | 'token_creation' | 'coin_transfer'
export type TxsResponse = TransactionsResponseValidated | TransactionsResponsePending | BlockTransactionsResponse;
...@@ -13,6 +13,8 @@ export enum QueryKeys { ...@@ -13,6 +13,8 @@ export enum QueryKeys {
blockTxs = 'block-transactions', blockTxs = 'block-transactions',
block = 'block', block = 'block',
blocks = 'blocks', blocks = 'blocks',
blocksReorgs = 'blocks-reorgs',
blocksUncles = 'blocks-uncles',
chartsTxs = 'charts-txs', chartsTxs = 'charts-txs',
chartsMarket = 'charts-market', chartsMarket = 'charts-market',
indexBlocks='indexBlocks', indexBlocks='indexBlocks',
......
import React from 'react';
const ScrollDirectionContext = React.createContext<'up' | 'down' | undefined>(undefined);
export default ScrollDirectionContext;
import { useRouter } from 'next/router';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import TxsContent from 'ui/txs/TxsContent';
const BlockTxs = () => {
const router = useRouter();
return (
<TxsContent
queryName={ QueryKeys.blockTxs }
apiPath={ `/node-api/blocks/${ router.query.id }/transactions` }
showBlockInfo={ false }
/>
);
};
export default BlockTxs;
import { Text, Show, Hide, Skeleton, Alert } from '@chakra-ui/react'; import { Text, Show, Hide, Alert } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
...@@ -6,7 +7,7 @@ import type { SocketMessage } from 'lib/socket/types'; ...@@ -6,7 +7,7 @@ import type { SocketMessage } from 'lib/socket/types';
import type { BlockType, BlocksResponse } from 'types/api/block'; import type { BlockType, BlocksResponse } from 'types/api/block';
import { QueryKeys } from 'types/client/queries'; import { QueryKeys } from 'types/client/queries';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; 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';
...@@ -15,25 +16,36 @@ import BlocksTable from 'ui/blocks/BlocksTable'; ...@@ -15,25 +16,36 @@ 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 SkeletonTable from 'ui/shared/SkeletonTable'; import SkeletonTable from 'ui/shared/SkeletonTable';
type QueryResult = UseQueryResult<BlocksResponse> & {
pagination: PaginationProps;
};
interface Props { interface Props {
type?: BlockType; type?: BlockType;
query: QueryResult;
} }
const BlocksContent = ({ type }: Props) => { const BlocksContent = ({ type, query }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const isMobile = useIsMobile();
const [ socketAlert, setSocketAlert ] = React.useState(''); const [ socketAlert, setSocketAlert ] = React.useState('');
const { data, isLoading, isError, pagination } = useQueryWithPages({
apiPath: '/node-api/blocks',
queryName: QueryKeys.blocks,
filters: { type },
});
const isPaginatorHidden = !isLoading && !isError && pagination.page === 1 && !pagination.hasNextPage;
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => { const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
queryClient.setQueryData([ QueryKeys.blocks, { page: pagination.page, filters: { type } } ], (prevData: BlocksResponse | undefined) => { const queryKey = (() => {
switch (type) {
case 'uncle':
return QueryKeys.blocksUncles;
case 'reorg':
return QueryKeys.blocksReorgs;
default:
return QueryKeys.blocks;
}
})();
queryClient.setQueryData([ queryKey, { page: query.pagination.page, filters: { type } } ], (prevData: BlocksResponse | undefined) => {
const shouldAddToList = !type || type === payload.block.type; const shouldAddToList = !type || type === payload.block.type;
if (!prevData) { if (!prevData) {
...@@ -44,7 +56,7 @@ const BlocksContent = ({ type }: Props) => { ...@@ -44,7 +56,7 @@ const BlocksContent = ({ type }: Props) => {
} }
return shouldAddToList ? { ...prevData, items: [ payload.block, ...prevData.items ] } : prevData; return shouldAddToList ? { ...prevData, items: [ payload.block, ...prevData.items ] } : prevData;
}); });
}, [ pagination.page, queryClient, type ]); }, [ query.pagination.page, queryClient, type ]);
const handleSocketClose = React.useCallback(() => { const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please click here to load new blocks.'); setSocketAlert('Connection is lost. Please click here to load new blocks.');
...@@ -58,7 +70,7 @@ const BlocksContent = ({ type }: Props) => { ...@@ -58,7 +70,7 @@ const BlocksContent = ({ type }: Props) => {
topic: 'blocks:new_block', topic: 'blocks:new_block',
onSocketClose: handleSocketClose, onSocketClose: handleSocketClose,
onSocketError: handleSocketError, onSocketError: handleSocketError,
isDisabled: isLoading || isError || pagination.page !== 1, isDisabled: query.isLoading || query.isError || query.pagination.page !== 1,
}); });
useSocketMessage({ useSocketMessage({
channel, channel,
...@@ -67,7 +79,7 @@ const BlocksContent = ({ type }: Props) => { ...@@ -67,7 +79,7 @@ const BlocksContent = ({ type }: Props) => {
}); });
const content = (() => { const content = (() => {
if (isLoading) { if (query.isLoading) {
return ( return (
<> <>
<Show below="lg" key="skeleton-mobile"> <Show below="lg" key="skeleton-mobile">
...@@ -80,37 +92,31 @@ const BlocksContent = ({ type }: Props) => { ...@@ -80,37 +92,31 @@ const BlocksContent = ({ type }: Props) => {
); );
} }
if (isError) { if (query.isError) {
return <DataFetchAlert/>; return <DataFetchAlert/>;
} }
if (data.items.length === 0) { if (query.data.items.length === 0) {
return <Text as="span">There are no blocks.</Text>; return <Text as="span">There are no blocks.</Text>;
} }
return ( return (
<> <>
{ socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> } { socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> }
<Show below="lg" key="content-mobile"><BlocksList data={ data.items }/></Show> <Show below="lg" key="content-mobile"><BlocksList data={ query.data.items }/></Show>
<Hide below="lg" key="content-desktop"><BlocksTable data={ data.items } top={ isPaginatorHidden ? 0 : 80 } page={ pagination.page }/></Hide> <Hide below="lg" key="content-desktop"><BlocksTable data={ query.data.items } top={ 80 } page={ 1 }/></Hide>
</> </>
); );
})(); })();
const totalText = data?.items.length ? const isPaginatorHidden = !query.isLoading && !query.isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
<Text mb={{ base: 0, lg: 6 }}>Total of { data.items[0].height.toLocaleString() } blocks</Text> :
null;
return ( return (
<> <>
{ !isLoading ? { isMobile && !isPaginatorHidden && (
totalText :
<Skeleton h="24px" w="200px" mb={{ base: 0, lg: 6 }}/>
}
{ !isPaginatorHidden && (
<ActionBar> <ActionBar>
<Pagination ml="auto" { ...pagination }/> <Pagination ml="auto" { ...query.pagination }/>
</ActionBar> </ActionBar>
) } ) }
{ content } { content }
...@@ -118,4 +124,4 @@ const BlocksContent = ({ type }: Props) => { ...@@ -118,4 +124,4 @@ const BlocksContent = ({ type }: Props) => {
); );
}; };
export default BlocksContent; export default React.memo(BlocksContent);
import { Flex, Box, Text, Skeleton } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
import type { HomeStats } from 'types/api/stats';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
interface Props {
pagination: PaginationProps;
}
const BlocksTabSlot = ({ pagination }: Props) => {
const isMobile = useIsMobile();
const fetch = useFetch();
const statsQuery = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
() => fetch('/node-api/stats'),
);
if (isMobile) {
return null;
}
return (
<Flex alignItems="center" columnGap={ 8 }>
{ statsQuery.isLoading && <Skeleton w="175px" h="24px"/> }
{ statsQuery.data?.network_utilization_percentage !== undefined && (
<Box>
<Text as="span" fontSize="sm">
Network utilization (last 50 blocks):{ nbsp }
</Text>
<Text as="span" fontSize="sm" color="blue.400" fontWeight={ 600 }>
{ statsQuery.data.network_utilization_percentage.toFixed(2) }%
</Text>
</Box>
) }
<Pagination my={ 1 } { ...pagination }/>
</Flex>
);
};
export default BlocksTabSlot;
...@@ -8,10 +8,10 @@ import { QueryKeys } from 'types/client/queries'; ...@@ -8,10 +8,10 @@ import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link'; import link from 'lib/link/link';
import TxsNewItemNotice from 'ui/txs/TxsNewItemNotice';
import LatestTxsItem from './LatestTxsItem'; import LatestTxsItem from './LatestTxsItem';
import LatestTxsItemSkeleton from './LatestTxsItemSkeleton'; import LatestTxsItemSkeleton from './LatestTxsItemSkeleton';
import LatestTxsNotice from './LatestTxsNotice';
const LatestTransactions = () => { const LatestTransactions = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -41,7 +41,7 @@ const LatestTransactions = () => { ...@@ -41,7 +41,7 @@ const LatestTransactions = () => {
const txsUrl = link('txs'); const txsUrl = link('txs');
content = ( content = (
<> <>
<LatestTxsNotice/> <TxsNewItemNotice borderBottomRadius={ 0 } url={ link('txs') }/>
<Box mb={{ base: 3, lg: 4 }}> <Box mb={{ base: 3, lg: 4 }}>
{ data.slice(0, txsCount).map((tx => <LatestTxsItem key={ tx.hash } tx={ tx }/>)) } { data.slice(0, txsCount).map((tx => <LatestTxsItem key={ tx.hash } tx={ tx }/>)) }
</Box> </Box>
......
import { Alert, Text, Link, useColorModeValue, useTheme } from '@chakra-ui/react';
import { transparentize } from '@chakra-ui/theme-tools';
import React from 'react';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import link from 'lib/link/link';
interface Props {
className?: string;
}
const LatestTxsNotice = ({ className }: Props) => {
const { num, socketAlert } = useNewTxsSocket();
let content;
if (socketAlert) {
content = 'Connection is lost. Please reload page';
} else if (!num) {
content = (
<Text>scanning new transactions...</Text>
);
} else {
const txsUrl = link('txs');
content = (
<>
<Link href={ txsUrl }>{ num } more transaction{ num > 1 ? 's' : '' }</Link>
<Text whiteSpace="pre"> ha{ num > 1 ? 've' : 's' } come in</Text>
</>
);
}
const theme = useTheme();
return (
<Alert
className={ className }
status="warning"
h="32px"
px={ 4 }
py="6px"
fontWeight={ 400 }
bgColor={ useColorModeValue('orange.50', transparentize('orange.200', 0.16)(theme)) }
color={ useColorModeValue('blackAlpha.800', 'whiteAlpha.800') }
borderBottomRadius={ 0 }
>
{ content }
</Alert>
);
};
export default LatestTxsNotice;
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 type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import BlockDetails from 'ui/block/BlockDetails'; import BlockDetails from 'ui/block/BlockDetails';
import BlockTxs from 'ui/block/BlockTxs';
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 Pagination from 'ui/shared/Pagination';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TxsContent from 'ui/txs/TxsContent';
const TABS: Array<RoutedTab> = [ const TAB_LIST_PROPS = {
{ id: 'index', title: 'Details', component: <BlockDetails/> }, marginBottom: 0,
{ id: 'txs', title: 'Transactions', component: <BlockTxs/> }, py: 5,
]; marginTop: -5,
};
const BlockPageContent = () => { const BlockPageContent = () => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile();
const blockTxsQuery = useQueryWithPages({
apiPath: `/node-api/blocks/${ router.query.id }/transactions`,
queryName: QueryKeys.blockTxs,
options: {
enabled: Boolean(router.query.id && router.query.tab === 'txs'),
},
});
if (!router.query.id) { if (!router.query.id) {
return null; return null;
} }
const tabs: Array<RoutedTab> = [
{ id: 'index', title: 'Details', component: <BlockDetails/> },
{ id: 'txs', title: 'Transactions', component: <TxsContent query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> },
];
return ( return (
<Page> <Page>
<PageTitle text={ `Block #${ router.query.id }` }/> <PageTitle text={ `Block #${ router.query.id }` }/>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 8 }}/> <RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ isMobile ? null : <Pagination { ...blockTxsQuery.pagination }/> }
stickyEnabled={ !isMobile }
/>
</Page> </Page>
); );
}; };
......
...@@ -2,12 +2,20 @@ import { test as base, expect } from '@playwright/experimental-ct-react'; ...@@ -2,12 +2,20 @@ import { test as base, expect } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as blockMock from 'mocks/blocks/block'; import * as blockMock from 'mocks/blocks/block';
import * as statsMock from 'mocks/stats/index';
import * as socketServer from 'playwright/fixtures/socketServer'; import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import BlocksContent from './BlocksContent'; import Blocks from './Blocks';
const API_URL = '/node-api/blocks'; const BLOCKS_API_URL = '/node-api/blocks?type=block';
const STATS_API_URL = '/node-api/stats';
const hooksConfig = {
router: {
query: { tab: 'blocks' },
isReady: true,
},
};
export const test = base.extend<socketServer.SocketServerFixture>({ export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket, createSocket: socketServer.createSocket,
...@@ -17,32 +25,38 @@ export const test = base.extend<socketServer.SocketServerFixture>({ ...@@ -17,32 +25,38 @@ export const test = base.extend<socketServer.SocketServerFixture>({
// test cases which use socket cannot run in parallel since the socket server always run on the same port // test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' }); test.describe.configure({ mode: 'serial' });
test('base view +@mobile', async({ mount, page }) => { test('base view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(blockMock.baseListResponse), body: JSON.stringify(blockMock.baseListResponse),
})); }));
await page.route(STATS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<BlocksContent/> <Blocks/>
</TestApp>, </TestApp>,
{ hooksConfig },
); );
await page.waitForResponse(API_URL); await page.waitForResponse(BLOCKS_API_URL);
await expect(component).toHaveScreenshot(); await expect(component.locator('main')).toHaveScreenshot();
}); });
test('new item from socket', async({ mount, page, createSocket }) => { test('new item from socket', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(blockMock.baseListResponse), body: JSON.stringify(blockMock.baseListResponse),
})); }));
const component = await mount( const component = await mount(
<TestApp withSocket> <TestApp withSocket>
<BlocksContent/> <Blocks/>
</TestApp>, </TestApp>,
{ hooksConfig },
); );
const socket = await createSocket(); const socket = await createSocket();
...@@ -56,24 +70,25 @@ test('new item from socket', async({ mount, page, createSocket }) => { ...@@ -56,24 +70,25 @@ test('new item from socket', async({ mount, page, createSocket }) => {
}, },
}); });
await expect(component).toHaveScreenshot(); await expect(component.locator('main')).toHaveScreenshot();
}); });
test('socket error', async({ mount, page, createSocket }) => { test('socket error', async({ mount, page, createSocket }) => {
await page.route(API_URL, (route) => route.fulfill({ await page.route(BLOCKS_API_URL, (route) => route.fulfill({
status: 200, status: 200,
body: JSON.stringify(blockMock.baseListResponse), body: JSON.stringify(blockMock.baseListResponse),
})); }));
const component = await mount( const component = await mount(
<TestApp withSocket> <TestApp withSocket>
<BlocksContent/> <Blocks/>
</TestApp>, </TestApp>,
{ hooksConfig },
); );
const socket = await createSocket(); const socket = await createSocket();
await socketServer.joinChannel(socket, 'blocks:new_block'); await socketServer.joinChannel(socket, 'blocks:new_block');
socket.close(); socket.close();
await expect(component).toHaveScreenshot(); await expect(component.locator('main')).toHaveScreenshot();
}); });
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { BlockType } from 'types/api/block';
import { QueryKeys } from 'types/client/queries';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import BlocksContent from 'ui/blocks/BlocksContent'; import BlocksContent from 'ui/blocks/BlocksContent';
import BlocksTabSlot from 'ui/blocks/BlocksTabSlot';
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 RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
const TABS: Array<RoutedTab> = [ const TAB_TO_TYPE: Record<string, BlockType> = {
{ id: 'blocks', title: 'All', component: <BlocksContent/> }, blocks: 'block',
{ id: 'reorgs', title: 'Forked', component: <BlocksContent type="reorg"/> }, reorgs: 'reorg',
{ id: 'uncles', title: 'Uncles', component: <BlocksContent type="uncle"/> }, uncles: 'uncle',
]; };
const TAB_TO_QUERY: Record<string, QueryKeys.blocks | QueryKeys.blocksReorgs | QueryKeys.blocksUncles> = {
blocks: QueryKeys.blocks,
reorgs: QueryKeys.blocksReorgs,
uncles: QueryKeys.blocksUncles,
};
const TAB_LIST_PROPS = {
marginBottom: 0,
py: 5,
marginTop: -5,
};
const BlocksPageContent = () => { const BlocksPageContent = () => {
const router = useRouter();
const isMobile = useIsMobile();
const type = router.query.tab && !Array.isArray(router.query.tab) ? TAB_TO_TYPE[router.query.tab] : 'block';
const queryName = router.query.tab && !Array.isArray(router.query.tab) ? TAB_TO_QUERY[router.query.tab] : QueryKeys.blocks;
const blocksQuery = useQueryWithPages({
apiPath: '/node-api/blocks',
queryName,
filters: { type },
});
const tabs: Array<RoutedTab> = [
{ id: 'blocks', title: 'All', component: <BlocksContent type="block" query={ blocksQuery }/> },
{ id: 'reorgs', title: 'Forked', component: <BlocksContent type="reorg" query={ blocksQuery }/> },
{ id: 'uncles', title: 'Uncles', component: <BlocksContent type="uncle" query={ blocksQuery }/> },
];
return ( return (
<Page> <Page>
<PageTitle text="Blocks"/> <PageTitle text="Blocks"/>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 8 }}/> <RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ <BlocksTabSlot pagination={ blocksQuery.pagination }/> }
stickyEnabled={ !isMobile }
/>
</Page> </Page>
); );
}; };
......
...@@ -20,7 +20,7 @@ const PrivateTags = () => { ...@@ -20,7 +20,7 @@ const PrivateTags = () => {
return ( return (
<Page> <Page>
<PageTitle text="Private tags"/> <PageTitle text="Private tags"/>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 8 }}/> <RoutedTabs tabs={ TABS }/>
</Page> </Page>
); );
}; };
......
...@@ -82,7 +82,7 @@ const TransactionPageContent = () => { ...@@ -82,7 +82,7 @@ const TransactionPageContent = () => {
</Flex> </Flex>
) } ) }
</Flex> </Flex>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 8 }}/> <RoutedTabs tabs={ TABS }/>
</Page> </Page>
); );
}; };
......
import { import { Box } from '@chakra-ui/react';
Box, import { useRouter } from 'next/router';
} from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import { QueryKeys } from 'types/client/queries';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types'; import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
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 RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import TxsTab from 'ui/txs/TxsTab'; import TxsContent from 'ui/txs/TxsContent';
import TxsTabSlot from 'ui/txs/TxsTabSlot';
const TAB_LIST_PROPS = {
marginBottom: 0,
py: 5,
marginTop: -5,
};
const Transactions = () => { const Transactions = () => {
const verifiedTitle = appConfig.network.verificationType === 'validation' ? 'Validated' : 'Mined'; const verifiedTitle = appConfig.network.verificationType === 'validation' ? 'Validated' : 'Mined';
const TABS: Array<RoutedTab> = [ const router = useRouter();
{ id: 'validated', title: verifiedTitle, component: <TxsTab tab="validated"/> }, const isMobile = useIsMobile();
{ id: 'pending', title: 'Pending', component: <TxsTab tab="pending"/> }, const filter = router.query.tab === 'pending' ? 'pending' : 'validated';
const txsQuery = useQueryWithPages({
apiPath: '/node-api/transactions',
queryName: filter === 'validated' ? QueryKeys.txsValidate : QueryKeys.txsPending,
filters: { filter },
});
const tabs: Array<RoutedTab> = [
{ id: 'validated', title: verifiedTitle, component: <TxsContent query={ txsQuery }/> },
{ id: 'pending', title: 'Pending', component: <TxsContent query={ txsQuery } showBlockInfo={ false }/> },
]; ];
return ( return (
<Page hideMobileHeaderOnScrollDown> <Page hideMobileHeaderOnScrollDown>
<Box h="100%"> <Box h="100%">
<PageTitle text="Transactions"/> <PageTitle text="Transactions"/>
<RoutedTabs tabs={ TABS } tabListMarginBottom={{ base: 6, lg: 8 }}/> <RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
rightSlot={ <TxsTabSlot pagination={ txsQuery.pagination }/> }
stickyEnabled={ !isMobile }
/>
</Box> </Box>
</Page> </Page>
); );
......
import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code } from '@chakra-ui/react'; import { VStack, Textarea, Button, Alert, AlertTitle, AlertDescription, Link, Code, Flex, Box } from '@chakra-ui/react';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import type { ChangeEvent } from 'react'; import type { ChangeEvent } from 'react';
import React from 'react'; import React from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import useToast from 'lib/hooks/useToast'; import useToast from 'lib/hooks/useToast';
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';
...@@ -12,6 +13,7 @@ import PageTitle from 'ui/shared/Page/PageTitle'; ...@@ -12,6 +13,7 @@ import PageTitle from 'ui/shared/Page/PageTitle';
{ /* will be deleted when we move to new CI */ } { /* will be deleted when we move to new CI */ }
const Vercel = () => { const Vercel = () => {
const toast = useToast(); const toast = useToast();
const [ num, setNum ] = useGradualIncrement(0);
const [ isFormVisible, setFormVisibility ] = React.useState(false); const [ isFormVisible, setFormVisibility ] = React.useState(false);
const [ token, setToken ] = React.useState(''); const [ token, setToken ] = React.useState('');
...@@ -47,10 +49,20 @@ const Vercel = () => { ...@@ -47,10 +49,20 @@ const Vercel = () => {
const prodUrl = 'https://blockscout.com/poa/core'; const prodUrl = 'https://blockscout.com/poa/core';
const handleNumIncrement = React.useCallback(() => {
for (let index = 0; index < 5; index++) {
setNum(5);
}
}, [ setNum ]);
return ( return (
<Page> <Page>
<VStack gap={ 4 } alignItems="flex-start" maxW="1000px"> <VStack gap={ 4 } alignItems="flex-start" maxW="1000px">
<PageTitle text="Vercel page"/> <PageTitle text="Vercel page"/>
<Flex columnGap={ 2 } alignItems="center">
<Box w="50px" textAlign="center">{ num }</Box>
<Button onClick={ handleNumIncrement } size="sm">add</Button>
</Flex>
{ isFormVisible && ( { isFormVisible && (
<> <>
<Alert status="error" flexDirection="column" alignItems="flex-start"> <Alert status="error" flexDirection="column" alignItems="flex-start">
......
import { Flex, useColorModeValue, chakra } from '@chakra-ui/react'; import { Flex, useColorModeValue, chakra } from '@chakra-ui/react';
import throttle from 'lodash/throttle';
import React from 'react'; import React from 'react';
import ScrollDirectionContext from 'ui/ScrollDirectionContext'; import { useScrollDirection } from 'lib/contexts/scrollDirection';
import useIsSticky from 'lib/hooks/useIsSticky';
type Props = { type Props = {
children: React.ReactNode; children: React.ReactNode;
...@@ -13,56 +13,30 @@ const TOP_UP = 106; ...@@ -13,56 +13,30 @@ const TOP_UP = 106;
const TOP_DOWN = 0; const TOP_DOWN = 0;
const ActionBar = ({ children, className }: Props) => { const ActionBar = ({ children, className }: Props) => {
const [ isSticky, setIsSticky ] = React.useState(false);
const ref = React.useRef<HTMLDivElement>(null); const ref = React.useRef<HTMLDivElement>(null);
const scrollDirection = useScrollDirection();
const handleScroll = React.useCallback(() => { const isSticky = useIsSticky(ref, TOP_UP + 5);
if (
Number(ref.current?.getBoundingClientRect().y) < TOP_UP + 5
) {
setIsSticky(true);
} else {
setIsSticky(false);
}
}, [ ]);
React.useEffect(() => {
const throttledHandleScroll = throttle(handleScroll, 300);
window.addEventListener('scroll', throttledHandleScroll);
return () => {
window.removeEventListener('scroll', throttledHandleScroll);
};
// replicate componentDidMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ ]);
const bgColor = useColorModeValue('white', 'black'); const bgColor = useColorModeValue('white', 'black');
return ( return (
<ScrollDirectionContext.Consumer> <Flex
{ (scrollDirection) => ( className={ className }
<Flex backgroundColor={ bgColor }
className={ className } py={ 6 }
backgroundColor={ bgColor } mx={{ base: -4, lg: 0 }}
py={ 6 } px={{ base: 4, lg: 0 }}
mx={{ base: -4, lg: 0 }} justifyContent="space-between"
px={{ base: 4, lg: 0 }} width={{ base: '100vw', lg: 'unset' }}
justifyContent="space-between" position="sticky"
width={{ base: '100vw', lg: 'unset' }} top={{ base: scrollDirection === 'down' ? `${ TOP_DOWN }px` : `${ TOP_UP }px`, lg: 0 }}
position="sticky" transitionProperty="top,box-shadow,background-color,color"
top={{ base: scrollDirection === 'down' ? `${ TOP_DOWN }px` : `${ TOP_UP }px`, lg: 0 }} transitionDuration="slow"
transitionProperty="top,box-shadow,background-color,color" zIndex={{ base: 'sticky2', lg: 'docked' }}
transitionDuration="slow" boxShadow={{ base: isSticky ? 'md' : 'none', lg: 'none' }}
zIndex={{ base: 'sticky2', lg: 'docked' }} ref={ ref }
boxShadow={{ base: isSticky ? 'md' : 'none', lg: 'none' }} >
ref={ ref } { children }
> </Flex>
{ children }
</Flex>
) }
</ScrollDirectionContext.Consumer>
); );
}; };
......
...@@ -44,7 +44,7 @@ const Page = ({ ...@@ -44,7 +44,7 @@ const Page = ({
return ( return (
<Flex w="100%" minH="100vh" alignItems="stretch"> <Flex w="100%" minH="100vh" alignItems="stretch">
<NavigationDesktop/> <NavigationDesktop/>
<Flex flexDir="column" width="100%"> <Flex flexDir="column" flexGrow={ 1 } w={{ base: '100%', lg: 'auto' }}>
<Header isHomePage={ isHomePage } hideOnScrollDown={ hideMobileHeaderOnScrollDown }/> <Header isHomePage={ isHomePage } hideOnScrollDown={ hideMobileHeaderOnScrollDown }/>
<ErrorBoundary renderErrorScreen={ renderErrorScreen }> <ErrorBoundary renderErrorScreen={ renderErrorScreen }>
{ renderedChildren } { renderedChildren }
......
...@@ -11,9 +11,10 @@ export type Props = { ...@@ -11,9 +11,10 @@ export type Props = {
hasNextPage: boolean; hasNextPage: boolean;
hasPaginationParams?: boolean; hasPaginationParams?: boolean;
className?: string; className?: string;
canGoBackwards: boolean;
} }
const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNextPage, hasPaginationParams, className }: Props) => { const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNextPage, hasPaginationParams, className, canGoBackwards }: Props) => {
return ( return (
<Flex <Flex
...@@ -37,8 +38,8 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext ...@@ -37,8 +38,8 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext
aria-label="Next page" aria-label="Next page"
w="36px" w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> } icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 }/> }
mr={ 6 } mr={ 3 }
disabled={ page === 1 } disabled={ !canGoBackwards || page === 1 }
/> />
<Button <Button
variant="outline" variant="outline"
...@@ -59,7 +60,7 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext ...@@ -59,7 +60,7 @@ const Pagination = ({ page, onNextPageClick, onPrevPageClick, resetPage, hasNext
aria-label="Next page" aria-label="Next page"
w="36px" w="36px"
icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> } icon={ <Icon as={ arrowIcon } w={ 5 } h={ 5 } transform="rotate(180deg)"/> }
ml={ 6 } ml={ 3 }
disabled={ !hasNextPage } disabled={ !hasNextPage }
/> />
{ /* not implemented yet */ } { /* not implemented yet */ }
......
...@@ -5,6 +5,8 @@ import { ...@@ -5,6 +5,8 @@ import {
TabList, TabList,
TabPanel, TabPanel,
TabPanels, TabPanels,
Box,
useColorModeValue,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system'; import type { StyleProps } from '@chakra-ui/styled-system';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
...@@ -12,7 +14,9 @@ import React, { useEffect, useState } from 'react'; ...@@ -12,7 +14,9 @@ import React, { useEffect, useState } from 'react';
import type { RoutedTab } from './types'; import type { RoutedTab } from './types';
import { useScrollDirection } from 'lib/contexts/scrollDirection';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useIsSticky from 'lib/hooks/useIsSticky';
import RoutedTabsMenu from './RoutedTabsMenu'; import RoutedTabsMenu from './RoutedTabsMenu';
import useAdaptiveTabs from './useAdaptiveTabs'; import useAdaptiveTabs from './useAdaptiveTabs';
...@@ -26,12 +30,16 @@ const hiddenItemStyles: StyleProps = { ...@@ -26,12 +30,16 @@ const hiddenItemStyles: StyleProps = {
interface Props { interface Props {
tabs: Array<RoutedTab>; tabs: Array<RoutedTab>;
tabListMarginBottom?: ChakraProps['marginBottom']; tabListProps?: ChakraProps;
rightSlot?: React.ReactNode;
stickyEnabled?: boolean;
} }
const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => { const RoutedTabs = ({ tabs, tabListProps, rightSlot, stickyEnabled }: Props) => {
const router = useRouter(); const router = useRouter();
const scrollDirection = useScrollDirection();
const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1); const [ activeTabIndex, setActiveTabIndex ] = useState<number>(tabs.length + 1);
useEffect(() => { useEffect(() => {
if (router.isReady) { if (router.isReady) {
let tabIndex = 0; let tabIndex = 0;
...@@ -46,7 +54,9 @@ const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => { ...@@ -46,7 +54,9 @@ const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => {
}, [ tabs, router ]); }, [ tabs, router ]);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const { tabsCut, tabsList, tabsRefs, listRef } = useAdaptiveTabs(tabs, isMobile); const { tabsCut, tabsList, tabsRefs, listRef, rightSlotRef } = useAdaptiveTabs(tabs, isMobile);
const isSticky = useIsSticky(listRef, 5, stickyEnabled);
const listBgColor = useColorModeValue('white', 'black');
const handleTabChange = React.useCallback((index: number) => { const handleTabChange = React.useCallback((index: number) => {
const nextTab = tabs[index]; const nextTab = tabs[index];
...@@ -59,9 +69,16 @@ const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => { ...@@ -59,9 +69,16 @@ const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => {
}, [ tabs, router ]); }, [ tabs, router ]);
return ( return (
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ handleTabChange } index={ activeTabIndex }> <Tabs
variant="soft-rounded"
colorScheme="blue"
isLazy
onChange={ handleTabChange }
index={ activeTabIndex }
position="relative"
>
<TabList <TabList
marginBottom={ tabListMarginBottom } marginBottom={{ base: 6, lg: 8 }}
flexWrap="nowrap" flexWrap="nowrap"
whiteSpace="nowrap" whiteSpace="nowrap"
ref={ listRef } ref={ listRef }
...@@ -77,6 +94,18 @@ const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => { ...@@ -77,6 +94,18 @@ const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => {
'-ms-overflow-style': 'none', /* IE and Edge */ '-ms-overflow-style': 'none', /* IE and Edge */
'scrollbar-width': 'none', /* Firefox */ 'scrollbar-width': 'none', /* Firefox */
}} }}
bgColor={ listBgColor }
transitionProperty="top,box-shadow,background-color,color"
transitionDuration="slow"
{
...(stickyEnabled ? {
position: 'sticky',
boxShadow: { base: isSticky ? 'md' : 'none', lg: 'none' },
top: { base: scrollDirection === 'down' ? `0px` : `106px`, lg: 0 },
zIndex: { base: 'sticky2', lg: 'docked' },
} : { })
}
{ ...tabListProps }
> >
{ tabsList.map((tab, index) => { { tabsList.map((tab, index) => {
if (!tab.id) { if (!tab.id) {
...@@ -111,6 +140,7 @@ const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => { ...@@ -111,6 +140,7 @@ const RoutedTabs = ({ tabs, tabListMarginBottom }: Props) => {
</Tab> </Tab>
); );
}) } }) }
{ rightSlot ? <Box ref={ rightSlotRef } ml="auto" > { rightSlot } </Box> : null }
</TabList> </TabList>
<TabPanels> <TabPanels>
{ tabsList.map((tab) => <TabPanel padding={ 0 } key={ tab.id }>{ tab.component }</TabPanel>) } { tabsList.map((tab) => <TabPanel padding={ 0 } key={ tab.id }>{ tab.component }</TabPanel>) }
......
...@@ -11,9 +11,11 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole ...@@ -11,9 +11,11 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole
const [ tabsCut, setTabsCut ] = React.useState(disabled ? tabs.length : 0); const [ tabsCut, setTabsCut ] = React.useState(disabled ? tabs.length : 0);
const [ tabsRefs, setTabsRefs ] = React.useState<Array<React.RefObject<HTMLButtonElement>>>([]); const [ tabsRefs, setTabsRefs ] = React.useState<Array<React.RefObject<HTMLButtonElement>>>([]);
const listRef = React.useRef<HTMLDivElement>(null); const listRef = React.useRef<HTMLDivElement>(null);
const rightSlotRef = React.useRef<HTMLDivElement>(null);
const calculateCut = React.useCallback(() => { const calculateCut = React.useCallback(() => {
const listWidth = listRef.current?.getBoundingClientRect().width; const listWidth = listRef.current?.getBoundingClientRect().width;
const rightSlotWidth = rightSlotRef.current?.getBoundingClientRect().width || 0;
const tabWidths = tabsRefs.map((tab) => tab.current?.getBoundingClientRect().width); const tabWidths = tabsRefs.map((tab) => tab.current?.getBoundingClientRect().width);
const menuWidth = tabWidths[tabWidths.length - 1]; const menuWidth = tabWidths[tabWidths.length - 1];
...@@ -26,11 +28,11 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole ...@@ -26,11 +28,11 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole
return result; return result;
} }
if (result.accWidth + item <= listWidth - menuWidth) { if (result.accWidth + item <= listWidth - rightSlotWidth - menuWidth) {
return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item }; return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item };
} }
if (result.accWidth + item <= listWidth && index === tabWidths.length - 2) { if (result.accWidth + item <= listWidth - rightSlotWidth && index === tabWidths.length - 2) {
return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item }; return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item };
} }
...@@ -82,6 +84,7 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole ...@@ -82,6 +84,7 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab>, disabled?: boole
tabsList, tabsList,
tabsRefs, tabsRefs,
listRef, listRef,
rightSlotRef,
}; };
}, [ tabsList, tabsCut, tabsRefs, listRef ]); }, [ tabsList, tabsCut, tabsRefs, listRef ]);
} }
import { HStack, Box, Flex, useColorModeValue } from '@chakra-ui/react'; import { HStack, Box, Flex, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import ScrollDirectionContext from 'ui/ScrollDirectionContext'; import { useScrollDirection } from 'lib/contexts/scrollDirection';
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';
...@@ -17,56 +17,53 @@ type Props = { ...@@ -17,56 +17,53 @@ type Props = {
const Header = ({ hideOnScrollDown, isHomePage }: Props) => { const Header = ({ hideOnScrollDown, isHomePage }: Props) => {
const bgColor = useColorModeValue('white', 'black'); const bgColor = useColorModeValue('white', 'black');
const scrollDirection = useScrollDirection();
return ( return (
<ScrollDirectionContext.Consumer> <>
{ (scrollDirection) => ( <Box bgColor={ bgColor } display={{ base: 'block', lg: 'none' }}>
<> <Flex
<Box bgColor={ bgColor } display={{ base: 'block', lg: 'none' }}> as="header"
<Flex position="fixed"
as="header" top={ 0 }
position="fixed" left={ 0 }
top={ 0 } paddingX={ 4 }
left={ 0 } paddingY={ 2 }
paddingX={ 4 } bgColor={ bgColor }
paddingY={ 2 } width="100%"
bgColor={ bgColor } alignItems="center"
width="100%" justifyContent="space-between"
alignItems="center" zIndex="sticky2"
justifyContent="space-between" transitionProperty="box-shadow"
zIndex="sticky2" transitionDuration="slow"
transitionProperty="box-shadow" boxShadow={ !hideOnScrollDown && scrollDirection === 'down' ? 'md' : 'none' }
transitionDuration="slow" >
boxShadow={ !hideOnScrollDown && scrollDirection === 'down' ? 'md' : 'none' } <Burger/>
> <NetworkLogo/>
<Burger/> <ProfileMenuMobile/>
<NetworkLogo/> </Flex>
<ProfileMenuMobile/> { !isHomePage && <SearchBar withShadow={ !hideOnScrollDown }/> }
</Flex> </Box>
{ !isHomePage && <SearchBar withShadow={ !hideOnScrollDown }/> } { !isHomePage && (
<HStack
as="header"
width="100%"
alignItems="center"
justifyContent="center"
gap={ 12 }
display={{ base: 'none', lg: 'flex' }}
paddingX={ 12 }
paddingTop={ 9 }
paddingBottom="52px"
>
<Box width="100%">
<SearchBar/>
</Box> </Box>
{ !isHomePage && ( <ColorModeToggler/>
<HStack <ProfileMenuDesktop/>
as="header" </HStack>
width="100%"
alignItems="center"
justifyContent="center"
gap={ 12 }
display={{ base: 'none', lg: 'flex' }}
paddingX={ 12 }
paddingTop={ 9 }
paddingBottom="52px"
>
<Box width="100%">
<SearchBar/>
</Box>
<ColorModeToggler/>
<ProfileMenuDesktop/>
</HStack>
) }
</>
) } ) }
</ScrollDirectionContext.Consumer> </>
); );
}; };
......
...@@ -4,7 +4,7 @@ import React from 'react'; ...@@ -4,7 +4,7 @@ import React from 'react';
import type { ChangeEvent, FormEvent } from 'react'; import type { ChangeEvent, FormEvent } from 'react';
import searchIcon from 'icons/search.svg'; import searchIcon from 'icons/search.svg';
import ScrollDirectionContext from 'ui/ScrollDirectionContext'; import { useScrollDirection } from 'lib/contexts/scrollDirection';
const TOP = 55; const TOP = 55;
...@@ -17,6 +17,7 @@ interface Props { ...@@ -17,6 +17,7 @@ interface Props {
const SearchBarMobile = ({ onChange, onSubmit, withShadow }: Props) => { const SearchBarMobile = ({ onChange, onSubmit, withShadow }: Props) => {
const [ isSticky, setIsSticky ] = React.useState(false); const [ isSticky, setIsSticky ] = React.useState(false);
const scrollDirection = useScrollDirection();
const handleScroll = React.useCallback(() => { const handleScroll = React.useCallback(() => {
if (window.pageYOffset !== 0) { if (window.pageYOffset !== 0) {
...@@ -43,41 +44,37 @@ const SearchBarMobile = ({ onChange, onSubmit, withShadow }: Props) => { ...@@ -43,41 +44,37 @@ const SearchBarMobile = ({ onChange, onSubmit, withShadow }: Props) => {
const bgColor = useColorModeValue('white', 'black'); const bgColor = useColorModeValue('white', 'black');
return ( return (
<ScrollDirectionContext.Consumer> <chakra.form
{ (scrollDirection) => ( noValidate
<chakra.form onSubmit={ onSubmit }
noValidate paddingX={ 4 }
onSubmit={ onSubmit } paddingTop={ 1 }
paddingX={ 4 } paddingBottom={ 2 }
paddingTop={ 1 } position="fixed"
paddingBottom={ 2 } top={ `${ TOP }px` }
position="fixed" left="0"
top={ `${ TOP }px` } zIndex="sticky1"
left="0" bgColor={ bgColor }
zIndex="sticky1" transform={ scrollDirection !== 'down' ? 'translateY(0)' : 'translateY(-100%)' }
bgColor={ bgColor } transitionProperty="transform,box-shadow"
transform={ scrollDirection !== 'down' ? 'translateY(0)' : 'translateY(-100%)' } transitionDuration="slow"
transitionProperty="transform,box-shadow" display={{ base: 'block', lg: 'none' }}
transitionDuration="slow" w="100%"
display={{ base: 'block', lg: 'none' }} boxShadow={ withShadow && scrollDirection !== 'down' && isSticky ? 'md' : 'none' }
w="100%" >
boxShadow={ withShadow && scrollDirection !== 'down' && isSticky ? 'md' : 'none' } <InputGroup size="sm">
> <InputLeftElement >
<InputGroup size="sm"> <Icon as={ searchIcon } boxSize={ 4 } color={ searchIconColor }/>
<InputLeftElement > </InputLeftElement>
<Icon as={ searchIcon } boxSize={ 4 } color={ searchIconColor }/> <Input
</InputLeftElement> paddingInlineStart="38px"
<Input placeholder="Search by addresses / ... "
paddingInlineStart="38px" ml="1px"
placeholder="Search by addresses / ... " onChange={ onChange }
ml="1px" borderColor={ inputBorderColor }
onChange={ onChange } />
borderColor={ inputBorderColor } </InputGroup>
/> </chakra.form>
</InputGroup>
</chakra.form>
) }
</ScrollDirectionContext.Consumer>
); );
}; };
......
import { Text, Box, Show, Hide } from '@chakra-ui/react'; import { Text, Box, Show, Hide } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { TTxsFilters } from 'types/api/txsFilters'; import type { TxsResponse } from 'types/api/transaction';
import type { QueryKeys } from 'types/client/queries';
import useQueryWithPages from 'lib/hooks/useQueryWithPages'; 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 TxsHeader from './TxsHeader'; 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 TxsSkeletonDesktop from './TxsSkeletonDesktop';
...@@ -15,30 +16,20 @@ import TxsSkeletonMobile from './TxsSkeletonMobile'; ...@@ -15,30 +16,20 @@ import TxsSkeletonMobile from './TxsSkeletonMobile';
import TxsTable from './TxsTable'; import TxsTable from './TxsTable';
import useTxsSort from './useTxsSort'; import useTxsSort from './useTxsSort';
type QueryResult = UseQueryResult<TxsResponse> & {
pagination: PaginationProps;
};
type Props = { type Props = {
queryName: QueryKeys.txsPending | QueryKeys.txsValidate | QueryKeys.blockTxs; query: QueryResult;
stateFilter?: TTxsFilters['filter'];
apiPath: string;
showBlockInfo?: boolean; showBlockInfo?: boolean;
showSocketInfo?: boolean;
} }
const TxsContent = ({ const TxsContent = ({ query, showBlockInfo = true, showSocketInfo = true }: Props) => {
queryName, const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query);
stateFilter, const isPaginatorHidden = !isLoading && !isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
apiPath, const isMobile = useIsMobile();
showBlockInfo = true,
}: Props) => {
const {
pagination,
...queryResult
} = useQueryWithPages({
apiPath,
queryName,
filters: stateFilter ? { filter: stateFilter } : undefined,
});
// } = useQueryWithPages({ ...filters, filter: stateFilter, apiPath });
const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(queryResult);
const isPaginatorHidden = !isLoading && !isError && pagination.page === 1 && !pagination.hasNextPage;
const content = (() => { const content = (() => {
if (isError) { if (isError) {
...@@ -64,14 +55,23 @@ const TxsContent = ({ ...@@ -64,14 +55,23 @@ const TxsContent = ({
<> <>
<Show below="lg" ssr={ false }> <Show below="lg" ssr={ false }>
<Box> <Box>
<TxsNewItemNotice> { showSocketInfo && (
{ ({ content }) => <Box>{ content }</Box> } <TxsNewItemNotice url={ window.location.href }>
</TxsNewItemNotice> { ({ content }) => <Box>{ content }</Box> }
</TxsNewItemNotice>
) }
{ txs.map(tx => <TxsListItem tx={ tx } key={ tx.hash } showBlockInfo={ showBlockInfo }/>) } { txs.map(tx => <TxsListItem tx={ tx } key={ tx.hash } showBlockInfo={ showBlockInfo }/>) }
</Box> </Box>
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
<TxsTable txs={ txs } sort={ setSortByField } sorting={ sorting } showBlockInfo={ showBlockInfo } top={ isPaginatorHidden ? 0 : 80 }/> <TxsTable
txs={ txs }
sort={ setSortByField }
sorting={ sorting }
showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo }
top={ isPaginatorHidden ? 0 : 80 }
/>
</Hide> </Hide>
</> </>
); );
...@@ -79,7 +79,15 @@ const TxsContent = ({ ...@@ -79,7 +79,15 @@ const TxsContent = ({
return ( return (
<> <>
<TxsHeader mt={ -6 } sorting={ sorting } setSorting={ setSortByValue } paginationProps={ pagination } showPagination={ !isPaginatorHidden }/> { isMobile && (
<TxsHeaderMobile
mt={ -6 }
sorting={ sorting }
setSorting={ setSortByValue }
paginationProps={ query.pagination }
showPagination={ !isPaginatorHidden }
/>
) }
{ content } { content }
</> </>
); );
......
...@@ -3,7 +3,6 @@ import React from 'react'; ...@@ -3,7 +3,6 @@ import React from 'react';
import type { Sort } from 'types/client/txs-sort'; import type { Sort } from 'types/client/txs-sort';
import useIsMobile from 'lib/hooks/useIsMobile';
// import FilterInput from 'ui/shared/FilterInput'; // import FilterInput from 'ui/shared/FilterInput';
import ActionBar from 'ui/shared/ActionBar'; import ActionBar from 'ui/shared/ActionBar';
import Pagination from 'ui/shared/Pagination'; import Pagination from 'ui/shared/Pagination';
...@@ -20,13 +19,7 @@ type Props = { ...@@ -20,13 +19,7 @@ type Props = {
showPagination?: boolean; showPagination?: boolean;
} }
const TxsHeader = ({ sorting, setSorting, paginationProps, className, showPagination = true }: Props) => { const TxsHeaderMobile = ({ sorting, setSorting, paginationProps, className, showPagination = true }: Props) => {
const isMobile = useIsMobile(false);
if (!showPagination && !isMobile) {
return null;
}
return ( return (
<ActionBar className={ className }> <ActionBar className={ className }>
<HStack> <HStack>
...@@ -36,13 +29,11 @@ const TxsHeader = ({ sorting, setSorting, paginationProps, className, showPagina ...@@ -36,13 +29,11 @@ const TxsHeader = ({ sorting, setSorting, paginationProps, className, showPagina
onFiltersChange={ setFilters } onFiltersChange={ setFilters }
appliedFiltersNum={ 0 } appliedFiltersNum={ 0 }
/> */ } /> */ }
{ isMobile && ( <TxsSorting
<TxsSorting isActive={ Boolean(sorting) }
isActive={ Boolean(sorting) } setSorting={ setSorting }
setSorting={ setSorting } sorting={ sorting }
sorting={ sorting } />
/>
) }
{ /* api is not implemented */ } { /* api is not implemented */ }
{ /* <FilterInput { /* <FilterInput
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
...@@ -57,4 +48,4 @@ const TxsHeader = ({ sorting, setSorting, paginationProps, className, showPagina ...@@ -57,4 +48,4 @@ const TxsHeader = ({ sorting, setSorting, paginationProps, className, showPagina
); );
}; };
export default chakra(TxsHeader); export default chakra(TxsHeaderMobile);
...@@ -23,7 +23,7 @@ test.describe.configure({ mode: 'serial' }); ...@@ -23,7 +23,7 @@ test.describe.configure({ mode: 'serial' });
test('new item in validated txs list', async({ mount, createSocket }) => { test('new item in validated txs list', async({ mount, createSocket }) => {
const component = await mount( const component = await mount(
<TestApp withSocket> <TestApp withSocket>
<TxsNewItemNotice/> <TxsNewItemNotice url="/"/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -35,10 +35,29 @@ test('new item in validated txs list', async({ mount, createSocket }) => { ...@@ -35,10 +35,29 @@ test('new item in validated txs list', async({ mount, createSocket }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test.describe('dark mode', () => {
test.use({ colorScheme: 'dark' });
test('default view', async({ mount, createSocket }) => {
const component = await mount(
<TestApp withSocket>
<TxsNewItemNotice url="/"/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'transactions:new_transaction');
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
await expect(component).toHaveScreenshot();
});
});
test('2 new items in validated txs list', async({ mount, page, createSocket }) => { test('2 new items in validated txs list', async({ mount, page, createSocket }) => {
const component = await mount( const component = await mount(
<TestApp withSocket> <TestApp withSocket>
<TxsNewItemNotice/> <TxsNewItemNotice url="/"/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -56,7 +75,7 @@ test('2 new items in validated txs list', async({ mount, page, createSocket }) = ...@@ -56,7 +75,7 @@ test('2 new items in validated txs list', async({ mount, page, createSocket }) =
test('connection loss', async({ mount, createSocket }) => { test('connection loss', async({ mount, createSocket }) => {
const component = await mount( const component = await mount(
<TestApp withSocket> <TestApp withSocket>
<TxsNewItemNotice/> <TxsNewItemNotice url="/"/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
...@@ -71,7 +90,7 @@ test('connection loss', async({ mount, createSocket }) => { ...@@ -71,7 +90,7 @@ test('connection loss', async({ mount, createSocket }) => {
test('fetching', async({ mount, createSocket }) => { test('fetching', async({ mount, createSocket }) => {
const component = await mount( const component = await mount(
<TestApp withSocket> <TestApp withSocket>
<TxsNewItemNotice/> <TxsNewItemNotice url="/"/>
</TestApp>, </TestApp>,
{ hooksConfig }, { hooksConfig },
); );
......
import { Alert, Link, Text, chakra } from '@chakra-ui/react'; import { Alert, Link, Text, chakra, useTheme, useColorModeValue } from '@chakra-ui/react';
import { transparentize } from '@chakra-ui/theme-tools';
import React from 'react'; import React from 'react';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket'; import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
...@@ -10,47 +11,45 @@ interface InjectedProps { ...@@ -10,47 +11,45 @@ interface InjectedProps {
interface Props { interface Props {
children?: (props: InjectedProps) => JSX.Element; children?: (props: InjectedProps) => JSX.Element;
className?: string; className?: string;
url: string;
} }
const TxsNewItemNotice = ({ children, className }: Props) => { const TxsNewItemNotice = ({ children, className, url }: Props) => {
const { num, socketAlert } = useNewTxsSocket(); const { num, socketAlert } = useNewTxsSocket();
const theme = useTheme();
const handleClick = React.useCallback(() => { const alertContent = (() => {
window.location.reload();
}, []);
const content = (() => {
if (socketAlert) { if (socketAlert) {
return ( return socketAlert;
<Alert
className={ className }
status="warning"
p={ 4 }
borderRadius={ 0 }
onClick={ handleClick }
cursor="pointer"
>
{ socketAlert }
</Alert>
);
} }
if (!num) { if (!num) {
return ( return 'scanning new transactions...';
<Alert className={ className } status="warning" p={ 4 } fontWeight={ 400 }>
scanning new transactions...
</Alert>
);
} }
return ( return (
<Alert className={ className } status="warning" p={ 4 } fontWeight={ 400 }> <>
<Link onClick={ handleClick }>{ num } more transaction{ num > 1 ? 's' : '' }</Link> <Link href={ url }>{ num } more transaction{ num > 1 ? 's' : '' }</Link>
<Text whiteSpace="pre"> ha{ num > 1 ? 've' : 's' } come in</Text> <Text whiteSpace="pre"> ha{ num > 1 ? 've' : 's' } come in</Text>
</Alert> </>
); );
})(); })();
const content = (
<Alert
className={ className }
status="warning"
px={ 4 }
py="6px"
fontWeight={ 400 }
fontSize="sm"
bgColor={ useColorModeValue('orange.50', transparentize('orange.200', 0.16)(theme)) }
color={ useColorModeValue('blackAlpha.800', 'whiteAlpha.800') }
>
{ alertContent }
</Alert>
);
return children ? children({ content }) : content; return children ? children({ content }) : content;
}; };
......
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import TxsContent from './TxsContent';
type Props = {
tab: 'validated' | 'pending';
}
const TxsTab = ({ tab }: Props) => {
if (tab === 'validated') {
return (
<TxsContent
queryName={ QueryKeys.txsValidate }
stateFilter="validated"
apiPath="/node-api/transactions"
/>
);
}
return (
<TxsContent
queryName={ QueryKeys.txsPending }
stateFilter="pending"
apiPath="/node-api/transactions"
showBlockInfo={ false }
/>
);
};
export default TxsTab;
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
import type { Props as PaginationProps } from 'ui/shared/Pagination';
import Pagination from 'ui/shared/Pagination';
interface Props {
pagination: PaginationProps;
}
const TxsTabSlot = ({ pagination }: Props) => {
const isMobile = useIsMobile();
if (isMobile) {
return null;
}
return <Pagination my={ 1 } { ...pagination }/>;
};
export default TxsTabSlot;
...@@ -17,9 +17,10 @@ type Props = { ...@@ -17,9 +17,10 @@ type Props = {
sorting?: Sort; sorting?: Sort;
top: number; top: number;
showBlockInfo: boolean; showBlockInfo: boolean;
showSocketInfo: boolean;
} }
const TxsTable = ({ txs, sort, sorting, top, showBlockInfo }: Props) => { const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo }: Props) => {
return ( return (
<Table variant="simple" minWidth="950px" size="xs"> <Table variant="simple" minWidth="950px" size="xs">
<TheadSticky top={ top }> <TheadSticky top={ top }>
...@@ -49,9 +50,11 @@ const TxsTable = ({ txs, sort, sorting, top, showBlockInfo }: Props) => { ...@@ -49,9 +50,11 @@ const TxsTable = ({ txs, sort, sorting, top, showBlockInfo }: Props) => {
</Tr> </Tr>
</TheadSticky> </TheadSticky>
<Tbody> <Tbody>
<TxsNewItemNotice borderRadius={ 0 }> { showSocketInfo && (
{ ({ content }) => <Tr><Td colSpan={ 10 } p={ 0 }>{ content }</Td></Tr> } <TxsNewItemNotice borderRadius={ 0 } url={ window.location.href }>
</TxsNewItemNotice> { ({ content }) => <Tr><Td colSpan={ 10 } p={ 0 }>{ content }</Td></Tr> }
</TxsNewItemNotice>
) }
{ txs.map((item) => ( { txs.map((item) => (
<TxsTableItem <TxsTableItem
key={ item.hash } key={ item.hash }
......
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import type { BlockTransactionsResponse } from 'types/api/block'; import type { TxsResponse } from 'types/api/transaction';
import type { TransactionsResponsePending, TransactionsResponseValidated } from 'types/api/transaction';
import type { Sort } from 'types/client/txs-sort'; import type { Sort } from 'types/client/txs-sort';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import sortTxs from 'lib/tx/sortTxs'; import sortTxs from 'lib/tx/sortTxs';
type TxsResponse = TransactionsResponseValidated | TransactionsResponsePending | BlockTransactionsResponse;
type HookResult = UseQueryResult<TxsResponse> & { type HookResult = UseQueryResult<TxsResponse> & {
sorting: Sort; sorting: Sort;
setSortByField: (field: 'val' | 'fee') => () => void; setSortByField: (field: 'val' | 'fee') => () => void;
......
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