Adding new screens for voting flow

This commit is contained in:
Richard Ramos 2018-10-16 16:20:54 -04:00
parent 82124301b1
commit 0c2b5ac5e3
17 changed files with 834 additions and 348 deletions

View File

@ -1,15 +1,20 @@
import React, { Fragment, PureComponent } from 'react';
import React, { PureComponent } from 'react';
import CssBaseline from '@material-ui/core/CssBaseline';
import 'typeface-roboto';
import AppBar from './standard/AppBar';
import AddPoll from './simple-voting/AddPoll';
import PollsList from './simple-voting/PollsList';
import Collapse from '@material-ui/core/Collapse';
import Hidden from '@material-ui/core/Hidden';
import Typography from '@material-ui/core/Typography';
import LinearProgress from '@material-ui/core/LinearProgress';
import { VotingContext } from '../context';
import { Route, Switch } from "react-router-dom";
import TitleScreen from './flow/TitleScreen';
import LearnAboutBallots from './flow/LearnAboutBallots';
import HowVotingWorks from './flow/HowVotingWorks';
import ConnectYourWallet from './flow/ConnectYourWallet';
import OtherWallets from './flow/OtherWallets';
class Voting extends PureComponent {
state = { addPoll: false };
@ -23,18 +28,18 @@ class Voting extends PureComponent {
<CssBaseline />
<AppBar togglePoll={togglePoll} symbol={symbol} />
{loading && <LinearProgress />}
<div style={{ margin: '30px', textAlign: 'center' }}>
<img src="images/logo.png" width="200" />
<Hidden smUp>
<Typography variant="headline" color="inherit">
What should we build next?
</Typography>
</Hidden>
</div>
<Collapse in={addPoll}>
<AddPoll togglePoll={togglePoll} getPolls={getPolls} />
</Collapse>
{rawPolls && <PollsList rawPolls={rawPolls} />}
<div id="votingDapp">
<Switch>
<Route exact path="/" render={() => <TitleScreen polls={rawPolls} />} />
<Route path="/learn" component={LearnAboutBallots} />
<Route path="/votingHelp" render={HowVotingWorks} />
<Route path="/wallet" render={ConnectYourWallet} />
<Route path="/otherWallets" render={OtherWallets} />
</Switch>
</div>
</div>
}
</VotingContext.Consumer>
@ -42,4 +47,4 @@ class Voting extends PureComponent {
}
}
export default Voting
export default Voting;

View File

@ -0,0 +1,19 @@
import {Link} from "react-router-dom";
import Button from '@material-ui/core/Button';
import React from 'react';
import Typography from '@material-ui/core/Typography'
const ConnectYourWallet = (props) => <div className="section center">
<Typography variant="headline">Connect your wallet</Typography>
<Typography variant="body1">To start voting, connect to a wallet where you hold your SNT assets.</Typography>
<div className="action">
<Button color="primary" variant="contained">CONNECT USING STATUS</Button>
</div>
<div className="action">
<Link to="/otherWallets">
<Button color="primary">CONNECT WITH ANOTHER WALLET</Button>
</Link>
</div>
</div>;
export default ConnectYourWallet;

View File

@ -0,0 +1,43 @@
import {Link} from "react-router-dom";
import Button from '@material-ui/core/Button';
import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Typography from '@material-ui/core/Typography';
const HowVotingWorks = (props) => <div className="section">
<Typography variant="headline">How voting works</Typography>
<Card>
<CardContent>
<Typography gutterBottom component="h2">
Any wallet with SNT can vote
</Typography>
<Typography component="p">
When a poll is created a snapshot is taken of every wallet that holds Status Network Tokens (SNT).
</Typography>
</CardContent>
</Card>
<Card>
<CardContent>
<Typography gutterBottom component="h2">
You don't spend your SNT!
</Typography>
<Typography component="p">
Your wallet gets one voting credit for every SNT it holds. To cast your vote, you sign a transaction, but you only spend a small amount of ETH for the transaction fee.
</Typography>
</CardContent>
</Card>
<Card>
<CardContent>
<Typography gutterBottom component="h2">
Your vote counts
</Typography>
<Typography component="p">
Most votes when poll ends wins! Multiple votes cost more to prevent whales from controlling the vote
</Typography>
</CardContent>
</Card>
<Link to="/wallet"><Button>Connect with your wallet</Button></Link>
</div>;
export default HowVotingWorks;

