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

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"]
}