Commit 13ae1199 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into auth

parents c598bea6 9ea805eb
import { useRouter } from 'next/router';
export default function useBasePath() {
const router = useRouter();
return `/${ router.query.network_type }/${ router.query.network_sub_type }`;
}
...@@ -11,30 +11,32 @@ import publicTagIcon from 'icons/publictags.svg'; ...@@ -11,30 +11,32 @@ import publicTagIcon from 'icons/publictags.svg';
import tokensIcon from 'icons/token.svg'; import tokensIcon from 'icons/token.svg';
import transactionsIcon from 'icons/transactions.svg'; import transactionsIcon from 'icons/transactions.svg';
import watchlistIcon from 'icons/watchlist.svg'; import watchlistIcon from 'icons/watchlist.svg';
import useBasePath from 'lib/hooks/useBasePath'; import useCurrentRoute from 'lib/link/useCurrentRoute';
import useLink from 'lib/link/useLink';
export default function useNavItems() { export default function useNavItems() {
const basePath = useBasePath(); const link = useLink();
const currentRoute = useCurrentRoute()();
return React.useMemo(() => { return React.useMemo(() => {
const mainNavItems = [ const mainNavItems = [
{ text: 'Blocks', pathname: basePath + '/blocks', icon: blocksIcon }, { text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute === 'blocks' },
{ text: 'Transactions', pathname: basePath + '/tx', icon: transactionsIcon }, { text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx') },
{ text: 'Tokens', pathname: basePath + '/tokens', icon: tokensIcon }, { text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens' },
{ text: 'Apps', pathname: basePath + '/apps', icon: appsIcon }, { text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps' },
{ text: 'Other', pathname: basePath + '/other', icon: gearIcon }, { text: 'Other', url: link('other'), icon: gearIcon, isActive: currentRoute === 'other' },
]; ];
const accountNavItems = [ const accountNavItems = [
{ text: 'Watchlist', pathname: basePath + '/account/watchlist', icon: watchlistIcon }, { text: 'Watchlist', url: link('watchlist'), icon: watchlistIcon, isActive: currentRoute === 'watchlist' },
{ text: 'Private tags', pathname: basePath + '/account/tag_address', icon: privateTagIcon }, { text: 'Private tags', url: link('private_tags_address'), icon: privateTagIcon, isActive: currentRoute.startsWith('private_tags') },
{ text: 'Public tags', pathname: basePath + '/account/public_tags_request', icon: publicTagIcon }, { text: 'Public tags', url: link('public_tags'), icon: publicTagIcon, isActive: currentRoute === 'public_tags' },
{ text: 'API keys', pathname: basePath + '/account/api_key', icon: apiKeysIcon }, { text: 'API keys', url: link('api_keys'), icon: apiKeysIcon, isActive: currentRoute === 'api_keys' },
{ text: 'Custom ABI', pathname: basePath + '/account/custom_abi', icon: abiIcon }, { text: 'Custom ABI', url: link('custom_abi'), icon: abiIcon, isActive: currentRoute === 'custom_abi' },
]; ];
const profileItem = { text: 'My profile', pathname: basePath + '/auth/profile', icon: profileIcon }; const profileItem = { text: 'My profile', url: link('profile'), icon: profileIcon, isActive: currentRoute === 'profile' };
return { mainNavItems, accountNavItems, profileItem }; return { mainNavItems, accountNavItems, profileItem };
}, [ basePath ]); }, [ link, currentRoute ]);
} }
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import NETWORKS from 'lib/networks/availableNetworks'; import findNetwork from 'lib/networks/findNetwork';
export default function useNetwork() { export default function useNetwork() {
const router = useRouter(); const router = useRouter();
const selectedNetwork = NETWORKS.find((network) => router.query.network_type === network.type && router.query.network_sub_type === network.subType); const selectedNetwork = findNetwork({
network_type: typeof router.query.network_type === 'string' ? router.query.network_type : '',
network_sub_type: typeof router.query.network_type === 'string' ? router.query.network_type : undefined,
});
return selectedNetwork; return selectedNetwork;
} }
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 isBrowser from 'lib/isBrowser';
import findNetwork from 'lib/networks/findNetwork';
import { ROUTES } 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 {
const route = ROUTES[routeName];
if (!route) {
return '';
}
const network = findNetwork({
network_type: urlParams?.network_type || '',
network_sub_type: urlParams?.network_sub_type,
});
const path = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => {
if (paramName === 'network_sub_type' && !network?.subType) {
return '';
}
const paramValue = urlParams?.[paramName];
return paramValue ? `/${ paramValue }` : '';
});
const url = new URL(path, isBrowser() ? window.location.origin : 'https://blockscout.com');
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
url.searchParams.append(key, value);
});
return url.toString();
}
export interface Route {
pattern: string;
crossNetworkNavigation?: boolean; // route will not change when switching networks
}
export type RouteName = keyof typeof ROUTES;
const BASE_PATH = '/[network_type]/[network_sub_type]';
export const ROUTES = {
// NETWORK MAIN PAGE
network_index: {
pattern: `${ BASE_PATH }`,
crossNetworkNavigation: true,
},
// ACCOUNT
watchlist: {
pattern: `${ BASE_PATH }/account/watchlist`,
crossNetworkNavigation: true,
},
private_tags_address: {
pattern: `${ BASE_PATH }/account/tag_address`,
crossNetworkNavigation: true,
},
private_tags_tx: {
pattern: `${ BASE_PATH }/account/tag_transaction`,
crossNetworkNavigation: true,
},
public_tags: {
pattern: `${ BASE_PATH }/account/public_tags_request`,
crossNetworkNavigation: true,
},
api_keys: {
pattern: `${ BASE_PATH }/account/api_key`,
crossNetworkNavigation: true,
},
custom_abi: {
pattern: `${ BASE_PATH }/account/custom_abi`,
crossNetworkNavigation: true,
},
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,
},
token_index: {
pattern: `${ BASE_PATH }/token/[id]`,
crossNetworkNavigation: true,
},
// ADDRESSES
address_index: {
pattern: `${ BASE_PATH }/address/[id]`,
crossNetworkNavigation: true,
},
// APPS
apps: {
pattern: `${ BASE_PATH }/apps`,
},
// SEARCH
search_results: {
pattern: `${ BASE_PATH }/apps`,
},
// ??? what URL will be here
other: {
pattern: `${ BASE_PATH }/search-results`,
},
};
// !!! for development purpose only !!!
// don't wanna strict ROUTES to type "Record<string, Route>"
// otherwise we lose benefit of using "keyof typeof ROUTES" for possible route names (it will be any string then)
// but we still want typescript to tell us if routes follow its interface
// so we do this simple type-checking here
//
// another option is to create common enum with all possible route names and use it across the project
// but it is a little bit overwhelming as it seems right now
function checkRoutes(route: Record<string, Route>) {
return route;
}
if (process.env.NODE_ENV === 'development') {
checkRoutes(ROUTES);
}
import { useRouter } from 'next/router';
import React from 'react';
import type { RouteName } from 'lib/link/routes';
import { ROUTES } from 'lib/link/routes';
export default function useCurrentRoute() {
const { route: nextRoute } = useRouter();
return React.useCallback((): RouteName => {
for (const routeName in ROUTES) {
const route = ROUTES[routeName as RouteName];
if (route.pattern === nextRoute) {
return routeName as RouteName;
}
}
return 'network_index';
}, [ nextRoute ]);
}
import { useRouter } from 'next/router';
import React from 'react';
import { link } from 'lib/link/link';
type LinkBuilderParams = Parameters<typeof link>;
export default function useLink() {
const router = useRouter();
const networkType = router.query.network_type;
const networkSubType = router.query.network_sub_type;
return React.useCallback((...args: LinkBuilderParams) => {
if (typeof networkType !== 'string' || typeof networkSubType !== 'string') {
return '';
}
return link(args[0], { network_type: networkType, network_sub_type: networkSubType, ...args[1] }, args[2]);
}, [ networkType, networkSubType ]);
}
import availableNetworks from 'lib/networks/availableNetworks';
interface Params {
network_type: string;
network_sub_type?: string;
}
export default function findNetwork(params: Params) {
return availableNetworks.find((network) =>
network.type === params.network_type &&
network.subType ? network.subType === params.network_sub_type : network.type === params.network_type,
);
}
import NETWORKS from './availableNetworks'; import NETWORKS from './availableNetworks';
export default function getAvailablePaths() { export default function getAvailablePaths() {
return NETWORKS.map(({ type, subType }) => ({ params: { network_type: type, network_sub_type: subType } })); return NETWORKS.map(({ type, subType }) => ({ params: { network_type: type, network_sub_type: subType || 'mainnet' } }));
} }
import NETWORKS from './availableNetworks'; import findNetwork from './findNetwork';
export default function getNetworkTitle({ network_type: type, network_sub_type: subType }: {network_type?: string; network_sub_type?: string}) { export default function getNetworkTitle({ network_type: type, network_sub_type: subType }: {network_type?: string; network_sub_type?: string}) {
const currentNetwork = NETWORKS.find(n => n.type === type && n.subType === subType); const currentNetwork = findNetwork({ network_type: type || '', network_sub_type: subType });
if (currentNetwork) { if (currentNetwork) {
return currentNetwork.name + (currentNetwork.shortName ? ` (${ currentNetwork.shortName })` : '') + ' Explorer'; return currentNetwork.name + (currentNetwork.shortName ? ` (${ currentNetwork.shortName })` : '') + ' Explorer';
} }
......
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 ]);
}
type ArrayElement<ArrayType extends Array<unknown>> =
ArrayType extends Array<(infer ElementType)> ? ElementType : never;
export default ArrayElement;
...@@ -10,13 +10,13 @@ import useColors from './useColors'; ...@@ -10,13 +10,13 @@ import useColors from './useColors';
interface Props { interface Props {
isCollapsed?: boolean; isCollapsed?: boolean;
isActive?: boolean; isActive?: boolean;
pathname: string; url: string;
text: string; text: string;
icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>; icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
px?: string | number; px?: string | number;
} }
const NavLink = ({ text, pathname, icon, isCollapsed, isActive, px }: Props) => { const NavLink = ({ text, url, icon, isCollapsed, isActive, px }: Props) => {
const colors = useColors(); const colors = useColors();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const width = (() => { const width = (() => {
...@@ -28,7 +28,7 @@ const NavLink = ({ text, pathname, icon, isCollapsed, isActive, px }: Props) => ...@@ -28,7 +28,7 @@ const NavLink = ({ text, pathname, icon, isCollapsed, isActive, px }: Props) =>
})(); })();
return ( return (
<NextLink href={ pathname } passHref> <NextLink href={ url } passHref>
<Link <Link
as="li" as="li"
listStyleType="none" listStyleType="none"
......
import { ChevronLeftIcon } from '@chakra-ui/icons'; import { ChevronLeftIcon } from '@chakra-ui/icons';
import { Flex, Box, VStack, useColorModeValue, useBreakpointValue } from '@chakra-ui/react'; import { Flex, Box, VStack, useColorModeValue, useBreakpointValue } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
...@@ -13,8 +12,6 @@ import NavFooter from './NavFooter'; ...@@ -13,8 +12,6 @@ import NavFooter from './NavFooter';
import NavLink from './NavLink'; import NavLink from './NavLink';
const NavigationDesktop = () => { const NavigationDesktop = () => {
const router = useRouter();
const { mainNavItems, accountNavItems } = useNavItems(); const { mainNavItems, accountNavItems } = useNavItems();
const isLargeScreen = useBreakpointValue({ base: false, xl: true }); const isLargeScreen = useBreakpointValue({ base: false, xl: true });
const navBarCollapsedCookie = cookies.get(cookies.NAMES.NAV_BAR_COLLAPSED); const navBarCollapsedCookie = cookies.get(cookies.NAMES.NAV_BAR_COLLAPSED);
...@@ -67,15 +64,13 @@ const NavigationDesktop = () => { ...@@ -67,15 +64,13 @@ const NavigationDesktop = () => {
</Box> </Box>
<Box as="nav" mt={ 14 }> <Box as="nav" mt={ 14 }>
<VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden"> <VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden">
{ mainNavItems.map((item) => { mainNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) }
<NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed } isActive={ router.asPath.startsWith(item.pathname) }/>) }
</VStack> </VStack>
</Box> </Box>
{ isAuth && ( { isAuth && (
<Box as="nav" mt={ 12 }> <Box as="nav" mt={ 12 }>
<VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden"> <VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden">
{ accountNavItems.map((item) => { accountNavItems.map((item) => <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>) }
<NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed } isActive={ router.asPath.startsWith(item.pathname) }/>) }
</VStack> </VStack>
</Box> </Box>
) } ) }
......
import { Box, VStack } from '@chakra-ui/react'; import { Box, VStack } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
...@@ -9,20 +8,19 @@ import NavLink from 'ui/blocks/navigation/NavLink'; ...@@ -9,20 +8,19 @@ import NavLink from 'ui/blocks/navigation/NavLink';
const NavigationMobile = () => { const NavigationMobile = () => {
const { mainNavItems, accountNavItems } = useNavItems(); const { mainNavItems, accountNavItems } = useNavItems();
const router = useRouter();
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN)); const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
return ( return (
<> <>
<Box as="nav" mt={ 6 }> <Box as="nav" mt={ 6 }>
<VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden"> <VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden">
{ mainNavItems.map((item) => <NavLink key={ item.text } { ...item } isActive={ router.asPath.startsWith(item.pathname) }/>) } { mainNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) }
</VStack> </VStack>
</Box> </Box>
{ isAuth && ( { isAuth && (
<Box as="nav" mt={ 6 }> <Box as="nav" mt={ 6 }>
<VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden"> <VStack as="ul" spacing="2" alignItems="flex-start" overflow="hidden">
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item } isActive={ router.asPath.startsWith(item.pathname) }/>) } { accountNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) }
</VStack> </VStack>
</Box> </Box>
) } ) }
......
...@@ -4,8 +4,8 @@ import React from 'react'; ...@@ -4,8 +4,8 @@ import React from 'react';
import type { FunctionComponent, SVGAttributes } from 'react'; import type { FunctionComponent, SVGAttributes } from 'react';
import blockscoutLogo from 'icons/logo.svg'; import blockscoutLogo from 'icons/logo.svg';
import useBasePath from 'lib/hooks/useBasePath';
import useNetwork from 'lib/hooks/useNetwork'; import useNetwork from 'lib/hooks/useNetwork';
import useLink from 'lib/link/useLink';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
interface Props { interface Props {
...@@ -15,7 +15,8 @@ interface Props { ...@@ -15,7 +15,8 @@ interface Props {
const NetworkLogo = ({ isCollapsed, onClick }: Props) => { const NetworkLogo = ({ isCollapsed, onClick }: Props) => {
const logoColor = useColorModeValue('blue.600', 'white'); const logoColor = useColorModeValue('blue.600', 'white');
const href = useBasePath(); const link = useLink();
const href = link('network_index');
const network = useNetwork(); const network = useNetwork();
const logo = network?.logo; const logo = network?.logo;
......
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,16 @@ import type { Network } from 'types/networks'; ...@@ -6,34 +6,16 @@ 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 localPath = (() => {
if (isAccount && isAccountSupported) {
return routeName;
}
if (isAccount && !isAccountSupported) {
return '';
}
// will change when blocks&transaction is implemented
return routeName;
})();
const pathName = `/${ type }${ subType ? '/' + subType : '' }${ localPath }`;
// will fix later after we agree on CI/CD workflow
const href = type === 'poa' && subType === 'core' ? pathName : 'https://blockscout.com' + pathName;
const hasIcon = Boolean(icon); const hasIcon = Boolean(icon);
const colors = useColors({ hasIcon }); const colors = useColors({ hasIcon });
...@@ -49,7 +31,7 @@ const NetworkMenuLink = ({ name, type, subType, icon, isActive, isMobile, routeN ...@@ -49,7 +31,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 }
......
...@@ -33,10 +33,10 @@ const ProfileMenuContent = ({ name, nickname, email }: Props) => { ...@@ -33,10 +33,10 @@ const ProfileMenuContent = ({ name, nickname, email }: Props) => {
> >
{ email } { email }
</Text> </Text>
<NavLink { ...profileItem } px="0px"/> <NavLink { ...profileItem } isActive={ undefined } px="0px"/>
<Box as="nav" mt={ 2 } pt={ 2 } borderTopColor={ borderColor } borderTopWidth="1px" { ...getDefaultTransitionProps() }> <Box as="nav" mt={ 2 } pt={ 2 } borderTopColor={ borderColor } borderTopWidth="1px" { ...getDefaultTransitionProps() }>
<VStack as="ul" spacing="0" alignItems="flex-start" overflow="hidden"> <VStack as="ul" spacing="0" alignItems="flex-start" overflow="hidden">
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item } px="0px"/>) } { accountNavItems.map((item) => <NavLink key={ item.text } { ...item } isActive={ undefined } px="0px"/>) }
</VStack> </VStack>
</Box> </Box>
<Box mt={ 2 } pt={ 3 } borderTopColor={ borderColor } borderTopWidth="1px" { ...getDefaultTransitionProps() }> <Box mt={ 2 } pt={ 3 } borderTopColor={ borderColor } borderTopWidth="1px" { ...getDefaultTransitionProps() }>
......
import type { ChangeEvent, FormEvent } from 'react'; import type { ChangeEvent, FormEvent } from 'react';
import React from 'react'; import React from 'react';
import useBasePath from 'lib/hooks/useBasePath';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import useLink from 'lib/link/useLink';
import SearchBarDesktop from './SearchBarDesktop'; import SearchBarDesktop from './SearchBarDesktop';
import SearchBarMobile from './SearchBarMobile'; import SearchBarMobile from './SearchBarMobile';
const SearchBar = () => { const SearchBar = () => {
const [ value, setValue ] = React.useState(''); const [ value, setValue ] = React.useState('');
const basePath = useBasePath(); const link = useLink();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => { const handleChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
...@@ -18,8 +18,9 @@ const SearchBar = () => { ...@@ -18,8 +18,9 @@ const SearchBar = () => {
const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => { const handleSubmit = React.useCallback((event: FormEvent<HTMLFormElement>) => {
event.preventDefault(); event.preventDefault();
window.location.assign(`https://blockscout.com${ basePath }/search-results?q=${ value }`); const url = link('search_results', undefined, { q: value });
}, [ value, basePath ]); window.location.assign(url);
}, [ link, value ]);
if (isMobile) { if (isMobile) {
return ( return (
......
...@@ -8,7 +8,7 @@ import { ...@@ -8,7 +8,7 @@ import {
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import useBasePath from 'lib/hooks/useBasePath'; import useLink from 'lib/link/useLink';
import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags'; import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags';
import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags'; import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags';
import Page from 'ui/shared/Page'; import Page from 'ui/shared/Page';
...@@ -25,13 +25,13 @@ type Props = { ...@@ -25,13 +25,13 @@ type Props = {
const PrivateTags = ({ tab }: Props) => { const PrivateTags = ({ tab }: Props) => {
const [ , setActiveTab ] = useState<TabName>(tab); const [ , setActiveTab ] = useState<TabName>(tab);
const basePath = useBasePath(); const link = useLink();
const onChangeTab = useCallback((index: number) => { const onChangeTab = useCallback((index: number) => {
setActiveTab(TABS[index]); setActiveTab(TABS[index]);
const newUrl = basePath + '/account/' + (TABS[index] === 'address' ? 'tag_address' : 'tag_transaction'); const newUrl = link(TABS[index] === 'address' ? 'private_tags_address' : 'private_tags_tx');
history.replaceState(history.state, '', newUrl); history.replaceState(history.state, '', newUrl);
}, [ setActiveTab, basePath ]); }, [ link ]);
return ( return (
<Page> <Page>
......
...@@ -8,7 +8,8 @@ import { ...@@ -8,7 +8,8 @@ import {
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import useBasePath from 'lib/hooks/useBasePath'; import type { RouteName } from 'lib/link/routes';
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';
import TxDetails from 'ui/tx/TxDetails'; import TxDetails from 'ui/tx/TxDetails';
...@@ -18,17 +19,18 @@ import TxRawTrace from 'ui/tx/TxRawTrace'; ...@@ -18,17 +19,18 @@ import TxRawTrace from 'ui/tx/TxRawTrace';
interface Tab { interface Tab {
type: 'details' | 'internal_txn' | 'logs' | 'raw_trace' | 'state'; type: 'details' | 'internal_txn' | 'logs' | 'raw_trace' | 'state';
path: string;
name: string; name: string;
path?: string;
component?: React.ReactNode; component?: React.ReactNode;
routeName: RouteName;
} }
const TABS: Array<Tab> = [ const TABS: Array<Tab> = [
{ type: 'details', path: '', 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', component: <TxRawTrace/> }, { type: 'raw_trace', routeName: 'tx_raw_trace', name: 'Raw trace', component: <TxRawTrace/> },
]; ];
export interface Props { export interface Props {
...@@ -38,14 +40,14 @@ export interface Props { ...@@ -38,14 +40,14 @@ export interface Props {
const TransactionPageContent = ({ tab }: Props) => { const TransactionPageContent = ({ tab }: Props) => {
const [ , setActiveTab ] = React.useState<Tab['type']>(tab); const [ , setActiveTab ] = React.useState<Tab['type']>(tab);
const router = useRouter(); const router = useRouter();
const basePath = useBasePath(); const link = useLink();
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 = basePath + '/tx/' + router.query.id + 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, basePath, router ]); }, [ setActiveTab, link, router.query.id ]);
const defaultIndex = TABS.map(({ type }) => type).indexOf(tab); const defaultIndex = TABS.map(({ type }) => type).indexOf(tab);
......
...@@ -2,7 +2,7 @@ import { Flex, Link } from '@chakra-ui/react'; ...@@ -2,7 +2,7 @@ import { Flex, Link } from '@chakra-ui/react';
import type { HTMLChakraProps } from '@chakra-ui/system'; import type { HTMLChakraProps } from '@chakra-ui/system';
import React from 'react'; import React from 'react';
import useBasePath from 'lib/hooks/useBasePath'; import useLink from 'lib/link/useLink';
import AddressWithDots from './AddressWithDots'; import AddressWithDots from './AddressWithDots';
import CopyToClipboard from './CopyToClipboard'; import CopyToClipboard from './CopyToClipboard';
...@@ -18,14 +18,15 @@ interface Props extends HTMLChakraProps<'div'> { ...@@ -18,14 +18,15 @@ interface Props extends HTMLChakraProps<'div'> {
} }
const AddressLinkWithTooltip = ({ address, type = 'address', truncated, withCopy = true, ...styles }: Props) => { const AddressLinkWithTooltip = ({ address, type = 'address', truncated, withCopy = true, ...styles }: Props) => {
const basePath = useBasePath(); const link = useLink();
let url; let url;
if (type === 'transaction') { if (type === 'transaction') {
url = basePath + '/tx/' + address; url = link('tx_index', { id: address });
} else if (type === 'token') { } else if (type === 'token') {
url = basePath + '/address/' + address + '/tokens#address-tabs'; url = link('token_index', { id: address });
} else { } else {
url = basePath + '/address/' + address; url = link('address_index', { id: address });
} }
return ( return (
<Flex columnGap={ 2 } alignItems="center" overflow="hidden" maxW="100%" { ...styles }> <Flex columnGap={ 2 } alignItems="center" overflow="hidden" maxW="100%" { ...styles }>
......
...@@ -3,7 +3,7 @@ import React from 'react'; ...@@ -3,7 +3,7 @@ import React from 'react';
import tokeIcon from 'icons/tokens/toke.svg'; import tokeIcon from 'icons/tokens/toke.svg';
import usdtIcon from 'icons/tokens/usdt.svg'; import usdtIcon from 'icons/tokens/usdt.svg';
import useBasePath from 'lib/hooks/useBasePath'; import useLink from 'lib/link/useLink';
// temporary solution // temporary solution
// don't know where to get icons and addresses yet // don't know where to get icons and addresses yet
...@@ -29,13 +29,13 @@ interface Props { ...@@ -29,13 +29,13 @@ interface Props {
const Token = ({ symbol, className }: Props) => { const Token = ({ symbol, className }: Props) => {
const token = TOKENS[symbol as keyof typeof TOKENS]; const token = TOKENS[symbol as keyof typeof TOKENS];
const basePath = useBasePath(); const link = useLink();
if (!token) { if (!token) {
return null; return null;
} }
const url = basePath + '/token/' + token.address; const url = link('token_index', { id: token.address });
return ( return (
<Center className={ className }> <Center className={ className }>
......
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