Refactor to a state where /proposlas loads.
This commit is contained in:
parent
eea3eea0f7
commit
5931de5460
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
|
@ -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);
|
||||
});
|
||||
};
|
||||
}
|
|
@ -46,7 +46,7 @@ export default class PublishWarningModal extends React.Component<Props> {
|
|||
<p>
|
||||
Are you sure you’re ready to publish your proposal? Once you’ve 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>
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
@ -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 doesn’t look like an email address or ETH address'
|
||||
: undefined;
|
||||
const inviteError = address && !isValidEmail(address) &&
|
||||
'That doesn’t 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}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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 who’s in control',
|
||||
subtitle:
|
||||
'Everything here cannot be changed after publishing, so make sure it’s right',
|
||||
[CREATE_STEP.PAYMENT]: {
|
||||
short: 'Payment',
|
||||
title: 'Choose how you get paid',
|
||||
subtitle: 'You’ll only be paid if your funding target is reached',
|
||||
help:
|
||||
'Double check everything! This data powers the smart contract, and is immutable once it’s deployed.',
|
||||
component: Governance,
|
||||
'Double check your address, and make sure it’s 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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 isn’t 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;
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 isn’t 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 you’ll 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 don’t
|
||||
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} />;
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 doesn’t 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 doesn’t 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: [],
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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[];
|
||||
|
|
Loading…
Reference in New Issue