migrate to Next.js

This commit is contained in:
Matthias Kretschmann 2019-12-07 23:02:18 +01:00
parent 7b54275822
commit fa51b4ac1a
Signed by: m
GPG Key ID: 606EEEF3C479A91F
50 changed files with 8809 additions and 6835 deletions

View File

@ -1,20 +1,37 @@
{
"extends": [
"oceanprotocol",
"oceanprotocol/react",
"prettier/react",
"prettier/standard",
"plugin:prettier/recommended"
],
"plugins": ["prettier"],
"extends": ["eslint:recommended", "prettier"],
"env": {
"es6": true,
"browser": true,
"jest": true
"node": true
},
"settings": {
"react": {
"version": "16.8"
"version": "detect"
}
}
},
"overrides": [
{
"files": ["**/*.ts", "**/*.tsx"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["./tsconfig.json"]
},
"extends": [
"oceanprotocol",
"oceanprotocol/react",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"prettier/react",
"prettier/standard",
"prettier/@typescript-eslint"
],
"plugins": ["@typescript-eslint", "prettier"],
"rules": {
"react/prop-types": "off",
"@typescript-eslint/explicit-function-return-type": "off"
}
}
]
}

6
.gitignore vendored
View File

@ -13,11 +13,9 @@
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.env.*
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.next

View File

@ -1,18 +1,17 @@
dist: xenial
language: node_js
node_js:
- "12"
node_js: node
before_script:
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
- chmod +x ./cc-test-reporter
- "./cc-test-reporter before-build"
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
- chmod +x ./cc-test-reporter
- './cc-test-reporter before-build'
script:
# will run `npm ci` automatically here
- npm test
- "./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT"
- npm run build
# will run `npm ci` automatically here
- npm test
- './cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT'
- npm run build
notifications:
email: false
email: false

View File

@ -82,4 +82,4 @@ distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
```
```

18
jest.config.js Normal file
View File

@ -0,0 +1,18 @@
module.exports = {
preset: 'ts-jest/presets/js-with-babel',
setupFilesAfterEnv: ['<rootDir>/jest/setup.ts'],
globals: {
'ts-jest': {
tsConfig: 'jest.tsconfig.json'
}
},
moduleNameMapper: {
'.+\\.(css|styl|less|sass|scss)$': '<rootDir>/jest/__mocks__/styleMock.js',
'.+\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
'<rootDir>/jest/__mocks__/fileMock.js',
'\\.svg': '<rootDir>/jest/__mocks__/svgrMock.js'
},
testPathIgnorePatterns: ['.next/', 'node_modules/', 'build/', 'coverage/'],
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/@types/**/*'],
collectCoverage: true
}

12
jest.tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"jsx": "react",
"allowJs": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"noImplicitAny": true,
"sourceMap": true,
"target": "es5"
}
}

View File

@ -0,0 +1 @@
module.exports = 'test-file-stub'

View File

@ -0,0 +1 @@
module.exports = {}

View File

@ -0,0 +1 @@
module.exports = 'svg'

2
next-env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="next" />
/// <reference types="next/types/global" />

41
next.config.js Normal file
View File

@ -0,0 +1,41 @@
const withCSS = require('@zeit/next-css')
// eslint-disable-next-line no-unused-vars
const withSvgr = (nextConfig = {}, nextComposePlugins = {}) => {
return Object.assign({}, nextConfig, {
webpack(config, options) {
config.module.rules.push({
test: /\.svg$/,
use: [
{
loader: '@svgr/webpack',
options: {
icon: true
}
}
]
})
if (typeof nextConfig.webpack === 'function') {
return nextConfig.webpack(config, options)
}
return config
}
})
}
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true'
})
module.exports = withBundleAnalyzer(
withSvgr(
withCSS({
cssModules: true,
cssLoaderOptions: {
localIdentName: '[local]___[hash:base64:5]'
}
})
)
)

