Refactor to a state where /proposlas loads.

This commit is contained in:
Will O'Beirne 2018-12-21 13:27:39 -05:00
parent eea3eea0f7
commit 5931de5460
No known key found for this signature in database
GPG Key ID: 44C190DB5DEAF9F6
30 changed files with 304 additions and 1093 deletions

View File

@ -101,7 +101,11 @@ def get_proposals(stage):
.all()
)
else:
proposals = Proposal.query.order_by(Proposal.date_created.desc()).all()
proposals = (
Proposal.query.filter_by(status="LIVE")
.order_by(Proposal.date_created.desc())
.all()
)
dumped_proposals = proposals_schema.dump(proposals)
return dumped_proposals
# except Exception as e:

View File

@ -147,12 +147,10 @@ export function putProposal(proposal: ProposalDraft): Promise<{ data: ProposalDr
return axios.put(`/api/v1/proposals/${proposal.proposalId}`, rest);
}
export function putProposalPublish(
proposal: ProposalDraft,
contractAddress: string,
): Promise<{ data: ProposalDraft }> {
return axios.put(`/api/v1/proposals/${proposal.proposalId}/publish`, {
contractAddress,
export async function putProposalPublish(proposal: ProposalDraft): Promise<{ data: Proposal }> {
return axios.put(`/api/v1/proposals/${proposal.proposalId}/publish`).then(res => {
res.data = formatProposalFromGet(res.data);
return res;
});
}

View File

@ -4,49 +4,49 @@ import { Spin, Icon } from 'antd';
import { Link } from 'react-router-dom';
import { createActions } from 'modules/create';
import { AppState } from 'store/reducers';
import { getProposalByAddress } from 'modules/proposals/selectors';
import { ProposalWithCrowdFund } from 'types';
import './Final.less';
interface StateProps {
form: AppState['create']['form'];
createdProposal: ProposalWithCrowdFund | null;
submittedProposal: AppState['create']['submittedProposal'];
submitError: AppState['create']['submitError'];
}
interface DispatchProps {
createProposal: typeof createActions['createProposal'];
submitProposal: typeof createActions['submitProposal'];
}
type Props = StateProps & DispatchProps;
class CreateFinal extends React.Component<Props> {
componentDidMount() {
this.create();
this.submit();
}
render() {
const { createdProposal } = this.props;
const { submittedProposal, submitError } = this.props;
let content;
// TODO - handle errors?
// if (crowdFundError) {
// content = (
// <div className="CreateFinal-message is-error">
// <Icon type="close-circle" />
// <div className="CreateFinal-message-text">
// Something went wrong during creation: "{crowdFundError}"{' '}
// <a onClick={this.create}>Click here</a> to try again.
// </div>
// </div>
// );
// } else
if (createdProposal) {
if (submitError) {
content = (
<div className="CreateFinal-message is-error">
<Icon type="close-circle" />
<div className="CreateFinal-message-text">
Something went wrong during creation: "{submitError}"{' '}
<a onClick={this.submit}>Click here</a> to try again.
</div>
</div>
);
} else
if (submittedProposal) {
content = (
<div className="CreateFinal-message is-success">
<Icon type="check-circle" />
<div className="CreateFinal-message-text">
Your proposal is now live and on the blockchain!{' '}
<Link to={`/proposals/${createdProposal.proposalUrlId}`}>Click here</Link> to
check it out.
Your proposal has been submitted!{' '}
<Link to={`/proposals/${submittedProposal.proposalUrlId}`}>
Click here
</Link>
{' '}to check it out.
</div>
</div>
);
@ -54,7 +54,9 @@ class CreateFinal extends React.Component<Props> {
content = (
<div className="CreateFinal-loader">
<Spin size="large" />
<div className="CreateFinal-loader-text">Deploying contract...</div>
<div className="CreateFinal-loader-text">
Submitting your proposal...
</div>
</div>
);
}
@ -62,9 +64,9 @@ class CreateFinal extends React.Component<Props> {
return <div className="CreateFinal">{content}</div>;
}
private create = () => {
private submit = () => {
if (this.props.form) {
this.props.createProposal(this.props.form);
this.props.submitProposal(this.props.form);
}
};
}
@ -72,9 +74,10 @@ class CreateFinal extends React.Component<Props> {
export default connect<StateProps, DispatchProps, {}, AppState>(
(state: AppState) => ({
form: state.create.form,
createdProposal: getProposalByAddress(state, 'notanaddress'),
submittedProposal: state.create.submittedProposal,
submitError: state.create.submitError,
}),
{
createProposal: createActions.createProposal,
submitProposal: createActions.submitProposal,
},
)(CreateFinal);

View File

@ -1,213 +0,0 @@
import React from 'react';
import { Input, Form, Icon, Button, Radio } from 'antd';
import { RadioChangeEvent } from 'antd/lib/radio';
import { ProposalDraft } from 'types';
import { getCreateErrors } from 'modules/create/utils';
import { ONE_DAY } from 'utils/time';
import { DONATION } from 'utils/constants';
interface State {
payoutAddress: string;
trustees: string[];
deadlineDuration: number;
voteDuration: number;
}
interface Props {
initialState?: Partial<State>;
updateForm(form: Partial<ProposalDraft>): void;
}
export default class CreateFlowTeam extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
payoutAddress: '',
trustees: [],
deadlineDuration: ONE_DAY * 60,
voteDuration: ONE_DAY * 7,
...(props.initialState || {}),
};
}
render() {
const { payoutAddress, trustees, deadlineDuration, voteDuration } = this.state;
const errors = getCreateErrors(this.state, true);
return (
<Form layout="vertical" style={{ maxWidth: 600, margin: '0 auto' }}>
<Form.Item
label="Payout address"
validateStatus={errors.payoutAddress ? 'error' : undefined}
help={errors.payoutAddress}
>
<Input
size="large"
name="payoutAddress"
placeholder={DONATION.ETH}
type="text"
value={payoutAddress}
onChange={this.handleInputChange}
/>
</Form.Item>
<Form.Item label="Trustee addresses">
<Input
placeholder="Payout address will also become a trustee"
size="large"
type="text"
disabled
value={payoutAddress}
/>
</Form.Item>
{trustees.map((address, idx) => (
<TrusteeFields
key={idx}
value={address}
index={idx}
error={errors.trustees && errors.trustees[idx]}
onChange={this.handleTrusteeChange}
onRemove={this.removeTrustee}
/>
))}
{trustees.length < 9 && (
<Button
type="dashed"
onClick={this.addTrustee}
style={{ margin: '-1rem 0 2rem' }}
>
<Icon type="plus" /> Add another trustee
</Button>
)}
<Form.Item label="Funding Deadline">
<Radio.Group
name="deadlineDuration"
value={deadlineDuration}
onChange={this.handleRadioChange}
size="large"
style={{ display: 'flex', textAlign: 'center' }}
>
{deadlineDuration === 300 && (
<Radio.Button style={{ flex: 1 }} value={300}>
5 minutes
</Radio.Button>
)}
<Radio.Button style={{ flex: 1 }} value={ONE_DAY * 30}>
30 Days
</Radio.Button>
<Radio.Button style={{ flex: 1 }} value={ONE_DAY * 60}>
60 Days
</Radio.Button>
<Radio.Button style={{ flex: 1 }} value={ONE_DAY * 90}>
90 Days
</Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="Milestone Voting Period">
<Radio.Group
name="voteDuration"
value={voteDuration}
onChange={this.handleRadioChange}
size="large"
style={{ display: 'flex', textAlign: 'center' }}
>
{voteDuration === 60 && (
<Radio.Button style={{ flex: 1 }} value={60}>
60 Seconds
</Radio.Button>
)}
<Radio.Button style={{ flex: 1 }} value={ONE_DAY * 3}>
3 Days
</Radio.Button>
<Radio.Button style={{ flex: 1 }} value={ONE_DAY * 7}>
7 Days
</Radio.Button>
<Radio.Button style={{ flex: 1 }} value={ONE_DAY * 10}>
10 Days
</Radio.Button>
</Radio.Group>
</Form.Item>
</Form>
);
}
private handleInputChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { value, name } = event.currentTarget;
this.setState({ [name]: value } as any, () => {
this.props.updateForm(this.state);
});
};
private handleRadioChange = (event: RadioChangeEvent) => {
const { value, name } = event.target;
this.setState({ [name as string]: value } as any, () => {
this.props.updateForm(this.state);
});
};
private handleTrusteeChange = (index: number, value: string) => {
const trustees = [...this.state.trustees];
trustees[index] = value;
this.setState({ trustees }, () => {
this.props.updateForm(this.state);
});
};
private addTrustee = () => {
const trustees = [...this.state.trustees, ''];
this.setState({ trustees });
};
private removeTrustee = (index: number) => {
const trustees = this.state.trustees.filter((_, i) => i !== index);
this.setState({ trustees }, () => {
this.props.updateForm(this.state);
});
};
}
interface TrusteeFieldsProps {
index: number;
value: string;
error: string | Falsy;
onChange(index: number, value: string): void;
onRemove(index: number): void;
}
const TrusteeFields = ({
index,
value,
error,
onChange,
onRemove,
}: TrusteeFieldsProps) => (
<Form.Item
validateStatus={error ? 'error' : undefined}
help={error}
style={{ marginTop: '-1rem' }}
>
<div style={{ display: 'flex' }}>
<Input
size="large"
placeholder={DONATION.ETH}
type="text"
value={value}
onChange={ev => onChange(index, ev.currentTarget.value)}
/>
<button
onClick={() => onRemove(index)}
style={{
paddingLeft: '0.5rem',
fontSize: '1.3rem',
cursor: 'pointer',
}}
>
<Icon type="close-circle-o" />
</button>
</div>
</Form.Item>
);

View File

@ -0,0 +1,93 @@
import React from 'react';
import { Input, Form, Radio } from 'antd';
import { RadioChangeEvent } from 'antd/lib/radio';
import { ProposalDraft } from 'types';
import { getCreateErrors } from 'modules/create/utils';
import { ONE_DAY } from 'utils/time';
import { DONATION } from 'utils/constants';
interface State {
payoutAddress: string;
deadlineDuration: number;
}
interface Props {
initialState?: Partial<State>;
updateForm(form: Partial<ProposalDraft>): void;
}
export default class CreateFlowPayment extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
payoutAddress: '',
deadlineDuration: ONE_DAY * 60,
...(props.initialState || {}),
};
}
render() {
const { payoutAddress, deadlineDuration } = this.state;
const errors = getCreateErrors(this.state, true);
return (
<Form layout="vertical" style={{ maxWidth: 600, margin: '0 auto' }}>
<Form.Item
label="Payout address"
validateStatus={errors.payoutAddress ? 'error' : undefined}
help={errors.payoutAddress}
>
<Input
size="large"
name="payoutAddress"
placeholder={DONATION.ETH}
type="text"
value={payoutAddress}
onChange={this.handleInputChange}
/>
</Form.Item>
<Form.Item label="Funding Deadline">
<Radio.Group
name="deadlineDuration"
value={deadlineDuration}
onChange={this.handleRadioChange}
size="large"
style={{ display: 'flex', textAlign: 'center' }}
>
{deadlineDuration === 300 && (
<Radio.Button style={{ flex: 1 }} value={300}>
5 minutes
</Radio.Button>
)}
<Radio.Button style={{ flex: 1 }} value={ONE_DAY * 30}>
30 Days
</Radio.Button>
<Radio.Button style={{ flex: 1 }} value={ONE_DAY * 60}>
60 Days
</Radio.Button>
<Radio.Button style={{ flex: 1 }} value={ONE_DAY * 90}>
90 Days
</Radio.Button>
</Radio.Group>
</Form.Item>
</Form>
);
}
private handleInputChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { value, name } = event.currentTarget;
this.setState({ [name]: value } as any, () => {
this.props.updateForm(this.state);
});
};
private handleRadioChange = (event: RadioChangeEvent) => {
const { value, name } = event.target;
this.setState({ [name as string]: value } as any, () => {
this.props.updateForm(this.state);
});
};
}

