Merge pull request #125 from oceanprotocol/feature/ai-for-good
AI For Good: channels, new front-page & categories list
@ -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
@ -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": {
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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})` }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
23
client/src/components/atoms/CategoryLink.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
const CategoryLink = ({
|
||||
category,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
category: string
|
||||
children?: any
|
||||
className?: string
|
||||
}) => (
|
||||
<Link
|
||||
to={`/search?categories=${encodeURIComponent(category)}`}
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
{children || category}
|
||||
</Link>
|
||||
)
|
||||
|
||||
export default CategoryLink
|
@ -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>
|
||||
|
@ -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;
|
@ -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
|
42
client/src/components/organisms/AssetsLatest.module.scss
Normal file
@ -0,0 +1,42 @@
|
||||
@import '../../styles/variables';
|
||||
|
||||
.latestAssetsWrap {
|
||||
// full width break out of container
|
||||
margin-right: calc(-50vw + 50%);
|
||||
}
|
||||
|
||||
.latestAssets {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
display: grid;
|
||||
grid-gap: $spacer;
|
||||
grid-auto-flow: column;
|
||||
padding: $spacer / 2 $spacer;
|
||||
border-left: 1px solid $brand-grey-lighter;
|
||||
|
||||
&::-webkit-scrollbar,
|
||||
&::-moz-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> article {
|
||||
min-width: calc(18rem + #{$spacer});
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: $font-size-h4;
|
||||
text-align: center;
|
||||
color: $brand-grey-light;
|
||||
border-bottom: 1px solid $brand-grey-lighter;
|
||||
padding-bottom: $spacer / 3;
|
||||
margin-top: $spacer * 3;
|
||||
margin-bottom: $spacer / 2;
|
||||
|
||||
@media (min-width: $break-point--small) {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
15
client/src/components/organisms/AssetsLatest.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
79
client/src/components/organisms/AssetsLatest.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import React, { PureComponent } from 'react'
|
||||
import { Logger } from '@oceanprotocol/squid'
|
||||
import { User } from '../../context'
|
||||
import Spinner from '../atoms/Spinner'
|
||||
import AssetTeaser from '../molecules/AssetTeaser'
|
||||
import styles from './AssetsLatest.module.scss'
|
||||
|
||||
interface AssetsLatestState {
|
||||
latestAssets?: any[]
|
||||
isLoadingLatest?: boolean
|
||||
}
|
||||
|
||||
export default class AssetsLatest extends PureComponent<{}, AssetsLatestState> {
|
||||
public state = { latestAssets: [], isLoadingLatest: true }
|
||||
|
||||
public _isMounted: boolean = false
|
||||
|
||||
public componentDidMount() {
|
||||
this._isMounted = true
|
||||
this._isMounted && this.getLatestAssets()
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
this._isMounted = false
|
||||
}
|
||||
|
||||
private getLatestAssets = async () => {
|
||||
const { ocean } = this.context
|
||||
|
||||
const searchQuery = {
|
||||
offset: 15,
|
||||
page: 1,
|
||||
query: {},
|
||||
sort: {
|
||||
created: -1
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const search = await ocean.aquarius.queryMetadata(searchQuery)
|
||||
this.setState({
|
||||
latestAssets: search.results,
|
||||
isLoadingLatest: false
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error(error.message)
|
||||
this.setState({ isLoadingLatest: false })
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { latestAssets, isLoadingLatest } = this.state
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className={styles.title}>Latest published assets</h2>
|
||||
<div className={styles.latestAssetsWrap}>
|
||||
{isLoadingLatest ? (
|
||||
<Spinner message="Loading..." />
|
||||
) : latestAssets && latestAssets.length ? (
|
||||
<div className={styles.latestAssets}>
|
||||
{latestAssets.map((asset: any) => (
|
||||
<AssetTeaser
|
||||
key={asset.id}
|
||||
asset={asset}
|
||||
minimal
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div>No data sets found.</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
AssetsLatest.contextType = User
|
@ -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}
|
||||
|
86
client/src/components/organisms/ChannelTeaser.module.scss
Normal file
@ -0,0 +1,86 @@
|
||||
@import '../../styles/variables';
|
||||
|
||||
.channel {
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: $break-point--medium) {
|
||||
padding-top: $spacer * 2;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
> div {
|
||||
&:first-child {
|
||||
margin-bottom: $spacer;
|
||||
|
||||
@media (min-width: $break-point--medium) {
|
||||
margin-right: $spacer;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: $break-point--medium) {
|
||||
flex: 1;
|
||||
|
||||
&:first-child {
|
||||
flex: 0 0 calc(18rem + #{$spacer * 2});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// style channel teaser following another one
|
||||
+ .channel {
|
||||
border-top: 1px solid $brand-grey-lighter;
|
||||
margin-top: $spacer * 2;
|
||||
}
|
||||
}
|
||||
|
||||
.channelTitle {
|
||||
margin-top: $spacer * 4;
|
||||
margin-bottom: $spacer / 4;
|
||||
color: $brand-black;
|
||||
|
||||
@media (min-width: $break-point--medium) {
|
||||
margin-top: -($spacer / 4);
|
||||
}
|
||||
}
|
||||
|
||||
.channelHeader {
|
||||
text-align: center;
|
||||
|
||||
@media (min-width: $break-point--small) {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
a {
|
||||
display: block;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
transform: none;
|
||||
|
||||
// category image
|
||||
// stylelint-disable-next-line
|
||||
.channelTitle + div {
|
||||
opacity: 1;
|
||||
background-size: 105%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channelTeaser {
|
||||
color: $brand-grey;
|
||||
}
|
||||
|
||||
.channelResults {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-gap: $spacer;
|
||||
|
||||
@media (min-width: $break-point--small) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
15
client/src/components/organisms/ChannelTeaser.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
99
client/src/components/organisms/ChannelTeaser.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import React, { Component } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { User } from '../../context'
|
||||
import { Logger } from '@oceanprotocol/squid'
|
||||
import Spinner from '../atoms/Spinner'
|
||||
import AssetTeaser from '../molecules/AssetTeaser'
|
||||
import styles from './ChannelTeaser.module.scss'
|
||||
import channels from '../../data/channels.json'
|
||||
import CategoryImage from '../atoms/CategoryImage'
|
||||
|
||||
interface ChannelTeaserProps {
|
||||
channel: string
|
||||
}
|
||||
|
||||
interface ChannelTeaserState {
|
||||
channelAssets?: any[]
|
||||
isLoadingChannel?: boolean
|
||||
}
|
||||
|
||||
export default class ChannelTeaser extends Component<
|
||||
ChannelTeaserProps,
|
||||
ChannelTeaserState
|
||||
> {
|
||||
public static contextType = User
|
||||
|
||||
// Get channel content
|
||||
public channel = channels.items
|
||||
.filter(({ tag }) => tag === this.props.channel)
|
||||
.map(channel => channel)[0]
|
||||
|
||||
public state = {
|
||||
channelAssets: [],
|
||||
isLoadingChannel: true
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
this.getChannelAssets()
|
||||
}
|
||||
|
||||
private getChannelAssets = async () => {
|
||||
const { ocean } = this.context
|
||||
|
||||
const searchQuery = {
|
||||
offset: 2,
|
||||
page: 1,
|
||||
query: {
|
||||
tags: [this.channel.tag]
|
||||
},
|
||||
sort: {
|
||||
created: -1
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const search = await ocean.aquarius.queryMetadata(searchQuery)
|
||||
this.setState({
|
||||
channelAssets: search.results,
|
||||
isLoadingChannel: false
|
||||
})
|
||||
} catch (error) {
|
||||
Logger.error(error.message)
|
||||
this.setState({ isLoadingChannel: false })
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { channelAssets, isLoadingChannel } = this.state
|
||||
const { title, tag, teaser } = this.channel
|
||||
|
||||
return (
|
||||
<div className={styles.channel}>
|
||||
<div>
|
||||
<header className={styles.channelHeader}>
|
||||
<Link to={`/channels/${tag}`}>
|
||||
<h2 className={styles.channelTitle}>{title}</h2>
|
||||
<CategoryImage category={title} />
|
||||
|
||||
<p className={styles.channelTeaser}>{teaser}</p>
|
||||
<p>Browse the channel →</p>
|
||||
</Link>
|
||||
</header>
|
||||
</div>
|
||||
<div>
|
||||
{isLoadingChannel ? (
|
||||
<Spinner message="Loading..." />
|
||||
) : channelAssets && channelAssets.length ? (
|
||||
<div className={styles.channelResults}>
|
||||
{channelAssets.map((asset: any) => (
|
||||
<AssetTeaser key={asset.id} asset={asset} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div>No data sets found.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -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%;
|
||||
}
|
@ -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 : []}
|
@ -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;
|
@ -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
|
||||
}
|
||||
|
@ -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 ? (
|
@ -1,4 +1,4 @@
|
||||
@import '../../styles/variables';
|
||||
@import '../../../styles/variables';
|
||||
|
||||
.files {
|
||||
text-align: center;
|
@ -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', () => {
|
@ -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<{
|
12
client/src/components/templates/Asset/index.module.scss
Normal 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;
|
||||
}
|
@ -12,7 +12,11 @@ describe('Details', () => {
|
||||
state: '',
|
||||
hash: ''
|
||||
}}
|
||||
match={{ params: { did: '' } }}
|
||||
match={{
|
||||
params: {
|
||||
did: ''
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
expect(container.firstChild).toBeInTheDocument()
|
83
client/src/components/templates/Asset/index.tsx
Normal 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
|
21
client/src/components/templates/Channel.module.scss
Normal 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);
|
||||
}
|
||||
}
|
48
client/src/components/templates/Channel.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
130
client/src/components/templates/Channel.tsx
Normal 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
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
||||
//
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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: [''] })
|
||||
|
12
client/src/data/channels.json
Normal 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)."
|
||||
}
|
||||
]
|
||||
}
|
@ -64,7 +64,7 @@
|
||||
"Agriculture & Bio Engineering",
|
||||
"Transportation",
|
||||
"Urban Planning",
|
||||
"Medicine",
|
||||
"Health & Medicine",
|
||||
"Business & Management",
|
||||
"Sports & Recreation",
|
||||
"Communication & Journalism",
|
||||
|
@ -1,4 +1,8 @@
|
||||
[
|
||||
{
|
||||
"title": "Channels",
|
||||
"link": "/channels"
|
||||
},
|
||||
{
|
||||
"title": "Publish",
|
||||
"link": "/publish"
|
||||
|
BIN
client/src/img/aiforgood.jpg
Normal file
After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 224 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 178 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 197 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 164 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 118 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 246 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 217 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 210 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 278 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 284 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 186 KiB |
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 226 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 279 KiB |
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 209 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 269 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 209 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 282 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 109 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 158 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 183 KiB |
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 423 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 227 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 164 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 133 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 196 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 112 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 318 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 204 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 140 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 326 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 222 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 182 KiB |
@ -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>
|
||||
)
|
||||
}
|
||||
|
15
client/src/routes/Channels.test.tsx
Normal 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()
|
||||
})
|
||||
})
|
24
client/src/routes/Channels.tsx
Normal 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
|
@ -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
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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;
|
||||
|
18
client/src/utils/cleanupContentType.test.ts
Normal 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')
|
||||
})
|
||||
})
|
43
client/src/utils/cleanupContentType.ts
Normal 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
|
@ -15,7 +15,7 @@
|
||||
},
|
||||
{
|
||||
"name": "aquarius",
|
||||
"version": "~0.2.2"
|
||||
"version": "~0.2.6"
|
||||
},
|
||||
{
|
||||
"name": "squid-js",
|
||||
|