2018-09-20 11:58:47 -07:00
|
|
|
|
import React from 'react';
|
|
|
|
|
import { connect } from 'react-redux';
|
2019-11-13 15:23:36 -08:00
|
|
|
|
import { Timeline } from 'antd';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import { getCreateErrors, KeyOfForm, FIELD_NAME_MAP } from 'modules/create/utils';
|
|
|
|
|
import Markdown from 'components/Markdown';
|
2018-11-14 08:43:00 -08:00
|
|
|
|
import UserAvatar from 'components/UserAvatar';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import { AppState } from 'store/reducers';
|
|
|
|
|
import { CREATE_STEP } from './index';
|
2018-11-14 08:43:00 -08:00
|
|
|
|
import { ProposalDraft } from 'types';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import './Review.less';
|
|
|
|
|
|
|
|
|
|
interface OwnProps {
|
|
|
|
|
setStep(step: CREATE_STEP): void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface StateProps {
|
2018-11-14 08:43:00 -08:00
|
|
|
|
form: ProposalDraft;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type Props = OwnProps & StateProps;
|
|
|
|
|
|
|
|
|
|
interface Field {
|
|
|
|
|
key: KeyOfForm;
|
|
|
|
|
content: React.ReactNode;
|
2018-10-19 15:03:37 -07:00
|
|
|
|
error: string | Falsy;
|
2019-03-06 12:25:58 -08:00
|
|
|
|
isHide?: boolean;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Section {
|
|
|
|
|
step: CREATE_STEP;
|
|
|
|
|
name: string;
|
|
|
|
|
fields: Field[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class CreateReview extends React.Component<Props> {
|
|
|
|
|
render() {
|
|
|
|
|
const { form } = this.props;
|
|
|
|
|
const errors = getCreateErrors(this.props.form);
|
|
|
|
|
const sections: Section[] = [
|
|
|
|
|
{
|
|
|
|
|
step: CREATE_STEP.BASICS,
|
|
|
|
|
name: 'Basics',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
key: 'title',
|
|
|
|
|
content: <h2 style={{ fontSize: '1.6rem', margin: 0 }}>{form.title}</h2>,
|
|
|
|
|
error: errors.title,
|
|
|
|
|
},
|
2019-03-06 12:25:58 -08:00
|
|
|
|
{
|
|
|
|
|
key: 'rfpOptIn',
|
|
|
|
|
content: <div>{form.rfpOptIn ? 'Accepted' : 'Declined'}</div>,
|
|
|
|
|
error: errors.rfpOptIn,
|
|
|
|
|
isHide: !form.rfp || (form.rfp && !form.rfp.matching && !form.rfp.bounty),
|
|
|
|
|
},
|
2018-09-20 11:58:47 -07:00
|
|
|
|
{
|
|
|
|
|
key: 'brief',
|
|
|
|
|
content: form.brief,
|
|
|
|
|
error: errors.brief,
|
|
|
|
|
},
|
|
|
|
|
{
|
2018-11-14 08:43:00 -08:00
|
|
|
|
key: 'target',
|
2018-12-27 09:41:26 -08:00
|
|
|
|
content: <div style={{ fontSize: '1.2rem' }}>{form.target} ZEC</div>,
|
2018-11-14 08:43:00 -08:00
|
|
|
|
error: errors.target,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
2018-09-27 13:25:49 -07:00
|
|
|
|
{
|
|
|
|
|
step: CREATE_STEP.TEAM,
|
|
|
|
|
name: 'Team',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
key: 'team',
|
2018-11-16 08:57:03 -08:00
|
|
|
|
content: <ReviewTeam team={form.team} invites={form.invites} />,
|
2018-09-27 13:25:49 -07:00
|
|
|
|
error: errors.team && errors.team.join(' '),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
2018-09-20 11:58:47 -07:00
|
|
|
|
{
|
|
|
|
|
step: CREATE_STEP.DETAILS,
|
|
|
|
|
name: 'Details',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
2018-11-14 09:59:48 -08:00
|
|
|
|
key: 'content',
|
|
|
|
|
content: <Markdown source={form.content} />,
|
2018-11-14 13:21:41 -08:00
|
|
|
|
error: errors.content,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
step: CREATE_STEP.MILESTONES,
|
|
|
|
|
name: 'Milestones',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
key: 'milestones',
|
|
|
|
|
content: <ReviewMilestones milestones={form.milestones} />,
|
|
|
|
|
error: errors.milestones && errors.milestones.join(' '),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
2018-12-21 10:27:39 -08:00
|
|
|
|
step: CREATE_STEP.PAYMENT,
|
2019-03-04 12:02:51 -08:00
|
|
|
|
name: 'Payment',
|
2018-09-20 11:58:47 -07:00
|
|
|
|
fields: [
|
|
|
|
|
{
|
2018-11-14 08:43:00 -08:00
|
|
|
|
key: 'payoutAddress',
|
|
|
|
|
content: <code>{form.payoutAddress}</code>,
|
|
|
|
|
error: errors.payoutAddress,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
2019-11-20 13:37:26 -08:00
|
|
|
|
{
|
2019-12-02 08:40:24 -08:00
|
|
|
|
step: CREATE_STEP.PAYMENT,
|
2019-11-20 13:37:26 -08:00
|
|
|
|
name: 'Tipping',
|
|
|
|
|
fields: [
|
|
|
|
|
{
|
|
|
|
|
key: 'tipJarAddress',
|
|
|
|
|
content: <code>{form.tipJarAddress}</code>,
|
|
|
|
|
error: errors.tipJarAddress,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
2018-09-20 11:58:47 -07:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="CreateReview">
|
|
|
|
|
{sections.map(s => (
|
2018-10-09 12:30:09 -07:00
|
|
|
|
<div className="CreateReview-section" key={s.step}>
|
2019-03-06 12:25:58 -08:00
|
|
|
|
{s.fields.map(
|
|
|
|
|
f =>
|
|
|
|
|
!f.isHide && (
|
|
|
|
|
<div className="ReviewField" key={f.key}>
|
|
|
|
|
<div className="ReviewField-label">
|
|
|
|
|
{FIELD_NAME_MAP[f.key]}
|
|
|
|
|
{f.error && (
|
|
|
|
|
<div className="ReviewField-label-error">{f.error}</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="ReviewField-content">
|
|
|
|
|
{this.isEmpty(form[f.key]) ? (
|
|
|
|
|
<div className="ReviewField-content-empty">N/A</div>
|
|
|
|
|
) : (
|
|
|
|
|
f.content
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
)}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
<div className="ReviewField">
|
|
|
|
|
<div className="ReviewField-label" />
|
|
|
|
|
<div className="ReviewField-content">
|
|
|
|
|
<button
|
|
|
|
|
className="ReviewField-content-edit"
|
|
|
|
|
onClick={() => this.setStep(s.step)}
|
|
|
|
|
>
|
|
|
|
|
Edit {s.name}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setStep = (step: CREATE_STEP) => {
|
|
|
|
|
this.props.setStep(step);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private isEmpty(value: any) {
|
2019-03-06 12:25:58 -08:00
|
|
|
|
if (typeof value === 'boolean') {
|
|
|
|
|
return false; // defined booleans are never empty
|
|
|
|
|
}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
return !value || value.length === 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default connect<StateProps, {}, OwnProps, AppState>(state => ({
|
2018-11-14 08:43:00 -08:00
|
|
|
|
form: state.create.form as ProposalDraft,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}))(CreateReview);
|
|
|
|
|
|
2018-09-27 13:25:49 -07:00
|
|
|
|
const ReviewMilestones = ({
|
|
|
|
|
milestones,
|
|
|
|
|
}: {
|
2018-11-14 08:43:00 -08:00
|
|
|
|
milestones: ProposalDraft['milestones'];
|
2018-09-27 13:25:49 -07:00
|
|
|
|
}) => (
|
2018-09-20 11:58:47 -07:00
|
|
|
|
<Timeline>
|
|
|
|
|
{milestones.map(m => (
|
2018-10-09 12:30:09 -07:00
|
|
|
|
<Timeline.Item key={m.title}>
|
2018-09-20 11:58:47 -07:00
|
|
|
|
<div className="ReviewMilestone">
|
2019-02-19 13:42:40 -08:00
|
|
|
|
<div className="ReviewMilestone-title">{m.title || <em>No title</em>}</div>
|
2018-09-20 11:58:47 -07:00
|
|
|
|
<div className="ReviewMilestone-info">
|
2019-11-13 14:38:17 -08:00
|
|
|
|
{m.immediatePayout || !m.daysEstimated ? '0' : m.daysEstimated} days
|
2018-09-20 11:58:47 -07:00
|
|
|
|
{' – '}
|
2019-11-13 14:38:17 -08:00
|
|
|
|
{m.payoutPercent || '0'}% of funds
|
2018-09-20 11:58:47 -07:00
|
|
|
|
</div>
|
2019-02-19 13:42:40 -08:00
|
|
|
|
<div className="ReviewMilestone-description">
|
|
|
|
|
{m.content || <em>No description</em>}
|
|
|
|
|
</div>
|
2018-09-20 11:58:47 -07:00
|
|
|
|
</div>
|
|
|
|
|
</Timeline.Item>
|
|
|
|
|
))}
|
|
|
|
|
</Timeline>
|
|
|
|
|
);
|
2018-09-27 13:25:49 -07:00
|
|
|
|
|
2018-11-16 08:57:03 -08:00
|
|
|
|
const ReviewTeam: React.SFC<{
|
|
|
|
|
team: ProposalDraft['team'];
|
|
|
|
|
invites: ProposalDraft['invites'];
|
2019-01-15 11:13:57 -08:00
|
|
|
|
}> = ({ team, invites }) => {
|
|
|
|
|
const pendingInvites = invites.filter(inv => inv.accepted === null).length;
|
|
|
|
|
return (
|
|
|
|
|
<div className="ReviewTeam">
|
|
|
|
|
{team.map((u, idx) => (
|
|
|
|
|
<div className="ReviewTeam-member" key={idx}>
|
|
|
|
|
<UserAvatar className="ReviewTeam-member-avatar" user={u} />
|
|
|
|
|
<div className="ReviewTeam-member-info">
|
|
|
|
|
<div className="ReviewTeam-member-info-name">{u.displayName}</div>
|
|
|
|
|
<div className="ReviewTeam-member-info-title">{u.title}</div>
|
|
|
|
|
</div>
|
2018-09-27 13:25:49 -07:00
|
|
|
|
</div>
|
2019-01-15 11:13:57 -08:00
|
|
|
|
))}
|
|
|
|
|
{!!pendingInvites && (
|
|
|
|
|
<div className="ReviewTeam-invites">+ {pendingInvites} invite(s) pending</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|