diff --git a/src/components/atoms/Button.module.scss b/src/components/atoms/Button.module.scss index 8805c03..4a98a2c 100644 --- a/src/components/atoms/Button.module.scss +++ b/src/components/atoms/Button.module.scss @@ -37,10 +37,9 @@ } &:disabled { - color: rgba($brand-white, .7); cursor: not-allowed; pointer-events: none; - opacity: .8; + opacity: .5; } } diff --git a/src/components/atoms/Button.tsx b/src/components/atoms/Button.tsx index 312565b..473db50 100644 --- a/src/components/atoms/Button.tsx +++ b/src/components/atoms/Button.tsx @@ -9,6 +9,7 @@ interface ButtonProps { link?: boolean href?: string onClick?: any + disabled?: boolean } export default class Button extends PureComponent { diff --git a/src/components/atoms/Form/Form.module.scss b/src/components/atoms/Form/Form.module.scss index 27fb268..7d55ec7 100644 --- a/src/components/atoms/Form/Form.module.scss +++ b/src/components/atoms/Form/Form.module.scss @@ -2,6 +2,10 @@ .form { width: 100%; + background: $brand-white; + padding: $spacer; + border: 1px solid $brand-grey-lighter; + border-radius: $border-radius; fieldset { border: 0; @@ -9,6 +13,13 @@ } } +.formMinimal { + composes: form; + background: none; + padding: 0; + border: 0; +} + .formHeader { margin-bottom: $spacer; } diff --git a/src/components/atoms/Form/Form.tsx b/src/components/atoms/Form/Form.tsx index 3ef1cfc..945f50e 100644 --- a/src/components/atoms/Form/Form.tsx +++ b/src/components/atoms/Form/Form.tsx @@ -6,14 +6,20 @@ const Form = ({ description, children, onSubmit, + minimal, ...props }: { title?: string description?: string children: any onSubmit?: any + minimal?: boolean }) => ( -
+ {title && (

{title}

diff --git a/src/components/atoms/Form/Input.tsx b/src/components/atoms/Form/Input.tsx index 405fac7..ad74e73 100644 --- a/src/components/atoms/Form/Input.tsx +++ b/src/components/atoms/Form/Input.tsx @@ -49,7 +49,15 @@ export default class Input extends PureComponent { } public InputComponent = () => { - const { type, options, group, name, required, onChange } = this.props + const { + type, + options, + group, + name, + required, + onChange, + value + } = this.props const wrapClass = this.inputWrapClasses() @@ -64,19 +72,22 @@ export default class Input extends PureComponent { onFocus={this.toggleFocus} onBlur={this.toggleFocus} onChange={onChange} + value={value} > {options && - options.map((option: string, index: number) => ( - - ))} + options + .sort((a, b) => a.localeCompare(b)) + .map((option: string, index: number) => ( + + ))} ) diff --git a/src/components/atoms/Spinner.module.scss b/src/components/atoms/Spinner.module.scss index 78992c6..c5705f6 100644 --- a/src/components/atoms/Spinner.module.scss +++ b/src/components/atoms/Spinner.module.scss @@ -3,7 +3,8 @@ .spinner { position: relative; text-align: center; - margin-bottom: $spacer / 2; + margin-bottom: $spacer; + margin-top: $spacer * 2; &:before { content: ''; @@ -13,7 +14,7 @@ left: 50%; width: 20px; height: 20px; - margin-top: -10px; + margin-top: -20px; margin-left: -10px; border-radius: 50%; border: 2px solid $brand-purple; @@ -24,7 +25,6 @@ .spinnerMessage { color: $brand-grey-light; - margin-top: $spacer / 2; } @keyframes spinner { diff --git a/src/data/form-publish.json b/src/data/form-publish.json index cc221be..588ed38 100644 --- a/src/data/form-publish.json +++ b/src/data/form-publish.json @@ -1,90 +1,114 @@ { - "title": "Publish a new data asset", - "description": "A cool form description", - "fields": { - "name": { - "label": "Title", - "placeholder": "i.e. My cool data set", - "type": "text", - "required": true, - "help": "Help me Obiwan" + "steps": [ + { + "title": "Essentials", + "description": "Start by adding a title and URLs for your data set.", + "fields": { + "name": { + "label": "Title", + "placeholder": "i.e. Almond sales data", + "type": "text", + "required": true, + "help": "Enter a concise title. You will be able to enter a more thorough description in the next step." + }, + "files": { + "label": "Files", + "placeholder": "i.e. https://file.com/file.json", + "type": "text", + "required": true, + "help": "Provide one or multiple urls to your data set files." + } + } }, - "files": { - "label": "Files", - "placeholder": "i.e. https://file.com/file.json", - "type": "text", - "required": true, - "help": "Provide one or multiple links to your data files." + { + "title": "Information", + "description": "Further describe and categorize your data set to help people discover it.", + "fields": { + "description": { + "label": "Description", + "description": "Add a thorough description with as much detail as possible.", + "placeholder": "i.e. Almond sales data ", + "type": "textarea", + "required": true, + "rows": 5 + }, + "categories": { + "label": "Categories", + "description": "Pick a category which best fits your data set.", + "type": "select", + "required": true, + "options": [ + "Image Recognition", + "Dataset Of Datasets", + "Language", + "Performing Arts", + "Visual Arts & Design", + "Philosophy", + "History", + "Theology", + "Anthropology & Archeology", + "Sociology", + "Psychology", + "Politics", + "Interdisciplinary", + "Economics & Finance", + "Demography", + "Biology", + "Chemistry", + "Physics & Energy", + "Earth & Climate", + "Space & Astronomy", + "Mathematics", + "Computer Technology", + "Engineering", + "Agriculture & Bio Engineering", + "Transportation", + "Urban Planning", + "Medicine", + "Business & Management", + "Sports & Recreation", + "Communication & Journalism", + "Other" + ] + } + } }, - "description": { - "label": "Description", - "placeholder": "i.e. My cool data set", - "type": "textarea", - "required": true, - "rows": 5 + { + "title": "Authorship", + "description": "Give proper attribution for your data set.", + "fields": { + "author": { + "label": "Author", + "placeholder": "i.e. Jelly McJellyfish", + "type": "text", + "required": true + }, + "copyrightHolder": { + "label": "Copyright Holder", + "placeholder": "i.e. Marine Institute of Jellyfish", + "type": "text", + "required": true + }, + "license": { + "label": "License", + "type": "select", + "required": true, + "options": [ + "Public Domain", + "CC BY: Attribution", + "CC BY-SA: Attribution ShareAlike", + "CC BY-ND: Attribution-NoDerivs", + "CC BY-NC: Attribution-NonCommercial", + "CC BY-NC-SA: Attribution-NonCommercial-ShareAlike", + "CC BY-NC-ND: Attribution-NonCommercial-NoDerivs" + ] + } + } }, - "categories": { - "label": "Categories", - "type": "select", - "options": [ - "Image Recognition", - "Dataset Of Datasets", - "Language", - "Performing Arts", - "Visual Arts & Design", - "Philosophy", - "History", - "Theology", - "Anthropology & Archeology", - "Sociology", - "Psychology", - "Politics", - "Interdisciplinary", - "Economics & Finance", - "Demography", - "Biology", - "Chemistry", - "Physics & Energy", - "Earth & Climate", - "Space & Astronomy", - "Mathematics", - "Computer Technology", - "Engineering", - "Agriculture & Bio Engineering", - "Transportation", - "Urban Planning", - "Medicine", - "Business & Management", - "Sports & Recreation", - "Communication & Journalism", - "Other" - ] - }, - "copyrightHolder": { - "label": "Copyright Holder", - "placeholder": "i.e. fwhfiw", - "type": "text", - "required": true - }, - "author": { - "label": "Author", - "placeholder": "i.e. Jelly McJellyfish", - "type": "text", - "required": true - }, - "license": { - "label": "License", - "type": "select", - "required": true, - "options": [ - "Public Domain", - "CC BY: Attribution", - "CC BY-SA: Attribution ShareAlike", - "CC BY-ND: Attribution-NoDerivs", - "CC BY-NC: Attribution-NonCommercial", - "CC BY-NC-SA: Attribution-NonCommercial-ShareAlike", - "CC BY-NC-ND: Attribution-NonCommercial-NoDerivs" - ] + { + "title": "Register", + "description": "Splendid, we got all the data. Now let's register your data set.", + "content": "After clicking the button below you will be asked by your wallet to sign this request." } - } + ] } diff --git a/src/routes/Home.module.scss b/src/routes/Home.module.scss index da87cc8..9e3082a 100644 --- a/src/routes/Home.module.scss +++ b/src/routes/Home.module.scss @@ -2,22 +2,9 @@ .home { display: block; -} -.published { - margin-top: $spacer * 3; - margin-bottom: $spacer; - - > div { - text-align: center; - margin-top: $spacer; - margin-bottom: $spacer; + form { + margin-top: $spacer * 2; + margin-bottom: $spacer * 4; } } - -.subTitle { - font-size: $font-size-h4; - color: $brand-grey-light; - border-bottom: 1px solid $brand-grey-lighter; - padding-bottom: $spacer / 2; -} diff --git a/src/routes/Home.tsx b/src/routes/Home.tsx index 1804bf7..7c7dc68 100644 --- a/src/routes/Home.tsx +++ b/src/routes/Home.tsx @@ -26,26 +26,21 @@ class Home extends Component { description={meta.description} className={styles.home} > - + Search} + group={ + + } /> - -
-

Your Data Sets

- -
-

None yet.

- + Publish A Data Set -
-
) } diff --git a/src/routes/Publish/Files/index.tsx b/src/routes/Publish/Files/index.tsx index 600fb61..dd8f40f 100644 --- a/src/routes/Publish/Files/index.tsx +++ b/src/routes/Publish/Files/index.tsx @@ -12,7 +12,6 @@ interface FilesProps { help?: string name: string onChange: any - // resetForm: any } interface FilesStates { @@ -32,12 +31,26 @@ export default class Files extends PureComponent { public addItem = (value: string) => { this.props.files.push({ url: value }) - // this.props.resetForm() + const event = { + currentTarget: { + name: 'files', + value: this.props.files + } + } + this.props.onChange(event as any) this.setState({ isFormShown: !this.state.isFormShown }) } public removeItem = (index: number) => { this.props.files.splice(index, 1) + const event = { + currentTarget: { + name: 'files', + value: this.props.files + } + } + this.props.onChange(event as any) + this.forceUpdate() } public render() { @@ -52,7 +65,7 @@ export default class Files extends PureComponent { diff --git a/src/routes/Publish/Progress.module.scss b/src/routes/Publish/Progress.module.scss new file mode 100644 index 0000000..6e89805 --- /dev/null +++ b/src/routes/Publish/Progress.module.scss @@ -0,0 +1,76 @@ +@import '../../styles/variables'; + +.progress { + display: block; + padding: 0; + position: relative; + margin-top: $spacer * 1.5; + margin-bottom: $spacer; +} + +.item { + display: inline-block; + width: 25%; + text-align: center; + color: $brand-grey-light; + + &:before { + content: ''; + display: block; + width: 60%; + height: .1rem; + background: $brand-grey-lighter; + position: absolute; + top: 20%; + left: -30%; + } + + &:first-child { + &:before { + display: none; + } + } + + span { + display: block; + } +} + +.active { + composes: item; + font-family: $font-family-button; + font-weight: $font-weight-bold; + color: $brand-black; + + .number { + background: $brand-black; + } + + &:before { + background: $green; + } +} + +.completed { + composes: active; + + .number { + background: $green; + } +} + +.label { + margin-top: $spacer / 8; + font-size: $font-size-small; +} + +.number { + width: 1.6rem; + height: 1.6rem; + margin: auto; + border-radius: 50%; + background: $brand-grey-light; + color: $brand-white; + font-family: $font-family-button; + font-weight: $font-weight-bold; +} diff --git a/src/routes/Publish/Progress.tsx b/src/routes/Publish/Progress.tsx new file mode 100644 index 0000000..5880dec --- /dev/null +++ b/src/routes/Publish/Progress.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import styles from './Progress.module.scss' + +const Progress = ({ + currentStep, + steps +}: { + currentStep: number + steps: any[] +}) => { + return ( +
    + {steps.map(({ title }, index) => ( +
  • index + 1 + ? styles.completed + : styles.item + } + > + {index + 1} + {title} +
  • + ))} +
+ ) +} + +export default Progress diff --git a/src/routes/Publish/Step.module.scss b/src/routes/Publish/Step.module.scss new file mode 100644 index 0000000..692e8d4 --- /dev/null +++ b/src/routes/Publish/Step.module.scss @@ -0,0 +1,26 @@ +@import '../../styles/variables'; + +.header { + margin-bottom: $spacer; +} + +.title { + font-size: $font-size-h2; + margin: 0; +} + +.description { + margin-top: $spacer / 4; + margin-bottom: 0; +} + +.actions { + display: flex; + justify-content: space-between; + padding-top: $spacer / 2; + + button:last-child { + min-width: 12rem; + margin-left: auto; + } +} diff --git a/src/routes/Publish/Step.tsx b/src/routes/Publish/Step.tsx new file mode 100644 index 0000000..8ab1014 --- /dev/null +++ b/src/routes/Publish/Step.tsx @@ -0,0 +1,164 @@ +import React, { PureComponent } from 'react' +import Input from '../../components/atoms/Form/Input' +import Label from '../../components/atoms/Form/Label' +import Row from '../../components/atoms/Form/Row' +import Button from '../../components/atoms/Button' +import { User } from '../../context/User' +import Files from './Files/' +import StepRegisterContent from './StepRegisterContent' +import styles from './Step.module.scss' + +interface StepProps { + currentStep: number + index: number + inputChange: any + inputToArrayChange: any + fields?: any[] + state: any + title: string + description: string + next: any + prev: any + totalSteps: number + tryAgain: any + toStart: any + publishedDid?: string + content?: string +} + +export default class Step extends PureComponent { + public previousButton() { + const { currentStep, prev } = this.props + + if (currentStep !== 1) { + return ( + + ) + } + return null + } + + public nextButton() { + const { currentStep, next, totalSteps, state } = this.props + + if (currentStep < totalSteps) { + return ( + + ) + } + return null + } + + public render() { + const { + currentStep, + index, + title, + description, + fields, + inputChange, + inputToArrayChange, + state, + totalSteps, + tryAgain, + toStart, + content + } = this.props + + if (currentStep !== index + 1) { + return null + } + + const lastStep = currentStep === totalSteps + + return ( + <> +
+

{title}

+

{description}

+
+ + {fields && + Object.entries(fields).map(([key, value]) => { + let onChange = inputChange + + if (key === 'categories') { + onChange = inputToArrayChange + } + + if (key === 'files') { + return ( + + + + + ) + } + + return ( + + ) + })} + + {lastStep && ( + + )} + +
+ {this.previousButton()} + {this.nextButton()} + + {lastStep && ( + + {states => + states.isLogged ? ( + + ) : ( + + ) + } + + )} +
+ + ) + } +} + +Step.contextType = User diff --git a/src/routes/Publish/StepRegisterContent.module.scss b/src/routes/Publish/StepRegisterContent.module.scss new file mode 100644 index 0000000..60a5936 --- /dev/null +++ b/src/routes/Publish/StepRegisterContent.module.scss @@ -0,0 +1,5 @@ +@import '../../styles/variables'; + +.message { + margin-bottom: $spacer; +} diff --git a/src/routes/Publish/StepRegisterContent.tsx b/src/routes/Publish/StepRegisterContent.tsx new file mode 100644 index 0000000..ea1a718 --- /dev/null +++ b/src/routes/Publish/StepRegisterContent.tsx @@ -0,0 +1,54 @@ +import React, { PureComponent } from 'react' +import Web3message from '../../components/Web3message' +import Spinner from '../../components/atoms/Spinner' +import styles from './StepRegisterContent.module.scss' + +interface StepRegisterContentProps { + tryAgain: any + toStart: any + state: any + content?: string +} + +export default class StepRegisterContent extends PureComponent< + StepRegisterContentProps, + {} +> { + public publishingState = () => ( + + ) + + public errorState = () => ( +
+ Something went wrong,{' '} + this.props.tryAgain()}>try again +
+ ) + + public publishedState = () => ( +
+ Your asset is published! See it{' '} + here, submit + another asset by clicking{' '} + this.props.toStart()}>here +
+ ) + + public render() { + return ( + <> + + + {this.props.state.isPublishing ? ( + this.publishingState() + ) : this.props.state.publishingError ? ( + this.errorState() + ) : this.props.state.isPublished ? ( + this.publishedState() + ) : ( +

{this.props.content}

+ )} + + ) + } +} diff --git a/src/routes/Publish/index.tsx b/src/routes/Publish/index.tsx index 8e23393..f93b5a5 100644 --- a/src/routes/Publish/index.tsx +++ b/src/routes/Publish/index.tsx @@ -1,17 +1,13 @@ import React, { ChangeEvent, Component, FormEvent } from 'react' import { Logger } from '@oceanprotocol/squid' import Route from '../../components/templates/Route' -import Button from '../../components/atoms/Button' import Form from '../../components/atoms/Form/Form' -import Input from '../../components/atoms/Form/Input' -import Label from '../../components/atoms/Form/Label' -import Row from '../../components/atoms/Form/Row' -import { User } from '../../context/User' import AssetModel from '../../models/AssetModel' -import Web3message from '../../components/Web3message' -import Files from './Files/' +import { User } from '../../context/User' +import Step from './Step' +import Progress from './Progress' -import form from '../../data/form-publish.json' +import { steps } from '../../data/form-publish.json' type AssetType = 'dataset' | 'algorithm' | 'container' | 'workflow' | 'other' @@ -19,22 +15,25 @@ interface PublishState { name?: string dateCreated?: Date description?: string - files?: any[] + files?: string[] price?: number author?: string type?: AssetType license?: string copyrightHolder?: string - categories?: string[] + categories?: string tags?: string[] isPublishing?: boolean isPublished?: boolean publishedDid?: string publishingError?: string + currentStep?: number + validationStatus?: any } class Publish extends Component<{}, PublishState> { public state = { + currentStep: 1, name: '', dateCreated: new Date(), description: '', @@ -44,58 +43,28 @@ class Publish extends Component<{}, PublishState> { type: 'dataset' as AssetType, license: '', copyrightHolder: '', - categories: [], + categories: '', isPublishing: false, isPublished: false, publishedDid: '', - publishingError: '' + publishingError: '', + validationStatus: { + 1: { name: false, files: false, allFieldsValid: false }, + 2: { description: false, categories: false, allFieldsValid: false }, + 3: { + author: false, + copyrightHolder: false, + license: false, + allFieldsValid: false + } + } } - public formFields = (entries: any[]) => - entries.map(([key, value]) => { - let onChange = this.inputChange - - if (key === 'files' || key === 'categories') { - onChange = this.inputToArrayChange - } - - if (key === 'files') { - return ( - - - - - ) - } - - return ( - - ) - }) - private inputChange = ( event: ChangeEvent | ChangeEvent ) => { + this.validateInputs(event.currentTarget.name, event.currentTarget.value) + this.setState({ [event.currentTarget.name]: event.currentTarget.value }) @@ -104,11 +73,28 @@ class Publish extends Component<{}, PublishState> { private inputToArrayChange = ( event: ChangeEvent | ChangeEvent ) => { + this.validateInputs(event.currentTarget.name, event.currentTarget.value) + this.setState({ [event.currentTarget.name]: [event.currentTarget.value] }) } + private next = () => { + let { currentStep } = this.state + const totalSteps = steps.length + + currentStep = + currentStep >= totalSteps - 1 ? totalSteps : currentStep + 1 + this.setState({ currentStep }) + } + + private prev = () => { + let { currentStep } = this.state + currentStep = currentStep <= 1 ? 1 : currentStep - 1 + this.setState({ currentStep }) + } + private tryAgain = () => { this.setState({ publishingError: '' }) } @@ -124,19 +110,140 @@ class Publish extends Component<{}, PublishState> { type: 'dataset' as AssetType, license: '', copyrightHolder: '', - categories: [], + categories: '', isPublishing: false, - isPublished: false + isPublished: false, + currentStep: 1 }) } + private validateInputs = (name: string, value: any) => { + let hasContent = value.length > 0 + + // Setting state for all fields + if (hasContent) { + this.setState( + prevState => ({ + validationStatus: { + ...prevState.validationStatus, + [this.state.currentStep]: { + ...prevState.validationStatus[ + this.state.currentStep + ], + [name]: true + } + } + }), + this.runValidation + ) + } else { + this.setState( + prevState => ({ + validationStatus: { + ...prevState.validationStatus, + [this.state.currentStep]: { + ...prevState.validationStatus[ + this.state.currentStep + ], + [name]: false + } + } + }), + this.runValidation + ) + } + } + + private runValidation = () => { + let { validationStatus } = this.state + // + // Step 1 + // + if (validationStatus[1].name && validationStatus[1].files) { + this.setState(prevState => ({ + validationStatus: { + ...prevState.validationStatus, + 1: { + ...prevState.validationStatus[1], + allFieldsValid: true + } + } + })) + } else { + this.setState(prevState => ({ + validationStatus: { + ...prevState.validationStatus, + 1: { + ...prevState.validationStatus[1], + allFieldsValid: false + } + } + })) + } + + // + // Step 2 + // + if (validationStatus[2].description && validationStatus[2].categories) { + this.setState(prevState => ({ + validationStatus: { + ...prevState.validationStatus, + 2: { + ...prevState.validationStatus[2], + allFieldsValid: true + } + } + })) + } else { + this.setState(prevState => ({ + validationStatus: { + ...prevState.validationStatus, + 2: { + ...prevState.validationStatus[2], + allFieldsValid: false + } + } + })) + } + + // + // Step 3 + // + if ( + validationStatus[3].author && + validationStatus[3].copyrightHolder && + validationStatus[3].license + ) { + this.setState(prevState => ({ + validationStatus: { + ...prevState.validationStatus, + 3: { + ...prevState.validationStatus[3], + allFieldsValid: true + } + } + })) + } else { + this.setState(prevState => ({ + validationStatus: { + ...prevState.validationStatus, + 3: { + ...prevState.validationStatus[3], + allFieldsValid: false + } + } + })) + } + } + private registerAsset = async (event: FormEvent) => { event.preventDefault() this.setState({ publishingError: '', isPublishing: true }) - const account = await this.context.ocean.getAccounts() + const { ocean } = this.context + const account = await ocean.getAccounts() const newAsset = { // OEP-08 Attributes // https://github.com/oceanprotocol/OEPs/tree/master/8 @@ -166,10 +273,7 @@ class Publish extends Component<{}, PublishState> { } try { - const asset = await this.context.ocean.registerAsset( - newAsset, - account[0] - ) + const asset = await ocean.registerAsset(newAsset, account[0]) this.setState({ publishedDid: asset.id, isPublished: true @@ -187,66 +291,37 @@ class Publish extends Component<{}, PublishState> { } public render() { - const entries = Object.entries(form.fields) - return ( - - + + - {this.state.isPublishing ? ( - this.publishingState() - ) : this.state.publishingError ? ( - this.errorState() - ) : this.state.isPublished ? ( - this.publishedState() - ) : ( -
- {this.formFields(entries)} - - - {states => - states.isLogged ? ( - - ) : ( - - ) - } - -
- )} +
+ {steps.map((step: any, index: number) => ( + + ))} +
) } - - public publishingState = () => { - return
Please sign with your crypto wallet
- } - - public errorState = () => { - return ( -
- Something went wrong,{' '} - this.tryAgain()}>try again -
- ) - } - - public publishedState = () => { - return ( -
- Your asset is published! See it{' '} - here, submit - another asset by clicking{' '} - this.toStart()}>here -
- ) - } } Publish.contextType = User