Commit 036dcde5 authored by tom's avatar tom

network menu and rewrite prev implementation

parent 3d0f21f8
...@@ -20,16 +20,16 @@ export default function useNavItems() { ...@@ -20,16 +20,16 @@ export default function useNavItems() {
return React.useMemo(() => { return React.useMemo(() => {
const mainNavItems = [ const mainNavItems = [
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: [ 'block', 'blocks' ].includes(currentRoute) }, { text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute === 'blocks' },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: [ 'tx', 'txs' ].includes(currentRoute) }, { text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx') },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: [ 'token', 'tokens' ].includes(currentRoute) }, { text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens' },
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps' }, { text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps' },
{ text: 'Other', url: link('other'), icon: gearIcon, isActive: currentRoute === 'other' }, { text: 'Other', url: link('other'), icon: gearIcon, isActive: currentRoute === 'other' },
]; ];
const accountNavItems = [ const accountNavItems = [
{ text: 'Watchlist', url: link('watchlist'), icon: watchlistIcon, isActive: currentRoute === 'watchlist' }, { text: 'Watchlist', url: link('watchlist'), icon: watchlistIcon, isActive: currentRoute === 'watchlist' },
{ text: 'Private tags', url: link('private_tags', { tab: 'address' }), icon: privateTagIcon, isActive: currentRoute === 'private_tags' }, { text: 'Private tags', url: link('private_tags_address'), icon: privateTagIcon, isActive: currentRoute.startsWith('private_tags') },
{ text: 'Public tags', url: link('public_tags'), icon: publicTagIcon, isActive: currentRoute === 'public_tags' }, { text: 'Public tags', url: link('public_tags'), icon: publicTagIcon, isActive: currentRoute === 'public_tags' },
{ text: 'API keys', url: link('api_keys'), icon: apiKeysIcon, isActive: currentRoute === 'api_keys' }, { text: 'API keys', url: link('api_keys'), icon: apiKeysIcon, isActive: currentRoute === 'api_keys' },
{ text: 'Custom ABI', url: link('custom_abi'), icon: abiIcon, isActive: currentRoute === 'custom_abi' }, { text: 'Custom ABI', url: link('custom_abi'), icon: abiIcon, isActive: currentRoute === 'custom_abi' },
......
export const ACCOUNT_ROUTES: Array<RouteName> = [ 'watchlist', 'private_tags_address', 'private_tags_tx', 'public_tags', 'api_keys', 'custom_abi' ];
import type { RouteName } from 'lib/link/routes';
export default function isAccountRoute(route: RouteName) {
return ACCOUNT_ROUTES.includes(route);
}
import { compile } from 'path-to-regexp';
import { ROUTES } from './routes'; import { ROUTES } from './routes';
import type { RouteName } from './routes'; import type { RouteName } from './routes';
const PATH_PARAM_REGEXP = /\/\[(\w+)\]/g;
export function link(routeName: RouteName, urlParams?: Record<string, string | undefined>, queryParams?: Record<string, string>): string { export function link(routeName: RouteName, urlParams?: Record<string, string | undefined>, queryParams?: Record<string, string>): string {
const route = ROUTES[routeName]; const route = ROUTES[routeName];
if (!route) { if (!route) {
return ''; return '';
} }
const toPath = compile(route.pattern, { encode: encodeURIComponent, validate: false }); const path = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => {
const paramValue = urlParams?.[paramName];
return paramValue ? `/${ paramValue }` : '';
});
const path = toPath(urlParams);
const url = new URL(path, window.location.origin); const url = new URL(path, window.location.origin);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => { queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
......
export interface Route { export interface Route {
pattern: string; pattern: string;
crossNetworkNavigation?: boolean; // route will not change when switching networks
} }
export type RouteName = keyof typeof ROUTES; export type RouteName = keyof typeof ROUTES;
const BASE_PATH = '/:network_type/:network_sub_type'; const BASE_PATH = '/[network_type]/[network_sub_type]';
export const ROUTES = { export const ROUTES = {
tx: { // NETWORK MAIN PAGE
pattern: `${ BASE_PATH }/tx/:id/:tab?`, network_index: {
}, pattern: `${ BASE_PATH }`,
txs: { crossNetworkNavigation: true,
pattern: `${ BASE_PATH }/txs`,
},
blocks: {
pattern: `${ BASE_PATH }/blocks`,
},
tokens: {
pattern: `${ BASE_PATH }/tokens`,
},
apps: {
pattern: `${ BASE_PATH }/apps`,
},
// ??? what URL will be here
other: {
pattern: `${ BASE_PATH }/other`,
}, },
// ACCOUNT
watchlist: { watchlist: {
pattern: `${ BASE_PATH }/account/watchlist`, pattern: `${ BASE_PATH }/account/watchlist`,
crossNetworkNavigation: true,
}, },
private_tags: { private_tags_address: {
pattern: `${ BASE_PATH }/account/tag_{:tab}`, pattern: `${ BASE_PATH }/account/tag_address`,
crossNetworkNavigation: true,
},
private_tags_tx: {
pattern: `${ BASE_PATH }/account/tag_transaction`,
crossNetworkNavigation: true,
}, },
public_tags: { public_tags: {
pattern: `${ BASE_PATH }/account/public_tags_request`, pattern: `${ BASE_PATH }/account/public_tags_request`,
crossNetworkNavigation: true,
}, },
api_keys: { api_keys: {
pattern: `${ BASE_PATH }/account/api_key`, pattern: `${ BASE_PATH }/account/api_key`,
crossNetworkNavigation: true,
}, },
custom_abi: { custom_abi: {
pattern: `${ BASE_PATH }/account/custom_abi`, pattern: `${ BASE_PATH }/account/custom_abi`,
crossNetworkNavigation: true,
}, },
profile: { profile: {
pattern: `${ BASE_PATH }/auth/profile`, pattern: `${ BASE_PATH }/auth/profile`,
crossNetworkNavigation: true,
},
// TRANSACTIONS
txs: {
pattern: `${ BASE_PATH }/txs`,
crossNetworkNavigation: true,
},
tx_index: {
pattern: `${ BASE_PATH }/tx/[id]`,
},
tx_internal: {
pattern: `${ BASE_PATH }/tx/[id]/internal-transactions`,
},
tx_logs: {
pattern: `${ BASE_PATH }/tx/[id]/logs`,
},
tx_raw_trace: {
pattern: `${ BASE_PATH }/tx/[id]/raw-trace`,
},
tx_state: {
pattern: `${ BASE_PATH }/tx/[id]/state`,
},
// BLOCKS
blocks: {
pattern: `${ BASE_PATH }/blocks`,
crossNetworkNavigation: true,
},
// TOKENS
tokens: {
pattern: `${ BASE_PATH }/tokens`,
crossNetworkNavigation: true,
},
// APPS
apps: {
pattern: `${ BASE_PATH }/apps`,
},
// ??? what URL will be here
other: {
pattern: `${ BASE_PATH }/other`,
}, },
}; };
......
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { match } from 'path-to-regexp';
import React from 'react'; import React from 'react';
import type { RouteName } from 'lib/link/routes'; import type { RouteName } from 'lib/link/routes';
import { ROUTES } from 'lib/link/routes'; import { ROUTES } from 'lib/link/routes';
export default function useCurrentRoute() { export default function useCurrentRoute() {
const { asPath } = useRouter(); const { route: nextRoute } = useRouter();
return React.useCallback(() => { return React.useCallback((): RouteName => {
for (const routeName in ROUTES) { for (const routeName in ROUTES) {
const route = ROUTES[routeName as RouteName]; const route = ROUTES[routeName as RouteName];
const isMatch = Boolean(match(route.pattern)(asPath)); if (route.pattern === nextRoute) {
if (isMatch) {
return routeName as RouteName; return routeName as RouteName;
} }
} }
return ''; return 'network_index';
}, [ asPath ]); }, [ nextRoute ]);
} }
export const ACCOUNT_ROUTES = [ '/watchlist', '/tag_address', '/tag_transaction', '/public_tags_request', '/api_key', '/custom_abi' ];
export default function isAccountRoute(route: string) {
return ACCOUNT_ROUTES.includes(route);
}
import { useRouter } from 'next/router';
import React from 'react';
import useNetwork from 'lib/hooks/useNetwork';
import isAccountRoute from 'lib/link/isAccountRoute';
import { link } from 'lib/link/link';
import { ROUTES } from 'lib/link/routes';
import useCurrentRoute from 'lib/link/useCurrentRoute';
import NETWORKS from 'lib/networks/availableNetworks';
export default function useNetworkNavigationItems() {
const selectedNetwork = useNetwork();
const currentRouteName = useCurrentRoute()();
const currentRoute = ROUTES[currentRouteName];
const router = useRouter();
const isAccount = isAccountRoute(currentRouteName);
return React.useMemo(() => {
return NETWORKS.map((network) => {
const routeName = (() => {
if ('crossNetworkNavigation' in currentRoute && currentRoute.crossNetworkNavigation) {
if ((isAccount && network.isAccountSupported) || !isAccount) {
return currentRouteName;
}
}
return 'network_index';
})();
const url = link(routeName, { ...router.query, network_type: network.type, network_sub_type: network.subType });
return {
...network,
url: url,
isActive: selectedNetwork?.type === network.type && selectedNetwork?.subType === network?.subType,
};
});
}, [ currentRoute, currentRouteName, isAccount, router.query, selectedNetwork?.subType, selectedNetwork?.type ]);
}
import { PopoverContent, PopoverBody, Text, Tabs, TabList, TabPanels, TabPanel, Tab, VStack } from '@chakra-ui/react'; import { PopoverContent, PopoverBody, Text, Tabs, TabList, TabPanels, TabPanel, Tab, VStack } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { NetworkGroup } from 'types/networks'; import type { NetworkGroup } from 'types/networks';
import useNetwork from 'lib/hooks/useNetwork'; import useNetwork from 'lib/hooks/useNetwork';
import NETWORKS from 'lib/networks/availableNetworks'; import NETWORKS from 'lib/networks/availableNetworks';
import useNetworkNavigationItems from 'lib/networks/useNetworkNavigationItems';
import NetworkMenuLink from './NetworkMenuLink'; import NetworkMenuLink from './NetworkMenuLink';
...@@ -13,9 +13,8 @@ const TABS: Array<NetworkGroup> = [ 'mainnets', 'testnets', 'other' ]; ...@@ -13,9 +13,8 @@ const TABS: Array<NetworkGroup> = [ 'mainnets', 'testnets', 'other' ];
const availableTabs = TABS.filter((tab) => NETWORKS.some(({ group }) => group === tab)); const availableTabs = TABS.filter((tab) => NETWORKS.some(({ group }) => group === tab));
const NetworkMenuPopup = () => { const NetworkMenuPopup = () => {
const router = useRouter();
const routeName = router.pathname.replace('/[network_type]/[network_sub_type]', '');
const selectedNetwork = useNetwork(); const selectedNetwork = useNetwork();
const items = useNetworkNavigationItems();
const selectedTab = availableTabs.findIndex((tab) => selectedNetwork?.group === tab); const selectedTab = availableTabs.findIndex((tab) => selectedNetwork?.group === tab);
return ( return (
...@@ -32,14 +31,12 @@ const NetworkMenuPopup = () => { ...@@ -32,14 +31,12 @@ const NetworkMenuPopup = () => {
{ availableTabs.map((tab) => ( { availableTabs.map((tab) => (
<TabPanel key={ tab } p={ 0 }> <TabPanel key={ tab } p={ 0 }>
<VStack as="ul" spacing={ 2 } alignItems="stretch" mt={ 4 }> <VStack as="ul" spacing={ 2 } alignItems="stretch" mt={ 4 }>
{ NETWORKS { items
.filter((network) => network.group === tab) .filter((network) => network.group === tab)
.map((network) => ( .map((network) => (
<NetworkMenuLink <NetworkMenuLink
key={ network.name } key={ network.name }
{ ...network } { ...network }
isActive={ network.name === selectedNetwork?.name }
routeName={ routeName }
/> />
)) } )) }
</VStack> </VStack>
......
import { Box, Select, VStack } from '@chakra-ui/react'; import { Box, Select, VStack } from '@chakra-ui/react';
import capitalize from 'lodash/capitalize'; import capitalize from 'lodash/capitalize';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { NetworkGroup } from 'types/networks'; import type { NetworkGroup } from 'types/networks';
import useNetwork from 'lib/hooks/useNetwork'; import useNetwork from 'lib/hooks/useNetwork';
import NETWORKS from 'lib/networks/availableNetworks'; import useNetworkNavigationItems from 'lib/networks/useNetworkNavigationItems';
import NetworkMenuLink from './NetworkMenuLink'; import NetworkMenuLink from './NetworkMenuLink';
const TABS: Array<NetworkGroup> = [ 'mainnets', 'testnets', 'other' ]; const TABS: Array<NetworkGroup> = [ 'mainnets', 'testnets', 'other' ];
const NetworkMenuContentMobile = () => { const NetworkMenuContentMobile = () => {
const router = useRouter();
const routeName = router.pathname.replace('/[network_type]/[network_sub_type]', '');
const selectedNetwork = useNetwork(); const selectedNetwork = useNetwork();
const [ selectedTab, setSelectedTab ] = React.useState<NetworkGroup>(TABS.find((tab) => selectedNetwork?.group === tab) || 'mainnets'); const [ selectedTab, setSelectedTab ] = React.useState<NetworkGroup>(TABS.find((tab) => selectedNetwork?.group === tab) || 'mainnets');
const items = useNetworkNavigationItems();
const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => { const handleSelectChange = React.useCallback((event: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedTab(event.target.value as NetworkGroup); setSelectedTab(event.target.value as NetworkGroup);
...@@ -28,14 +26,12 @@ const NetworkMenuContentMobile = () => { ...@@ -28,14 +26,12 @@ const NetworkMenuContentMobile = () => {
{ TABS.map((tab) => <option key={ tab } value={ tab }>{ capitalize(tab) }</option>) } { TABS.map((tab) => <option key={ tab } value={ tab }>{ capitalize(tab) }</option>) }
</Select> </Select>
<VStack as="ul" spacing={ 2 } alignItems="stretch" mt={ 6 }> <VStack as="ul" spacing={ 2 } alignItems="stretch" mt={ 6 }>
{ NETWORKS { items
.filter(({ group }) => group === selectedTab) .filter(({ group }) => group === selectedTab)
.map((network) => ( .map((network) => (
<NetworkMenuLink <NetworkMenuLink
key={ network.name } key={ network.name }
{ ...network } { ...network }
isActive={ network.name === selectedNetwork?.name }
routeName={ routeName }
isMobile isMobile
/> />
)) ))
......
...@@ -6,34 +6,36 @@ import type { Network } from 'types/networks'; ...@@ -6,34 +6,36 @@ import type { Network } from 'types/networks';
import checkIcon from 'icons/check.svg'; import checkIcon from 'icons/check.svg';
import placeholderIcon from 'icons/networks/icons/placeholder.svg'; import placeholderIcon from 'icons/networks/icons/placeholder.svg';
import isAccountRoute from 'lib/networks/isAccountRoute';
import useColors from './useColors'; import useColors from './useColors';
interface Props extends Network { interface Props extends Network {
isActive: boolean; isActive: boolean;
isMobile?: boolean; isMobile?: boolean;
routeName: string; url: string;
} }
const NetworkMenuLink = ({ name, type, subType, icon, isActive, isMobile, routeName, isAccountSupported }: Props) => { const NetworkMenuLink = ({ name, type, subType, icon, isActive, isMobile, url }: Props) => {
const isAccount = isAccountRoute(routeName); // const isAccount = isAccountRoute(routeName);
const localPath = (() => { // const localPath = (() => {
if (isAccount && isAccountSupported) { // if (isAccount && isAccountSupported) {
return routeName; // return routeName;
} // }
if (isAccount && !isAccountSupported) { // if (isAccount && !isAccountSupported) {
return ''; // return '';
} // }
// will change when blocks&transaction is implemented // // will change when blocks&transaction is implemented
return routeName; // return routeName;
})(); // })();
const pathName = `/${ type }${ subType ? '/' + subType : '' }${ localPath }`; // const pathName = `/${ type }${ subType ? '/' + subType : '' }${ localPath }`;
// const url = link(routeName, { network_type: type, network_sub_type: subType });
// console.log('__>__', url);
// will fix later after we agree on CI/CD workflow // will fix later after we agree on CI/CD workflow
const href = type === 'xdai' && subType === 'testnet' ? pathName : 'https://blockscout.com' + pathName; // const href = type === 'xdai' && subType === 'testnet' ? pathName : 'https://blockscout.com' + pathName;
const hasIcon = Boolean(icon); const hasIcon = Boolean(icon);
const colors = useColors({ hasIcon }); const colors = useColors({ hasIcon });
...@@ -49,7 +51,7 @@ const NetworkMenuLink = ({ name, type, subType, icon, isActive, isMobile, routeN ...@@ -49,7 +51,7 @@ const NetworkMenuLink = ({ name, type, subType, icon, isActive, isMobile, routeN
return ( return (
<Box as="li" listStyleType="none"> <Box as="li" listStyleType="none">
<NextLink href={ href } passHref> <NextLink href={ url } passHref>
<Flex <Flex
as="a" as="a"
px={ isMobile ? 3 : 4 } px={ isMobile ? 3 : 4 }
......
...@@ -8,6 +8,7 @@ import { ...@@ -8,6 +8,7 @@ import {
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { RouteName } from 'lib/link/routes';
import useLink from 'lib/link/useLink'; import useLink from 'lib/link/useLink';
import Page from 'ui/shared/Page'; import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader'; import PageHeader from 'ui/shared/PageHeader';
...@@ -20,14 +21,15 @@ interface Tab { ...@@ -20,14 +21,15 @@ interface Tab {
name: string; name: string;
path?: string; path?: string;
component?: React.ReactNode; component?: React.ReactNode;
routeName: RouteName;
} }
const TABS: Array<Tab> = [ const TABS: Array<Tab> = [
{ type: 'details', name: 'Details', component: <TxDetails/> }, { type: 'details', routeName: 'tx_index', name: 'Details', component: <TxDetails/> },
{ type: 'internal_txn', path: 'internal-transactions', name: 'Internal txn', component: <TxInternals/> }, { type: 'internal_txn', routeName: 'tx_internal', name: 'Internal txn', component: <TxInternals/> },
{ type: 'logs', path: 'logs', name: 'Logs', component: <TxLogs/> }, { type: 'logs', routeName: 'tx_logs', name: 'Logs', component: <TxLogs/> },
{ type: 'state', path: 'state', name: 'State' }, { type: 'state', routeName: 'tx_state', name: 'State' },
{ type: 'raw_trace', path: 'raw-trace', name: 'Raw trace' }, { type: 'raw_trace', routeName: 'tx_raw_trace', name: 'Raw trace' },
]; ];
export interface Props { export interface Props {
...@@ -42,7 +44,7 @@ const TransactionPageContent = ({ tab }: Props) => { ...@@ -42,7 +44,7 @@ const TransactionPageContent = ({ tab }: Props) => {
const handleTabChange = React.useCallback((index: number) => { const handleTabChange = React.useCallback((index: number) => {
const nextTab = TABS[index]; const nextTab = TABS[index];
setActiveTab(nextTab.type); setActiveTab(nextTab.type);
const newUrl = link('tx', { id: router.query.id as string, tab: nextTab.path }); const newUrl = link(nextTab.routeName, { id: router.query.id as string });
window.history.replaceState(history.state, '', newUrl); window.history.replaceState(history.state, '', newUrl);
}, [ setActiveTab, link, router.query.id ]); }, [ setActiveTab, link, router.query.id ]);
......
...@@ -4497,11 +4497,6 @@ path-parse@^1.0.6, path-parse@^1.0.7: ...@@ -4497,11 +4497,6 @@ path-parse@^1.0.6, path-parse@^1.0.7:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-to-regexp@^6.2.1:
version "6.2.1"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5"
integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==
path-type@^4.0.0: path-type@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
......
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