13816
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,32 +4,48 @@
"version": "0.1.0",
"license": "Apache-2.0",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "npm run lint && react-scripts test --coverage --watchAll=false",
"test:watch": "react-scripts test --coverage",
"start": "next dev",
"build": "next build",
"serve": "next start",
"analyze": "ANALYZE=true next build",
"test": "npm run lint && NODE_ENV=test jest",
"test:watch": "npm run lint && NODE_ENV=test jest --watch",
"lint": "eslint --ignore-path .gitignore --ext .js .",
"format": "prettier ./src/**/*.{js,scss,json} --write"
"format": "prettier --ignore-path .gitignore **/**/*.{css,yml,js,jsx,ts,tsx,json} --write"
},
"dependencies": {
"@ethereum-navigator/atlas": "^0.5.1",
"@oceanprotocol/art": "^2.2.0",
"@oceanprotocol/typographies": "^0.1.0",
"@zeit/next-css": "^1.0.1",
"axios": "^0.19.0",
"next": "9.1.4",
"next-seo": "^3.1.0",
"next-svgr": "^0.0.2",
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-scripts": "^3.2.0"
"react-dom": "^16.12.0"
},
"devDependencies": {
"@next/bundle-analyzer": "^9.1.4",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"eslint": "^6.7.1",
"@types/jest": "^24.0.23",
"@types/next-seo": "^1.10.0",
"@types/node": "^12.12.14",
"@types/react": "^16.9.15",
"@typescript-eslint/eslint-plugin": "^2.10.0",
"@typescript-eslint/parser": "^2.10.0",
"cssnano": "^4.1.10",
"eslint": "^6.7.2",
"eslint-config-oceanprotocol": "^1.5.0",
"eslint-config-prettier": "^6.7.0",
"eslint-plugin-prettier": "^3.1.1",
"jest-mock-axios": "^3.1.2",
"node-sass": "^4.13.0",
"prettier": "^1.19.1"
"jest": "^24.9.0",
"postcss-preset-env": "^6.7.0",
"prettier": "^1.19.1",
"ts-jest": "^24.2.0",
"typescript": "^3.7.3",
"webpack": "^4.41.2"
},
"repository": {
"type": "git",
@ -47,11 +63,5 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"jest": {
"collectCoverageFrom": [
"src/**/*.js",
"!src/serviceWorker.js"
]
}
}

8
postcss.config.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
plugins: {
'postcss-preset-env': {
importFrom: './src/styles/_variables.css'
},
cssnano: {}
}
}

View File

