From 64fdef7697d732e95dd30acfd0625a125f874069 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Sat, 27 Apr 2019 18:10:34 -0400 Subject: [PATCH] feat: withdrawal screens + prettier --- app/images/red-info.svg | 8 + app/js/App.js | 54 +++--- app/js/components/Address.js | 56 ++++++ app/js/components/Admin.js | 185 ++++++++++--------- app/js/components/Allocation.js | 34 ++-- app/js/components/Complete.js | 12 +- app/js/components/ContributorSelection.js | 41 ++--- app/js/components/Error.js | 12 +- app/js/components/Header.js | 32 ++-- app/js/components/History.js | 9 - app/js/components/Home.js | 205 +++++++++++----------- app/js/components/Loading.js | 2 +- app/js/components/Step1.js | 50 ++++-- app/js/components/Step2.js | 31 ++++ app/js/components/Withdrawal.js | 72 ++++++++ app/js/components/allocation.scss | 15 -- app/js/components/home.scss | 23 +++ app/js/components/withdrawal.scss | 4 + app/js/contributors.js | 90 +++++----- app/js/index.js | 23 ++- app/js/services/Meritocracy.js | 64 +++---- app/js/validators.js | 70 ++++++-- 22 files changed, 676 insertions(+), 416 deletions(-) create mode 100644 app/images/red-info.svg create mode 100644 app/js/components/Address.js delete mode 100644 app/js/components/History.js create mode 100644 app/js/components/Step2.js create mode 100644 app/js/components/Withdrawal.js delete mode 100644 app/js/components/allocation.scss create mode 100644 app/js/components/withdrawal.scss diff --git a/app/images/red-info.svg b/app/images/red-info.svg new file mode 100644 index 0000000..d6d2002 --- /dev/null +++ b/app/images/red-info.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/js/App.js b/app/js/App.js index da362dd..57703f8 100644 --- a/app/js/App.js +++ b/app/js/App.js @@ -1,11 +1,11 @@ /*global web3*/ -import React, {Fragment} from 'react'; -import {HashRouter, Route, Redirect, Switch} from "react-router-dom"; +import React from 'react'; +import { HashRouter, Route, Redirect, Switch } from 'react-router-dom'; import ThemeProvider from 'react-bootstrap/ThemeProvider'; import EmbarkJS from 'Embark/EmbarkJS'; -import {isAdmin} from './services/Meritocracy'; +import { isAdmin } from './services/Meritocracy'; import Header from './components/Header'; import Home from './components/Home'; import Admin from './components/Admin'; @@ -14,7 +14,6 @@ const MAINNET = 1; const TESTNET = 3; class App extends React.Component { - state = { error: null, loading: true, @@ -22,52 +21,57 @@ class App extends React.Component { }; componentDidMount() { - EmbarkJS.onReady(async (err) => { + EmbarkJS.onReady(async err => { if (err) { - return this.setState({error: err.message || err}); + return this.setState({ error: err.message || err }); } const netId = await web3.eth.net.getId(); if (EmbarkJS.environment === 'testnet' && netId !== TESTNET) { - return this.setState({error: 'Please connect to Ropsten'}); + return this.setState({ error: 'Please connect to Ropsten' }); } if (EmbarkJS.environment === 'livenet' && netId !== MAINNET) { - return this.setState({error: 'Please connect to Mainnet'}); + return this.setState({ error: 'Please connect to Mainnet' }); } const isUserAdmin = await isAdmin(web3.eth.defaultAccount); - this.setState({loading: false, isUserAdmin}) + this.setState({ loading: false, isUserAdmin }); }); } render() { - const {error, loading, isUserAdmin} = this.state; + const { error, loading, isUserAdmin } = this.state; if (error) { - return (
-
Something went wrong connecting to Ethereum. Please make sure you have a node running or are using Metamask - to connect to the Ethereum network: + return ( +
+
+ Something went wrong connecting to Ethereum. Please make sure you have a node running or are using Metamask + to connect to the Ethereum network: +
+
{error}
-
{error}
-
); + ); } if (loading) { return

Loading, please wait

; } - return ( - -
- - - {isUserAdmin && } + return ( + + +
+ + + {isUserAdmin && } - - - - ); + + + + + ); } } diff --git a/app/js/components/Address.js b/app/js/components/Address.js new file mode 100644 index 0000000..0a427da --- /dev/null +++ b/app/js/components/Address.js @@ -0,0 +1,56 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +function compactAddress(addr) { + return addr.substring(0, 6) + '...' + addr.substring(38); +} + +class Address extends Component { + constructor(props) { + super(props); + this.state = { + addressHovered: false, + fixed: false + }; + } + + mouseOverAddress = () => { + this.setState({ addressHovered: true }); + }; + + mouseOutAddress = () => { + this.setState({ addressHovered: false }); + }; + + handleClick = () => { + this.setState({ fixed: !this.state.fixed }); + }; + + render() { + const address = + this.props.compact || (!this.state.fixed && !this.state.addressHovered) + ? compactAddress(this.props.value) + : this.props.value; + return ( + + {address} + + ); + } +} + +Address.defaultProps = { + compact: false +}; + +Address.propTypes = { + value: PropTypes.string, + compact: PropTypes.bool +}; + +export default Address; diff --git a/app/js/components/Admin.js b/app/js/components/Admin.js index 0bf0c4f..75417d8 100644 --- a/app/js/components/Admin.js +++ b/app/js/components/Admin.js @@ -1,13 +1,12 @@ -/*global web3*/ -import React, {Fragment} from 'react'; -import {Button, Form, Alert, ListGroup, OverlayTrigger, Tooltip, Modal} from 'react-bootstrap'; +import React, { Fragment } from 'react'; +import { Button, Form, Alert, ListGroup, OverlayTrigger, Tooltip, Modal } from 'react-bootstrap'; import ValidatedForm from 'react-validation/build/form'; import Input from 'react-validation/build/input'; -import {required, isAddress} from '../validators'; -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faTrash} from "@fortawesome/free-solid-svg-icons"; +import { required, isAddress } from '../validators'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTrash } from '@fortawesome/free-solid-svg-icons'; -import {addContributor, getFormattedContributorList, removeContributor} from '../services/Meritocracy'; +import { addContributor, getFormattedContributorList, removeContributor } from '../services/Meritocracy'; import './admin.scss'; @@ -27,121 +26,139 @@ class Admin extends React.Component { try { const contributorList = await getFormattedContributorList(); - this.setState({busy: false, contributorList}); - } catch (e) { - this.setState({errorMsg: e.message || e}); + this.setState({ busy: false, contributorList }); + } catch (error) { + this.setState({ errorMsg: error.message || error }); } } onChange = (name, e) => { - this.setState({[name]: e.target.value}); + this.setState({ [name]: e.target.value }); }; - addContributor = async (e) => { + addContributor = async e => { e.preventDefault(); - this.setState({busy: true, successMsg: ''}); + this.setState({ busy: true, successMsg: '' }); try { await addContributor(this.state.contributorName, this.state.contributorAddress); const contributorList = this.state.contributorList; - contributorList.push({label: this.state.contributorName, value: this.state.contributorAddress}); + contributorList.push({ label: this.state.contributorName, value: this.state.contributorAddress }); - this.setState({busy: false, successMsg: 'Contributor added!'}); - } catch (e) { - this.setState({error: e.message || e, busy: false}); + this.setState({ busy: false, successMsg: 'Contributor added!' }); + } catch (error) { + this.setState({ error: error.message || error, busy: false }); } }; removeContributor = (e, contributorIndex) => { e.preventDefault(); - this.setState({focusedContributorIndex: contributorIndex, showDeleteModal: true}); + this.setState({ focusedContributorIndex: contributorIndex, showDeleteModal: true }); }; doRemove = async () => { const idx = this.state.focusedContributorIndex; - this.setState({focusedContributorIndex: -1, showDeleteModal: false, busy: true}); + this.setState({ focusedContributorIndex: -1, showDeleteModal: false, busy: true }); try { await removeContributor(this.state.contributorList[idx].value); const contributorList = this.state.contributorList; contributorList.splice(idx, 1); - this.setState({contributorList, busy: false, successMsg: 'Contributor removed!'}); - } catch (e) { - this.setState({error: e.message || e, busy: false}); + this.setState({ contributorList, busy: false, successMsg: 'Contributor removed!' }); + } catch (error) { + this.setState({ error: error.message || error, busy: false }); } }; handleClose = () => { - this.setState({showDeleteModal: false}); + this.setState({ showDeleteModal: false }); }; render() { - const {contributorAddress, contributorName, error, busy, contributorList, successMsg, focusedContributorIndex} = this.state; + const { + contributorAddress, + contributorName, + error, + busy, + contributorList, + successMsg, + focusedContributorIndex + } = this.state; const currentContributor = focusedContributorIndex > -1 ? contributorList[focusedContributorIndex] : {}; - return ( -

Admin Panel

- {error && {error}} - {successMsg && {successMsg}} - {busy && Working...} -

Add a contributor

- this.addContributor(e)}> - - Contributor name - this.onChange('contributorName', e)} - className="form-control" - validations={[required]}/> - + return ( + +

Admin Panel

+ {error && {error}} + {successMsg && {successMsg}} + {busy && Working...} +

Add a contributor

+ this.addContributor(e)}> + + Contributor name + this.onChange('contributorName', e)} + className="form-control" + validations={[required]} + /> + - - Contributor address - this.onChange('contributorAddress', e)} - className="form-control" - validations={[required, isAddress]}/> - - - -

