feat: leaderboard for admins (#23)
This commit is contained in:
parent
58b73de7f1
commit
445e7f8471
|
@ -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>
|
||||
|
|
|
@ -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 => ({
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
};
|
Loading…
Reference in New Issue