@ -1,35 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/icons/icon-96x96.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#141414" />
<meta
name="description"
content="Overview and status checks of all Ocean Protocol RPC network connections."
/>
<link rel="apple-touch-icon" href="icons/icon-256x256.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<meta property="og:title" content="Ocean Protocol Status" />
<meta
property="og:description"
content="Overview and status checks of all Ocean Protocol RPC network connections."
/>
<meta
property="og:image"
content="https://status.oceanprotocol.com/share.png"
/>
<meta property="og:url" content="https://status.oceanprotocol.com" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@oceanprotocol" />
<title>Ocean Protocol Status</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@ -1,50 +0,0 @@
{
"short_name": "Ocean Status",
"name": "Ocean Protocol Status",
"start_url": ".",
"display": "standalone",
"theme_color": "#141414",
"background_color": "#141414",
"icons": [
{
"src": "icons/icon-48x48.png?v=a2156652544310e91b0703e484d8e51f",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "icons/icon-72x72.png?v=a2156652544310e91b0703e484d8e51f",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View File

@ -1,2 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *

5
site.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
title: 'Ocean Protocol Status',
description: `Testing all RPC network connections from your browser, refreshed every 5 sec.`,
url: 'https://status.oceanprotocol.com'
}

13
src/@types/global.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
declare module '*.module.css' {
const classes: { readonly [key: string]: string }
export default classes
}
declare module '*.svg' {
import * as React from 'react'
export const ReactComponent: React.FunctionComponent<React.SVGProps<
SVGSVGElement
>>
const src: string
export default src
}

View File

@ -1,31 +0,0 @@
import React from 'react'
import { ReactComponent as Logo } from '@oceanprotocol/art/logo/logo-white.svg'
import styles from './App.module.scss'
import atlas from '@ethereum-navigator/atlas'
import Network from './Network'
export default function App() {
return (
<div className={styles.app}>
<header className={styles.header}>
<Logo />
<h1>Ocean Protocol Status</h1>
<p>
Testing all RPC network connections from your browser, refreshed every
5 sec.
</p>
</header>
<div className={styles.networks}>
{atlas
.filter(
item => item.project === 'Ocean Protocol' && item.name !== 'Spree'
)
.reverse()
.map((network, i) => (
<Network key={i} network={network} />
))}
</div>
</div>
)
}

View File

@ -1,39 +0,0 @@
@import './styles/variables';
.app {
text-align: center;
padding: $spacer / 2;
}
h1, h2 {
color: $brand-white;
}
.header {
text-align: center;
margin-top: $spacer;
h1 {
margin-bottom: $spacer / 2;
}
p {
font-size: $font-size-large;
color: $brand-white;
margin: auto;
max-width: 35rem;
}
svg {
width: 75px;
height: 75px;
}
}
.networks {
display: grid;
gap: $spacer;
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
margin: $spacer * 2 auto;
max-width: 80rem;
}

14
src/Layout.module.css Normal file
View File

@ -0,0 +1,14 @@
.app {
padding: var(--spacer);
background: var(--brand-black);
min-height: 100vh;
}
@media screen and (min-width: 640px) {
.app {
padding: calc(var(--spacer) * 2);
padding-bottom: var(--spacer);
height: auto;
min-height: calc(100vh - var(--page-frame) * 2);
}
}

44
src/Layout.tsx Normal file
View File

@ -0,0 +1,44 @@
import React, { ReactNode } from 'react'
import Head from 'next/head'
import { NextSeo } from 'next-seo'
import styles from './Layout.module.css'
import { title, description, url } from '../site.config'
export default function Layout({
children,
pageTitle = title
}: {
children: ReactNode
pageTitle?: string
}) {
return (
<div className={styles.app}>
<Head>
<link rel="icon" href="/icons/icon-96x96.png" />
<link rel="apple-touch-icon" href="icons/icon-256x256.png" />
<meta name="theme-color" content="#141414" />
</Head>
<NextSeo
title={pageTitle}
description={description}
canonical={url}
openGraph={{
url,
title,
description,
images: [{ url: `${url}/share.png` }],
// eslint-disable-next-line @typescript-eslint/camelcase
site_name: title
}}
twitter={{
handle: '@oceanprotocol',
site: '@oceanprotocol',
cardType: 'summary_large_image'
}}
/>
{children}
</div>
)
}

View File

@ -1,91 +0,0 @@
import React, { useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { axiosRpcRequest } from './rpc'
import styles from './Network.module.scss'
Network.propTypes = {
network: PropTypes.shape({
name: PropTypes.string.isRequired,
networkId: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
rpcUrl: PropTypes.string.isRequired,
explorerUrl: PropTypes.string.isRequired
})
}
export default function Network({ network }) {
const [status, setStatus] = useState('')
const [block, setBlock] = useState('')
const [latency, setLatency] = useState('')
const [clientVersion, setClientVersion] = useState('')
useEffect(() => {
async function getStatusAndBlock() {
const response = await axiosRpcRequest(network.rpcUrl, 'eth_blockNumber')
if (!response || response.status !== 200) {
setStatus('Offline')
return
}
setStatus('Online')
response.duration && setLatency(response.duration)
const blockNumber =
response && response.data && parseInt(response.data.result, 16)
setBlock(blockNumber)
}
async function getClientVersion() {
const response = await axiosRpcRequest(
network.rpcUrl,
'web3_clientVersion'
)
response && response.data && setClientVersion(response.data.result)
}
getStatusAndBlock()
getClientVersion()
const timer = setInterval(() => {
getStatusAndBlock()
getClientVersion()
}, 5000) // run every 5 sec.
return () => {
clearInterval(timer)
}
}, [network])
const isOnline = status === 'Online'
return (
<div className={styles.network}>
<h2 className={styles.title}>
{network.name}
<code>{network.networkId}</code>
<span>{network.type}</span>
</h2>
<p>
<code>{network.rpcUrl}</code>
</p>
<p className={styles.status}>
<span className={isOnline ? styles.success : styles.error}>
{status}
</span>
{latency && (
<span className={styles.latency} title="Latency">
{latency} ms
</span>
)}
</p>
{block && (
<p className={styles.block} title="Current block number">
At block #
<a href={`${network.explorerUrl}/blocks/${block}`}>{block}</a>
</p>
)}
{clientVersion && <p className={styles.clientVersion}>{clientVersion}</p>}
</div>
)
}

View File

@ -1,59 +0,0 @@
@import './styles/variables';
.network {
border: 1px solid $brand-grey-dark;
padding: $spacer;
border-radius: $border-radius;
text-align: left;
p:last-child {
margin-bottom: 0;
}
}
.title {
margin-bottom: $spacer / 4;
margin-top: 0;
font-size: $font-size-h2;
span,
code {
color: $brand-grey-light;
font-size: $font-size-base;
display: inline-block;
margin-left: $spacer / 2;
}
code {
font-weight: $font-weight-base;
}
}
.block {
a {
color: $brand-grey-lighter;
}
}
.status {
margin-bottom: 0;
}
.success {
color: $green;
}
.error {
color: $red;
}
.latency {
display: inline-block;
margin-left: $spacer / 4;
font-size: $font-size-mini;
}
.clientVersion {
font-size: $font-size-mini;
}

View File

@ -1,50 +0,0 @@
import React from 'react'
import { render, wait } from '@testing-library/react'
import mockAxios from 'axios'
import Network from './Network'
const mockResponse = {
status: 200,
duration: 1000,
data: { result: '0x345' }
}
afterEach(() => {
mockAxios.reset()
jest.clearAllTimers()
})
describe('Network', () => {
const network = {
name: 'Pacific',
project: 'Ocean Protocol',
type: 'mainnet',
networkId: '0xCEA11',
rpcUrl: 'https://pacific.oceanprotocol.com',
explorerUrl: 'https://submarine.oceanprotocol.com'
}
it('renders without crashing', async () => {
mockAxios.post.mockResolvedValue(mockResponse)
const { container } = render(<Network network={network} />)
expect(container.firstChild).toBeInTheDocument()
await wait()
expect(mockAxios.post).toHaveBeenCalledTimes(2)
})
it('renders without response', async () => {
mockAxios.post.mockResolvedValue(undefined)
const { container } = render(<Network network={network} />)
await wait()
expect(container.firstChild).toBeInTheDocument()
})
it('re-fetches after 5 sec.', async () => {
jest.useFakeTimers()
mockAxios.post.mockResolvedValue(mockResponse)
render(<Network network={network} />)
jest.advanceTimersByTime(6000)
await wait()
// expect(setInterval).toHaveBeenCalledTimes(1)
})
})

View File

@ -1,2 +0,0 @@
import mockAxios from 'jest-mock-axios'
export default mockAxios

View File

@ -0,0 +1,10 @@
import React from 'react'
import { render } from '@testing-library/react'
import Layout from '../Layout'
describe('Layout', () => {
it('renders without crashing', () => {
const { container } = render(<Layout pageTitle="Hello">Hello</Layout>)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -0,0 +1,68 @@
import React from 'react'
import { render, wait, waitForElement } from '@testing-library/react'
import axios from 'axios'
import Network from '../components/Network'
import { mocked } from 'ts-jest/dist/util/testing'
jest.mock('axios')
const axiosMock: any = mocked(axios)
const mockResponse = {
status: 200,
duration: 1000,
data: { result: '0x345' }
}
const network = {
name: 'Pacific',
project: 'Ocean Protocol',
type: 'mainnet',
networkId: '0xCEA11',
rpcUrl: 'https://pacific.oceanprotocol.com',
explorerUrl: 'https://submarine.oceanprotocol.com'
}
const networkNoRpc = {
name: 'Pacific',
project: 'Ocean Protocol',
type: 'mainnet',
networkId: '0xCEA11',
explorerUrl: 'https://submarine.oceanprotocol.com'
}
afterEach(() => {
jest.clearAllTimers()
})
describe('Network', () => {
it('renders without crashing', async () => {
axiosMock.post.mockResolvedValue(mockResponse)
const { container, rerender, getByText } = render(
<Network network={network} />
)
expect(container.firstChild).toBeInTheDocument()
await waitForElement(() => getByText('Online'))
expect(axiosMock.post).toHaveBeenCalledTimes(2)
rerender(<Network network={networkNoRpc} />)
await waitForElement(() => getByText('Online'))
expect(axiosMock.post).toHaveBeenCalledTimes(2)
})
it('renders without response', async () => {
axiosMock.post.mockResolvedValue(undefined)
const { container } = render(<Network network={network} />)
await wait()
expect(container.firstChild).toBeInTheDocument()
})
it('re-fetches after 5 sec.', async () => {
jest.useFakeTimers()
axiosMock.post.mockResolvedValue(mockResponse)
const { getByText } = render(<Network network={network} />)
jest.advanceTimersByTime(6000)
await waitForElement(() => getByText('Online'))
// expect(setInterval).toHaveBeenCalledTimes(1)
})
})

View File

@ -0,0 +1,10 @@
import React from 'react'
import { render } from '@testing-library/react'
import Spinner from '../components/Spinner'
describe('Spinner', () => {
it('renders without crashing', () => {
const { container } = render(<Spinner message="Hello" />)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -1,10 +1,10 @@
import React from 'react'
import { render } from '@testing-library/react'
import App from './App'
import Home from '../pages'
describe('App', () => {
describe('Home', () => {
it('renders without crashing', () => {
const { container } = render(<App />)
const { container } = render(<Home />)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -0,0 +1,30 @@
.networkData {
min-height: 66px;
}
.block a {
color: var(--brand-grey-lighter);
}
.status {
font-size: var(--font-size-large);
margin-bottom: 0;
}
.success {
color: var(--green);
}
.error {
color: var(--red);
}
.latency {
display: inline-block;
margin-left: calc(var(--spacer) / 4);
font-size: var(--font-size-mini);
}
.clientVersion {
font-size: var(--font-size-mini);
}

View File

@ -0,0 +1,89 @@
import React, { useState, useEffect } from 'react'
import { fetchRpc, AxiosResponseCustom } from '../../rpc'
import Spinner from '../Spinner'
import { NetworkProps } from '.'
import styles from './Data.module.css'
export default function Data({ network }: { network: NetworkProps }) {
const { rpcUrl, explorerUrl } = network
const [status, setStatus] = useState('')
const [block, setBlock] = useState(0)
const [latency, setLatency] = useState(0)
const [clientVersion, setClientVersion] = useState('')
async function getStatusAndBlock() {
if (!rpcUrl) return
const response: AxiosResponseCustom = await fetchRpc(
rpcUrl,
'eth_blockNumber'
)
if (!response || response.status !== 200) {
setStatus('Offline')
return
}
setStatus('Online')
response.duration && setLatency(response.duration)
const blockNumber =
response && response.data && parseInt(response.data.result, 16)
setBlock(blockNumber)
}
async function getClientVersion() {
if (!rpcUrl) return
const response: AxiosResponseCustom = await fetchRpc(
rpcUrl,
'web3_clientVersion'
)
response && response.data && setClientVersion(response.data.result)
}
useEffect(() => {
getStatusAndBlock()
getClientVersion()
const timer = setInterval(() => {
getStatusAndBlock()
getClientVersion()
}, 5000) // run every 5 sec.
return () => {
clearInterval(timer)
}
}, [network])
const isOnline = status === 'Online'
return (
<div className={styles.networkData}>
{block > 0 ? (
<>
<h2 className={styles.status}>
<span className={isOnline ? styles.success : styles.error}>
{status}
</span>
{latency && (
<span className={styles.latency} title="Latency">
{latency} ms
</span>
)}
</h2>
{block && (
<p className={styles.block} title="Current block number">
At block #<a href={`${explorerUrl}/blocks/${block}`}>{block}</a>
</p>
)}
{clientVersion && (
<p className={styles.clientVersion}>{clientVersion}</p>
)}
</>
) : (
<Spinner />
)}
</div>
)
}

View File

@ -0,0 +1,32 @@
.network {
border: 1px solid var(--brand-grey-dark);
padding: var(--spacer);
border-radius: var(--border-radius);
text-align: left;
}
.network p:last-child {
margin-bottom: 0;
}
.networkHeader {
margin-bottom: var(--spacer);
}
.title {
margin-bottom: calc(var(--spacer) / 8);
margin-top: 0;
font-size: var(--font-size-h2);
}
.title span,
.title code {
color: var(--brand-grey-light);
font-size: var(--font-size-base);
display: inline-block;
margin-left: calc(var(--spacer) / 2);
}
.title code {
font-weight: var(--font-weight-base);
}

View File

@ -0,0 +1,32 @@
import React from 'react'
import styles from './index.module.css'
import Data from './Data'
export interface NetworkProps {
name: string
project?: string
networkId: string
chainId?: string
type: string
rpcUrl?: string
explorerUrl?: string
}
export default function Network({ network }: { network: NetworkProps }) {
return (
<div className={styles.network}>
<header className={styles.networkHeader}>
<h2 className={styles.title}>
{network.name}
<code>{network.networkId}</code>
<span>{network.type}</span>
</h2>
<p>
<code>{network.rpcUrl}</code>
</p>
</header>
<Data network={network} />
</div>
)
}

View File

@ -0,0 +1,33 @@
.spinner {
position: relative;
text-align: center;
margin-top: calc(var(--spacer) * 2);
line-height: 1.3;
}
.spinner:before {
content: '';
box-sizing: border-box;
position: absolute;
top: 0;
left: 50%;
width: 20px;
height: 20px;
margin-top: -20px;
margin-left: -10px;
border-radius: 50%;
border: 2px solid var(--brand-purple);
border-top-color: var(--brand-violet);
animation: spinner 0.6s linear infinite;
}
.spinnerMessage {
color: var(--brand-grey-light);
padding-top: calc(var(--spacer) / 4);
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,17 @@
import React from 'react'
import styles from './Spinner.module.css'
const Spinner = ({ message }: { message?: string }) => {
return (
<div className={styles.spinner}>
{message && (
<div
className={styles.spinnerMessage}
dangerouslySetInnerHTML={{ __html: message }}
/>
)}
</div>
)
}
export default Spinner

View File

@ -1,19 +0,0 @@
import React from 'react'
import ReactDOM from 'react-dom'
import './styles/global.scss'
import App from './App'
import * as serviceWorker from './serviceWorker'
function renderToDOM() {
const rootElement = document.getElementById('root')
if (rootElement !== null) {
ReactDOM.render(<App />, rootElement)
}
}
export { renderToDOM }
renderToDOM()
serviceWorker.register()

View File

@ -1,22 +0,0 @@
import ReactDOM from 'react-dom'
import { renderToDOM } from '.'
describe('test ReactDOM.render', () => {
const originalRender = ReactDOM.render
const originalGetElement = global.document.getElementById
beforeEach(() => {
global.document.getElementById = () => true
ReactDOM.render = jest.fn()
})
afterAll(() => {
global.document.getElementById = originalGetElement
ReactDOM.render = originalRender
})
it('should call ReactDOM.render', () => {
renderToDOM()
expect(ReactDOM.render).toHaveBeenCalled()
})
})

12
src/pages/_app.tsx Normal file
View File

@ -0,0 +1,12 @@
import React from 'react'
import App from 'next/app'
import '@oceanprotocol/typographies/css/ocean-typo.css'
import '../styles/global.css'
export default class MyApp extends App {
render() {
const { Component, pageProps } = this.props
return <Component {...pageProps} />
}
}

View File

@ -0,0 +1,33 @@
h1,
h2 {
color: var(--brand-white);
}
.header {
text-align: center;
margin-top: var(--spacer);
}
.header h1 {
margin-bottom: calc(var(--spacer) / 2);
}
.header p {
font-size: var(--font-size-large);
color: var(--brand-white);
margin: auto;
max-width: 35rem;
}
.header svg {
width: 75px;
height: 75px;
}
.networks {
display: grid;
gap: var(--spacer);
grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
margin: calc(var(--spacer) * 2) auto;
max-width: 80rem;
}

31
src/pages/index.tsx Normal file
View File

@ -0,0 +1,31 @@
import React from 'react'
import atlas from '@ethereum-navigator/atlas'
import Logo from '@oceanprotocol/art/logo/logo-white.svg'
import Layout from '../Layout'
import Network, { NetworkProps } from '../components/Network'
import styles from './index.module.css'
import { title, description } from '../../site.config'
export default function Home() {
return (
<Layout>
<header className={styles.header}>
<Logo />
<h1>{title}</h1>
<p>{description}</p>
</header>
<div className={styles.networks}>
{atlas
.filter(
(item: NetworkProps) =>
item.project === 'Ocean Protocol' && item.name !== 'Spree'
)
.reverse()
.map((network: NetworkProps) => (
<Network key={network.name} network={network} />
))}
</div>
</Layout>
)
}

View File

@ -1,6 +1,18 @@
import axios from 'axios'
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios'
async function axiosRpcRequest(url, method) {
export interface AxiosRequestConfigCustom extends AxiosRequestConfig {
metadata?: {
startTime: number | Date
endTime?: number | Date
}
}
export interface AxiosResponseCustom extends AxiosResponse {
duration?: number
config: AxiosRequestConfigCustom
}
async function fetchRpc(url: string, method: string) {
try {
const response = await axios.post(url, {
method,
@ -15,11 +27,11 @@ async function axiosRpcRequest(url, method) {
}
}
export { axiosRpcRequest }
export { fetchRpc }
// Measure response time and deliver as `response.duration`
axios.interceptors.request.use(
config => {
(config: AxiosRequestConfigCustom) => {
config.metadata = { startTime: new Date() }
return config
},
@ -27,7 +39,7 @@ axios.interceptors.request.use(
)
axios.interceptors.response.use(
response => {
(response: any) => {
response.config.metadata.endTime = new Date()
response.duration =
response.config.metadata.endTime - response.config.metadata.startTime

View File

@ -1,135 +0,0 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
)
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config)
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
)
})
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config)
}
})
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing
if (installingWorker == null) {
return
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
)
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration)
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.')
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration)
}
}
}
}
}
})
.catch(error => {
console.error('Error during service worker registration:', error)
})
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type')
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload()
})
})
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config)
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
)
})
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister()
})
}
}

