feat: admin panel (#29)
* feat: added kudos wall * feat: add admin forms * feat: add forfeit and allocate functionality * fix: moved leaderboard out of admin panel
This commit is contained in:
parent
7233fb934b
commit
9d983c2b9d
|
@ -91,3 +91,7 @@ $input-icon-height: 20px;
|
||||||
.btn {
|
.btn {
|
||||||
padding: 11px 36px;
|
padding: 11px 36px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-small {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import EmbarkJS from 'Embark/EmbarkJS';
|
||||||
import { isAdmin } from './services/Meritocracy';
|
import { isAdmin } from './services/Meritocracy';
|
||||||
import Header from './components/Header';
|
import Header from './components/Header';
|
||||||
import Home from './components/Home';
|
import Home from './components/Home';
|
||||||
|
import Leaderboard from './components/Leaderboard';
|
||||||
import Admin from './components/Admin';
|
import Admin from './components/Admin';
|
||||||
|
|
||||||
const MAINNET = 1;
|
const MAINNET = 1;
|
||||||
|
@ -65,8 +66,8 @@ class App extends React.Component {
|
||||||
<Header isUserAdmin={isUserAdmin} />
|
<Header isUserAdmin={isUserAdmin} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path="/" component={Home} />
|
<Route exact path="/" component={Home} />
|
||||||
|
<Route path="/leaderboard" component={Leaderboard} />
|
||||||
{isUserAdmin && <Route exact path="/admin" component={Admin} />}
|
{isUserAdmin && <Route exact path="/admin" component={Admin} />}
|
||||||
|
|
||||||
<Redirect to="/404" />
|
<Redirect to="/404" />
|
||||||
</Switch>
|
</Switch>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
|
/* global web3 */
|
||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { Button, Form, Alert, ListGroup, OverlayTrigger, Tooltip, Modal, Tabs, Tab, Table } from 'react-bootstrap';
|
import { Button, Form, Alert, ListGroup, OverlayTrigger, Tooltip, Modal, Tabs, Tab } from 'react-bootstrap';
|
||||||
import ValidatedForm from 'react-validation/build/form';
|
import ValidatedForm from 'react-validation/build/form';
|
||||||
import Input from 'react-validation/build/input';
|
import Input from 'react-validation/build/input';
|
||||||
import { required, isAddress } from '../validators';
|
import { required, isAddress, isNumber, higherThan } from '../validators';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
import { faTrash } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { addContributor, getFormattedContributorList, removeContributor, getContributorData } from '../services/Meritocracy';
|
import {
|
||||||
import { sortByAlpha, sortByAttribute, sortNullableArray } from '../utils';
|
addContributor,
|
||||||
|
getFormattedContributorList,
|
||||||
|
removeContributor,
|
||||||
|
forfeitAllocation,
|
||||||
|
lastForfeited,
|
||||||
|
allocate
|
||||||
|
} from '../services/Meritocracy';
|
||||||
|
import { sortByAlpha } from '../utils';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
import './admin.scss';
|
import './admin.scss';
|
||||||
|
|
||||||
const sort = (orderBy) => {
|
|
||||||
if(orderBy === 'praises') return sortNullableArray('praises');
|
|
||||||
if(orderBy === 'label') return sortByAlpha('label');
|
|
||||||
return sortByAttribute(orderBy);
|
|
||||||
};
|
|
||||||
|
|
||||||
class Admin extends React.Component {
|
class Admin extends React.Component {
|
||||||
state = {
|
state = {
|
||||||
contributorName: '',
|
contributorName: '',
|
||||||
|
@ -27,7 +30,9 @@ class Admin extends React.Component {
|
||||||
showDeleteModal: false,
|
showDeleteModal: false,
|
||||||
focusedContributorIndex: -1,
|
focusedContributorIndex: -1,
|
||||||
sortBy: 'label',
|
sortBy: 'label',
|
||||||
tab: 'admin'
|
tab: 'admin',
|
||||||
|
sntPerContributor: 0,
|
||||||
|
lastForfeited: null
|
||||||
};
|
};
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
|
@ -36,15 +41,7 @@ class Admin extends React.Component {
|
||||||
|
|
||||||
this.setState({ busy: false, contributorList });
|
this.setState({ busy: false, contributorList });
|
||||||
|
|
||||||
// TODO: this can be replaced by event sourcing
|
this.getLastForfeitDate();
|
||||||
contributorList.forEach(contrib => {
|
|
||||||
getContributorData(contrib.value)
|
|
||||||
.then(data => {
|
|
||||||
contrib = Object.assign(contrib, data);
|
|
||||||
this.setState({contributorList});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setState({ errorMsg: error.message || error });
|
this.setState({ errorMsg: error.message || error });
|
||||||
}
|
}
|
||||||
|
@ -54,9 +51,14 @@ class Admin extends React.Component {
|
||||||
this.setState({ [name]: e.target.value });
|
this.setState({ [name]: e.target.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getLastForfeitDate = async () => {
|
||||||
|
const date = await lastForfeited();
|
||||||
|
this.setState({ lastForfeited: date });
|
||||||
|
};
|
||||||
|
|
||||||
addContributor = async e => {
|
addContributor = async e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.setState({ busy: true, successMsg: '' });
|
this.setState({ busy: true, successMsg: '', error: '' });
|
||||||
try {
|
try {
|
||||||
await addContributor(this.state.contributorName, this.state.contributorAddress);
|
await addContributor(this.state.contributorName, this.state.contributorAddress);
|
||||||
|
|
||||||
|
@ -69,6 +71,41 @@ class Admin extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
allocateFunds = async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
/* eslint-disable-next-line no-alert*/
|
||||||
|
if (!confirm('Are you sure?')) return;
|
||||||
|
|
||||||
|
this.setState({ busy: true, successMsg: '', error: '' });
|
||||||
|
|
||||||
|
const { contributorList, sntPerContributor } = this.state;
|
||||||
|
const sntAmount = web3.utils.toWei((contributorList.length * parseInt(sntPerContributor, 10)).toString(), 'ether');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await allocate(sntAmount);
|
||||||
|
this.setState({ busy: false, successMsg: 'Funds allocated!' });
|
||||||
|
} catch (error) {
|
||||||
|
this.setState({ error: error.message || error, busy: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
forfeit = async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
/* eslint-disable-next-line no-alert*/
|
||||||
|
if (!confirm('Are you sure?')) return;
|
||||||
|
|
||||||
|
this.setState({ busy: true, successMsg: '' });
|
||||||
|
try {
|
||||||
|
await forfeitAllocation();
|
||||||
|
await this.getLastForfeitDate();
|
||||||
|
this.setState({ busy: false, successMsg: 'Funds forfeited!' });
|
||||||
|
} catch (error) {
|
||||||
|
this.setState({ error: error.message || error, busy: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
removeContributor = (e, contributorIndex) => {
|
removeContributor = (e, contributorIndex) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.setState({ focusedContributorIndex: contributorIndex, showDeleteModal: true });
|
this.setState({ focusedContributorIndex: contributorIndex, showDeleteModal: true });
|
||||||
|
@ -93,12 +130,9 @@ class Admin extends React.Component {
|
||||||
this.setState({ showDeleteModal: false });
|
this.setState({ showDeleteModal: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
sortBy = (order) => () => {
|
|
||||||
this.setState({sortBy: order});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
lastForfeited,
|
||||||
contributorAddress,
|
contributorAddress,
|
||||||
contributorName,
|
contributorName,
|
||||||
error,
|
error,
|
||||||
|
@ -106,17 +140,19 @@ class Admin extends React.Component {
|
||||||
contributorList,
|
contributorList,
|
||||||
successMsg,
|
successMsg,
|
||||||
focusedContributorIndex,
|
focusedContributorIndex,
|
||||||
sortBy,
|
tab,
|
||||||
tab
|
sntPerContributor
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const currentContributor = focusedContributorIndex > -1 ? contributorList[focusedContributorIndex] : {};
|
const currentContributor = focusedContributorIndex > -1 ? contributorList[focusedContributorIndex] : {};
|
||||||
const sortedContributorList = contributorList.sort(sort(sortBy));
|
const nextForfeit = (lastForfeited ? lastForfeited * 1000 : new Date().getTime()) + 86400 * 6 * 1000;
|
||||||
|
const nextForfeitDate =
|
||||||
|
new Date(nextForfeit).toLocaleDateString() + ' ' + new Date(nextForfeit).toLocaleTimeString();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Tabs className="home-tabs mb-3" activeKey={tab} onSelect={tab => this.setState({ tab })}>
|
<Tabs className="home-tabs mb-3" activeKey={tab} onSelect={tab => this.setState({ tab })}>
|
||||||
<Tab eventKey="admin" title="Admin Panel" className="admin-panel">
|
<Tab eventKey="admin" title="Contributors" className="admin-panel">
|
||||||
<h2>Admin Panel</h2>
|
<h2>Contributors</h2>
|
||||||
{error && <Alert variant="danger">{error}</Alert>}
|
{error && <Alert variant="danger">{error}</Alert>}
|
||||||
{successMsg && <Alert variant="success">{successMsg}</Alert>}
|
{successMsg && <Alert variant="success">{successMsg}</Alert>}
|
||||||
{busy && <Alert variant="primary">Working...</Alert>}
|
{busy && <Alert variant="primary">Working...</Alert>}
|
||||||
|
@ -149,11 +185,13 @@ class Admin extends React.Component {
|
||||||
Add
|
Add
|
||||||
</Button>
|
</Button>
|
||||||
</ValidatedForm>
|
</ValidatedForm>
|
||||||
|
<hr className="mt-5 mb-5" />
|
||||||
<h3>Contributor List</h3>
|
<h3>Contributor List</h3>
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
{contributorList.sort(sortByAlpha('label')).map((contributor, idx) => (
|
{contributorList.sort(sortByAlpha('label')).map((contributor, idx) => (
|
||||||
<ListGroup.Item key={contributor.value} action className="contributor-item">
|
<ListGroup.Item key={contributor.value} action className="contributor-item">
|
||||||
<span className="font-weight-bold">{contributor.label}:</span> {contributor.value}
|
<span className="font-weight-bold">{contributor.label}:</span>{' '}
|
||||||
|
<span className="text-small">{contributor.value}</span>
|
||||||
<div className="contributor-controls float-right">
|
<div className="contributor-controls float-right">
|
||||||
<OverlayTrigger placement="top" overlay={<Tooltip>Delete contributor</Tooltip>}>
|
<OverlayTrigger placement="top" overlay={<Tooltip>Delete contributor</Tooltip>}>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -167,31 +205,44 @@ class Admin extends React.Component {
|
||||||
))}
|
))}
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="leaderboard" title="Leaderboard" className="leaderboard-panel">
|
<Tab eventKey="allocation" title="Allocation" className="allocation-panel">
|
||||||
<Table striped bordered hover responsive size="sm">
|
<h2>Allocation</h2>
|
||||||
<thead>
|
{error && <Alert variant="danger">{error}</Alert>}
|
||||||
<tr>
|
{successMsg && <Alert variant="success">{successMsg}</Alert>}
|
||||||
<th onClick={this.sortBy('label')}>Contributor</th>
|
{busy && <Alert variant="primary">Working...</Alert>}
|
||||||
<th onClick={this.sortBy('allocation')}>Allocation</th>
|
<ValidatedForm>
|
||||||
<th onClick={this.sortBy('totalReceived')}>SNT Received</th>
|
<Form.Group controlId="fundAllocation">
|
||||||
<th onClick={this.sortBy('totalForfeited')}>SNT Forfeited</th>
|
<Form.Label>SNT per contributor</Form.Label>
|
||||||
<th onClick={this.sortBy('praises')}>Praises Received</th>
|
<Form.Text className="text-muted">
|
||||||
</tr>
|
Total: {contributorList.length * parseInt(sntPerContributor, 10) || 0} SNT
|
||||||
</thead>
|
</Form.Text>
|
||||||
<tbody>
|
<Input
|
||||||
{
|
type="text"
|
||||||
sortedContributorList.map((contrib, i) => (
|
placeholder="0"
|
||||||
<tr key={i}>
|
value={sntPerContributor}
|
||||||
<td>{contrib.label}</td>
|
onChange={e => this.onChange('sntPerContributor', e)}
|
||||||
<td>{contrib.allocation}</td>
|
className="form-control"
|
||||||
<td>{contrib.totalReceived}</td>
|
validations={[required, isNumber, higherThan.bind(null, 0)]}
|
||||||
<td>{contrib.totalForfeited}</td>
|
/>
|
||||||
<td>{contrib.praises ? contrib.praises.length : 0}</td>
|
</Form.Group>
|
||||||
</tr>
|
<Button variant="primary" onClick={this.allocateFunds}>
|
||||||
))
|
Allocate Funds
|
||||||
}
|
</Button>
|
||||||
</tbody>
|
</ValidatedForm>
|
||||||
</Table>
|
<hr className="mt-5 mb-5" />
|
||||||
|
<ValidatedForm>
|
||||||
|
<Form.Group>
|
||||||
|
<Button variant="primary" disabled={nextForfeit > new Date().getTime()} onClick={this.forfeit}>
|
||||||
|
Forfeit Allocation
|
||||||
|
</Button>
|
||||||
|
{lastForfeited && (
|
||||||
|
<Form.Text className="text-muted">
|
||||||
|
Forfeited {moment.unix(lastForfeited).fromNow()}.<br />{' '}
|
||||||
|
{nextForfeit > new Date().getTime() && 'Can be forfeited on ' + nextForfeitDate}
|
||||||
|
</Form.Text>
|
||||||
|
)}
|
||||||
|
</Form.Group>
|
||||||
|
</ValidatedForm>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<Modal show={this.state.showDeleteModal} onHide={this.handleClose}>
|
<Modal show={this.state.showDeleteModal} onHide={this.handleClose}>
|
||||||
|
|
|
@ -10,18 +10,16 @@ const Header = ({ isUserAdmin }) => (
|
||||||
<img alt="Logo" src={logo} className="mr-3" />
|
<img alt="Logo" src={logo} className="mr-3" />
|
||||||
Status Meritocracy
|
Status Meritocracy
|
||||||
</Navbar.Brand>
|
</Navbar.Brand>
|
||||||
{isUserAdmin && (
|
<React.Fragment>
|
||||||
<React.Fragment>
|
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
||||||
<Navbar.Toggle aria-controls="basic-navbar-nav" />
|
<Navbar.Collapse id="basic-navbar-nav">
|
||||||
<Navbar.Collapse id="basic-navbar-nav">
|
<Nav className="mr-auto">
|
||||||
<Nav className="mr-auto">
|
<Nav.Link href="#/">Home</Nav.Link>
|
||||||
<Nav.Link href="#/">Home</Nav.Link>
|
<Nav.Link href="#/leaderboard">Leaderboard</Nav.Link>
|
||||||
<Nav.Link href="#/admin">Admin</Nav.Link>
|
{isUserAdmin && <Nav.Link href="#/admin">Admin</Nav.Link>}
|
||||||
<Nav.Link href="#/wall">The Wall</Nav.Link>
|
</Nav>
|
||||||
</Nav>
|
</Navbar.Collapse>
|
||||||
</Navbar.Collapse>
|
</React.Fragment>
|
||||||
</React.Fragment>
|
|
||||||
)}
|
|
||||||
</Navbar>
|
</Navbar>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,8 @@ import Loading from './Loading';
|
||||||
import Complete from './Complete';
|
import Complete from './Complete';
|
||||||
import Error from './Error';
|
import Error from './Error';
|
||||||
import Withdrawal from './Withdrawal';
|
import Withdrawal from './Withdrawal';
|
||||||
import {sortByAlpha, sortByAttribute} from '../utils';
|
import { sortByAlpha, sortByAttribute } from '../utils';
|
||||||
|
import Praise from './Praise';
|
||||||
/*
|
/*
|
||||||
TODO:
|
TODO:
|
||||||
- list praise for contributor
|
- list praise for contributor
|
||||||
|
@ -49,16 +50,15 @@ class Home extends React.Component {
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
try {
|
try {
|
||||||
const contributorList = (await getFormattedContributorList());
|
const contributorList = await getFormattedContributorList();
|
||||||
|
|
||||||
const currentContributor = await getCurrentContributorData();
|
const currentContributor = await getCurrentContributorData();
|
||||||
|
|
||||||
this.setState({ busy: false, currentContributor, contributorList: contributorList.sort(sortByAlpha('label'))});
|
this.setState({ busy: false, currentContributor, contributorList: contributorList.sort(sortByAlpha('label')) });
|
||||||
|
|
||||||
getAllPraises().then(praises => {
|
|
||||||
this.setState({praises: praises.sort(sortByAttribute('time'))});
|
|
||||||
});
|
|
||||||
|
|
||||||
|
getAllPraises().then(praises => {
|
||||||
|
this.setState({ praises: praises.sort(sortByAttribute('time')) });
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setState({ errorMsg: error.message || error });
|
this.setState({ errorMsg: error.message || error });
|
||||||
}
|
}
|
||||||
|
@ -213,7 +213,7 @@ class Home extends React.Component {
|
||||||
onChangeAward={this.handleAwardChange}
|
onChangeAward={this.handleAwardChange}
|
||||||
onSelectContributor={this.handleContributorSelection}
|
onSelectContributor={this.handleContributorSelection}
|
||||||
onClickPlus5={this.handlePlus5}
|
onClickPlus5={this.handlePlus5}
|
||||||
contributorList={contributorList}
|
contributorList={contributorList.filter(x => x.value !== currentContributor.addr)}
|
||||||
selectedContributors={selectedContributors}
|
selectedContributors={selectedContributors}
|
||||||
award={award}
|
award={award}
|
||||||
isChecked={checkbox}
|
isChecked={checkbox}
|
||||||
|
@ -239,14 +239,16 @@ class Home extends React.Component {
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="wall" title="Wall">
|
<Tab eventKey="wall" title="Wall">
|
||||||
<Container className="pt-4">
|
<Container className="pt-4">
|
||||||
{praises.map((item, i) => <Praise key={i} individual={false} contributorList={contributorList} item={item} />)}
|
{praises.map((item, i) => (
|
||||||
|
<Praise key={i} individual={false} contributorList={contributorList} item={item} />
|
||||||
|
))}
|
||||||
</Container>
|
</Container>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab eventKey="withdraw" title="Withdraw" className="withdraw-panel">
|
<Tab eventKey="withdraw" title="Withdraw" className="withdraw-panel">
|
||||||
{step === 'HOME' && (
|
{step === 'HOME' && (
|
||||||
<Withdrawal
|
<Withdrawal
|
||||||
onClick={this.withdrawTokens}
|
onClick={this.withdrawTokens}
|
||||||
totalReceived={currentContributor.totalReceived}
|
received={currentContributor.received}
|
||||||
allocation={currentContributor.allocation}
|
allocation={currentContributor.allocation}
|
||||||
contributorList={contributorList}
|
contributorList={contributorList}
|
||||||
praises={currentContributor.praises}
|
praises={currentContributor.praises}
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Table } from 'react-bootstrap';
|
||||||
|
import { getFormattedContributorList, getContributorData } from '../services/Meritocracy';
|
||||||
|
import { sortByAlpha, sortByAttribute, sortNullableArray } from '../utils';
|
||||||
|
|
||||||
|
const sort = orderBy => {
|
||||||
|
if (orderBy === 'praises') return sortNullableArray('praises');
|
||||||
|
if (orderBy === 'label') return sortByAlpha('label');
|
||||||
|
return sortByAttribute(orderBy);
|
||||||
|
};
|
||||||
|
|
||||||
|
class Leaderboard extends React.Component {
|
||||||
|
state = {
|
||||||
|
contributorList: [],
|
||||||
|
sortBy: 'label',
|
||||||
|
errorMsg: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
try {
|
||||||
|
const contributorList = await getFormattedContributorList();
|
||||||
|
this.setState({ contributorList });
|
||||||
|
|
||||||
|
// TODO: this can be replaced by event sourcing
|
||||||
|
contributorList.forEach(contrib => {
|
||||||
|
getContributorData(contrib.value).then(data => {
|
||||||
|
contrib = Object.assign(contrib, data);
|
||||||
|
this.setState({ contributorList });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.setState({ errorMsg: error.message || error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortBy = order => () => {
|
||||||
|
this.setState({ sortBy: order });
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { contributorList, sortBy } = this.state;
|
||||||
|
const sortedContributorList = contributorList.sort(sort(sortBy));
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<h2>Leaderboard</h2>
|
||||||
|
<Table striped bordered hover responsive size="sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th onClick={this.sortBy('label')}>Contributor</th>
|
||||||
|
<th onClick={this.sortBy('allocation')}>Allocation</th>
|
||||||
|
<th onClick={this.sortBy('totalReceived')}>SNT Received</th>
|
||||||
|
<th onClick={this.sortBy('totalForfeited')}>SNT Forfeited</th>
|
||||||
|
<th onClick={this.sortBy('praises')}>Praises Received</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sortedContributorList.map((contrib, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
<td>{contrib.label}</td>
|
||||||
|
<td>{contrib.allocation}</td>
|
||||||
|
<td>{contrib.totalReceived}</td>
|
||||||
|
<td>{contrib.totalForfeited}</td>
|
||||||
|
<td>{contrib.praises ? contrib.praises.length : 0}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Leaderboard;
|
|
@ -1,10 +1,11 @@
|
||||||
/* global web3 */
|
/* global web3 */
|
||||||
import React, {Fragment} from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import Address from './Address';
|
import Address from './Address';
|
||||||
import { Row, Col } from 'react-bootstrap';
|
import { Row, Col } from 'react-bootstrap';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const Praise = ({contributorList, item, individual}) => {
|
const Praise = ({ contributorList, item, individual }) => {
|
||||||
const name = contributorList.find(x => x.value === item.author);
|
const name = contributorList.find(x => x.value === item.author);
|
||||||
const date = moment.unix(item.time).fromNow();
|
const date = moment.unix(item.time).fromNow();
|
||||||
return (
|
return (
|
||||||
|
@ -12,14 +13,16 @@ const Praise = ({contributorList, item, individual}) => {
|
||||||
<Col className="mb-4 text-muted">
|
<Col className="mb-4 text-muted">
|
||||||
{!item.praise && (
|
{!item.praise && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{(name && name.label) || <Address value={item.author} compact={true} />} {individual ? "has sent you" : "sent"}{' '}
|
{(name && name.label) || <Address value={item.author} compact={true} />}{' '}
|
||||||
{web3.utils.fromWei(item.amount, 'ether')} SNT {!individual && <span>to {item.destination}</span>}, <small>{date}</small>
|
{individual ? 'has sent you' : 'sent'} {web3.utils.fromWei(item.amount, 'ether')} SNT{' '}
|
||||||
|
{!individual && <span>to {item.destination}</span>}, <small>{date}</small>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{item.praise && (
|
{item.praise && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{(name && name.label) || <Address value={item.author} compact={true} />}{!individual && <span> to {item.destination}</span>}, <small>{date}</small>
|
{(name && name.label) || <Address value={item.author} compact={true} />}
|
||||||
|
{!individual && <span> to {item.destination}</span>}, <small>{date}</small>
|
||||||
<div className="chatBubble p-3">
|
<div className="chatBubble p-3">
|
||||||
"{item.praise}"
|
"{item.praise}"
|
||||||
<small className="float-right">{web3.utils.fromWei(item.amount, 'ether')} SNT</small>
|
<small className="float-right">{web3.utils.fromWei(item.amount, 'ether')} SNT</small>
|
||||||
|
@ -31,4 +34,10 @@ const Praise = ({contributorList, item, individual}) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Praise.propTypes = {
|
||||||
|
contributorList: PropTypes.array,
|
||||||
|
individual: PropTypes.bool,
|
||||||
|
item: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
export default Praise;
|
export default Praise;
|
||||||
|
|
|
@ -5,12 +5,12 @@ import Praise from './Praise';
|
||||||
|
|
||||||
import './withdrawal.scss';
|
import './withdrawal.scss';
|
||||||
|
|
||||||
const Withdrawal = ({ totalReceived, allocation, onClick, contributorList, praises }) => (
|
const Withdrawal = ({ received, allocation, onClick, contributorList, praises }) => (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="text-center p-4">
|
<div className="text-center p-4">
|
||||||
<p className="text-muted mb-0 mt-5">You have been awarded</p>
|
<p className="text-muted mb-0 mt-5">You have been awarded</p>
|
||||||
<p className="awarded mb-0">
|
<p className="awarded mb-0">
|
||||||
{totalReceived || 0} <span className="text-muted">SNT</span>
|
{received || 0} <span className="text-muted">SNT</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-muted">Available for withdraw</p>
|
<p className="text-muted">Available for withdraw</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,15 +22,15 @@ const Withdrawal = ({ totalReceived, allocation, onClick, contributorList, prais
|
||||||
|
|
||||||
<p className="text-center">
|
<p className="text-center">
|
||||||
<Button
|
<Button
|
||||||
variant={allocation !== '0' || totalReceived === '0' ? 'secondary' : 'primary'}
|
variant={allocation !== '0' || received === '0' ? 'secondary' : 'primary'}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={allocation !== '0' || totalReceived === '0'}
|
disabled={allocation !== '0' || received === '0'}
|
||||||
>
|
>
|
||||||
Withdraw
|
Withdraw
|
||||||
</Button>
|
</Button>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{totalReceived !== '0' && parseInt(allocation, 10) > 0 && (
|
{received !== '0' && parseInt(allocation, 10) > 0 && (
|
||||||
<div className="text-muted text-left border rounded p-2 mb-2 learn-more">
|
<div className="text-muted text-left border rounded p-2 mb-2 learn-more">
|
||||||
<img src={info} alt="" />
|
<img src={info} alt="" />
|
||||||
<p className="m-0 p-0">
|
<p className="m-0 p-0">
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
.chatBubble {
|
.chatBubble {
|
||||||
background: #ECEFFC;
|
background: #ECEFFC;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
word-wrap: break-word;
|
||||||
}
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
/*global web3*/
|
/*global web3*/
|
||||||
import Meritocracy from 'Embark/contracts/Meritocracy';
|
import Meritocracy from 'Embark/contracts/Meritocracy';
|
||||||
|
import SNT from 'Embark/contracts/SNT';
|
||||||
|
|
||||||
import EmbarkJS from 'Embark/EmbarkJS';
|
import EmbarkJS from 'Embark/EmbarkJS';
|
||||||
|
|
||||||
let contributorList;
|
let contributorList;
|
||||||
|
@ -82,9 +84,7 @@ export async function getFormattedContributorList(hash) {
|
||||||
list = list.map(prepareOptions);
|
list = list.map(prepareOptions);
|
||||||
|
|
||||||
const registry = await Meritocracy.methods.getRegistry().call({ from: mainAccount });
|
const registry = await Meritocracy.methods.getRegistry().call({ from: mainAccount });
|
||||||
list = list.filter(
|
list = list.filter(contributorData => registry.includes(contributorData.value));
|
||||||
contributorData => registry.includes(contributorData.value) && contributorData.value !== mainAccount
|
|
||||||
);
|
|
||||||
|
|
||||||
resolve(list);
|
resolve(list);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -96,6 +96,79 @@ export async function getFormattedContributorList(hash) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function forfeitAllocation() {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const mainAccount = web3.eth.defaultAccount;
|
||||||
|
try {
|
||||||
|
const toSend = Meritocracy.methods.forfeitAllocations();
|
||||||
|
let gas = await toSend.estimateGas({ from: mainAccount });
|
||||||
|
const receipt = await toSend.send({ from: mainAccount, gas: gas + 1000 });
|
||||||
|
resolve(receipt);
|
||||||
|
} catch (error) {
|
||||||
|
const message = 'Error forfeiting allocation';
|
||||||
|
console.error(message);
|
||||||
|
console.error(error);
|
||||||
|
reject(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function allocate(sntAmount) {
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const mainAccount = web3.eth.defaultAccount;
|
||||||
|
try {
|
||||||
|
let toSend, gas;
|
||||||
|
|
||||||
|
const balance = web3.utils.toBN(await SNT.methods.balanceOf(mainAccount).call());
|
||||||
|
const allowance = web3.utils.toBN(await SNT.methods.allowance(mainAccount, Meritocracy.options.address).call());
|
||||||
|
|
||||||
|
if (balance.lt(web3.utils.toBN(sntAmount))) {
|
||||||
|
throw new Error('Not enough SNT');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowance.gt(web3.utils.toBN('0')) && allowance.lt(web3.utils.toBN(sntAmount))) {
|
||||||
|
alert('Reset allowance to 0');
|
||||||
|
toSend = SNT.methods.approve(Meritocracy.options.address, '0');
|
||||||
|
gas = await toSend.estimateGas({ from: mainAccount });
|
||||||
|
await toSend.send({ from: mainAccount, gas: gas + 1000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowance.eq(web3.utils.toBN('0'))) {
|
||||||
|
alert(`Approving ${web3.utils.fromWei(sntAmount, 'ether')} to meritocracy contract`);
|
||||||
|
toSend = SNT.methods.approve(Meritocracy.options.address, sntAmount);
|
||||||
|
gas = await toSend.estimateGas({ from: mainAccount });
|
||||||
|
await toSend.send({ from: mainAccount, gas: gas + 1000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
alert('Allocating SNT');
|
||||||
|
toSend = Meritocracy.methods.allocate(sntAmount);
|
||||||
|
gas = await toSend.estimateGas({ from: mainAccount });
|
||||||
|
await toSend.send({ from: mainAccount, gas: gas + 1000 });
|
||||||
|
|
||||||
|
resolve(true);
|
||||||
|
} catch (error) {
|
||||||
|
let message;
|
||||||
|
|
||||||
|
if (error.message === 'Not enough SNT') {
|
||||||
|
message = 'Not enough SNT';
|
||||||
|
} else {
|
||||||
|
message = 'Error forfeiting allocation';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(message);
|
||||||
|
console.error(error);
|
||||||
|
reject(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lastForfeited() {
|
||||||
|
return new Promise(async resolve => {
|
||||||
|
const date = await Meritocracy.methods.lastForfeit().call();
|
||||||
|
resolve(date);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const prepareOptions = option => {
|
const prepareOptions = option => {
|
||||||
if (option.value.match(/^0x[0-9A-Za-z]{40}$/)) {
|
if (option.value.match(/^0x[0-9A-Za-z]{40}$/)) {
|
||||||
// Address
|
// Address
|
||||||
|
@ -110,7 +183,7 @@ const prepareOptions = option => {
|
||||||
return option;
|
return option;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getCurrentContributorData(){
|
export async function getCurrentContributorData() {
|
||||||
const mainAccount = web3.eth.defaultAccount;
|
const mainAccount = web3.eth.defaultAccount;
|
||||||
const contribData = await getContributorData(mainAccount);
|
const contribData = await getContributorData(mainAccount);
|
||||||
return contribData;
|
return contribData;
|
||||||
|
@ -120,7 +193,7 @@ export async function getContributorData(_address) {
|
||||||
const currentContributor = await getContributor(_address);
|
const currentContributor = await getContributor(_address);
|
||||||
|
|
||||||
let praises = [];
|
let praises = [];
|
||||||
for(let i = 0; i < currentContributor.praiseNum; i++){
|
for (let i = 0; i < currentContributor.praiseNum; i++) {
|
||||||
praises.push(Meritocracy.methods.getStatus(_address, i).call());
|
praises.push(Meritocracy.methods.getStatus(_address, i).call());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,30 +202,29 @@ export async function getContributorData(_address) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const contribData = contributorList.find(x => x.value === _address);
|
const contribData = contributorList.find(x => x.value === _address);
|
||||||
if(contribData) currentContributor.name = contribData.label;
|
if (contribData) currentContributor.name = contribData.label;
|
||||||
|
|
||||||
currentContributor.praises = await Promise.all(praises);
|
currentContributor.praises = await Promise.all(praises);
|
||||||
currentContributor.allocation = web3.utils.fromWei(currentContributor.allocation, "ether");
|
currentContributor.allocation = web3.utils.fromWei(currentContributor.allocation, 'ether');
|
||||||
currentContributor.totalForfeited = web3.utils.fromWei(currentContributor.totalForfeited, "ether");
|
currentContributor.totalForfeited = web3.utils.fromWei(currentContributor.totalForfeited, 'ether');
|
||||||
currentContributor.totalReceived = web3.utils.fromWei(currentContributor.totalReceived, "ether");
|
currentContributor.totalReceived = web3.utils.fromWei(currentContributor.totalReceived, 'ether');
|
||||||
currentContributor.received = web3.utils.fromWei(currentContributor.received, "ether");
|
currentContributor.received = web3.utils.fromWei(currentContributor.received, 'ether');
|
||||||
|
|
||||||
return currentContributor;
|
return currentContributor;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAllPraises() {
|
export function getAllPraises() {
|
||||||
|
|
||||||
return new Promise(function(resolve) {
|
return new Promise(function(resolve) {
|
||||||
let praisesPromises = [];
|
let praisesPromises = [];
|
||||||
let praiseNumPromises = [];
|
let praiseNumPromises = [];
|
||||||
for(let i = 0; i < contributorList.length; i++){
|
for (let i = 0; i < contributorList.length; i++) {
|
||||||
praiseNumPromises.push(Meritocracy.methods.getStatusLength(contributorList[i].value).call());
|
praiseNumPromises.push(Meritocracy.methods.getStatusLength(contributorList[i].value).call());
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.all(praiseNumPromises).then(praiseNums => {
|
Promise.all(praiseNumPromises).then(praiseNums => {
|
||||||
for(let i = 0; i < contributorList.length; i++){
|
for (let i = 0; i < contributorList.length; i++) {
|
||||||
let currPraises = [];
|
let currPraises = [];
|
||||||
for(let j = 0; j < praiseNums[i]; j++){
|
for (let j = 0; j < praiseNums[i]; j++) {
|
||||||
currPraises.push(Meritocracy.methods.getStatus(contributorList[i].value, j).call());
|
currPraises.push(Meritocracy.methods.getStatus(contributorList[i].value, j).call());
|
||||||
}
|
}
|
||||||
praisesPromises.push(currPraises);
|
praisesPromises.push(currPraises);
|
||||||
|
@ -165,17 +237,16 @@ export function getAllPraises() {
|
||||||
);
|
);
|
||||||
|
|
||||||
allPromises.then(praises => {
|
allPromises.then(praises => {
|
||||||
for(let i = 0; i < praises.length; i++){
|
for (let i = 0; i < praises.length; i++) {
|
||||||
praises[i] = praises[i].map(x => {
|
praises[i] = praises[i].map(x => {
|
||||||
x.destination = contributorList[i].label;
|
x.destination = contributorList[i].label;
|
||||||
return x;
|
return x;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
resolve(praises.flat());
|
resolve(praises.flat());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getContributor(_address) {
|
export async function getContributor(_address) {
|
||||||
|
|
|
@ -24,7 +24,7 @@ export const isInteger = value => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isNumber = value => {
|
export const isNumber = value => {
|
||||||
if (Number.isNaN(value)) {
|
if (isNaN(value)) {
|
||||||
return (
|
return (
|
||||||
<Form.Control.Feedback type="invalid" className="d-block">
|
<Form.Control.Feedback type="invalid" className="d-block">
|
||||||
This field needs to be an number
|
This field needs to be an number
|
||||||
|
|
Loading…
Reference in New Issue