View File

@ -0,0 +1,79 @@
import {Link} from "react-router-dom";
import Button from '@material-ui/core/Button';
import React, {Component} from 'react';
import Card from '@material-ui/core/Card';
import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent';
import Typography from '@material-ui/core/Typography';
import DialogTitle from '@material-ui/core/DialogTitle';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
class LearnAboutBallots extends Component {
state = {
open: false
};
handleClickOpen = () => {
this.setState({
open: true,
});
};
handleClose = value => {
this.setState({ open: false });
};
render(){
return (
<div>
<Typography variant="headline">What should Status Incubate invest in next?</Typography>
<BallotDialog
selectedValue={this.state.selectedValue}
open={this.state.open}
onClose={this.handleClose}
/>
<Card>
<CardContent>
<Typography gutterBottom component="h2">Pixura</Typography>
<Typography component="p">A protocol for digital asset ownership</Typography>
</CardContent>
<CardActions>
<Button size="small" color="primary" onClick={this.handleClickOpen}>Learn more &gt;</Button>
</CardActions>
</Card>
<Link to="/votingHelp"> <Button>How voting works</Button></Link>
</div>
);
}
}
class BallotDialog extends Component {
handleClose = () => {
this.props.onClose(this.props.selectedValue);
};
handleListItemClick = value => {
this.props.onClose(value);
};
render() {
const { onClose, selectedValue, ...other } = this.props;
return (
<Dialog onClose={this.handleClose} aria-labelledby="simple-dialog-title" {...other}>
<DialogTitle>Pixura</DialogTitle>
<DialogContent>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClose} color="primary" autoFocus>Ok</Button>
</DialogActions>
</Dialog>
);
}
}
export default LearnAboutBallots;

View File

@ -0,0 +1,47 @@
import {Link} from "react-router-dom";
import Button from '@material-ui/core/Button';
import React from 'react';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Typography from '@material-ui/core/Typography';
const OtherWallets = (props) => <div className="section">
<Typography variant="headline">Connect with another wallet</Typography>
<Typography variant="body1">Do you hold your SNT in another wallet? Don't worry, we've got you covered. You can also vote using the following wallets.</Typography>
<Card>
<CardContent>
<Typography gutterBottom component="h2">
Ledger or Metamask
</Typography>
<Typography component="p">
Text about sending the link to your email account and open it on desktop
<Button color="primary" variant="contained">
CALL TO ACTION
</Button>
</Typography>
</CardContent>
</Card>
<Card>
<CardContent>
<Typography gutterBottom component="h2">
Web3 wallet / browser
</Typography>
<Typography component="p">
Some explanation and CTA
</Typography>
</CardContent>
</Card>
<Card>
<CardContent>
<Typography gutterBottom component="h2">
Exchanges
</Typography>
<Typography component="p">
Unfortunately we cannot...
</Typography>
</CardContent>
</Card>
<Link to="/wallet"><Button variant="text">Back</Button></Link>
</div>
export default OtherWallets;

View File