51
src/styles/_variables.css Normal file
View File

@ -0,0 +1,51 @@
:root {
--brand-white: #fff;
--brand-black: #141414;
--brand-pink: #ff4092;
--brand-purple: #7b1173;
--brand-violet: #e000cf;
--brand-blue: #11597b;
--brand-grey: #41474e;
--brand-grey-light: #8b98a9;
--brand-grey-dark: #303030;
--brand-grey-lighter: #e2e2e2;
--green: #5fb359;
--red: #d80606;
--orange: #b35f36;
--yellow: #eac146;
--font-family-base: 'Sharp Sans', -apple-system, BlinkMacSystemFont,
'Segoe UI', Helvetica, Arial, sans-serif;
--font-family-title: 'Sharp Sans Display', -apple-system, BlinkMacSystemFont,
'Segoe UI', Helvetica, Arial, sans-serif;
--font-family-monospace: 'Fira Code', 'Fira Mono', Menlo, Monaco, Consolas,
'Courier New', monospace;
--font-size-root: 16px;
--font-size-base: 1rem;
--font-size-large: 1.2rem;
--font-size-small: 0.85rem;
--font-size-mini: 0.65rem;
--font-size-text: $font-size-base;
--font-size-label: $font-size-base;
--font-size-title: 1.4rem;
--font-size-h1: 3rem;
--font-size-h2: 1.7rem;
--font-size-h3: 1.4rem;
--font-size-h4: $font-size-large;
--font-size-h5: 1.1rem;
--font-weight-base: 500;
--font-weight-bold: 600;
--line-height: 1.6;
--spacer: 2rem;
--page-frame: 0.75rem;
--break-point--small: 640px;
--break-point--medium: 860px;
--break-point--large: 1140px;
--break-point--huge: 1400px;
--border-radius: 0.2rem;
}

