admin app with retire button

This commit is contained in:
Jernej Pregelj 2019-07-05 10:58:12 +02:00
parent 4c744d1ea9
commit bf7fb99df1
188 changed files with 24716 additions and 0 deletions

2
admin/.dockerignore Normal file
View File

@ -0,0 +1,2 @@
node_modules
.env.local

54
admin/.env.local.example Normal file
View File

@ -0,0 +1,54 @@
#
# When none of the following variables are set,
# Commons will default connecting to Nile
#
#
# Connect to Pacific
#
# REACT_APP_NODE_URI="https://pacific.oceanprotocol.com"
# REACT_APP_AQUARIUS_URI="https://aquarius.pacific.dev-ocean.com"
# REACT_APP_BRIZO_URI="https://brizo.pacific.dev-ocean.com"
# REACT_APP_SECRET_STORE_URI="https://secret-store.pacific.oceanprotocol.com"
# REACT_APP_FAUCET_URI="https://faucet.pacific.dev-ocean.com"
# REACT_APP_BRIZO_ADDRESS="0x008c25ed3594e094db4592f4115d5fa74c4f41ea"
#
# Connect to Nile
#
REACT_APP_NODE_URI="https://nile.dev-ocean.com"
REACT_APP_AQUARIUS_URI="https://nginx-aquarius.dev-ocean.com"
REACT_APP_BRIZO_URI="https://nginx-brizo.dev-ocean.com"
REACT_APP_SECRET_STORE_URI="https://secret-store.dev-ocean.com"
REACT_APP_FAUCET_URI="https://faucet.nile.dev-ocean.com"
REACT_APP_BRIZO_ADDRESS="0x4aaab179035dc57b35e2ce066919048686f82972"
#
# Connect to Duero
#
# REACT_APP_NODE_URI="https://duero.dev-ocean.com"
# REACT_APP_AQUARIUS_URI="https://aquarius.duero.dev-ocean.com"
# REACT_APP_BRIZO_URI="https://brizo.duero.dev-ocean.com"
# REACT_APP_SECRET_STORE_URI="https://secret-store.duero.dev-ocean.com"
# REACT_APP_FAUCET_URI="https://faucet.duero.dev-ocean.com"
# REACT_APP_BRIZO_ADDRESS="0x9d4ed58293f71122ad6a733c1603927a150735d0"
#
# Connect to Nile Commons instances
#
# REACT_APP_NODE_URI="https://nile.dev-ocean.com"
# REACT_APP_AQUARIUS_URI="https://aquarius.marketplace.dev-ocean.com"
# REACT_APP_BRIZO_URI="https://brizo.marketplace.dev-ocean.com"
# REACT_APP_SECRET_STORE_URI="https://secret-store.dev-ocean.com"
# REACT_APP_FAUCET_URI="https://faucet.nile.dev-ocean.com"
# REACT_APP_BRIZO_ADDRESS="0x4aaab179035dc57b35e2ce066919048686f82972"
#
# Connect to Spree (local with Barge)
#
# REACT_APP_NODE_URI="htts://localhost:8545"
# REACT_APP_AQUARIUS_URI="http://aquarius:5000"
# REACT_APP_BRIZO_URI="http://localhost:8030"
# REACT_APP_SECRET_STORE_URI="http://localhost:12001"
# REACT_APP_FAUCET_URI="http://localhost:3001"
# REACT_APP_BRIZO_ADDRESS="0x00bd138abd70e2f00903268f3db08f2d25677c9e"

25
admin/Dockerfile Normal file
View File

@ -0,0 +1,25 @@
FROM node:11-alpine
LABEL maintainer="Ocean Protocol <devops@oceanprotocol.com>"
RUN apk add --no-cache --update\
bash\
g++\
gcc\
git\
gettext\
make\
python
COPY . /app/admin
WORKDIR /app/admin
RUN npm install -g npm serve
RUN npm install
RUN npm run build
# Default ENV values
ENV LISTEN_ADDRESS='0.0.0.0'
ENV LISTEN_PORT='3000'
ENTRYPOINT ["/app/admin/scripts/docker-entrypoint.sh"]

2
admin/__mocks__/axios.js Normal file
View File

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

View File

@ -0,0 +1,66 @@
const oceanMock = {
ocean: {
accounts: {
list: () => ['xxx', 'xxx']
},
aquarius: {
queryMetadata: () => {
return {
results: [],
totalResults: 1,
totalPages: 1
}
}
},
assets: {
resolve: jest.fn(),
order: () => {
return {
next: jest.fn()
}
},
consume: jest.fn()
},
keeper: {
conditions: {
accessSecretStoreCondition: {
getGrantedDidByConsumer: () => {
return {
find: jest.fn()
}
}
}
}
},
versions: {
get: jest.fn(() =>
Promise.resolve({
squid: {
name: 'Squid-js',
status: 'Working'
},
aquarius: {
name: 'Aquarius',
status: 'Working'
},
brizo: {
name: 'Brizo',
network: 'Nile',
status: 'Working',
contracts: {
hello: 'hello',
hello2: 'hello2'
}
},
status: {
ok: true,
network: true,
contracts: true
}
})
)
}
}
}
export default oceanMock

View File

@ -0,0 +1,33 @@
import oceanMock from './ocean-mock'
const userMock = {
isLogged: false,
isLoading: false,
isWeb3: false,
isOceanNetwork: false,
account: '',
web3: {},
...oceanMock,
balance: { eth: 0, ocn: 0 },
network: '',
requestFromFaucet: jest.fn(),
unlockAccounts: jest.fn(),
message: ''
}
const userMockConnected = {
isLogged: true,
isLoading: false,
isWeb3: true,
isOceanNetwork: true,
account: '0xxxxxx',
web3: {},
...oceanMock,
balance: { eth: 0, ocn: 0 },
network: '',
requestFromFaucet: jest.fn(),
unlockAccounts: jest.fn(),
message: ''
}
export { userMock, userMockConnected }

17250
admin/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

82
admin/package.json Normal file
View File

@ -0,0 +1,82 @@
{
"name": "commons-admin",
"description": "Ocean Protocol marketplace admin to manage open data sets.",
"version": "0.5.4",
"license": "Apache-2.0",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts --max_old_space_size=4096 build",
"test": "react-scripts test --coverage --watchAll=false",
"test:watch": "react-scripts test --coverage",
"eject": "react-scripts eject"
},
"dependencies": {
"@oceanprotocol/art": "^2.2.0",
"@oceanprotocol/squid": "0.6.2",
"@oceanprotocol/typographies": "^0.1.0",
"@sindresorhus/slugify": "^0.9.1",
"axios": "^0.19.0",
"classnames": "^2.2.6",
"eslint-plugin-prettier": "^3.1.0",
"ethereum-blockies": "github:MyEtherWallet/blockies",
"filesize": "^4.1.2",
"history": "^4.9.0",
"is-url": "^1.2.4",
"moment": "^2.24.0",
"query-string": "^6.8.1",
"react": "^16.8.6",
"react-collapsed": "^2.0.1",
"react-datepicker": "^2.7.0",
"react-dom": "^16.8.6",
"react-dotdotdot": "^1.3.0",
"react-ga": "^2.6.0",
"react-helmet": "^5.2.1",
"react-markdown": "^4.1.0",
"react-moment": "^0.9.2",
"react-paginate": "^6.3.0",
"react-popper": "^1.3.3",
"react-router-dom": "^5.0.1",
"react-transition-group": "^4.1.1",
"web3": "1.0.0-beta.37"
},
"devDependencies": {
"@react-mock/state": "^0.1.8",
"@testing-library/react": "^8.0.4",
"@types/classnames": "^2.2.7",
"@types/filesize": "^4.1.0",
"@types/is-url": "^1.2.28",
"@types/jest": "^24.0.15",
"@types/react": "^16.8.22",
"@types/react-datepicker": "^2.3.0",
"@types/react-dom": "^16.8.4",
"@types/react-dotdotdot": "^1.2.0",
"@types/react-helmet": "^5.0.8",
"@types/react-paginate": "^6.2.1",
"@types/react-router-dom": "^4.3.4",
"@types/react-transition-group": "^2.9.2",
"@types/web3": "^1.0.19",
"jest-dom": "^3.5.0",
"jest-mock-axios": "^3.0.0",
"node-sass": "^4.12.0",
"react-scripts": "^3.0.0",
"stylelint-config-bigchaindb": "^1.2.2",
"typescript": "3.4.5"
},
"repository": {
"type": "git",
"url": "https://github.com/oceanprotocol/commons"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
],
"jest": {
"collectCoverageFrom": [
"src/**/*.{ts,tsx}",
"!src/serviceWorker.ts",
"!src/**/*.d.ts"
]
}
}

BIN
admin/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

49
admin/public/index.html Normal file
View File

