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, isOceanNetwork: false,
account: '', account: '',
web3: {}, web3: {},
ocean: {}, ocean: {
aquarius: {
queryMetadata: jest.fn()
}
},
balance: { eth: 0, ocn: 0 }, balance: { eth: 0, ocn: 0 },
network: '', network: '',
requestFromFaucet: jest.fn(), requestFromFaucet: jest.fn(),
@ -20,7 +24,11 @@ const userMockConnected = {
isOceanNetwork: true, isOceanNetwork: true,
account: '0xxxxxx', account: '0xxxxxx',
web3: {}, web3: {},
ocean: {}, ocean: {
aquarius: {
queryMetadata: jest.fn()
}
},
balance: { eth: 0, ocn: 0 }, balance: { eth: 0, ocn: 0 },
network: '', network: '',
requestFromFaucet: jest.fn(), 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/art": "^2.2.0",
"@oceanprotocol/squid": "^0.5.11", "@oceanprotocol/squid": "^0.5.11",
"@oceanprotocol/typographies": "^0.1.0", "@oceanprotocol/typographies": "^0.1.0",
"@sindresorhus/slugify": "^0.9.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"ethereum-blockies": "MyEtherWallet/blockies", "ethereum-blockies": "MyEtherWallet/blockies",
"filesize": "^4.1.2", "filesize": "^4.1.2",
@ -34,7 +35,6 @@
"react-popper": "^1.3.3", "react-popper": "^1.3.3",
"react-router-dom": "^5.0.0", "react-router-dom": "^5.0.0",
"react-transition-group": "^4.0.0", "react-transition-group": "^4.0.0",
"slugify": "^1.3.4",
"web3": "1.0.0-beta.37" "web3": "1.0.0-beta.37"
}, },
"devDependencies": { "devDependencies": {

View File

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

View File

@ -2,8 +2,24 @@
.categoryImage { .categoryImage {
height: 4rem; height: 4rem;
background-size: cover; background-size: 100%;
background-position: center; background-position: center;
margin-bottom: $spacer / $line-height; margin-bottom: $spacer / $line-height;
background-color: $body-background; 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 React, { PureComponent } from 'react'
import cx from 'classnames'
import styles from './CategoryImage.module.scss' import styles from './CategoryImage.module.scss'
import agriculture from '../../img/categories/agriculture.jpg' 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 transport from '../../img/categories/transport.jpg'
import urbanplanning from '../../img/categories/urbanplanning.jpg' import urbanplanning from '../../img/categories/urbanplanning.jpg'
import visualart from '../../img/categories/visualart.jpg' import visualart from '../../img/categories/visualart.jpg'
import aiforgood from '../../img/aiforgood.jpg'
import fallback from '@oceanprotocol/art/jellyfish/jellyfish-back.svg' import fallback from '@oceanprotocol/art/jellyfish/jellyfish-back.svg'
const categoryImageFile = (category: string) => { const categoryImageFile = (category: string) => {
@ -95,6 +97,8 @@ const categoryImageFile = (category: string) => {
case 'mathematics': case 'mathematics':
return mathematics return mathematics
case 'Medicine': case 'Medicine':
case 'Health & Medicine':
case 'Health':
case 'medicine': case 'medicine':
return medicine return medicine
case 'Other': case 'Other':
@ -133,21 +137,31 @@ const categoryImageFile = (category: string) => {
case 'Visual Arts & Design': case 'Visual Arts & Design':
case 'visualart': case 'visualart':
return visualart return visualart
// technically no category
// but corresponding to title of a channel
case 'AI For Good':
return aiforgood
default: default:
return fallback return fallback
} }
} }
export default class CategoryImage extends PureComponent<{ category: string }> { export default class CategoryImage extends PureComponent<{
category: string
header?: boolean
dimmed?: boolean
}> {
public render() { public render() {
const image = categoryImageFile(this.props.category) const image = categoryImageFile(this.props.category)
const classNames = cx(styles.categoryImage, {
[styles.header]: this.props.header,
[styles.dimmed]: this.props.dimmed
})
return ( return (
<div <div
className={styles.categoryImage} className={classNames}
style={{ style={{ backgroundImage: `url(${image})` }}
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 cx from 'classnames'
import React, { PureComponent, FormEvent, ChangeEvent } from 'react' import React, { PureComponent, FormEvent, ChangeEvent } from 'react'
import slugify from 'slugify' import slugify from '@sindresorhus/slugify'
import DatePicker from 'react-datepicker' import DatePicker from 'react-datepicker'
import { ReactComponent as SearchIcon } from '../../../img/search.svg' import { ReactComponent as SearchIcon } from '../../../img/search.svg'
import Help from './Help' import Help from './Help'
@ -131,20 +131,14 @@ export default class Input extends PureComponent<InputProps, InputState> {
<div className={styles.radioWrap} key={index}> <div className={styles.radioWrap} key={index}>
<input <input
className={styles.radio} className={styles.radio}
id={slugify(option, { id={slugify(option)}
lower: true
})}
type={type} type={type}
name={name} name={name}
value={slugify(option, { value={slugify(option)}
lower: true
})}
/> />
<label <label
className={styles.radioLabel} className={styles.radioLabel}
htmlFor={slugify(option, { htmlFor={slugify(option)}
lower: true
})}
> >
{option} {option}
</label> </label>

View File

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

View File

@ -2,10 +2,19 @@ import React from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import moment from 'moment' import moment from 'moment'
import Dotdotdot from 'react-dotdotdot' 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' 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 { metadata } = asset.findServiceByType('Metadata')
const { base } = metadata const { base } = metadata
@ -22,17 +31,22 @@ const AssetLink = ({ asset, list }: { asset: any; list?: boolean }) => {
</Link> </Link>
</article> </article>
) : ( ) : (
<article className={styles.asset}> <article
className={
minimal ? cx(styles.asset, styles.minimal) : styles.asset
}
>
<Link to={`/asset/${asset.id}`}> <Link to={`/asset/${asset.id}`}>
{base.categories && ( {base.categories && !minimal && (
<CategoryImage category={base.categories[0]} /> <CategoryImage dimmed category={base.categories[0]} />
)} )}
<h1>{base.name}</h1> <h1>{base.name}</h1>
<div className={styles.description}> {!minimal && (
<Dotdotdot clamp={3}>{base.description}</Dotdotdot> <div className={styles.description}>
</div> <Dotdotdot clamp={3}>{base.description}</Dotdotdot>
</div>
)}
<footer className={styles.assetFooter}> <footer className={styles.assetFooter}>
{base.categories && <div>{base.categories[0]}</div>} {base.categories && <div>{base.categories[0]}</div>}
</footer> </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 { Logger } from '@oceanprotocol/squid'
import { User } from '../../context' import { User } from '../../context'
import Spinner from '../atoms/Spinner' import Spinner from '../atoms/Spinner'
import Asset from '../molecules/Asset' import AssetTeaser from '../molecules/AssetTeaser'
import styles from './AssetsUser.module.scss' import styles from './AssetsUser.module.scss'
export default class AssetsUser extends PureComponent< export default class AssetsUser extends PureComponent<
@ -82,7 +82,7 @@ export default class AssetsUser extends PureComponent<
) )
.filter(asset => !!asset) .filter(asset => !!asset)
.map((asset: any) => ( .map((asset: any) => (
<Asset <AssetTeaser
list={this.props.list} list={this.props.list}
key={asset.id} key={asset.id}
asset={asset} 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 { .metaPrimary {
margin-bottom: $spacer; margin-bottom: $spacer;
@ -41,18 +41,41 @@
.description { .description {
// respect line breaks from textarea // respect line breaks from textarea
white-space: pre-line; 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 { .metaFixed {
border-top: 1px solid $brand-grey-lighter; border: 1px solid $brand-grey-lighter;
border-bottom: 1px solid $brand-grey-lighter; padding: $spacer;
padding-top: $spacer; border-radius: $border-radius;
padding-bottom: $spacer;
margin-top: $spacer; margin-top: $spacer;
margin-bottom: $spacer; margin-bottom: $spacer * $line-height;
list-style: none;
padding-left: 0;
font-size: $font-size-small; font-size: $font-size-small;
position: relative;
ul {
padding: 0;
margin: 0;
list-style: none;
}
li { li {
width: 100%; width: 100%;
@ -60,7 +83,7 @@
@media (min-width: $break-point--small) { @media (min-width: $break-point--small) {
display: flex; display: flex;
margin-bottom: 0; margin-bottom: $spacer / 3;
} }
&:before { &: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 { .metaLabel {
display: block; display: block;
@ -85,10 +119,15 @@
overflow-wrap: break-word; overflow-wrap: break-word;
} }
/* stylelint-disable declaration-no-important */
code { code {
display: inline; display: block;
padding: 0 !important;
background: none !important;
} }
/* stylelint-enable declaration-no-important */
@media (min-width: $break-point--small) { @media (min-width: $break-point--small) {
width: 70%; width: 70%;
} }

View File

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

View File

@ -1,4 +1,4 @@
@import '../../styles/variables'; @import '../../../styles/variables';
.buttonMain { .buttonMain {
margin: auto; margin: auto;
@ -25,13 +25,16 @@
.file { .file {
display: inline-block; 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; padding: $spacer $spacer / 2;
margin-bottom: $spacer / 2; margin-bottom: $spacer / 2;
text-align: left; text-align: left;
position: relative; position: relative;
height: 8rem; height: 8.5rem;
width: 6rem; width: 6.5rem;
&:before { &:before {
content: ''; content: '';
@ -54,6 +57,12 @@
} }
} }
.empty {
font-size: $font-size-mini;
font-weight: $font-weight-base;
opacity: .75;
}
// move spinner a bit up // move spinner a bit up
+ div { + div {
margin-top: $spacer / 2; margin-top: $spacer / 2;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import React, { PureComponent } from 'react' import React, { PureComponent } from 'react'
import { DDO, File } from '@oceanprotocol/squid' import { DDO, File } from '@oceanprotocol/squid'
import AssetFile from './AssetFile' import AssetFile from './AssetFile'
import { User } from '../../context' import { User } from '../../../context'
import Web3message from '../../components/organisms/Web3message' import Web3message from '../../organisms/Web3message'
import styles from './AssetFilesDetails.module.scss' import styles from './AssetFilesDetails.module.scss'
export default class AssetFilesDetails extends PureComponent<{ 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: '', state: '',
hash: '' hash: ''
}} }}
match={{ params: { did: '' } }} match={{
params: {
did: ''
}
}}
/> />
) )
expect(container.firstChild).toBeInTheDocument() 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) { @media (min-width: $break-point--small) {
font-size: $font-size-h1; font-size: $font-size-h1;
} }
// category image
+ div:not(.description) {
margin-bottom: $spacer;
}
} }
.description { .description {
margin-top: $spacer / 2; margin-top: $spacer / 2;
font-size: $font-size-large; 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 Content from '../atoms/Content'
import styles from './Route.module.scss' import styles from './Route.module.scss'
import meta from '../../data/meta.json' import meta from '../../data/meta.json'
import Markdown from '../atoms/Markdown'
const Route = ({ const Route = ({
title, title,
description, description,
image,
wide, wide,
children, children,
className className
}: { }: {
title: string title: string
description?: string description?: string
image?: any
children: any children: any
wide?: boolean wide?: boolean
className?: string className?: string
@ -24,23 +27,24 @@ const Route = ({
{description && <meta name="description" content={description} />} {description && <meta name="description" content={description} />}
</Helmet> </Helmet>
<Content wide={wide}> <article>
<article> <header className={styles.header}>
<header className={styles.header}> <Content wide={wide}>
<h1 className={styles.title}>{title}</h1> <h1 className={styles.title}>{title}</h1>
{image && image}
{description && ( {description && (
<p <Markdown
text={description}
className={styles.description} className={styles.description}
dangerouslySetInnerHTML={{
__html: description
}}
/> />
)} )}
</header> </Content>
</header>
{children} {children}
</article> </article>
</Content>
</div> </div>
) )

View File

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

View File

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

View File

@ -22,4 +22,4 @@ export const User = React.createContext({
message: '' 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", "Agriculture & Bio Engineering",
"Transportation", "Transportation",
"Urban Planning", "Urban Planning",
"Medicine", "Health & Medicine",
"Business & Management", "Business & Management",
"Sports & Recreation", "Sports & Recreation",
"Communication & Journalism", "Communication & Journalism",

View File

@ -1,4 +1,8 @@
[ [
{
"title": "Channels",
"link": "/channels"
},
{ {
"title": "Publish", "title": "Publish",
"link": "/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 React, { Component } from 'react'
import Route from '../components/templates/Route' import Route from '../components/templates/Route'
import Content from '../components/atoms/Content'
class About extends Component { class About extends Component {
public render() { public render() {
@ -8,28 +9,31 @@ class About extends Component {
title="About" title="About"
description="A marketplace to find and publish open data sets in the Ocean Network." description="A marketplace to find and publish open data sets in the Ocean Network."
> >
<p> <Content>
Commons is built on top of the Ocean{' '} <p>
<a href="https://docs.oceanprotocol.com/concepts/testnets/#the-nile-testnet"> Commons is built on top of the Ocean{' '}
Nile test network <a href="https://docs.oceanprotocol.com/concepts/testnets/#the-nile-testnet">
</a>{' '} Nile test network
and is targeted at enthusiastic data scientists with some </a>{' '}
crypto experience. It can be used with any Web3-capable and is targeted at enthusiastic data scientists with
browser, like Firefox with MetaMask installed. some crypto experience. It can be used with any
</p> Web3-capable browser, like Firefox with MetaMask
installed.
</p>
<ul> <ul>
<li> <li>
<a href="https://blog.oceanprotocol.com/the-commons-marketplace-c57a44288314"> <a href="https://blog.oceanprotocol.com/the-commons-marketplace-c57a44288314">
Read the blog post Read the blog post
</a> </a>
</li> </li>
<li> <li>
<a href="https://github.com/oceanprotocol/commons"> <a href="https://github.com/oceanprotocol/commons">
Check out oceanprotocol/commons on GitHub Check out oceanprotocol/commons on GitHub
</a> </a>
</li> </li>
</ul> </ul>
</Content>
</Route> </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 { User } from '../context'
import Web3message from '../components/organisms/Web3message' import Web3message from '../components/organisms/Web3message'
import styles from './Faucet.module.scss' import styles from './Faucet.module.scss'
import Content from '../components/atoms/Content'
interface FaucetState { interface FaucetState {
isLoading: boolean isLoading: boolean
@ -101,19 +102,21 @@ export default class Faucet extends PureComponent<{}, FaucetState> {
title="Faucet" title="Faucet"
description="Shower yourself with some Ether for Ocean's Nile test network." description="Shower yourself with some Ether for Ocean's Nile test network."
> >
<Web3message /> <Content>
<Web3message />
<div className={styles.action}> <div className={styles.action}>
{isLoading ? ( {isLoading ? (
<Spinner message="Getting Ether..." /> <Spinner message="Getting Ether..." />
) : error ? ( ) : error ? (
<this.Error /> <this.Error />
) : success ? ( ) : success ? (
<this.Success /> <this.Success />
) : ( ) : (
isWeb3 && <this.Action /> isWeb3 && <this.Action />
)} )}
</div> </div>
</Content>
</Route> </Route>
) )
} }

View File

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

View File

@ -12,4 +12,49 @@
margin: 0; margin: 0;
visibility: hidden; 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 React from 'react'
import { Router } from 'react-router'
import { createBrowserHistory } from 'history'
import { render } from 'react-testing-library' import { render } from 'react-testing-library'
import Home from './Home' import Home from './Home'
import { userMock } from '../../__mocks__/user-mock'
import { User } from '../context'
const history = createBrowserHistory()
describe('Home', () => { describe('Home', () => {
it('renders without crashing', () => { 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() expect(container.firstChild).toBeInTheDocument()
}) })
}) })

View File

@ -1,13 +1,18 @@
import React, { ChangeEvent, Component, FormEvent } from 'react' 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 Button from '../components/atoms/Button'
import Form from '../components/atoms/Form/Form' import Form from '../components/atoms/Form/Form'
import Input from '../components/atoms/Form/Input' import Input from '../components/atoms/Form/Input'
import Route from '../components/templates/Route' import Route from '../components/templates/Route'
import AssetsUser from '../components/organisms/AssetsUser'
import styles from './Home.module.scss' import styles from './Home.module.scss'
import meta from '../data/meta.json' 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 { interface HomeProps {
history: History history: History
@ -17,34 +22,11 @@ interface HomeState {
search?: string search?: string
} }
class Home extends Component<HomeProps, HomeState> { export default class Home extends Component<HomeProps, HomeState> {
public state = { search: '' } public static contextType = User
public render() { public state = {
return ( search: ''
<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>
)
} }
private inputChange = (event: ChangeEvent<HTMLInputElement>) => { private inputChange = (event: ChangeEvent<HTMLInputElement>) => {
@ -57,6 +39,64 @@ class Home extends Component<HomeProps, HomeState> {
event.preventDefault() event.preventDefault()
this.props.history.push(`/search?text=${this.state.search}`) 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 React, { Component } from 'react'
import Route from '../components/templates/Route' import Route from '../components/templates/Route'
import Content from '../components/atoms/Content'
class NotFound extends Component { class NotFound extends Component {
public render() { 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 React from 'react'
import { render, fireEvent, waitForElement } from 'react-testing-library' import { render, fireEvent, waitForElement } from 'react-testing-library'
import Files, { getFileCompression } from '.' import Files from '.'
const onChange = jest.fn() const onChange = jest.fn()
@ -71,20 +71,3 @@ describe('Files', () => {
fireEvent.click(getByText('Add File')) 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 styles from './index.module.scss'
import { serviceHost, servicePort, serviceScheme } from '../../../config' import { serviceHost, servicePort, serviceScheme } from '../../../config'
import cleanupContentType from '../../../utils/cleanupContentType'
interface File { interface File {
url: string url: string
@ -38,32 +39,6 @@ interface FilesStates {
isFormShown: boolean 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> { export default class Files extends PureComponent<FilesProps, FilesStates> {
public state: FilesStates = { public state: FilesStates = {
isFormShown: false isFormShown: false
@ -106,7 +81,7 @@ export default class Files extends PureComponent<FilesProps, FilesStates> {
res = await response.json() res = await response.json()
file.contentLength = res.result.contentLength file.contentLength = res.result.contentLength
file.contentType = res.result.contentType file.contentType = res.result.contentType
file.compression = await getFileCompression(res.result.contentType) file.compression = await cleanupContentType(res.result.contentType)
file.found = res.result.found file.found = res.result.found
} catch (error) { } catch (error) {
// error // error

View File

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

View File

@ -17,6 +17,16 @@
grid-gap: $spacer; grid-gap: $spacer;
@media (min-width: $break-point--small) { @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 Search from './Search'
import { User } from '../context' import { User } from '../context'
import { createMemoryHistory } from 'history' import { createMemoryHistory } from 'history'
import { BrowserRouter as Router } from 'react-router-dom'
describe('Search', () => { describe('Search', () => {
it('renders without crashing', () => { it('renders without crashing', () => {
@ -35,15 +36,17 @@ describe('Search', () => {
message: '' message: ''
}} }}
> >
<Search <Router>
location={{ <Search
search: '?text=Hello&page=1', location={{
pathname: '/search', search: '?text=Hello&page=1',
state: '', pathname: '/search',
hash: '' state: '',
}} hash: ''
history={history} }}
/> history={history}
/>
</Router>
</User.Provider> </User.Provider>
) )
expect(container.firstChild).toBeInTheDocument() expect(container.firstChild).toBeInTheDocument()

View File

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

View File

@ -5,6 +5,7 @@ import Input from '../components/atoms/Form/Input'
import Route from '../components/templates/Route' import Route from '../components/templates/Route'
import styles from './Styleguide.module.scss' import styles from './Styleguide.module.scss'
import form from '../data/form-styleguide.json' import form from '../data/form-styleguide.json'
import Content from '../components/atoms/Content'
class Styleguide extends Component { class Styleguide extends Component {
public formFields = (entries: any[]) => public formFields = (entries: any[]) =>
@ -25,18 +26,22 @@ class Styleguide extends Component {
const entries = Object.entries(form.fields) const entries = Object.entries(form.fields)
return ( return (
<Route title="Styleguide" className={styles.styleguide}> <Route title="Styleguide" className={styles.styleguide}>
<div className={styles.buttons}> <Content>
<Button>I am a button</Button> <div className={styles.buttons}>
<Button primary>I am a primary button</Button> <Button>I am a button</Button>
<Button href="https://hello.com"> <Button primary>I am a primary button</Button>
I am a link disguised as a button <Button href="https://hello.com">
</Button> I am a link disguised as a button
<Button link>I am a button disguised as a text link</Button> </Button>
</div> <Button link>
I am a button disguised as a text link
</Button>
</div>
<Form title={form.title} description={form.description}> <Form title={form.title} description={form.description}>
{this.formFields(entries)} {this.formFields(entries)}
</Form> </Form>
</Content>
</Route> </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, $font-family-monospace: 'Fira Code', 'Fira Mono', Menlo, Monaco, Consolas,
'Courier New', monospace; 'Courier New', monospace;
$font-size-root: 15px; $font-size-root: 16px;
$font-size-base: 1rem; $font-size-base: 1rem;
$font-size-large: 1.2rem; $font-size-large: 1.2rem;
$font-size-small: .85rem; $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", "name": "aquarius",
"version": "~0.2.2" "version": "~0.2.6"
}, },
{ {
"name": "squid-js", "name": "squid-js",