View File

@ -1,62 +0,0 @@
// Colors
$brand-white: #fff;
$brand-black: #141414;
$brand-pink: #ff4092;
$brand-purple: #7b1173;
$brand-violet: #e000cf;
$brand-blue: #11597b;
$brand-grey: #41474e;
$brand-grey-light: #8b98a9;
$brand-grey-dark: #303030;
$brand-grey-lighter: #e2e2e2;
$green: #5fb359;
$red: #d80606;
$orange: #b35f36;
$yellow: #eac146;
$brand-gradient: linear-gradient(to right bottom, $brand-purple, $brand-pink);
$body-background: $brand-black;
// Fonts
$font-family-base: 'Sharp Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI',
Helvetica, Arial, sans-serif;
$font-family-title: 'Sharp Sans Display', -apple-system, BlinkMacSystemFont,
'Segoe UI', Helvetica, Arial, sans-serif;
$font-family-monospace: 'Fira Code', 'Fira Mono', Menlo, Monaco, Consolas,
'Courier New', monospace;
$font-size-root: 16px;
$font-size-base: 1rem;
$font-size-large: 1.2rem;
$font-size-small: 0.85rem;
$font-size-mini: 0.65rem;
$font-size-text: $font-size-base;
$font-size-label: $font-size-base;
$font-size-title: 1.4rem;
$font-size-h1: 3rem;
$font-size-h2: 1.7rem;
$font-size-h3: 1.4rem;
$font-size-h4: $font-size-large;
$font-size-h5: 1.1rem;
$font-weight-base: 500;
$font-weight-bold: 600;
$line-height: 1.6;
// Sizes
$spacer: 2rem;
$page-frame: 0.75rem;
$break-point--small: 640px;
$break-point--medium: 860px;
$break-point--large: 1140px;
$break-point--huge: 1400px;
$brand-border-width: 1px;
$border-radius: 0.2rem;
$narrowWidth: 35rem;

