2018-09-20 11:58:47 -07:00
|
|
|
|
import React from 'react';
|
2019-02-19 11:48:51 -08:00
|
|
|
|
import { connect } from 'react-redux';
|
2019-03-06 12:25:58 -08:00
|
|
|
|
import { Input, Form, Icon, Select, Alert, Popconfirm, message, Radio } from 'antd';
|
2018-10-19 15:03:37 -07:00
|
|
|
|
import { SelectValue } from 'antd/lib/select';
|
2019-03-06 12:25:58 -08:00
|
|
|
|
import { RadioChangeEvent } from 'antd/lib/radio';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import { PROPOSAL_CATEGORY, CATEGORY_UI } from 'api/constants';
|
2019-02-01 11:13:30 -08:00
|
|
|
|
import { ProposalDraft, RFP } from 'types';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import { getCreateErrors } from 'modules/create/utils';
|
2018-10-19 15:03:37 -07:00
|
|
|
|
import { typedKeys } from 'utils/ts';
|
2019-02-01 11:13:30 -08:00
|
|
|
|
import { Link } from 'react-router-dom';
|
2019-02-19 11:48:51 -08:00
|
|
|
|
import { unlinkProposalRFP } from 'modules/create/actions';
|
|
|
|
|
import { AppState } from 'store/reducers';
|
|
|
|
|
|
|
|
|
|
interface OwnProps {
|
|
|
|
|
proposalId: number;
|
|
|
|
|
initialState?: Partial<State>;
|
|
|
|
|
updateForm(form: Partial<ProposalDraft>): void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface StateProps {
|
|
|
|
|
isUnlinkingProposalRFP: AppState['create']['isUnlinkingProposalRFP'];
|
|
|
|
|
unlinkProposalRFPError: AppState['create']['unlinkProposalRFPError'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface DispatchProps {
|
|
|
|
|
unlinkProposalRFP: typeof unlinkProposalRFP;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Props = OwnProps & StateProps & DispatchProps;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
2018-11-14 08:43:00 -08:00
|
|
|
|
interface State extends Partial<ProposalDraft> {
|
2018-09-20 11:58:47 -07:00
|
|
|
|
title: string;
|
|
|
|
|
brief: string;
|
2018-11-14 08:43:00 -08:00
|
|
|
|
category?: PROPOSAL_CATEGORY;
|
|
|
|
|
target: string;
|
2019-02-01 11:13:30 -08:00
|
|
|
|
rfp?: RFP;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-19 11:48:51 -08:00
|
|
|
|
class CreateFlowBasics extends React.Component<Props, State> {
|
2018-09-20 11:58:47 -07:00
|
|
|
|
constructor(props: Props) {
|
|
|
|
|
super(props);
|
|
|
|
|
this.state = {
|
|
|
|
|
title: '',
|
|
|
|
|
brief: '',
|
2018-11-14 08:43:00 -08:00
|
|
|
|
category: undefined,
|
|
|
|
|
target: '',
|
2018-09-20 11:58:47 -07:00
|
|
|
|
...(props.initialState || {}),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-19 11:48:51 -08:00
|
|
|
|
componentDidUpdate(prevProps: Props) {
|
|
|
|
|
const { unlinkProposalRFPError, isUnlinkingProposalRFP } = this.props;
|
|
|
|
|
if (
|
|
|
|
|
unlinkProposalRFPError &&
|
|
|
|
|
unlinkProposalRFPError !== prevProps.unlinkProposalRFPError
|
|
|
|
|
) {
|
|
|
|
|
console.error('Failed to unlink request:', unlinkProposalRFPError);
|
|
|
|
|
message.error('Failed to unlink request');
|
|
|
|
|
} else if (!isUnlinkingProposalRFP && prevProps.isUnlinkingProposalRFP) {
|
|
|
|
|
this.setState({ rfp: undefined });
|
|
|
|
|
message.success('Unlinked proposal from request');
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
|
|
|
|
render() {
|
2019-02-19 11:48:51 -08:00
|
|
|
|
const { isUnlinkingProposalRFP } = this.props;
|
2019-03-06 12:25:58 -08:00
|
|
|
|
const { title, brief, category, target, rfp, rfpOptIn } = this.state;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
const errors = getCreateErrors(this.state, true);
|
|
|
|
|
|
2019-03-28 10:25:34 -07:00
|
|
|
|
// Don't show target error at zero since it defaults to that
|
|
|
|
|
// Error just shows up at the end to prevent submission
|
|
|
|
|
if (target === '0') {
|
|
|
|
|
errors.target = undefined;
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-06 12:25:58 -08:00
|
|
|
|
const rfpOptInRequired =
|
2019-04-04 17:16:00 -07:00
|
|
|
|
rfp && (rfp.matching || (rfp.bounty && parseFloat(rfp.bounty.toString()) > 0));
|
2019-03-06 12:25:58 -08:00
|
|
|
|
|
2018-09-20 11:58:47 -07:00
|
|
|
|
return (
|
|
|
|
|
<Form layout="vertical" style={{ maxWidth: 600, margin: '0 auto' }}>
|
2019-02-01 11:13:30 -08:00
|
|
|
|
{rfp && (
|
|
|
|
|
<Alert
|
2019-03-06 12:25:58 -08:00
|
|
|
|
className="CreateFlow-rfpAlert"
|
2019-02-01 11:13:30 -08:00
|
|
|
|
type="info"
|
|
|
|
|
message="This proposal is linked to a request"
|
|
|
|
|
description={
|
|
|
|
|
<>
|
|
|
|
|
This proposal is for the open request{' '}
|
2019-03-14 18:55:13 -07:00
|
|
|
|
<Link to={`/requests/${rfp.id}`} target="_blank">
|
2019-02-01 11:13:30 -08:00
|
|
|
|
{rfp.title}
|
|
|
|
|
</Link>
|
2019-02-19 11:48:51 -08:00
|
|
|
|
. If you didn’t mean to do this, or want to unlink it,{' '}
|
|
|
|
|
<Popconfirm
|
|
|
|
|
title="Are you sure? This cannot be undone."
|
|
|
|
|
onConfirm={this.unlinkRfp}
|
|
|
|
|
okButtonProps={{ loading: isUnlinkingProposalRFP }}
|
|
|
|
|
>
|
|
|
|
|
<a>click here</a>
|
|
|
|
|
</Popconfirm>{' '}
|
|
|
|
|
to do so.
|
2019-02-01 11:13:30 -08:00
|
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
showIcon
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
2019-03-06 12:25:58 -08:00
|
|
|
|
{rfpOptInRequired && (
|
|
|
|
|
<Alert
|
|
|
|
|
className="CreateFlow-rfpAlert"
|
|
|
|
|
type="warning"
|
|
|
|
|
message="KYC (know your customer)"
|
|
|
|
|
description={
|
|
|
|
|
<>
|
|
|
|
|
<div>
|
|
|
|
|
This RFP offers either a bounty or matching. This will require ZFGrants
|
|
|
|
|
to fulfill{' '}
|
|
|
|
|
<a
|
|
|
|
|
target="_blank"
|
|
|
|
|
href="https://en.wikipedia.org/wiki/Know_your_customer"
|
|
|
|
|
>
|
|
|
|
|
KYC
|
|
|
|
|
</a>{' '}
|
|
|
|
|
due dilligence. In the event your proposal is successful, you will need
|
|
|
|
|
to provide identifying information to ZFGrants.
|
|
|
|
|
<Radio.Group onChange={this.handleRfpOptIn}>
|
|
|
|
|
<Radio value={true} checked={rfpOptIn && rfpOptIn === true}>
|
|
|
|
|
<b>Yes</b>, I am willing to provide KYC information
|
|
|
|
|
</Radio>
|
|
|
|
|
<Radio
|
|
|
|
|
value={false}
|
|
|
|
|
checked={rfpOptIn !== null && rfpOptIn === false}
|
|
|
|
|
>
|
|
|
|
|
<b>No</b>, I do not wish to provide KYC information and understand I
|
|
|
|
|
will not receive any matching or bounty funds from ZFGrants
|
|
|
|
|
</Radio>
|
|
|
|
|
</Radio.Group>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
2018-09-20 11:58:47 -07:00
|
|
|
|
<Form.Item
|
|
|
|
|
label="Title"
|
|
|
|
|
validateStatus={errors.title ? 'error' : undefined}
|
|
|
|
|
help={errors.title}
|
|
|
|
|
>
|
|
|
|
|
<Input
|
|
|
|
|
size="large"
|
|
|
|
|
name="title"
|
|
|
|
|
placeholder="Short and sweet"
|
|
|
|
|
type="text"
|
|
|
|
|
value={title}
|
|
|
|
|
onChange={this.handleInputChange}
|
2019-03-18 11:35:08 -07:00
|
|
|
|
maxLength={200}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
label="Brief"
|
|
|
|
|
validateStatus={errors.brief ? 'error' : undefined}
|
|
|
|
|
help={errors.brief}
|
|
|
|
|
>
|
|
|
|
|
<Input.TextArea
|
|
|
|
|
name="brief"
|
|
|
|
|
placeholder="An elevator-pitch version of your proposal, max 140 chars"
|
|
|
|
|
value={brief}
|
|
|
|
|
onChange={this.handleInputChange}
|
|
|
|
|
rows={3}
|
2019-03-18 11:35:08 -07:00
|
|
|
|
maxLength={200}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
<Form.Item label="Category">
|
|
|
|
|
<Select
|
|
|
|
|
size="large"
|
|
|
|
|
placeholder="Select a category"
|
|
|
|
|
value={category || undefined}
|
|
|
|
|
onChange={this.handleCategoryChange}
|
|
|
|
|
>
|
2018-10-19 15:03:37 -07:00
|
|
|
|
{typedKeys(PROPOSAL_CATEGORY).map(c => (
|
2018-09-20 11:58:47 -07:00
|
|
|
|
<Select.Option value={c} key={c}>
|
|
|
|
|
<Icon
|
|
|
|
|
type={CATEGORY_UI[c].icon}
|
|
|
|
|
style={{ color: CATEGORY_UI[c].color }}
|
|
|
|
|
/>{' '}
|
|
|
|
|
{CATEGORY_UI[c].label}
|
|
|
|
|
</Select.Option>
|
|
|
|
|
))}
|
|
|
|
|
</Select>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
label="Target amount"
|
2018-11-14 08:43:00 -08:00
|
|
|
|
validateStatus={errors.target ? 'error' : undefined}
|
|
|
|
|
help={errors.target || 'This cannot be changed once your proposal starts'}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
>
|
|
|
|
|
<Input
|
|
|
|
|
size="large"
|
2018-11-14 08:43:00 -08:00
|
|
|
|
name="target"
|
2018-09-20 11:58:47 -07:00
|
|
|
|
placeholder="1.5"
|
|
|
|
|
type="number"
|
2018-11-14 08:43:00 -08:00
|
|
|
|
value={target}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
onChange={this.handleInputChange}
|
2018-12-27 09:41:26 -08:00
|
|
|
|
addonAfter="ZEC"
|
2019-03-18 11:47:46 -07:00
|
|
|
|
maxLength={16}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
);
|
|
|
|
|
}
|
2019-02-19 11:48:51 -08:00
|
|
|
|
|
|
|
|
|
private handleInputChange = (
|
|
|
|
|
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
|
|
|
|
|
) => {
|
|
|
|
|
const { value, name } = event.currentTarget;
|
|
|
|
|
this.setState({ [name]: value } as any, () => {
|
|
|
|
|
this.props.updateForm(this.state);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private handleCategoryChange = (value: SelectValue) => {
|
|
|
|
|
this.setState({ category: value as PROPOSAL_CATEGORY }, () => {
|
|
|
|
|
this.props.updateForm(this.state);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2019-03-06 12:25:58 -08:00
|
|
|
|
private handleRfpOptIn = (e: RadioChangeEvent) => {
|
|
|
|
|
this.setState({ rfpOptIn: e.target.value }, () => {
|
|
|
|
|
this.props.updateForm(this.state);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2019-02-19 11:48:51 -08:00
|
|
|
|
private unlinkRfp = () => {
|
|
|
|
|
this.props.unlinkProposalRFP(this.props.proposalId);
|
|
|
|
|
};
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
2019-02-19 11:48:51 -08:00
|
|
|
|
|
|
|
|
|
export default connect<StateProps, DispatchProps, OwnProps, AppState>(
|
|
|
|
|
state => ({
|
|
|
|
|
isUnlinkingProposalRFP: state.create.isUnlinkingProposalRFP,
|
|
|
|
|
unlinkProposalRFPError: state.create.unlinkProposalRFPError,
|
|
|
|
|
}),
|
|
|
|
|
{ unlinkProposalRFP },
|
|
|
|
|
)(CreateFlowBasics);
|