View File

@ -46,7 +46,7 @@ export default class PublishWarningModal extends React.Component<Props> {
<p>
Are you sure youre ready to publish your proposal? Once youve done so, you
won't be able to change certain fields such as: target amount, payout address,
team, trustees, deadline & vote durations.
team, & deadline.
</p>
</div>
</Modal>

View File

@ -103,7 +103,7 @@ class CreateReview extends React.Component<Props> {
],
},
{
step: CREATE_STEP.GOVERNANCE,
step: CREATE_STEP.PAYMENT,
name: 'Governance',
fields: [
{
@ -111,15 +111,6 @@ class CreateReview extends React.Component<Props> {
content: <code>{form.payoutAddress}</code>,
error: errors.payoutAddress,
},
{
key: 'trustees',
content: form.trustees.map(t => (
<div key={t}>
<code>{t}</code>
</div>
)),
error: errors.trustees && errors.trustees.join(' '),
},
{
key: 'deadlineDuration',
content: `${Math.floor(
@ -127,13 +118,6 @@ class CreateReview extends React.Component<Props> {
)} days`,
error: errors.deadlineDuration,
},
{
key: 'voteDuration',
content: `${Math.floor(
moment.duration((form.voteDuration || 0) * 1000).asDays(),
)} days`,
error: errors.voteDuration,
},
],
},
];

View File