126
src/styles/global.css Normal file
View File

@ -0,0 +1,126 @@
@import '_variables.css';
*,
*:before,
*:after {
box-sizing: border-box;
}
html,
body {
width: 100%;
margin: 0;
padding: 0;
}
html {
font-size: var(--font-size-root);
}
body {
color: var(--brand-grey-light);
font-size: var(--font-size-base);
font-family: var(--font-family-base);
font-weight: var(--font-weight-base);
line-height: var(--line-height);
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
position: relative;
}
@media screen and (min-width: 640px) {
body {
padding: var(--page-frame);
}
}
a {
text-decoration: none;
color: var(--brand-pink);
transition: 0.2s ease-out;
}
p {
margin: 0;
margin-bottom: calc(var(--spacer) / var(--line-height));
}
h1,
h2,
h3,
h4,
h5 {
font-family: var(--font-family-title);
color: var(--brand-white);
line-height: 1.2;
font-weight: var(--font-weight-bold);
}
h1 {
font-size: var(--font-size-h1);
}
h2 {
font-size: var(--font-size-h2);
}
h3 {
font-size: var(--font-size-h3);
}
h4 {
font-size: var(--font-size-h4);
}
h5 {
font-size: var(--font-size-h5);
}
figure,
img,
svg,
video,
audio,
embed,
canvas,
picture {
max-width: 100%;
height: auto;
margin: 0 auto;
display: block;
}
ul {
margin-top: 0;
margin-bottom: calc(var(--spacer) / var(--line-height));
padding-left: 0;
list-style: none;
padding-left: calc(var(--spacer) / var(--line-height));
}
ul li {
position: relative;
display: block;
margin-bottom: 0;
}
ul li + li {
margin-top: calc(var(--spacer) / 4);
}
ul li:before {
content: ' \25AA';
top: -2px;
position: absolute;
left: -1rem;
color: var(--brand-grey-light);
user-select: none;
}
code {
font-family: var(--font-family-monospace);
font-size: var(--font-size-small);
color: var(--brand-grey-light);
text-shadow: none;
}

