Commit 4f9d218e authored by tom's avatar tom

Merge branch 'main' of github.com:tom2drum/block-scout into mobile-view-navigation

parents e690669d bcc6e990
API_HOST=blockscout.com
API_BASE_PATH=/xdai/testnet/api
\ No newline at end of file
API_AUTHORIZATION_TOKEN=xxx
\ No newline at end of file
API_AUTHORIZATION_TOKEN=xxx
NEXT_PUBLIC_BLOCKSCOUT_VERSION=xxx
NEXT_PUBLIC_FOOTER_GITHUB_LINK=https://github.com/blockscout/blockscout
NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
NEXT_PUBLIC_FOOTER_TELEGRAM_LINK=https://t.me/poa_network
NEXT_PUBLIC_FOOTER_STAKING_LINK=https://duneanalytics.com/maxaleks/xdai-staking
NEXT_PUBLIC_SUPPORTED_NETWORKS=[{"name":"Gnosis Chain","type":"xdai","subType":"mainnet","group":"mainnets","isAccountSupported":true},{"name":"Optimism on Gnosis Chain","type":"xdai","subType":"optimism","group":"mainnets"},{"name":"Arbitrum on xDai","type":"xdai","subType":"aox","group":"mainnets"},{"name":"Ethereum","type":"eth","subType":"mainnet","group":"mainnets"},{"name":"Ethereum Classic","type":"etc","subType":"mainnet","group":"mainnets"},{"name":"POA","type":"poa","subType":"core","group":"mainnets"},{"name":"RSK","type":"rsk","subType":"mainnet","group":"mainnets"},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets","isAccountSupported":true},{"name":"POA Sokol","type":"poa","subType":"sokol","group":"testnets"},{"name":"ARTIS Σ1","type":"artis","subType":"sigma1","group":"other"},{"name":"LUKSO L14","type":"lukso","subType":"l14","group":"other"}]
\ No newline at end of file
API_HOST=blockscout.com
API_BASE_PATH=/xdai/testnet/api
\ No newline at end of file
[Design](https://www.figma.com/file/07zoJSAP7Vo655ertmlppA/My_Account?node-id=279%3A1006) | [API Doc](https://github.com/blockscout/blockscout-account/blob/account/apps/block_scout_web/API.md)
[Design](https://www.figma.com/file/07zoJSAP7Vo655ertmlppA/My_Account?node-id=279%3A1006) | [API Doc](https://github.com/blockscout/blockscout-account/blob/account/apps/block_scout_web/API.md) | [Swagger](https://app.swaggerhub.com/apis/NIKITOSING4/blockscout-account-api/1.0)
-----
## Technology stack
Core technologies what used in the project are
- [yarn](https://yarnpkg.com/) as package manager
- [React](https://reactjs.org/) as UI library
- [Next.js](https://nextjs.org/) as application framework
- [Chakra](https://chakra-ui.com/) as component library; our theme customization could be found in `/theme` folder
- [css-modules](https://github.com/css-modules/css-modules) as lib for styling custom components
And of course our premier language is [Typescript](https://www.typescriptlang.org/)
-----
## Local Development
For local development follow next steps:
For local development please follow next steps:
- clone repo
- install dependencies with `yarn`
- create local env file `env.local` according to `env.example`
- `API_AUTHORIZATION_TOKEN` could be obtain [here](https://blockscout.com/xdai/testnet/auth/auth0_api)
- create local env file `env.local` according to `env.example` snapshot (see list of used environment variables [below](#environment-variables))
- run `yarn dev` to spin up local dev server and navigate to the host from logs output
## Environment variables
### Variables list
The app instance could be customized by passing following variables to NodeJS environment.
| Variable | Type | Description | Default value
| --- | --- | --- | --- |
| NEXT_PUBLIC_BLOCKSCOUT_VERSION | `string` | Current running version of Blockscout (used to display link to release in the footer) |
| NEXT_PUBLIC_FOOTER_GITHUB_LINK | `string` | Link to Github in the footer | `https://github.com/blockscout/blockscout` |
| NEXT_PUBLIC_FOOTER_TWITTER_LINK | `string` | Link to Twitter in the footer | `https://www.twitter.com/blockscoutcom` |
| NEXT_PUBLIC_FOOTER_TELEGRAM_LINK | `string` | Link to Telegram in the footer | `https://t.me/poa_network` |
| NEXT_PUBLIC_FOOTER_STAKING_LINK | `string` | Link to staking dashboard in the footer | `https://duneanalytics.com/maxaleks/xdai-staking` |
| NEXT_PUBLIC_SUPPORTED_NETWORKS | `Array<Network>` where `Network` can have following [properties](#network-configuration-properties) | Configuration of supported networks | `[{"name":"Gnosis Chain","type":"xdai","subType":"mainnet","group":"mainnets","isAccountSupported":true},{"name":"Optimism on Gnosis Chain","type":"xdai","subType":"optimism","group":"mainnets"},{"name":"Arbitrum on xDai","type":"xdai","subType":"aox","group":"mainnets"},{"name":"Ethereum","type":"eth","subType":"mainnet","group":"mainnets"},{"name":"Ethereum Classic","type":"etc","subType":"mainnet","group":"mainnets"},{"name":"POA","type":"poa","subType":"core","group":"mainnets"},{"name":"RSK","type":"rsk","subType":"mainnet","group":"mainnets"},{"name":"Gnosis Chain Testnet","type":"xdai","subType":"testnet","group":"testnets","isAccountSupported":true},{"name":"POA Sokol","type":"poa","subType":"sokol","group":"testnets"},{"name":"ARTIS Σ1","type":"artis","subType":"sigma1","group":"other"},{"name":"LUKSO L14","type":"lukso","subType":"l14","group":"other"}]` |
### Network configuration properties
| Property | Type | Description | Example value
| --- | --- | --- | --- |
| name | `string` | Displayed name of the network | `"Gnosis Chain"` |
| type | `string` | Network type (used as first part of the base path) | `"xdai"` |
| subType | `string` | Network subtype (used as second part of the base path) | `"mainnet"` |
| group | `mainnets \| testnets \| other` | Indicates in which tab network appears in the menu | `"mainnets"` |
| isAccountSupported | `boolean` *(optional)* | Set to true if network has account feature | `true` |
| icon | `string` *(optional)* | Network icon; if not provided, will fallback to icon predefined in the project; if the project doesn't have icon for such network then the common placeholder will be shown; *Note* that icon size should be 30px by 30px | `"https://www.fillmurray.com/60/60"` |
*Note* the base path for the network is built up from its `type` and `subType` like so `https://blockscout.com/<type>/<subType>`
\ No newline at end of file
......@@ -9,7 +9,7 @@ export default function fetch(path: string, init?: RequestInit): Promise<Respons
authorization: `Bearer ${ process.env.API_AUTHORIZATION_TOKEN }`,
'content-type': 'application/json',
};
const url = `https://${ process.env.API_HOST }${ process.env.API_BASE_PATH }${ path }`;
const url = `https://blockscout.com${ path }`;
return nodeFetch(url, {
headers,
......
import type { NextApiRequest, NextApiResponse } from 'next';
import fetch from 'lib/api/fetch';
import * as cookies from 'lib/cookies';
type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE';
......@@ -8,7 +9,16 @@ export default function handler<TRes>(getUrl: (_req: NextApiRequest) => string,
return async(_req: NextApiRequest, res: NextApiResponse<TRes>) => {
if (_req.method && allowedMethods.includes(_req.method as Methods)) {
const isBodyDisallowed = _req.method === 'GET' || _req.method === 'HEAD';
const response = await fetch(getUrl(_req), {
const networkType = _req.cookies[cookies.NAMES.NETWORK_TYPE];
const networkSubType = _req.cookies[cookies.NAMES.NETWORK_SUB_TYPE];
if (!networkType || !networkSubType) {
// eslint-disable-next-line no-console
console.error(`Incorrect network: NETWORK_TYPE=${ networkType } NETWORK_SUB_TYPE=${ networkSubType }`);
}
const url = `/${ networkType }/${ networkSubType }/api${ getUrl(_req) }`;
const response = await fetch(url, {
method: _req.method,
body: isBodyDisallowed ? undefined : _req.body,
});
......
......@@ -4,7 +4,9 @@ import { Cookies } from 'typescript-cookie';
import isBrowser from './isBrowser';
export enum NAMES {
NAV_BAR_COLLAPSED='nav_bar_collapsed'
NAV_BAR_COLLAPSED='nav_bar_collapsed',
NETWORK_TYPE='network_type',
NETWORK_SUB_TYPE='network_sub_type',
}
export function get(name?: string | undefined | null) {
......
......@@ -25,11 +25,11 @@ export default function useNavItems() {
];
const accountNavItems = [
{ text: 'Watchlist', pathname: basePath + '/watchlist', icon: watchlistIcon },
{ text: 'Private tags', pathname: basePath + '/private-tags', icon: privateTagIcon },
{ text: 'Public tags', pathname: basePath + '/public-tags', icon: publicTagIcon },
{ text: 'API keys', pathname: basePath + '/api-keys', icon: apiKeysIcon },
{ text: 'Custom ABI', pathname: basePath + '/custom-abi', icon: abiIcon },
{ text: 'Watchlist', pathname: basePath + '/account/watchlist', icon: watchlistIcon },
{ text: 'Private tags', pathname: basePath + '/account/private-tags', icon: privateTagIcon },
{ text: 'Public tags', pathname: basePath + '/account/public-tags', icon: publicTagIcon },
{ text: 'API keys', pathname: basePath + '/account/api-keys', icon: apiKeysIcon },
{ text: 'Custom ABI', pathname: basePath + '/account/custom-abi', icon: abiIcon },
];
return { mainNavItems, accountNavItems };
......
......@@ -10,88 +10,101 @@ import poaSokolIcon from 'icons/networks/poa-sokol.svg';
import poaIcon from 'icons/networks/poa.svg';
import rskIcon from 'icons/networks/rsk.svg';
export const NETWORKS: Array<Network> = [
{
name: 'Gnosis Chain',
type: 'xdai',
subType: 'mainnet',
icon: gnosisIcon,
group: 'mainnets',
isAccountSupported: true,
isNewUiSupported: true,
},
{
name: 'Optimism on Gnosis Chain',
type: 'xdai',
subType: 'optimism',
icon: optimismIcon,
group: 'mainnets',
},
{
name: 'Arbitrum on xDai',
type: 'xdai',
subType: 'aox',
icon: arbitrumIcon,
group: 'mainnets',
},
{
name: 'Ethereum',
type: 'eth',
subType: 'mainnet',
icon: ethereumIcon,
group: 'mainnets',
},
{
name: 'Ethereum Classic',
type: 'etc',
subType: 'mainnet',
icon: ethereumClassicIcon,
group: 'mainnets',
},
{
name: 'POA',
type: 'poa',
subType: 'core',
icon: poaIcon,
group: 'mainnets',
},
{
name: 'RSK',
type: 'rsk',
subType: 'mainnet',
icon: rskIcon,
group: 'mainnets',
},
{
name: 'Gnosis Chain Testnet',
type: 'xdai',
subType: 'testnet',
icon: arbitrumIcon,
group: 'testnets',
isAccountSupported: true,
isNewUiSupported: true,
},
{
name: 'POA Sokol',
type: 'poa',
subType: 'sokol',
icon: poaSokolIcon,
group: 'testnets',
},
{
name: 'ARTIS Σ1',
type: 'artis',
subType: 'sigma1',
icon: artisIcon,
group: 'other',
},
{
name: 'LUKSO L14',
type: 'lukso',
subType: 'l14',
group: 'other',
},
];
// will change later when we agree how to host 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,
};
export const NETWORKS: Array<Network> = (() => {
try {
const networksFromConfig: Array<Network> = JSON.parse(process.env.NEXT_PUBLIC_SUPPORTED_NETWORKS || '[]');
return networksFromConfig.map((network) => ({ ...network, icon: network.icon || ICONS[`${ network.type }/${ network.subType }`] }));
} catch (error) {
return [];
}
})();
// for easy env creation
// const FOR_CONFIG = [
// {
// name: 'Gnosis Chain',
// type: 'xdai',
// subType: 'mainnet',
// group: 'mainnets',
// isAccountSupported: true,
// },
// {
// name: 'Optimism on Gnosis Chain',
// type: 'xdai',
// subType: 'optimism',
// group: 'mainnets',
// icon: 'https://www.fillmurray.com/60/60'
// },
// {
// name: 'Arbitrum on xDai',
// type: 'xdai',
// subType: 'aox',
// group: 'mainnets',
// },
// {
// name: 'Ethereum',
// type: 'eth',
// subType: 'mainnet',
// group: 'mainnets',
// },
// {
// name: 'Ethereum Classic',
// type: 'etc',
// subType: 'mainnet',
// group: 'mainnets',
// },
// {
// name: 'POA',
// type: 'poa',
// subType: 'core',
// group: 'mainnets',
// },
// {
// name: 'RSK',
// type: 'rsk',
// subType: 'mainnet',
// group: 'mainnets',
// },
// {
// name: 'Gnosis Chain Testnet',
// type: 'xdai',
// subType: 'testnet',
// group: 'testnets',
// isAccountSupported: true,
// },
// {
// name: 'POA Sokol',
// type: 'poa',
// subType: 'sokol',
// group: 'testnets',
// },
// {
// name: 'ARTIS Σ1',
// type: 'artis',
// subType: 'sigma1',
// group: 'other',
// },
// {
// name: 'LUKSO L14',
// type: 'lukso',
// subType: 'l14',
// group: 'other',
// },
// ];
export const ACCOUNT_ROUTES = [ '/watchlist', '/private-tags', '/public-tags', '/api-keys', '/custom-abi' ];
......
......@@ -11,7 +11,7 @@ module.exports = withReactSvg({
return [
{
source: '/',
destination: '/xdai/mainnet',
destination: '/xdai/testnet',
permanent: true,
},
];
......
......@@ -8,7 +8,6 @@ export interface Network {
type: string;
subType: string;
group: 'mainnets' | 'testnets' | 'other';
icon?: FunctionComponent<SVGAttributes<SVGElement>>;
icon?: FunctionComponent<SVGAttributes<SVGElement>> | string;
isAccountSupported?: boolean;
isNewUiSupported?: boolean;
}
......@@ -9,11 +9,14 @@ import useIsMobile from 'lib/hooks/useIsMobile';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
const SOCIAL_LINKS = [
{ link: '#gh', icon: ghIcon },
{ link: '#tw', icon: twIcon },
{ link: '#tg', icon: tgIcon },
{ link: '#stats', icon: statsIcon },
];
{ link: process.env.NEXT_PUBLIC_FOOTER_GITHUB_LINK, icon: ghIcon },
{ link: process.env.NEXT_PUBLIC_FOOTER_TWITTER_LINK, icon: twIcon },
{ link: process.env.NEXT_PUBLIC_FOOTER_TELEGRAM_LINK, icon: tgIcon },
{ link: process.env.NEXT_PUBLIC_FOOTER_STAKING_LINK, icon: statsIcon },
].filter(({ link }) => link !== undefined);
const BLOCKSCOUT_VERSION = process.env.NEXT_PUBLIC_BLOCKSCOUT_VERSION;
const VERSION_URL = `https://github.com/blockscout/blockscout/tree/${ BLOCKSCOUT_VERSION }`;
interface Props {
isCollapsed?: boolean;
......@@ -58,7 +61,7 @@ const NavFooter = ({ isCollapsed }: Props) => {
<Text variant="secondary">
Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks.
</Text>
<Text variant="secondary">Version: <Link>v4.2.1-beta</Link></Text>
<Text variant="secondary">Version: <Link href={ VERSION_URL } target="_blank">{ BLOCKSCOUT_VERSION }</Link></Text>
</>
) }
</VStack>
......
import { Popover, PopoverTrigger, Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import * as cookies from 'lib/cookies';
import NetworkMenuButton from './NetworkMenuButton';
import NetworkMenuPopup from './NetworkMenuPopup';
interface Props {
isCollapsed: boolean;
}
const NetworkMenu = ({ isCollapsed }: Props) => {
const router = useRouter();
const networkType = router.query.network_type;
const networkSubType = router.query.network_sub_type;
React.useEffect(() => {
if (typeof networkType === 'string') {
cookies.set(cookies.NAMES.NETWORK_TYPE, networkType);
}
if (typeof networkSubType === 'string') {
cookies.set(cookies.NAMES.NETWORK_SUB_TYPE, networkSubType);
}
}, [ networkType, networkSubType ]);
return (
<Popover openDelay={ 300 } placement="right-start" gutter={ 22 } isLazy>
<PopoverTrigger>
......
import { Box, Flex, Icon, Text } from '@chakra-ui/react';
import { Box, Flex, Icon, Text, Image } from '@chakra-ui/react';
import NextLink from 'next/link';
import React from 'react';
......@@ -15,7 +15,7 @@ interface Props extends Network {
routeName: string;
}
const NetworkMenuLink = ({ name, type, subType, icon, isActive, routeName, isAccountSupported, isNewUiSupported }: Props) => {
const NetworkMenuLink = ({ name, type, subType, icon, isActive, routeName, isAccountSupported }: Props) => {
const isAccount = isAccountRoute(routeName);
const localPath = (() => {
if (isAccount && isAccountSupported) {
......@@ -32,10 +32,20 @@ const NetworkMenuLink = ({ name, type, subType, icon, isActive, routeName, isAcc
const pathName = `/${ type }/${ subType }${ localPath }`;
// will fix later after we agree on CI/CD workflow
const href = isNewUiSupported ? pathName : 'https://blockscout.com' + pathName;
const href = type === 'xdai' && subType === 'testnet' ? pathName : 'https://blockscout.com' + pathName;
const hasIcon = Boolean(icon);
const colors = useColors({ hasIcon });
const iconEl = typeof icon === 'string' ? (
<Image w="30px" h="30px" src={ icon } alt={ `${ type } ${ subType } network icon` }/>
) : (
<Icon
as={ hasIcon ? icon : placeholderIcon }
boxSize="30px"
color={ isActive ? colors.icon.active : colors.icon.default }
/>
);
return (
<Box as="li" listStyleType="none">
<NextLink href={ href } passHref>
......@@ -51,11 +61,7 @@ const NetworkMenuLink = ({ name, type, subType, icon, isActive, routeName, isAcc
bgColor={ isActive ? colors.bg.active : colors.bg.default }
_hover={{ color: isActive ? colors.text.active : colors.text.hover }}
>
<Icon
as={ hasIcon ? icon : placeholderIcon }
boxSize="30px"
color={ isActive ? colors.icon.active : colors.icon.default }
/>
{ iconEl }
<Text
marginLeft={ 3 }
fontWeight="500"
......
......@@ -9,23 +9,26 @@ import { NETWORKS } from 'lib/networks';
import NetworkMenuLink from './NetworkMenuLink';
const TABS: Array<NetworkGroup> = [ 'mainnets', 'testnets', 'other' ];
const availableTabs = TABS.filter((tab) => NETWORKS.some(({ group }) => group === tab));
const NetworkMenuPopup = () => {
const router = useRouter();
const routeName = router.pathname.replace('/[network_type]/[network_sub_type]', '');
const selectedNetwork = NETWORKS.find((network) => router.query.network_type === network.type && router.query.network_sub_type === network.subType);
const selectedTab = TABS.findIndex((tab) => selectedNetwork?.group === tab);
const selectedTab = availableTabs.findIndex((tab) => selectedNetwork?.group === tab);
return (
<PopoverContent w="382px">
<PopoverBody>
<Text as="h4" fontSize="18px" lineHeight="30px" fontWeight="500">Networks</Text>
<Tabs variant="soft-rounded" mt={ 4 } isLazy defaultIndex={ selectedTab !== -1 ? selectedTab : undefined }>
<TabList>
{ TABS.map((tab) => <Tab key={ tab } textTransform="capitalize">{ tab }</Tab>) }
</TabList>
{ availableTabs.length > 1 && (
<TabList>
{ availableTabs.map((tab) => <Tab key={ tab } textTransform="capitalize">{ tab }</Tab>) }
</TabList>
) }
<TabPanels mt={ 8 }>
{ TABS.map((tab) => (
{ availableTabs.map((tab) => (
<TabPanel key={ tab } p={ 0 }>
<VStack as="ul" spacing={ 2 } alignItems="stretch" mt={ 4 }>
{ NETWORKS
......
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