mirror of
https://github.com/oceanprotocol/status
synced 2024-11-22 01:36:59 +01:00
migrate to Next.js
This commit is contained in:
parent
7b54275822
commit
fa51b4ac1a
37
.eslintrc
37
.eslintrc
@ -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
6
.gitignore
vendored
@ -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
|
@ -1,17 +1,16 @@
|
||||
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"
|
||||
- './cc-test-reporter before-build'
|
||||
|
||||
script:
|
||||
# will run `npm ci` automatically here
|
||||
- npm test
|
||||
- "./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT"
|
||||
- './cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT'
|
||||
- npm run build
|
||||
|
||||
notifications:
|
||||
|
18
jest.config.js
Normal file
18
jest.config.js
Normal 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
12
jest.tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react",
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"noImplicitAny": true,
|
||||
"sourceMap": true,
|
||||
"target": "es5"
|
||||
}
|
||||
}
|
1
jest/__mocks__/fileMock.js
Normal file
1
jest/__mocks__/fileMock.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = 'test-file-stub'
|
1
jest/__mocks__/styleMock.js
Normal file
1
jest/__mocks__/styleMock.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = {}
|
1
jest/__mocks__/svgrMock.js
Normal file
1
jest/__mocks__/svgrMock.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = 'svg'
|
2
next-env.d.ts
vendored
Normal file
2
next-env.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
41
next.config.js
Normal file
41
next.config.js
Normal 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]'
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
14578
package-lock.json
generated
14578
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@ -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
8
postcss.config.js
Normal file
@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-env': {
|
||||
importFrom: './src/styles/_variables.css'
|
||||
},
|
||||
cssnano: {}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
5
site.config.js
Normal file
5
site.config.js
Normal 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
13
src/@types/global.d.ts
vendored
Normal 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
|
||||
}
|
31
src/App.js
31
src/App.js
@ -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>
|
||||
)
|
||||
}
|
@ -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
14
src/Layout.module.css
Normal 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
44
src/Layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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)
|
||||
})
|
||||
})
|
@ -1,2 +0,0 @@
|
||||
import mockAxios from 'jest-mock-axios'
|
||||
export default mockAxios
|
10
src/__tests__/Layout.test.tsx
Normal file
10
src/__tests__/Layout.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
68
src/__tests__/Network.test.tsx
Normal file
68
src/__tests__/Network.test.tsx
Normal 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)
|
||||
})
|
||||
})
|
10
src/__tests__/Spinner.test.tsx
Normal file
10
src/__tests__/Spinner.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
@ -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()
|
||||
})
|
||||
})
|
30
src/components/Network/Data.module.css
Normal file
30
src/components/Network/Data.module.css
Normal 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);
|
||||
}
|
89
src/components/Network/Data.tsx
Normal file
89
src/components/Network/Data.tsx
Normal 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>
|
||||
)
|
||||
}
|
32
src/components/Network/index.module.css
Normal file
32
src/components/Network/index.module.css
Normal 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);
|
||||
}
|
32
src/components/Network/index.tsx
Normal file
32
src/components/Network/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
33
src/components/Spinner.module.css
Normal file
33
src/components/Spinner.module.css
Normal 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);
|
||||
}
|
||||
}
|
17
src/components/Spinner.tsx
Normal file
17
src/components/Spinner.tsx
Normal 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
|
19
src/index.js
19
src/index.js
@ -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()
|
@ -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
12
src/pages/_app.tsx
Normal 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} />
|
||||
}
|
||||
}
|
33
src/pages/index.module.css
Normal file
33
src/pages/index.module.css
Normal 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
31
src/pages/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -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
|
@ -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
51
src/styles/_variables.css
Normal 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;
|
||||
}
|
@ -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
126
src/styles/global.css
Normal 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;
|
||||
}
|
@ -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: ▪ ▪
|
||||
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
25
tsconfig.json
Normal 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"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user