View File

@ -1,334 +0,0 @@
@import 'variables';
@import '../../node_modules/@oceanprotocol/typographies/sass/ocean-typo.scss';
*,
*:before,
*:after {
box-sizing: border-box;
}
/* stylelint-disable selector-max-id */
html,
body,
#root {
height: 100%;
}
/* stylelint-enable selector-max-id */
html {
font-size: $font-size-root;
}
body {
color: $brand-grey-light;
font-size: $font-size-base;
font-family: $font-family-base;
font-weight: $font-weight-base;
line-height: $line-height;
background: $body-background;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
position: relative;
margin: 0;
@media screen and (min-width: $break-point--small) {
padding: $page-frame;
}
}
a {
text-decoration: none;
color: $brand-pink;
transition: 0.2s ease-out;
&:hover,
&:focus {
color: darken($brand-pink, 15%);
text-decoration: none;
transform: translate3d(0, -0.1rem, 0);
}
&:active {
color: darken($brand-pink, 15%);
text-decoration: none;
transform: none;
transition: none;
}
}
p {
margin: 0;
margin-bottom: $spacer / $line-height;
}
// Lists
/////////////////////////////////////
ul {
li {
&:before {
content: ' \25AA'; // Black Small Square: &#9642;
top: -2px;
}
}
}
ol {
counter-reset: ol-counter;
li {
&:before {
content: counter(ol-counter) '.';
counter-increment: ol-counter;
font-weight: $font-weight-bold;
top: -1px;
}
}
ul li:before {
content: ' \25AA';
}
}
ul,
ol {
margin-top: 0;
margin-bottom: $spacer;
padding-left: $spacer / $line-height;
list-style: none;
li {
position: relative;
display: block;
&:before {
position: absolute;
left: -($spacer / $line-height);
color: $brand-grey-light;
user-select: none;
}
+ li {
margin-top: $spacer / 8;
}
ul,
ol,
p {
margin-bottom: 0;
margin-top: $spacer / 8;
}
}
}
// Inline typography
/////////////////////////////////////
b,
strong,
.bold {
font-weight: $font-weight-bold;
}
em,
.italic {
font-style: italic;
}
small {
font-size: $font-size-small;
display: inline-block;
}
abbr[title],
dfn {
text-transform: none;
font-style: normal;
font-size: inherit;
border-bottom: 1px dashed $brand-grey-light;
cursor: help;
font-feature-settings: inherit;
}
h1,
h2,
h3,
h4,
h5 {
font-family: $font-family-title;
color: inherit;
line-height: 1.2;
font-weight: $font-weight-bold;
}
h1 {
font-size: $font-size-h1;
}
h2 {
font-size: $font-size-h2;
}
h3 {
font-size: $font-size-h3;
}
h4 {
font-size: $font-size-h4;
}
h5 {
font-size: $font-size-h5;
}
// Responsive Media
/////////////////////////////////////
figure,
img,
svg,
video,
audio,
embed,
canvas,
picture {
max-width: 100%;
height: auto;
margin: 0 auto;
display: block;
}
hr {
margin: $spacer 0;
border: 0;
border-bottom: 0.1rem solid $brand-grey-lighter;
}
// Quotes
/////////////////////////////////////
q {
font-style: italic;
}
cite {
font-style: normal;
text-transform: uppercase;
}
blockquote,
blockquote > p {
font-style: italic;
color: lighten($brand-grey, 15%);
}
blockquote {
margin: 0 0 $spacer;
padding-left: $spacer / 2;
border-left: 0.2rem solid $brand-grey-lighter;
@media screen and (min-width: $break-point--small) {
padding-left: $spacer / $line-height;
}
}
// Tables
/////////////////////////////////////
table {
width: 100%;
margin-bottom: $spacer * $line-height;
border-collapse: collapse;
th,
td {
border: 0;
margin: 0;
padding: $spacer / 2;
border-bottom: 1px solid $brand-grey-lighter;
text-align: left;
font-size: 90%;
&[align='center'] {
text-align: center;
}
&[align='right'] {
text-align: right;
}
}
th {
font-weight: 600;
}
}
// Code
/////////////////////////////////////
code,
kbd,
pre,
samp {
font-family: $font-family-monospace;
font-size: $font-size-small;
border-radius: $border-radius;
text-shadow: none;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
h1 &,
h2 &,
h3 &,
h4 &,
h5 & {
font-size: inherit;
}
}
:not(pre) > code {
color: inherit;
display: inline-block;
}
a > code {
color: $brand-pink;
}
pre {
display: block;
margin-bottom: $spacer;
padding: 0;
background: lighten($brand-grey-lighter, 5%);
// make 'em scrollable
overflow: auto;
-webkit-overflow-scrolling: touch;
code {
padding: $spacer;
white-space: pre;
display: block;
color: $brand-grey-lighter;
overflow-wrap: normal;
word-wrap: normal;
word-break: normal;
overflow: auto;
}
}
// Selection
/////////////////////////////////////
::-moz-selection {
background: $brand-grey-light;
color: #fff;
}
::selection {
background: $brand-grey-light;
color: #fff;
}

25
tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"jsx": "preserve",
"lib": ["dom", "es2017"],
"module": "esnext",
"moduleResolution": "node",
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"preserveConstEnums": true,
"removeComments": false,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"target": "esnext",
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true
},
"exclude": ["node_modules", ".next"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}