@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name="theme-color" content="#141414" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Commons</title>
<style>
.loader {
display: block;
position: relative;
margin-top: calc(50vh - 15px);
}
.loader:before {
content: '';
box-sizing: border-box;
position: absolute;
top: -80%;
left: 50%;
width: 20px;
height: 20px;
margin-top: -10px;
margin-left: -10px;
border-radius: 50%;
border: 2px solid #7b1173;
border-top-color: #e000cf;
animation: spinner 0.6s linear infinite;
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"><span class="loader"></span></div>
</body>
</html>

View File

@ -0,0 +1,25 @@
{
"short_name": "Commons",
"name": "Commons Marketplace",
"icons": [
{
"src": "icons/favicon_512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/favicon_256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "icons/favicon_128.png",
"sizes": "128x128",
"type": "image/png"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#141414",
"background_color": "#ffffff"
}

2
admin/public/robots.txt Normal file
View File

@ -0,0 +1,2 @@
User-agent: *
Disallow: /search

BIN
admin/public/share.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

View File

@ -0,0 +1,11 @@
#!/bin/sh
if [ "${LOCAL_CONTRACTS}" = "true" ]; then
echo "Waiting for contracts to be generated..."
while [ ! -f "/app/frontend/node_modules/@oceanprotocol/keeper-contracts/artifacts/ready" ]; do
sleep 2
done
fi
npm run build
echo "Starting Commons..."
serve -l tcp://"${LISTEN_ADDRESS}":"${LISTEN_PORT}" -s /app/frontend/build/

View File

@ -0,0 +1,5 @@
/// <reference types="node" />
declare module 'ethereum-blockies' {
export function toDataUrl(address: string): string
}

View File

@ -0,0 +1 @@
declare module 'react-collapsed'

27
admin/src/App.module.scss Normal file
View File

@ -0,0 +1,27 @@
@import './styles/variables';
.app {
height: 100%;
// for sticky footer
display: flex;
min-height: calc(100vh - #{$page-frame * 2});
flex-direction: column;
}
.main {
flex: 1;
}
.loader {
text-align: center;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
margin-top: 25vh;
> div {
width: 100%;
}
}

29
admin/src/App.test.tsx Normal file
View File

@ -0,0 +1,29 @@
import React from 'react'
import { render } from '@testing-library/react'
import App from './App'
import { User } from './context'
import { userMock, userMockConnected } from '../__mocks__/user-mock'
describe('App', () => {
it('should be able to run tests', () => {
expect(1 + 2).toEqual(3)
})
it('renders without crashing', () => {
const { container } = render(
<User.Provider value={userMockConnected}>
<App />
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()
})
it('renders loading state', () => {
const { container } = render(
<User.Provider value={{ ...userMock, isLoading: true }}>
<App />
</User.Provider>
)
expect(container.querySelector('.spinner')).toBeInTheDocument()
})
})

37
admin/src/App.tsx Normal file
View File

@ -0,0 +1,37 @@
import React, { Component } from 'react'
import { BrowserRouter as Router } from 'react-router-dom'
import Header from './components/organisms/Header'
import Footer from './components/organisms/Footer'
import Spinner from './components/atoms/Spinner'
import { User } from './context'
import Routes from './Routes'
import './styles/global.scss'
import styles from './App.module.scss'
export default class App extends Component {
public render() {
return (
<div className={styles.app}>
<Router>
<>
<Header />
<main className={styles.main}>
{this.context.isLoading ? (
<div className={styles.loader}>
<Spinner message={this.context.message} />
</div>
) : (
<Routes />
)}
</main>
<Footer />
</>
</Router>
</div>
)
}
}
App.contextType = User

19
admin/src/Routes.test.tsx Normal file
View File

@ -0,0 +1,19 @@
import React from 'react'
import { BrowserRouter as Router } from 'react-router-dom'
import { render } from '@testing-library/react'
import Routes from './Routes'
import { User } from './context'
import { userMockConnected } from '../__mocks__/user-mock'
describe('Routes', () => {
it('renders without crashing', () => {
const { container } = render(
<User.Provider value={userMockConnected}>
<Router>
<Routes />
</Router>
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()
})
})

21
admin/src/Routes.tsx Normal file
View File

@ -0,0 +1,21 @@
import React from 'react'
import { Route, Switch } from 'react-router-dom'
import Home from './routes/Home'
import NotFound from './routes/NotFound'
import Search from './routes/Search'
import About from './routes/About'
import Asset from './components/templates/Asset'
const Routes = () => (
<Switch>
<Route component={Home} exact path="/" />
<Route component={Search} path="/search" />
<Route component={Asset} path="/asset/:did" />
<Route component={About} path="/about" />
<Route component={NotFound} />
</Switch>
)
export default Routes

View File

@ -0,0 +1,24 @@
@import '../../styles/variables';
.account {
display: flex;
align-items: center;
text-align: left;
> div {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family: $font-family-monospace;
font-size: $font-size-small;
}
}
.blockies {
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
display: inline-block;
margin-right: $spacer / 3;
margin-left: 0;
}

View File

@ -0,0 +1,28 @@
import React from 'react'
import { render } from '@testing-library/react'
import { toDataUrl } from 'ethereum-blockies'
import Account from './Account'
describe('Account', () => {
it('renders without crashing', () => {
const { container } = render(<Account account={'0xxxxxxxxxxxxxxx'} />)
expect(container.firstChild).toBeInTheDocument()
})
it('outputs empty state without account', () => {
const { container } = render(<Account account={''} />)
expect(container.firstChild).toHaveTextContent('No account selected')
})
it('outputs blockie img', () => {
const account = '0xxxxxxxxxxxxxxx'
const blockies = toDataUrl(account)
const { container } = render(<Account account={account} />)
expect(container.querySelector('.blockies')).toBeInTheDocument()
expect(container.querySelector('.blockies')).toHaveAttribute(
'src',
blockies
)
})
})

View File

@ -0,0 +1,19 @@
import React from 'react'
import Dotdotdot from 'react-dotdotdot'
import { toDataUrl } from 'ethereum-blockies'
import styles from './Account.module.scss'
const Account = ({ account }: { account: string }) => {
const blockies = account && toDataUrl(account)
return account && blockies ? (
<div className={styles.account}>
<img className={styles.blockies} src={blockies} alt="Blockies" />
<Dotdotdot clamp={2}>{account}</Dotdotdot>
</div>
) : (
<em>No account selected</em>
)
}
export default Account

View File

@ -0,0 +1,73 @@
@import '../../styles/variables';
.button {
border: 0;
cursor: pointer;
outline: 0;
margin: 0;
display: inline-block;
width: fit-content;
padding: $spacer / 4 $spacer;
font-size: $font-size-base;
font-family: $font-family-base;
font-weight: $font-weight-bold;
text-transform: uppercase;
border-radius: 2px;
transition: .2s ease-out;
color: $brand-white;
background: $brand-grey-light;
box-shadow: 0 9px 18px 0 rgba(0, 0, 0, .1);
min-height: 45px;
user-select: none;
&:hover,
&:focus {
color: $brand-white;
background: $brand-grey-light;
text-decoration: none;
transform: translate3d(0, -.05rem, 0);
box-shadow: 0 12px 30px 0 rgba(0, 0, 0, .1);
}
&:active {
background: $brand-grey-light;
transition: none;
transform: none;
box-shadow: 0 5px 18px 0 rgba(0, 0, 0, .1);
}
&:disabled {
cursor: not-allowed;
pointer-events: none;
opacity: .5;
}
}
.buttonPrimary {
composes: button;
background: $brand-gradient;
&:hover,
&:focus {
background: $brand-gradient;
}
&:active {
background: $brand-gradient;
}
}
.link {
border: 0;
outline: 0;
display: inline-block;
width: fit-content;
background: 0;
padding: 0;
color: $brand-pink;
font-size: $font-size-base;
font-weight: $font-weight-base;
font-family: inherit;
box-shadow: none;
cursor: pointer;
}

View File

@ -0,0 +1,54 @@
import React from 'react'
import { render } from '@testing-library/react'
import { BrowserRouter as Router } from 'react-router-dom'
import Button from './Button'
describe('Button', () => {
it('default renders correctly without crashing', () => {
const { getByTestId } = render(
<Button data-testid="button-default">I am a default button</Button>
)
expect(getByTestId('button-default')).toHaveTextContent('default')
})
it('primary renders correctly without crashing', () => {
const { getByTestId } = render(
<Button data-testid="button-primary" primary>
I am a primary button
</Button>
)
expect(getByTestId('button-primary')).toHaveTextContent('primary')
expect(getByTestId('button-primary').className).toMatch(/buttonPrimary/)
})
it('Link renders correctly without crashing', () => {
const { getByTestId } = render(
<Router>
<Button data-testid="button-to" to="https://hello.com">
I am a Link button
</Button>
</Router>
)
expect(getByTestId('button-to')).toHaveTextContent('Link')
})
it('href renders correctly without crashing', () => {
const { getByTestId } = render(
<Button data-testid="button-href" href="https://hello.com">
I am a href button
</Button>
)
expect(getByTestId('button-href')).toHaveTextContent('href')
expect(getByTestId('button-href').nodeName).toBe('A')
})
it('link renders correctly without crashing', () => {
const { getByTestId } = render(
<Button data-testid="button-link" link>
I am a link button
</Button>
)
expect(getByTestId('button-link')).toHaveTextContent('link')
expect(getByTestId('button-link').className).toMatch(/link/)
})
})

View File

@ -0,0 +1,60 @@
import React, { PureComponent } from 'react'
import { Link } from 'react-router-dom'
import cx from 'classnames'
import styles from './Button.module.scss'
interface ButtonProps {
children: string
className?: string
primary?: boolean
link?: boolean
href?: string
onClick?: any
disabled?: boolean
to?: string
}
export default class Button extends PureComponent<ButtonProps, any> {
public render() {
let classes
const {
primary,
link,
href,
children,
className,
to,
...props
} = this.props
if (primary) {
classes = styles.buttonPrimary
} else if (link) {
classes = styles.link
} else {
classes = styles.button
}
if (to) {
return (
<Link to={to} className={cx(classes, className)} {...props}>
{children}
</Link>
)
}
if (href) {
return (
<a href={href} className={cx(classes, className)} {...props}>
{children}
</a>
)
}
return (
<button className={cx(classes, className)} {...props}>
{children}
</button>
)
}
}

View File

@ -0,0 +1,25 @@
@import '../../styles/variables';
.categoryImage {
height: 4rem;
background-size: 100%;
background-position: center;
margin-bottom: $spacer / $line-height;
background-color: $body-background;
border-radius: $border-radius;
overflow: hidden;
opacity: .85;
transition: .2s ease-out;
border: 1px solid $brand-grey-lighter;
}
.header {
composes: categoryImage;
height: 8rem;
margin-top: $spacer / $line-height;
}
.dimmed {
composes: categoryImage;
opacity: .6;
}

View File

@ -0,0 +1,25 @@
import React from 'react'
import { render } from '@testing-library/react'
import CategoryImage from './CategoryImage'
import formPublish from '../../data/form-publish.json'
describe('CategoryImage', () => {
it('renders fallback image', () => {
const { container } = render(<CategoryImage category={''} />)
expect(container.firstChild).toBeInTheDocument()
expect(container.firstChild.style.backgroundImage).toMatch(
/jellyfish-back/
)
})
it('renders all the category images', () => {
const { options } = formPublish.steps[1].fields
? formPublish.steps[1].fields.categories
: []
options.map((category: string) => {
const { container } = render(<CategoryImage category={category} />)
expect(container.firstChild).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,168 @@
import React, { PureComponent } from 'react'
import cx from 'classnames'
import styles from './CategoryImage.module.scss'
import agriculture from '../../img/categories/agriculture.jpg'
import anthroarche from '../../img/categories/anthroarche.jpg'
import astronomy from '../../img/categories/astronomy.jpg'
import biology from '../../img/categories/biology.jpg'
import business from '../../img/categories/business.jpg'
import chemistry from '../../img/categories/chemistry.jpg'
import communication from '../../img/categories/communication.jpg'
import computer from '../../img/categories/computer.jpg'
import dataofdata from '../../img/categories/dataofdata.jpg'
import deeplearning from '../../img/categories/deeplearning.jpg'
import demographics from '../../img/categories/demographics.jpg'
import earth from '../../img/categories/earth.jpg'
import economics from '../../img/categories/economics.jpg'
import engineering from '../../img/categories/engineering.jpg'
import history from '../../img/categories/history.jpg'
import imagesets from '../../img/categories/imagesets.jpg'
import language from '../../img/categories/language.jpg'
import law from '../../img/categories/law.jpg'
import mathematics from '../../img/categories/mathematics.jpg'
import medicine from '../../img/categories/medicine.jpg'
import other from '../../img/categories/other.jpg'
import performingarts from '../../img/categories/performingarts.jpg'
import philosophy from '../../img/categories/philosophy.jpg'
import physics from '../../img/categories/physics.jpg'
import politics from '../../img/categories/politics.jpg'
import psychology from '../../img/categories/psychology.jpg'
import sociology from '../../img/categories/sociology.jpg'
import sports from '../../img/categories/sports.jpg'
import theology from '../../img/categories/theology.jpg'
import transport from '../../img/categories/transport.jpg'
import urbanplanning from '../../img/categories/urbanplanning.jpg'
import visualart from '../../img/categories/visualart.jpg'
import aiforgood from '../../img/aiforgood.jpg'
import fallback from '@oceanprotocol/art/jellyfish/jellyfish-back.svg'
const categoryImageFile = (category: string) => {
switch (category) {
case 'Agriculture & Bio Engineering':
case 'agriculture':
return agriculture
case 'Anthropology & Archeology':
case 'anthroarche':
return anthroarche
case 'Space & Astronomy':
case 'astronomy':
return astronomy
case 'Biology':
case 'biology':
return biology
case 'Business & Management':
case 'business':
return business
case 'Chemistry':
case 'chemistry':
return chemistry
case 'Communication & Journalism':
case 'communication':
return communication
case 'Computer Technology':
case 'computer':
return computer
case 'Dataset Of Datasets':
case 'dataofdata':
return dataofdata
case 'Deep Learning':
case 'deeplearning':
return deeplearning
case 'Demography':
case 'demographics':
return demographics
case 'Earth & Climate':
case 'earth':
return earth
case 'Economics & Finance':
case 'economics-and-finance':
return economics
case 'Engineering':
case 'engineering':
return engineering
case 'History':
case 'history':
return history
case 'Image Recognition':
case 'imagesets':
return imagesets
case 'Language':
case 'language':
return language
case 'Law':
case 'law':
return law
case 'Mathematics':
case 'mathematics':
return mathematics
case 'Medicine':
case 'Health & Medicine':
case 'Health':
case 'medicine':
return medicine
case 'Other':
case 'other':
return other
case 'Performing Arts':
case 'performingarts':
return performingarts
case 'Philosophy':
case 'philosophy':
return philosophy
case 'Physics & Energy':
case 'physics':
return physics
case 'Politics':
case 'politics':
return politics
case 'Psychology':
case 'psychology':
return psychology
case 'Sociology':
case 'sociology':
return sociology
case 'Sports & Recreation':
case 'sports':
return sports
case 'Theology':
case 'theology':
return theology
case 'Transportation':
case 'transport':
return transport
case 'Urban Planning':
case 'urbanplanning':
return urbanplanning
case 'Visual Arts & Design':
case 'visualart':
return visualart
// technically no category
// but corresponding to title of a channel
case 'AI For Good':
return aiforgood
default:
return fallback
}
}
export default class CategoryImage extends PureComponent<{
category: string
header?: boolean
dimmed?: boolean
}> {
public render() {
const image = categoryImageFile(this.props.category)
const classNames = cx(styles.categoryImage, {
[styles.header]: this.props.header,
[styles.dimmed]: this.props.dimmed
})
return (
<div
className={classNames}
style={{ backgroundImage: `url(${image})` }}
/>
)
}
}

View File

@ -0,0 +1,23 @@
import React from 'react'
import { Link } from 'react-router-dom'
const CategoryLink = ({
category,
children,
className,
...props
}: {
category: string
children?: any
className?: string
}) => (
<Link
to={`/search?categories=${encodeURIComponent(category)}`}
className={className}
{...props}
>
{children || category}
</Link>
)
export default CategoryLink

View File

@ -0,0 +1,16 @@
@import '../../styles/variables';
.content {
padding: 0 $spacer / 1.5;
max-width: 47rem;
margin: 0 auto;
@media (min-width: $break-point--small) {
padding: 0 $spacer;
}
}
.wide {
composes: content;
max-width: $break-point--large;
}

View File

@ -0,0 +1,8 @@
import React from 'react'
import styles from './Content.module.scss'
const Content = ({ wide, children }: { wide?: boolean; children: any }) => (
<div className={wide ? styles.wide : styles.content}>{children}</div>
)
export default Content

View File

@ -0,0 +1,39 @@
@import '../../../styles/variables';
.form {
width: 100%;
background: $brand-white;
padding: $spacer / 1.5;
border: 1px solid $brand-grey-lighter;
border-radius: $border-radius;
@media (min-width: $break-point--small) {
padding: $spacer;
}
fieldset {
border: 0;
padding: 0;
}
}
.formMinimal {
composes: form;
background: none;
padding: 0;
border: 0;
}
.formHeader {
margin-bottom: $spacer;
}
.formTitle {
font-size: $font-size-h2;
margin: 0;
}
.formDescription {
margin-bottom: 0;
margin-top: $spacer / 2;
}

View File

@ -0,0 +1,29 @@
import React from 'react'
import { render } from '@testing-library/react'
import Form from './Form'
describe('Form', () => {
it('renders without crashing', () => {
const { container } = render(<Form>Hello</Form>)
expect(container.firstChild).toBeInTheDocument()
})
it('renders title & description when set', () => {
const { container } = render(
<Form title="Hello Title" description="Hello Description">
Hello
</Form>
)
expect(container.querySelector('.formTitle')).toHaveTextContent(
'Hello Title'
)
expect(container.querySelector('.formDescription')).toHaveTextContent(
'Hello Description'
)
})
it('can switch to minimal', () => {
const { container } = render(<Form minimal>Hello</Form>)
expect(container.firstChild).toHaveClass('formMinimal')
})
})

View File

@ -0,0 +1,36 @@
import React from 'react'
import styles from './Form.module.scss'
const Form = ({
title,
description,
children,
onSubmit,
minimal,
...props
}: {
title?: string
description?: string
children: any
onSubmit?: any
minimal?: boolean
}) => (
<form
className={minimal ? styles.formMinimal : styles.form}
onSubmit={onSubmit}
{...props}
>
{title && (
<header className={styles.formHeader}>
<h1 className={styles.formTitle}>{title}</h1>
{description && (
<p className={styles.formDescription}>{description}</p>
)}
</header>
)}
{children}
</form>
)
export default Form

View File

@ -0,0 +1,7 @@
@import '../../../styles/variables';
.help {
font-size: $font-size-small;
color: darken($brand-grey-light, 10%);
margin-top: $spacer / 4;
}

View File

@ -0,0 +1,13 @@
import React from 'react'
import ReactDOM from 'react-dom'
import Help from './Help'
it('FormHelp renders without crashing', () => {
const div = document.createElement('div')
ReactDOM.render(
<Help>Price of your data set asset in Ocean Tokens.</Help>,
div
)
ReactDOM.unmountComponentAtNode(div)
})

View File

@ -0,0 +1,8 @@
import React from 'react'
import styles from './Help.module.scss'
const FormHelp = ({ children }: { children: string }) => (
<div className={styles.help}>{children}</div>
)
export default FormHelp

View File

@ -0,0 +1,190 @@
@import '../../../styles/variables';
@import './InputDate.module.scss';
.inputWrap {
background: $brand-gradient;
border-radius: $border-radius;
padding: 2px;
display: flex;
position: relative;
&.isFocused {
background: $brand-black;
}
> div,
> div > div {
width: 100%;
}
}
.inputWrapSearch {
composes: inputWrap;
.input {
padding-left: $spacer * 1.5;
}
svg {
position: absolute;
left: $spacer / 2;
width: 1.25rem;
height: 1.25rem;
top: 50%;
margin-top: -.6rem;
fill: rgba($brand-grey-light, .7);
}
}
.input {
font-size: $font-size-base;
font-family: $font-family-base;
font-weight: $font-weight-bold;
color: $brand-black;
border: none;
box-shadow: none;
width: 100%;
background: $brand-white;
padding: $spacer / 3;
margin: 0;
border-radius: $border-radius;
transition: .2s ease-out;
min-height: 43px;
appearance: none;
&:focus {
border: none;
box-shadow: none;
outline: 0;
}
&::placeholder {
font-family: $font-family-base;
font-size: $font-size-base;
color: $brand-grey-light;
font-weight: $font-weight-base;
transition: .2s ease-out;
opacity: .7;
}
&[readonly],
&[disabled] {
background-color: $brand-grey-lighter;
cursor: not-allowed;
pointer-events: none;
}
// &::-webkit-credentials-auto-fill-button,
// &::-webkit-caps-lock-indicator {
// background: $brand-white;
// }
// &:-webkit-autofill,
// &:-webkit-autofill:hover,
// &:-webkit-autofill:focus {
// -webkit-text-fill-color: $brand-white;
// box-shadow: 0 0 0 1000px $brand-black inset;
// transition: background-color 5000s ease-in-out 0s;
// }
}
.select {
composes: input;
height: 43px;
padding-right: 3rem;
border: 0;
// custom arrow
// stylelint-disable
background-image: linear-gradient(45deg, transparent 50%, $brand-purple 50%),
linear-gradient(135deg, $brand-purple 50%, transparent 50%),
linear-gradient(
to right,
$brand-pink 1px,
lighten($brand-grey-lighter, 5%) 2px,
lighten($brand-grey-lighter, 5%)
);
background-position: calc(100% - 18px) calc(1rem + 5px),
calc(100% - 13px) calc(1rem + 5px), 100% 0;
background-size: 5px 5px, 5px 5px, 2.5rem 3rem;
// stylelint-enable
background-repeat: no-repeat;
&:focus {
outline: 0;
font-family: $font-family-base;
}
}
.radioGroup {
margin-top: $spacer / 2;
margin-bottom: -2%;
@media screen and (min-width: $break-point--small) {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
}
.radioWrap {
position: relative;
padding: $spacer / 2;
text-align: center;
display: flex;
align-items: center;
margin-bottom: 2%;
@media screen and (min-width: $break-point--small) {
flex: 0 0 49%;
}
}
.radio {
&:checked + label {
border-color: $brand-pink;
}
}
.radioLabel {
margin: 0;
padding: 0;
font-weight: $font-weight-bold;
font-size: $font-size-small;
line-height: 1.2;
border: 2px solid $brand-grey-lighter;
border-radius: .2rem;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
color: $brand-grey;
text-align: left;
padding-left: 2.5rem;
display: flex;
align-items: center;
}
// Size modifiers
.inputSmall {
composes: input;
font-size: $font-size-small;
min-height: 32px;
padding: $spacer / 4;
&::placeholder {
font-size: $font-size-small;
}
}
.selectSmall {
composes: select;
height: 32px;
padding-right: 2rem;
// custom arrow
background-position: calc(100% - 14px) 1rem, calc(100% - 9px) 1rem, 100% 0;
background-size: 5px 5px, 5px 5px, 2rem 3rem;
}

View File

@ -0,0 +1,90 @@
import React from 'react'
import { render } from '@testing-library/react'
import Input from './Input'
describe('Input', () => {
it('renders default without crashing', () => {
const { container } = render(<Input name="my-input" label="My Input" />)
expect(container.firstChild).toBeInTheDocument()
expect(container.querySelector('.label')).toHaveTextContent('My Input')
expect(container.querySelector('.input')).toHaveAttribute(
'id',
'my-input'
)
})
it('renders as text input by default', () => {
const { container } = render(<Input name="my-input" label="My Input" />)
expect(container.querySelector('.input')).toHaveAttribute(
'type',
'text'
)
})
it('renders search', () => {
const { container } = render(
<Input name="my-input" label="My Input" type="search" />
)
expect(container.querySelector('.input')).toHaveAttribute(
'type',
'search'
)
expect(container.querySelector('label + div')).toHaveClass(
'inputWrapSearch'
)
})
it('renders select', () => {
const { container } = render(
<Input
name="my-input"
label="My Input"
type="select"
options={['hello', 'hello2']}
/>
)
expect(container.querySelector('select')).toBeInTheDocument()
})
it('renders textarea', () => {
const { container } = render(
<Input name="my-input" label="My Input" type="textarea" rows={40} />
)
expect(container.querySelector('textarea')).toBeInTheDocument()
})
it('renders radios', () => {
const { container } = render(
<Input
name="my-input"
label="My Input"
type="radio"
options={['hello', 'hello2']}
/>
)
expect(container.querySelector('input[type=radio]')).toBeInTheDocument()
})
it('renders checkboxes', () => {
const { container } = render(
<Input
name="my-input"
label="My Input"
type="checkbox"
options={['hello', 'hello2']}
/>
)
expect(
container.querySelector('input[type=checkbox]')
).toBeInTheDocument()
})
it('renders date picker', () => {
const { container } = render(
<Input name="my-input" label="My Input" type="date" />
)
expect(
container.querySelector('.react-datepicker-wrapper')
).toBeInTheDocument()
})
})

View File

@ -0,0 +1,221 @@
import cx from 'classnames'
import React, { PureComponent, FormEvent, ChangeEvent } from 'react'
import slugify from '@sindresorhus/slugify'
import DatePicker from 'react-datepicker'
import { ReactComponent as SearchIcon } from '../../../img/search.svg'
import Help from './Help'
import Label from './Label'
import Row from './Row'
import InputGroup from './InputGroup'
import styles from './Input.module.scss'
interface InputProps {
name: string
label: string
placeholder?: string
required?: boolean
help?: string
tag?: string
type?: string
options?: string[]
additionalComponent?: any
value?: string
onChange?(
event:
| FormEvent<HTMLInputElement>
| ChangeEvent<HTMLInputElement>
| ChangeEvent<HTMLSelectElement>
| ChangeEvent<HTMLTextAreaElement>
): void
rows?: number
group?: any
multiple?: boolean
}
interface InputState {
isFocused: boolean
dateCreated?: Date
}
export default class Input extends PureComponent<InputProps, InputState> {
public state: InputState = {
isFocused: false,
dateCreated: new Date()
}
public inputWrapClasses() {
if (this.props.type === 'search') {
return styles.inputWrapSearch
} else if (this.props.type === 'search' && this.state.isFocused) {
return cx(styles.inputWrapSearch, styles.isFocused)
} else if (this.state.isFocused && this.props.type !== 'search') {
return cx(styles.inputWrap, styles.isFocused)
} else {
return styles.inputWrap
}
}
public toggleFocus = () => {
this.setState({ isFocused: !this.state.isFocused })
}
private handleDateChange = (date: Date) => {
this.setState({ dateCreated: date })
const event = {
currentTarget: {
name: 'dateCreated',
value: date
}
}
this.props.onChange!(event as any) // eslint-disable-line @typescript-eslint/no-non-null-assertion
}
public InputComponent = () => {
const {
type,
options,
group,
name,
required,
onChange,
value
} = this.props
const wrapClass = this.inputWrapClasses()
switch (type) {
case 'select':
return (
<div className={wrapClass}>
<select
id={name}
className={styles.select}
name={name}
required={required}
onFocus={this.toggleFocus}
onBlur={this.toggleFocus}
onChange={onChange}
value={value}
>
<option value="">---</option>
{options &&
options
.sort((a, b) => a.localeCompare(b))
.map((option: string, index: number) => (
<option key={index} value={option}>
{option}
</option>
))}
</select>
</div>
)
case 'textarea':
return (
<div className={wrapClass}>
<textarea
id={name}
className={styles.input}
onFocus={this.toggleFocus}
onBlur={this.toggleFocus}
{...this.props}
/>
</div>
)
case 'radio':
case 'checkbox':
return (
<div className={styles.radioGroup}>
{options &&
options.map((option: string, index: number) => (
<div className={styles.radioWrap} key={index}>
<input
className={styles.radio}
id={slugify(option)}
type={type}
name={name}
value={slugify(option)}
/>
<label
className={styles.radioLabel}
htmlFor={slugify(option)}
>
{option}
</label>
</div>
))}
</div>
)
case 'date':
return (
<div className={wrapClass}>
<DatePicker
selected={this.state.dateCreated}
onChange={this.handleDateChange}
className={styles.input}
onFocus={this.toggleFocus}
onBlur={this.toggleFocus}
id={name}
name={name}
/>
</div>
)
default:
return (
<div className={wrapClass}>
{group ? (
<InputGroup>
<input
id={name}
type={type || 'text'}
className={styles.input}
onFocus={this.toggleFocus}
onBlur={this.toggleFocus}
{...this.props}
/>
{group}
</InputGroup>
) : (
<input
id={name}
type={type || 'text'}
className={styles.input}
onFocus={this.toggleFocus}
onBlur={this.toggleFocus}
{...this.props}
/>
)}
{type === 'search' && <SearchIcon />}
</div>
)
}
}
public render() {
const {
name,
label,
required,
help,
additionalComponent,
multiple
} = this.props
return (
<Row>
<Label htmlFor={name} required={required}>
{label}
</Label>
<this.InputComponent />
{help && <Help>{help}</Help>}
{multiple && 'hello'}
{additionalComponent && additionalComponent}
</Row>
)
}
}

View File

@ -0,0 +1,70 @@
@import '../../../styles/variables';
@import '../../../../node_modules/react-datepicker/dist/react-datepicker-cssmodules.css';
//
// Date picker
//
:global .react-datepicker {
font-family: inherit;
color: inherit;
border-color: $brand-black;
}
:global .react-datepicker__header {
background: $brand-black;
border-radius: 0;
.react-datepicker__day-name,
.react-datepicker__day,
.react-datepicker__time-name,
.react-datepicker__current-month,
.react-datepicker-time__header,
.react-datepicker-year-header {
color: $brand-white;
}
}
:global .react-datepicker__current-month,
:global .react-datepicker-time__header,
:global .react-datepicker-year-header,
:global .react-datepicker__day-name {
font-weight: $font-weight-bold;
}
:global .react-datepicker__month-container {
float: none;
}
:global .react-datepicker-popper {
max-width: 16rem;
}
:global .react-datepicker-popper[data-placement^='top'] .react-datepicker__triangle:before,
:global .react-datepicker__year-read-view--down-arrow:before,
:global .react-datepicker__month-read-view--down-arrow:before,
:global .react-datepicker__month-year-read-view--down-arrow:before {
border-top-color: $brand-black;
}
:global .react-datepicker__day--selected,
:global .react-datepicker__day--in-selecting-range,
:global .react-datepicker__day--in-range,
:global .react-datepicker__month-text--selected,
:global .react-datepicker__month-text--in-selecting-range,
:global .react-datepicker__month-text--in-range {
background-color: $brand-black;
border-radius: 50%;
font-weight: $font-weight-bold;
}
:global .react-datepicker__day:hover,
:global .react-datepicker__month-text:hover {
background-color: $brand-pink;
border-radius: 50%;
font-weight: $font-weight-bold;
color: $brand-white;
}
:global .react-datepicker__day--outside-month {
color: $brand-grey-light;
}

View File

@ -0,0 +1,39 @@
@import '../../../styles/variables';
.inputGroup {
width: 100%;
@media screen and (min-width: $break-point--small) {
display: flex;
}
> input {
@media screen and (min-width: $break-point--small) {
width: 75%;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
> button {
width: 100%;
position: absolute;
left: 0;
bottom: -120%;
@media screen and (min-width: $break-point--small) {
position: relative;
bottom: auto;
width: 25%;
height: 100%;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
box-shadow: none;
}
&:hover,
&:focus {
transform: none;
}
}
}

View File

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

View File

@ -0,0 +1,8 @@
import React from 'react'
import styles from './InputGroup.module.scss'
const InputGroup = ({ children }: { children: any }) => (
<div className={styles.inputGroup}>{children}</div>
)
export default InputGroup

View File

@ -0,0 +1,22 @@
@import '../../../styles/variables';
.label {
color: $brand-grey;
font-size: $font-size-base;
font-family: $font-family-title;
line-height: 1.2;
display: block;
margin-bottom: $spacer / 6;
}
.required {
composes: label;
&:after {
content: '*';
font-size: $font-size-base;
color: $brand-grey-light;
display: inline-block;
margin-left: .1rem;
}
}

View File

@ -0,0 +1,20 @@
import React from 'react'
import { render } from '@testing-library/react'
import Label from './Label'
describe('Label', () => {
it('renders without crashing', () => {
const { container } = render(<Label htmlFor="hello">Hello</Label>)
expect(container.firstChild).toBeInTheDocument()
})
it('renders required state', () => {
const { container } = render(
<Label required htmlFor="hello">
Hello
</Label>
)
expect(container.firstChild).toHaveAttribute('title', 'Required')
expect(container.firstChild).toHaveClass('required')
})
})

View File

@ -0,0 +1,22 @@
import React from 'react'
import styles from './Label.module.scss'
const Label = ({
required,
children,
...props
}: {
required?: boolean
children: string
htmlFor: string
}) => (
<label
className={required ? styles.required : styles.label}
title={required ? 'Required' : ''}
{...props}
>
{children}
</label>
)
export default Label

View File

@ -0,0 +1,5 @@
@import '../../../styles/variables';
.row {
margin-bottom: $spacer;
}

View File

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

View File

@ -0,0 +1,8 @@
import React from 'react'
import styles from './Row.module.scss'
const Row = ({ children }: { children: any }) => (
<div className={styles.row}>{children}</div>
)
export default Row

View File

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

View File

@ -0,0 +1,18 @@
import React from 'react'
import ReactMarkdown from 'react-markdown'
const Markdown = ({
text,
className
}: {
text: string
className?: string
}) => {
// fix react-markdown \n transformation
// https://github.com/rexxars/react-markdown/issues/105#issuecomment-351585313
const textCleaned = text.replace(/\\n/g, '\n ')
return <ReactMarkdown source={textCleaned} className={className} />
}
export default Markdown

View File

@ -0,0 +1,71 @@
import React from 'react'
import Helmet from 'react-helmet'
import { withRouter, RouteComponentProps } from 'react-router-dom'
import meta from '../../data/meta.json'
import imageDefault from '../../img/share.png'
const MetaTags = ({
title,
description,
url,
image
}: {
title: string
description: string
url: string
image: string
}) => (
<Helmet defaultTitle={meta.title} titleTemplate={`%s - ${meta.title}`}>
<html lang="en" />
{title && <title>{title}</title>}
{/* General tags */}
<meta name="description" content={description} />
<meta name="image" content={image} />
<link rel="canonical" href={url} />
{/* OpenGraph tags */}
<meta property="og:url" content={url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={image} />
{/* Twitter Card tags */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:creator" content="@oceanprotocol" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={image} />
{/* Prevent search engine indexing except for live */}
{window.location.hostname !== 'commons.oceanprotocol.com' && (
<meta name="robots" content="noindex,nofollow" />
)}
</Helmet>
)
interface SeoProps extends RouteComponentProps {
title?: string
description?: string
shareImage?: string
}
const Seo = ({ title, description, shareImage, location }: SeoProps) => {
title = title || meta.title
description = description || meta.description
shareImage = shareImage || meta.url + imageDefault
const url = meta.url + location.pathname + location.search
return (
<MetaTags
title={title}
description={description}
url={url}
image={shareImage}
/>
)
}
export default withRouter(Seo)

View File

@ -0,0 +1,50 @@
@import '../../styles/variables';
.spinner {
position: relative;
text-align: center;
margin-top: $spacer * $line-height;
margin-bottom: $spacer / 2;
line-height: 1.3;
&: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 $brand-purple;
border-top-color: $brand-violet;
animation: spinner .6s linear infinite;
}
}
.spinnerMessage {
color: $brand-grey-light;
padding-top: $spacer / 4;
}
.small {
composes: spinner;
margin: 0;
display: inline-block;
&:before {
width: $font-size-small;
height: $font-size-small;
margin-top: -($font-size-small);
margin-left: -($font-size-small / 2);
border-width: .1rem;
}
}
@keyframes spinner {
to {
transform: rotate(360deg);
}
}

View File

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

View File

@ -0,0 +1,34 @@
@import '../../../styles/variables';
.status {
display: inline-block;
position: relative;
cursor: help;
padding: .5rem;
}
// default: red square
.statusIndicator {
width: $font-size-small;
height: $font-size-small;
display: block;
background: $red;
}
// yellow triangle
.statusIndicatorCloseEnough {
composes: statusIndicator;
background: none;
width: 0;
height: 0;
border-left: $font-size-small / 1.7 solid transparent;
border-right: $font-size-small / 1.7 solid transparent;
border-bottom: $font-size-small solid $yellow;
}
// green circle
.statusIndicatorActive {
composes: statusIndicator;
border-radius: 50%;
background: $green;
}

View File

@ -0,0 +1,35 @@
import React from 'react'
import cx from 'classnames'
import { User } from '../../../context'
import styles from './Indicator.module.scss'
const Indicator = ({
className,
togglePopover,
forwardedRef
}: {
className?: string
togglePopover: () => void
forwardedRef: (ref: HTMLElement | null) => void
}) => (
<div
className={cx(styles.status, className)}
onMouseOver={togglePopover}
onMouseOut={togglePopover}
ref={forwardedRef}
>
<User.Consumer>
{states =>
!states.isWeb3 ? (
<span className={styles.statusIndicator} />
) : !states.isLogged || !states.isOceanNetwork ? (
<span className={styles.statusIndicatorCloseEnough} />
) : states.isLogged ? (
<span className={styles.statusIndicatorActive} />
) : null
}
</User.Consumer>
</div>
)
export default Indicator

View File

@ -0,0 +1,56 @@
@import '../../../styles/variables';
$popoverWidth: 18rem;
.popover {
position: relative;
width: $popoverWidth;
padding: $spacer / 2;
background: $brand-black;
border-radius: .1rem;
border: .1rem solid $brand-grey-light;
box-shadow: 0 6px 16px 0 rgba($brand-black, .3);
color: $brand-grey-light;
font-size: $font-size-small;
animation: showPopup .2s ease-in forwards;
white-space: initial;
text-align: left;
}
@keyframes showPopup {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.popoverInfoline {
border-bottom: .05rem solid $brand-grey;
padding: $spacer / 3 0;
&:first-child {
padding-top: 0;
}
&:last-child {
padding-bottom: 0;
border-bottom: 0;
}
button {
font-size: $font-size-small;
}
}
.balance {
font-size: $font-size-small;
margin-left: $spacer / 2;
white-space: nowrap;
&:first-child {
margin-left: 0;
}
}

View File

@ -0,0 +1,53 @@
import React from 'react'
import { render } from '@testing-library/react'
import Popover from './Popover'
import { userMock, userMockConnected } from '../../../../__mocks__/user-mock'
import { User } from '../../../context'
describe('Popover', () => {
it('renders without crashing', () => {
const { container } = render(
<User.Provider value={userMock}>
<Popover forwardedRef={() => null} style={{}} />
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()
})
it('renders connected without crashing', () => {
const { container } = render(
<User.Provider value={userMockConnected}>
<Popover forwardedRef={() => null} style={{}} />
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()
})
it('renders correct network', () => {
const { container } = render(
<User.Provider value={{ ...userMockConnected, network: 'Nile' }}>
<Popover forwardedRef={() => null} style={{}} />
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()
expect(container.firstChild).toHaveTextContent('Connected to Nile')
})
it('renders with wrong network', () => {
const { container } = render(
<User.Provider
value={{
...userMockConnected,
isOceanNetwork: false,
network: '1'
}}
>
<Popover forwardedRef={() => null} style={{}} />
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()
expect(container.firstChild).toHaveTextContent(
'Please connect to Custom RPC'
)
})
})

View File

@ -0,0 +1,67 @@
import React, { PureComponent } from 'react'
import Account from '../../atoms/Account'
import { User } from '../../../context'
import styles from './Popover.module.scss'
export default class Popover extends PureComponent<{
forwardedRef: (ref: HTMLElement | null) => void
style: React.CSSProperties
}> {
public render() {
const {
account,
balance,
network,
isWeb3,
isOceanNetwork
} = this.context
return (
<div
className={styles.popover}
ref={this.props.forwardedRef}
style={this.props.style}
>
{!isWeb3 ? (
<div className={styles.popoverInfoline}>
No Web3 detected. Use a browser with MetaMask installed
to publish assets.
</div>
) : (
<>
<div className={styles.popoverInfoline}>
<Account account={account} />
</div>
{account && balance && (
<div className={styles.popoverInfoline}>
<span
className={styles.balance}
title={(balance.eth / 1e18).toFixed(10)}
>
<strong>
{(balance.eth / 1e18)
.toFixed(3)
.slice(0, -1)}
</strong>{' '}
ETH
</span>
<span className={styles.balance}>
<strong>{balance.ocn}</strong> OCEAN
</span>
</div>
)}
<div className={styles.popoverInfoline}>
{network && !isOceanNetwork
? 'Please connect to Custom RPC\n https://pacific.oceanprotocol.com'
: network && `Connected to ${network} network`}
</div>
</>
)}
</div>
)
}
}
Popover.contextType = User

View File

@ -0,0 +1,20 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import AccountStatus from '.'
describe('AccountStatus', () => {
it('renders without crashing', () => {
const { container } = render(<AccountStatus />)
expect(container.firstChild).toBeInTheDocument()
})
it('togglePopover fires', () => {
const { container } = render(<AccountStatus />)
const indicator = container.querySelector('.statusIndicator')
indicator && fireEvent.mouseOver(indicator)
expect(container.querySelector('.popover')).toBeInTheDocument()
indicator && fireEvent.mouseOut(indicator)
})
})

View File

@ -0,0 +1,54 @@
import React, { PureComponent } from 'react'
import { Manager, Reference, Popper } from 'react-popper'
import AccountPopover from './Popover'
import AccountIndicator from './Indicator'
interface AccountStatusProps {
className?: string
}
interface AccountStatusState {
isPopoverOpen: boolean
}
export default class AccountStatus extends PureComponent<
AccountStatusProps,
AccountStatusState
> {
public state = {
isPopoverOpen: false
}
private togglePopover() {
this.setState(prevState => ({
isPopoverOpen: !prevState.isPopoverOpen
}))
}
public render() {
return (
<Manager>
<Reference>
{({ ref }) => (
<AccountIndicator
togglePopover={() => this.togglePopover()}
className={this.props.className}
forwardedRef={ref}
/>
)}
</Reference>
{this.state.isPopoverOpen && (
<Popper placement="auto">
{({ ref, style, placement }) => (
<AccountPopover
forwardedRef={ref}
style={style}
data-placement={placement}
/>
)}
</Popper>
)}
</Manager>
)
}
}

View File

@ -0,0 +1,95 @@
@import '../../styles/variables';
.asset {
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-all;
> a {
display: block;
height: 100%;
padding: $spacer;
border: 1px solid $brand-grey-lighter;
border-radius: $border-radius;
background: $brand-white;
color: inherit;
position: relative;
&:hover,
&:focus {
color: inherit;
border-color: $brand-pink;
transform: none;
// category image
> div:first-child {
opacity: 1;
background-size: 105%;
}
}
}
h1 {
font-size: $font-size-large;
margin-top: 0;
}
}
.minimal {
h1 {
margin-bottom: 0;
}
}
.assetList {
> a {
color: $brand-grey-dark;
border-bottom: 1px solid $brand-grey-lighter;
padding-top: $spacer / 2;
padding-bottom: $spacer / 2;
display: flex;
justify-content: space-between;
align-items: center;
&:hover,
&:focus {
color: $brand-pink;
transform: none;
}
}
h1 {
font-size: $font-size-base;
color: inherit;
margin: 0;
}
}
.description {
&,
p,
strong,
a,
h1,
h2,
h3,
h4,
h5 {
font-weight: $font-weight-base;
font-family: $font-family-base;
margin-bottom: 0;
font-size: $font-size-small;
color: $brand-grey;
}
}
.date {
font-size: $font-size-small;
color: $brand-grey-light;
}
.assetFooter {
margin-top: $spacer / 2;
font-size: $font-size-small;
color: $brand-grey-light;
}

View File

@ -0,0 +1,58 @@
import React from 'react'
import { Link } from 'react-router-dom'
import moment from 'moment'
import Dotdotdot from 'react-dotdotdot'
import cx from 'classnames'
import styles from './AssetTeaser.module.scss'
import CategoryImage from '../atoms/CategoryImage'
const AssetTeaser = ({
asset,
list,
minimal
}: {
asset: any
list?: boolean
minimal?: boolean
}) => {
const { metadata } = asset.findServiceByType('Metadata')
const { base } = metadata
return list ? (
<article className={styles.assetList}>
<Link to={`/asset/${asset.id}`}>
<h1>{base.name}</h1>
<div
className={styles.date}
title={`Published on ${base.datePublished}`}
>
{moment(base.datePublished, 'YYYYMMDD').fromNow()}
</div>
</Link>
</article>
) : (
<article
className={
minimal ? cx(styles.asset, styles.minimal) : styles.asset
}
>
<Link to={`/asset/${asset.id}`}>
{base.categories && !minimal && (
<CategoryImage dimmed category={base.categories[0]} />
)}
<h1>{base.name}</h1>
{!minimal && (
<div className={styles.description}>
<Dotdotdot clamp={3}>{base.description}</Dotdotdot>
</div>
)}
<footer className={styles.assetFooter}>
{base.categories && <div>{base.categories[0]}</div>}
</footer>
</Link>
</article>
)
}
export default AssetTeaser

View File

@ -0,0 +1,72 @@
@import '../../styles/variables';
.pagination {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-top: $spacer * 2;
margin-bottom: $spacer;
padding-left: 0;
li {
margin: 0;
&:before {
display: none;
}
}
}
.number {
text-align: center;
font-weight: $font-weight-bold;
padding: $spacer / 4 $spacer / 2;
margin-left: -1px;
margin-top: -1px;
display: inline-block;
cursor: pointer;
border: 1px solid $brand-grey-lighter;
min-width: 3.5rem;
&,
&:hover,
&:focus,
&:active {
transform: none;
outline: 0;
}
&:hover,
&:focus,
&:active {
background: lighten($brand-grey-lighter, 7%);
}
}
.current,
.prev,
.next,
.break {
composes: number;
}
.current {
cursor: default;
pointer-events: none;
&,
&:hover,
&:focus,
&:active {
color: $brand-grey-light;
background: lighten($brand-grey-lighter, 7%);
}
}
.next {
text-align: right;
}
.prevNextDisabled {
opacity: 0;
}

View File

@ -0,0 +1,17 @@
import React from 'react'
import { render } from '@testing-library/react'
import Pagination from './Pagination'
describe('Pagination', () => {
it('renders without crashing', () => {
const { container } = render(
<Pagination
totalPages={20}
currentPage={1}
handlePageClick={() => Promise.resolve()}
/>
)
expect(container.firstChild).toBeInTheDocument()
container.firstChild && expect(container.firstChild.nodeName).toBe('UL')
})
})

View File

@ -0,0 +1,72 @@
import React, { PureComponent } from 'react'
import ReactPaginate from 'react-paginate'
import styles from './Pagination.module.scss'
interface PaginationProps {
totalPages: number
currentPage: number
handlePageClick(data: { selected: number }): Promise<any>
}
interface PaginationState {
smallViewport: boolean
}
export default class Pagination extends PureComponent<
PaginationProps,
PaginationState
> {
public state = { smallViewport: true }
private mq = window.matchMedia && window.matchMedia('(min-width: 600px)')
public componentDidMount() {
if (window.matchMedia) {
this.mq.addListener(this.viewportChange)
this.viewportChange(this.mq)
}
}
public componentWillUnmount() {
if (window.matchMedia) {
this.mq.removeListener(this.viewportChange)
}
}
private viewportChange = (mq: { matches: boolean }) => {
if (mq.matches) {
this.setState({ smallViewport: false })
} else {
this.setState({ smallViewport: true })
}
}
public render() {
const { totalPages, currentPage, handlePageClick } = this.props
const { smallViewport } = this.state
return (
totalPages > 1 && (
<ReactPaginate
pageCount={totalPages}
// react-pagination starts counting at 0, we start at 1
initialPage={currentPage - 1}
// adapt based on media query match
marginPagesDisplayed={smallViewport ? 0 : 1}
pageRangeDisplayed={smallViewport ? 3 : 6}
onPageChange={data => handlePageClick(data)}
disableInitialCallback
previousLabel={'←'}
nextLabel={'→'}
breakLabel={'...'}
containerClassName={styles.pagination}
pageLinkClassName={styles.number}
activeLinkClassName={styles.current}
previousLinkClassName={styles.prev}
nextLinkClassName={styles.next}
disabledClassName={styles.prevNextDisabled}
breakLinkClassName={styles.break}
/>
)
)
}
}

View File

@ -0,0 +1,22 @@
@import '../../../styles/variables';
.spinner {
composes: spinner, small from '../../atoms/Spinner.module.scss';
margin-right: $spacer;
}
.commit {
margin-left: $spacer / 8;
code {
color: $brand-grey-light;
font-size: $font-size-mini;
}
}
.network {
color: $brand-grey-light;
text-transform: capitalize;
margin-left: $spacer / 8;
font-size: $font-size-mini;
}

View File

@ -0,0 +1,23 @@
import React from 'react'
import { render } from '@testing-library/react'
import VersionNumber from './VersionNumber'
describe('VersionNumber', () => {
it('renders without crashing', () => {
const { container } = render(<VersionNumber name="Commons" />)
expect(container.firstChild).toBeInTheDocument()
})
it('renders with all props set', () => {
const { container } = render(
<VersionNumber
name="Commons"
version="6.6.6"
network="Nile"
commit="xxxxxxxxxxx"
/>
)
expect(container.firstChild).toBeInTheDocument()
expect(container.firstChild).toHaveTextContent('6.6.6')
})
})

View File

@ -0,0 +1,53 @@
import React from 'react'
import { OceanPlatformTechStatus } from '@oceanprotocol/squid'
import slugify from '@sindresorhus/slugify'
import Spinner from '../../atoms/Spinner'
import styles from './VersionNumber.module.scss'
const VersionNumber = ({
name,
version,
network,
status,
commit
}: {
name: string
version?: string
network?: string
status?: OceanPlatformTechStatus
commit?: string
}) =>
version ? (
<>
<a
href={`https://github.com/oceanprotocol/${slugify(
name
)}/releases/tag/v${version}`}
title="Go to release on GitHub"
>
<code>v{version}</code>
</a>
{commit && (
<a
href={`https://github.com/oceanprotocol/${slugify(
name
)}/commit/${commit}`}
className={styles.commit}
title={`Go to commit ${commit} on GitHub`}
>
<code>{commit.substring(0, 7)}</code>
</a>
)}
{network && <span className={styles.network}>{` ${network}`}</span>}
</>
) : (
<span>
{status === OceanPlatformTechStatus.Loading ? (
<Spinner className={styles.spinner} small />
) : (
status || 'Could not get version'
)}
</span>
)
export default VersionNumber

View File

@ -0,0 +1,37 @@
@import '../../../styles/variables';
.status {
text-align: center;
padding-top: $spacer / 2;
padding-bottom: $spacer;
display: flex;
justify-content: space-between;
}
.element {
display: inline-block;
margin-left: $spacer / 2;
margin-right: $spacer / 2;
text-align: center;
}
.indicator,
.indicatorActive {
display: inline-block;
margin-right: $spacer / 4;
margin-bottom: -.1rem;
}
.indicator {
composes: statusIndicator from '../AccountStatus/Indicator.module.scss';
}
.indicatorActive {
composes: statusIndicatorActive from '../AccountStatus/Indicator.module.scss';
}
.indicatorLabel {
font-family: $font-family-title;
color: $brand-grey;
text-transform: capitalize;
}

View File

@ -0,0 +1,23 @@
import React from 'react'
import { render } from '@testing-library/react'
import VersionStatus from './VersionStatus'
describe('VersionStatus', () => {
it('renders without crashing', () => {
const { container } = render(
<VersionStatus
status={{ ok: false, contracts: false, network: false }}
/>
)
expect(container.firstChild).toBeInTheDocument()
})
it('renders true states', () => {
const { container } = render(
<VersionStatus
status={{ ok: true, contracts: false, network: false }}
/>
)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -0,0 +1,41 @@
import React from 'react'
import styles from './VersionStatus.module.scss'
const statusInfo: { [key: string]: string } = {
ok: 'Shows if connection to all component endpoints can be established.',
network: 'Shows if all components are on the same network.',
contracts: 'Shows if contracts loaded by components are the same version.'
}
const VersionStatus = ({
status
}: {
status: { ok: boolean; network: boolean; contracts: boolean }
}) => {
return (
<div className={styles.status}>
{Object.entries(status).map(([key, value]) => (
<div
className={styles.element}
key={key}
title={statusInfo[key]}
>
<span
className={
value === true
? styles.indicatorActive
: styles.indicator
}
>
{value}
</span>
<span className={styles.indicatorLabel}>
{key === 'ok' ? 'components' : key}
</span>
</div>
))}
</div>
)
}
export default VersionStatus

View File

@ -0,0 +1,57 @@
@import '../../../styles/variables';
.tableWrap {
// make 'em scrollable
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.table {
border-top: 1px solid $brand-grey-lighter;
table {
margin-left: $spacer;
width: calc(100% - #{$spacer});
margin-bottom: -1px;
td {
padding: $spacer / 6 $spacer / 2;
// stylelint-disable-next-line selector-max-compound-selectors
&,
code {
font-size: $font-size-mini;
}
}
}
td {
padding: $spacer / 4 $spacer / 2;
vertical-align: top;
&:last-child {
text-align: right;
}
// stylelint-disable-next-line selector-no-qualifying-type
&[colspan] {
padding: 0;
}
}
a {
color: $brand-grey;
&:hover,
&:focus {
&,
code {
color: $brand-pink;
}
}
}
}
.label {
min-width: 15rem;
}

View File

@ -0,0 +1,40 @@
import React from 'react'
import { render } from '@testing-library/react'
import { VersionTableContracts } from './VersionTable'
describe('VersionTableContracts', () => {
it('renders without crashing', () => {
const { container } = render(
<VersionTableContracts
contracts={{ hello: 'hello', hello2: 'hello2' }}
network="nile"
keeperVersion="6.6.6"
/>
)
expect(container.firstChild).toBeInTheDocument()
})
it('renders correct Submarine links', () => {
const { container, rerender } = render(
<VersionTableContracts
contracts={{ hello: 'hello', hello2: 'hello2' }}
network="duero"
keeperVersion="6.6.6"
/>
)
expect(container.querySelector('tr:last-child a').href).toMatch(
/submarine.duero.dev-ocean/
)
rerender(
<VersionTableContracts
contracts={{ hello: 'hello', hello2: 'hello2' }}
network="pacific"
keeperVersion="6.6.6"
/>
)
expect(container.querySelector('tr:last-child a').href).toMatch(
/submarine.pacific.dev-ocean/
)
})
})

View File

@ -0,0 +1,114 @@
import React from 'react'
import { VersionNumbersState } from '.'
import VersionTableRow from './VersionTableRow'
import styles from './VersionTable.module.scss'
import VersionNumber from './VersionNumber'
import {
serviceUri,
nodeUri,
aquariusUri,
brizoUri,
brizoAddress,
secretStoreUri,
faucetUri
} from '../../../config'
const commonsConfig = {
serviceUri,
nodeUri,
aquariusUri,
brizoUri,
brizoAddress,
secretStoreUri,
faucetUri
}
export const VersionTableContracts = ({
contracts,
network,
keeperVersion
}: {
contracts: {
[contractName: string]: string
}
network: string
keeperVersion?: string
}) => (
<table>
<tbody>
<tr>
<td>
<strong>Keeper Contracts</strong>
</td>
<td>
<VersionNumber
name={'Keeper Contracts'}
version={keeperVersion}
/>
</td>
</tr>
{contracts &&
Object.keys(contracts)
// sort alphabetically
.sort((a, b) => a.localeCompare(b))
.map(key => {
const submarineLink = `https://submarine${
network === 'duero'
? '.duero'
: network === 'pacific'
? '.pacific'
: ''
}.dev-ocean.com/address/${contracts[key]}`
return (
<tr key={key}>
<td>
<code className={styles.label}>{key}</code>
</td>
<td>
<a href={submarineLink}>
<code>{contracts[key]}</code>
</a>
</td>
</tr>
)
})}
</tbody>
</table>
)
export const VersionTableCommons = () => (
<table>
<tbody>
{Object.entries(commonsConfig).map(([key, value]) => (
<tr key={key}>
<td>
<code className={styles.label}>{key}</code>
</td>
<td>
<code>{value}</code>
</td>
</tr>
))}
</tbody>
</table>
)
const VersionTable = ({ data }: { data: VersionNumbersState }) => {
return (
<div className={styles.tableWrap}>
<table className={styles.table}>
<tbody>
{Object.entries(data)
.filter(([key]) => key !== 'status')
.map(([key, value]) => (
<VersionTableRow key={key} value={value} />
))}
</tbody>
</table>
</div>
)
}
export default VersionTable

View File

@ -0,0 +1,15 @@
@import '../../../styles/variables';
.handle {
display: inline-block;
border: 0;
background: none;
box-shadow: none;
padding: 0;
margin: 0;
margin-left: -1rem;
margin-top: -.1rem;
padding-right: .5rem;
cursor: pointer;
color: $brand-grey-light;
}

View File

@ -0,0 +1,76 @@
import React from 'react'
import useCollapse from 'react-collapsed'
import slugify from '@sindresorhus/slugify'
import styles from './VersionTableRow.module.scss'
import { VersionTableContracts, VersionTableCommons } from './VersionTable'
import VersionNumber from './VersionNumber'
const VersionTableRow = ({ value }: { value: any }) => {
const collapseStyles = {
transitionDuration: '0.01s'
}
const expandStyles = {
transitionDuration: '0.01s',
transitionTimingFunction: 'ease-out'
}
const { getCollapseProps, getToggleProps, isOpen } = useCollapse({
collapseStyles,
expandStyles
})
return (
<>
<tr>
<td>
{(value.name === 'Commons' || value.contracts) && (
<button className={styles.handle} {...getToggleProps()}>
{isOpen ? (
<span>&#9660;</span>
) : (
<span>&#9658;</span>
)}
</button>
)}
<a
href={`https://github.com/oceanprotocol/${slugify(
value.name || value.software
)}`}
>
<strong>{value.name || value.software}</strong>
</a>
</td>
<td>
<VersionNumber
name={value.name || value.software}
version={value.version}
status={value.status}
network={value.network}
commit={value.commit}
/>
</td>
</tr>
{value.name === 'Commons' && (
<tr {...getCollapseProps()}>
<td colSpan={2}>
<VersionTableCommons />
</td>
</tr>
)}
{value.contracts && (
<tr {...getCollapseProps()}>
<td colSpan={2}>
<VersionTableContracts
contracts={value.contracts}
network={value.network || ''}
keeperVersion={value.keeperVersion}
/>
</td>
</tr>
)}
</>
)
}
export default VersionTableRow

View File

@ -0,0 +1,16 @@
@import '../../../styles/variables';
.versions {
margin-top: $spacer * 2;
}
.versionsTitle {
font-size: $font-size-large;
margin-bottom: $spacer / 2;
}
.versionsMinimal {
font-family: $font-family-monospace;
font-size: $font-size-mini;
margin-top: $spacer;
}

View File

@ -0,0 +1,85 @@
import React from 'react'
import { render } from '@testing-library/react'
import mockAxios from 'jest-mock-axios'
import { StateMock } from '@react-mock/state'
import VersionNumbers from '.'
import { User } from '../../../context'
import { userMockConnected } from '../../../../__mocks__/user-mock'
afterEach(() => {
mockAxios.reset()
})
const stateMockIncomplete = {
commons: {
name: 'Commons',
version: undefined
},
squid: {
name: 'Squid-js',
version: undefined
},
aquarius: {
name: 'Aquarius',
version: undefined
},
brizo: {
name: 'Brizo',
version: undefined,
contracts: undefined,
network: undefined,
keeperVersion: undefined,
keeperUrl: undefined
},
faucet: {
name: 'Faucet',
version: undefined
},
status: {
ok: false,
network: false,
contracts: false
}
}
const mockResponse = {
data: {
software: 'Faucet',
version: '6.6.6'
}
}
const mockResponseFaulty = {
status: 404,
statusText: 'Not Found',
data: {}
}
describe('VersionNumbers', () => {
it('renders without crashing', () => {
const { container } = render(
<User.Provider value={userMockConnected}>
<VersionNumbers />
</User.Provider>
)
mockAxios.mockResponse(mockResponse)
expect(mockAxios.get).toHaveBeenCalled()
expect(container.firstChild).toBeInTheDocument()
})
it('renders without proper component response', () => {
const { container } = render(
<User.Provider value={userMockConnected}>
<StateMock state={stateMockIncomplete}>
<VersionNumbers />
</StateMock>
</User.Provider>
)
mockAxios.mockResponse(mockResponseFaulty)
expect(mockAxios.get).toHaveBeenCalled()
expect(container.querySelector('table')).toHaveTextContent(
'Could not get version'
)
})
})

View File

@ -0,0 +1,161 @@
import React, { PureComponent } from 'react'
import {
OceanPlatformVersions,
OceanPlatformTechStatus,
Logger
} from '@oceanprotocol/squid'
import axios from 'axios'
import { version } from '../../../../package.json'
import styles from './index.module.scss'
import { nodeUri, faucetUri } from '../../../config'
import { User } from '../../../context'
import VersionTable from './VersionTable'
import VersionStatus from './VersionStatus'
// construct values which are not part of any response
export const commonsVersion =
process.env.NODE_ENV === 'production' ? version : `${version}-dev`
const commonsNetwork = new URL(nodeUri).hostname.split('.')[0]
const faucetNetwork = new URL(faucetUri).hostname.split('.')[1]
interface VersionNumbersProps {
minimal?: boolean
}
export interface VersionNumbersState extends OceanPlatformVersions {
commons: {
name: string
version: string
network: string
}
faucet: {
name: string
version: string
network: string
status: OceanPlatformTechStatus
}
}
export default class VersionNumbers extends PureComponent<
VersionNumbersProps,
VersionNumbersState
> {
public static contextType = User
// define a minimal default state to fill UI
public state: VersionNumbersState = {
commons: {
name: 'Commons',
network: commonsNetwork,
version: commonsVersion
},
squid: {
name: 'Squid-js',
status: OceanPlatformTechStatus.Loading
},
aquarius: {
name: 'Aquarius',
status: OceanPlatformTechStatus.Loading
},
brizo: {
name: 'Brizo',
status: OceanPlatformTechStatus.Loading
},
faucet: {
name: 'Faucet',
version: '',
network: faucetNetwork,
status: OceanPlatformTechStatus.Loading
},
status: {
ok: false,
network: false,
contracts: false
}
}
// for canceling axios requests
public signal = axios.CancelToken.source()
public async componentDidMount() {
this.getOceanVersions()
this.getFaucetVersion()
}
public componentWillUnmount() {
this.signal.cancel()
}
private async getOceanVersions() {
const { ocean } = this.context
// wait until ocean object is properly populated
if (ocean.versions === undefined) return
const response = await ocean.versions.get()
const { squid, brizo, aquarius, status } = response
this.setState({
...this.state,
squid,
brizo,
aquarius,
status
})
}
private async getFaucetVersion() {
try {
const response = await axios.get(faucetUri, {
headers: { Accept: 'application/json' },
cancelToken: this.signal.token
})
// fail silently
if (response.status !== 200) return
this.setState({
...this.state,
faucet: {
...this.state.faucet,
version: response.data.version,
status: OceanPlatformTechStatus.Working
}
})
} catch (error) {
!axios.isCancel(error) && Logger.error(error.message)
}
}
private MinimalOutput = () => {
const { commons, squid, brizo, aquarius } = this.state
return (
<p className={styles.versionsMinimal}>
<a
title={`${squid.name} v${squid.version}\n${brizo.name} v${brizo.version}\n${aquarius.name} v${aquarius.version}`}
href={'/about'}
>
v{commons.version} {squid.network && `(${squid.network})`}
</a>
</p>
)
}
public render() {
const { minimal } = this.props
return minimal ? (
<this.MinimalOutput />
) : (
<div className={styles.versions} id="#oceanversions">
<h2 className={styles.versionsTitle}>
Ocean Components Status
</h2>
<VersionStatus status={this.state.status} />
<VersionTable data={this.state} />
</div>
)
}
}

View File

@ -0,0 +1,42 @@
@import '../../styles/variables';
.latestAssetsWrap {
// full width break out of container
margin-right: calc(-50vw + 50%);
}
.latestAssets {
width: 100%;
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
display: grid;
grid-gap: $spacer;
grid-auto-flow: column;
padding: $spacer / 2 $spacer;
border-left: 1px solid $brand-grey-lighter;
&::-webkit-scrollbar,
&::-moz-scrollbar {
display: none;
}
> article {
min-width: calc(18rem + #{$spacer});
}
}
.title {
font-size: $font-size-h4;
text-align: center;
color: $brand-grey-light;
border-bottom: 1px solid $brand-grey-lighter;
padding-bottom: $spacer / 3;
margin-top: $spacer * 3;
margin-bottom: $spacer / 2;
@media (min-width: $break-point--small) {
text-align: left;
}
}

View File

@ -0,0 +1,19 @@
import React from 'react'
import { BrowserRouter } from 'react-router-dom'
import { render } from '@testing-library/react'
import AssetsLatest from './AssetsLatest'
import { User } from '../../context'
import { userMockConnected } from '../../../__mocks__/user-mock'
describe('AssetsLatest', () => {
it('renders without crashing', () => {
const { container } = render(
<User.Provider value={userMockConnected}>
<BrowserRouter>
<AssetsLatest />
</BrowserRouter>
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -0,0 +1,79 @@
import React, { PureComponent } from 'react'
import { Logger } from '@oceanprotocol/squid'
import { User } from '../../context'
import Spinner from '../atoms/Spinner'
import AssetTeaser from '../molecules/AssetTeaser'
import styles from './AssetsLatest.module.scss'
interface AssetsLatestState {
latestAssets?: any[]
isLoadingLatest?: boolean
}
export default class AssetsLatest extends PureComponent<{}, AssetsLatestState> {
public state = { latestAssets: [], isLoadingLatest: true }
public _isMounted: boolean = false
public componentDidMount() {
this._isMounted = true
this._isMounted && this.getLatestAssets()
}
public componentWillUnmount() {
this._isMounted = false
}
private getLatestAssets = async () => {
const { ocean } = this.context
const searchQuery = {
offset: 15,
page: 1,
query: {},
sort: {
created: -1
}
}
try {
const search = await ocean.aquarius.queryMetadata(searchQuery)
this.setState({
latestAssets: search.results,
isLoadingLatest: false
})
} catch (error) {
Logger.error(error.message)
this.setState({ isLoadingLatest: false })
}
}
public render() {
const { latestAssets, isLoadingLatest } = this.state
return (
<>
<h2 className={styles.title}>Latest published assets</h2>
<div className={styles.latestAssetsWrap}>
{isLoadingLatest ? (
<Spinner message="Loading..." />
) : latestAssets && latestAssets.length ? (
<div className={styles.latestAssets}>
{latestAssets.map((asset: any) => (
<AssetTeaser
key={asset.id}
asset={asset}
minimal
/>
))}
</div>
) : (
<div>No data sets found.</div>
)}
</div>
</>
)
}
}
AssetsLatest.contextType = User

View File

@ -0,0 +1,25 @@
@import '../../styles/variables';
.assetsUser {
margin-top: $spacer;
margin-bottom: $spacer;
}
.subTitle {
font-size: $font-size-h4;
color: $brand-grey-light;
border-bottom: 1px solid $brand-grey-lighter;
padding-bottom: $spacer / 2;
margin-bottom: 0;
}
.link {
display: block;
margin-top: $spacer / 2;
}
.empty {
text-align: center;
margin-top: $spacer;
color: $brand-grey-light;
}

View File

@ -0,0 +1,109 @@
import React, { PureComponent } from 'react'
import { Link } from 'react-router-dom'
import { Logger } from '@oceanprotocol/squid'
import { User } from '../../context'
import Spinner from '../atoms/Spinner'
import AssetTeaser from '../molecules/AssetTeaser'
import styles from './AssetsUser.module.scss'
export default class AssetsUser extends PureComponent<
{ list?: boolean; recent?: number },
{ results: any[]; isLoading: boolean }
> {
public state = { results: [], isLoading: true }
public _isMounted: boolean = false
public componentDidMount() {
this._isMounted = true
this._isMounted && this.searchOcean()
}
public componentWillUnmount() {
this._isMounted = false
}
private async searchOcean() {
const { account, ocean } = this.context
if (account) {
ocean.keeper.didRegistry.contract.getPastEvents(
'DIDAttributeRegistered',
{
filter: { _owner: account },
fromBlock: 0,
toBlock: 'latest'
},
async (error: any, events: any) => {
if (error) {
Logger.log('error retrieving', error)
this._isMounted && this.setState({ isLoading: false })
} else {
const results = []
for (const event of events) {
const ddo = await ocean.assets.resolve(
`did:op:${event.returnValues._did.substring(2)}`
)
results.push(ddo)
}
this._isMounted &&
this.setState({ results, isLoading: false })
}
}
)
} else {
this.setState({ isLoading: false })
}
}
public render() {
const { account, isOceanNetwork } = this.context
return (
isOceanNetwork &&
account && (
<div className={styles.assetsUser}>
{this.props.recent && (
<h2 className={styles.subTitle}>
Your Latest Published Data Sets
</h2>
)}
{this.state.isLoading ? (
<Spinner />
) : this.state.results.length ? (
<>
{this.state.results
.slice(
0,
this.props.recent
? this.props.recent
: undefined
)
.filter(asset => !!asset)
.map((asset: any) => (
<AssetTeaser
list={this.props.list}
key={asset.id}
asset={asset}
/>
))}
{this.props.recent && (
<Link className={styles.link} to={'/history'}>
All Data Sets
</Link>
)}
</>
) : (
<div className={styles.empty}>
<p>No Data Sets Yet.</p>
<Link to="/publish">+ Publish A Data Set</Link>
</div>
)}
</div>
)
)
}
}
AssetsUser.contextType = User

View File

@ -0,0 +1,86 @@
@import '../../styles/variables';
.channel {
width: 100%;
@media (min-width: $break-point--medium) {
padding-top: $spacer * 2;
display: flex;
}
> div {
&:first-child {
margin-bottom: $spacer;
@media (min-width: $break-point--medium) {
margin-right: $spacer;
}
p:last-child {
margin-bottom: 0;
}
}
@media (min-width: $break-point--medium) {
flex: 1;
&:first-child {
flex: 0 0 calc(18rem + #{$spacer * 2});
}
}
}
// style channel teaser following another one
+ .channel {
border-top: 1px solid $brand-grey-lighter;
margin-top: $spacer * 2;
}
}
.channelTitle {
margin-top: $spacer * 4;
margin-bottom: $spacer / 4;
color: $brand-black;
@media (min-width: $break-point--medium) {
margin-top: -($spacer / 4);
}
}
.channelHeader {
text-align: center;
@media (min-width: $break-point--small) {
text-align: left;
}
a {
display: block;
&:hover,
&:focus {
transform: none;
// category image
// stylelint-disable-next-line
.channelTitle + div {
opacity: 1;
background-size: 105%;
}
}
}
}
.channelTeaser {
color: $brand-grey;
}
.channelResults {
display: grid;
grid-template-columns: 1fr;
grid-gap: $spacer;
@media (min-width: $break-point--small) {
grid-template-columns: 1fr 1fr;
}
}

View File

@ -0,0 +1,19 @@
import React from 'react'
import { render } from '@testing-library/react'
import ChannelTeaser from './ChannelTeaser'
import { BrowserRouter } from 'react-router-dom'
import { User } from '../../context'
import { userMockConnected } from '../../../__mocks__/user-mock'
describe('ChannelTeaser', () => {
it('renders without crashing', () => {
const { container } = render(
<User.Provider value={userMockConnected}>
<BrowserRouter>
<ChannelTeaser channel="ai-for-good" />
</BrowserRouter>
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -0,0 +1,99 @@
import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import { User } from '../../context'
import { Logger } from '@oceanprotocol/squid'
import Spinner from '../atoms/Spinner'
import AssetTeaser from '../molecules/AssetTeaser'
import styles from './ChannelTeaser.module.scss'
import channels from '../../data/channels.json'
import CategoryImage from '../atoms/CategoryImage'
interface ChannelTeaserProps {
channel: string
}
interface ChannelTeaserState {
channelAssets?: any[]
isLoadingChannel?: boolean
}
export default class ChannelTeaser extends Component<
ChannelTeaserProps,
ChannelTeaserState
> {
public static contextType = User
// Get channel content
public channel = channels.items
.filter(({ tag }) => tag === this.props.channel)
.map(channel => channel)[0]
public state = {
channelAssets: [],
isLoadingChannel: true
}
public async componentDidMount() {
this.getChannelAssets()
}
private getChannelAssets = async () => {
const { ocean } = this.context
const searchQuery = {
offset: 2,
page: 1,
query: {
tags: [this.channel.tag]
},
sort: {
created: -1
}
}
try {
const search = await ocean.aquarius.queryMetadata(searchQuery)
this.setState({
channelAssets: search.results,
isLoadingChannel: false
})
} catch (error) {
Logger.error(error.message)
this.setState({ isLoadingChannel: false })
}
}
public render() {
const { channelAssets, isLoadingChannel } = this.state
const { title, tag, teaser } = this.channel
return (
<div className={styles.channel}>
<div>
<header className={styles.channelHeader}>
<Link to={`/channels/${tag}`}>
<h2 className={styles.channelTitle}>{title}</h2>
<CategoryImage category={title} />
<p className={styles.channelTeaser}>{teaser}</p>
<p>Browse the channel </p>
</Link>
</header>
</div>
<div>
{isLoadingChannel ? (
<Spinner message="Loading..." />
) : channelAssets && channelAssets.length ? (
<div className={styles.channelResults}>
{channelAssets.map((asset: any) => (
<AssetTeaser key={asset.id} asset={asset} />
))}
</div>
) : (
<div>No data sets found.</div>
)}
</div>
</div>
)
}
}

View File

@ -0,0 +1,93 @@
@import '../../styles/variables';
.footer {
color: $brand-grey-light;
width: 100%;
text-align: center;
margin-top: $spacer;
padding-top: $spacer;
padding-bottom: $spacer;
align-self: flex-end;
> div {
align-self: flex-end;
@media screen and (min-width: $break-point--small) {
text-align: left;
display: flex;
justify-content: space-between;
}
}
&,
small {
font-size: $font-size-mini;
}
a {
color: inherit;
&:hover,
&:focus {
color: $brand-grey;
}
}
svg {
display: inline-block;
width: $font-size-large;
height: $font-size-large;
}
}
.links {
margin-top: $spacer / 2;
@media screen and (min-width: $break-point--small) {
text-align: right;
margin-top: 0;
}
a {
margin: 0 $spacer / 2;
display: inline-block;
&:last-child {
margin-right: 0;
}
}
}
.stats {
text-align: center;
margin-bottom: $spacer * $line-height;
font-size: $font-size-small;
p {
margin-bottom: $spacer / 4;
}
p:last-child {
margin-bottom: 0;
}
}
.aicommons {
svg {
width: 100px;
height: auto;
vertical-align: middle;
margin-top: -.05rem;
margin-left: $spacer / 6;
fill: currentColor;
}
a {
&:hover,
&:focus {
svg {
fill: $brand-pink;
}
}
}
}

View File

@ -0,0 +1,54 @@
import React from 'react'
import { Market } from '../../context'
import Content from '../atoms/Content'
import { ReactComponent as AiCommons } from '../../img/aicommons.svg'
import styles from './Footer.module.scss'
import meta from '../../data/meta.json'
import VersionNumbers from '../molecules/VersionNumbers'
const Footer = () => (
<footer className={styles.footer}>
<aside className={styles.stats}>
<Content wide>
<p>
Online since March 2019.
<Market.Consumer>
{state =>
state.totalAssets > 0 &&
` With a total of ${state.totalAssets} registered assets.`
}
</Market.Consumer>
</p>
<p className={styles.aicommons}>
Proud supporter of{' '}
<a
href="https://aicommons.com/?utm_source=commons.oceanprotocol.com"
title="AI Commons"
>
<AiCommons />
</a>
</p>
<VersionNumbers minimal />
</Content>
</aside>
<Content wide>
<small>
&copy; {new Date().getFullYear()}{' '}
<a href={meta.social[0].url}>{meta.company}</a> &mdash; All
Rights Reserved
</small>
<nav className={styles.links}>
{meta.social.map(site => (
<a key={site.title} href={site.url}>
{site.title}
</a>
))}
</nav>
</Content>
</footer>
)
export default Footer

View File

@ -0,0 +1,103 @@
@import '../../styles/variables';
.header {
width: 100%;
padding: $spacer / 2 0;
}
.headerContent {
composes: wide from '../atoms/Content.module.scss';
display: flex;
align-items: center;
}
.headerLogo {
display: flex;
align-items: center;
cursor: pointer;
&:hover,
&:focus,
&:active {
transform: none;
}
}
.headerLogoImage {
width: 4rem;
height: 4rem;
fill: #fff;
margin: 0;
}
.headerTitle {
font-size: $font-size-h3;
color: $brand-grey-light;
margin-left: $spacer / 2;
display: none;
@media (min-width: $break-point--medium) {
display: inline-block;
}
}
.headerMenu {
flex: 1;
justify-self: flex-end;
text-align: right;
white-space: nowrap;
overflow-y: hidden;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
margin-right: -($spacer / 1.5);
padding-right: $spacer;
border-left: 1px solid $brand-grey-lighter;
margin-left: $spacer / 8;
@media (min-width: $break-point--medium) {
padding-right: 0;
margin-right: 0;
margin-left: 0;
border-left: 0;
overflow: initial;
}
&::-webkit-scrollbar,
&::-moz-scrollbar {
display: none;
}
&::-webkit-scrollbar {
width: 3px;
height: 3px;
transition: opacity .2s ease-out;
}
}
.link {
display: inline-block;
margin: 0 $spacer / 2;
font-weight: $font-weight-bold;
color: $brand-grey;
&:last-child {
margin-right: 0;
}
&:hover,
&:focus,
&:active {
color: $brand-pink;
}
}
.linkActive {
composes: link;
color: $brand-pink;
pointer-events: none;
}
.accountStatus {
margin-left: $spacer / 2;
margin-bottom: -.5rem;
}

View File

@ -0,0 +1,29 @@
import React, { PureComponent } from 'react'
import { NavLink } from 'react-router-dom'
import { ReactComponent as Logo } from '@oceanprotocol/art/logo/logo.svg'
import { User } from '../../context'
import AccountStatus from '../molecules/AccountStatus'
import styles from './Header.module.scss'
import meta from '../../data/meta.json'
export default class Header extends PureComponent {
public render() {
return (
<header className={styles.header}>
<div className={styles.headerContent}>
<NavLink to={'/'} className={styles.headerLogo}>
<Logo className={styles.headerLogoImage} />
<h1 className={styles.headerTitle}>{meta.title}</h1>
</NavLink>
<nav className={styles.headerMenu}>
<AccountStatus className={styles.accountStatus} />
</nav>
</div>
</header>
)
}
}
Header.contextType = User

View File

@ -0,0 +1,22 @@
@import '../../styles/variables';
.message {
margin-bottom: $spacer;
color: $brand-grey;
position: relative;
border-bottom: .1rem solid $brand-grey-lighter;
border-top: .1rem solid $brand-grey-lighter;
padding-top: $spacer / 2;
padding-bottom: $spacer / 2;
text-align: left;
}
.warnings {
padding-left: $spacer;
}
.status {
margin-left: -($spacer);
margin-right: $spacer / 2;
padding: 0;
}

View File

@ -0,0 +1,64 @@
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Web3message from './Web3message'
import { User } from '../../context'
import { userMock, userMockConnected } from '../../../__mocks__/user-mock'
describe('Web3message', () => {
it('renders with noWeb3 message', () => {
const { container } = render(
<User.Provider value={{ ...userMock }}>
<Web3message />
</User.Provider>
)
expect(container.firstChild).toHaveTextContent('Not a Web3 Browser')
})
it('renders with wrongNetwork message', () => {
const { container } = render(
<User.Provider value={{ ...userMock, isWeb3: true }}>
<Web3message />
</User.Provider>
)
expect(container.firstChild).toHaveTextContent(
'Not connected to Pacific network'
)
})
it('renders with noAccount message', () => {
const { container } = render(
<User.Provider
value={{ ...userMock, isWeb3: true, isOceanNetwork: true }}
>
<Web3message />
</User.Provider>
)
expect(container.firstChild).toHaveTextContent('No accounts detected')
})
it('renders with hasAccount message', () => {
const { container } = render(
<User.Provider value={userMockConnected}>
<Web3message />
</User.Provider>
)
expect(container.firstChild).toHaveTextContent('0xxxxxx')
})
it('button click fires unlockAccounts', () => {
const { getByText } = render(
<User.Provider
value={{
...userMock,
isWeb3: true,
isOceanNetwork: true
}}
>
<Web3message />
</User.Provider>
)
fireEvent.click(getByText('Unlock Account'))
expect(userMock.unlockAccounts).toBeCalled()
})
})

Some files were not shown because too many files have changed in this diff Show More