Merge pull request #1908 from 0xProject/feature/vote_index_page

Implement the vote index page
This commit is contained in:
Jacob Evans
2019-07-17 18:27:09 +10:00
committed by GitHub
15 changed files with 550 additions and 147 deletions

View File

@@ -23,6 +23,7 @@ export interface ButtonInterface {
hasIcon?: boolean | string;
isInline?: boolean;
padding?: string;
fontSize?: string;
href?: string;
type?: string;
target?: string;
@@ -78,7 +79,7 @@ const ButtonBase = styled.button<ButtonInterface>`
!props.isNoPadding && !props.isWithArrow && ((!!props.padding && props.padding) || '18px 30px')};
white-space: ${props => props.isWithArrow && 'nowrap'};
text-align: center;
font-size: ${props => (props.isWithArrow ? '20px' : '18px')};
font-size: ${props => (props.fontSize ? props.fontSize : props.isWithArrow ? '20px' : '18px')};
text-decoration: none;
cursor: pointer;
outline: none;

View File

@@ -1,6 +1,8 @@
import * as React from 'react';
import styled from 'styled-components';
import { opacify } from 'polished';
export interface WrapProps {
bgColor?: string;
id?: string;
@@ -27,7 +29,10 @@ export interface SectionProps extends WrapProps {
isFullWidth?: boolean;
isFlex?: boolean;
padding?: string;
margin?: string;
paddingMobile?: string;
hasBorder?: boolean;
hasHover?: boolean;
flexBreakpoint?: string;
maxWidth?: string;
bgColor?: 'dark' | 'light' | string;
@@ -91,12 +96,18 @@ export const WrapSticky = styled.div<WrapProps>`
const SectionBase = styled.section<SectionProps>`
width: ${props => !props.isFullWidth && 'calc(100% - 60px)'};
max-width: 1500px;
margin: 0 auto;
cursor: ${props => props.hasHover && 'pointer'};
border: ${props => props.hasBorder && `1px solid ${props.theme.lightBgColor}`};
margin: ${props => (props.margin ? props.margin : '0 auto')};
padding: ${props => props.isPadded && (props.padding || '120px 0')};
background-color: ${props => props.theme[`${props.bgColor}BgColor`] || props.bgColor};
position: relative;
overflow: ${props => !props.isFullWidth && 'hidden'};
&:hover {
background-color: ${props => props.hasHover && opacify(0.2, props.theme[`lightBgColor`])};
}
@media (max-width: 768px) {
padding: ${props => props.isPadded && (props.paddingMobile || '40px 0')};
}

View File

@@ -16,6 +16,7 @@ interface HeadingProps extends BaseTextInterface {
isFlex?: boolean;
isNoMargin?: boolean;
isMuted?: boolean | number;
isInline?: boolean;
marginBottom?: string;
color?: string;
}
@@ -30,7 +31,7 @@ interface ParagraphProps extends BaseTextInterface {
const StyledHeading = styled.h1<HeadingProps>`
max-width: ${props => props.maxWidth};
color: ${props => props.color || props.theme.textColor};
display: ${props => props.isFlex && `inline-flex`};
display: ${props => (props.isFlex ? `inline-flex` : props.isInline ? 'inline' : undefined)};
align-items: center;
justify-content: ${props => props.isFlex && `space-between`};
font-size: ${props =>

View File

@@ -26,6 +26,7 @@ import { Explore } from 'ts/pages/explore';
import { NextEcosystem } from 'ts/pages/ecosystem';
import { Extensions } from 'ts/pages/extensions';
import { Governance } from 'ts/pages/governance/governance';
import { VoteIndex } from 'ts/pages/governance/vote_index';
import { Next0xInstant } from 'ts/pages/instant';
import { NextLanding } from 'ts/pages/landing';
import { NextLaunchKit } from 'ts/pages/launch_kit';
@@ -120,7 +121,8 @@ render(
<Route exact={true} path={WebsitePaths.Instant} component={Next0xInstant as any} />
<Route exact={true} path={WebsitePaths.LaunchKit} component={NextLaunchKit as any} />
<Route exact={true} path={WebsitePaths.Ecosystem} component={NextEcosystem as any} />
<Route exact={true} path={WebsitePaths.Vote} component={Governance as any} />
<Route exact={true} path={`${WebsitePaths.Vote}/:zeip`} component={Governance as any} />
<Route exact={true} path={WebsitePaths.Vote} component={VoteIndex as any} />
<Route exact={true} path={WebsitePaths.Extensions} component={Extensions as any} />
<Route
exact={true}

View File

@@ -5,7 +5,7 @@ import * as React from 'react';
import { Paragraph } from 'ts/components/text';
interface Props {
deadline: number;
deadline: moment.Moment;
}
interface TimeStructure {
@@ -23,7 +23,7 @@ const now = moment();
export const Countdown: React.StatelessComponent<Props> = ({ deadline }) => {
const pstOffset = '-0800';
const time = moment(deadline, 'X').utcOffset(pstOffset);
const time = deadline.utcOffset(pstOffset);
const isPassed = time.isBefore(now);
const voteTextPrefix = isPassed ? `Voting ended: ` : `Vote ends: `;
const timeText = !isPassed ? `${getRelativeTime(time)}` : '';

View File

@@ -0,0 +1,143 @@
import { BigNumber } from '@0x/utils';
import * as moment from 'moment';
import { TallyInterface } from 'ts/types';
export interface ProposalLink {
text: string;
url: string;
}
export interface ProposalProperty {
title: string;
summary: string;
rating: number;
links: ProposalLink[];
}
export interface Proposal {
zeipId: number;
title: string;
summary: string;
url: string;
voteStartDate: moment.Moment;
voteEndDate: moment.Moment;
benefit: ProposalProperty;
risks: ProposalProperty;
}
export interface Proposals {
[id: number]: Proposal;
}
export const proposals: Proposals = {
23: {
zeipId: 23,
title: 'Trade Bundles of Assets',
summary: `This ZEIP introduces the MultiAssetProxy, which adds support for trading arbitrary bundles of assets to 0x protocol. Historically, only a single asset could be traded per each side of a trade. With the introduction of the MultiAssetProxy, users will be able to trade multiple ERC721 assets or even mix ERC721 and ERC20 assets within a single order.`,
url: 'https://blog.0xproject.com/zeip-23-trade-bundles-of-assets-fe69eb3ed960',
voteStartDate: moment(1551042800, 'X'),
voteEndDate: moment(1551142800, 'X'),
benefit: {
title: 'Benefit',
summary: `Supporting trades for bundles of assets has been one of the most commonly requested features since the launch of 0x v2. The idea for this feature originated from discussions with gaming and NFT related projects. However, this upgrade also provides utility to relayers for prediction markets or baskets of tokens. The MultiAssetProxy will enable brand new ways of trading.`,
rating: 3,
links: [
{
text: 'Technical detail',
url: 'https://github.com/0xProject/ZEIPs/issues/23',
},
],
},
risks: {
title: 'Risk',
summary: `While the MultiAssetProxys code is relatively straightforward and has successfully undergone a full third-party audit, a bug within the code could result in the loss of user funds. Deploying the MultiAssetProxy is a hot upgrade that requires modifying the state of existing contracts within 0x protocol. The contracts being modified contain allowances to many users tokens. We encourage the community to verify the code, as well as the state changes.`,
rating: 2,
links: [
{
text: 'View Code',
url:
'https://github.com/0xProject/0x-monorepo/blob/development/contracts/asset-proxy/contracts/src/MultiAssetProxy.sol#L25',
},
{
text: 'View Audit',
url: 'https://github.com/ConsenSys/0x-audit-report-2018-12',
},
],
},
},
39: {
zeipId: 39,
title: 'StaticCallAssetProxy',
summary: `This ZEIP introduces the ability to validate stateful assets during settlement, ensuring the asset has not been modified.`,
url: '',
voteStartDate: moment(1563814800, 'X'),
voteEndDate: moment(1564419600, 'X'),
benefit: {
title: 'Benefit',
summary: `Stateful assets can be traded safely on 0x without the risk of front running attacks which can de-value the underlying asset. An asset is guaranteed by the 0x protocol to contain the same state as described in the order during settlement. `,
rating: 3,
links: [
{
text: 'Technical detail',
url: 'https://github.com/0xProject/ZEIPs/issues/39',
},
],
},
risks: {
title: 'Risk',
summary: `There is no risk in deploying ZEIP-39 as it has no ability to move assets from a users account.`,
rating: 1,
links: [
{
text: 'View Code',
url: '',
},
{
text: 'View Audit',
url: '',
},
],
},
},
24: {
zeipId: 24,
title: 'Support ERC-1155 MultiToken Standard',
summary: `This ZEIP introduces the ERC-1155 Asset Proxy, which adds support for trading ERC-1155 assets to 0x protocol. ERC-1155 is an evolution in token standards allowing mixed fungible and non-fungible assets within the same contract, enabling greater efficiency in the transfer and creation of new token concepts.`,
url: 'https://github.com/0xProject/ZEIPs/issues/24',
voteStartDate: moment(1563814800, 'X'),
voteEndDate: moment(1564419600, 'X'),
benefit: {
title: 'Benefit',
summary: `0x is designed to support numerous assets on the Ethereum blockchain. Adding support for the ERC1155 proxy enables new and more efficient types of trading such as batch transfers, shared deposit contracts and new types of tokens.`,
rating: 3,
links: [
{
text: 'Technical detail',
url: 'https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md',
},
],
},
risks: {
title: 'Risk',
summary: `The ERC1155 AssetProxys code is relatively straightforward and has successfully undergone a full third-party audit. Any bug within the ERC1155 Asset Proxy is minimised to only ERC1155 assets.`,
rating: 1,
links: [
{
text: 'View Code',
url:
'https://github.com/0xProject/0x-monorepo/blob/development/contracts/asset-proxy/contracts/src/ERC1155Proxy.sol#L24',
},
{
text: 'View Audit',
url: 'https://github.com/ConsenSys/0x-audit-report-2019-05',
},
],
},
},
};
export const ZERO_TALLY: TallyInterface = {
yes: new BigNumber(0),
no: new BigNumber(0),
};

View File

@@ -1,6 +1,7 @@
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import * as React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import styled from 'styled-components';
import { Banner } from 'ts/components/banner';
@@ -11,11 +12,13 @@ import { Column, FlexWrap, Section } from 'ts/components/newLayout';
import { SiteWrap } from 'ts/components/siteWrap';
import { Heading, Paragraph } from 'ts/components/text';
import { Countdown } from 'ts/pages/governance/countdown';
import { Proposal, proposals } from 'ts/pages/governance/data';
import { ModalVote } from 'ts/pages/governance/modal_vote';
import { RatingBar } from 'ts/pages/governance/rating_bar';
import { VoteInfo, VoteValue } from 'ts/pages/governance/vote_form';
import { VoteStats } from 'ts/pages/governance/vote_stats';
import { colors } from 'ts/style/colors';
import { TallyInterface } from 'ts/types';
import { configs } from 'ts/utils/configs';
import { documentConstants } from 'ts/utils/document_meta_constants';
import { utils } from 'ts/utils/utils';
@@ -24,15 +27,6 @@ interface LabelInterface {
[key: number]: string;
}
export interface TallyInterface {
zeip?: string;
yes?: BigNumber;
no?: BigNumber;
blockNumber?: string;
totalVotes?: string;
totalBalance?: BigNumber;
}
interface State {
isContactModalOpen: boolean;
isVoteModalOpen: boolean;
@@ -54,54 +48,20 @@ const riskLabels: LabelInterface = {
3: 'High Risk',
};
const proposalData = {
zeipId: 23,
title: 'ZEIP-23: Trade Bundles of Assets',
summary: `This ZEIP introduces the MultiAssetProxy, which adds support for trading arbitrary bundles of assets to 0x protocol. Historically, only a single asset could be traded per each side of a trade. With the introduction of the MultiAssetProxy, users will be able to trade multiple ERC721 assets or even mix ERC721 and ERC20 assets within a single order.`,
url: 'https://blog.0xproject.com/zeip-23-trade-bundles-of-assets-fe69eb3ed960',
votingDeadline: 1551142800,
benefit: {
title: 'Benefit',
summary: `Supporting trades for bundles of assets has been one of the most commonly requested features since the launch of 0x v2. The idea for this feature originated from discussions with gaming and NFT related projects. However, this upgrade also provides utility to relayers for prediction markets or baskets of tokens. The MultiAssetProxy will enable brand new ways of trading.`,
rating: 3,
links: [
{
text: 'Technical detail',
url: 'https://github.com/0xProject/ZEIPs/issues/23',
},
],
},
risks: {
title: 'Risk',
summary: `While the MultiAssetProxys code is relatively straightforward and has successfully undergone a full third-party audit, a bug within the code could result in the loss of user funds. Deploying the MultiAssetProxy is a hot upgrade that requires modifying the state of existing contracts within 0x protocol. The contracts being modified contain allowances to many users tokens. We encourage the community to verify the code, as well as the state changes.`,
rating: 2,
links: [
{
text: 'View Code',
url:
'https://github.com/0xProject/0x-monorepo/blob/development/contracts/asset-proxy/contracts/src/MultiAssetProxy.sol#L25',
},
{
text: 'View Audit',
url: 'https://github.com/ConsenSys/0x-audit-report-2018-12',
},
],
},
};
export class Governance extends React.Component {
export class Governance extends React.Component<RouteComponentProps<any>> {
public state: State = {
isContactModalOpen: false,
isVoteModalOpen: false,
isWalletConnected: false,
isVoteReceived: false,
providerName: 'Metamask',
tally: {
totalBalance: new BigNumber(0),
yes: new BigNumber(0),
no: new BigNumber(0),
},
};
private readonly _proposalData: Proposal;
constructor(props: RouteComponentProps<any>) {
super(props);
const zeipId = parseInt(props.match.params.zeip.split('-')[1], 10);
this._proposalData = proposals[zeipId];
}
public componentDidMount(): void {
// tslint:disable:no-floating-promises
this._fetchVoteStatusAsync();
@@ -113,12 +73,12 @@ export class Governance extends React.Component {
<DocumentTitle {...documentConstants.VOTE} />
<Section maxWidth="1170px" isFlex={true}>
<Column width="55%" maxWidth="560px">
<Countdown deadline={proposalData.votingDeadline} />
<Heading size="medium">{proposalData.title}</Heading>
<Paragraph>{proposalData.summary}</Paragraph>
<Countdown deadline={this._proposalData.voteEndDate} />
<Heading size="medium">{this._proposalData.title}</Heading>
<Paragraph>{this._proposalData.summary}</Paragraph>
<Button
href={proposalData.url}
target={proposalData.url !== undefined ? '_blank' : undefined}
href={this._proposalData.url}
target={this._proposalData.url !== undefined ? '_blank' : undefined}
isWithArrow={true}
isAccentColor={true}
>
@@ -135,11 +95,11 @@ export class Governance extends React.Component {
<Section bgColor="dark" maxWidth="1170px">
<SectionWrap>
<Heading>{proposalData.benefit.title}</Heading>
<Heading>{this._proposalData.benefit.title}</Heading>
<FlexWrap>
<Column width="55%" maxWidth="560px">
<Paragraph>{proposalData.benefit.summary}</Paragraph>
{_.map(proposalData.benefit.links, (link, index) => (
<Paragraph>{this._proposalData.benefit.summary}</Paragraph>
{_.map(this._proposalData.benefit.links, (link, index) => (
<MoreLink
href={link.url}
target={link.url !== undefined ? '_blank' : undefined}
@@ -155,17 +115,17 @@ export class Governance extends React.Component {
<RatingBar
color={colors.brandLight}
labels={benefitLabels}
rating={proposalData.benefit.rating}
rating={this._proposalData.benefit.rating}
/>
</Column>
</FlexWrap>
</SectionWrap>
<SectionWrap>
<Heading>{proposalData.risks.title}</Heading>
<Heading>{this._proposalData.risks.title}</Heading>
<FlexWrap>
<Column width="55%" maxWidth="560px">
<Paragraph>{proposalData.risks.summary}</Paragraph>
{_.map(proposalData.risks.links, (link, index) => (
<Paragraph>{this._proposalData.risks.summary}</Paragraph>
{_.map(this._proposalData.risks.links, (link, index) => (
<MoreLink
href={link.url}
target={link.url !== undefined ? '_blank' : undefined}
@@ -178,20 +138,25 @@ export class Governance extends React.Component {
))}
</Column>
<Column width="30%" maxWidth="360px">
<RatingBar color="#AE5400" labels={riskLabels} rating={proposalData.risks.rating} />
<RatingBar
color="#AE5400"
labels={riskLabels}
rating={this._proposalData.risks.rating}
/>
</Column>
</FlexWrap>
</SectionWrap>
</Section>
<Banner
heading="Vote with ZRX on ZEIP-23"
heading={`Vote with ZRX on ZEIP-${this._proposalData.zeipId}`}
subline="Use 0x Instant to quickly purchase ZRX for voting"
mainCta={{ text: 'Get ZRX', onClick: this._onLaunchInstantClick.bind(this) }}
secondaryCta={{ text: 'Vote', onClick: this._onOpenVoteModal.bind(this) }}
/>
<ModalContact isOpen={this.state.isContactModalOpen} onDismiss={this._onDismissContactModal} />
<ModalVote
zeipId={this._proposalData.zeipId}
isOpen={this.state.isVoteModalOpen}
onDismiss={this._onDismissVoteModal}
onWalletConnected={this._onWalletConnected.bind(this)}
@@ -238,14 +203,14 @@ export class Governance extends React.Component {
tally.no = tally.no.plus(userBalance);
}
tally.totalBalance = tally.yes.plus(tally.no);
this.setState({ ...this.state, isVoteReceived: true, tally });
};
private async _fetchVoteStatusAsync(): Promise<void> {
try {
const voteDomain = utils.isProduction() ? `https://${configs.DOMAIN_VOTE}` : 'http://localhost:3000';
const voteEndpoint = `${voteDomain}/v1/tally/${proposalData.zeipId}`;
const voteDomain = utils.isProduction()
? `https://${configs.DOMAIN_VOTE}`
: `https://${configs.DOMAIN_VOTE}/staging`;
const voteEndpoint = `${voteDomain}/v1/tally/${this._proposalData.zeipId}`;
const response = await fetch(voteEndpoint, {
method: 'get',
mode: 'cors',

View File

@@ -24,6 +24,7 @@ interface Props {
onDismiss?: () => void;
onWalletConnected?: (providerName: string) => void;
onVoted?: (voteInfo: VoteInfo) => void;
zeipId: number;
}
interface State {
@@ -38,7 +39,6 @@ interface State {
isU2fSupported: boolean;
isVoted: boolean;
votePreference: string | null;
zeip: number;
voteHash?: string;
signedVote?: any;
errorMessage?: string;
@@ -76,7 +76,6 @@ export class ModalVote extends React.Component<Props> {
isSuccessful: false,
isVoted: false,
votePreference: null,
zeip: 23,
errors: {},
};
// shared fields
@@ -84,7 +83,7 @@ export class ModalVote extends React.Component<Props> {
super(props);
}
public render(): React.ReactNode {
const { isOpen, onDismiss } = this.props;
const { isOpen, onDismiss, zeipId } = this.props;
const { isSuccessful, selectedAddress, currentBalance, isErrorModalOpen, errorMessage } = this.state;
const bigNumberFormat = {
decimalSeparator: '.',
@@ -109,7 +108,7 @@ export class ModalVote extends React.Component<Props> {
<StyledDialogContent>
{this._renderFormContent()}
<Confirmation isSuccessful={isSuccessful}>
<Icon name="zeip-23" size="large" margin={[0, 0, 'default', 0]} />
<Icon name={`zeip-${zeipId}`} size="large" margin={[0, 0, 'default', 0]} />
<Heading color={colors.textDarkPrimary} size={34} asElement="h2">
Vote Received!
</Heading>
@@ -153,7 +152,8 @@ export class ModalVote extends React.Component<Props> {
}
}
private _shareViaTwitterAsync(): void {
const tweetText = encodeURIComponent(`I voted on ZEIP-23! 🗳️#VoteWithZRX https://0x.org/vote`);
const { zeipId } = this.props;
const tweetText = encodeURIComponent(`I voted on ZEIP-${zeipId}! 🗳️#VoteWithZRX https://0x.org/vote`);
window.open(`https://twitter.com/intent/tweet?text=${tweetText}`, 'Share your vote', 'width=500,height=400');
}
private _renderConnectWalletFormContent(): React.ReactNode {
@@ -190,6 +190,7 @@ export class ModalVote extends React.Component<Props> {
isLedger={isLedger}
ledgerSubproviderIfExists={ledgerSubproviderIfExists}
provider={providerEngine}
zeipId={this.props.zeipId}
onVoted={this._onVoted.bind(this)}
onError={this._onError.bind(this)}
/>

View File

@@ -45,6 +45,7 @@ interface Props {
injectedProvider?: InjectedProvider;
ledgerSubproviderIfExists?: LedgerSubprovider;
provider?: ZeroExProvider;
zeipId: number;
}
interface State {
@@ -55,7 +56,6 @@ interface State {
isVoted: boolean;
selectedAddress?: string;
votePreference?: string;
zeip: number;
voteHash?: string;
signedVote?: SignedVote;
comment?: string;
@@ -79,8 +79,6 @@ interface ErrorProps {
[key: string]: string;
}
const defaultZeip = 23;
// This is a copy of the generic form and includes a number of extra fields
// TODO remove the extraneous fields
export class VoteForm extends React.Component<Props> {
@@ -91,7 +89,6 @@ export class VoteForm extends React.Component<Props> {
isSuccessful: false,
isLedger: false,
isVoted: false,
zeip: defaultZeip,
errors: {},
};
public networkId: number;
@@ -102,7 +99,6 @@ export class VoteForm extends React.Component<Props> {
isSuccessful: false,
isVoted: false,
votePreference: null,
zeip: VoteForm.defaultProps.zeip,
errors: {},
};
// shared fields
@@ -112,7 +108,7 @@ export class VoteForm extends React.Component<Props> {
}
public render(): React.ReactNode {
const { votePreference, errors, isSuccessful, isAwaitingLedgerSignature } = this.state;
const { currentBalance, selectedAddress } = this.props;
const { currentBalance, selectedAddress, zeipId } = this.props;
const bigNumberFormat = {
decimalSeparator: '.',
groupSeparator: ',',
@@ -129,7 +125,7 @@ export class VoteForm extends React.Component<Props> {
return (
<Form onSubmit={this._createAndSubmitVoteAsync.bind(this)} isSuccessful={isSuccessful}>
<Heading color={colors.textDarkPrimary} size={34} asElement="h2">
ZEIP-23 Vote
ZEIP-{zeipId} Vote
</Heading>
<Paragraph isMuted={true} color={colors.textDarkPrimary}>
Make sure you are informed to the best of your ability before casting your vote. It will have
@@ -191,8 +187,8 @@ export class VoteForm extends React.Component<Props> {
private readonly _createAndSubmitVoteAsync = async (e: React.FormEvent): Promise<void> => {
e.preventDefault();
const { zeip, votePreference, comment } = this.state;
const { currentBalance, selectedAddress, isLedger } = this.props;
const { votePreference, comment } = this.state;
const { currentBalance, selectedAddress, isLedger, zeipId } = this.props;
const makerAddress = selectedAddress;
if (isLedger) {
@@ -209,7 +205,7 @@ export class VoteForm extends React.Component<Props> {
name: '0x Protocol Governance',
};
const message = {
zeip,
zeip: zeipId,
preference: votePreference,
from: makerAddress,
};
@@ -236,7 +232,9 @@ export class VoteForm extends React.Component<Props> {
isAwaitingLedgerSignature: false,
}));
const voteDomain = utils.isProduction() ? `https://${configs.DOMAIN_VOTE}` : 'http://localhost:3000';
const voteDomain = utils.isProduction()
? `https://${configs.DOMAIN_VOTE}`
: `https://${configs.DOMAIN_VOTE}/staging`;
const voteEndpoint = `${voteDomain}/v1/vote`;
const requestBody = { ...signedVote, comment };
const response = await fetch(voteEndpoint, {

View File

@@ -0,0 +1,113 @@
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import * as React from 'react';
import styled from 'styled-components';
import { DocumentTitle } from 'ts/components/document_title';
import { Column, Section } from 'ts/components/newLayout';
import { SiteWrap } from 'ts/components/siteWrap';
import { Heading, Paragraph } from 'ts/components/text';
import { Proposal, proposals } from 'ts/pages/governance/data';
import { VoteIndexCard } from 'ts/pages/governance/vote_index_card';
import { TallyInterface } from 'ts/types';
import { configs } from 'ts/utils/configs';
import { documentConstants } from 'ts/utils/document_meta_constants';
import { utils } from 'ts/utils/utils';
const ZEIP_IDS = Object.keys(proposals).map(idString => parseInt(idString, 10));
const ZEIP_PROPOSALS: Proposal[] = ZEIP_IDS.map(id => proposals[id]).sort(
(a, b) => b.voteStartDate.unix() - a.voteStartDate.unix(),
);
export interface VoteIndexProps {}
interface ZeipTallyMap {
[id: number]: TallyInterface;
}
export interface VoteIndexState {
tallys?: ZeipTallyMap;
}
export class VoteIndex extends React.Component<VoteIndexProps, VoteIndexState> {
public state: VoteIndexState = {
tallys: undefined,
};
public componentDidMount(): void {
// tslint:disable:no-floating-promises
this._fetchTallysAsync();
}
public render(): React.ReactNode {
return (
<SiteWrap>
<DocumentTitle {...documentConstants.VOTE} />
<Section isTextCentered={true} isPadded={true} padding="150px 0px 110px">
<Column>
<Heading size="medium" isCentered={true}>
0x Protocol Governance
</Heading>
<Paragraph size="medium" isCentered={true} isMuted={true} marginBottom="0">
Vote on 0x Improvement Proposals (ZEIPs) using ZRX tokens
</Paragraph>
</Column>
</Section>
<VoteIndexCardWrapper>
{ZEIP_PROPOSALS.map(proposal => {
const tally = this.state.tallys && this.state.tallys[proposal.zeipId];
return <VoteIndexCard key={proposal.zeipId} tally={tally} {...proposal} />;
})}
</VoteIndexCardWrapper>
</SiteWrap>
);
}
private async _fetchVoteStatusAsync(zeipId: number): Promise<TallyInterface> {
try {
const voteDomain = utils.isProduction()
? `https://${configs.DOMAIN_VOTE}`
: `https://${configs.DOMAIN_VOTE}/staging`;
const voteEndpoint = `${voteDomain}/v1/tally/${zeipId}`;
const response = await fetch(voteEndpoint, {
method: 'get',
mode: 'cors',
credentials: 'same-origin',
headers: {
'content-type': 'application/json; charset=utf-8',
},
});
if (!response.ok) {
throw new Error('Request failed');
}
const responseData = await response.json();
let { no, yes } = responseData;
yes = new BigNumber(yes);
no = new BigNumber(no);
const tally = {
...responseData,
yes: new BigNumber(yes),
no: new BigNumber(no),
};
return tally;
} catch (e) {
// Empty block
return {
yes: new BigNumber(0),
no: new BigNumber(0),
};
}
}
private async _fetchTallysAsync(): Promise<void> {
const tallyResponses = await Promise.all(ZEIP_IDS.map(async zeipId => this._fetchVoteStatusAsync(zeipId)));
const tallys: { [key: number]: TallyInterface } = {};
ZEIP_IDS.forEach((zeipId, i) => (tallys[zeipId] = tallyResponses[i]));
this.setState({ tallys });
}
}
const VoteIndexCardWrapper = styled.div`
margin-bottom: 150px;
`;

View File

@@ -0,0 +1,106 @@
import * as _ from 'lodash';
import * as moment from 'moment';
import * as React from 'react';
import { Link as ReactRouterLink } from 'react-router-dom';
import styled from 'styled-components';
import { Column, FlexWrap, Section } from 'ts/components/newLayout';
import { Heading, Paragraph } from 'ts/components/text';
import { getTotalBalancesString } from 'ts/pages/governance/vote_stats';
import { VoteStatusText } from 'ts/pages/governance/vote_status_text';
import { TallyInterface, VoteOutcome, VoteTime } from 'ts/types';
export interface VoteIndexCardProps {
title: string;
zeipId: number;
summary: string;
voteStartDate: moment.Moment;
voteEndDate: moment.Moment;
// Non-static properties
tally?: TallyInterface;
}
const getVoteTime = (voteStartDate: moment.Moment, voteEndDate: moment.Moment): VoteTime | undefined => {
const now = moment();
if (now.isBefore(voteEndDate) && now.isAfter(voteStartDate)) {
return 'happening';
}
if (now.isBefore(voteStartDate)) {
return 'upcoming';
}
return undefined;
};
const getVoteOutcome = (tally?: TallyInterface): VoteOutcome | undefined => {
if (!tally) {
return undefined;
}
if (tally.no.isGreaterThanOrEqualTo(tally.yes)) {
return 'rejected';
} else if (tally.yes.isGreaterThan(tally.no)) {
return 'accepted';
}
return undefined;
};
const getDateString = (voteStartDate: moment.Moment, voteEndDate: moment.Moment): string => {
const voteTime = getVoteTime(voteStartDate, voteEndDate);
const pstOffset = '-0800';
const endDate = voteEndDate.utcOffset(pstOffset);
const startDate = voteStartDate.utcOffset(pstOffset);
if (voteTime === 'happening') {
return `Ends ${endDate.format('MMMM Do YYYY, h:mm a')} PST`;
}
if (voteTime === 'upcoming') {
return `Starting ${startDate.format('MMMM Do YYYY, h:mm a')} PST`;
}
return `Ended ${endDate.format('MMMM Do YYYY')}`;
};
export const VoteIndexCard: React.StatelessComponent<VoteIndexCardProps> = ({
title,
zeipId,
summary,
voteStartDate,
voteEndDate,
tally,
}) => {
const voteTime = getVoteTime(voteStartDate, voteEndDate);
const voteStatus = voteTime || getVoteOutcome(tally);
const totalBalances = getTotalBalancesString(tally);
const isPastProposal = voteTime === undefined;
return (
<ReactRouterLink to={`vote/zeip-${zeipId}`}>
<Section
hasBorder={isPastProposal}
bgColor={!isPastProposal ? 'dark' : 'none'}
padding="60px 30px 40px"
hasHover={true}
margin="30px auto"
maxWidth="100%"
>
<FlexWrap>
<Column width="60%" padding="0px 20px 0px 0px">
<Heading>
{`${title} `}
<Muted>{`(ZEIP-${zeipId})`}</Muted>
</Heading>
<Paragraph>{summary}</Paragraph>
</Column>
<Column>
<div className="flex flex-column items-end">
<VoteStatusText status={voteStatus} />
<Paragraph marginBottom="12px" isMuted={1}>{`${totalBalances} ZRX Total Vote`}</Paragraph>
<Paragraph marginBottom="12px">{getDateString(voteStartDate, voteEndDate)}</Paragraph>
</div>
</Column>
</FlexWrap>
</Section>
</ReactRouterLink>
);
};
const Muted = styled.span`
opacity: 0.6;
`;

View File

@@ -3,16 +3,17 @@ import { Web3Wrapper } from '@0x/web3-wrapper';
import * as React from 'react';
import { Heading, Paragraph } from 'ts/components/text';
import { TallyInterface } from 'ts/pages/governance/governance';
import { ZERO_TALLY } from 'ts/pages/governance/data';
import { VoteBar } from 'ts/pages/governance/vote_bar';
import { colors } from 'ts/style/colors';
import { TallyInterface } from 'ts/types';
import { constants } from 'ts/utils/constants';
interface VoteStatsProps {
export interface VoteStatsProps {
tally?: TallyInterface;
}
export const VoteStats: React.StatelessComponent<VoteStatsProps> = ({ tally }) => {
export const getTotalBalancesString = (tally: TallyInterface = ZERO_TALLY): string => {
const bigNumberFormat = {
decimalSeparator: '.',
groupSeparator: ',',
@@ -21,15 +22,23 @@ export const VoteStats: React.StatelessComponent<VoteStatsProps> = ({ tally }) =
fractionGroupSeparator: ' ',
fractionGroupSize: 0,
};
const { yes, totalBalance } = tally;
const HUNDRED = new BigNumber(100);
const { yes, no } = tally;
const totalBalance = yes.plus(no);
const totalBalanceString = Web3Wrapper.toUnitAmount(totalBalance, constants.DECIMAL_PLACES_ETH).toFormat(
0,
BigNumber.ROUND_FLOOR,
bigNumberFormat,
);
let yesPercentage = HUNDRED.times(yes.dividedBy(totalBalance));
let noPercentage = HUNDRED.minus(yesPercentage);
return totalBalanceString;
};
export const VoteStats: React.StatelessComponent<VoteStatsProps> = ({ tally }) => {
const { yes, no } = tally;
const totalBalance = yes.plus(no);
const oneHundred = new BigNumber(100);
const totalBalanceString = getTotalBalancesString(tally);
let yesPercentage = oneHundred.times(yes.dividedBy(totalBalance));
let noPercentage = oneHundred.minus(yesPercentage);
if (isNaN(yesPercentage.toNumber())) {
yesPercentage = new BigNumber(0);
@@ -50,3 +59,7 @@ export const VoteStats: React.StatelessComponent<VoteStatsProps> = ({ tally }) =
</>
);
};
VoteStats.defaultProps = {
tally: ZERO_TALLY,
};

View File

@@ -0,0 +1,88 @@
import * as React from 'react';
import styled from 'styled-components';
import { Button } from 'ts/components/button';
import { VoteStatus } from 'ts/types';
const checkColor = '#00AE99';
const renderCheck = (width: number = 18) => (
<svg width={width} viewBox="0 0 18 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 1L6 12L1 7" stroke={checkColor} strokeWidth="1.4" />
</svg>
);
const clockColor = '#EECE29';
const renderClock = (width: number = 18) => (
<svg width={width} viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="9" cy="9" r="8" stroke={clockColor} strokeWidth="0.75" />
<line x1="8.08838" y1="4.76465" x2="8.08838" y2="10.4117" stroke={clockColor} />
<line x1="13.2354" y1="9.9707" x2="7.58829" y2="9.9707" stroke={clockColor} />
</svg>
);
const crossColor = '#D34F4F';
const renderCross = (width: number = 14) => (
<svg width={width} viewBox="0 0 13 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.50551 6.99961L0 12.5051L0.989949 13.4951L6.49546 7.98956L12.001 13.4951L12.9909 12.5051L7.48541 6.99961L12.99 1.49508L12 0.505127L6.49546 6.00966L0.990926 0.505127L0.0009767 1.49508L5.50551 6.99961Z"
fill={crossColor}
/>
</svg>
);
export interface VoteStatusTextProps {
status: VoteStatus;
}
export const VoteStatusText: React.StatelessComponent<VoteStatusTextProps> = ({ status }) => {
switch (status) {
case 'upcoming':
return (
<VoteStatusTextBase color={clockColor}>
<span>{renderClock()}</span>
Upcoming
</VoteStatusTextBase>
);
case 'accepted':
return (
<VoteStatusTextBase color={checkColor}>
<span>{renderCheck()}</span>
Accepted
</VoteStatusTextBase>
);
case 'rejected':
return (
<VoteStatusTextBase color={crossColor}>
<span>{renderCross()}</span>
Rejected
</VoteStatusTextBase>
);
case 'happening':
return (
<VoteStatusTextBase>
<Button isWithArrow={true} isAccentColor={true} fontSize="22px">
Vote Now
</Button>
</VoteStatusTextBase>
);
default:
return <VoteStatusTextBase>Loading...</VoteStatusTextBase>;
}
};
interface VoteStatusTextBaseProps {
color?: string;
}
const VoteStatusTextBase = styled.div<VoteStatusTextBaseProps>`
font-size: 22px;
color: ${props => props.color};
margin-bottom: 12px;
span {
position: relative;
margin-right: 8px;
top: 1px;
}
`;

View File

@@ -1,50 +0,0 @@
import * as _ from 'lodash';
import * as React from 'react';
import styled from 'styled-components';
import { Button } from 'ts/components/button';
import { DocumentTitle } from 'ts/components/document_title';
import { Column, Section } from 'ts/components/newLayout';
import { SiteWrap } from 'ts/components/siteWrap';
import { Heading, Paragraph } from 'ts/components/text';
import { constants } from 'ts/utils/constants';
import { documentConstants } from 'ts/utils/document_meta_constants';
export const VotePlaceholder = () => (
<SiteWrap>
<DocumentTitle {...documentConstants.VOTE} />
<Section isTextCentered={true} isPadded={true} padding="150px 0px">
<Column>
<Heading size="medium" isCentered={true}>
Come back on February 18th to vote
</Heading>
<Paragraph size="medium" isCentered={true} isMuted={true} marginBottom="0">
0x is conducting a vote on ZEIP-23, which adds the ability to trade bundles of ERC-20 and ERC-721
tokens via the 0x protocol. Integrating ZEIP-23 requires a modification to the protocols smart
contract pipeline, which has access to live digital assets. All ZRX token holders have the right to
vote on this improvement proposal.
</Paragraph>
<LinkWrap>
<Button
href={constants.URL_VOTE_BLOG_POST}
isWithArrow={true}
isAccentColor={true}
shouldUseAnchorTag={true}
target="_blank"
>
Learn More
</Button>
</LinkWrap>
</Column>
</Section>
</SiteWrap>
);
const LinkWrap = styled.div`
display: inline-flex;
margin-top: 60px;
a + a {
margin-left: 60px;
}
`;

View File

@@ -765,4 +765,15 @@ export interface Package {
description: string;
link: ALink;
}
export type VoteOutcome = 'accepted' | 'rejected';
export type VoteTime = 'upcoming' | 'happening';
export type VoteStatus = VoteOutcome | VoteTime;
export interface TallyInterface {
zeipId?: number;
yes?: BigNumber;
no?: BigNumber;
blockNumber?: string;
}
// tslint:disable:max-file-line-count