feat: reward screen

This commit is contained in:
Richard Ramos 2019-04-26 18:17:13 -04:00
parent 4cdf41835d
commit 6c296c626a
20 changed files with 381 additions and 80 deletions

View File

@ -0,0 +1,3 @@
<svg width="9" height="16" viewBox="0 0 9 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.00687048 7.89841C0.027045 7.67387 0.122799 7.45524 0.292809 7.28566L7.30422 0.292065C7.6926 -0.0953277 8.3188 -0.0987967 8.70893 0.290334C9.09635 0.676771 9.0907 1.30894 8.70719 1.69148L2.38119 8.00141L8.70719 14.3114C9.09557 14.6987 9.09905 15.3234 8.70893 15.7125C8.32151 16.0989 7.68773 16.0933 7.30422 15.7108L0.292809 8.71716C0.0681878 8.49311 -0.0276859 8.18971 0.00687048 7.89841Z" fill="#4360DF"/>
</svg>

After

Width:  |  Height:  |  Size: 559 B

View File

@ -0,0 +1,3 @@
<svg width="9" height="16" viewBox="0 0 9 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.70719 8.71434L1.69578 15.7079C1.3074 16.0953 0.681196 16.0988 0.291074 15.7097C-0.0963472 15.3232 -0.0907033 14.6911 0.292809 14.3085L6.61881 7.99859L0.292809 1.68865C-0.0955705 1.30126 -0.0990483 0.676638 0.291074 0.287507C0.678495 -0.0989301 1.31227 -0.0933004 1.69578 0.289237L8.70719 7.28284C8.93181 7.50689 9.02769 7.81029 8.99313 8.10159C8.97295 8.32613 8.8772 8.54476 8.70719 8.71434Z" fill="#4360DF"/>
</svg>

After

Width:  |  Height:  |  Size: 563 B

BIN
app/images/complete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,3 @@
<svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.49501 0.505054C1.22164 0.231688 0.778427 0.231688 0.505059 0.505054C0.231692 0.778421 0.231692 1.22164 0.505059 1.495L4.50506 5.495C4.77843 5.76837 5.22164 5.76837 5.49501 5.495L9.49501 1.495C9.76838 1.22164 9.76838 0.778421 9.49501 0.505054C9.22164 0.231688 8.77843 0.231688 8.50506 0.505054L4.99812 4.01199L1.49501 0.505054Z" fill="#4360DF"/>
</svg>

After

Width:  |  Height:  |  Size: 498 B

BIN
app/images/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

5
app/images/info.svg Normal file
View File

@ -0,0 +1,5 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 16.3612C13 16.9134 12.5522 17.3612 12 17.3612C11.4478 17.3612 11 16.9134 11 16.3612V11.4403C11 11.4403 11 10.4099 12 10.4099C12.8345 10.4099 13 11.2942 13 11.4403V16.3612Z" fill="black"/>
<path d="M13 8.4099C13 8.96214 12.5522 9.4099 12 9.4099C11.4478 9.4099 11 8.96214 11 8.4099C11 7.85765 11.4478 7.4099 12 7.4099C12.5522 7.4099 13 7.85765 13 8.4099Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 12.4099C2 17.9327 6.47705 22.4099 12 22.4099C17.5229 22.4099 22 17.9327 22 12.4099C22 6.88707 17.5229 2.4099 12 2.4099C6.47705 2.4099 2 6.88707 2 12.4099ZM20 12.4099C20 16.8282 16.4182 20.4099 12 20.4099C7.58179 20.4099 4 16.8282 4 12.4099C4 7.99156 7.58179 4.4099 12 4.4099C16.4182 4.4099 20 7.99156 20 12.4099Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 866 B

