2018-01-12 10:44:16 -08:00
|
|
|
import React from "react";
|
|
|
|
import moment from "moment";
|
2018-01-12 08:14:34 -08:00
|
|
|
import { observable, action, computed } from "mobx";
|
2018-01-11 10:49:08 -08:00
|
|
|
import { inject, observer } from "mobx-react";
|
|
|
|
import { toAscii } from "../helpers";
|
|
|
|
import { constants } from "../constants";
|
2018-01-16 16:05:12 -08:00
|
|
|
import { messages } from "../messages";
|
2018-01-12 10:44:16 -08:00
|
|
|
import swal from "sweetalert2";
|
2018-01-11 10:49:08 -08:00
|
|
|
|
|
|
|
const ACCEPT = 1;
|
|
|
|
const REJECT = 2;
|
2018-01-16 15:58:45 -08:00
|
|
|
const USDateTimeFormat = "MM/DD/YYYY h:mm:ss A";
|
2018-01-16 16:02:07 -08:00
|
|
|
|
|
|
|
const zeroTimeTo = "00:00";
|
|
|
|
|
2018-01-11 10:49:08 -08:00
|
|
|
@inject("commonStore", "contractsStore", "ballotStore", "routing")
|
|
|
|
@observer
|
|
|
|
export class BallotCard extends React.Component {
|
2018-01-12 10:44:16 -08:00
|
|
|
@observable startTime;
|
|
|
|
@observable endTime;
|
2018-01-16 16:02:07 -08:00
|
|
|
@observable timeTo = {};
|
|
|
|
@observable timeToStart = {
|
|
|
|
val: 0,
|
|
|
|
displayValue: zeroTimeTo,
|
|
|
|
title: "To start"
|
|
|
|
};
|
|
|
|
@observable timeToFinish = {
|
|
|
|
val: 0,
|
|
|
|
displayValue: zeroTimeTo,
|
|
|
|
title: "To close"
|
|
|
|
};
|
2018-01-12 10:44:16 -08:00
|
|
|
@observable creator;
|
|
|
|
@observable progress;
|
|
|
|
@observable totalVoters;
|
|
|
|
@observable isFinalized;
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-15 05:47:34 -08:00
|
|
|
@computed get finalizeButtonDisplayName() {
|
|
|
|
const displayName = this.isFinalized ? "Finalized" : "Finalize ballot";
|
|
|
|
return displayName;
|
|
|
|
}
|
|
|
|
|
|
|
|
@computed get finalizeButtonClass () {
|
|
|
|
const cls = this.isFinalized ? "ballots-footer-finalize ballots-footer-finalize-finalized" : "ballots-footer-finalize";
|
|
|
|
return cls;
|
|
|
|
}
|
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
@computed get votesForNumber() {
|
|
|
|
let votes = (this.totalVoters + this.progress) / 2;
|
|
|
|
return votes;
|
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
@computed get votesForPercents() {
|
2018-01-12 11:09:04 -08:00
|
|
|
if (this.totalVoters <= 0) {
|
2018-01-12 10:44:16 -08:00
|
|
|
return 0;
|
2018-01-12 11:09:04 -08:00
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
let votesPercents = Math.round(this.votesForNumber / this.totalVoters * 100);
|
|
|
|
return votesPercents;
|
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
@computed get votesAgainstNumber() {
|
|
|
|
let votes = (this.totalVoters - this.progress) / 2;
|
|
|
|
return votes;
|
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
@computed get votesAgainstPercents() {
|
2018-01-12 11:09:04 -08:00
|
|
|
if (this.totalVoters <= 0) {
|
2018-01-12 10:44:16 -08:00
|
|
|
return 0;
|
2018-01-12 11:09:04 -08:00
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
let votesPercents = Math.round(this.votesAgainstNumber / this.totalVoters * 100);
|
|
|
|
return votesPercents;
|
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
@action("Get start time of keys ballot")
|
|
|
|
getStartTime = async () => {
|
|
|
|
const { contractsStore, id, votingType } = this.props;
|
2018-01-12 11:51:32 -08:00
|
|
|
let startTime = await this.getContract(contractsStore, votingType).getStartTime(id);
|
2018-01-16 15:58:45 -08:00
|
|
|
this.startTime = moment.utc(startTime * 1000).format(USDateTimeFormat);
|
2018-01-12 10:44:16 -08:00
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
@action("Get end time of keys ballot")
|
|
|
|
getEndTime = async () => {
|
|
|
|
const { contractsStore, id, votingType } = this.props;
|
2018-01-12 11:51:32 -08:00
|
|
|
let endTime = await this.getContract(contractsStore, votingType).getEndTime(id);
|
2018-01-16 15:58:45 -08:00
|
|
|
this.endTime = moment.utc(endTime * 1000).format(USDateTimeFormat);
|
2018-01-12 10:44:16 -08:00
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-16 16:02:07 -08:00
|
|
|
@action("Calculate time to start/finish")
|
|
|
|
calcTimeTo = () => {
|
|
|
|
const _now = moment();
|
|
|
|
const start = moment.utc(this.startTime, USDateTimeFormat);
|
2018-01-16 15:58:45 -08:00
|
|
|
const finish = moment.utc(this.endTime, USDateTimeFormat);
|
2018-01-16 16:02:07 -08:00
|
|
|
let msStart = start.diff(_now);
|
|
|
|
let msFinish = finish.diff(_now);
|
|
|
|
|
|
|
|
if (msStart > 0) {
|
|
|
|
this.timeToStart.val = msStart;
|
|
|
|
this.timeToStart.displayValue = this.formatMs(msStart, ":mm:ss");
|
|
|
|
return this.timeTo = this.timeToStart;
|
2018-01-12 11:09:04 -08:00
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-16 16:02:07 -08:00
|
|
|
if (msFinish > 0) {
|
|
|
|
this.timeToFinish.val = msFinish;
|
|
|
|
this.timeToFinish.displayValue = this.formatMs(msFinish, ":mm:ss");
|
|
|
|
return this.timeTo = this.timeToFinish;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.timeToFinish.val = 0;
|
|
|
|
this.timeToFinish.displayValue = zeroTimeTo;
|
|
|
|
return this.timeTo = this.timeToFinish;
|
|
|
|
}
|
|
|
|
|
|
|
|
formatMs (ms, format) {
|
2018-01-12 10:44:16 -08:00
|
|
|
let dur = moment.duration(ms);
|
2018-01-16 16:02:07 -08:00
|
|
|
let hours = Math.floor(dur.asHours());
|
|
|
|
hours = hours < 10 ? "0" + hours : hours;
|
|
|
|
let formattedMs = hours + moment.utc(ms).format(":mm:ss");
|
|
|
|
return formattedMs;
|
2018-01-12 10:44:16 -08:00
|
|
|
}
|
2018-01-16 16:02:07 -08:00
|
|
|
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
@action("Get times")
|
|
|
|
getTimes = async () => {
|
|
|
|
await this.getStartTime();
|
|
|
|
await this.getEndTime();
|
2018-01-16 16:02:07 -08:00
|
|
|
this.calcTimeTo();
|
2018-01-12 10:44:16 -08:00
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
@action("Get creator")
|
|
|
|
getCreator = async () => {
|
|
|
|
const { contractsStore, id, votingType } = this.props;
|
2018-01-12 11:51:32 -08:00
|
|
|
let votingState = await this.getContract(contractsStore, votingType).votingState(id);
|
2018-01-12 10:44:16 -08:00
|
|
|
this.getValidatorFullname(votingState.creator);
|
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
@action("Get progress")
|
|
|
|
getProgress = async () => {
|
|
|
|
const { contractsStore, id, votingType } = this.props;
|
2018-01-12 11:51:32 -08:00
|
|
|
let progress = await this.getContract(contractsStore, votingType).getProgress(id);
|
2018-01-12 10:44:16 -08:00
|
|
|
this.progress = Number(progress);
|
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
@action("Get total voters")
|
|
|
|
getTotalVoters = async () => {
|
|
|
|
const { contractsStore, id, votingType } = this.props;
|
2018-01-12 11:51:32 -08:00
|
|
|
let totalVoters = await this.getContract(contractsStore, votingType).getTotalVoters(id);
|
2018-01-12 10:44:16 -08:00
|
|
|
this.totalVoters = Number(totalVoters);
|
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
@action("Get isFinalized")
|
|
|
|
getIsFinalized = async() => {
|
|
|
|
const { contractsStore, id, votingType } = this.props;
|
2018-01-12 11:51:32 -08:00
|
|
|
this.isFinalized = await this.getContract(contractsStore, votingType).getIsFinalized(id);
|
2018-01-12 10:44:16 -08:00
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
@action("Get validator full name")
|
|
|
|
getValidatorFullname = async (_miningKey) => {
|
|
|
|
const { contractsStore } = this.props;
|
|
|
|
let validator = await contractsStore.validatorMetadata.validators(_miningKey);
|
|
|
|
let firstName = toAscii(validator.firstName);
|
|
|
|
let lastName = toAscii(validator.lastName);
|
2018-01-12 11:09:04 -08:00
|
|
|
let fullName = `${firstName} ${lastName}`;
|
2018-01-12 10:44:16 -08:00
|
|
|
this.creator = fullName ? fullName : _miningKey;
|
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
isValidaVote = async () => {
|
|
|
|
const { contractsStore, id, votingType } = this.props;
|
2018-01-12 11:51:32 -08:00
|
|
|
let isValidVote = await this.getContract(contractsStore, votingType).isValidVote(id, contractsStore.votingKey);
|
2018-01-12 10:44:16 -08:00
|
|
|
return isValidVote;
|
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
isActive = async () => {
|
|
|
|
const { contractsStore, id, votingType } = this.props;
|
2018-01-12 11:51:32 -08:00
|
|
|
let isActive = await this.getContract(contractsStore, votingType).isActive(id);
|
2018-01-12 10:44:16 -08:00
|
|
|
return isActive;
|
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
vote = async ({choice}) => {
|
2018-01-16 16:05:12 -08:00
|
|
|
if (this.timeToStart.val > 0) {
|
|
|
|
swal("Warning!", messages.ballotIsNotActiveMsg(this.timeTo.displayValue), "warning");
|
|
|
|
return;
|
|
|
|
}
|
2018-01-12 10:44:16 -08:00
|
|
|
const { commonStore, contractsStore, id, votingType } = this.props;
|
|
|
|
const { push } = this.props.routing;
|
|
|
|
if (!contractsStore.isValidVotingKey) {
|
2018-01-16 16:05:12 -08:00
|
|
|
swal("Warning!", messages.invalidVotingKeyMsg(contractsStore.votingKey), "warning");
|
2018-01-12 10:44:16 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
commonStore.showLoading();
|
|
|
|
let isValidVote = await this.isValidaVote();
|
|
|
|
if (!isValidVote) {
|
|
|
|
commonStore.hideLoading();
|
2018-01-16 16:05:12 -08:00
|
|
|
swal("Warning!", messages.INVALID_VOTE_MSG, "warning");
|
2018-01-12 10:44:16 -08:00
|
|
|
return;
|
|
|
|
}
|
2018-01-12 11:51:32 -08:00
|
|
|
this.getContract(contractsStore, votingType).vote(id, choice, contractsStore.votingKey)
|
2018-01-12 10:44:16 -08:00
|
|
|
.on("receipt", () => {
|
|
|
|
commonStore.hideLoading();
|
2018-01-16 16:05:12 -08:00
|
|
|
swal("Congratulations!", messages.VOTED_SUCCESS_MSG, "success").then((result) => {
|
2018-01-12 10:44:16 -08:00
|
|
|
push(`${commonStore.rootPath}`);
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.on("error", (e) => {
|
|
|
|
commonStore.hideLoading();
|
|
|
|
swal("Error!", e.message, "error");
|
|
|
|
});
|
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
finalize = async (e) => {
|
2018-01-15 06:21:10 -08:00
|
|
|
if (this.isFinalized) { return; }
|
2018-01-16 16:05:12 -08:00
|
|
|
|
|
|
|
if (this.timeToStart.val > 0) {
|
|
|
|
swal("Warning!", messages.ballotIsNotActiveMsg(this.timeTo.displayValue), "warning");
|
|
|
|
return;
|
|
|
|
}
|
2018-01-12 10:44:16 -08:00
|
|
|
const { commonStore, contractsStore, id, votingType } = this.props;
|
|
|
|
const { push } = this.props.routing;
|
|
|
|
if (!contractsStore.isValidVotingKey) {
|
2018-01-16 16:05:12 -08:00
|
|
|
swal("Warning!", messages.invalidVotingKeyMsg(contractsStore.votingKey), "warning");
|
2018-01-12 10:44:16 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (this.isFinalized) {
|
2018-01-16 16:05:12 -08:00
|
|
|
swal("Warning!", messages.ALREADY_FINALIZED_MSG, "warning");
|
2018-01-12 10:44:16 -08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
commonStore.showLoading();
|
|
|
|
let isActive = await this.isActive();
|
|
|
|
if (isActive) {
|
|
|
|
commonStore.hideLoading();
|
2018-01-16 16:05:12 -08:00
|
|
|
swal("Warning!", messages.INVALID_FINALIZE_MSG, "warning");
|
2018-01-12 10:44:16 -08:00
|
|
|
return;
|
|
|
|
}
|
2018-01-12 11:51:32 -08:00
|
|
|
this.getContract(contractsStore, votingType).finalize(id, contractsStore.votingKey)
|
2018-01-12 10:44:16 -08:00
|
|
|
.on("receipt", () => {
|
|
|
|
commonStore.hideLoading();
|
2018-01-16 16:05:12 -08:00
|
|
|
swal("Congratulations!", messages.FINALIZED_SUCCESS_MSG, "success").then((result) => {
|
2018-01-12 10:44:16 -08:00
|
|
|
push(`${commonStore.rootPath}`);
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.on("error", (e) => {
|
|
|
|
commonStore.hideLoading();
|
|
|
|
swal("Error!", e.message, "error");
|
|
|
|
});
|
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 11:51:32 -08:00
|
|
|
getContract(contractsStore, votingType) {
|
|
|
|
switch(votingType) {
|
|
|
|
case "votingToChangeKeys":
|
|
|
|
return contractsStore.votingToChangeKeys;
|
|
|
|
case "votingToChangeMinThreshold":
|
|
|
|
return contractsStore.votingToChangeMinThreshold;
|
|
|
|
case "votingToChangeProxy":
|
|
|
|
return contractsStore.votingToChangeProxy;
|
|
|
|
default:
|
|
|
|
return contractsStore.votingToChangeKeys;
|
2018-01-12 12:00:26 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getThreshold(contractsStore, votingType) {
|
|
|
|
switch(votingType) {
|
|
|
|
case "votingToChangeKeys":
|
|
|
|
return contractsStore.keysBallotThreshold;
|
|
|
|
case "votingToChangeMinThreshold":
|
|
|
|
return contractsStore.minThresholdBallotThreshold;
|
|
|
|
case "votingToChangeProxy":
|
|
|
|
return contractsStore.proxyBallotThreshold;
|
|
|
|
default:
|
|
|
|
return contractsStore.keysBallotThreshold;
|
2018-01-12 11:51:32 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
this.isFinalized = false;
|
|
|
|
this.getTimes();
|
|
|
|
this.getCreator();
|
|
|
|
this.getTotalVoters();
|
|
|
|
this.getProgress();
|
|
|
|
this.getIsFinalized();
|
|
|
|
}
|
2018-01-12 08:14:34 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
componentDidMount() {
|
|
|
|
this.interval = setInterval(() => {
|
2018-01-16 16:02:07 -08:00
|
|
|
this.calcTimeTo();
|
2018-01-12 11:09:04 -08:00
|
|
|
}, 1000);
|
2018-01-12 10:44:16 -08:00
|
|
|
}
|
2018-01-12 08:14:34 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
componentWillUnmount() {
|
|
|
|
window.clearInterval(this.interval);
|
|
|
|
}
|
2018-01-12 08:14:34 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
showCard = () => {
|
|
|
|
let { commonStore } = this.props;
|
|
|
|
let show = commonStore.isActiveFilter ? !this.isFinalized : true;
|
|
|
|
return show;
|
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
|
2018-01-12 10:44:16 -08:00
|
|
|
isCreatorPattern = () => {
|
|
|
|
let { commonStore } = this.props;
|
|
|
|
if (commonStore.searchTerm) {
|
|
|
|
if (commonStore.searchTerm.length > 0) {
|
2018-01-12 11:09:04 -08:00
|
|
|
const isCreatorPattern = String(this.creator).toLowerCase().includes(commonStore.searchTerm);
|
2018-01-12 10:44:16 -08:00
|
|
|
return isCreatorPattern;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
2018-01-11 10:49:08 -08:00
|
|
|
}
|
2018-01-12 10:44:16 -08:00
|
|
|
|
|
|
|
render () {
|
|
|
|
let { contractsStore, votingType, children, isSearchPattern } = this.props;
|
|
|
|
let ballotClass = (this.showCard() && (this.isCreatorPattern() || isSearchPattern)) ? "ballots-i" : "ballots-i display-none";
|
2018-01-12 12:00:26 -08:00
|
|
|
const threshold = this.getThreshold(contractsStore, votingType);
|
2018-01-12 10:44:16 -08:00
|
|
|
return (
|
|
|
|
<div className={ballotClass}>
|
|
|
|
<div className="ballots-about">
|
|
|
|
<div className="ballots-about-i ballots-about-i_name">
|
|
|
|
<div className="ballots-about-td">
|
|
|
|
<p className="ballots-about-i--title">Name</p>
|
|
|
|
</div>
|
|
|
|
<div className="ballots-about-td">
|
|
|
|
<p className="ballots-i--name">{this.creator}</p>
|
|
|
|
<p className="ballots-i--created">{this.startTime}</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{children}
|
|
|
|
<div className="ballots-about-i ballots-about-i_time">
|
|
|
|
<div className="ballots-about-td">
|
|
|
|
<p className="ballots-about-i--title">Time</p>
|
|
|
|
</div>
|
|
|
|
<div className="ballots-about-td">
|
2018-01-16 16:02:07 -08:00
|
|
|
<p className="ballots-i--time">{this.timeTo.displayValue}</p>
|
|
|
|
<p className="ballots-i--to-close">{this.timeTo.title}</p>
|
2018-01-12 10:44:16 -08:00
|
|
|
</div>
|
|
|
|
</div>
|
2018-01-11 10:49:08 -08:00
|
|
|
</div>
|
2018-01-12 10:44:16 -08:00
|
|
|
<div className="ballots-i-scale">
|
|
|
|
<div className="ballots-i-scale-column">
|
|
|
|
<button type="button" onClick={(e) => this.vote({choice: REJECT})} className="ballots-i--vote ballots-i--vote_no">No</button>
|
|
|
|
<div className="vote-scale--container">
|
|
|
|
<p className="vote-scale--value">No</p>
|
|
|
|
<p className="vote-scale--votes">Votes: {this.votesAgainstNumber}</p>
|
|
|
|
<p className="vote-scale--percentage">{this.votesAgainstPercents}%</p>
|
|
|
|
<div className="vote-scale">
|
|
|
|
<div className="vote-scale--fill vote-scale--fill_yes" style={{width: `${this.votesAgainstPercents}%`}}></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className="ballots-i-scale-column">
|
|
|
|
<div className="vote-scale--container">
|
|
|
|
<p className="vote-scale--value">Yes</p>
|
|
|
|
<p className="vote-scale--votes">Votes: {this.votesForNumber}</p>
|
|
|
|
<p className="vote-scale--percentage">{this.votesForPercents}%</p>
|
|
|
|
<div className="vote-scale">
|
|
|
|
<div className="vote-scale--fill vote-scale--fill_no" style={{width: `${this.votesForPercents}%`}}></div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<button type="button" onClick={(e) => this.vote({choice: ACCEPT})} className="ballots-i--vote ballots-i--vote_yes">Yes</button>
|
2018-01-11 10:49:08 -08:00
|
|
|
</div>
|
|
|
|
</div>
|
2018-01-12 10:44:16 -08:00
|
|
|
<div className="info">
|
|
|
|
Minimum {threshold} from {contractsStore.validatorsLength} validators is required to pass the proposal
|
|
|
|
</div>
|
|
|
|
<hr />
|
|
|
|
<div className="ballots-footer">
|
|
|
|
<div className="ballots-footer-left">
|
2018-01-15 05:47:34 -08:00
|
|
|
<button type="button" onClick={(e) => this.finalize(e)} className={this.finalizeButtonClass}>{this.finalizeButtonDisplayName}</button>
|
2018-01-12 10:44:16 -08:00
|
|
|
<p>{constants.CARD_FINALIZE_DESCRIPTION}</p>
|
2018-01-11 10:49:08 -08:00
|
|
|
</div>
|
2018-01-12 10:44:16 -08:00
|
|
|
<div type="button" className="ballots-i--vote ballots-i--vote_no">Proxy Ballot ID: {this.props.id}</div>
|
2018-01-11 10:49:08 -08:00
|
|
|
</div>
|
|
|
|
</div>
|
2018-01-12 10:44:16 -08:00
|
|
|
);
|
|
|
|
}
|
2018-01-11 10:49:08 -08:00
|
|
|
}
|