Commit 30193c74 authored by tom's avatar tom

rewrite network menu

parent 73bb6ce1
...@@ -6,3 +6,13 @@ NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_TELEGRAM_LINK ...@@ -6,3 +6,13 @@ NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_TELEGRAM_LINK
NEXT_PUBLIC_FOOTER_STAKING_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_STAKING_LINK NEXT_PUBLIC_FOOTER_STAKING_LINK=APP_NEXT_NEXT_PUBLIC_FOOTER_STAKING_LINK
NEXT_PUBLIC_SENTRY_DSN=APP_NEXT_NEXT_PUBLIC_SENTRY_DSN NEXT_PUBLIC_SENTRY_DSN=APP_NEXT_NEXT_PUBLIC_SENTRY_DSN
NEXT_PUBLIC_APP_INSTANCE=APP_NEXT_NEXT_PUBLIC_APP_INSTANCE NEXT_PUBLIC_APP_INSTANCE=APP_NEXT_NEXT_PUBLIC_APP_INSTANCE
NEXT_PUBLIC_NETWORK_NAME=APP_NEXT_NEXT_PUBLIC_NETWORK_NAME
NEXT_PUBLIC_NETWORK_SHORT_NAME=APP_NEXT_NEXT_PUBLIC_NETWORK_SHORT_NAME
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=APP_NEXT_NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME
NEXT_PUBLIC_NETWORK_TYPE=APP_NEXT_NEXT_PUBLIC_NETWORK_TYPE
NEXT_PUBLIC_NETWORK_SUBTYPE=APP_NEXT_NEXT_PUBLIC_NETWORK_SUBTYPE
NEXT_PUBLIC_NETWORK_ID=APP_NEXT_NEXT_PUBLIC_NETWORK_ID
NEXT_PUBLIC_NETWORK_CURRENCY=APP_NEXT_NEXT_PUBLIC_NETWORK_CURRENCY
NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS=APP_NEXT_NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS
NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED=APP_NEXT_NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED
NEXT_PUBLIC_FEATURED_NETWORKS=APP_NEXT_NEXT_PUBLIC_FEATURED_NETWORKS
const env = process.env.VERCEL_ENV || process.env.NODE_ENV;
const isDev = env === 'development';
const config = Object.freeze({
env,
isDev,
networkType: process.env.NEXT_PUBLIC_NETWORK_TYPE,
networkSubtype: process.env.NEXT_PUBLIC_NETWORK_SUBTYPE,
basePath: '/' + [ process.env.NEXT_PUBLIC_NETWORK_TYPE, process.env.NEXT_PUBLIC_NETWORK_SUBTYPE ].filter(Boolean).join('/'),
// TODO domain should be passed in CI during runtime
domain: isDev ? 'http://localhost:3000' : 'https://blockscout.com',
});
export default config;
import isBrowser from 'lib/isBrowser'; import appConfig from 'configs/app/config';
import findNetwork from 'lib/networks/findNetwork';
import { ROUTES } from './routes'; import { ROUTES } from './routes';
import type { RouteName } from './routes'; import type { RouteName } from './routes';
...@@ -12,27 +11,26 @@ export function link(routeName: RouteName, urlParams?: Record<string, Array<stri ...@@ -12,27 +11,26 @@ export function link(routeName: RouteName, urlParams?: Record<string, Array<stri
return ''; return '';
} }
const network = findNetwork({ // if we pass network type, we have to get subtype from params too
network_type: typeof urlParams?.network_type === 'string' ? urlParams?.network_type : '', // otherwise getting it from config since it is not cross-chain link
network_sub_type: typeof urlParams?.network_sub_type === 'string' ? urlParams?.network_sub_type : undefined, const networkSubType = typeof urlParams?.network_type === 'string' ? urlParams?.network_sub_type : appConfig.networkSubtype;
});
const path = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => { const path = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => {
if (paramName === 'network_sub_type' && !network?.subType) { if (paramName === 'network_sub_type' && !networkSubType) {
return ''; return '';
} }
let paramValue = urlParams?.[paramName]; let paramValue = urlParams?.[paramName];
if (Array.isArray(paramValue)) { if (Array.isArray(paramValue)) {
// FIXME we don't have yet params as array, but typescript says that we could // FIXME we don't have yet params as array, but typescript says that we could
// dun't know how to manage it, fix me if you find an issue // dunno know how to manage it, fix me if you find an issue
paramValue = paramValue.join(','); paramValue = paramValue.join(',');
} }
return paramValue ? `/${ paramValue }` : ''; return paramValue ? `/${ paramValue }` : '';
}); });
const url = new URL(path, isBrowser() ? window.location.origin : 'https://blockscout.com'); const url = new URL(path, appConfig.domain);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => { queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
url.searchParams.append(key, value); url.searchParams.append(key, value);
......
...@@ -17,31 +17,24 @@ export const ROUTES = { ...@@ -17,31 +17,24 @@ export const ROUTES = {
// ACCOUNT // ACCOUNT
watchlist: { watchlist: {
pattern: `${ BASE_PATH }/account/watchlist`, pattern: `${ BASE_PATH }/account/watchlist`,
crossNetworkNavigation: true,
}, },
private_tags_address: { private_tags_address: {
pattern: `${ BASE_PATH }/account/tag_address`, pattern: `${ BASE_PATH }/account/tag_address`,
crossNetworkNavigation: true,
}, },
private_tags_tx: { private_tags_tx: {
pattern: `${ BASE_PATH }/account/tag_transaction`, 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 // TRANSACTIONS
......
import type { FeaturedNetwork } from 'types/networks';
import arbitrumIcon from 'icons/networks/icons/arbitrum.svg';
import artisIcon from 'icons/networks/icons/artis.svg';
import ethereumClassicIcon from 'icons/networks/icons/ethereum-classic.svg';
import ethereumIcon from 'icons/networks/icons/ethereum.svg';
import gnosisIcon from 'icons/networks/icons/gnosis.svg';
import optimismIcon from 'icons/networks/icons/optimism.svg';
import poaSokolIcon from 'icons/networks/icons/poa-sokol.svg';
import poaIcon from 'icons/networks/icons/poa.svg';
import rskIcon from 'icons/networks/icons/rsk.svg';
// predefined network icons
const ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'/xdai/mainnet': gnosisIcon,
'/xdai/optimism': optimismIcon,
'/xdai/aox': arbitrumIcon,
'/eth/mainnet': ethereumIcon,
'/etc/mainnet': ethereumClassicIcon,
'/poa/core': poaIcon,
'/rsk/mainnet': rskIcon,
'/xdai/testnet': arbitrumIcon,
'/poa/sokol': poaSokolIcon,
'/artis/sigma1': artisIcon,
};
// for easy .env.example update
// const FEATURED_NETWORKS = JSON.stringify([
// {
// title: 'Gnosis Chain',
// basePath: '/xdai/mainnet',
// group: 'mainnets',
// },
// {
// title: 'Optimism on Gnosis Chain',
// basePath: '/xdai/optimism',
// group: 'mainnets',
// icon: 'https://www.fillmurray.com/60/60',
// },
// {
// title: 'Arbitrum on xDai',
// basePath: '/xdai/aox',
// group: 'mainnets',
// },
// {
// title: 'Ethereum',
// basePath: '/eth/mainnet',
// group: 'mainnets',
// },
// {
// title: 'Ethereum Classic',
// basePath: '/etx/mainnet',
// group: 'mainnets',
// },
// {
// title: 'POA',
// basePath: '/poa/core',
// group: 'mainnets',
// },
// {
// title: 'RSK',
// basePath: '/rsk/mainnet',
// group: 'mainnets',
// },
// {
// title: 'Gnosis Chain Testnet',
// basePath: '/xdai/testnet',
// group: 'testnets',
// },
// {
// title: 'POA Sokol',
// basePath: '/poa/sokol',
// group: 'testnets',
// },
// {
// title: 'ARTIS Σ1',
// basePath: '/artis/sigma1',
// group: 'other',
// },
// {
// title: 'LUKSO L14',
// basePath: '/lukso/l14',
// group: 'other',
// },
// {
// title: 'Astar',
// basePath: '/astar',
// group: 'other',
// },
// ]);
const CONFIG_VALUE = process.env.NEXT_PUBLIC_FEATURED_NETWORKS?.replaceAll('\'', '"');
function parseNetworkConfig() {
try {
return JSON.parse(CONFIG_VALUE || '[]');
} catch (error) {
return [];
}
}
const featuredNetworks: Array<FeaturedNetwork> = (() => {
const networksFromConfig: Array<FeaturedNetwork> = parseNetworkConfig();
return networksFromConfig.map((network) => ({
...network,
icon: network.icon || ICONS[network.basePath],
}));
})();
export default featuredNetworks;
const supportedNetworks = process.env.NEXT_PUBLIC_SUPPORTED_NETWORKS?.replaceAll('\'', '"'); const featuredNetworks = process.env.NEXT_PUBLIC_FEATURED_NETWORKS?.replaceAll('\'', '"');
// should be CommonJS module since it used for next.config.js // should be CommonJS module since it used for next.config.js
function parseNetworkConfig() { function parseNetworkConfig() {
try { try {
return JSON.parse(supportedNetworks || '[]'); return JSON.parse(featuredNetworks || '[]');
} catch (error) { } catch (error) {
return []; return [];
} }
......
import appConfig from 'configs/app/config';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import useNetwork from 'lib/hooks/useNetwork';
import isAccountRoute from 'lib/link/isAccountRoute';
import { link } from 'lib/link/link'; import { link } from 'lib/link/link';
import { ROUTES } from 'lib/link/routes'; import { ROUTES } from 'lib/link/routes';
import useCurrentRoute from 'lib/link/useCurrentRoute'; import useCurrentRoute from 'lib/link/useCurrentRoute';
import NETWORKS from 'lib/networks/availableNetworks'; import featuredNetworks from 'lib/networks/featuredNetworks';
export default function useNetworkNavigationItems() { export default function useNetworkNavigationItems() {
const selectedNetwork = useNetwork();
const currentRouteName = useCurrentRoute()(); const currentRouteName = useCurrentRoute()();
const currentRoute = ROUTES[currentRouteName]; const currentRoute = ROUTES[currentRouteName];
const router = useRouter(); const router = useRouter();
const isAccount = isAccountRoute(currentRouteName);
return React.useMemo(() => { return React.useMemo(() => {
return NETWORKS.map((network) => { return featuredNetworks.map((network) => {
const routeName = 'crossNetworkNavigation' in currentRoute && currentRoute.crossNetworkNavigation ? currentRouteName : 'network_index';
const routeName = (() => { const [ , networkType, networkSubtype ] = network.basePath.split('/');
if ('crossNetworkNavigation' in currentRoute && currentRoute.crossNetworkNavigation) { const url = link(routeName, { ...router.query, network_type: networkType, network_sub_type: networkSubtype });
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 { return {
...network, ...network,
url: url, url: url,
isActive: selectedNetwork?.type === network.type && selectedNetwork?.subType === network?.subType, isActive: appConfig.basePath === network.basePath,
}; };
}); });
}, [ currentRoute, currentRouteName, isAccount, router.query, selectedNetwork?.subType, selectedNetwork?.type ]); }, [ currentRoute, currentRouteName, router.query ]);
} }
import appConfig from 'configs/app/config';
import type { NextRequest } from 'next/server'; import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { NAMES } from 'lib/cookies'; import { NAMES } from 'lib/cookies';
import getCspPolicy from 'lib/csp/getCspPolicy'; import getCspPolicy from 'lib/csp/getCspPolicy';
import { link } from 'lib/link/link'; import { link } from 'lib/link/link';
import findNetwork from 'lib/networks/findNetwork';
const cspPolicy = getCspPolicy(); const cspPolicy = getCspPolicy();
...@@ -20,9 +20,8 @@ export function middleware(req: NextRequest) { ...@@ -20,9 +20,8 @@ export function middleware(req: NextRequest) {
network_type: networkType, network_type: networkType,
network_sub_type: networkSubtype, network_sub_type: networkSubtype,
}; };
const selectedNetwork = findNetwork(networkParams);
if (!selectedNetwork) { if (appConfig.networkType !== networkType && appConfig.networkSubtype !== networkSubtype) {
const url = req.nextUrl.clone(); const url = req.nextUrl.clone();
url.pathname = `/404`; url.pathname = `/404`;
return NextResponse.rewrite(url); return NextResponse.rewrite(url);
......
...@@ -2,6 +2,7 @@ import type { FunctionComponent, SVGAttributes } from 'react'; ...@@ -2,6 +2,7 @@ import type { FunctionComponent, SVGAttributes } from 'react';
export type NetworkGroup = 'mainnets' | 'testnets' | 'other'; export type NetworkGroup = 'mainnets' | 'testnets' | 'other';
// todo_tom delete this
export interface Network { export interface Network {
name: string; name: string;
chainId: number; // https://chainlist.org/ chainId: number; // https://chainlist.org/
...@@ -17,3 +18,10 @@ export interface Network { ...@@ -17,3 +18,10 @@ export interface Network {
isAccountSupported?: boolean; isAccountSupported?: boolean;
assetsNamePath?: string; assetsNamePath?: string;
} }
export interface FeaturedNetwork {
title: string;
basePath: string;
group: 'mainnets' | 'testnets' | 'other';
icon?: FunctionComponent<SVGAttributes<SVGElement>> | string;
}
...@@ -35,7 +35,7 @@ const NetworkMenuPopup = () => { ...@@ -35,7 +35,7 @@ const NetworkMenuPopup = () => {
.filter((network) => network.group === tab) .filter((network) => network.group === tab)
.map((network) => ( .map((network) => (
<NetworkMenuLink <NetworkMenuLink
key={ network.name } key={ network.title }
{ ...network } { ...network }
/> />
)) } )) }
......
...@@ -30,7 +30,7 @@ const NetworkMenuContentMobile = () => { ...@@ -30,7 +30,7 @@ const NetworkMenuContentMobile = () => {
.filter(({ group }) => group === selectedTab) .filter(({ group }) => group === selectedTab)
.map((network) => ( .map((network) => (
<NetworkMenuLink <NetworkMenuLink
key={ network.name } key={ network.title }
{ ...network } { ...network }
isMobile isMobile
/> />
......
...@@ -2,25 +2,25 @@ import { Box, Flex, Icon, Text, Image } from '@chakra-ui/react'; ...@@ -2,25 +2,25 @@ import { Box, Flex, Icon, Text, Image } from '@chakra-ui/react';
import NextLink from 'next/link'; import NextLink from 'next/link';
import React from 'react'; import React from 'react';
import type { Network } from 'types/networks'; import type { FeaturedNetwork } 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 useColors from './useColors'; import useColors from './useColors';
interface Props extends Network { interface Props extends FeaturedNetwork {
isActive: boolean; isActive: boolean;
isMobile?: boolean; isMobile?: boolean;
url: string; url: string;
} }
const NetworkMenuLink = ({ name, type, subType, icon, isActive, isMobile, url }: Props) => { const NetworkMenuLink = ({ title, icon, isActive, isMobile, url }: Props) => {
const hasIcon = Boolean(icon); const hasIcon = Boolean(icon);
const colors = useColors({ hasIcon }); const colors = useColors({ hasIcon });
const iconEl = typeof icon === 'string' ? ( const iconEl = typeof icon === 'string' ? (
<Image w="30px" h="30px" src={ icon } alt={ `${ type } ${ subType ? subType : '' } network icon` }/> <Image w="30px" h="30px" src={ icon } alt={ `${ title } network icon` }/>
) : ( ) : (
<Icon <Icon
as={ hasIcon ? icon : placeholderIcon } as={ hasIcon ? icon : placeholderIcon }
...@@ -52,7 +52,7 @@ const NetworkMenuLink = ({ name, type, subType, icon, isActive, isMobile, url }: ...@@ -52,7 +52,7 @@ const NetworkMenuLink = ({ name, type, subType, icon, isActive, isMobile, url }:
fontSize={ isMobile ? 'sm' : 'md' } fontSize={ isMobile ? 'sm' : 'md' }
lineHeight={ isMobile ? '20px' : '24px' } lineHeight={ isMobile ? '20px' : '24px' }
> >
{ name } { title }
</Text> </Text>
{ isActive && ( { isActive && (
<Icon <Icon
......
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