feat: leaderboard for admins (#23)

This commit is contained in:
Richard Ramos 2019-05-07 09:34:59 -04:00 committed by GitHub
parent 58b73de7f1
commit 445e7f8471
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 151 additions and 75 deletions

View File

@ -1,15 +1,21 @@
import React, { Fragment } from 'react';
import { Button, Form, Alert, ListGroup, OverlayTrigger, Tooltip, Modal } from 'react-bootstrap';
import { Button, Form, Alert, ListGroup, OverlayTrigger, Tooltip, Modal, Tabs, Tab, Table } 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 { addContributor, getFormattedContributorList, removeContributor } from '../services/Meritocracy';
import { addContributor, getFormattedContributorList, removeContributor, getContributorData } from '../services/Meritocracy';
import { sortByAlpha, sortByAttribute, sortNullableArray } from '../utils';
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 {
state = {
contributorName: '',
@ -19,7 +25,9 @@ class Admin extends React.Component {
successMsg: '',
contributorList: [],
showDeleteModal: false,
focusedContributorIndex: -1
focusedContributorIndex: -1,
sortBy: 'label',
tab: 'admin'
};
async componentDidMount() {
@ -27,6 +35,16 @@ class Admin extends React.Component {
const contributorList = await getFormattedContributorList();
this.setState({ busy: false, 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 });
}
@ -75,6 +93,10 @@ class Admin extends React.Component {
this.setState({ showDeleteModal: false });
};
sortBy = (order) => () => {
this.setState({sortBy: order});
}
render() {
const {
contributorAddress,
@ -83,63 +105,95 @@ class Admin extends React.Component {
busy,
contributorList,
successMsg,
focusedContributorIndex
focusedContributorIndex,
sortBy,
tab
} = this.state;
const currentContributor = focusedContributorIndex > -1 ? contributorList[focusedContributorIndex] : {};
const sortedContributorList = contributorList.sort(sort(sortBy));
return (
<Fragment>
<h2>Admin Panel</h2>
{error && <Alert variant="danger">{error}</Alert>}
{successMsg && <Alert variant="success">{successMsg}</Alert>}
{busy && <Alert variant="primary">Working...</Alert>}
<h3>Add a contributor</h3>
<ValidatedForm onSubmit={e => this.addContributor(e)}>
<Form.Group controlId="formContributor">
<Form.Label>Contributor name</Form.Label>
<Input
type="text"
placeholder="Name"
value={contributorName}
onChange={e => this.onChange('contributorName', e)}
className="form-control"
validations={[required]}
/>
</Form.Group>
<Form.Group controlId="formAddress">
<Form.Label>Contributor address</Form.Label>
<Input
type="text"
placeholder="0x"
value={contributorAddress}
onChange={e => this.onChange('contributorAddress', e)}
className="form-control"
validations={[required, isAddress]}
/>
</Form.Group>
<Button variant="primary" onClick={e => this.addContributor(e)}>
Add
</Button>
</ValidatedForm>
<h3>Contributor List</h3>
<ListGroup>
{contributorList.map((contributor, idx) => (
<ListGroup.Item key={contributor.value} action className="contributor-item">
<span className="font-weight-bold">{contributor.label}:</span> {contributor.value}
<div className="contributor-controls float-right">
<OverlayTrigger placement="top" overlay={<Tooltip>Delete contributor</Tooltip>}>
<FontAwesomeIcon
icon={faTrash}
className="text-danger icon"
onClick={e => this.removeContributor(e, idx)}
/>
</OverlayTrigger>
</div>
</ListGroup.Item>
))}
</ListGroup>
<Tabs className="home-tabs mb-3" activeKey={tab} onSelect={tab => this.setState({ tab })}>
<Tab eventKey="admin" title="Admin Panel" className="admin-panel">
<h2>Admin Panel</h2>
{error && <Alert variant="danger">{error}</Alert>}
{successMsg && <Alert variant="success">{successMsg}</Alert>}
{busy && <Alert variant="primary">Working...</Alert>}
<h3>Add a contributor</h3>
<ValidatedForm onSubmit={e => this.addContributor(e)}>
<Form.Group controlId="formContributor">
<Form.Label>Contributor name</Form.Label>
<Input
type="text"
placeholder="Name"
value={contributorName}
onChange={e => this.onChange('contributorName', e)}
className="form-control"
validations={[required]}
/>
</Form.Group>
<Form.Group controlId="formAddress">
<Form.Label>Contributor address</Form.Label>
<Input
type="text"
placeholder="0x"
value={contributorAddress}
onChange={e => this.onChange('contributorAddress', e)}
className="form-control"
validations={[required, isAddress]}
/>
</Form.Group>
<Button variant="primary" onClick={e => this.addContributor(e)}>
Add
</Button>
</ValidatedForm>
<h3>Contributor List</h3>
<ListGroup>
{contributorList.sort(sortByAlpha('label')).map((contributor, idx) => (
<ListGroup.Item key={contributor.value} action className="contributor-item">
<span className="font-weight-bold">{contributor.label}:</span> {contributor.value}
<div className="contributor-controls float-right">
<OverlayTrigger placement="top" overlay={<Tooltip>Delete contributor</Tooltip>}>
<FontAwesomeIcon
icon={faTrash}
className="text-danger icon"
onClick={e => this.removeContributor(e, idx)}
/>
</OverlayTrigger>
</div>
</ListGroup.Item>
))}
</ListGroup>
</Tab>
<Tab eventKey="leaderboard" title="Leaderboard" className="leaderboard-panel">
<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>
</Tab>
</Tabs>
<Modal show={this.state.showDeleteModal} onHide={this.handleClose}>
<Modal.Header closeButton>
<Modal.Title>Are you sure you want to remove this contributor?</Modal.Title>

View File

@ -6,12 +6,6 @@ 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,
@ -40,7 +34,7 @@ const ContributorSelection = ({
isMulti
value={selectedContributors}
onChange={onSelectContributor}
options={contributorList.sort(sortByAlpha)}
options={contributorList}
placeholder="Choose Contributor(s)..."
className="mb-2 contributorSelector"
theme={theme => ({

View File

@ -10,7 +10,7 @@ import Loading from './Loading';
import Complete from './Complete';
import Error from './Error';
import Withdrawal from './Withdrawal';
import {sortByAlpha} from '../utils';
/*
TODO:
- list praise for contributor
@ -48,11 +48,11 @@ class Home extends React.Component {
async componentDidMount() {
try {
const contributorList = await getFormattedContributorList();
const contributorList = (await getFormattedContributorList());
const currentContributor = await getCurrentContributorData();
this.setState({ busy: false, currentContributor, contributorList });
this.setState({ busy: false, currentContributor, contributorList: contributorList.sort(sortByAlpha('label'))});
} catch (error) {
this.setState({ errorMsg: error.message || error });
}

View File

@ -110,31 +110,37 @@ const prepareOptions = option => {
return option;
};
export async function getCurrentContributorData() {
export async function getCurrentContributorData(){
const mainAccount = web3.eth.defaultAccount;
const currentContributor = await getContributor(mainAccount);
const contribData = await getContributorData(mainAccount);
return contribData;
}
export async function getContributorData(_address) {
const currentContributor = await getContributor(_address);
let praises = [];
for (let i = 0; i < currentContributor.praiseNum; i++) {
praises.push(Meritocracy.methods.getStatus(mainAccount, i).call());
for(let i = 0; i < currentContributor.praiseNum; i++){
praises.push(Meritocracy.methods.getStatus(_address, i).call());
}
if (!contributorList) {
await getContributorList();
}
const contribData = contributorList.find(x => x.value === mainAccount);
if (contribData) currentContributor.name = contribData.label;
const contribData = contributorList.find(x => x.value === _address);
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;
}
export async function getContributor(_address) {
const contributor = await Meritocracy.methods.contributors(_address).call();
contributor.praiseNum = await Meritocracy.methods.getStatusLength(_address).call();

22
app/js/utils.js Normal file
View File

@ -0,0 +1,22 @@
export const sortByAlpha = field => (a, b) => {
const a_field = a[field].toLowerCase();
const b_field = b[field].toLowerCase();
if (a_field < b_field) return -1;
if (a_field > b_field) return 1;
return 0;
};
export const sortByAttribute = field => (a, b) => {
if (a[field] > b[field]) return -1;
if (a[field] < b[field]) return 1;
return 0;
};
export const sortNullableArray = field => (a, b) => {
const a_field = a[field] || [];
const b_field = b[field] || [];
if (a_field.length > b_field.length) return -1;
if (a_field.length < b_field.length) return 1;
return 0;
};