1
0
mirror of https://github.com/oceanprotocol/commons.git synced 2023-03-15 18:03:00 +01:00

Merge pull request #125 from oceanprotocol/feature/ai-for-good

AI For Good: channels, new front-page & categories list
This commit is contained in:
Jernej Pregelj 2019-05-28 10:02:08 +02:00 committed by GitHub
commit dc32c71bba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
94 changed files with 1707 additions and 591 deletions

View File

@ -5,7 +5,11 @@ const userMock = {
isOceanNetwork: false,
account: '',
web3: {},
ocean: {},
ocean: {
aquarius: {
queryMetadata: jest.fn()
}
},
balance: { eth: 0, ocn: 0 },
network: '',
requestFromFaucet: jest.fn(),
@ -20,7 +24,11 @@ const userMockConnected = {
isOceanNetwork: true,
account: '0xxxxxx',
web3: {},
ocean: {},
ocean: {
aquarius: {
queryMetadata: jest.fn()
}
},
balance: { eth: 0, ocn: 0 },
network: '',
requestFromFaucet: jest.fn(),

539
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@
"@oceanprotocol/art": "^2.2.0",
"@oceanprotocol/squid": "^0.5.11",
"@oceanprotocol/typographies": "^0.1.0",
"@sindresorhus/slugify": "^0.9.1",
"classnames": "^2.2.6",
"ethereum-blockies": "MyEtherWallet/blockies",
"filesize": "^4.1.2",
@ -34,7 +35,6 @@
"react-popper": "^1.3.3",
"react-router-dom": "^5.0.0",
"react-transition-group": "^4.0.0",
"slugify": "^1.3.4",
"web3": "1.0.0-beta.37"
},
"devDependencies": {

View File

@ -3,25 +3,30 @@ import { Route, Switch } from 'react-router-dom'
import withTracker from './hoc/withTracker'
import About from './routes/About'
import Details from './routes/Details/'
import Home from './routes/Home'
import NotFound from './routes/NotFound'
import Publish from './routes/Publish/'
import Search from './routes/Search'
import Faucet from './routes/Faucet'
import History from './routes/History'
import Channels from './routes/Channels'
import Styleguide from './routes/Styleguide'
import Asset from './components/templates/Asset'
import Channel from './components/templates/Channel'
const Routes = () => (
<Switch>
<Route exact component={withTracker(Home)} path="/" />
<Route component={withTracker(Home)} exact path="/" />
<Route component={withTracker(Styleguide)} path="/styleguide" />
<Route component={withTracker(About)} path="/about" />
<Route component={withTracker(Publish)} path="/publish" />
<Route component={withTracker(Search)} path="/search" />
<Route component={withTracker(Details)} path="/asset/:did" />
<Route component={withTracker(Asset)} path="/asset/:did" />
<Route component={withTracker(Faucet)} path="/faucet" />
<Route component={withTracker(History)} path="/history" />
<Route component={withTracker(Channels)} exact path="/channels" />
<Route component={withTracker(Channel)} path="/channels/:channel" />
<Route component={withTracker(NotFound)} />
</Switch>
)

View File

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

View File

@ -1,4 +1,5 @@
import React, { PureComponent } from 'react'
import cx from 'classnames'
import styles from './CategoryImage.module.scss'
import agriculture from '../../img/categories/agriculture.jpg'
@ -33,6 +34,7 @@ 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) => {
@ -95,6 +97,8 @@ const categoryImageFile = (category: string) => {
case 'mathematics':
return mathematics
case 'Medicine':
case 'Health & Medicine':
case 'Health':
case 'medicine':
return medicine
case 'Other':
@ -133,21 +137,31 @@ const categoryImageFile = (category: string) => {
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 }> {
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={styles.categoryImage}
style={{
backgroundImage: `url(${image})`
}}
className={classNames}
style={{ backgroundImage: `url(${image})` }}
/>
)
}

View File

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

View File

@ -1,6 +1,6 @@
import cx from 'classnames'
import React, { PureComponent, FormEvent, ChangeEvent } from 'react'
import slugify from 'slugify'
import slugify from '@sindresorhus/slugify'
import DatePicker from 'react-datepicker'
import { ReactComponent as SearchIcon } from '../../../img/search.svg'
import Help from './Help'
@ -131,20 +131,14 @@ export default class Input extends PureComponent<InputProps, InputState> {
<div className={styles.radioWrap} key={index}>
<input
className={styles.radio}
id={slugify(option, {
lower: true
})}
id={slugify(option)}
type={type}
name={name}
value={slugify(option, {
lower: true
})}
value={slugify(option)}
/>
<label
className={styles.radioLabel}
htmlFor={slugify(option, {
lower: true
})}
htmlFor={slugify(option)}
>
{option}
</label>

View File

@ -20,6 +20,12 @@
color: inherit;
border-color: $brand-pink;
transform: none;
// category image
> div:first-child {
opacity: 1;
background-size: 105%;
}
}
}
@ -29,6 +35,12 @@
}
}
.minimal {
h1 {
margin-bottom: 0;
}
}
.assetList {
> a {
color: $brand-grey-dark;

View File

@ -2,10 +2,19 @@ import React from 'react'
import { Link } from 'react-router-dom'
import moment from 'moment'
import Dotdotdot from 'react-dotdotdot'
import styles from './Asset.module.scss'
import cx from 'classnames'
import styles from './AssetTeaser.module.scss'
import CategoryImage from '../atoms/CategoryImage'
const AssetLink = ({ asset, list }: { asset: any; list?: boolean }) => {
const AssetTeaser = ({
asset,
list,
minimal
}: {
asset: any
list?: boolean
minimal?: boolean
}) => {
const { metadata } = asset.findServiceByType('Metadata')
const { base } = metadata
@ -22,17 +31,22 @@ const AssetLink = ({ asset, list }: { asset: any; list?: boolean }) => {
</Link>
</article>
) : (
<article className={styles.asset}>
<article
className={
minimal ? cx(styles.asset, styles.minimal) : styles.asset
}
>
<Link to={`/asset/${asset.id}`}>
{base.categories && (
<CategoryImage category={base.categories[0]} />
{base.categories && !minimal && (
<CategoryImage dimmed category={base.categories[0]} />
)}
<h1>{base.name}</h1>
<div className={styles.description}>
<Dotdotdot clamp={3}>{base.description}</Dotdotdot>
</div>
{!minimal && (
<div className={styles.description}>
<Dotdotdot clamp={3}>{base.description}</Dotdotdot>
</div>
)}
<footer className={styles.assetFooter}>
{base.categories && <div>{base.categories[0]}</div>}
</footer>
@ -41,4 +55,4 @@ const AssetLink = ({ asset, list }: { asset: any; list?: boolean }) => {
)
}
export default AssetLink
export default AssetTeaser

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import { Link } from 'react-router-dom'
import { Logger } from '@oceanprotocol/squid'
import { User } from '../../context'
import Spinner from '../atoms/Spinner'
import Asset from '../molecules/Asset'
import AssetTeaser from '../molecules/AssetTeaser'
import styles from './AssetsUser.module.scss'
export default class AssetsUser extends PureComponent<
@ -82,7 +82,7 @@ export default class AssetsUser extends PureComponent<
)
.filter(asset => !!asset)
.map((asset: any) => (
<Asset
<AssetTeaser
list={this.props.list}
key={asset.id}
asset={asset}

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
@import '../../styles/variables';
@import '../../../styles/variables';
.metaPrimary {
margin-bottom: $spacer;
@ -41,18 +41,41 @@
.description {
// respect line breaks from textarea
white-space: pre-line;
// handle assets where heading are used extensively in the description
h1 {
font-size: $font-size-h3;
}
h2 {
font-size: $font-size-h4;
}
h3 {
font-size: $font-size-h5;
}
h4,
h5,
h6 {
font-size: $font-size-base;
}
}
.meta {
border-top: 1px solid $brand-grey-lighter;
border-bottom: 1px solid $brand-grey-lighter;
padding-top: $spacer;
padding-bottom: $spacer;
.metaFixed {
border: 1px solid $brand-grey-lighter;
padding: $spacer;
border-radius: $border-radius;
margin-top: $spacer;
margin-bottom: $spacer;
list-style: none;
padding-left: 0;
margin-bottom: $spacer * $line-height;
font-size: $font-size-small;
position: relative;
ul {
padding: 0;
margin: 0;
list-style: none;
}
li {
width: 100%;
@ -60,7 +83,7 @@
@media (min-width: $break-point--small) {
display: flex;
margin-bottom: 0;
margin-bottom: $spacer / 3;
}
&:before {
@ -69,6 +92,17 @@
}
}
.metaFixedTitle {
font-size: $font-size-small;
margin: 0;
font-family: $font-family-base;
font-weight: $font-weight-base;
color: $brand-grey-light;
position: absolute;
bottom: $spacer / 4;
right: $spacer / 4;
}
.metaLabel {
display: block;
@ -85,10 +119,15 @@
overflow-wrap: break-word;
}
/* stylelint-disable declaration-no-important */
code {
display: inline;
display: block;
padding: 0 !important;
background: none !important;
}
/* stylelint-enable declaration-no-important */
@media (min-width: $break-point--small) {
width: 70%;
}

View File

@ -1,8 +1,8 @@
import React, { PureComponent } from 'react'
import { Link } from 'react-router-dom'
import Moment from 'react-moment'
import { DDO, MetaData, File } from '@oceanprotocol/squid'
import Markdown from '../../components/atoms/Markdown'
import Markdown from '../../atoms/Markdown'
import CategoryLink from '../../atoms/CategoryLink'
import styles from './AssetDetails.module.scss'
import AssetFilesDetails from './AssetFilesDetails'
@ -46,10 +46,7 @@ export default class AssetDetails extends PureComponent<AssetDetailsProps> {
</span>
{base.categories && (
// TODO: Make this link to search for respective category
<Link to={`/search?text=${base.categories[0]}`}>
{base.categories[0]}
</Link>
<CategoryLink category={base.categories[0]} />
)}
{base.files && datafilesLine(base.files)}
@ -63,28 +60,40 @@ export default class AssetDetails extends PureComponent<AssetDetailsProps> {
/>
)}
<ul className={styles.meta}>
<li>
<span className={styles.metaLabel}>
<strong>Author</strong>
</span>
<span className={styles.metaValue}>{base.author}</span>
</li>
<li>
<span className={styles.metaLabel}>
<strong>License</strong>
</span>
<span className={styles.metaValue}>{base.license}</span>
</li>
<li>
<span className={styles.metaLabel}>
<strong>DID</strong>
</span>
<span className={styles.metaValue}>
<code>{ddo.id}</code>
</span>
</li>
</ul>
<div className={styles.metaFixed}>
<h2
className={styles.metaFixedTitle}
title="This metadata can not be changed because it is used to generate the checksums for the DDO, and to encrypt the file urls."
>
Fixed Metadata
</h2>
<ul>
<li>
<span className={styles.metaLabel}>
<strong>Author</strong>
</span>
<span className={styles.metaValue}>
{base.author}
</span>
</li>
<li>
<span className={styles.metaLabel}>
<strong>License</strong>
</span>
<span className={styles.metaValue}>
{base.license}
</span>
</li>
<li>
<span className={styles.metaLabel}>
<strong>DID</strong>
</span>
<span className={styles.metaValue}>
<code>{ddo.id}</code>
</span>
</li>
</ul>
</div>
<AssetFilesDetails
files={base.files ? base.files : []}

View File

@ -1,4 +1,4 @@
@import '../../styles/variables';
@import '../../../styles/variables';
.buttonMain {
margin: auto;
@ -25,13 +25,16 @@
.file {
display: inline-block;
background: $brand-grey;
background: $brand-grey-dark
url('../../../../node_modules/@oceanprotocol/art/jellyfish/jellyfish-grid.svg')
no-repeat -1rem 4.5rem;
background-size: 100%;
padding: $spacer $spacer / 2;
margin-bottom: $spacer / 2;
text-align: left;
position: relative;
height: 8rem;
width: 6rem;
height: 8.5rem;
width: 6.5rem;
&:before {
content: '';
@ -54,6 +57,12 @@
}
}
.empty {
font-size: $font-size-mini;
font-weight: $font-weight-base;
opacity: .75;
}
// move spinner a bit up
+ div {
margin-top: $spacer / 2;

View File

@ -5,13 +5,13 @@ import { render, fireEvent } from 'react-testing-library'
import { DDO } from '@oceanprotocol/squid'
import { StateMock } from '@react-mock/state'
import ReactGA from 'react-ga'
import { User } from '../../context'
import { User } from '../../../context'
import AssetFile, { messages } from './AssetFile'
const file = {
index: 0,
url: 'https://hello.com',
contentType: 'zip',
contentType: 'application/x-zip',
contentLength: 100
}

View File

@ -1,11 +1,12 @@
import React, { PureComponent } from 'react'
import { Logger, DDO, File } from '@oceanprotocol/squid'
import filesize from 'filesize'
import Button from '../../components/atoms/Button'
import Spinner from '../../components/atoms/Spinner'
import { User } from '../../context'
import Button from '../../atoms/Button'
import Spinner from '../../atoms/Spinner'
import { User } from '../../../context'
import styles from './AssetFile.module.scss'
import ReactGA from 'react-ga'
import cleanupContentType from '../../../utils/cleanupContentType'
export const messages = {
start: 'Decrypting file URL...',
@ -96,19 +97,27 @@ export default class AssetFile extends PureComponent<
const { ddo, file } = this.props
const { isLoading, error, step } = this.state
const { isLogged, isOceanNetwork } = this.context
const { index } = file
const { index, contentType, contentLength } = file
return (
<div className={styles.fileWrap}>
<ul key={index} className={styles.file}>
<li>
{file.contentType && file.contentType.split('/')[1]}
</li>
<li>
{file.contentLength && filesize(file.contentLength)}
</li>
{/* <li>{file.encoding}</li> */}
{/* <li>{file.compression}</li> */}
{contentType || contentLength ? (
<>
<li>
{contentType && cleanupContentType(contentType)}
</li>
<li>
{contentLength && contentLength > 0
? filesize(contentLength)
: ''}
</li>
{/* <li>{encoding}</li> */}
{/* <li>{compression}</li> */}
</>
) : (
<li className={styles.empty}>No file info available</li>
)}
</ul>
{isLoading ? (

View File

@ -1,4 +1,4 @@
@import '../../styles/variables';
@import '../../../styles/variables';
.files {
text-align: center;

View File

@ -4,8 +4,8 @@ import React from 'react'
import { render } from 'react-testing-library'
import { DDO } from '@oceanprotocol/squid'
import AssetFilesDetails from './AssetFilesDetails'
import { User } from '../../context'
import { userMockConnected } from '../../../__mocks__/user-mock'
import { User } from '../../../context'
import { userMockConnected } from '../../../../__mocks__/user-mock'
describe('AssetFilesDetails', () => {
it('renders without crashing', () => {

View File

@ -1,8 +1,8 @@
import React, { PureComponent } from 'react'
import { DDO, File } from '@oceanprotocol/squid'
import AssetFile from './AssetFile'
import { User } from '../../context'
import Web3message from '../../components/organisms/Web3message'
import { User } from '../../../context'
import Web3message from '../../organisms/Web3message'
import styles from './AssetFilesDetails.module.scss'
export default class AssetFilesDetails extends PureComponent<{

View File

@ -0,0 +1,12 @@
@import '../../../styles/variables';
.error {
text-align: center;
margin: 20vh auto 0 auto;
background: $red;
border-radius: $border-radius;
padding: $spacer / 2;
width: fit-content;
color: $brand-white;
font-weight: $font-weight-bold;
}

View File

@ -12,7 +12,11 @@ describe('Details', () => {
state: '',
hash: ''
}}
match={{ params: { did: '' } }}
match={{
params: {
did: ''
}
}}
/>
)
expect(container.firstChild).toBeInTheDocument()

View File

@ -0,0 +1,83 @@
import React, { Component } from 'react'
import { DDO, MetaData, Logger } from '@oceanprotocol/squid'
import Route from '../Route'
import Spinner from '../../atoms/Spinner'
import { User } from '../../../context'
import AssetDetails from './AssetDetails'
import stylesApp from '../../../App.module.scss'
import Content from '../../atoms/Content'
import CategoryImage from '../../atoms/CategoryImage'
import styles from './index.module.scss'
interface AssetProps {
location: Location
match: {
params: {
did: string
}
}
}
interface AssetState {
ddo: DDO
metadata: MetaData
error: string
}
export default class Asset extends Component<AssetProps, AssetState> {
public state = {
ddo: ({} as any) as DDO,
metadata: ({ base: { name: '' } } as any) as MetaData,
error: ''
}
public async componentDidMount() {
this.getData()
}
private async getData() {
try {
const { ocean } = this.context
const ddo = await ocean.assets.resolve(this.props.match.params.did)
const { metadata } = ddo.findServiceByType('Metadata')
this.setState({ ddo, metadata })
} catch (error) {
Logger.error(error.message)
this.setState({
error: `We encountered an error: ${error.message}.`
})
}
}
public render() {
const { metadata, ddo, error } = this.state
const isLoading = metadata.base.name === ''
return isLoading ? (
<div className={stylesApp.loader}>
<Spinner message={'Loading asset...'} />
</div>
) : error !== '' ? (
<div className={styles.error}>{error}</div>
) : (
<Route
title={metadata.base.name}
image={
metadata.base.categories && (
<CategoryImage
header
dimmed
category={metadata.base.categories[0]}
/>
)
}
>
<Content>
<AssetDetails metadata={metadata} ddo={ddo} />
</Content>
</Route>
)
}
}
Asset.contextType = User

View File

@ -0,0 +1,21 @@
@import '../../styles/variables';
.results {
display: grid;
grid-template-columns: 1fr;
grid-gap: $spacer;
max-width: calc(18rem + #{$spacer * 2});
margin: auto;
margin-top: $spacer * 2;
@media (min-width: $break-point--small) {
margin-left: 0;
margin-right: 0;
max-width: none;
grid-template-columns: repeat(2, 1fr);
}
@media (min-width: $break-point--medium) {
grid-template-columns: repeat(3, 1fr);
}
}

View File

@ -0,0 +1,48 @@
import React from 'react'
import { render } from 'react-testing-library'
import Channel from './Channel'
import { User } from '../../context'
import { createMemoryHistory } from 'history'
describe('Channel', () => {
it('renders without crashing', () => {
const history = createMemoryHistory()
const { container } = render(
<User.Provider
value={{
isLogged: false,
isLoading: false,
isWeb3: false,
isOceanNetwork: false,
account: '',
web3: {},
ocean: {
aquarius: {
queryMetadata: () => {
return {
results: [],
totalResults: 1,
totalPages: 1
}
}
}
},
balance: { eth: 0, ocn: 0 },
network: '',
requestFromFaucet: () => {},
unlockAccounts: () => {},
message: ''
}}
>
<Channel
match={{
params: { channel: 'ai-for-good' }
}}
history={history}
/>
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -0,0 +1,130 @@
import React, { PureComponent } from 'react'
import { Logger } from '@oceanprotocol/squid'
import { History } from 'history'
import Spinner from '../../components/atoms/Spinner'
import Route from '../../components/templates/Route'
import { User } from '../../context'
import AssetTeaser from '../molecules/AssetTeaser'
import Pagination from '../../components/molecules/Pagination'
import styles from './Channel.module.scss'
import Content from '../../components/atoms/Content'
import channels from '../../data/channels.json'
import CategoryImage from '../atoms/CategoryImage'
interface ChannelProps {
history: History
match: {
params: {
channel: string
}
}
}
interface ChannelState {
results: any[]
totalResults: number
offset: number
totalPages: number
currentPage: number
isLoading: boolean
title: string
description: string
}
export default class Channel extends PureComponent<ChannelProps, ChannelState> {
// get content data based on received channel param
public channel = channels.items
.filter(({ tag }) => tag === this.props.match.params.channel)
.map(channel => channel)[0]
public state = {
results: [],
totalResults: 0,
offset: 25,
totalPages: 1,
currentPage: 1,
isLoading: true,
title: this.channel.title,
description: this.channel.description
}
public async componentDidMount() {
this.getChannelAssets()
}
private getChannelAssets = async () => {
const { ocean } = this.context
const { offset, currentPage } = this.state
const searchQuery = {
offset,
page: currentPage,
query: {
tags: [this.channel.tag]
},
sort: {
created: -1
}
}
try {
const search = await ocean.aquarius.queryMetadata(searchQuery)
this.setState({
results: search.results,
totalResults: search.totalResults,
totalPages: search.totalPages,
isLoading: false
})
} catch (error) {
Logger.error(error.message)
this.setState({ isLoading: false })
}
}
private handlePageClick = async (data: { selected: number }) => {
// react-pagination starts counting at 0, we start at 1
let toPage = data.selected + 1
this.props.history.push({ search: `?page=${toPage}` })
await this.setState({ currentPage: toPage, isLoading: true })
await this.getChannelAssets()
}
public renderResults = () =>
this.state.isLoading ? (
<Spinner message="Searching..." />
) : this.state.results && this.state.results.length ? (
<div className={styles.results}>
{this.state.results.map((asset: any) => (
<AssetTeaser key={asset.id} asset={asset} />
))}
</div>
) : (
<div>No data sets found.</div>
)
public render() {
const { title, description, totalPages, currentPage } = this.state
return (
<Route
title={title}
description={description}
image={<CategoryImage header category={title} />}
>
<Content wide>
{this.renderResults()}
<Pagination
totalPages={totalPages}
currentPage={currentPage}
handlePageClick={this.handlePageClick}
/>
</Content>
</Route>
)
}
}
Channel.contextType = User

View File

@ -12,9 +12,15 @@
@media (min-width: $break-point--small) {
font-size: $font-size-h1;
}
// category image
+ div:not(.description) {
margin-bottom: $spacer;
}
}
.description {
margin-top: $spacer / 2;
font-size: $font-size-large;
color: $brand-grey;
}

View File

@ -3,16 +3,19 @@ import Helmet from 'react-helmet'
import Content from '../atoms/Content'
import styles from './Route.module.scss'
import meta from '../../data/meta.json'
import Markdown from '../atoms/Markdown'
const Route = ({
title,
description,
image,
wide,
children,
className
}: {
title: string
description?: string
image?: any
children: any
wide?: boolean
className?: string
@ -24,23 +27,24 @@ const Route = ({
{description && <meta name="description" content={description} />}
</Helmet>
<Content wide={wide}>
<article>
<header className={styles.header}>
<article>
<header className={styles.header}>
<Content wide={wide}>
<h1 className={styles.title}>{title}</h1>
{image && image}
{description && (
<p
<Markdown
text={description}
className={styles.description}
dangerouslySetInnerHTML={{
__html: description
}}
/>
)}
</header>
</Content>
</header>
{children}
</article>
</Content>
{children}
</article>
</div>
)

View File

@ -45,33 +45,32 @@ export const faucetPort = process.env.REACT_APP_FAUCET_PORT || 443
// OCEAN LOCAL CONNECTIONS
// e.g. when running with barge
//
// export const nodeScheme = 'http'
// export const nodeHost = 'localhost'
// export const nodePort = 8545
/*
export const nodeScheme = 'http'
export const nodeHost = 'localhost'
export const nodePort = 8545
// export const aquariusScheme = 'http'
// export const aquariusHost = 'aquarius'
// export const aquariusPort = 5000
export const aquariusScheme = 'http'
export const aquariusHost = 'aquarius'
export const aquariusPort = 5000
// export const brizoScheme = 'http'
// export const brizoHost = 'localhost'
// export const brizoPort = 8030
export const brizoScheme = 'http'
export const brizoHost = 'localhost'
export const brizoPort = 8030
export const brizoAddress = '0x00bd138abd70e2f00903268f3db08f2d25677c9e'
// export const parityScheme = 'http'
// export const parityHost = 'localhost'
// export const parityPort = 8545
// export const threshold = 0
// export const password = 'node0'
// export const address = '0x00bd138abd70e2f00903268f3db08f2d25677c9e'
export const parityScheme = 'http'
export const parityHost = 'localhost'
export const parityPort = 8545
// export const secretStoreScheme = 'http'
// export const secretStoreHost = 'localhost'
// export const secretStorePort = 12001
// export const faucetScheme = 'http'
// export const faucetHost = 'localhost'
// export const faucetPort = 3001
export const secretStoreScheme = 'http'
export const secretStoreHost = 'localhost'
export const secretStorePort = 12001
export const faucetScheme = 'http'
export const faucetHost = 'localhost'
export const faucetPort = 3001
*/
export const verbose = true
//

View File

@ -1,6 +1,13 @@
import React, { PureComponent } from 'react'
import { Logger, Ocean } from '@oceanprotocol/squid'
import { Market } from '.'
import formPublish from '../data/form-publish.json'
const categories =
(formPublish.steps[1].fields &&
formPublish.steps[1].fields.categories &&
formPublish.steps[1].fields.categories.options) ||
[]
interface MarketProviderProps {
ocean: Ocean
@ -8,6 +15,7 @@ interface MarketProviderProps {
interface MarketProviderState {
totalAssets: number
categories: string[]
}
export default class MarketProvider extends PureComponent<
@ -15,7 +23,8 @@ export default class MarketProvider extends PureComponent<
MarketProviderState
> {
public state = {
totalAssets: 0
totalAssets: 0,
categories
}
public async componentDidMount() {}
@ -32,9 +41,7 @@ export default class MarketProvider extends PureComponent<
const searchQuery = {
offset: 1,
page: 1,
query: {
price: [-1, 1]
},
query: {},
sort: {
value: 1
}

View File

@ -22,4 +22,4 @@ export const User = React.createContext({
message: ''
})
export const Market = React.createContext({ totalAssets: 0 })
export const Market = React.createContext({ totalAssets: 0, categories: [''] })

View File

@ -0,0 +1,12 @@
{
"title": "Channels",
"description": "Channels are curated collections of existing data sets from multiple categories, showing them in one prominent view.\n\nInterested in your own channel? Let us know [@oceanprotocol](https://twitter.com/intent/tweet?text=%40oceanprotocol%20Hey%2C%20I%20am%20interested%20in%20creating%20a%20Commons%20Channel&url=https://commons.oceanprotocol.com/channels/).",
"items": [
{
"title": "AI For Good",
"tag": "ai-for-good",
"teaser": "AI for Good is an initiative to promote the use of artificial intelligence for good causes.",
"description": "[AI for Good](https://ai4good.org) is an initiative to promote the use of artificial intelligence for taking action on the UN SDGs such as fighting poverty, climate change, improving healthcare, etc. The AI for Good Commons channel is aimed at helping scale AI for Good by enabling open datasets to be published and shared and connecting AI problem owners with problem solvers.\n\nThink you have an asset which would fit here? Let us know [@oceanprotocol](https://twitter.com/intent/tweet?text=%40oceanprotocol%20Hey%2C%20I%20have%20an%20asset%20I%20would%20like%20to%20add%20to%20the%20AI%20for%20Good%20Channel%20on%20Commons%20https%3A%2F%2Fcommons.oceanprotocol.com%2Fchannels%2Fai-for-good)."
}
]
}

View File

@ -64,7 +64,7 @@
"Agriculture & Bio Engineering",
"Transportation",
"Urban Planning",
"Medicine",
"Health & Medicine",
"Business & Management",
"Sports & Recreation",
"Communication & Journalism",

View File

@ -1,4 +1,8 @@
[
{
"title": "Channels",
"link": "/channels"
},
{
"title": "Publish",
"link": "/publish"

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 182 KiB

View File

@ -1,5 +1,6 @@
import React, { Component } from 'react'
import Route from '../components/templates/Route'
import Content from '../components/atoms/Content'
class About extends Component {
public render() {
@ -8,28 +9,31 @@ class About extends Component {
title="About"
description="A marketplace to find and publish open data sets in the Ocean Network."
>
<p>
Commons is built on top of the Ocean{' '}
<a href="https://docs.oceanprotocol.com/concepts/testnets/#the-nile-testnet">
Nile test network
</a>{' '}
and is targeted at enthusiastic data scientists with some
crypto experience. It can be used with any Web3-capable
browser, like Firefox with MetaMask installed.
</p>
<Content>
<p>
Commons is built on top of the Ocean{' '}
<a href="https://docs.oceanprotocol.com/concepts/testnets/#the-nile-testnet">
Nile test network
</a>{' '}
and is targeted at enthusiastic data scientists with
some crypto experience. It can be used with any
Web3-capable browser, like Firefox with MetaMask
installed.
</p>
<ul>
<li>
<a href="https://blog.oceanprotocol.com/the-commons-marketplace-c57a44288314">
Read the blog post
</a>
</li>
<li>
<a href="https://github.com/oceanprotocol/commons">
Check out oceanprotocol/commons on GitHub
</a>
</li>
</ul>
<ul>
<li>
<a href="https://blog.oceanprotocol.com/the-commons-marketplace-c57a44288314">
Read the blog post
</a>
</li>
<li>
<a href="https://github.com/oceanprotocol/commons">
Check out oceanprotocol/commons on GitHub
</a>
</li>
</ul>
</Content>
</Route>
)
}

View File

@ -0,0 +1,15 @@
import React from 'react'
import { BrowserRouter as Router } from 'react-router-dom'
import { render } from 'react-testing-library'
import Channels from './Channels'
describe('Channels', () => {
it('renders without crashing', () => {
const { container } = render(
<Router>
<Channels />
</Router>
)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -0,0 +1,24 @@
import React, { Component } from 'react'
import Route from '../components/templates/Route'
import Content from '../components/atoms/Content'
import channels from '../data/channels.json'
import ChannelTeaser from '../components/organisms/ChannelTeaser'
class Channels extends Component {
public render() {
return (
<Route title={channels.title} description={channels.description}>
<Content wide>
{channels.items.map(channel => (
<ChannelTeaser
key={channel.title}
channel={channel.tag}
/>
))}
</Content>
</Route>
)
}
}
export default Channels

View File

@ -1,59 +0,0 @@
import React, { Component } from 'react'
import { DDO, MetaData, Logger } from '@oceanprotocol/squid'
import Route from '../../components/templates/Route'
import Spinner from '../../components/atoms/Spinner'
import { User } from '../../context'
import AssetDetails from './AssetDetails'
import stylesApp from '../../App.module.scss'
interface DetailsProps {
location: Location
match: {
params: {
did: string
}
}
}
interface DetailsState {
ddo: DDO
metadata: MetaData
}
export default class Details extends Component<DetailsProps, DetailsState> {
public state = {
ddo: ({} as any) as DDO,
metadata: ({ base: { name: '' } } as any) as MetaData
}
public async componentDidMount() {
this.getData()
}
private async getData() {
try {
const { ocean } = this.context
const ddo = await ocean.assets.resolve(this.props.match.params.did)
const { metadata } = ddo.findServiceByType('Metadata')
this.setState({ ddo, metadata })
} catch (error) {
Logger.error(error.message)
}
}
public render() {
const { metadata, ddo } = this.state
return metadata.base.name !== '' ? (
<Route title={metadata.base.name}>
<AssetDetails metadata={metadata} ddo={ddo} />
</Route>
) : (
<div className={stylesApp.loader}>
<Spinner message={'Loading asset...'} />
</div>
)
}
}
Details.contextType = User

View File

@ -6,6 +6,7 @@ import Spinner from '../components/atoms/Spinner'
import { User } from '../context'
import Web3message from '../components/organisms/Web3message'
import styles from './Faucet.module.scss'
import Content from '../components/atoms/Content'
interface FaucetState {
isLoading: boolean
@ -101,19 +102,21 @@ export default class Faucet extends PureComponent<{}, FaucetState> {
title="Faucet"
description="Shower yourself with some Ether for Ocean's Nile test network."
>
<Web3message />
<Content>
<Web3message />
<div className={styles.action}>
{isLoading ? (
<Spinner message="Getting Ether..." />
) : error ? (
<this.Error />
) : success ? (
<this.Success />
) : (
isWeb3 && <this.Action />
)}
</div>
<div className={styles.action}>
{isLoading ? (
<Spinner message="Getting Ether..." />
) : error ? (
<this.Error />
) : success ? (
<this.Success />
) : (
isWeb3 && <this.Action />
)}
</div>
</Content>
</Route>
)
}

View File

@ -3,16 +3,18 @@ import Route from '../components/templates/Route'
import AssetsUser from '../components/organisms/AssetsUser'
import Web3message from '../components/organisms/Web3message'
import { User } from '../context'
import Content from '../components/atoms/Content'
export default class History extends Component {
public render() {
return (
<Route title="History">
{(!this.context.isLogged || !this.context.isOceanNetwork) && (
<Web3message />
)}
<Content>
{(!this.context.isLogged ||
!this.context.isOceanNetwork) && <Web3message />}
<AssetsUser list />
<AssetsUser list />
</Content>
</Route>
)
}

View File

@ -12,4 +12,49 @@
margin: 0;
visibility: hidden;
}
// grab the channel teaser on front page
// stylelint-disable-next-line
article > div > h2 + div {
padding-top: $spacer / 2;
}
}
.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;
@media (min-width: $break-point--small) {
text-align: left;
}
}
.categories {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
grid-gap: $spacer;
}
.category {
h3 {
font-size: $font-size-base;
color: $brand-grey;
margin-top: -($spacer / 3);
margin-bottom: 0;
}
&:hover,
&:focus {
transform: none;
> div {
opacity: 1;
background-size: 105%;
}
}
}

View File

@ -1,10 +1,22 @@
import React from 'react'
import { Router } from 'react-router'
import { createBrowserHistory } from 'history'
import { render } from 'react-testing-library'
import Home from './Home'
import { userMock } from '../../__mocks__/user-mock'
import { User } from '../context'
const history = createBrowserHistory()
describe('Home', () => {
it('renders without crashing', () => {
const { container } = render(<Home history={''} />)
const { container } = render(
<User.Provider value={{ ...userMock }}>
<Router history={history}>
<Home history={history} />
</Router>
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()
})
})

View File

@ -1,13 +1,18 @@
import React, { ChangeEvent, Component, FormEvent } from 'react'
import { History } from 'history'
import { User, Market } from '../context'
import CategoryImage from '../components/atoms/CategoryImage'
import CategoryLink from '../components/atoms/CategoryLink'
import Button from '../components/atoms/Button'
import Form from '../components/atoms/Form/Form'
import Input from '../components/atoms/Form/Input'
import Route from '../components/templates/Route'
import AssetsUser from '../components/organisms/AssetsUser'
import styles from './Home.module.scss'
import meta from '../data/meta.json'
import { History } from 'history'
import Content from '../components/atoms/Content'
import AssetsLatest from '../components/organisms/AssetsLatest'
import ChannelTeaser from '../components/organisms/ChannelTeaser'
interface HomeProps {
history: History
@ -17,34 +22,11 @@ interface HomeState {
search?: string
}
class Home extends Component<HomeProps, HomeState> {
public state = { search: '' }
export default class Home extends Component<HomeProps, HomeState> {
public static contextType = User
public render() {
return (
<Route
title={meta.title}
description={meta.description}
className={styles.home}
>
<Form onSubmit={this.searchAssets} minimal>
<Input
type="search"
name="search"
label="Search for data sets"
placeholder="e.g. shapes of plants"
value={this.state.search}
onChange={this.inputChange}
group={
<Button primary disabled={!this.state.search}>
Search
</Button>
}
/>
</Form>
<AssetsUser recent={5} list />
</Route>
)
public state = {
search: ''
}
private inputChange = (event: ChangeEvent<HTMLInputElement>) => {
@ -57,6 +39,64 @@ class Home extends Component<HomeProps, HomeState> {
event.preventDefault()
this.props.history.push(`/search?text=${this.state.search}`)
}
}
export default Home
public render() {
const { search } = this.state
return (
<Route
title={meta.title}
description={meta.description}
className={styles.home}
>
<Content>
<Form onSubmit={this.searchAssets} minimal>
<Input
type="search"
name="search"
label="Search for data sets"
placeholder="e.g. shapes of plants"
value={search}
onChange={this.inputChange}
group={
<Button primary disabled={!search}>
Search
</Button>
}
/>
</Form>
</Content>
<Content wide>
<h2 className={styles.title}>Featured Channel</h2>
<ChannelTeaser channel="ai-for-good" />
<AssetsLatest />
</Content>
<Content wide>
<h2 className={styles.title}>Explore Categories</h2>
<div className={styles.categories}>
<Market.Consumer>
{({ categories }) =>
categories
.sort((a, b) => a.localeCompare(b)) // sort alphabetically
.map((category: string) => (
<CategoryLink
category={category}
key={category}
className={styles.category}
>
<CategoryImage
category={category}
/>
<h3>{category}</h3>
</CategoryLink>
))
}
</Market.Consumer>
</div>
</Content>
</Route>
)
}
}

View File

@ -1,9 +1,14 @@
import React, { Component } from 'react'
import Route from '../components/templates/Route'
import Content from '../components/atoms/Content'
class NotFound extends Component {
public render() {
return <Route title="404 - Not Found">Not Found</Route>
return (
<Route title="404 - Not Found">
<Content>Not Found</Content>
</Route>
)
}
}

View File

@ -1,6 +1,6 @@
import React from 'react'
import { render, fireEvent, waitForElement } from 'react-testing-library'
import Files, { getFileCompression } from '.'
import Files from '.'
const onChange = jest.fn()
@ -71,20 +71,3 @@ describe('Files', () => {
fireEvent.click(getByText('Add File'))
})
})
describe('getFileCompression', () => {
it('outputs known compression', async () => {
const compression = await getFileCompression('application/zip')
expect(compression).toBe('zip')
})
it('outputs known x- compression', async () => {
const compression = await getFileCompression('application/x-gtar')
expect(compression).toBe('gtar')
})
it('outputs unknown compression', async () => {
const compression = await getFileCompression('blabla')
expect(compression).toBe('none')
})
})

View File

@ -7,6 +7,7 @@ import Item from './Item'
import styles from './index.module.scss'
import { serviceHost, servicePort, serviceScheme } from '../../../config'
import cleanupContentType from '../../../utils/cleanupContentType'
interface File {
url: string
@ -38,32 +39,6 @@ interface FilesStates {
isFormShown: boolean
}
export const getFileCompression = async (contentType: string) => {
// TODO: add all the possible archive & compression MIME types
if (
contentType === 'application/zip' ||
contentType === 'application/gzip' ||
contentType === 'application/x-lzma' ||
contentType === 'application/x-xz' ||
contentType === 'application/x-tar' ||
contentType === 'application/x-gtar' ||
contentType === 'application/x-bzip2' ||
contentType === 'application/x-7z-compressed' ||
contentType === 'application/x-rar-compressed' ||
contentType === 'application/x-apple-diskimage'
) {
const contentTypeSplit = contentType.split('/')
if (contentTypeSplit[1].includes('x-')) {
return contentTypeSplit[1].replace('x-', '')
}
return contentTypeSplit[1]
} else {
return 'none'
}
}
export default class Files extends PureComponent<FilesProps, FilesStates> {
public state: FilesStates = {
isFormShown: false
@ -106,7 +81,7 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
res = await response.json()
file.contentLength = res.result.contentLength
file.contentType = res.result.contentType
file.compression = await getFileCompression(res.result.contentType)
file.compression = await cleanupContentType(res.result.contentType)
file.found = res.result.found
} catch (error) {
// error

View File

@ -10,6 +10,7 @@ import Progress from './Progress'
import ReactGA from 'react-ga'
import { steps } from '../../data/form-publish.json'
import Content from '../../components/atoms/Content'
type AssetType = 'dataset' | 'algorithm' | 'container' | 'workflow' | 'other'
@ -319,33 +320,37 @@ class Publish extends Component<{}, PublishState> {
title="Publish"
description="Publish a new data set into the Ocean Protocol Network."
>
{(!this.context.isLogged || !this.context.isOceanNetwork) && (
<Web3message />
)}
<Content>
{(!this.context.isLogged ||
!this.context.isOceanNetwork) && <Web3message />}
<Progress steps={steps} currentStep={this.state.currentStep} />
<Progress
steps={steps}
currentStep={this.state.currentStep}
/>
<Form onSubmit={this.registerAsset}>
{steps.map((step: any, index: number) => (
<Step
key={index}
index={index}
title={step.title}
description={step.description}
currentStep={this.state.currentStep}
fields={step.fields}
inputChange={this.inputChange}
inputToArrayChange={this.inputToArrayChange}
state={this.state}
next={this.next}
prev={this.prev}
totalSteps={steps.length}
tryAgain={this.tryAgain}
toStart={this.toStart}
content={step.content}
/>
))}
</Form>
<Form onSubmit={this.registerAsset}>
{steps.map((step: any, index: number) => (
<Step
key={index}
index={index}
title={step.title}
description={step.description}
currentStep={this.state.currentStep}
fields={step.fields}
inputChange={this.inputChange}
inputToArrayChange={this.inputToArrayChange}
state={this.state}
next={this.next}
prev={this.prev}
totalSteps={steps.length}
tryAgain={this.tryAgain}
toStart={this.toStart}
content={step.content}
/>
))}
</Form>
</Content>
</Route>
)
}

View File

@ -17,6 +17,16 @@
grid-gap: $spacer;
@media (min-width: $break-point--small) {
grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));
grid-template-columns: repeat(2, 1fr);
}
@media (min-width: $break-point--medium) {
grid-template-columns: repeat(3, 1fr);
}
}
.empty {
text-align: center;
margin-top: $spacer * 4;
color: $brand-grey-light;
}

View File

@ -3,6 +3,7 @@ import { render } from 'react-testing-library'
import Search from './Search'
import { User } from '../context'
import { createMemoryHistory } from 'history'
import { BrowserRouter as Router } from 'react-router-dom'
describe('Search', () => {
it('renders without crashing', () => {
@ -35,15 +36,17 @@ describe('Search', () => {
message: ''
}}
>
<Search
location={{
search: '?text=Hello&page=1',
pathname: '/search',
state: '',
hash: ''
}}
history={history}
/>
<Router>
<Search
location={{
search: '?text=Hello&page=1',
pathname: '/search',
state: '',
hash: ''
}}
history={history}
/>
</Router>
</User.Provider>
)
expect(container.firstChild).toBeInTheDocument()

View File

@ -1,13 +1,15 @@
import React, { PureComponent } from 'react'
import { Link } from 'react-router-dom'
import queryString from 'query-string'
import { History, Location } from 'history'
import { Logger } from '@oceanprotocol/squid'
import Spinner from '../components/atoms/Spinner'
import Route from '../components/templates/Route'
import { User } from '../context'
import Asset from '../components/molecules/Asset'
import AssetTeaser from '../components/molecules/AssetTeaser'
import Pagination from '../components/molecules/Pagination'
import styles from './Search.module.scss'
import Content from '../components/atoms/Content'
interface SearchProps {
location: Location
@ -22,6 +24,7 @@ interface SearchState {
currentPage: number
isLoading: boolean
searchTerm: string
searchCategories: string
}
export default class Search extends PureComponent<SearchProps, SearchState> {
@ -32,21 +35,29 @@ export default class Search extends PureComponent<SearchProps, SearchState> {
totalPages: 1,
currentPage: 1,
isLoading: true,
searchTerm: ''
searchTerm: '',
searchCategories: ''
}
public async componentDidMount() {
const searchTerm = await queryString.parse(this.props.location.search)
.text
const searchPage = queryString.parse(this.props.location.search).page
const { search } = this.props.location
const { text, page, categories } = queryString.parse(search)
await this.setState({
searchTerm: encodeURIComponent(`${searchTerm}`)
})
if (text) {
await this.setState({
searchTerm: decodeURIComponent(`${text}`)
})
}
if (categories) {
await this.setState({
searchCategories: decodeURIComponent(`${categories}`)
})
}
// switch to respective page if query string is present
if (searchPage) {
const currentPage = Number(searchPage)
if (page) {
const currentPage = Number(page)
await this.setState({ currentPage })
}
@ -55,16 +66,23 @@ export default class Search extends PureComponent<SearchProps, SearchState> {
private searchAssets = async () => {
const { ocean } = this.context
const { offset, currentPage, searchTerm, searchCategories } = this.state
const queryValues =
searchCategories !== '' && searchTerm !== ''
? { text: [searchTerm], categories: [searchCategories] }
: searchCategories !== '' && searchTerm === ''
? { categories: [searchCategories] }
: { text: [searchTerm] }
const searchQuery = {
offset: this.state.offset,
page: this.state.currentPage,
offset,
page: currentPage,
query: {
text: [decodeURIComponent(this.state.searchTerm)],
price: [-1, 1]
...queryValues
},
sort: {
datePublished: 1
created: -1
}
}
@ -77,7 +95,7 @@ export default class Search extends PureComponent<SearchProps, SearchState> {
isLoading: false
})
} catch (error) {
Logger.error(error)
Logger.error(error.message)
this.setState({ isLoading: false })
}
}
@ -101,11 +119,14 @@ export default class Search extends PureComponent<SearchProps, SearchState> {
) : this.state.results && this.state.results.length ? (
<div className={styles.results}>
{this.state.results.map((asset: any) => (
<Asset key={asset.id} asset={asset} />
<AssetTeaser key={asset.id} asset={asset} />
))}
</div>
) : (
<div>No data sets found.</div>
<div className={styles.empty}>
<p>No Data Sets Found.</p>
<Link to="/publish">+ Publish A Data Set</Link>
</div>
)
public render() {
@ -113,23 +134,26 @@ export default class Search extends PureComponent<SearchProps, SearchState> {
return (
<Route title="Search" wide>
{totalResults > 0 && (
<h2
className={styles.resultsTitle}
dangerouslySetInnerHTML={{
__html: `${totalResults} results for <span>${decodeURIComponent(
this.state.searchTerm
)}</span>`
}}
/>
)}
{this.renderResults()}
<Content wide>
{!this.state.isLoading && (
<h2 className={styles.resultsTitle}>
{totalResults} results for{' '}
<span>
{decodeURIComponent(
this.state.searchTerm ||
this.state.searchCategories
)}
</span>
</h2>
)}
{this.renderResults()}
<Pagination
totalPages={totalPages}
currentPage={currentPage}
handlePageClick={this.handlePageClick}
/>
<Pagination
totalPages={totalPages}
currentPage={currentPage}
handlePageClick={this.handlePageClick}
/>
</Content>
</Route>
)
}

View File

@ -5,6 +5,7 @@ import Input from '../components/atoms/Form/Input'
import Route from '../components/templates/Route'
import styles from './Styleguide.module.scss'
import form from '../data/form-styleguide.json'
import Content from '../components/atoms/Content'
class Styleguide extends Component {
public formFields = (entries: any[]) =>
@ -25,18 +26,22 @@ class Styleguide extends Component {
const entries = Object.entries(form.fields)
return (
<Route title="Styleguide" className={styles.styleguide}>
<div className={styles.buttons}>
<Button>I am a button</Button>
<Button primary>I am a primary button</Button>
<Button href="https://hello.com">
I am a link disguised as a button
</Button>
<Button link>I am a button disguised as a text link</Button>
</div>
<Content>
<div className={styles.buttons}>
<Button>I am a button</Button>
<Button primary>I am a primary button</Button>
<Button href="https://hello.com">
I am a link disguised as a button
</Button>
<Button link>
I am a button disguised as a text link
</Button>
</div>
<Form title={form.title} description={form.description}>
{this.formFields(entries)}
</Form>
<Form title={form.title} description={form.description}>
{this.formFields(entries)}
</Form>
</Content>
</Route>
)
}

View File

@ -28,7 +28,7 @@ $font-family-title: 'Sharp Sans Display', -apple-system, BlinkMacSystemFont,
$font-family-monospace: 'Fira Code', 'Fira Mono', Menlo, Monaco, Consolas,
'Courier New', monospace;
$font-size-root: 15px;
$font-size-root: 16px;
$font-size-base: 1rem;
$font-size-large: 1.2rem;
$font-size-small: .85rem;

View File

@ -0,0 +1,18 @@
import cleanupContentType from './cleanupContentType'
describe('getFileCompression', () => {
it('outputs known compression', async () => {
const compression = await cleanupContentType('application/zip')
expect(compression).toBe('zip')
})
it('outputs known x- compression', async () => {
const compression = await cleanupContentType('application/x-gtar')
expect(compression).toBe('gtar')
})
it('pass through unknown compression', async () => {
const compression = await cleanupContentType('blabla')
expect(compression).toBe('blabla')
})
})

View File

@ -0,0 +1,43 @@
const cleanupContentType = (contentType: string) => {
// strip away the 'application/' part
const contentTypeSplit = contentType.split('/')[1]
if (!contentTypeSplit) return contentType
let contentTypeCleaned
// TODO: add all the possible archive & compression MIME types
if (
contentType === 'application/x-lzma' ||
contentType === 'application/x-xz' ||
contentType === 'application/x-tar' ||
contentType === 'application/x-gtar' ||
contentType === 'application/x-bzip2' ||
contentType === 'application/x-gzip' ||
contentType === 'application/x-7z-compressed' ||
contentType === 'application/x-rar-compressed' ||
contentType === 'application/x-zip-compressed' ||
contentType === 'application/x-apple-diskimage'
) {
contentTypeCleaned = contentTypeSplit
.replace('x-', '')
.replace('-compressed', '')
} else {
contentTypeCleaned = contentTypeSplit
}
// Manual replacements
contentTypeCleaned = contentTypeCleaned
.replace(
'vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'xlsx'
)
.replace('vnd.ms-excel', 'xls')
.replace('apple-diskimage', 'dmg')
.replace('octet-stream', 'Binary')
.replace('svg+xml', 'svg')
return contentTypeCleaned
}
export default cleanupContentType

View File

@ -15,7 +15,7 @@
},
{
"name": "aquarius",
"version": "~0.2.2"
"version": "~0.2.6"
},
{
"name": "squid-js",