BIN
app/images/spinner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.6868 9.90763C10.4882 9.97671 9.7371 9.69751 8.53839 9.76674C8.24109 9.78344 7.94584 9.82653 7.65613 9.8955C7.83306 7.67874 9.40193 5.73957 11.5353 5.61631C12.8445 5.54076 14.1531 6.34908 14.224 7.66127C14.2939 8.95099 13.3105 9.81381 11.6869 9.90747L11.6868 9.90763ZM8.46823 14.4419C7.21403 14.5128 5.96065 13.7561 5.89258 12.5281C5.82564 11.3211 6.7679 10.5136 8.32323 10.4259C9.47129 10.3612 10.191 10.6226 11.339 10.5577C11.6237 10.5421 11.9065 10.5018 12.1842 10.4372C12.015 12.5118 10.5121 14.3268 8.46823 14.4419ZM10 0.000161758C4.4771 -2.09028e-09 0 4.47703 0 10C0 15.523 4.4771 20 10 20C15.5229 20 20 15.5228 20 10C20 4.47719 15.5229 0 10 0" fill="#4360DF"/>
</svg>

After

Width:  |  Height:  |  Size: 823 B

View File

@ -0,0 +1,44 @@
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) => {
e.preventDefault();
this.setState(prevState => ({ showHelp: !prevState.showHelp }));
}
render() {
const {value} = this.props;
const {showHelp} = this.state;
return (
<div className="text-center p-4 allocation">
<p className="text-muted mb-2">Reward Status contributors for all the times they impressed you.</p>
<p className="mb-2">
<a href="#" onClick={this.handleClickHelp}>Learn more <img src={downArrow} alt="" className="ml-2" /></a>
</p>
{showHelp && (
<div className="text-muted text-left border rounded p-2 mb-2 learn-more">
<img src={info} alt="" />
<p className="m-0 p-0">
Status Meritocracy is an SNT Reward System that allows a Contributor in the registry to
award allocated SNT, along with praise, to other Contributors.<br />
<a href="https://github.com/status-im/meritocracy/blob/master/register.md">Register</a> to
receive a budget and participate.</p>
</div>
)}
<p className="allocation mb-0">{value} <span className="text-muted">SNT</span></p>
<p className="text-muted">Available</p>
</div>
);
}
}
export default Allocation;

View File

@ -0,0 +1,14 @@
import React from 'react';
import CompleteIcon from '../../images/complete.png';
import {Button} from 'react-bootstrap';
const Complete = ({onClick}) => (
<div className="text-center mt-5 pt-5">
<img src={CompleteIcon} alt="" width="160" height="160" className="mt-5" />
<h4 className="text-center pr-5 pl-5 mt-3">Thank you</h4>
<p className="text-muted">Your SNT has been awarded.</p>
<p><Button onClick={onClick} variant="link">Back</Button></p>
</div>
)
export default Complete;

View File

@ -0,0 +1,70 @@
import React, {Fragment} from 'react';
import Select from 'react-select';
import {Form} from 'react-bootstrap';
import Allocation from './Allocation';
import statusLogo from '../../images/status-logo.svg';
import "./contributor-selector.scss";
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}) => (
<Fragment>
<Allocation value={allocation - award * selectedContributors.length} />
<div className="container">
<div className="row mb-2">
<div className="col-10 label">
Enter contributors and award SNT
</div>
<div className="col-2">
<div className="plus-5" title="Add +5" onClick={onClickPlus5}>
<img alt="+5" src={statusLogo} />
<span>+5</span>
</div>
</div>
</div>
<div className="row">
<div className="col-10">
<Select
isMulti
value={selectedContributors}
onChange={onSelectContributor}
options={contributorList.sort(sortByAlpha)}
placeholder="Choose Contributor(s)..."
className="mb-2 contributorSelector"
theme={(theme) => ({
...theme,
borderRadius: '4px',
border: 'none',
padding: '10px',
colors: {
...theme.colors,
neutral0: '#EEF2F5',
neutral10: '#EEF2F5',
},
spacing: {
...theme.spacing,
controlHeight: 50,
}
})}
/>
</div>
<div className="col-2 p-0">
<Form.Control
type="number"
step="1"
onChange={onChangeAward}
value={award}
/>
</div>
</div>
</div>
</Fragment>
);
export default ContributorSelection;

View File