@ -0,0 +1,137 @@
import {Link} from "react-router-dom";
import Button from '@material-ui/core/Button';
import React, {Component} from 'react';
import Typography from '@material-ui/core/Typography'
const pad = (n, width, z) => {
z = z || '0';
n = n + '';
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
}
class TitleScreen extends Component {
state = {
ipfsHash: '',
content: {},
time: {},
seconds: 100000
}
timer = 0
secondsToTime(secs){
let days = Math.floor(secs / 86400);
let divisor_for_hours = secs % 86400;
let hours = Math.floor(divisor_for_hours / (60 * 60));
let divisor_for_minutes = secs % (60 * 60);
let minutes = Math.floor(divisor_for_minutes / 60);
let obj = {
"d": days,
"h": hours,
"m": minutes
};
return obj;
}
startTimer() {
if (this.timer == 0 && this.state.seconds > 0) {
this.timer = setInterval(() => this.countDown(), 1000);
}
}
countDown() {
let seconds = this.state.seconds - 1;
this.setState({
time: this.secondsToTime(seconds),
seconds: seconds,
});
if (seconds == 0) {
clearInterval(this.timer);
}
}
componentWillUnmount(){
clearInterval(this.timer);
}
componentDidUpdate(prevProps) {
if (this.props.polls !== prevProps.polls) {
if(this.state.ipfsHash === ''){
const ipfsHash = web3.utils.toAscii(this.props.polls[0]._description);
this.setState({ipfsHash});
EmbarkJS.Storage.get(ipfsHash).then(text => {
this.setState({content: JSON.parse(text)});
});
}
const seconds = this.props.polls[0]._endTime - (new Date()).getTime() / 1000
if(seconds > 0){
let timeLeftVar = this.secondsToTime(seconds);
this.setState({ time: timeLeftVar, seconds });
this.startTimer();
} else {
this.setState({seconds});
}
}
}
render(){
const {time, content, seconds} = this.state;
const {title, description} = content;
const {polls} = this.props;
let canceled = true;
let startBlock, endTime;
if(polls && polls.length){
canceled = polls[0]._canceled;
startBlock = polls[0]._startBlock;
endTime = polls[0]._endTime;
}
return (polls && !canceled ? <div>
<div className="section">
<img src="images/logo.png" width="200" />
<Typography variant="headline">{title}</Typography>
<Typography variant="body1" component="div" dangerouslySetInnerHTML={{__html: description}}></Typography>
</div>
<hr />
{ seconds > 0 && <div className="votingTimer">
<Typography variant="body1">Voting ends in</Typography>
<ul>
<li>
<Typography variant="headline">{pad(time.d, 2)}</Typography>
<Typography variant="body1">Days</Typography>
</li>
<li>
<Typography variant="headline">{pad(time.h, 2)}</Typography>
<Typography variant="body1">Hours</Typography>
</li>
<li>
<Typography variant="headline">{pad(time.m, 2)}</Typography>
<Typography variant="body1">Mins</Typography>
</li>
</ul>
<div className="action">
<Link to="/learn"><Button variant="contained" color="primary">Get started</Button></Link>
</div>
</div>}
{ seconds < 0 && <div className="pollClosed">
<Typography variant="headline">Poll closed</Typography>
<Typography variant="body1">The vote was finished {parseInt(Math.abs(seconds) / 86400, 10)} day(s) ago</Typography>
<div className="action">
<Link to="/learn"><Button variant="contained" color="primary">View results</Button></Link>
</div>
</div> }
</div> : null);
}
}
export default TitleScreen;

View File