Contributor List

- - {contributorList.map((contributor, idx) => ( - - {contributor.label}: {contributor.value} - -
- - Delete contributor - - }> - this.removeContributor(e, idx)}/> - -
-
- ))} -
- - - - Are you sure you want to remove this contributor? - - -

Name: {currentContributor.label}

-

Address: {currentContributor.value}

-
- - - - -
-
); +
+

Contributor List

+ + {contributorList.map((contributor, idx) => ( + + {contributor.label}: {contributor.value} +
+ Delete contributor}> + this.removeContributor(e, idx)} + /> + +
+
+ ))} +
+ + + + Are you sure you want to remove this contributor? + + +

Name: {currentContributor.label}

+

Address: {currentContributor.value}

+
+ + + + +
+
+ ); } } diff --git a/app/js/components/Allocation.js b/app/js/components/Allocation.js index 0df7b0b..95520b6 100644 --- a/app/js/components/Allocation.js +++ b/app/js/components/Allocation.js @@ -1,40 +1,44 @@ -import React, {Component} from 'react'; +import React, { Component } from 'react'; import info from '../../images/info.svg'; import downArrow from '../../images/down-arrow.svg'; -import "./allocation.scss"; - class Allocation extends Component { state = { showHelp: false - } + }; - handleClickHelp = (e) => { + handleClickHelp = e => { e.preventDefault(); this.setState(prevState => ({ showHelp: !prevState.showHelp })); - } + }; render() { - const {value} = this.props; - const {showHelp} = this.state; - + const { value } = this.props; + const { showHelp } = this.state; + return (

Reward Status contributors for all the times they impressed you.

- Learn more + + Learn more +

{showHelp && (

- Status Meritocracy is an SNT Reward System that allows a Contributor in the registry to - award allocated SNT, along with praise, to other Contributors.
- Register to - receive a budget and participate.

+ Status Meritocracy is an SNT Reward System that allows a Contributor in the registry to award allocated + SNT, along with praise, to other Contributors. +
+ Register to receive a + budget and participate. +

)} -

{value} SNT

+

+ {value} SNT +

Available

); diff --git a/app/js/components/Complete.js b/app/js/components/Complete.js index 38525cb..01cc8c1 100644 --- a/app/js/components/Complete.js +++ b/app/js/components/Complete.js @@ -1,14 +1,18 @@ import React from 'react'; import CompleteIcon from '../../images/complete.png'; -import {Button} from 'react-bootstrap'; +import { Button } from 'react-bootstrap'; -const Complete = ({onClick}) => ( +const Complete = ({ onClick }) => (

Thank you

Your SNT has been awarded.

-

+

+ +

-) +); export default Complete; diff --git a/app/js/components/ContributorSelection.js b/app/js/components/ContributorSelection.js index 2a0d36f..10f23be 100644 --- a/app/js/components/ContributorSelection.js +++ b/app/js/components/ContributorSelection.js @@ -1,25 +1,31 @@ -import React, {Fragment} from 'react'; +import React, { Fragment } from 'react'; import Select from 'react-select'; -import {Form} from 'react-bootstrap'; +import { Form } from 'react-bootstrap'; import Allocation from './Allocation'; import statusLogo from '../../images/status-logo.svg'; -import "./contributor-selector.scss"; +import './contributor-selector.scss'; -const sortByAlpha = (a,b) => { +const sortByAlpha = (a, b) => { if (a.label < b.label) return -1; if (a.label > b.label) return 1; return 0; -} +}; -const ContributorSelection = ({allocation, contributorList, selectedContributors, onSelectContributor, onChangeAward, onClickPlus5, award}) => ( +const ContributorSelection = ({ + allocation, + contributorList, + selectedContributors, + onSelectContributor, + onChangeAward, + onClickPlus5, + award +}) => (
-
- Enter contributors and award SNT -
+
Enter contributors and award SNT
+5 @@ -37,33 +43,28 @@ const ContributorSelection = ({allocation, contributorList, selectedContributors options={contributorList.sort(sortByAlpha)} placeholder="Choose Contributor(s)..." className="mb-2 contributorSelector" - theme={(theme) => ({ + theme={theme => ({ ...theme, borderRadius: '4px', border: 'none', padding: '10px', colors: { - ...theme.colors, + ...theme.colors, neutral0: '#EEF2F5', - neutral10: '#EEF2F5', + neutral10: '#EEF2F5' }, spacing: { ...theme.spacing, - controlHeight: 50, + controlHeight: 50 } })} />
- +
-
+
); diff --git a/app/js/components/Error.js b/app/js/components/Error.js index 84bf894..b9c2fe1 100644 --- a/app/js/components/Error.js +++ b/app/js/components/Error.js @@ -1,14 +1,18 @@ import React from 'react'; import ErrorIcon from '../../images/error.png'; -import {Button} from 'react-bootstrap'; +import { Button } from 'react-bootstrap'; -const Error = ({onClick, title, message}) => ( +const Error = ({ onClick, title, message }) => (

{title}

{message}

-

+

+ +

-) +); export default Error; diff --git a/app/js/components/Header.js b/app/js/components/Header.js index 4a351f8..ca9f9b5 100644 --- a/app/js/components/Header.js +++ b/app/js/components/Header.js @@ -1,19 +1,25 @@ -import React from 'react' -import {Navbar, Nav} from 'react-bootstrap'; +import React from 'react'; +import { Navbar, Nav } from 'react-bootstrap'; -import './header.scss' +import './header.scss'; import logo from '../../images/logo.png'; -const Header = ({isUserAdmin}) => ( - LogoStatus Meritocracy - - {isUserAdmin && - - } +const Header = ({ isUserAdmin }) => ( + + + Logo + Status Meritocracy + + + {isUserAdmin && ( + + + + )} ); -export default Header +export default Header; diff --git a/app/js/components/History.js b/app/js/components/History.js deleted file mode 100644 index 6ae75f0..0000000 --- a/app/js/components/History.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -const History = ({value}) => ( -
- -
-); - -export default Allocation; diff --git a/app/js/components/Home.js b/app/js/components/Home.js index 70bb781..2876681 100644 --- a/app/js/components/Home.js +++ b/app/js/components/Home.js @@ -1,14 +1,15 @@ /*global web3*/ -import React, {Fragment} from 'react'; -import {Row, Col, Alert, Button, Container, Form, Tabs, Tab} from 'react-bootstrap'; +import React, { Fragment } from 'react'; +import { Tabs, Tab } from 'react-bootstrap'; import Meritocracy from 'Embark/contracts/Meritocracy'; -import arrowLeft from '../../images/arrow-left.svg'; -import {getFormattedContributorList, getCurrentContributorData} from '../services/Meritocracy'; +import { getFormattedContributorList, getCurrentContributorData } from '../services/Meritocracy'; import './home.scss'; import Step1 from './Step1'; +import Step2 from './Step2'; import Loading from './Loading'; import Complete from './Complete'; import Error from './Error'; +import Withdrawal from './Withdrawal'; /* TODO: @@ -17,10 +18,8 @@ TODO: */ class Home extends React.Component { - state = { errorMsg: null, - busy: true, selectedContributors: [], contributorList: [], currentContributor: { @@ -32,8 +31,9 @@ class Home extends React.Component { }, award: 0, praise: '', - step: 'ERROR', + step: 'HOME', checkbox: false, + tab: 'reward' }; constructor(props) { @@ -52,9 +52,9 @@ class Home extends React.Component { const currentContributor = await getCurrentContributorData(); - this.setState({busy: false, currentContributor, contributorList}); - } catch (e) { - this.setState({errorMsg: e.message || e}); + this.setState({ busy: false, currentContributor, contributorList }); + } catch (error) { + this.setState({ errorMsg: error.message || error }); } } @@ -65,8 +65,8 @@ class Home extends React.Component { } handleAwardChange(e) { - if(e.target.value.trim() === "") { - this.setState({award: ""}); + if (e.target.value.trim() === '') { + this.setState({ award: '' }); return; } this._setAward(e.target.value); @@ -74,49 +74,54 @@ class Home extends React.Component { handlePlus5 = () => { this._setAward(this.state.award + 5); - } - - _setAward = (value) => { - let _amount = parseInt(value, 10); - if(_amount < 0 || isNaN(_amount)) _amount = 0; - - const { currentContributor: {allocation}, selectedContributors} = this.state; - const maxAllocation = selectedContributors.length ? Math.floor(allocation / selectedContributors.length) : 0; - const award = (_amount <= maxAllocation ? _amount : maxAllocation ); + }; - this.setState({award}); - } + _setAward = value => { + let _amount = parseInt(value, 10); + if (_amount < 0 || isNaN(_amount)) _amount = 0; + + const { + currentContributor: { allocation }, + selectedContributors + } = this.state; + const maxAllocation = selectedContributors.length > 0 ? Math.floor(allocation / selectedContributors.length) : 0; + const award = _amount <= maxAllocation ? _amount : maxAllocation; + + this.setState({ award }); + }; handlePraiseChange(e) { this.setState({ praise: e.target.value }); } - handleCheckbox = (e) => { + handleCheckbox = () => { this.setState(prevState => ({ checkbox: !prevState.checkbox })); - } + }; - resetUIFields(){ + resetUIFields() { this.setState({ praise: '', selectedContributors: [], errorMsg: '', - award: 0 + award: 0, + checkbox: false }); } - async awardTokens(e) { - const {award, selectedContributors, praise} = this.state; + async awardTokens() { + const { award, selectedContributors, praise } = this.state; - this.moveStep('BUSY'); + this.moveStep('BUSY')(); let addresses = selectedContributors.map(a => a.value); - const sntAmount = web3.utils.toWei(award.toString(), "ether"); + const sntAmount = web3.utils.toWei(award.toString(), 'ether'); let toSend; - switch(addresses.length) { + + switch (addresses.length) { case 0: - this.setState({errorMsg: 'No Contributor Selected'}); + this.setState({ errorMsg: 'No Contributor Selected' }); return; case 1: toSend = Meritocracy.methods.award(addresses[0], sntAmount, praise); @@ -127,69 +132,74 @@ class Home extends React.Component { } try { - const estimatedGas = await toSend.estimateGas({from: web3.eth.defaultAccount}); - const receipt = await toSend.send({from: web3.eth.defaultAccount, gas: estimatedGas + 1000}); + const estimatedGas = await toSend.estimateGas({ from: web3.eth.defaultAccount }); + await toSend.send({ from: web3.eth.defaultAccount, gas: estimatedGas + 1000 }); this.resetUIFields(); const currentContributor = await getCurrentContributorData(); - this.setState({currentContributor}); + this.setState({ currentContributor }); this.moveStep('COMPLETE')(); - } catch(e) { - this.setState({errorMsg: 'tx failed? got enough tokens to award?'}); - console.error(e); - } finally { - + } catch (error) { + this.setState({ errorMsg: 'tx failed? got enough tokens to award?' }); + console.error(error); } } - - async withdrawTokens(e) { - const {currentContributor} = this.state; + async withdrawTokens() { + const { currentContributor } = this.state; if (currentContributor.received === 0) { - this.setState({errorMsg: 'can only call withdraw when you have tokens'}); + this.setState({ errorMsg: 'can only call withdraw when you have tokens' }); return; } - if ( currentContributor.allocation > 0 ) { - this.setState({errorMsg: 'you must allocate all your tokens'}); + if (currentContributor.allocation > 0) { + this.setState({ errorMsg: 'you must allocate all your tokens' }); return; } + this.moveStep('BUSY')(); + const toSend = Meritocracy.methods.withdraw(); try { - this.setState({busy: true}); + this.setState({ busy: true }); - const estimatedGas = await toSend.estimateGas({from: web3.eth.defaultAccount}); - const receipt = await toSend.send({from: web3.eth.defaultAccount, gas: estimatedGas + 1000}); + const estimatedGas = await toSend.estimateGas({ from: web3.eth.defaultAccount }); + await toSend.send({ from: web3.eth.defaultAccount, gas: estimatedGas + 1000 }); const currentContributor = await getCurrentContributorData(); - this.setState({currentContributor}); - - } catch(e) { - this.setState({errorMsg: 'tx failed? Did you allocate all your tokens first?'}); - console.error(e); - } finally { - this.setState({busy: false}); + this.setState({ currentContributor }); + + this.moveStep('COMPLETE')(); + } catch (error) { + console.error(error); + this.setState({ errorMsg: 'tx failed? Did you allocate all your tokens first?' }); } } - moveStep = nexStep => () => { - this.setState({step: nexStep}); - } - + this.setState({ step: nexStep, errorMsg: '' }); + }; render() { - const { selectedContributors, contributorList, award, currentContributor, praise, busy, errorMsg, step, checkbox } = this.state; + const { + selectedContributors, + contributorList, + award, + currentContributor, + praise, + errorMsg, + step, + checkbox, + tab + } = this.state; - if(errorMsg) return ; + if (errorMsg) return ; return ( - + this.setState({ tab })}> - {step === 'HOME' && ( -

Research shows that a note of praise and learning how much our work helped others, increases motivation.

-

- { selectedContributors.map(x => x.label).join(', ') } - SNT {award * selectedContributors.length} -

-
- Add note - - -
- - -
-
+ )} - { step === 'BUSY' && } - { step === 'COMPLETE' && } - { step === 'ERROR' && } + {step === 'BUSY' && } + + {step === 'COMPLETE' && } - -

Your Total Received Kudos: {currentContributor.totalReceived || 0} SNT

-

Your Total Forfeited Kudos: {currentContributor.totalForfeited || 0} SNT

- -

Your Kudos History

-

Your Received Kudos: {currentContributor.received} SNT

- -

- -

- - - - {currentContributor.praises && currentContributor.praises.map((item, i) => { - const name = options.find(x => x.value === item.author); - return {(name && name.label) || item.author} has sent - you {web3.utils.fromWei(item.amount, "ether")} SNT {item.praise && "\"" + item.praise + "\""}; - })} - - - + + {step === 'HOME' && ( + + )} + {step === 'BUSY' && } + {step === 'COMPLETE' && } diff --git a/app/js/components/Loading.js b/app/js/components/Loading.js index fe17843..1f3ec65 100644 --- a/app/js/components/Loading.js +++ b/app/js/components/Loading.js @@ -7,6 +7,6 @@ const Loading = () => (
Waiting for the confirmation from miners
-) +); export default Loading; diff --git a/app/js/components/Step1.js b/app/js/components/Step1.js index 70ee3be..1e6e192 100644 --- a/app/js/components/Step1.js +++ b/app/js/components/Step1.js @@ -1,11 +1,22 @@ -import React, {Fragment} from 'react'; +import React, { Fragment } from 'react'; import arrowRight from '../../images/arrow-right.svg'; import ContributorSelection from './ContributorSelection'; -import {Button, Form} from 'react-bootstrap'; +import { Button, Form } from 'react-bootstrap'; -const Step1 = ({allocation, onChangeAward, onSelectContributor, onClickPlus5, contributorList, selectedContributors, award, isChecked, onClickCheckbox, onClickNext}) => ( - - ( + + - - - + + + -
- -
-
+
+ +
+
); -export default Step1; \ No newline at end of file +export default Step1; diff --git a/app/js/components/Step2.js b/app/js/components/Step2.js new file mode 100644 index 0000000..3b9d74b --- /dev/null +++ b/app/js/components/Step2.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { Button, Form } from 'react-bootstrap'; +import arrowLeft from '../../images/arrow-left.svg'; + +const Step2 = ({ selectedContributors, award, praise, onChangeNote, onClickBack, onClickAward }) => ( +
+

+ Research shows that a note of praise and learning how much our work helped others, increases motivation. +

+

+ {selectedContributors.map(x => x.label).join(', ')} + + SNT {award * selectedContributors.length} + +

+
+ Add note + + +
+ + +
+
+); + +export default Step2; diff --git a/app/js/components/Withdrawal.js b/app/js/components/Withdrawal.js new file mode 100644 index 0000000..52ac800 --- /dev/null +++ b/app/js/components/Withdrawal.js @@ -0,0 +1,72 @@ +/* global web3 */ +import React, { Fragment } from 'react'; +import { Row, Col, Button, Container } from 'react-bootstrap'; +import moment from 'moment'; +import info from '../../images/red-info.svg'; +import Address from './Address'; + +import './withdrawal.scss'; + +const Withdrawal = ({ totalReceived, allocation, onClick, contributorList, praises }) => ( + +
+

You have been awarded

+

+ {totalReceived || 0} SNT +

+

Available for withdraw

+
+ + + {praises && + praises.map((item, i) => { + const name = contributorList.find(x => x.value === item.author); + const date = moment.unix(item.time).fromNow(); + return ( + + + {!item.praise && ( + + {(name && name.label) ||
} has sent you{' '} + {web3.utils.fromWei(item.amount, 'ether')} SNT {date} + + )} + + {item.praise && ( + + {(name && name.label) || item.author}, {date} +
+ "{item.praise}" + {web3.utils.fromWei(item.amount, 'ether')} SNT +
+
+ )} + + + ); + })} + + +

+ +

+ + {allocation !== '0' && ( +
+ +

+ Your budget wasn’t fullly rewarded to others. Note that you can only withdraw your own reward if you’ve spend + your full budget to reward others. +

+
+ )} + +); + +export default Withdrawal; diff --git a/app/js/components/allocation.scss b/app/js/components/allocation.scss deleted file mode 100644 index 3f144a3..0000000 --- a/app/js/components/allocation.scss +++ /dev/null @@ -1,15 +0,0 @@ -.learn-more { - display: flex; - - img { - width: 25px; - margin: 0 10px 0 0; - object-fit: contain; - align-self: flex-start; - } - - p { - flex: 1 1 auto; - } - -} diff --git a/app/js/components/home.scss b/app/js/components/home.scss index 65f871f..b75a4fd 100644 --- a/app/js/components/home.scss +++ b/app/js/components/home.scss @@ -37,3 +37,26 @@ font-size: 13px; } } + +.withdraw-panel { + .awarded { + font-size: 32px; + } +} + +.learn-more { + display: flex; + + img { + width: 25px; + margin: 0 10px 0 0; + object-fit: contain; + align-self: flex-start; + } + + p { + flex: 1 1 auto; + } + +} + diff --git a/app/js/components/withdrawal.scss b/app/js/components/withdrawal.scss new file mode 100644 index 0000000..2888e7b --- /dev/null +++ b/app/js/components/withdrawal.scss @@ -0,0 +1,4 @@ +.chatBubble { + background: #ECEFFC; + border-radius: 8px; +} \ No newline at end of file diff --git a/app/js/contributors.js b/app/js/contributors.js index 01d396c..73a72b6 100644 --- a/app/js/contributors.js +++ b/app/js/contributors.js @@ -1,49 +1,49 @@ const contributors = [ - { 'label' : 'Andreas S.', 'value' : '0x4923121411e884a4af66ec025712eba600a782d3' }, - { 'label' : 'andrey.dev', 'value' : '0xA4EcA293cb578a68b190e3e07c2B170dc753fe44' }, - { 'label' : 'Anna', 'value': '0x2e1ce0f514387a188f4aeff4ceb6c2c0dea66ca7'}, - { 'label' : 'barry', 'value' : '0xa46b0546481a04b7de049a8a20f8a9b2b2c5cc05' }, - { 'label' : 'BrianXV', 'value' : '0x03b832b3fa819d7a4b6c819e4df1e60a173e739a' }, - { 'label' : 'cammellos', 'value' : '0xd0ec8a940fe9712c1521c2190f41604ecaa7ec9e' }, - { 'label' : 'Carl', 'value': '0x9fb937ab76b68dae29e3de20cf797d44f00e6410'}, - { 'label' : 'ceri', 'value' : '0x68f47e153e1aa7d6529e078feff86eada87ddee3' }, - { 'label' : 'Dani', 'value' : '0x89c010bc7085eb150b66582f13681f9e36904bea' }, - { 'label' : 'dmitryn', 'value' : '0x6b0d7ba67aa3d84122749dc7906b8e7f25ed1af8' }, - { 'label' : 'gravityblast', 'value' : '0xb5a2c17c7fd72070fcf078bb8458f2f595441066' }, - { 'label' : 'guylouis.stateofus.eth', 'value' : '0x6913f3bdbb7c303977d6244c0e0071b4ebc6f359' }, - { 'label' : 'Hester', 'value' : '0x8c4f71b3cf6a76de2cc239a6fa84e1a80e589598' }, - { 'label' : 'Hutch', 'value' : '0x34a4b73100d11815ee4bb0ebcc86ba5824b12134' }, - { 'label' : 'igor.stateofus.eth', 'value' : '0xff91043525391cd3a316450bc80ef874fb0ef446' }, - { 'label' : 'jakubgs.eth', 'value' : '0x9b64770c9485A5188D238C305E53627A67C05D7D'}, - { 'label' : 'Jinho', 'value' : '0x7407bF49004ee99d9B2caA2fb90B476bfF2DbCaf' }, - { 'label' : 'Jonathan Barker', 'value' : '0xf23d05F375A8367b150f7Ad1A37DFd9E3c35eE56' }, - { 'label' : 'Jonathan Rainville', 'value' : '0x9ce0056c5fc6bb9459a4dcfa35eaad8c1fee5ce9' }, - { 'label' : 'Jonny Z', 'value' : '0xa40b07ac80d1f89b233b74e78d254c90906c33ee' }, - { 'label' : 'Julien', 'value' : '0x6c618ddbf53aa9540c279e3670d4d26fb367fd4e' }, - { 'label' : 'Maciej', 'value' : '0x227612e69b1d06250e7035c1c12840561ebf3c56' }, - { 'label' : 'michele', 'value' : '0x658a1d2c105b35d9aaad38480dbbfe47b9054962' }, - { 'label' : 'Nabil', 'value' : '0x528c9e62bb0e7083f4b42802297b38ba237776a0' }, - { 'label' : 'Oskar', 'value' : '0x3fd6e2dfa535ce8b1e7eb7116a009eba3890b6bd' }, - { 'label' : 'PascalPrecht', 'value' : '0x6f490165DdD8d604b52dB9D9BF9b63aE997DC11C' }, - { 'label' : 'pedro.stateofus.eth', 'value' : '0x78EA50b13de394671474314aA261556717bF9185' }, - { 'label' : 'Rachel', 'value' : '0x4b9ba5B0dEE90f5B84Bcbfbf921cF02e1C8da113' }, - { 'label' : 'Rajanie', 'value' : '0x8af0d6fabc4a90ea0b95f80ab62beb816ed32a69' }, - { 'label' : 'Ricardo Schmidt <3esmit>', 'value' : '0x3D597789ea16054a084ac84ce87F50df9198F415' }, - { 'label' : 'Sergey', 'value' : '0xb9f914fe1c6edae2351fb42276868470083a3cd2' }, - { 'label' : 'shemnon', 'value' : '0x82ad1b2419fd71dfe2d5db9b3c832c60ec96c53b' }, - { 'label' : 'sonja.stateofus.eth', 'value' : '0xCF03738e9605C0B38cEAa7349bF6926463f01A25' }, - { 'label' : 'Swader', 'value' : '0x9702797d92e2a06070b446e49a594a943686e28f' }, - { 'label' : 'yenda', 'value' : '0xe829f7947175fe6a338344e70aa770a8c134372c' }, - { 'label' : 'petty', 'value' : '0x2942577508e060ea092c0CD7802ae42c1CEA2BAe' }, - { 'label' : 'chu', 'value' : '0xd21DB0e43048AcB94f428eD61dC244c82f1ff2a8' }, - { 'label' : 'Yessin', 'value' : '0xbaba92b7822a56c05554ab5d1bc1d0b7e212499d' }, - { 'label' : 'michaelb', 'value' : '0xdba0bade45727776bbb0d93176ee1ddba830f319' }, - { 'label' : 'cryptowanderer', 'value' : '0x406abd306b633b6460666b4092784a3330370c7b' }, - { 'label' : 'adam.stateofus.eth', 'value' : '0x074032269ca1775896c92304d45f80b5a67a5bcb' }, - { 'label' : 'André Medeiros', 'value' : '0xf062E478870B17B55d1dC64888914B82aD9808B4' }, - { 'label' : 'rramos', 'value' : '0xc379330ae48716b81d7411813c3250cd89271788' }, - { 'label' : 'emizzle', 'value' : '0x91Ef8ef20Adf13E42757a3Ed6Ff2b1249bE15544' }, - { 'label' : 'jason.stateofus.eth', 'value' : '0x4636fb2F6D1DC335EA655795064c2092c89148aB' } + { label: 'Andreas S.', value: '0x4923121411e884a4af66ec025712eba600a782d3' }, + { label: 'andrey.dev', value: '0xA4EcA293cb578a68b190e3e07c2B170dc753fe44' }, + { label: 'Anna', value: '0x2e1ce0f514387a188f4aeff4ceb6c2c0dea66ca7' }, + { label: 'barry', value: '0xa46b0546481a04b7de049a8a20f8a9b2b2c5cc05' }, + { label: 'BrianXV', value: '0x03b832b3fa819d7a4b6c819e4df1e60a173e739a' }, + { label: 'cammellos', value: '0xd0ec8a940fe9712c1521c2190f41604ecaa7ec9e' }, + { label: 'Carl', value: '0x9fb937ab76b68dae29e3de20cf797d44f00e6410' }, + { label: 'ceri', value: '0x68f47e153e1aa7d6529e078feff86eada87ddee3' }, + { label: 'Dani', value: '0x89c010bc7085eb150b66582f13681f9e36904bea' }, + { label: 'dmitryn', value: '0x6b0d7ba67aa3d84122749dc7906b8e7f25ed1af8' }, + { label: 'gravityblast', value: '0xb5a2c17c7fd72070fcf078bb8458f2f595441066' }, + { label: 'guylouis.stateofus.eth', value: '0x6913f3bdbb7c303977d6244c0e0071b4ebc6f359' }, + { label: 'Hester', value: '0x8c4f71b3cf6a76de2cc239a6fa84e1a80e589598' }, + { label: 'Hutch', value: '0x34a4b73100d11815ee4bb0ebcc86ba5824b12134' }, + { label: 'igor.stateofus.eth', value: '0xff91043525391cd3a316450bc80ef874fb0ef446' }, + { label: 'jakubgs.eth', value: '0x9b64770c9485A5188D238C305E53627A67C05D7D' }, + { label: 'Jinho', value: '0x7407bF49004ee99d9B2caA2fb90B476bfF2DbCaf' }, + { label: 'Jonathan Barker', value: '0xf23d05F375A8367b150f7Ad1A37DFd9E3c35eE56' }, + { label: 'Jonathan Rainville', value: '0x9ce0056c5fc6bb9459a4dcfa35eaad8c1fee5ce9' }, + { label: 'Jonny Z', value: '0xa40b07ac80d1f89b233b74e78d254c90906c33ee' }, + { label: 'Julien', value: '0x6c618ddbf53aa9540c279e3670d4d26fb367fd4e' }, + { label: 'Maciej', value: '0x227612e69b1d06250e7035c1c12840561ebf3c56' }, + { label: 'michele', value: '0x658a1d2c105b35d9aaad38480dbbfe47b9054962' }, + { label: 'Nabil', value: '0x528c9e62bb0e7083f4b42802297b38ba237776a0' }, + { label: 'Oskar', value: '0x3fd6e2dfa535ce8b1e7eb7116a009eba3890b6bd' }, + { label: 'PascalPrecht', value: '0x6f490165DdD8d604b52dB9D9BF9b63aE997DC11C' }, + { label: 'pedro.stateofus.eth', value: '0x78EA50b13de394671474314aA261556717bF9185' }, + { label: 'Rachel', value: '0x4b9ba5B0dEE90f5B84Bcbfbf921cF02e1C8da113' }, + { label: 'Rajanie', value: '0x8af0d6fabc4a90ea0b95f80ab62beb816ed32a69' }, + { label: 'Ricardo Schmidt <3esmit>', value: '0x3D597789ea16054a084ac84ce87F50df9198F415' }, + { label: 'Sergey', value: '0xb9f914fe1c6edae2351fb42276868470083a3cd2' }, + { label: 'shemnon', value: '0x82ad1b2419fd71dfe2d5db9b3c832c60ec96c53b' }, + { label: 'sonja.stateofus.eth', value: '0xCF03738e9605C0B38cEAa7349bF6926463f01A25' }, + { label: 'Swader', value: '0x9702797d92e2a06070b446e49a594a943686e28f' }, + { label: 'yenda', value: '0xe829f7947175fe6a338344e70aa770a8c134372c' }, + { label: 'petty', value: '0x2942577508e060ea092c0CD7802ae42c1CEA2BAe' }, + { label: 'chu', value: '0xd21DB0e43048AcB94f428eD61dC244c82f1ff2a8' }, + { label: 'Yessin', value: '0xbaba92b7822a56c05554ab5d1bc1d0b7e212499d' }, + { label: 'michaelb', value: '0xdba0bade45727776bbb0d93176ee1ddba830f319' }, + { label: 'cryptowanderer', value: '0x406abd306b633b6460666b4092784a3330370c7b' }, + { label: 'adam.stateofus.eth', value: '0x074032269ca1775896c92304d45f80b5a67a5bcb' }, + { label: 'André Medeiros', value: '0xf062E478870B17B55d1dC64888914B82aD9808B4' }, + { label: 'rramos', value: '0xc379330ae48716b81d7411813c3250cd89271788' }, + { label: 'emizzle', value: '0x91Ef8ef20Adf13E42757a3Ed6Ff2b1249bE15544' }, + { label: 'jason.stateofus.eth', value: '0x4636fb2F6D1DC335EA655795064c2092c89148aB' } ]; module.exports = contributors; diff --git a/app/js/index.js b/app/js/index.js index aad5514..d1cc81b 100644 --- a/app/js/index.js +++ b/app/js/index.js @@ -1,13 +1,10 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; - -import App from './App'; - -import '../css/fonts/Inter/inter.css'; -import '../css/bootstrap-overrides.scss'; -import '../css/index.scss'; - -ReactDOM.render( - , - document.getElementById('app') -); +import React from 'react'; +import ReactDOM from 'react-dom'; + +import App from './App'; + +import '../css/fonts/Inter/inter.css'; +import '../css/bootstrap-overrides.scss'; +import '../css/index.scss'; + +ReactDOM.render(, document.querySelector('#app')); diff --git a/app/js/services/Meritocracy.js b/app/js/services/Meritocracy.js index 23ae98a..36f3cd4 100644 --- a/app/js/services/Meritocracy.js +++ b/app/js/services/Meritocracy.js @@ -2,7 +2,6 @@ import Meritocracy from 'Embark/contracts/Meritocracy'; import EmbarkJS from 'Embark/EmbarkJS'; - let contributorList; export function addContributor(name, address) { @@ -10,19 +9,19 @@ export function addContributor(name, address) { const mainAccount = web3.eth.defaultAccount; try { const list = await getContributorList(); - list.push({label: name, value: address}); + list.push({ label: name, value: address }); const newHash = await saveContributorList(list); const addContributor = Meritocracy.methods.addContributor(address, web3.utils.toHex(newHash)); - let gas = await addContributor.estimateGas({from: mainAccount}); - const receipt = await addContributor.send({from: mainAccount, gas: gas + 1000}); + let gas = await addContributor.estimateGas({ from: mainAccount }); + const receipt = await addContributor.send({ from: mainAccount, gas: gas + 1000 }); resolve(receipt); - } catch (e) { + } catch (error) { const message = 'Error adding contributor'; console.error(message); - console.error(e); + console.error(error); reject(message); } }); @@ -32,7 +31,7 @@ export function removeContributor(address) { return new Promise(async (resolve, reject) => { const mainAccount = web3.eth.defaultAccount; try { - const registry = await Meritocracy.methods.getRegistry().call({from: mainAccount}); + const registry = await Meritocracy.methods.getRegistry().call({ from: mainAccount }); let index = registry.indexOf(address); const list = await getContributorList(); @@ -42,14 +41,14 @@ export function removeContributor(address) { const newHash = await saveContributorList(list); const removeContributor = Meritocracy.methods.removeContributor(index, web3.utils.toHex(newHash)); - let gas = await removeContributor.estimateGas({from: mainAccount}); - const receipt = await removeContributor.send({from: mainAccount, gas: gas + 1000}); + let gas = await removeContributor.estimateGas({ from: mainAccount }); + const receipt = await removeContributor.send({ from: mainAccount, gas: gas + 1000 }); resolve(receipt); - } catch (e) { + } catch (error) { const message = 'Error removing contributor'; console.error(message); - console.error(e); + console.error(error); reject(message); } }); @@ -66,10 +65,10 @@ export function getContributorList(hash) { const content = await EmbarkJS.Storage.get(hash); contributorList = JSON.parse(content); resolve(contributorList); - } catch (e) { + } catch (error) { const message = 'Error getting contributor file on IPFS'; console.error(message); - console.error(e); + console.error(error); reject(message); } }); @@ -82,23 +81,27 @@ export async function getFormattedContributorList(hash) { let list = await getContributorList(hash); list = list.map(prepareOptions); - const registry = await Meritocracy.methods.getRegistry().call({from: mainAccount}); - list = list.filter(contributorData => registry.includes(contributorData.value) && contributorData.value !== mainAccount); + const registry = await Meritocracy.methods.getRegistry().call({ from: mainAccount }); + list = list.filter( + contributorData => registry.includes(contributorData.value) && contributorData.value !== mainAccount + ); resolve(list); - } catch (e) { + } catch (error) { const message = 'Error getting formatted contributor file on IPFS'; console.error(message); - console.error(e); + console.error(error); reject(message); } }); } const prepareOptions = option => { - if (option.value.match(/^0x[0-9A-Za-z]{40}$/)) { // Address + if (option.value.match(/^0x[0-9A-Za-z]{40}$/)) { + // Address option.value = web3.utils.toChecksumAddress(option.value); - } else { // ENS Name + } else { + // ENS Name // TODO: resolve ENS names // EmbarkJS.Names.resolve("ethereum.eth").then(address => { // console.log("the address for ethereum.eth is: " + address); @@ -107,12 +110,12 @@ const prepareOptions = option => { return option; }; -export async function getCurrentContributorData(){ +export async function getCurrentContributorData() { const mainAccount = web3.eth.defaultAccount; const currentContributor = await getContributor(mainAccount); let praises = []; - for(let i = 0; i < currentContributor.praiseNum; i++){ + for (let i = 0; i < currentContributor.praiseNum; i++) { praises.push(Meritocracy.methods.getStatus(mainAccount, i).call()); } @@ -121,13 +124,13 @@ export async function getCurrentContributorData(){ } const contribData = contributorList.find(x => x.value === mainAccount); - if(contribData) currentContributor.name = contribData.label; + if (contribData) currentContributor.name = contribData.label; currentContributor.praises = await Promise.all(praises); - currentContributor.allocation = web3.utils.fromWei(currentContributor.allocation, "ether"); - currentContributor.totalForfeited = web3.utils.fromWei(currentContributor.totalForfeited, "ether"); - currentContributor.totalReceived = web3.utils.fromWei(currentContributor.totalReceived, "ether"); - currentContributor.received = web3.utils.fromWei(currentContributor.received, "ether"); + currentContributor.allocation = web3.utils.fromWei(currentContributor.allocation, 'ether'); + currentContributor.totalForfeited = web3.utils.fromWei(currentContributor.totalForfeited, 'ether'); + currentContributor.totalReceived = web3.utils.fromWei(currentContributor.totalReceived, 'ether'); + currentContributor.received = web3.utils.fromWei(currentContributor.received, 'ether'); return currentContributor; } @@ -144,10 +147,10 @@ export function saveContributorList(list) { contributorList = list; const newHash = await EmbarkJS.Storage.saveText(JSON.stringify(list)); resolve(newHash); - } catch (e) { + } catch (error) { const message = 'Error saving contributor file on IPFS'; console.error(message); - console.error(e); + console.error(error); reject(message); } }); @@ -158,12 +161,11 @@ export function isAdmin(address) { try { const result = await Meritocracy.methods.admins(address).call(); resolve(result); - } catch (e) { + } catch (error) { const message = 'Could not get status of user'; console.error(message); - console.error(e); + console.error(error); reject(message); } }); } - diff --git a/app/js/validators.js b/app/js/validators.js index 53d4c50..81d45d4 100644 --- a/app/js/validators.js +++ b/app/js/validators.js @@ -1,60 +1,96 @@ /*global Web3*/ import React from 'react'; -import {Form} from 'react-bootstrap'; +import { Form } from 'react-bootstrap'; -export const required = (value) => { - if (!value.toString().trim().length) { - return This field is required; +export const required = value => { + if (value.toString().trim().length === 0) { + return ( + + This field is required + + ); } }; -export const isInteger = (value) => { +export const isInteger = value => { value = parseFloat(value); if (!Number.isInteger(value)) { - return This field needs to be an integer; + return ( + + This field needs to be an integer + + ); } }; -export const isNumber = (value) => { +export const isNumber = value => { if (Number.isNaN(value)) { - return This field needs to be an number; + return ( + + This field needs to be an number + + ); } }; export const lowerThan = (max, value) => { if (value >= max) { - return This field needs to be lower than {max}; + return ( + + This field needs to be lower than {max} + + ); } }; export const lowerEqThan = (max, value) => { if (value > max) { - return This field needs to be lower or equal than {max}; + return ( + + This field needs to be lower or equal than {max} + + ); } }; export const higherThan = (min, value) => { if (value <= min) { - return This field needs to be higher than {min}; + return ( + + This field needs to be higher than {min} + + ); } }; export const higherEqThan = (min, value) => { if (value < min) { - return This field needs to be higher or equal than {min}; + return ( + + This field needs to be higher or equal than {min} + + ); } }; -export const isAddress = (value) => { +export const isAddress = value => { if (!Web3.utils.isAddress(value)) { - return This field needs to be a valid Ethereum address; + return ( + + This field needs to be a valid Ethereum address + + ); } }; -export const isJSON = (value) => { +export const isJSON = value => { try { JSON.parse(value); - } catch (e) { - return This field needs to be a valid JSON string; + } catch (error) { + return ( + + This field needs to be a valid JSON string + + ); } };