feat: leaderboard for admins (#23)
This commit is contained in:
parent
58b73de7f1
commit
445e7f8471
|
@ -1,15 +1,21 @@
|
||||||
import React, { Fragment } from 'react';
|
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 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 } 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 { addContributor, getFormattedContributorList, removeContributor } from '../services/Meritocracy';
|
import { sortByAlpha, sortByAttribute, sortNullableArray } from '../utils';
|
||||||
|
|
||||||
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: '',
|
||||||
|
@ -19,7 +25,9 @@ class Admin extends React.Component {
|
||||||
successMsg: '',
|
successMsg: '',
|
||||||
contributorList: [],
|
contributorList: [],
|
||||||
showDeleteModal: false,
|
showDeleteModal: false,
|
||||||
focusedContributorIndex: -1
|
focusedContributorIndex: -1,
|
||||||
|
sortBy: 'label',
|
||||||
|
tab: 'admin'
|
||||||
};
|
};
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
|
@ -27,6 +35,16 @@ class Admin extends React.Component {
|
||||||
const contributorList = await getFormattedContributorList();
|
const contributorList = await getFormattedContributorList();
|
||||||
|
|
||||||
this.setState({ busy: false, contributorList });
|
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) {
|
} catch (error) {
|
||||||
this.setState({ errorMsg: error.message || error });
|
this.setState({ errorMsg: error.message || error });
|
||||||
}
|
}
|
||||||
|
@ -75,6 +93,10 @@ class Admin extends React.Component {
|
||||||
this.setState({ showDeleteModal: false });
|
this.setState({ showDeleteModal: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sortBy = (order) => () => {
|
||||||
|
this.setState({sortBy: order});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
contributorAddress,
|
contributorAddress,
|
||||||
|
@ -83,12 +105,17 @@ class Admin extends React.Component {
|
||||||
busy,
|
busy,
|
||||||
contributorList,
|
contributorList,
|
||||||
successMsg,
|
successMsg,
|
||||||
focusedContributorIndex
|
focusedContributorIndex,
|
||||||
|
sortBy,
|
||||||
|
tab
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const currentContributor = focusedContributorIndex > -1 ? contributorList[focusedContributorIndex] : {};
|
const currentContributor = focusedContributorIndex > -1 ? contributorList[focusedContributorIndex] : {};
|
||||||
|
const sortedContributorList = contributorList.sort(sort(sortBy));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
<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>
|
<h2>Admin Panel</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>}
|
||||||
|
@ -124,7 +151,7 @@ class Admin extends React.Component {
|
||||||
</ValidatedForm>
|
</ValidatedForm>
|
||||||
<h3>Contributor List</h3>
|
<h3>Contributor List</h3>
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
{contributorList.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> {contributor.value}
|
||||||
<div className="contributor-controls float-right">
|
<div className="contributor-controls float-right">
|
||||||
|
@ -139,7 +166,34 @@ class Admin extends React.Component {
|
||||||
</ListGroup.Item>
|
</ListGroup.Item>
|
||||||
))}
|
))}
|
||||||
</ListGroup>
|
</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 show={this.state.showDeleteModal} onHide={this.handleClose}>
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
<Modal.Title>Are you sure you want to remove this contributor?</Modal.Title>
|
<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';
|
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 = ({
|
const ContributorSelection = ({
|
||||||
allocation,
|
allocation,
|
||||||
contributorList,
|
contributorList,
|
||||||
|
@ -40,7 +34,7 @@ const ContributorSelection = ({
|
||||||
isMulti
|
isMulti
|
||||||
value={selectedContributors}
|
value={selectedContributors}
|
||||||
onChange={onSelectContributor}
|
onChange={onSelectContributor}
|
||||||
options={contributorList.sort(sortByAlpha)}
|
options={contributorList}
|
||||||
placeholder="Choose Contributor(s)..."
|
placeholder="Choose Contributor(s)..."
|
||||||
className="mb-2 contributorSelector"
|
className="mb-2 contributorSelector"
|
||||||
theme={theme => ({
|
theme={theme => ({
|
||||||
|
|
|
@ -10,7 +10,7 @@ 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} from '../utils';
|
||||||
/*
|
/*
|
||||||
TODO:
|
TODO:
|
||||||
- list praise for contributor
|
- list praise for contributor
|
||||||
|
@ -48,11 +48,11 @@ 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 });
|
this.setState({ busy: false, currentContributor, contributorList: contributorList.sort(sortByAlpha('label'))});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.setState({ errorMsg: error.message || error });
|
this.setState({ errorMsg: error.message || error });
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,29 +112,35 @@ const prepareOptions = option => {
|
||||||
|
|
||||||
export async function getCurrentContributorData(){
|
export async function getCurrentContributorData(){
|
||||||
const mainAccount = web3.eth.defaultAccount;
|
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 = [];
|
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());
|
praises.push(Meritocracy.methods.getStatus(_address, i).call());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!contributorList) {
|
if (!contributorList) {
|
||||||
await getContributorList();
|
await getContributorList();
|
||||||
}
|
}
|
||||||
|
|
||||||
const contribData = contributorList.find(x => x.value === mainAccount);
|
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 async function getContributor(_address) {
|
export async function getContributor(_address) {
|
||||||
const contributor = await Meritocracy.methods.contributors(_address).call();
|
const contributor = await Meritocracy.methods.contributors(_address).call();
|
||||||
contributor.praiseNum = await Meritocracy.methods.getStatusLength(_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