@ -4,7 +4,7 @@ import { Icon, Form, Input, Button, Popconfirm, message } from 'antd';
import { User, TeamInvite, ProposalDraft } from 'types';
import TeamMemberComponent from './TeamMember';
import { postProposalInvite, deleteProposalInvite } from 'api/api';
import { isValidAddress, isValidEmail } from 'utils/validators';
import { isValidEmail } from 'utils/validators';
import { AppState } from 'store/reducers';
import './Team.less';
@ -51,10 +51,8 @@ class CreateFlowTeam extends React.Component<Props, State> {
render() {
const { team, invites, address } = this.state;
const inviteError =
address && !isValidEmail(address) && !isValidAddress(address)
? 'That doesnt look like an email address or ETH address'
: undefined;
const inviteError = address && !isValidEmail(address) &&
'That doesnt look like a valid email address';
const inviteDisabled = !!inviteError || !address;
const pendingInvites = invites.filter(inv => inv.accepted === null);
@ -95,7 +93,7 @@ class CreateFlowTeam extends React.Component<Props, State> {
>
<Input
className="TeamForm-add-form-field-input"
placeholder="Email address or ETH address"
placeholder="Email address"
size="large"
value={address}
onChange={this.handleChangeInviteAddress}

View File

@ -1,10 +1,7 @@
import { PROPOSAL_CATEGORY } from 'api/constants';
import { ProposalDraft } from 'types';
const createExampleProposal = (
payoutAddress: string,
trustees: string[],
): Partial<ProposalDraft> => {
const createExampleProposal = (payoutAddress: string): Partial<ProposalDraft> => {
return {
title: 'Grant.io T-Shirts',
brief: "The most stylish wear, sporting your favorite brand's logo",
@ -13,7 +10,6 @@ const createExampleProposal = (
'![](https://i.imgur.com/aQagS0D.png)\n\nWe all know it, Grant.io is the bee\'s knees. But wouldn\'t it be great if you could show all your friends and family how much you love it? Well that\'s what we\'re here to offer today.\n\n# What We\'re Building\n\nWhy, T-Shirts of course! These beautiful shirts made out of 100% cotton and laser printed for long lasting goodness come from American Apparel. We\'ll be offering them in 4 styles:\n\n* Crew neck (wrinkled)\n* Crew neck (straight)\n* Scoop neck (fitted)\n* V neck (fitted)\n\nShirt sizings will be as follows:\n\n| Size | S | M | L | XL |\n|--------|-----|-----|-----|------|\n| **Width** | 18" | 20" | 22" | 24" |\n| **Length** | 28" | 29" | 30" | 31" |\n\n# Who We Are\n\nWe are the team behind grant.io. In addition to our software engineering experience, we have over 78 years of T-Shirt printing expertise combined. Sometimes I wake up at night and realize I was printing shirts in my dreams. Weird, man.\n\n# Expense Breakdown\n\n* $1,000 - A professional designer will hand-craft each letter on the shirt.\n* $500 - We\'ll get the shirt printed from 5 different factories and choose the best quality one.\n* $3,000 - The full run of prints, with 20 smalls, 20 mediums, and 20 larges.\n* $500 - Pizza. Lots of pizza.\n\n**Total**: $5,000',
target: '5',
payoutAddress,
trustees,
milestones: [
{
title: 'Initial Funding',
@ -40,8 +36,7 @@ const createExampleProposal = (
immediatePayout: false,
},
],
deadlineDuration: 300,
voteDuration: 60,
deadlineDuration: 300
};
};

View File

@ -10,11 +10,11 @@ import Basics from './Basics';
import Team from './Team';
import Details from './Details';
import Milestones from './Milestones';
import Governance from './Governance';
import Payment from './Payment';
import Review from './Review';
import Preview from './Preview';
import Final from './Final';
import PublishWarningModal from './PubishWarningModal';
import PublishWarningModal from './PublishWarningModal';
import createExampleProposal from './example';
import { createActions } from 'modules/create';
import { ProposalDraft } from 'types';
@ -29,7 +29,7 @@ export enum CREATE_STEP {
TEAM = 'TEAM',
DETAILS = 'DETAILS',
MILESTONES = 'MILESTONES',
GOVERNANCE = 'GOVERNANCE',
PAYMENT = 'PAYMENT',
REVIEW = 'REVIEW',
}
@ -38,7 +38,7 @@ const STEP_ORDER = [
CREATE_STEP.TEAM,
CREATE_STEP.DETAILS,
CREATE_STEP.MILESTONES,
CREATE_STEP.GOVERNANCE,
CREATE_STEP.PAYMENT,
CREATE_STEP.REVIEW,
];
@ -82,14 +82,13 @@ const STEP_INFO: { [key in CREATE_STEP]: StepInfo } = {
'Contributors are more willing to fund proposals with funding spread across multiple deadlines',
component: Milestones,
},
[CREATE_STEP.GOVERNANCE]: {
short: 'Governance',
title: 'Choose how you get paid, and whos in control',
subtitle:
'Everything here cannot be changed after publishing, so make sure its right',
[CREATE_STEP.PAYMENT]: {
short: 'Payment',
title: 'Choose how you get paid',
subtitle: 'Youll only be paid if your funding target is reached',
help:
'Double check everything! This data powers the smart contract, and is immutable once its deployed.',
component: Governance,
'Double check your address, and make sure its secure. Once sent, payments are irreversible!',
component: Payment,
},
[CREATE_STEP.REVIEW]: {
short: 'Review',
@ -312,9 +311,9 @@ class CreateFlow extends React.Component<Props, State> {
private fillInExample = () => {
const { accounts } = this.props;
const [payoutAddress, ...trustees] = accounts;
const [payoutAddress] = accounts;
this.updateForm(createExampleProposal(payoutAddress, trustees || []));
this.updateForm(createExampleProposal(payoutAddress));
setTimeout(() => {
this.setState({
isExample: true,

View File

@ -1,8 +1,8 @@
import React from 'react';
import moment from 'moment';
import BN from 'bn.js';
import { Spin, Form, Input, Button, Icon } from 'antd';
import { ProposalWithCrowdFund } from 'types';
import './style.less';
import classnames from 'classnames';
import { fromWei } from 'utils/units';
import { connect } from 'react-redux';
@ -14,6 +14,7 @@ import UnitDisplay from 'components/UnitDisplay';
import { getAmountError } from 'utils/validators';
import { CATEGORY_UI } from 'api/constants';
import MetaMaskRequiredButton from 'components/MetaMaskRequiredButton';
import './style.less';
interface OwnProps {
proposal: ProposalWithCrowdFund;
@ -49,9 +50,10 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
return;
}
const { proposal } = this.props;
const { crowdFund } = proposal;
const remainingTarget = crowdFund.target.sub(crowdFund.funded);
// TODO: Get values from proposal
const target = new BN(0);
const funded = new BN(0);
const remainingTarget = target.sub(funded);
const amount = parseFloat(value);
let amountError = null;
@ -78,15 +80,22 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
const amountFloat = parseFloat(amountToRaise) || 0;
let content;
if (proposal) {
const { crowdFund } = proposal;
// TODO: Get values from proposal
console.warn('TODO: Get real values from proposal for CampaignBlock');
const isRaiseGoalReached = false;
const deadline = 0;
const isFrozen = false;
const target = new BN(0);
const funded = new BN(0);
const percentFunded = 0;
const beneficiary = 'z123';
const isFundingOver =
crowdFund.isRaiseGoalReached ||
crowdFund.deadline < Date.now() ||
crowdFund.isFrozen;
isRaiseGoalReached ||
deadline < Date.now() ||
isFrozen;
const isDisabled = isFundingOver || !!amountError || !amountFloat || isPreview;
const remainingEthNum = parseFloat(
fromWei(crowdFund.target.sub(crowdFund.funded), 'ether'),
);
const remainingEthNum = parseFloat(fromWei(target.sub(funded), 'ether'));
content = (
<React.Fragment>
@ -110,21 +119,21 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
<div className="ProposalCampaignBlock-info">
<div className="ProposalCampaignBlock-info-label">Deadline</div>
<div className="ProposalCampaignBlock-info-value">
{moment(crowdFund.deadline).fromNow()}
{moment(deadline).fromNow()}
</div>
</div>
)}
<div className="ProposalCampaignBlock-info">
<div className="ProposalCampaignBlock-info-label">Beneficiary</div>
<div className="ProposalCampaignBlock-info-value">
<ShortAddress address={crowdFund.beneficiary} />
<ShortAddress address={beneficiary} />
</div>
</div>
<div className="ProposalCampaignBlock-info">
<div className="ProposalCampaignBlock-info-label">Funding</div>
<div className="ProposalCampaignBlock-info-value">
<UnitDisplay value={crowdFund.funded} /> /{' '}
<UnitDisplay value={crowdFund.target} symbol="ETH" />
<UnitDisplay value={funded} /> /{' '}
<UnitDisplay value={target} symbol="ETH" />
</div>
</div>
@ -132,10 +141,10 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
<div
className={classnames({
['ProposalCampaignBlock-fundingOver']: true,
['is-success']: crowdFund.isRaiseGoalReached,
['is-success']: isRaiseGoalReached,
})}
>
{crowdFund.isRaiseGoalReached ? (
{isRaiseGoalReached ? (
<>
<Icon type="check-circle-o" />
<span>Proposal has been funded</span>
@ -153,7 +162,7 @@ export class ProposalCampaignBlock extends React.Component<Props, State> {
<div
className="ProposalCampaignBlock-bar-inner"
style={{
width: `${crowdFund.percentFunded}%`,
width: `${percentFunded}%`,
}}
/>
</div>

View File

@ -19,15 +19,12 @@ type Props = StateProps & OwnProps;
class CancelModal extends React.Component<Props> {
componentDidUpdate() {
if (this.props.proposal.crowdFund.isFrozen) {
this.props.handleClose();
}
// TODO: Close on success of action
}
render() {
const { proposal, isVisible, isRefundActionPending, refundActionError } = this.props;
const hasBeenFunded = proposal.crowdFund.isRaiseGoalReached;
const hasContributors = !!proposal.crowdFund.contributors.length;
const { isVisible, isRefundActionPending, refundActionError } = this.props;
const hasContributors = false; // TODO: Determine if it has contributors from proposal
const disabled = isRefundActionPending;
return (
@ -41,20 +38,10 @@ class CancelModal extends React.Component<Props> {
okButtonProps={{ type: 'danger', loading: disabled }}
cancelButtonProps={{ disabled }}
>
{hasBeenFunded ? (
<p>
Are you sure you would like to issue a refund?{' '}
<strong>This cannot be undone</strong>. Once you issue a refund, all
contributors will be able to receive a refund of the remaining proposal
balance.
</p>
) : (
<p>
Are you sure you would like to cancel this proposal?{' '}
<strong>This cannot be undone</strong>. Once you cancel it, all contributors
will be able to receive refunds.
</p>
)}
<p>
Are you sure you would like to cancel this proposal?{' '}
<strong>This cannot be undone</strong>.
</p>
<p>
Canceled proposals cannot be deleted and will still be viewable by contributors
or anyone with a direct link. However, they will be de-listed everywhere else on
@ -69,7 +56,7 @@ class CancelModal extends React.Component<Props> {
{refundActionError && (
<Alert
type="error"
message={`Failed to ${hasBeenFunded ? 'refund' : 'cancel'} proposal`}
message="Failed to cancel proposal"
description={refundActionError}
showIcon
/>

View File

@ -1,19 +1,17 @@
import React from 'react';
import { Spin } from 'antd';
import { CrowdFund } from 'types';
import AddressRow from 'components/AddressRow';
import Placeholder from 'components/Placeholder';
import UnitDisplay from 'components/UnitDisplay';
interface Props {
crowdFund: CrowdFund;
}
const ContributorsBlock = ({ crowdFund }: Props) => {
const ContributorsBlock = () => {
// TODO: Get contributors from proposal
console.warn('TODO: Get contributors from proposal for Proposal/Contributors/index.tsx');
const proposal = { contributors: [] as any };
let content;
if (crowdFund) {
if (crowdFund.contributors.length) {
content = crowdFund.contributors.map(contributor => (
if (proposal) {
if (proposal.contributors.length) {
content = proposal.contributors.map((contributor: any) => (
<AddressRow
key={contributor.address}
address={contributor.address}
@ -38,7 +36,7 @@ const ContributorsBlock = ({ crowdFund }: Props) => {
return (
<div className="Proposal-top-side-block">
{crowdFund.contributors.length ? (
{proposal.contributors.length ? (
<>
<h1 className="Proposal-top-main-block-title">Contributors</h1>
<div className="Proposal-top-main-block">{content}</div>

View File

@ -1,207 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { Progress, Button, Alert } from 'antd';
import { ProposalWithCrowdFund } from 'types';
import { AppState } from 'store/reducers';
import classnames from 'classnames';
import Placeholder from 'components/Placeholder';
interface OwnProps {
proposal: ProposalWithCrowdFund;
}
interface StateProps {
isRefundActionPending: boolean;
refundActionError: string;
}
type Props = OwnProps & StateProps;
class GovernanceRefunds extends React.Component<Props> {
render() {
const { proposal, isRefundActionPending, refundActionError } = this.props;
const { crowdFund } = proposal;
const isStillFunding =
!crowdFund.isRaiseGoalReached && crowdFund.deadline > Date.now();
const account = 'sorrynotanaccount';
if (isStillFunding && !crowdFund.isFrozen) {
return (
<Placeholder
title="Refund governance isnt available yet"
subtitle={`
Refund voting and status will be displayed here once the
project has been funded, or if it the project is canceled
or fails to receive funding
`}
/>
);
}
// TODO: Cyclomatic complexity is too damn high here. This state should be
// figured out on the backend and enumerated, not calculated in this component.
const contributor = crowdFund.contributors.find(c => c.address === account);
const isTrustee = crowdFund.trustees.includes(account);
const hasVotedForRefund = contributor && contributor.refundVote;
const hasRefunded = contributor && contributor.refunded;
const refundPct = crowdFund.percentVotingForRefund;
const didFundingFail =
!crowdFund.isRaiseGoalReached && crowdFund.deadline < Date.now();
let text;
let button;
if (!isTrustee && contributor) {
let canRefund = false;
if (hasRefunded) {
return (
<Alert
type="success"
message="Your refund has been processed"
description={`
We apologize for any inconvenience this propsal has caused you. Please
let us know if there's anything we could have done to improve your
experience.
`}
showIcon
/>
);
} else if (refundPct > 50) {
text = `
The majority of funders have voted for a refund. Click below
to receive your refund.
`;
canRefund = true;
} else if (didFundingFail || crowdFund.isFrozen) {
text = `
The project was either canceled, or failed to reach its funding
target before the deadline. Click below to receive a refund of
your contribution.
`;
canRefund = true;
} else {
text = `
As a funder of this project, you have the right to vote for a refund. If the
amount of funds contributed by refund voters exceeds half of the project's
total raised funds, all funders will be able to request refunds.
`;
if (hasVotedForRefund) {
button = {
text: 'Undo vote for refund',
type: 'danger',
onClick: () => this.voteRefund(false),
};
} else {
button = {
text: 'Vote for refund',
type: 'danger',
onClick: () => this.voteRefund(true),
};
}
}
if (canRefund) {
text = (
<>
{text}
{!crowdFund.isFrozen && (
<Alert
style={{ marginTop: '1rem' }}
type="info"
showIcon
message={`
This will require multiple transactions to process, sorry
for the inconvenience
`}
/>
)}
</>
);
button = {
text: 'Get your refund',
type: 'primary',
onClick: () => this.withdrawRefund(),
};
}
} else {
if (crowdFund.isFrozen || didFundingFail) {
text = `
The project failed to receive funding or was canceled. Contributors are
open to refund their contributions.
`;
} else if (refundPct < 50) {
text = `
Funders can vote to request refunds. If the amount of funds contributed by
refund voters exceeds half of the funds contributed, all funders will be able
to request refunds.
`;
} else {
text = `
The funders of this project have voted for a refund. All funders can request refunds,
and the project will no longer receive any payouts.
`;
}
}
return (
<>
<div style={{ display: 'flex', alignItems: 'center' }}>
<div
className={classnames({
['ProposalGovernance-progress']: true,
[refundPct < 10
? 'is-starting'
: refundPct < 50
? 'is-started'
: 'is-finishing']: true,
})}
>
<Progress type="dashboard" percent={refundPct} format={p => `${p}%`} />
<div className="ProposalGovernance-progress-text">voted for a refund</div>
</div>
<div>
<p style={{ fontSize: '1rem' }}>{text}</p>
{button && (
<Button
type={button.type as any}
onClick={button.onClick}
loading={isRefundActionPending}
block
>
{button.text}
</Button>
)}
</div>
</div>
{refundActionError && (
<Alert
type="error"
message="Something went wrong!"
description={refundActionError}
style={{ margin: '1rem 0 0' }}
showIcon
/>
)}
</>
);
}
voteRefund = (vote: boolean) => {
console.warn('TODO - implement or remove voteRefund', vote);
};
withdrawRefund = () => {
const { proposal } = this.props;
console.warn('TODO - implement or remove withdrawRefund', proposal);
};
}
const ConnectedGovernanceRefunds = connect<StateProps, {}, OwnProps, AppState>(state => {
console.warn('TODO - new redux isRefundActionPending/refundActionError?', state);
return {
isRefundActionPending: false,
refundActionError: '',
};
})(GovernanceRefunds);
export default ConnectedGovernanceRefunds;

View File

@ -1,21 +0,0 @@
import React from 'react';
import GovernanceRefunds from './Refunds';
import { ProposalWithCrowdFund } from 'types';
import './style.less';
interface Props {
proposal: ProposalWithCrowdFund;
}
export default class ProposalGovernance extends React.Component<Props> {
render() {
const { proposal } = this.props;
return (
<div className="ProposalGovernance">
<div className="ProposalGovernance-content">
<GovernanceRefunds proposal={proposal} />
</div>
</div>
);
}
}

View File

@ -1,53 +0,0 @@
@import '~styles/variables.less';
@small-screen: 1080px;
.ProposalGovernance {
display: flex;
justify-content: center;
padding-top: 1rem;
&-content {
max-width: 800px;
}
&-progress {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
margin: 0 2rem 0.5rem 0;
&-text {
white-space: nowrap;
opacity: 0.6;
font-size: 0.75rem;
}
// Ant progress overrides
.ant-progress-circle-path {
stroke: inherit;
}
.ant-progress-text {
color: inherit !important;
}
&.is-starting {
.ant-progress-circle-path {
stroke: @primary-color;
}
}
&.is-started {
.ant-progress-circle-path {
stroke: @warning-color;
}
}
&.is-finishing {
.ant-progress-circle-path {
stroke: @error-color;
}
}
}
}

View File

@ -1,50 +0,0 @@
@import '~styles/variables.less';
@small-screen: 1080px;
.MilestoneAction {
&-top {
display: flex;
align-items: center;
}
&-text {
font-size: 1rem;
}
&-progress {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
margin: 0 2rem 0.5rem 0;
&-text {
white-space: nowrap;
opacity: 0.6;
font-size: 0.75rem;
}
// Ant progress overrides
.ant-progress-text {
color: inherit !important;
}
&.is-starting {
.ant-progress-circle-path {
stroke: @primary-color;
}
}
&.is-started {
.ant-progress-circle-path {
stroke: @warning-color;
}
}
&.is-finishing {
.ant-progress-circle-path {
stroke: @error-color;
}
}
}
}

View File

@ -1,254 +0,0 @@
import React from 'react';
import moment from 'moment';
import { connect } from 'react-redux';
import { Button, Progress, Alert } from 'antd';
import { ProposalWithCrowdFund, MILESTONE_STATE } from 'types';
import { AppState } from 'store/reducers';
import UnitDisplay from 'components/UnitDisplay';
import Placeholder from 'components/Placeholder';
import './MilestoneAction.less';
import { REJECTED } from 'redux-promise-middleware';
interface OwnProps {
proposal: ProposalWithCrowdFund;
}
interface StateProps {
isMilestoneActionPending: boolean;
milestoneActionError: string;
accounts: string[];
}
type Props = OwnProps & StateProps;
export class Milestones extends React.Component<Props> {
render() {
const {
proposal,
accounts,
isMilestoneActionPending,
milestoneActionError,
} = this.props;
const { crowdFund } = proposal;
if (!crowdFund.isRaiseGoalReached) {
return (
<Placeholder
title="Milestone governance isnt available yet"
subtitle={`
Milestone history and voting status will be displayed here
once the project has been funded
`}
/>
);
}
const contributor = crowdFund.contributors.find(c => c.address === accounts[0]);
const isTrustee = crowdFund.trustees.includes(accounts[0]);
const firstMilestone = proposal.milestones[0];
const isImmediatePayout = crowdFund.immediateFirstMilestonePayout;
// TODO: Should this information be abstracted to a lib or redux?
const hasImmediatePayoutStarted =
isImmediatePayout && firstMilestone.payoutRequestVoteDeadline;
const hasImmediatePayoutBeenPaid = isImmediatePayout && firstMilestone.isPaid;
const activeVoteMilestone = proposal.milestones.find(
m => m.state === MILESTONE_STATE.ACTIVE,
);
const uncollectedMilestone = proposal.milestones.find(
m => m.state === MILESTONE_STATE.PAID && !m.isPaid,
);
const nextUnpaidMilestone = proposal.milestones.find(
m => m.state !== MILESTONE_STATE.PAID,
);
let content;
let button;
let showVoteProgress = false;
if (isTrustee) {
// Trustee views, i.e. admin actions
if (isImmediatePayout && !hasImmediatePayoutBeenPaid) {
if (!hasImmediatePayoutStarted) {
content = (
<p className="MilestoneAction-text">
Congratulations on getting funded! You can now begin the process of
receiving your initial payment. Click below to begin a milestone payout
request. It will instantly be approved, and youll be able to request the
funds immediately after.
</p>
);
button = {
text: 'Request initial payout',
type: 'primary',
onClick: () => this.requestPayout(0),
};
} else {
content = (
<p className="MilestoneAction-text">
Your initial payout is ready! Click below to claim it.
</p>
);
button = {
text: 'Receive initial payout',
type: 'primary',
onClick: () => this.payPayout(0),
};
}
} else if (activeVoteMilestone) {
content = (
<p className="MilestoneAction-text">
The vote for your payout is in progress. If payout rejection votes dont
exceed 50% before{' '}
{moment(activeVoteMilestone.payoutRequestVoteDeadline).format(
'MMM Do, h:mm a',
)}
, you will be able to collect your payment then.
</p>
);
showVoteProgress = true;
} else if (uncollectedMilestone) {
content = (
<p className="MilestoneAction-text">
Congratulations! Your milestone payout request was succesful. Click below to
receive your payment of{' '}
<strong>
<UnitDisplay value={uncollectedMilestone.amount} symbol="ETH" />
</strong>
.
</p>
);
button = {
text: 'Receive milestone payout',
type: 'primary',
onClick: () => this.payPayout(uncollectedMilestone.index),
};
} else if (nextUnpaidMilestone) {
content = (
<p className="MilestoneAction-text">
{nextUnpaidMilestone.state === REJECTED
? 'You can make another request for this milestone payout. '
: 'You can request a payout for this milestone. '}
If fewer than 50% of funders vote against it before{' '}
{moment(Date.now() + crowdFund.milestoneVotingPeriod).format('MMM Do h:mm a')}
, you will be able to collect your payout here.
</p>
);
button = {
text: 'Request milestone payout',
type: 'primary',
onClick: () => this.requestPayout(nextUnpaidMilestone.index),
};
} else {
content = <p>All milestones have been paid! Thanks for the hard work.</p>;
}
} else {
// User views, i.e. funders or any public spectator
if (activeVoteMilestone) {
const hasVotedAgainst =
contributor && contributor.milestoneNoVotes[activeVoteMilestone.index];
if (contributor) {
if (hasVotedAgainst) {
button = {
text: 'Revert vote against payout',
type: 'danger',
onClick: () => this.votePayout(activeVoteMilestone.index, false),
};
} else {
button = {
text: 'Vote against payout',
type: 'danger',
onClick: () => this.votePayout(activeVoteMilestone.index, true),
};
}
}
content = (
<p className="MilestoneAction-text">
A milestone vote is currently in progress. If funders vote against paying out
the milestone by over 50% before{' '}
{moment(activeVoteMilestone.payoutRequestVoteDeadline).format(
'MMM Do h:mm a',
)}
, the team will not receive the funds.
{contributor && ' Since you funded this proposal, you can vote below.'}
</p>
);
showVoteProgress = true;
} else if (nextUnpaidMilestone) {
content = (
<p className="MilestoneAction-text">
There is no milestone vote currently active.
</p>
);
} else {
content = (
<p className="MilestoneAction-text">All milestones have been paid out.</p>
);
}
}
return (
<div className="MilestonAction">
<div className="MilestoneAction-top">
{showVoteProgress &&
activeVoteMilestone && (
<div className="MilestoneAction-progress">
<Progress
type="dashboard"
percent={activeVoteMilestone.percentAgainstPayout}
format={p => `${p}%`}
status="exception"
/>
<div className="MilestoneAction-progress-text">voted against payout</div>
</div>
)}
<div>
{content}
{button && (
<Button
type={button.type as any}
loading={isMilestoneActionPending}
onClick={button.onClick}
block
>
{button.text}
</Button>
)}
</div>
</div>
{milestoneActionError && (
<Alert
type="error"
message="Something went wrong!"
description={milestoneActionError}
style={{ margin: '1rem 0' }}
showIcon
/>
)}
</div>
);
}
private requestPayout = (milestoneIndex: number) => {
console.warn('TODO - implement/refactor requestPayout', milestoneIndex);
};
private payPayout = (milestoneIndex: number) => {
console.warn('TODO - implement/refactor payPayout', milestoneIndex);
};
private votePayout = (milestoneIndex: number, vote: boolean) => {
console.warn('TODO - implement/refactor votePayout', milestoneIndex, vote);
};
}
const ConnectedMilestones = connect((state: AppState) => {
console.warn(
'TODO - new redux user-role-for-proposal/accounts + isMilestoneActionPending + milestoneActionError',
state,
);
return {
accounts: [],
isMilestoneActionPending: false,
milestoneActionError: '',
};
})(Milestones);
export default (props: OwnProps) => <ConnectedMilestones {...props} />;

View File

@ -4,7 +4,6 @@ import moment from 'moment';
import { Alert, Steps, Spin } from 'antd';
import { ProposalWithCrowdFund, MILESTONE_STATE } from 'types';
import UnitDisplay from 'components/UnitDisplay';
import MilestoneAction from './MilestoneAction';
import { AppState } from 'store/reducers';
import { connect } from 'react-redux';
import classnames from 'classnames';
@ -85,15 +84,9 @@ class ProposalMilestones extends React.Component<Props, State> {
if (!proposal) {
return <Spin />;
}
const {
milestones,
crowdFund,
crowdFund: { milestoneVotingPeriod, percentVotingForRefund },
} = proposal;
const { accounts } = this.props;
const { milestones } = proposal;
const wasRefunded = percentVotingForRefund > 50;
const isTrustee = crowdFund.trustees.includes(accounts[0]);
const isTrustee = false; // TODO: Replace with being on the team
const milestoneCount = milestones.length;
const milestoneSteps = milestones.map((milestone, i) => {
@ -107,9 +100,6 @@ class ProposalMilestones extends React.Component<Props, State> {
const reward = (
<UnitDisplay value={milestone.amount} symbol="ETH" displayShortBalance={4} />
);
const approvalPeriod = milestone.isImmediatePayout
? 'Immediate'
: moment.duration(milestoneVotingPeriod).humanize();
const alertStyle = { width: 'fit-content', margin: '0 0 1rem 0' };
const stepProps = {
@ -173,18 +163,6 @@ class ProposalMilestones extends React.Component<Props, State> {
break;
}
if (wasRefunded) {
notification = (
<Alert
type="error"
message={
<span>A majority of the funders of this project voted for a refund.</span>
}
style={alertStyle}
/>
);
}
const statuses = (
<div className="ProposalMilestones-milestone-status">
{!milestone.isImmediatePayout && (
@ -195,9 +173,6 @@ class ProposalMilestones extends React.Component<Props, State> {
<div>
Reward: <strong>{reward}</strong>
</div>
<div>
Approval period: <strong>{approvalPeriod}</strong>
</div>
</div>
);
@ -210,15 +185,6 @@ class ProposalMilestones extends React.Component<Props, State> {
{notification}
{milestone.content}
</div>
{this.state.activeMilestoneIdx === i &&
!wasRefunded && (
<>
<div className="ProposalMilestones-milestone-divider" />
<div className="ProposalMilestones-milestone-action">
<MilestoneAction proposal={proposal} />
</div>
</>
)}
</div>
</div>
);

View File

@ -13,7 +13,6 @@ import TeamBlock from './TeamBlock';
import Milestones from './Milestones';
import CommentsTab from './Comments';
import UpdatesTab from './Updates';
import GovernanceTab from './Governance';
import ContributorsTab from './Contributors';
// import CommunityTab from './Community';
import UpdateModal from './UpdateModal';
@ -80,7 +79,7 @@ export class ProposalDetail extends React.Component<Props, State> {
}
render() {
const { proposal, isPreview, account } = this.props;
const { proposal, isPreview } = this.props;
const {
isBodyExpanded,
isBodyOverflowing,
@ -93,12 +92,11 @@ export class ProposalDetail extends React.Component<Props, State> {
if (!proposal) {
return <Spin />;
} else {
const { crowdFund } = proposal;
const isTrustee = !!account && crowdFund.trustees.includes(account);
const isContributor = !!crowdFund.contributors.find(c => c.address === account);
const hasBeenFunded = crowdFund.isRaiseGoalReached;
const isProposalActive = !hasBeenFunded && crowdFund.deadline > Date.now();
const canRefund = (hasBeenFunded || isProposalActive) && !crowdFund.isFrozen;
const deadline = 0; // TODO: Use actual date for deadline
const isTrustee = false; // TODO: determine rework to isAdmin
const hasBeenFunded = false; // TODO: deterimne if proposal has reached funding
const isProposalActive = !hasBeenFunded && deadline > Date.now();
const canCancel = false; // TODO: Allow canceling if proposal hasn't gone live yet
const adminMenu = isTrustee && (
<Menu>
@ -110,11 +108,11 @@ export class ProposalDetail extends React.Component<Props, State> {
Edit proposal
</Menu.Item>
<Menu.Item
style={{ color: canRefund ? '#e74c3c' : undefined }}
style={{ color: canCancel ? '#e74c3c' : undefined }}
onClick={this.openCancelModal}
disabled={!canRefund}
disabled={!canCancel}
>
{hasBeenFunded ? 'Refund contributors' : 'Cancel proposal'}
Cancel proposal
</Menu.Item>
</Menu>
);
@ -192,13 +190,8 @@ export class ProposalDetail extends React.Component<Props, State> {
<Tabs.TabPane tab="Updates" key="updates" disabled={isPreview}>
<UpdatesTab proposalId={proposal.proposalId} />
</Tabs.TabPane>
{isContributor && (
<Tabs.TabPane tab="Refund" key="refund">
<GovernanceTab proposal={proposal} />
</Tabs.TabPane>
)}
<Tabs.TabPane tab="Contributors" key="contributors">
<ContributorsTab crowdFund={proposal.crowdFund} />
<ContributorsTab />
</Tabs.TabPane>
</Tabs>
)}

View File

@ -1,5 +1,6 @@
import React from 'react';
import classnames from 'classnames';
import BN from 'bn.js';
import { Progress, Icon } from 'antd';
import moment from 'moment';
import { Redirect } from 'react-router-dom';
@ -21,9 +22,13 @@ export class ProposalCard extends React.Component<ProposalWithCrowdFund> {
proposalUrlId,
category,
dateCreated,
crowdFund,
team,
} = this.props;
// TODO: Real values from proposal
console.warn('TODO: Real values for ProposalCard');
const target = new BN(0);
const funded = new BN(0);
const percentFunded = 0;
return (
<div
@ -33,21 +38,21 @@ export class ProposalCard extends React.Component<ProposalWithCrowdFund> {
<h3 className="ProposalCard-title">{title}</h3>
<div className="ProposalCard-funding">
<div className="ProposalCard-funding-raised">
<UnitDisplay value={crowdFund.funded} symbol="ETH" /> <small>raised</small> of{' '}
<UnitDisplay value={crowdFund.target} symbol="ETH" /> goal
<UnitDisplay value={funded} symbol="ETH" /> <small>raised</small> of{' '}
<UnitDisplay value={target} symbol="ETH" /> goal
</div>
<div
className={classnames({
['ProposalCard-funding-percent']: true,
['is-funded']: crowdFund.percentFunded >= 100,
['is-funded']: percentFunded >= 100,
})}
>
{crowdFund.percentFunded}%
{percentFunded}%
</div>
</div>
<Progress
percent={crowdFund.percentFunded}
status={crowdFund.percentFunded >= 100 ? 'success' : 'active'}
percent={percentFunded}
status={percentFunded >= 100 ? 'success' : 'active'}
showInfo={false}
/>

View File

@ -13,35 +13,40 @@ import './style.less';
type ProposalSortFn = (p1: ProposalWithCrowdFund, p2: ProposalWithCrowdFund) => number;
const sortFunctions: { [key in PROPOSAL_SORT]: ProposalSortFn } = {
// TODO: Move sorts server side due to pagination
[PROPOSAL_SORT.NEWEST]: (p1, p2) => p2.dateCreated - p1.dateCreated,
[PROPOSAL_SORT.OLDEST]: (p1, p2) => p1.dateCreated - p2.dateCreated,
[PROPOSAL_SORT.LEAST_FUNDED]: (p1, p2) => {
// TODO: Fix least funded sort
return p1.proposalId - p2.proposalId;
// First show sub-100% funding
const p1Pct = p1.crowdFund.percentFunded;
const p2Pct = p2.crowdFund.percentFunded;
if (p1Pct < 1 && p2Pct >= 1) {
return -1;
} else if (p2Pct < 1 && p1Pct >= 1) {
return 1;
} else if (p1Pct < 1 && p2Pct < 1) {
return p1Pct - p2Pct;
}
// const p1Pct = p1.crowdFund.percentFunded;
// const p2Pct = p2.crowdFund.percentFunded;
// if (p1Pct < 1 && p2Pct >= 1) {
// return -1;
// } else if (p2Pct < 1 && p1Pct >= 1) {
// return 1;
// } else if (p1Pct < 1 && p2Pct < 1) {
// return p1Pct - p2Pct;
// }
// Then show most overall funds
return p1.crowdFund.funded.cmp(p2.crowdFund.funded);
// return p1.crowdFund.funded.cmp(p2.crowdFund.funded);
},
[PROPOSAL_SORT.MOST_FUNDED]: (p1, p2) => {
// TODO: Fix most funded sort
return p2.proposalId - p1.proposalId;
// First show sub-100% funding
const p1Pct = p1.crowdFund.percentFunded;
const p2Pct = p2.crowdFund.percentFunded;
if (p1Pct < 1 && p2Pct >= 1) {
return 1;
} else if (p2Pct < 1 && p1Pct >= 1) {
return -1;
} else if (p1Pct < 1 && p2Pct < 1) {
return p2Pct - p1Pct;
}
// const p1Pct = p1.crowdFund.percentFunded;
// const p2Pct = p2.crowdFund.percentFunded;
// if (p1Pct < 1 && p2Pct >= 1) {
// return 1;
// } else if (p2Pct < 1 && p1Pct >= 1) {
// return -1;
// } else if (p1Pct < 1 && p2Pct < 1) {
// return p2Pct - p1Pct;
// }
// Then show most overall funds
return p2.crowdFund.funded.cmp(p1.crowdFund.funded);
// return p2.crowdFund.funded.cmp(p1.crowdFund.funded);
},
};

View File

@ -2,6 +2,7 @@ import { Dispatch } from 'redux';
import { ProposalDraft } from 'types';
// import { AppState } from 'store/reducers';
import types, { CreateDraftOptions } from './types';
import { putProposal, putProposalPublish } from 'api/api';
// type GetState = () => AppState;
@ -44,6 +45,22 @@ export function deleteDraft(proposalId: number) {
};
}
export function createProposal(form: ProposalDraft) {
console.log('TODO - implement createProposal', form);
export function submitProposal(form: ProposalDraft) {
return async (dispatch: Dispatch<any>) => {
dispatch({ type: types.SUBMIT_PROPOSAL_PENDING });
try {
await putProposal(form);
const res = await putProposalPublish(form);
dispatch({
type: types.SUBMIT_PROPOSAL_FULFILLED,
payload: res.data,
});
} catch(err) {
dispatch({
type: types.SUBMIT_PROPOSAL_REJECTED,
payload: err.message || err.toString(),
error: true,
});
}
};
}

View File

@ -1,5 +1,5 @@
import types from './types';
import { ProposalDraft } from 'types';
import { ProposalDraft, Proposal } from 'types';
export interface CreateState {
drafts: ProposalDraft[] | null;
@ -20,6 +20,10 @@ export interface CreateState {
isDeletingDraft: boolean;
deleteDraftError: string | null;
submittedProposal: Proposal | null;
isSubmitting: boolean;
submitError: string | null;
}
export const INITIAL_STATE: CreateState = {
@ -41,6 +45,10 @@ export const INITIAL_STATE: CreateState = {
isDeletingDraft: false,
deleteDraftError: null,
submittedProposal: null,
isSubmitting: false,
submitError: null,
};
export default function createReducer(
@ -48,8 +56,6 @@ export default function createReducer(
action: any,
): CreateState {
switch (action.type) {
case types.CREATE_DRAFT_PENDING:
case types.UPDATE_FORM:
return {
...state,
@ -156,6 +162,25 @@ export default function createReducer(
isDeletingDraft: false,
deleteDraftError: action.payload,
};
case types.SUBMIT_PROPOSAL_PENDING:
return {
...state,
isSubmitting: true,
submitError: null,
};
case types.SUBMIT_PROPOSAL_FULFILLED:
return {
...state,
submittedProposal: action.payload,
isSubmitting: false,
};
case types.SUBMIT_PROPOSAL_REJECTED:
return {
...state,
submitError: action.payload,
isSubmitting: false,
}
}
return state;
}

View File

@ -26,10 +26,10 @@ enum CreateTypes {
DELETE_DRAFT_FULFILLED = 'DELETE_DRAFT_FULFILLED',
DELETE_DRAFT_REJECTED = 'DELETE_DRAFT_REJECTED',
SUBMIT = 'CREATE_PROPOSAL',
SUBMIT_PENDING = 'CREATE_PROPOSAL_PENDING',
SUBMIT_FULFILLED = 'CREATE_PROPOSAL_FULFILLED',
SUBMIT_REJECTED = 'CREATE_PROPOSAL_REJECTED',
SUBMIT_PROPOSAL = 'SUBMIT_PROPOSAL',
SUBMIT_PROPOSAL_PENDING = 'SUBMIT_PROPOSAL_PENDING',
SUBMIT_PROPOSAL_FULFILLED = 'SUBMIT_PROPOSAL_FULFILLED',
SUBMIT_PROPOSAL_REJECTED = 'SUBMIT_PROPOSAL_REJECTED',
}
export interface CreateDraftOptions {

View File

@ -17,10 +17,8 @@ interface CreateFormErrors {
team?: string[];
content?: string;
payoutAddress?: string;
trustees?: string[];
milestones?: string[];
deadlineDuration?: string;
voteDuration?: string;
}
export type KeyOfForm = keyof CreateFormErrors;
@ -32,10 +30,8 @@ export const FIELD_NAME_MAP: { [key in KeyOfForm]: string } = {
team: 'Team',
content: 'Details',
payoutAddress: 'Payout address',
trustees: 'Trustees',
milestones: 'Milestones',
deadlineDuration: 'Funding deadline',
voteDuration: 'Milestone deadline',
};
const requiredFields = [
@ -45,9 +41,7 @@ const requiredFields = [
'target',
'content',
'payoutAddress',
'trustees',
'deadlineDuration',
'voteDuration',
];
export function getCreateErrors(
@ -55,7 +49,7 @@ export function getCreateErrors(
skipRequired?: boolean,
): CreateFormErrors {
const errors: CreateFormErrors = {};
const { title, team, milestones, target, payoutAddress, trustees } = form;
const { title, team, milestones, target, payoutAddress } = form;
// Required fields with no extra validation
if (!skipRequired) {
@ -92,31 +86,6 @@ export function getCreateErrors(
errors.payoutAddress = 'That doesnt look like a valid address';
}
// Trustees
if (trustees) {
let didTrusteeError = false;
const trusteeErrors = trustees.map((address, idx) => {
if (!address) {
return '';
}
let err = '';
if (!address) {
err = 'That doesnt look like a valid address';
} else if (trustees.indexOf(address) !== idx) {
err = 'That address is already a trustee';
} else if (payoutAddress === address) {
err = 'That address is already a trustee';
}
didTrusteeError = didTrusteeError || !!err;
return err;
});
if (didTrusteeError) {
errors.trustees = trusteeErrors;
}
}
// Milestones
if (milestones) {
let didMilestoneError = false;
@ -193,10 +162,10 @@ export function proposalToContractData(form: ProposalDraft): any {
return {
ethAmount: targetInWei,
payoutAddress: form.payoutAddress,
trusteesAddresses: form.trustees,
trusteesAddresses: [],
milestoneAmounts,
durationInMinutes: form.deadlineDuration || ONE_DAY * 60,
milestoneVotingPeriodInMinutes: form.voteDuration || ONE_DAY * 7,
milestoneVotingPeriodInMinutes: ONE_DAY * 7,
immediateFirstMilestonePayout,
};
}
@ -243,7 +212,7 @@ export function makeProposalPreviewFromDraft(
amountVotingForRefund: Wei('0'),
percentVotingForRefund: 0,
beneficiary: draft.payoutAddress,
trustees: draft.trustees,
trustees: [],
deadline: Date.now() + 100000,
contributors: [],
milestones: [],

View File

@ -1,6 +1,6 @@
import BN from 'bn.js';
import { socialMediaToUrl } from 'utils/social';
import { User, CrowdFund, ProposalWithCrowdFund, UserProposal } from 'types';
import { User, ProposalWithCrowdFund, UserProposal } from 'types';
import { UserState } from 'modules/users/reducers';
import { AppState } from 'store/reducers';
@ -23,34 +23,8 @@ export function formatUserFromGet(user: UserState) {
return user;
}
export function formatCrowdFundFromGet(crowdFund: CrowdFund, base = 10): CrowdFund {
const bnKeys = ['amountVotingForRefund', 'balance', 'funded', 'target'] as Array<
keyof CrowdFund
>;
bnKeys.forEach(k => {
crowdFund[k] = new BN(crowdFund[k] as string, base);
});
crowdFund.milestones = crowdFund.milestones.map(ms => {
ms.amount = new BN(ms.amount, base);
ms.amountAgainstPayout = new BN(ms.amountAgainstPayout, base);
return ms;
});
crowdFund.contributors = crowdFund.contributors.map(c => {
c.contributionAmount = new BN(c.contributionAmount, base);
return c;
});
return crowdFund;
}
export function formatProposalFromGet(proposal: ProposalWithCrowdFund) {
proposal.proposalUrlId = generateProposalUrl(proposal.proposalId, proposal.title);
proposal.crowdFund = formatCrowdFundFromGet(proposal.crowdFund);
for (let i = 0; i < proposal.crowdFund.milestones.length; i++) {
proposal.milestones[i] = {
...proposal.milestones[i],
...proposal.crowdFund.milestones[i],
};
}
return proposal;
}
@ -76,16 +50,6 @@ export function extractProposalIdFromUrl(slug: string) {
// pre-hydration massage (BNify JSONed BNs)
export function massageSerializedState(state: AppState) {
// proposals
state.proposal.proposals.forEach(p => {
formatCrowdFundFromGet(p.crowdFund, 16);
for (let i = 0; i < p.crowdFund.milestones.length; i++) {
p.milestones[i] = {
...p.milestones[i],
...p.crowdFund.milestones[i],
};
}
});
// users
const bnUserProp = (p: UserProposal) => {
p.funded = new BN(p.funded, 16);

View File

@ -36,12 +36,12 @@ const HTML: React.SFC<Props> = ({
<meta name="msapplication-TileColor" content="#fff" />
<meta name="theme-color" content="#fff" />
{/* TODO: import from @fortawesome */}
<link
{/* <link
rel="stylesheet"
href="https://use.fontawesome.com/releases/v5.2.0/css/all.css"
integrity="sha384-hWVjflwFxL6sNzntih27bfxkr27PmbbK/iSvJ+a4+0owXq79v+lsFkW54bOGbiDQ"
crossOrigin="anonymous"
/>
/> */}
{/* Custom link & meta tags from webpack */}
{linkTags.map((l, idx) => (
<link key={idx} {...l as any} />

View File

@ -18,9 +18,8 @@ const msPaid = { state: PAID, isPaid: true };
const msActive = { state: ACTIVE, isPaid: false };
const msRejected = { state: REJECTED, isPaid: false };
const dummyProposal = getProposalWithCrowdFund({});
const trustee = dummyProposal.crowdFund.beneficiary;
const contributor = dummyProposal.crowdFund.contributors[0].address;
const trustee = 'z123';
const contributor = 'z456';
const refundedProposal = getProposalWithCrowdFund({ amount: 5, funded: 5 });
refundedProposal.crowdFund.percentVotingForRefund = 100;

View File

@ -53,9 +53,7 @@ export interface ProposalDraft {
stage: string;
target: string;
payoutAddress: string;
trustees: string[];
deadlineDuration: number;
voteDuration: number;
milestones: CreateMilestone[];
team: User[];
invites: TeamInvite[];