@ -0,0 +1,14 @@
import React from 'react';
import ErrorIcon from '../../images/error.png';
import {Button} from 'react-bootstrap';
const Error = ({onClick, title, message}) => (
<div className="text-center mt-5 pt-5">
<img src={ErrorIcon} alt="" width="160" height="160" className="mt-5" />
<h4 className="text-center pr-5 pl-5 mt-3">{title}</h4>
<p className="text-muted">{message}</p>
<p><Button onClick={onClick} variant="link">Back</Button></p>
</div>
)
export default Error;

View File

@ -0,0 +1,9 @@
import React from 'react';
const History = ({value}) => (
<div className="text-center p-4">
</div>
);
export default Allocation;

View File

@ -1,14 +1,14 @@
/*global web3*/
import React, {Fragment} from 'react';
import {Row, Col, Alert, Button, Container, Form, Tabs, Tab} from 'react-bootstrap';
import NumericInput from 'react-numeric-input';
import Select from 'react-select';
import Meritocracy from 'Embark/contracts/Meritocracy';
import arrowLeft from '../../images/arrow-left.svg';
import {getFormattedContributorList, getCurrentContributorData} from '../services/Meritocracy';
import './home.scss';
import Step1 from './Step1';
import Loading from './Loading';
import Complete from './Complete';
import Error from './Error';
/*
TODO:
@ -31,7 +31,9 @@ class Home extends React.Component {
status: []
},
award: 0,
praise: ''
praise: '',
step: 'ERROR',
checkbox: false,
};
constructor(props) {
@ -57,14 +59,31 @@ class Home extends React.Component {
}
handleContributorSelection(_selectedContributors) {
this.setState({ selectedContributors: _selectedContributors });
this.setState({ selectedContributors: _selectedContributors }, () => {
this._setAward(this.state.award);
});
}
handleAwardChange(_amount) {
const { currentContributor: {allocation}, selectedContributors} = this.state;
handleAwardChange(e) {
if(e.target.value.trim() === "") {
this.setState({award: ""});
return;
}
this._setAward(e.target.value);
}
const maxAllocation = allocation / selectedContributors.length;
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});
}
@ -72,6 +91,10 @@ class Home extends React.Component {
this.setState({ praise: e.target.value });
}
handleCheckbox = (e) => {
this.setState(prevState => ({ checkbox: !prevState.checkbox }));
}
resetUIFields(){
this.setState({
praise: '',
@ -84,11 +107,7 @@ class Home extends React.Component {
async awardTokens(e) {
const {award, selectedContributors, praise} = this.state;
// TODO some sanity checks
if(award <= 0) {
this.setState({errorMsg: 'amount must be more than 0'});
return;
}
this.moveStep('BUSY');
let addresses = selectedContributors.map(a => a.value);
@ -108,18 +127,17 @@ class Home extends React.Component {
}
try {
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});
this.resetUIFields();
const currentContributor = await getCurrentContributorData();
this.setState({currentContributor});
this.moveStep('COMPLETE')();
} catch(e) {
this.setState({errorMsg: 'tx failed? got enough tokens to award?'});
console.error(e);
} finally {
this.setState({busy: false});
}
}
@ -147,6 +165,7 @@ class Home extends React.Component {
const currentContributor = await getCurrentContributorData();
this.setState({currentContributor});
} catch(e) {
this.setState({errorMsg: 'tx failed? Did you allocate all your tokens first?'});
console.error(e);
@ -155,79 +174,90 @@ class Home extends React.Component {
}
}
moveStep = nexStep => () => {
this.setState({step: nexStep});
}
render() {
const { selectedContributors, contributorList, award, currentContributor, praise, busy, errorMsg } = this.state;
const { selectedContributors, contributorList, award, currentContributor, praise, busy, errorMsg, step, checkbox } = this.state;
const maxAllocation = selectedContributors.length ? currentContributor.allocation / selectedContributors.length : 0;
if(errorMsg) return <Error title="Error" value={errorMsg} />;
const orderedContributors = contributorList.sort((a,b) => {
if (a.label < b.label) return -1;
if (a.label > b.label) return 1;
return 0;
});
return (
<Fragment>
<Tabs defaultActiveKey="reward" className="home-tabs mb-3">
<Tab eventKey="reward" title="Reward" className="reward-panel">
{step === 'HOME' && (
<Step1
allocation={currentContributor.allocation}
onChangeAward={this.handleAwardChange}
onSelectContributor={this.handleContributorSelection}
onClickPlus5={this.handlePlus5}
contributorList={contributorList}
selectedContributors={selectedContributors}
award={award}
isChecked={checkbox}
onClickCheckbox={this.handleCheckbox}
onClickNext={this.moveStep('PRAISE')}
/>
)}
return (<Fragment>
{errorMsg && <Alert variant="danger">{errorMsg}</Alert>}
{busy && <p>Working...</p>}
{step === 'PRAISE' && (
<div>
<p className="text-center mt-5 text-muted">Research shows that a note of praise and learning how much our work helped others, increases motivation.</p>
<p className="mb-0">
<span className="font-weight-bold">{ selectedContributors.map(x => x.label).join(', ') }</span>
<span className="float-right text-muted">SNT <b>{award * selectedContributors.length}</b></span>
</p>
<Form>
<Form.Label className="small-text">Add note</Form.Label>
<Form.Control disabled={busy} as="textarea" rows="5" onChange={this.handlePraiseChange}
value={praise} className="p-2"/>
</Form>
<div className="fixed-bottom bg-white">
<Button onClick={this.moveStep('HOME')} variant="link"><img src={arrowLeft} alt="" className="mr-2" /> Back</Button>
<Button disabled={busy} variant="primary" className="float-right mr-2 mb-2" onClick={this.awardTokens}>Award</Button>
</div>
</div>
)}
<Tabs defaultActiveKey="reward" className="home-tabs mb-3">
<Tab eventKey="reward" title="Reward" className="reward-panel">
<div className="text-center p-4">
<p className="text-muted">Reward Status contributors for all the times they impressed you.</p>
<p className="allocation mb-0">{currentContributor.allocation} <span className="text-muted">SNT</span></p>
<p className="text-muted">Available</p>
</div>
{ step === 'BUSY' && <Loading /> }
{ step === 'COMPLETE' && <Complete onClick={this.moveStep('HOME')} /> }
{ step === 'ERROR' && <Error onClick={this.moveStep('PRAISE')} title="Error" message="Your transaction could not be processed" /> }
</Tab>
<Select
isMulti
value={selectedContributors}
onChange={this.handleContributorSelection}
options={orderedContributors}
placeholder="Choose Contributor(s)..."
isDisabled={busy}
className="mb-2"
/>
<Tab eventKey="withdraw" title="Withdraw">
<p>Your Total Received Kudos: {currentContributor.totalReceived || 0} SNT</p>
<p>Your Total Forfeited Kudos: {currentContributor.totalForfeited || 0} SNT</p>
{selectedContributors.length === 0 && <Alert variant="secondary">
Please select one or more contributors
</Alert>}
<h4>Your Kudos History</h4>
<p>Your Received Kudos: <b>{currentContributor.received} SNT</b></p>
<NumericInput mobile step={5} min={0} max={maxAllocation} onChange={this.handleAwardChange} value={award}
disabled={busy} className="form-control mb-2"/>
<p className="text-center">
<Button variant="outline-primary" onClick={this.withdrawTokens} disabled={busy}>
Withdraw
</Button>
</p>
<Form>
<Form.Control disabled={busy} placeholder="Enter your praise..." onChange={this.handlePraiseChange}
value={praise}/>
</Form>
<p className="text-center"> Total Awarding: {award * selectedContributors.length} SNT </p>
<p className="text-center"><Button disabled={busy} variant="outline-primary" onClick={this.awardTokens}>Award</Button></p>
</Tab>
<Container>
<Row>
{currentContributor.praises && currentContributor.praises.map((item, i) => {
const name = options.find(x => x.value === item.author);
return <Col key={i}>{(name && name.label) || item.author} has sent
you {web3.utils.fromWei(item.amount, "ether")} SNT {item.praise && "\"" + item.praise + "\""}</Col>;
})}
</Row>
</Container>
<Tab eventKey="withdraw" title="Withdraw">
<p>Your Total Received Kudos: {currentContributor.totalReceived || 0} SNT</p>
<p>Your Total Forfeited Kudos: {currentContributor.totalForfeited || 0} SNT</p>
<h4>Your Kudos History</h4>
<p>Your Received Kudos: <b>{currentContributor.received} SNT</b></p>
<p className="text-center">
<Button variant="outline-primary" onClick={this.withdrawTokens} disabled={busy}>
Withdraw
</Button>
</p>
<Container>
<Row>
{currentContributor.praises && currentContributor.praises.map((item, i) => {
const name = options.find(x => x.value === item.author);
return <Col key={i}>{(name && name.label) || item.author} has sent
you {web3.utils.fromWei(item.amount, "ether")} SNT {item.praise && "\"" + item.praise + "\""}</Col>;
})}
</Row>
</Container>
</Tab>
</Tabs>
</Fragment>);
</Tab>
</Tabs>
</Fragment>
);
}
}

View File

@ -0,0 +1,12 @@
import React from 'react';
import './loading.scss';
import spinner from '../../images/spinner.png';
const Loading = () => (
<div className="busy text-center mt-5 pt-5">
<img src={spinner} alt="" className="mt-5" />
<h5 className="text-muted text-center pr-5 pl-5">Waiting for the confirmation from miners</h5>
</div>
)
export default Loading;

View File

@ -0,0 +1,28 @@
import React, {Fragment} from 'react';
import arrowRight from '../../images/arrow-right.svg';
import ContributorSelection from './ContributorSelection';
import {Button, Form} from 'react-bootstrap';
const Step1 = ({allocation, onChangeAward, onSelectContributor, onClickPlus5, contributorList, selectedContributors, award, isChecked, onClickCheckbox, onClickNext}) => (
<Fragment>
<ContributorSelection
allocation={allocation}
onChangeAward={onChangeAward}
onSelectContributor={onSelectContributor}
onClickPlus5={onClickPlus5}
contributorList={contributorList}
selectedContributors={selectedContributors}
award={award}
/>
<Form.Group>
<Form.Check type="checkbox" className="TOC pl-5 pr-2 mt-4" checked={isChecked} onChange={onClickCheckbox} label="I understand that I only receive rewards if I spend my entire reward budget." />
</Form.Group>
<div className="fixed-bottom bg-white">
<Button disabled={selectedContributors.length === 0 || !(award > 0) || !isChecked} onClick={onClickNext} variant="link" className="float-right p-3">Next <img src={arrowRight} alt="" className="ml-2" /></Button>
</div>
</Fragment>
);
export default Step1;

View File

@ -0,0 +1,15 @@
.learn-more {
display: flex;
img {
width: 25px;
margin: 0 10px 0 0;
object-fit: contain;
align-self: flex-start;
}
p {
flex: 1 1 auto;
}
}

View File

@ -0,0 +1,31 @@
@import "../../css/variable-overrides";
.contributorSelector > div:first-child,
.contributorSelector input {
border-color: #EEF2F5 !important;
}
.contributorSelector > div:nth-child(3){
background: #FFFFFF !important;
z-index: 99999;
}
.label {
font-size: 15px;
}
.plus-5 {
width: 45px;
margin: auto;
position: relative;
cursor: pointer;
span {
position: absolute;
top: -10px;
right: 10px;
font-size: 12px;
color: $dark;
}
}

View File

@ -23,11 +23,17 @@
}
.reward-panel {
padding-bottom: 70px;
.allocation {
font-size: 32px;
}
.react-numeric-input {
.TOC {
font-size: 15px;
}
.small-text {
font-size: 13px;
}
}

View File

@ -0,0 +1,11 @@
.busy {
img {
-webkit-animation:spin 7s linear infinite;
-moz-animation:spin 7s linear infinite;
animation:spin 7s linear infinite;
}
@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }
}