@ -49,29 +49,10 @@ const InnerForm = ({
<Card>
<CardContent>
<form onSubmit={handleSubmit} className={classes.form}>
<TextField
id="title"
label="Enter your proposal title"
className={classes.textField}
value={values.title}
onChange={handleChange}
margin="normal"
fullWidth
error={!!errors.title}
InputProps={{
classes: {
input: classes.textFieldInput
},
}}
InputLabelProps={{
className: classes.textFieldFormLabel
}}
helperText={errors.title}
/>
<TextField
id="ballots"
label="Enter the ballots array ([{&quot;title&quot;:&quot;&quot;, &quot;subtitle&quot;:&quot;&quot;, &quot;content&quot;:&quot;&quot;}]) (optional)"
label="Enter the poll details(title: &quot;&quot;description: &quot;&quot;, ballots: [{&quot;title&quot;:&quot;&quot;, &quot;subtitle&quot;:&quot;&quot;, &quot;content&quot;:&quot;&quot;}]) (optional)"
className={classes.textField}
value={values.ballots}
onChange={handleChange}
@ -144,22 +125,51 @@ const InnerForm = ({
const StyledForm = withStyles(styles)(InnerForm);
const AddPoll = withFormik({
mapPropsToValues: props => ({ title: '', ballots: '', startBlock: '', endTime: ''}),
mapPropsToValues: props => ({ ballots: JSON.stringify({
title: "What should we build next?",
description: `<p>Status Incubate exists to help early-stage startups reinvent the Web. Your vote help us decide where to invest.</p>
<p><a href="#">Learn more about Status Incubate</a>`,
ballots: [
{
title: "Option1",
subtitle: "Subtitle Option1",
content: "Text About Option1"
},
{
title: "Option2",
subtitle: "Subtitle Option2",
content: "Text About Option2"
},
{
title: "Option3",
subtitle: "Subtitle Option3",
content: "Text About Option3"
}
]
}), startBlock: '', endTime: ''}),
validate(values, props){
return web3.eth.getBlockNumber()
.then(currentBlock => {
const errors = {};
const { title, ballots, startBlock, endTime } = values;
let { ballots, startBlock, endTime } = values;
if(title.toString().trim() === "") {
errors.title = "Required";
}
let ballotOptions;
let pollDetails;
try {
ballotOptions = JSON.parse(ballots);
pollDetails = JSON.parse(ballots);
const details = Object.keys(pollDetails);
const validAttributes = ['title', 'description', 'ballots'];
if(details.filter(o1 => validAttributes.filter(o2 => o2 === o1).length === 0).length > 0){
errors.ballots = "Only 'description', 'ballots' are allowed" + (i+1);
}
if(pollDetails.title.toString().trim() == ""){
errors.ballots = "Title is required";
}
const ballotOptions = pollDetails.ballots;
if(!Array.isArray(ballotOptions)){
errors.ballots = "JSON must be an array of objects";
@ -181,6 +191,7 @@ const AddPoll = withFormik({
}
}
} catch(err){
console.log(err);
if(ballots.trim() !== "")
errors.ballots = "Invalid JSON";
}
@ -221,25 +232,26 @@ const AddPoll = withFormik({
},
async handleSubmit(values, { setSubmitting, setErrors, props, resetForm }) {
const { title, ballots, startBlock, endTime } = values;
const { ballots, startBlock, endTime } = values;
const { eth: { getBlockNumber }, utils: { toHex } } = window.web3;
const addPollCustomBlock = PollManager.methods["addPoll(uint256,uint256,bytes,uint8)"];
const addPollOnlyEndTime = PollManager.methods["addPoll(uint256,bytes,uint8)"];
let date = new Date();
const d90 = date.setDate(date.getDate() + 90).getTime() / 1000;
date.setDate(date.getDate() + 90);
const d90 = date.getTime() / 1000;
const endTime = endTime ? endTime : d90;
const endTime90 = parseInt(endTime ? endTime : d90);
const options = JSON.parse(ballots);
const ipfsHash = await EmbarkJS.Storage.saveText(ballots);
const encodedDesc = "0x" + rlp.encode([title, ipfsHash]).toString('hex');
const encodedDesc = toHex(ipfsHash);
let toSend;
if(startBlock){
toSend = addPollCustomBlock(startBlock, endTime, encodedDesc, options.length || 0);
toSend = addPollCustomBlock(startBlock, endTime90, encodedDesc, options.length || 0);
} else {
toSend = addPollOnlyEndTime(endTime, encodedDesc, options.length || 0);
toSend = addPollOnlyEndTime(endTime90, encodedDesc, options.length || 0);
}
setSubmitting(true);

View File

@ -189,6 +189,70 @@ class Poll extends PureComponent {
}
return (
<Fragment>
{title}
</Fragment>);
}
}
const PollsList = ({ classes }) => (
<VotingContext.Consumer>
{({ updatePoll, rawPolls, pollOrder, appendToPoll, ideaSites, symbol }) =>
<Fragment>
{rawPolls
.sort(sortingFn[pollOrder])
.map((poll, i) => !poll._canceled && <Poll key={poll.idPoll} classes={classes} appendToPoll={appendToPoll} updatePoll={updatePoll} symbol={symbol} ideaSites={ideaSites} {...poll} />)}
</Fragment>
}
</VotingContext.Consumer>
)
class BallotSlider extends Component {
constructor(props){
super(props);
this.state = {
value: props.votes || 0
}
}
handleChange = (event, value) => {
if(value > this.props.maxVotesAvailable){
value = this.props.maxVotesAvailable;
}
this.setState({value});
this.props.updateVotes(value);
};
render(){
const {maxVotes, maxVotesAvailable, classes, cantVote, balance, symbol} = this.props;
const {value} = this.state;
const nextVote = value + 1;
return <Fragment>
<Slider disabled={cantVote} classes={{ thumb: classes.thumb }} style={{ width: '95%' }} value={value} min={0} max={maxVotes} step={1} onChange={this.handleChange} />
{balance > 0 && !cantVote && <b>Your votes: {value} ({value * value} {symbol})</b>}
{ nextVote <= maxVotesAvailable && !cantVote ? <small>- Additional vote will cost {nextVote*nextVote - value*value} {symbol}</small> : (balance > 0 && !cantVote && <small>- Not enough balance available to buy additional votes</small>) }
</Fragment>
}
}
export default withStyles(styles)(PollsList);
/*
<Card>
<CardContent>
<Typography variant="title">{title}</Typography>
@ -256,51 +320,4 @@ class Poll extends PureComponent {
{isSubmitting ? <CircularProgress /> : <Button variant="contained" disabled={disableVote} color="primary" onClick={this.handleClick}>{buttonText}</Button>}
</CardActions>}
</Card>
)
}
}
const PollsList = ({ classes }) => (
<VotingContext.Consumer>
{({ updatePoll, rawPolls, pollOrder, appendToPoll, ideaSites, symbol }) =>
<Fragment>
{rawPolls
.sort(sortingFn[pollOrder])
.map((poll, i) => !poll._canceled && <Poll key={poll.idPoll} classes={classes} appendToPoll={appendToPoll} updatePoll={updatePoll} symbol={symbol} ideaSites={ideaSites} {...poll} />)}
</Fragment>
}
</VotingContext.Consumer>
)
class BallotSlider extends Component {
constructor(props){
super(props);
this.state = {
value: props.votes || 0
}
}
handleChange = (event, value) => {
if(value > this.props.maxVotesAvailable){
value = this.props.maxVotesAvailable;
}
this.setState({value});
this.props.updateVotes(value);
};
render(){
const {maxVotes, maxVotesAvailable, classes, cantVote, balance, symbol} = this.props;
const {value} = this.state;
const nextVote = value + 1;
return <Fragment>
<Slider disabled={cantVote} classes={{ thumb: classes.thumb }} style={{ width: '95%' }} value={value} min={0} max={maxVotes} step={1} onChange={this.handleChange} />
{balance > 0 && !cantVote && <b>Your votes: {value} ({value * value} {symbol})</b>}
{ nextVote <= maxVotesAvailable && !cantVote ? <small>- Additional vote will cost {nextVote*nextVote - value*value} {symbol}</small> : (balance > 0 && !cantVote && <small>- Not enough balance available to buy additional votes</small>) }
</Fragment>
}
}
export default withStyles(styles)(PollsList);
)*/

View File

@ -40,15 +40,9 @@ function ButtonAppBar(props) {
<AppBar position="static">
<Toolbar className={classes.toolBar}>
<Hidden mdDown>
<IconButton className={classes.menuButton} color="inherit" aria-label="Menu" onClick={toggleAdmin} />
</Hidden>
<Hidden smDown>
<Typography variant="display1" color="inherit" className={classes.flex}>
What should we build next?
</Typography>
<Button className={classes.menuButton} color="inherit" aria-label="Menu" onClick={toggleAdmin}>Admin</Button>
</Hidden>
{snt && <Button disabled={!hasSnt(snt)} variant="outlined" color="inherit" onClick={togglePoll}>{hasSnt(snt) ? 'Add Proposal' : 'Your account has no ' + symbol}</Button>}
<OrderingFilter />
</Toolbar>
</AppBar>
</div>

View File

51
app/css/styles.css Normal file
View File

@ -0,0 +1,51 @@
#votingDapp div.section {
padding: 20px;
}
#votingDapp hr {
border: 3px solid #edf2f5;
}
#votingDapp div.votingTimer {
padding: 20px;
text-align: center;
}
#votingDapp div.votingTimer h4 {
font-weight: normal;
}
#votingDapp div.votingTimer ul {
width: 150px;
margin: auto;
list-style: none;
padding: 0;
}
#votingDapp div.votingTimer ul li {
width: 50px;
float: left;
padding: 0;
margin: 0;
}
#votingDapp div.action {
display: block;
width: 100%;
float: left;
clear: both;
margin-top: 1em;
}
#votingDapp div.action a {
text-decoration: none;
}
#votingDapp div.pollClosed {
text-align: center;
padding-top: 2em;
}
#votingDapp div.center {
text-align: center;
}

View File

@ -10,6 +10,10 @@ import { VotingContext } from './context';
import Web3Render from './components/standard/Web3Render';
import fetchIdeas from './utils/fetchIdeas';
import { getPolls, omitPolls } from './utils/polls';
import { HashRouter as Router, Route, Link } from "react-router-dom";
window['Token'] = SNT;
import './dapp.css';
@ -52,7 +56,9 @@ class App extends React.Component {
this.setState({ loading: true })
const { nPolls, poll } = PollManager.methods;
const polls = await nPolls().call();
if (polls) getPolls(polls, poll).then(omitPolls).then(rawPolls => { this.setState({ rawPolls, loading: false })});
if (polls) getPolls(polls, poll).then(omitPolls).then(rawPolls => {
this.setState({ rawPolls, loading: false });
});
else this.setState({ rawPolls: [], loading: false });
}
@ -100,13 +106,13 @@ class App extends React.Component {
const votingContext = { getPolls: _getPolls, toggleAdmin, updatePoll, appendToPoll, setPollOrder, ...this.state };
return (
<Web3Render ready={web3Provider}>
<VotingContext.Provider value={votingContext}>
<Fragment>
{admin ?
<AdminView setAccount={this.setAccount} /> :
<Voting />}
</Fragment>
</VotingContext.Provider>
<Router>
<VotingContext.Provider value={votingContext}>
<Fragment>
{admin ? <AdminView setAccount={this.setAccount} /> : <Route path="/" component={Voting}/>}
</Fragment>
</VotingContext.Provider>
</Router>
</Web3Render>
);
}

View File

@ -2,6 +2,7 @@
<head>
<title>Status.im - Survey</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="css/styles.css">
</head>
<body>
<div id="app">

View File

@ -42,7 +42,7 @@ contract PollManager is Controlled {
/// @notice Create a Poll and enable it immediatly
/// @param _endTime Block where the poll ends
/// @param _description RLP encoded: [poll_title, [poll_ballots]]
/// @param _description IPFS hash with the description
/// @param _numBallots Number of ballots
function addPoll(
uint _endTime,
@ -58,7 +58,7 @@ contract PollManager is Controlled {
/// @notice Create a Poll
/// @param _startBlock Block where the poll starts
/// @param _endTime Block where the poll ends
/// @param _description RLP encoded: [poll_title, [poll_ballots]]
/// @param _description IPFS hash with the description
/// @param _numBallots Number of ballots
function addPoll(
uint _startBlock,
@ -84,12 +84,12 @@ contract PollManager is Controlled {
p.description = _description;
p.author = msg.sender;
emit PollCreated(_idPoll);
emit PollCreated(_idPoll);
}
/// @notice Update poll description (title or ballots) as long as it hasn't started
/// @param _idPoll Poll to update
/// @param _description RLP encoded: [poll_title, [poll_ballots]]
/// @param _description IPFS hash with the description
/// @param _numBallots Number of ballots
function updatePollDescription(
uint _idPoll,

View File

@ -2,6 +2,7 @@
"contracts": ["contracts/**"],
"app": {
"js/dapp.js": ["app/dapp.js"],
"css/styles.css": ["app/css/**"],
"index.html": "app/index.html",
"images/": ["app/images/**"]
},

532
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -33,15 +33,17 @@
"@material-ui/icons": "^3.0.0",
"@material-ui/lab": "^1.0.0-alpha.12",
"axios": "^0.18.0",
"lodash": "^4.17.10",
"bignumber.js": "^5.0.0",
"bootstrap": "^3.3.7",
"formik": "^0.11.11",
"jquery": "^3.3.1",
"lodash": "^4.17.10",
"react": "^16.4.2",
"react-blockies": "^1.3.0",
"react-bootstrap": "^0.32.1",
"react-dom": "^16.4.2",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"react-toggle": "^4.0.2",
"rlp": "^2.1.0",
"typeface-roboto": "0.0.54"