mirror of
https://github.com/oceanprotocol/commons.git
synced 2023-03-15 18:03:00 +01:00
admin app with retire button
This commit is contained in:
parent
4c744d1ea9
commit
bf7fb99df1
2
admin/.dockerignore
Normal file
2
admin/.dockerignore
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
.env.local
|
54
admin/.env.local.example
Normal file
54
admin/.env.local.example
Normal 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
25
admin/Dockerfile
Normal 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
2
admin/__mocks__/axios.js
Normal file
@ -0,0 +1,2 @@
|
||||
import mockAxios from 'jest-mock-axios'
|
||||
export default mockAxios
|
66
admin/__mocks__/ocean-mock.ts
Normal file
66
admin/__mocks__/ocean-mock.ts
Normal 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
|
33
admin/__mocks__/user-mock.ts
Normal file
33
admin/__mocks__/user-mock.ts
Normal 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
17250
admin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
82
admin/package.json
Normal file
82
admin/package.json
Normal 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
BIN
admin/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
BIN
admin/public/icons/favicon_128.png
Normal file
BIN
admin/public/icons/favicon_128.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
BIN
admin/public/icons/favicon_256.png
Normal file
BIN
admin/public/icons/favicon_256.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
BIN
admin/public/icons/favicon_512.png
Normal file
BIN
admin/public/icons/favicon_512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
49
admin/public/index.html
Normal file
49
admin/public/index.html
Normal 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>
|
25
admin/public/manifest.json
Normal file
25
admin/public/manifest.json
Normal 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
2
admin/public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /search
|
BIN
admin/public/share.png
Normal file
BIN
admin/public/share.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 167 KiB |
11
admin/scripts/docker-entrypoint.sh
Executable file
11
admin/scripts/docker-entrypoint.sh
Executable 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/
|
5
admin/src/@types/ethereum-blockies/index.d.ts
vendored
Normal file
5
admin/src/@types/ethereum-blockies/index.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/// <reference types="node" />
|
||||
|
||||
declare module 'ethereum-blockies' {
|
||||
export function toDataUrl(address: string): string
|
||||
}
|
1
admin/src/@types/react-collapsed/index.d.ts
vendored
Normal file
1
admin/src/@types/react-collapsed/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module 'react-collapsed'
|
27
admin/src/App.module.scss
Normal file
27
admin/src/App.module.scss
Normal 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
29
admin/src/App.test.tsx
Normal 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
37
admin/src/App.tsx
Normal 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
19
admin/src/Routes.test.tsx
Normal 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
21
admin/src/Routes.tsx
Normal 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
|
24
admin/src/components/atoms/Account.module.scss
Normal file
24
admin/src/components/atoms/Account.module.scss
Normal 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;
|
||||
}
|
28
admin/src/components/atoms/Account.test.tsx
Normal file
28
admin/src/components/atoms/Account.test.tsx
Normal 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
|
||||
)
|
||||
})
|
||||
})
|
19
admin/src/components/atoms/Account.tsx
Normal file
19
admin/src/components/atoms/Account.tsx
Normal 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
|
73
admin/src/components/atoms/Button.module.scss
Normal file
73
admin/src/components/atoms/Button.module.scss
Normal 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;
|
||||
}
|
54
admin/src/components/atoms/Button.test.tsx
Normal file
54
admin/src/components/atoms/Button.test.tsx
Normal 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/)
|
||||
})
|
||||
})
|
60
admin/src/components/atoms/Button.tsx
Normal file
60
admin/src/components/atoms/Button.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
25
admin/src/components/atoms/CategoryImage.module.scss
Normal file
25
admin/src/components/atoms/CategoryImage.module.scss
Normal 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;
|
||||
}
|
25
admin/src/components/atoms/CategoryImage.test.tsx
Normal file
25
admin/src/components/atoms/CategoryImage.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
168
admin/src/components/atoms/CategoryImage.tsx
Normal file
168
admin/src/components/atoms/CategoryImage.tsx
Normal 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})` }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
23
admin/src/components/atoms/CategoryLink.tsx
Normal file
23
admin/src/components/atoms/CategoryLink.tsx
Normal 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
|
16
admin/src/components/atoms/Content.module.scss
Normal file
16
admin/src/components/atoms/Content.module.scss
Normal 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;
|
||||
}
|
8
admin/src/components/atoms/Content.tsx
Normal file
8
admin/src/components/atoms/Content.tsx
Normal 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
|
39
admin/src/components/atoms/Form/Form.module.scss
Normal file
39
admin/src/components/atoms/Form/Form.module.scss
Normal 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;
|
||||
}
|
29
admin/src/components/atoms/Form/Form.test.tsx
Normal file
29
admin/src/components/atoms/Form/Form.test.tsx
Normal 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')
|
||||
})
|
||||
})
|
36
admin/src/components/atoms/Form/Form.tsx
Normal file
36
admin/src/components/atoms/Form/Form.tsx
Normal 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
|
7
admin/src/components/atoms/Form/Help.module.scss
Normal file
7
admin/src/components/atoms/Form/Help.module.scss
Normal file
@ -0,0 +1,7 @@
|
||||
@import '../../../styles/variables';
|
||||
|
||||
.help {
|
||||
font-size: $font-size-small;
|
||||
color: darken($brand-grey-light, 10%);
|
||||
margin-top: $spacer / 4;
|
||||
}
|
13
admin/src/components/atoms/Form/Help.test.tsx
Normal file
13
admin/src/components/atoms/Form/Help.test.tsx
Normal 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)
|
||||
})
|
8
admin/src/components/atoms/Form/Help.tsx
Normal file
8
admin/src/components/atoms/Form/Help.tsx
Normal 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
|
190
admin/src/components/atoms/Form/Input.module.scss
Normal file
190
admin/src/components/atoms/Form/Input.module.scss
Normal 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;
|
||||
}
|
90
admin/src/components/atoms/Form/Input.test.tsx
Normal file
90
admin/src/components/atoms/Form/Input.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
221
admin/src/components/atoms/Form/Input.tsx
Normal file
221
admin/src/components/atoms/Form/Input.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
70
admin/src/components/atoms/Form/InputDate.module.scss
Normal file
70
admin/src/components/atoms/Form/InputDate.module.scss
Normal 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;
|
||||
}
|
39
admin/src/components/atoms/Form/InputGroup.module.scss
Normal file
39
admin/src/components/atoms/Form/InputGroup.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
10
admin/src/components/atoms/Form/InputGroup.test.tsx
Normal file
10
admin/src/components/atoms/Form/InputGroup.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
8
admin/src/components/atoms/Form/InputGroup.tsx
Normal file
8
admin/src/components/atoms/Form/InputGroup.tsx
Normal 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
|
22
admin/src/components/atoms/Form/Label.module.scss
Normal file
22
admin/src/components/atoms/Form/Label.module.scss
Normal 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;
|
||||
}
|
||||
}
|
20
admin/src/components/atoms/Form/Label.test.tsx
Normal file
20
admin/src/components/atoms/Form/Label.test.tsx
Normal 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')
|
||||
})
|
||||
})
|
22
admin/src/components/atoms/Form/Label.tsx
Normal file
22
admin/src/components/atoms/Form/Label.tsx
Normal 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
|
5
admin/src/components/atoms/Form/Row.module.scss
Normal file
5
admin/src/components/atoms/Form/Row.module.scss
Normal file
@ -0,0 +1,5 @@
|
||||
@import '../../../styles/variables';
|
||||
|
||||
.row {
|
||||
margin-bottom: $spacer;
|
||||
}
|
10
admin/src/components/atoms/Form/Row.test.tsx
Normal file
10
admin/src/components/atoms/Form/Row.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
8
admin/src/components/atoms/Form/Row.tsx
Normal file
8
admin/src/components/atoms/Form/Row.tsx
Normal 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
|
10
admin/src/components/atoms/Markdown.test.tsx
Normal file
10
admin/src/components/atoms/Markdown.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
18
admin/src/components/atoms/Markdown.tsx
Normal file
18
admin/src/components/atoms/Markdown.tsx
Normal 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
|
71
admin/src/components/atoms/Seo.tsx
Normal file
71
admin/src/components/atoms/Seo.tsx
Normal 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)
|
50
admin/src/components/atoms/Spinner.module.scss
Normal file
50
admin/src/components/atoms/Spinner.module.scss
Normal 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);
|
||||
}
|
||||
}
|
27
admin/src/components/atoms/Spinner.tsx
Normal file
27
admin/src/components/atoms/Spinner.tsx
Normal 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
|
@ -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;
|
||||
}
|
35
admin/src/components/molecules/AccountStatus/Indicator.tsx
Normal file
35
admin/src/components/molecules/AccountStatus/Indicator.tsx
Normal 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
|
@ -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;
|
||||
}
|
||||
}
|
@ -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'
|
||||
)
|
||||
})
|
||||
})
|
67
admin/src/components/molecules/AccountStatus/Popover.tsx
Normal file
67
admin/src/components/molecules/AccountStatus/Popover.tsx
Normal 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
|
20
admin/src/components/molecules/AccountStatus/index.test.tsx
Normal file
20
admin/src/components/molecules/AccountStatus/index.test.tsx
Normal 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)
|
||||
})
|
||||
})
|
54
admin/src/components/molecules/AccountStatus/index.tsx
Normal file
54
admin/src/components/molecules/AccountStatus/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
95
admin/src/components/molecules/AssetTeaser.module.scss
Normal file
95
admin/src/components/molecules/AssetTeaser.module.scss
Normal 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;
|
||||
}
|
58
admin/src/components/molecules/AssetTeaser.tsx
Normal file
58
admin/src/components/molecules/AssetTeaser.tsx
Normal 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
|
72
admin/src/components/molecules/Pagination.module.scss
Normal file
72
admin/src/components/molecules/Pagination.module.scss
Normal 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;
|
||||
}
|
17
admin/src/components/molecules/Pagination.test.tsx
Normal file
17
admin/src/components/molecules/Pagination.test.tsx
Normal 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')
|
||||
})
|
||||
})
|
72
admin/src/components/molecules/Pagination.tsx
Normal file
72
admin/src/components/molecules/Pagination.tsx
Normal 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}
|
||||
/>
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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')
|
||||
})
|
||||
})
|
@ -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
|
@ -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;
|
||||
}
|
@ -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()
|
||||
})
|
||||
})
|
@ -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
|
@ -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;
|
||||
}
|
@ -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/
|
||||
)
|
||||
})
|
||||
})
|
114
admin/src/components/molecules/VersionNumbers/VersionTable.tsx
Normal file
114
admin/src/components/molecules/VersionNumbers/VersionTable.tsx
Normal 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
|
@ -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;
|
||||
}
|
@ -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>▼</span>
|
||||
) : (
|
||||
<span>►</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
|
@ -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;
|
||||
}
|
85
admin/src/components/molecules/VersionNumbers/index.test.tsx
Normal file
85
admin/src/components/molecules/VersionNumbers/index.test.tsx
Normal 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'
|
||||
)
|
||||
})
|
||||
})
|
161
admin/src/components/molecules/VersionNumbers/index.tsx
Normal file
161
admin/src/components/molecules/VersionNumbers/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
42
admin/src/components/organisms/AssetsLatest.module.scss
Normal file
42
admin/src/components/organisms/AssetsLatest.module.scss
Normal 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;
|
||||
}
|
||||
}
|
19
admin/src/components/organisms/AssetsLatest.test.tsx
Normal file
19
admin/src/components/organisms/AssetsLatest.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
79
admin/src/components/organisms/AssetsLatest.tsx
Normal file
79
admin/src/components/organisms/AssetsLatest.tsx
Normal 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
|
25
admin/src/components/organisms/AssetsUser.module.scss
Normal file
25
admin/src/components/organisms/AssetsUser.module.scss
Normal 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;
|
||||
}
|
109
admin/src/components/organisms/AssetsUser.tsx
Normal file
109
admin/src/components/organisms/AssetsUser.tsx
Normal 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
|
86
admin/src/components/organisms/ChannelTeaser.module.scss
Normal file
86
admin/src/components/organisms/ChannelTeaser.module.scss
Normal 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;
|
||||
}
|
||||
}
|
19
admin/src/components/organisms/ChannelTeaser.test.tsx
Normal file
19
admin/src/components/organisms/ChannelTeaser.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
99
admin/src/components/organisms/ChannelTeaser.tsx
Normal file
99
admin/src/components/organisms/ChannelTeaser.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
93
admin/src/components/organisms/Footer.module.scss
Normal file
93
admin/src/components/organisms/Footer.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
admin/src/components/organisms/Footer.tsx
Normal file
54
admin/src/components/organisms/Footer.tsx
Normal 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>
|
||||
© {new Date().getFullYear()}{' '}
|
||||
<a href={meta.social[0].url}>{meta.company}</a> — 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
|
103
admin/src/components/organisms/Header.module.scss
Normal file
103
admin/src/components/organisms/Header.module.scss
Normal 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;
|
||||
}
|
29
admin/src/components/organisms/Header.tsx
Normal file
29
admin/src/components/organisms/Header.tsx
Normal 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
|
22
admin/src/components/organisms/Web3message.module.scss
Normal file
22
admin/src/components/organisms/Web3message.module.scss
Normal 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;
|
||||
}
|
64
admin/src/components/organisms/Web3message.test.tsx
Normal file
64
admin/src/components/organisms/Web3message.test.tsx
Normal 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
Loading…
Reference in New Issue
Block a user