1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-09-28 03:58:59 +02:00

Merge pull request #37 from oceanprotocol/feature/price

Custom price input
This commit is contained in:
Matthias Kretschmann 2020-08-03 19:33:05 +02:00 committed by GitHub
commit 12d3b2f0a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 737 additions and 122 deletions

View File

@ -35,11 +35,9 @@
"required": true "required": true
}, },
{ {
"name": "cost", "name": "price",
"label": "Price", "label": "Price",
"help": "Set your price for accessing this data set in Ocean Tokens.",
"type": "price", "type": "price",
"min": 1,
"required": true "required": true
}, },
{ {

View File

@ -5,12 +5,9 @@ import {
ServiceMetadata ServiceMetadata
} from '@oceanprotocol/lib' } from '@oceanprotocol/lib'
export declare type AccessType = 'Download' | 'Compute'
export interface AdditionalInformationMarket extends AdditionalInformation { export interface AdditionalInformationMarket extends AdditionalInformation {
links?: File[] links?: File[]
termsAndConditions: boolean termsAndConditions: boolean
access: AccessType | string
} }
export interface MetadataMarket extends Metadata { export interface MetadataMarket extends Metadata {
@ -24,7 +21,10 @@ export interface MetadataPublishForm {
files: string | File[] files: string | File[]
author: string author: string
license: string license: string
cost: string price: {
cost: number
tokensToMint: number
}
access: 'Download' | 'Compute' | string access: 'Download' | 'Compute' | string
termsAndConditions: boolean termsAndConditions: boolean
// ---- optional fields ---- // ---- optional fields ----

View File

@ -1,9 +1,23 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import styles from './Help.module.css' import styles from './Help.module.css'
import Markdown from '../Markdown' import Markdown from '../Markdown'
import classNames from 'classnames/bind'
const FormHelp = ({ children }: { children: string }): ReactElement => ( const cx = classNames.bind(styles)
<Markdown className={styles.help} text={children} />
) const FormHelp = ({
children,
className
}: {
children: string
className?: string
}): ReactElement => {
const styleClasses = cx({
help: true,
[className]: className
})
return <Markdown className={styleClasses} text={children} />
}
export default FormHelp export default FormHelp

View File

@ -12,6 +12,7 @@
border-radius: var(--border-radius); border-radius: var(--border-radius);
transition: 0.2s ease-out; transition: 0.2s ease-out;
min-height: 43px; min-height: 43px;
min-width: 0;
appearance: none; appearance: none;
display: block; display: block;
} }
@ -33,7 +34,8 @@
.input[readonly], .input[readonly],
.input[disabled] { .input[disabled] {
background-color: var(--brand-grey-lighter); background-color: var(--brand-grey-dimmed);
color: var(--brand-grey-light);
cursor: not-allowed; cursor: not-allowed;
pointer-events: none; pointer-events: none;
} }
@ -84,6 +86,52 @@
padding-left: 0.5rem; padding-left: 0.5rem;
} }
.prefixGroup,
.postfixGroup {
display: inline-flex;
align-items: center;
}
.prefixGroup input {
border-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.postfixGroup input {
border-right: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.prefix,
.postfix {
border: 1px solid var(--brand-grey-lighter);
min-height: 43px;
display: flex;
align-items: center;
padding-left: calc(var(--spacer) / 4);
padding-right: calc(var(--spacer) / 4);
color: var(--color-secondary);
font-size: var(--font-size-small);
transition: border 0.2s ease-out;
white-space: nowrap;
}
.prefix {
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
}
.postfix {
border-top-right-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
.input:focus + .postfix {
border-color: var(--brand-grey);
}
/* Size modifiers */ /* Size modifiers */
.small { .small {

View File

@ -4,9 +4,23 @@ import styles from './InputElement.module.css'
import { InputProps } from '.' import { InputProps } from '.'
import FilesInput from '../../molecules/FormFields/FilesInput' import FilesInput from '../../molecules/FormFields/FilesInput'
import Terms from '../../molecules/FormFields/Terms' import Terms from '../../molecules/FormFields/Terms'
import Price from '../../molecules/FormFields/Price'
const DefaultInput = (
{ name, type }: { name: string; type?: string },
props: InputProps
) => (
<input
id={name}
className={styles.input}
name={name}
{...props}
type={type || 'text'}
/>
)
export default function InputElement(props: InputProps): ReactElement { export default function InputElement(props: InputProps): ReactElement {
const { type, options, rows, name, value } = props const { type, options, rows, name, prefix, postfix } = props
switch (type) { switch (type) {
case 'select': case 'select':
@ -56,18 +70,19 @@ export default function InputElement(props: InputProps): ReactElement {
) )
case 'files': case 'files':
return <FilesInput name={name} {...props} /> return <FilesInput name={name} {...props} />
case 'price':
return <Price name={name} {...props} />
case 'terms': case 'terms':
return <Terms name={name} {...props} /> return <Terms name={name} {...props} />
default: default:
return ( return prefix || postfix ? (
<input <div className={`${prefix ? styles.prefixGroup : styles.postfixGroup}`}>
id={name} {prefix && <div className={styles.prefix}>{prefix}</div>}
className={styles.input} <DefaultInput name={name} type={type || 'text'} />
name={name} {postfix && <div className={styles.postfix}>{postfix}</div>}
{...props} </div>
value={value || ''} ) : (
type={type || 'text'} <DefaultInput name={name} type={type || 'text'} />
/>
) )
} }
} }

View File

@ -1,4 +1,4 @@
import React, { FormEvent, ChangeEvent, ReactElement } from 'react' import React, { FormEvent, ChangeEvent, ReactElement, ReactNode } from 'react'
import InputElement from './InputElement' import InputElement from './InputElement'
import Help from './Help' import Help from './Help'
import Label from './Label' import Label from './Label'
@ -31,8 +31,12 @@ export interface InputProps {
pattern?: string pattern?: string
min?: string min?: string
disabled?: boolean disabled?: boolean
readOnly?: boolean
field?: any field?: any
form?: any form?: any
prefix?: string
postfix?: string
step?: string
} }
export default function Input(props: Partial<InputProps>): ReactElement { export default function Input(props: Partial<InputProps>): ReactElement {

View File

@ -1,6 +1,6 @@
.conversion { .conversion {
display: inline-block; display: inline-block;
font-size: var(--font-size-mini); font-size: var(--font-size-small);
margin-left: calc(var(--spacer) / 6); margin-left: calc(var(--spacer) / 6);
color: var(--color-secondary); color: var(--color-secondary);
} }

View File

@ -2,19 +2,29 @@ import React, { useEffect, useState, ReactElement } from 'react'
import useSWR from 'swr' import useSWR from 'swr'
import { fetchData, isBrowser } from '../../../utils' import { fetchData, isBrowser } from '../../../utils'
import styles from './Conversion.module.css' import styles from './Conversion.module.css'
import classNames from 'classnames/bind'
const cx = classNames.bind(styles)
const currencies = 'EUR' // comma-separated list const currencies = 'EUR' // comma-separated list
const url = `https://api.coingecko.com/api/v3/simple/price?ids=ocean-protocol&vs_currencies=${currencies}&include_24hr_change=true` const url = `https://api.coingecko.com/api/v3/simple/price?ids=ocean-protocol&vs_currencies=${currencies}&include_24hr_change=true`
export default function Conversion({ export default function Conversion({
price, price,
update = true update = true,
className
}: { }: {
price: string // expects price in OCEAN, not wei price: string // expects price in OCEAN, not wei
update?: boolean update?: boolean
className?: string
}): ReactElement { }): ReactElement {
const [priceEur, setPriceEur] = useState('0.00') const [priceEur, setPriceEur] = useState('0.00')
const styleClasses = cx({
conversion: true,
[className]: className
})
const onSuccess = async (data: { 'ocean-protocol': { eur: number } }) => { const onSuccess = async (data: { 'ocean-protocol': { eur: number } }) => {
if (!data) return if (!data) return
if (!price || price === '' || price === '0') { if (!price || price === '' || price === '0') {
@ -45,5 +55,12 @@ export default function Conversion({
}) })
} }
return <span className={styles.conversion}> EUR {priceEur}</span> return (
<span
className={styleClasses}
title="Approximation based on current spot price on Coingecko"
>
EUR {priceEur}
</span>
)
} }

View File

@ -1,4 +1,4 @@
import React from 'react' import React, { ReactElement } from 'react'
import DataTable from 'react-data-table-component' import DataTable from 'react-data-table-component'
export declare type AssetTablePagination = { export declare type AssetTablePagination = {

View File

@ -0,0 +1,26 @@
.tabList {
text-align: center;
border-bottom: 1px solid var(--brand-grey-lighter);
padding-top: calc(var(--spacer) / 2);
padding-bottom: calc(var(--spacer) / 2);
}
.tab {
display: inline-block;
padding: calc(var(--spacer) / 12) var(--spacer);
border-radius: var(--border-radius);
font-weight: var(--font-weight-bold);
font-size: var(--font-size-small);
text-transform: uppercase;
cursor: pointer;
color: var(--brand-grey-light);
}
.tab[aria-selected='true'] {
background: var(--brand-black);
color: var(--brand-white);
}
.tabContent {
padding: var(--spacer);
}

View File

@ -0,0 +1,33 @@
import React, { ReactElement, ReactNode } from 'react'
import { Tab, Tabs as ReactTabs, TabList, TabPanel } from 'react-tabs'
import styles from './Tabs.module.css'
interface TabsItem {
title: string
content: ReactNode
}
export default function Tabs({
items,
className
}: {
items: TabsItem[]
className?: string
}): ReactElement {
return (
<ReactTabs className={`${className && className}`}>
<TabList className={styles.tabList}>
{items.map((item) => (
<Tab className={styles.tab} key={item.title}>
{item.title}
</Tab>
))}
</TabList>
<div className={styles.tabContent}>
{items.map((item) => (
<TabPanel key={item.title}>{item.content}</TabPanel>
))}
</div>
</ReactTabs>
)
}

View File

@ -36,7 +36,7 @@ export default function FilesInput(props: InputProps): ReactElement {
return ( return (
<> <>
{typeof field.value === 'object' ? ( {field && typeof field.value === 'object' ? (
<FileInfo file={field.value[0]} removeItem={removeItem} /> <FileInfo file={field.value[0]} removeItem={removeItem} />
) : ( ) : (
<FileInput <FileInput

View File

@ -0,0 +1,56 @@
.advanced {
}
.wallet {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: calc(var(--spacer) / 2);
}
.balance {
text-align: center;
font-size: var(--font-size-small);
border: 1px solid var(--brand-grey-lighter);
border-right: 0;
margin-right: -3px;
padding: calc(var(--spacer) / 4.5) calc(var(--spacer) / 2);
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
color: var(--color-secondary);
}
.balance strong {
color: var(--brand-grey);
}
.title {
font-size: var(--font-size-base);
margin-top: var(--spacer);
margin-bottom: 0;
color: var(--color-secondary);
text-align: center;
padding-bottom: calc(var(--spacer) / 3);
}
.tokens {
display: grid;
margin-bottom: -2rem;
margin-left: -3rem;
margin-right: -3rem;
}
@media screen and (min-width: 40rem) {
.tokens {
grid-template-columns: 1fr 1fr;
}
}
.alertArea {
margin-left: -3rem;
margin-right: -3rem;
padding: var(--spacer) calc(var(--spacer) / 2);
padding-bottom: 0;
margin-top: var(--spacer);
border-top: 1px solid var(--brand-grey-lighter);
}

View File

@ -0,0 +1,89 @@
import React, { ReactElement, useState, ChangeEvent, useEffect } from 'react'
import stylesIndex from './index.module.css'
import styles from './Advanced.module.css'
import FormHelp from '../../../atoms/Input/Help'
import Wallet from '../../Wallet'
import { useOcean } from '@oceanprotocol/react'
import Alert from '../../../atoms/Alert'
import Coin from './Coin'
import { isCorrectNetwork } from '../../../../utils/wallet'
import { useSiteMetadata } from '../../../../hooks/useSiteMetadata'
export default function Advanced({
ocean,
tokensToMint,
weightOnDataToken,
onChange
}: {
ocean: string
tokensToMint: number
weightOnDataToken: string
onChange: (event: ChangeEvent<HTMLInputElement>) => void
}): ReactElement {
const { appConfig } = useSiteMetadata()
const { account, balance, chainId } = useOcean()
const [error, setError] = useState<string>()
const correctNetwork = isCorrectNetwork(chainId)
const desiredNetworkName = appConfig.network.replace(/^\w/, (c: string) =>
c.toUpperCase()
)
// Check: account, network & insuffciant balance
useEffect(() => {
if (!account) {
setError(`No account connected. Please connect your Web3 wallet.`)
} else if (!correctNetwork) {
setError(`Wrong Network. Please connect to ${desiredNetworkName}.`)
} else if (balance.ocean < ocean) {
setError(`Insufficiant balance. You need at least ${ocean} OCEAN`)
} else {
setError(undefined)
}
}, [ocean])
return (
<div className={stylesIndex.content}>
<div className={styles.advanced}>
<FormHelp className={stylesIndex.help}>
{`Let's create a decentralized, automated market for your data set. A Data Token contract for this data set worth the entered amount of OCEAN will be created. Additionally, you will provide liquidity into a Data Token/OCEAN
liquidity pool with Balancer.`}
</FormHelp>
<aside className={styles.wallet}>
{balance && balance.ocean && (
<div className={styles.balance}>
OCEAN <strong>{balance.ocean}</strong>
</div>
)}
<Wallet />
</aside>
<h4 className={styles.title}>Data Token Liquidity Pool</h4>
<div className={styles.tokens}>
<Coin
name="ocean"
symbol="OCEAN"
value={ocean}
weight={`${100 - Number(weightOnDataToken)}%`}
onChange={onChange}
/>
<Coin
name="tokensToMint"
symbol="OCEAN-CAV"
value={tokensToMint.toString()}
weight={`${weightOnDataToken}%`}
readOnly
/>
</div>
{error && (
<div className={styles.alertArea}>
<Alert text={error} state="error" />
</div>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,59 @@
.coin {
padding: var(--spacer) calc(var(--spacer) / 2);
border-top: 1px solid var(--brand-grey-lighter);
}
.coin:last-child {
border-left: 1px solid var(--brand-grey-lighter);
}
.icon {
composes: box from '../../../atoms/Box.module.css';
padding: calc(var(--spacer) / 1.5);
width: 6rem;
height: 6rem;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--brand-grey-lighter);
border-radius: 50%;
background-color: var(--brand-white);
margin-bottom: var(--spacer);
}
.coin:last-child .icon path {
fill: var(--brand-grey-dimmed);
stroke: var(--brand-black);
stroke-width: 5px;
stroke-linejoin: round;
}
.data {
position: relative;
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: center;
text-align: center;
max-width: 12rem;
margin: auto;
}
.max {
position: absolute;
right: var(--spacer);
top: calc(var(--spacer) / 2.5);
}
.weight {
width: 100%;
margin-top: var(--spacer);
text-transform: uppercase;
font-size: var(--font-size-small);
color: var(--color-secondary);
}
.weight strong {
color: var(--brand-grey);
font-size: var(--font-size-base);
}

View File

@ -0,0 +1,46 @@
import React, { ReactElement, ChangeEvent } from 'react'
import stylesIndex from './index.module.css'
import styles from './Coin.module.css'
import InputElement from '../../../atoms/Input/InputElement'
import { ReactComponent as Logo } from '../../../../images/logo.svg'
import Conversion from '../../../atoms/Price/Conversion'
export default function Coin({
symbol,
name,
value,
weight,
onChange,
readOnly
}: {
symbol: string
name: string
value: string
weight: string
onChange?: (event: ChangeEvent<HTMLInputElement>) => void
readOnly?: boolean
}): ReactElement {
return (
<div className={styles.coin}>
<figure className={styles.icon}>
<Logo />
</figure>
<div className={styles.data}>
<InputElement
value={value}
name={name}
type="number"
onChange={onChange}
readOnly={readOnly}
prefix={symbol}
/>
<Conversion price={value} className={stylesIndex.conversion} />
<div className={styles.weight}>
Weight <strong>{weight}</strong>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,8 @@
.form {
max-width: 12rem;
margin: 0 auto;
}
.simple label {
display: none;
}

View File

@ -0,0 +1,40 @@
import React, { ReactElement, ChangeEvent } from 'react'
import stylesIndex from './index.module.css'
import styles from './Simple.module.css'
import FormHelp from '../../../atoms/Input/Help'
import Label from '../../../atoms/Input/Label'
import InputElement from '../../../atoms/Input/InputElement'
import Conversion from '../../../atoms/Price/Conversion'
export default function Simple({
ocean,
onChange
}: {
ocean: string
onChange: (event: ChangeEvent<HTMLInputElement>) => void
}): ReactElement {
return (
<div className={stylesIndex.content}>
<div className={styles.simple}>
<FormHelp className={stylesIndex.help}>
Set your price for accessing this data set. A Data Token contract for
this data set, worth the entered amount of OCEAN will be created.
</FormHelp>
<form className={styles.form}>
<Label htmlFor="ocean">Ocean Tokens</Label>
<InputElement
value={ocean}
name="ocean"
type="number"
prefix="OCEAN"
onChange={onChange}
/>
<Conversion price={ocean} className={stylesIndex.conversion} />
</form>
</div>
</div>
)
}

View File

@ -0,0 +1,33 @@
.price {
border: 1px solid var(--brand-grey-lighter);
background: var(--brand-grey-dimmed);
border-radius: var(--border-radius);
}
.content {
padding: 0 calc(var(--spacer) / 2);
}
.content label {
color: var(--color-secondary);
}
.content input {
text-align: center;
}
.content p {
margin-bottom: 0;
}
.conversion {
width: 100%;
display: block;
text-align: center;
margin-top: calc(var(--spacer) / 6);
}
.help {
text-align: center;
margin-bottom: calc(var(--spacer) / 1.5);
}

View File

@ -0,0 +1,54 @@
import React, { ReactElement, useState, ChangeEvent, useEffect } from 'react'
import { InputProps } from '../../../atoms/Input'
import styles from './index.module.css'
import Tabs from '../../../atoms/Tabs'
import Simple from './Simple'
import Advanced from './Advanced'
import { useField } from 'formik'
export default function Price(props: InputProps): ReactElement {
const [field, meta, helpers] = useField(props)
const cost = 1
const weightOnDataToken = '90' // in %
const [ocean, setOcean] = useState('1')
const [tokensToMint, setTokensToMint] = useState<number>()
function handleOceanChange(event: ChangeEvent<HTMLInputElement>) {
setOcean(event.target.value)
}
// Always update everything when ocean changes
useEffect(() => {
const tokensToMint = Number(ocean) * (Number(weightOnDataToken) / 10)
setTokensToMint(tokensToMint)
helpers.setValue({ cost, tokensToMint })
}, [ocean])
const tabs = [
{
title: 'Simple: Fixed',
content: <Simple ocean={ocean} onChange={handleOceanChange} />
},
{
title: 'Advanced: Dynamic',
content: (
<Advanced
ocean={ocean}
tokensToMint={tokensToMint}
weightOnDataToken={weightOnDataToken}
onChange={handleOceanChange}
/>
)
}
]
return (
<div className={styles.price}>
<Tabs items={tabs} />
<pre>
<code>{JSON.stringify(field.value)}</code>
</pre>
</div>
)
}

View File

@ -66,7 +66,7 @@
padding: calc(var(--spacer) / 2); padding: calc(var(--spacer) / 2);
margin-left: var(--spacer); margin-left: var(--spacer);
text-transform: uppercase; text-transform: uppercase;
color: var(--brand-grey); color: var(--brand-grey-light);
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
font-size: var(--font-size-base); font-size: var(--font-size-base);
position: relative; position: relative;
@ -82,13 +82,13 @@
.link:hover, .link:hover,
.link:focus, .link:focus,
.link:active { .link:active {
color: var(--brand-pink); color: var(--brand-grey);
} }
.link[aria-current], .link[aria-current],
.link[aria-current]:hover, .link[aria-current]:hover,
.link[aria-current]:focus { .link[aria-current]:focus {
color: var(--brand-pink); color: var(--brand-black);
} }
.link:last-child { .link:last-child {

View File

@ -8,12 +8,16 @@
padding: calc(var(--spacer) / 4); padding: calc(var(--spacer) / 4);
white-space: nowrap; white-space: nowrap;
background: none; background: none;
color: var(--brand-grey);
margin: 0; margin: 0;
transition: border 0.2s ease-out; transition: border 0.2s ease-out;
cursor: pointer; cursor: pointer;
} }
.button,
.address {
color: var(--brand-grey);
}
.button:hover, .button:hover,
.button:focus { .button:focus {
transform: none; transform: none;
@ -21,6 +25,10 @@
border-color: var(--brand-grey-light); border-color: var(--brand-grey-light);
} }
.button.initial {
color: var(--brand-pink);
}
.blockies { .blockies {
width: var(--font-size-large); width: var(--font-size-large);
height: var(--font-size-large); height: var(--font-size-large);

View File

@ -1,4 +1,4 @@
import React from 'react' import React, { FormEvent } from 'react'
import styles from './Account.module.css' import styles from './Account.module.css'
import { useOcean } from '@oceanprotocol/react' import { useOcean } from '@oceanprotocol/react'
import { toDataUrl } from 'ethereum-blockies' import { toDataUrl } from 'ethereum-blockies'
@ -26,8 +26,20 @@ const Account = React.forwardRef((props, ref: any) => {
const { accountId, status, connect, chainId } = useOcean() const { accountId, status, connect, chainId } = useOcean()
const hasSuccess = status === 1 && isCorrectNetwork(chainId) const hasSuccess = status === 1 && isCorrectNetwork(chainId)
async function handleActivation(e: FormEvent<HTMLButtonElement>) {
// prevent accidentially submitting a form the button might be in
e.preventDefault()
await connect()
}
return accountId ? ( return accountId ? (
<button className={styles.button} aria-label="Account" ref={ref}> <button
className={styles.button}
aria-label="Account"
ref={ref}
// prevent accidentially submitting a form the button might be in
onClick={(e) => e.preventDefault()}
>
<Blockies account={accountId} /> <Blockies account={accountId} />
<span className={styles.address} title={accountId}> <span className={styles.address} title={accountId}>
{accountTruncate(accountId)} {accountTruncate(accountId)}
@ -39,13 +51,13 @@ const Account = React.forwardRef((props, ref: any) => {
</button> </button>
) : ( ) : (
<button <button
className={styles.button} className={`${styles.button} ${styles.initial}`}
onClick={async () => await connect()} onClick={(e) => handleActivation(e)}
// Need the `ref` here although we do not want // Need the `ref` here although we do not want
// the Tippy to show in this state. // the Tippy to show in this state.
ref={ref} ref={ref}
> >
Activate Wallet Connect Wallet
</button> </button>
) )
}) })

View File

@ -21,7 +21,7 @@ export default function Web3Feedback({
const isOceanConnectionError = status === -1 const isOceanConnectionError = status === -1
const correctNetwork = isCorrectNetwork(chainId) const correctNetwork = isCorrectNetwork(chainId)
const showFeedback = !account || isOceanConnectionError || !correctNetwork const showFeedback = !account || isOceanConnectionError || !correctNetwork
const networkName = appConfig.network.replace(/^\w/, (c: string) => const desiredNetworkName = appConfig.network.replace(/^\w/, (c: string) =>
c.toUpperCase() c.toUpperCase()
) )
@ -50,7 +50,7 @@ export default function Web3Feedback({
: isOceanConnectionError : isOceanConnectionError
? 'Please try again.' ? 'Please try again.'
: !correctNetwork : !correctNetwork
? `Please connect to ${networkName}.` ? `Please connect to ${desiredNetworkName}.`
: isBalanceInsufficient === true : isBalanceInsufficient === true
? 'You do not have enough OCEAN in your wallet to purchase this asset.' ? 'You do not have enough OCEAN in your wallet to purchase this asset.'
: 'Something went wrong.' : 'Something went wrong.'

View File

@ -4,29 +4,3 @@
margin: auto; margin: auto;
padding: 0; padding: 0;
} }
.tabList {
text-align: center;
border-bottom: 1px solid var(--brand-grey-lighter);
padding-top: calc(var(--spacer) / 2);
padding-bottom: calc(var(--spacer) / 2);
}
.tab {
display: inline-block;
padding: calc(var(--spacer) / 12) var(--spacer);
border-radius: var(--border-radius);
font-weight: var(--font-weight-bold);
text-transform: uppercase;
cursor: pointer;
color: var(--brand-grey-light);
}
.tab[aria-selected='true'] {
background: var(--brand-black);
color: var(--brand-white);
}
.tabContent {
padding: var(--spacer);
}

View File

@ -1,10 +1,10 @@
import React, { ReactElement } from 'react' import React, { ReactElement } from 'react'
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'
import styles from './index.module.css' import styles from './index.module.css'
import Compute from './Compute' import Compute from './Compute'
import Consume from './Consume' import Consume from './Consume'
import { MetadataMarket } from '../../../@types/Metadata' import { MetadataMarket } from '../../../@types/Metadata'
import { DDO } from '@oceanprotocol/lib' import { DDO } from '@oceanprotocol/lib'
import Tabs from '../../atoms/Tabs'
export default function AssetActions({ export default function AssetActions({
metadata, metadata,
@ -15,23 +15,22 @@ export default function AssetActions({
}): ReactElement { }): ReactElement {
const { access } = metadata.additionalInformation const { access } = metadata.additionalInformation
const isCompute = access && access === 'Compute' const isCompute = access && access === 'Compute'
const UseContent = isCompute ? (
return (
<Tabs className={styles.actions}>
<TabList className={styles.tabList}>
<Tab className={styles.tab}>Use</Tab>
<Tab className={styles.tab}>Trade</Tab>
</TabList>
<div className={styles.tabContent}>
<TabPanel>
{isCompute ? (
<Compute ddo={ddo} /> <Compute ddo={ddo} />
) : ( ) : (
<Consume ddo={ddo} file={metadata.main.files[0]} /> <Consume ddo={ddo} file={metadata.main.files[0]} />
)}
</TabPanel>
<TabPanel>Trade Me</TabPanel>
</div>
</Tabs>
) )
const tabs = [
{
title: 'Use',
content: UseContent
},
{
title: 'Trade',
content: 'Trade Me'
}
]
return <Tabs items={tabs} className={styles.actions} />
} }

View File

@ -51,6 +51,10 @@ export default function AssetContent({
/> */} /> */}
{/* <DeleteAction ddo={ddo} /> */} {/* <DeleteAction ddo={ddo} /> */}
</div> </div>
<pre>
<code>{JSON.stringify(ddo, null, 2)}</code>
</pre>
</div> </div>
<div> <div>
<div className={styles.sticky}> <div className={styles.sticky}>

View File

@ -38,6 +38,7 @@ export default function Preview({
key.includes('tags') || key.includes('tags') ||
key.includes('files') || key.includes('files') ||
key.includes('termsAndConditions') || key.includes('termsAndConditions') ||
key.includes('price') ||
value === undefined || value === undefined ||
value === '' value === ''
) )

View File

@ -19,7 +19,6 @@ export default function PublishForm({
status, status,
setStatus, setStatus,
isValid, isValid,
touched,
setErrors, setErrors,
setTouched, setTouched,
resetForm, resetForm,
@ -42,7 +41,6 @@ export default function PublishForm({
{content.data.map((field: FormFieldProps) => ( {content.data.map((field: FormFieldProps) => (
<Field key={field.name} {...field} component={Input} /> <Field key={field.name} {...field} component={Input} />
))} ))}
{isLoading ? ( {isLoading ? (
<Loader message={publishStepText} /> <Loader message={publishStepText} />
) : ( ) : (

View File

@ -7,7 +7,7 @@ import styles from './index.module.css'
import PublishForm from './PublishForm' import PublishForm from './PublishForm'
import Web3Feedback from '../../molecules/Wallet/Feedback' import Web3Feedback from '../../molecules/Wallet/Feedback'
import { FormContent } from '../../../@types/Form' import { FormContent } from '../../../@types/Form'
import { initialValues, validationSchema } from './validation' import { initialValues, validationSchema } from '../../../models/FormPublish'
import { MetadataPublishForm } from '../../../@types/Metadata' import { MetadataPublishForm } from '../../../@types/Metadata'
import { transformPublishFormToMetadata } from './utils' import { transformPublishFormToMetadata } from './utils'
import Preview from './Preview' import Preview from './Preview'
@ -28,20 +28,20 @@ export default function PublishPage({
`) `)
const metadata = transformPublishFormToMetadata(values) const metadata = transformPublishFormToMetadata(values)
const tokensToMint = '4' // how to know this? const { cost, tokensToMint } = values.price
const serviceType = values.access === 'Download' ? 'access' : 'compute' const serviceType = values.access === 'Download' ? 'access' : 'compute'
console.log(` console.log(`
Transformed metadata values: Transformed metadata values:
---------------------- ----------------------
${JSON.stringify(metadata, null, 2)} ${JSON.stringify(metadata, null, 2)}
Cost: 1 Cost: ${cost}
Tokens to mint: ${tokensToMint} Tokens to mint: ${tokensToMint}
`) `)
try { try {
const ddo = await publish(metadata as any, tokensToMint, [ const ddo = await publish(metadata as any, tokensToMint.toString(), [
{ serviceType, cost: '1' } { serviceType, cost: cost.toString() }
]) ])
if (publishError) { if (publishError) {
@ -82,6 +82,26 @@ export default function PublishPage({
<Web3Feedback /> <Web3Feedback />
</div> </div>
</aside> </aside>
<div>
<h5>Collected Form Values</h5>
<pre>
<code>{JSON.stringify(values, null, 2)}</code>
</pre>
</div>
<div>
<h5>Transformed Values</h5>
<pre>
<code>
{JSON.stringify(
transformPublishFormToMetadata(values),
null,
2
)}
</code>
</pre>
</div>
</> </>
)} )}
</Formik> </Formik>

View File

@ -16,8 +16,7 @@ export function transformPublishFormToMetadata(
tags, tags,
links, links,
termsAndConditions, termsAndConditions,
files, files
access
} = data } = data
const metadata: MetadataMarket = { const metadata: MetadataMarket = {
@ -38,8 +37,7 @@ export function transformPublishFormToMetadata(
// links: { // links: {
// url: links // url: links
// }, // },
termsAndConditions, termsAndConditions
access: access || 'Download'
}, },
curation: AssetModel.curation curation: AssetModel.curation
} }

View File

@ -1,7 +1,7 @@
code { code {
font-family: var(--font-family-monospace); font-family: var(--font-family-monospace);
font-size: var(--font-size-small); font-size: var(--font-size-small);
color: var(--brand-grey-light); color: var(--brand-grey);
text-shadow: none; text-shadow: none;
} }
@ -14,13 +14,22 @@ code {
pre { pre {
display: block; display: block;
margin: calc(var(--spacer) / 2) 0; margin: calc(var(--spacer) / 2) 0;
padding: 0; padding: calc(var(--spacer) / 2);
border: 1px solid var(--brand-grey-dark); background-color: var(--brand-grey-dimmed) !important;
border-radius: var(--border-radius); border-radius: var(--border-radius);
overflow: auto;
-webkit-overflow-scrolling: touch;
max-height: 800px;
width: 100%;
position: relative;
} }
pre code { pre code {
padding: calc(var(--spacer) / 2); padding: 0;
white-space: pre;
display: block; display: block;
white-space: pre-wrap; overflow-wrap: normal;
word-wrap: normal;
word-break: normal;
width: 100%;
} }

View File

@ -132,6 +132,28 @@ fieldset {
margin: 0; margin: 0;
} }
table {
width: 100%;
border-collapse: collapse;
display: block;
overflow: auto;
-webkit-overflow-scrolling: touch;
}
table th,
table td {
border: 0;
margin: 0;
padding: calc(var(--spacer) / 2);
border-bottom: 1px solid var(--brand-grey-lighter);
text-align: left;
font-size: 90%;
}
table th {
font-weight: var(--font-weight-bold);
}
@import '_code.css'; @import '_code.css';
@import '_toast.css'; @import '_toast.css';
@import '_web3modal.css'; @import '_web3modal.css';

26
src/images/logo.svg Normal file
View File

@ -0,0 +1,26 @@
<svg width="394" height="399" viewBox="0 0 394 399" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M196.742 57.2631C212.525 57.2631 225.33 44.4482 225.33 28.6321C225.33 12.82 212.525 0 196.742 0C180.944 0 168.139 12.82 168.139 28.6321C168.139 44.4482 180.944 57.2631 196.742 57.2631Z" fill="#141414"/>
<path d="M29.167 213.019C40.8609 213.019 50.3274 203.527 50.3274 191.826C50.3274 180.125 40.8609 170.644 29.167 170.644C17.4833 170.644 8.00663 180.125 8.00663 191.826C8.00663 203.527 17.4833 213.019 29.167 213.019Z" fill="#141414"/>
<path d="M365.451 213.019C377.129 213.019 386.606 203.527 386.606 191.826C386.606 180.125 377.129 170.644 365.451 170.644C353.762 170.644 344.285 180.125 344.285 191.826C344.285 203.527 353.762 213.019 365.451 213.019Z" fill="#141414"/>
<path d="M218.467 275.433C218.467 287.132 208.991 296.623 197.309 296.623C185.622 296.623 176.146 287.132 176.146 275.433C176.146 263.733 185.622 254.248 197.309 254.248C208.991 254.248 218.467 263.733 218.467 275.433Z" fill="#141414"/>
<path d="M112.662 255.393C124.355 255.393 133.825 245.902 133.825 234.206C133.825 222.5 124.355 213.019 112.662 213.019C100.975 213.019 91.5044 222.5 91.5044 234.206C91.5044 245.902 100.975 255.393 112.662 255.393Z" fill="#141414"/>
<path d="M301.965 234.206C301.965 245.902 292.489 255.393 280.807 255.393C269.114 255.393 259.644 245.902 259.644 234.206C259.644 222.5 269.114 213.019 280.807 213.019C292.489 213.019 301.965 222.5 301.965 234.206Z" fill="#141414"/>
<path d="M29.1748 270.282C37.0689 270.282 43.4646 263.864 43.4646 255.971C43.4646 248.058 37.0689 241.65 29.1748 241.65C21.2703 241.65 14.8695 248.058 14.8695 255.971C14.8695 263.864 21.2703 270.282 29.1748 270.282Z" fill="#141414"/>
<path d="M379.743 255.971C379.743 263.864 373.338 270.282 365.446 270.282C357.548 270.282 351.148 263.864 351.148 255.971C351.148 248.058 357.548 241.65 365.446 241.65C373.338 241.65 379.743 248.058 379.743 255.971Z" fill="#141414"/>
<path d="M197.312 355.031C205.196 355.031 211.604 348.613 211.604 340.705C211.604 332.802 205.196 326.4 197.312 326.4C189.422 326.4 183.009 332.802 183.009 340.705C183.009 348.613 189.422 355.031 197.312 355.031Z" fill="#141414"/>
<path d="M126.962 298.346C126.962 306.251 120.563 312.656 112.67 312.656C104.772 312.656 98.3672 306.251 98.3672 298.346C98.3672 290.435 104.772 284.025 112.67 284.025C120.563 284.025 126.962 290.435 126.962 298.346Z" fill="#141414"/>
<path d="M280.812 312.656C288.693 312.656 295.102 306.251 295.102 298.346C295.102 290.435 288.693 284.025 280.812 284.025C272.91 284.025 266.507 290.435 266.507 298.346C266.507 306.251 272.91 312.656 280.812 312.656Z" fill="#141414"/>
<path d="M36.6018 306.35C36.6018 310.469 33.2752 313.802 29.1697 313.802C25.0588 313.802 21.7323 310.469 21.7323 306.35C21.7323 302.241 25.0588 298.913 29.1697 298.913C33.2752 298.913 36.6018 302.241 36.6018 306.35Z" fill="#141414"/>
<path d="M365.448 313.802C369.546 313.802 372.88 310.469 372.88 306.35C372.88 302.241 369.546 298.913 365.448 298.913C361.335 298.913 358.011 302.241 358.011 306.35C358.011 310.469 361.335 313.802 365.448 313.802Z" fill="#141414"/>
<path d="M204.741 391.102C204.741 395.214 201.407 398.551 197.309 398.551C193.201 398.551 189.872 395.214 189.872 391.102C189.872 386.989 193.201 383.663 197.309 383.663C201.407 383.663 204.741 386.989 204.741 391.102Z" fill="#141414"/>
<path d="M112.662 356.176C116.768 356.176 120.1 352.839 120.1 348.732C120.1 344.615 116.768 341.288 112.662 341.288C108.562 341.288 105.23 344.615 105.23 348.732C105.23 352.839 108.562 356.176 112.662 356.176Z" fill="#141414"/>
<path d="M288.239 348.732C288.239 352.839 284.905 356.176 280.807 356.176C276.688 356.176 273.369 352.839 273.369 348.732C273.369 344.615 276.688 341.288 280.807 341.288C284.905 341.288 288.239 344.615 288.239 348.732Z" fill="#141414"/>
<path d="M225.33 113.381C225.33 129.195 212.525 142.012 196.742 142.012C180.944 142.012 168.139 129.195 168.139 113.381C168.139 97.572 180.944 84.7494 196.742 84.7494C212.525 84.7494 225.33 97.572 225.33 113.381Z" fill="#141414"/>
<path d="M196.742 225.617C212.525 225.617 225.33 212.797 225.33 196.99C225.33 181.173 212.525 168.353 196.742 168.353C180.944 168.353 168.139 181.173 168.139 196.99C168.139 212.797 180.944 225.617 196.742 225.617Z" fill="#141414"/>
<path d="M393.469 113.381C393.469 129.195 380.667 142.012 364.876 142.012C349.081 142.012 336.279 129.195 336.279 113.381C336.279 97.572 349.081 84.7494 364.876 84.7494C380.667 84.7494 393.469 97.572 393.469 113.381Z" fill="#141414"/>
<path d="M28.5977 142.012C44.3907 142.012 57.1903 129.195 57.1903 113.381C57.1903 97.572 44.3907 84.7494 28.5977 84.7494C12.8046 84.7494 0 97.572 0 113.381C0 129.195 12.8046 142.012 28.5977 142.012Z" fill="#141414"/>
<path d="M141.832 71.0062C141.832 86.818 129.031 99.6378 113.237 99.6378C97.4422 99.6378 84.6416 86.818 84.6416 71.0062C84.6416 55.1842 97.4422 42.3747 113.237 42.3747C129.031 42.3747 141.832 55.1842 141.832 71.0062Z" fill="#141414"/>
<path d="M113.237 183.242C129.031 183.242 141.832 170.421 141.832 154.608C141.832 138.8 129.031 125.979 113.237 125.979C97.4422 125.979 84.6416 138.8 84.6416 154.608C84.6416 170.421 97.4422 183.242 113.237 183.242Z" fill="#141414"/>
<path d="M309.971 71.0062C309.971 86.818 297.167 99.6378 281.373 99.6378C265.58 99.6378 252.781 86.818 252.781 71.0062C252.781 55.1842 265.58 42.3747 281.373 42.3747C297.167 42.3747 309.971 55.1842 309.971 71.0062Z" fill="#141414"/>
<path d="M281.373 183.242C297.167 183.242 309.971 170.421 309.971 154.608C309.971 138.8 297.167 125.979 281.373 125.979C265.58 125.979 252.781 138.8 252.781 154.608C252.781 170.421 265.58 183.242 281.373 183.242Z" fill="#141414"/>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -5,21 +5,20 @@ const AssetModel: MetadataMarket = {
// https://github.com/oceanprotocol/OEPs/tree/master/8 // https://github.com/oceanprotocol/OEPs/tree/master/8
main: { main: {
type: 'dataset', type: 'dataset',
name: '', name: undefined,
dateCreated: '', dateCreated: undefined,
author: '', author: undefined,
license: '', license: undefined,
files: [] files: []
}, },
additionalInformation: { additionalInformation: {
description: '', description: undefined,
copyrightHolder: '', copyrightHolder: undefined,
tags: [], tags: undefined,
links: [], links: undefined,
// custom items // custom items
termsAndConditions: false, termsAndConditions: false
access: 'Download'
}, },
curation: { curation: {
rating: 0, rating: 0,

View File

@ -1,4 +1,4 @@
import { MetadataPublishForm } from '../../../@types/Metadata' import { MetadataPublishForm } from '../@types/Metadata'
import { File as FileMetadata } from '@oceanprotocol/lib' import { File as FileMetadata } from '@oceanprotocol/lib'
import * as Yup from 'yup' import * as Yup from 'yup'
@ -6,7 +6,10 @@ export const validationSchema = Yup.object().shape<MetadataPublishForm>({
// ---- required fields ---- // ---- required fields ----
name: Yup.string().required('Required'), name: Yup.string().required('Required'),
author: Yup.string().required('Required'), author: Yup.string().required('Required'),
cost: Yup.string().required('Required'), price: Yup.object().shape({
cost: Yup.number().required('Required'),
tokensToMint: Yup.number().required('Required')
}),
files: Yup.array<FileMetadata>().required('Required').nullable(), files: Yup.array<FileMetadata>().required('Required').nullable(),
description: Yup.string().required('Required'), description: Yup.string().required('Required'),
license: Yup.string().required('Required'), license: Yup.string().required('Required'),
@ -22,15 +25,18 @@ export const validationSchema = Yup.object().shape<MetadataPublishForm>({
}) })
export const initialValues: MetadataPublishForm = { export const initialValues: MetadataPublishForm = {
name: '', name: undefined,
author: '', author: undefined,
cost: '', price: {
files: '', cost: 1,
description: '', tokensToMint: 1
license: '', },
access: '', files: undefined,
description: undefined,
license: undefined,
access: undefined,
termsAndConditions: false, termsAndConditions: false,
copyrightHolder: '', copyrightHolder: undefined,
tags: '', tags: undefined,
links: '' links: undefined
} }

View File

@ -32,7 +32,6 @@ export const contentQuery = graphql`
type type
required required
options options
min
} }
success success
} }