2018-09-20 11:58:47 -07:00
|
|
|
|
import React from 'react';
|
|
|
|
|
import { connect } from 'react-redux';
|
|
|
|
|
import { compose } from 'recompose';
|
2018-11-13 08:07:09 -08:00
|
|
|
|
import { Steps, Icon } from 'antd';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import qs from 'query-string';
|
|
|
|
|
import { withRouter, RouteComponentProps } from 'react-router';
|
2018-10-19 15:03:37 -07:00
|
|
|
|
import { History } from 'history';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import { debounce } from 'underscore';
|
|
|
|
|
import Basics from './Basics';
|
2018-09-27 13:25:49 -07:00
|
|
|
|
import Team from './Team';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import Details from './Details';
|
|
|
|
|
import Milestones from './Milestones';
|
2018-12-28 15:05:34 -08:00
|
|
|
|
import Payment from './Payment';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import Review from './Review';
|
|
|
|
|
import Preview from './Preview';
|
|
|
|
|
import Final from './Final';
|
2019-11-13 13:37:12 -08:00
|
|
|
|
import Explainer from './Explainer';
|
2019-01-31 14:56:16 -08:00
|
|
|
|
import SubmitWarningModal from './SubmitWarningModal';
|
2018-09-25 12:49:47 -07:00
|
|
|
|
import createExampleProposal from './example';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import { createActions } from 'modules/create';
|
2018-11-14 08:43:00 -08:00
|
|
|
|
import { ProposalDraft } from 'types';
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import { getCreateErrors } from 'modules/create/utils';
|
2019-11-13 13:37:12 -08:00
|
|
|
|
import ls from 'local-storage';
|
2018-12-14 11:36:22 -08:00
|
|
|
|
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import { AppState } from 'store/reducers';
|
2018-09-25 12:49:47 -07:00
|
|
|
|
|
2018-09-20 11:58:47 -07:00
|
|
|
|
import './index.less';
|
|
|
|
|
|
|
|
|
|
export enum CREATE_STEP {
|
|
|
|
|
BASICS = 'BASICS',
|
2018-09-27 13:25:49 -07:00
|
|
|
|
TEAM = 'TEAM',
|
2018-09-20 11:58:47 -07:00
|
|
|
|
DETAILS = 'DETAILS',
|
|
|
|
|
MILESTONES = 'MILESTONES',
|
2018-12-28 15:05:34 -08:00
|
|
|
|
PAYMENT = 'PAYMENT',
|
2018-09-20 11:58:47 -07:00
|
|
|
|
REVIEW = 'REVIEW',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const STEP_ORDER = [
|
|
|
|
|
CREATE_STEP.BASICS,
|
2018-09-27 13:25:49 -07:00
|
|
|
|
CREATE_STEP.TEAM,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
CREATE_STEP.DETAILS,
|
|
|
|
|
CREATE_STEP.MILESTONES,
|
2018-12-28 15:05:34 -08:00
|
|
|
|
CREATE_STEP.PAYMENT,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
CREATE_STEP.REVIEW,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
interface StepInfo {
|
|
|
|
|
short: string;
|
|
|
|
|
title: React.ReactNode;
|
|
|
|
|
subtitle: React.ReactNode;
|
|
|
|
|
help: React.ReactNode;
|
2018-10-19 15:03:37 -07:00
|
|
|
|
component: any;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
2019-11-13 13:37:12 -08:00
|
|
|
|
|
|
|
|
|
interface LSExplainer {
|
|
|
|
|
noExplain: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-20 11:58:47 -07:00
|
|
|
|
const STEP_INFO: { [key in CREATE_STEP]: StepInfo } = {
|
|
|
|
|
[CREATE_STEP.BASICS]: {
|
|
|
|
|
short: 'Basics',
|
|
|
|
|
title: 'Let’s start with the basics',
|
|
|
|
|
subtitle: 'Don’t worry, you can come back and change things before publishing',
|
|
|
|
|
help:
|
|
|
|
|
'You don’t have to fill out everything at once right now, you can come back later.',
|
|
|
|
|
component: Basics,
|
|
|
|
|
},
|
2018-09-27 13:25:49 -07:00
|
|
|
|
[CREATE_STEP.TEAM]: {
|
|
|
|
|
short: 'Team',
|
|
|
|
|
title: 'Assemble your team',
|
|
|
|
|
subtitle: 'Let everyone know if you’re flying solo, or who you’re working with',
|
|
|
|
|
help:
|
|
|
|
|
'More team members, real names, and linked social accounts adds legitimacy to your proposal',
|
|
|
|
|
component: Team,
|
|
|
|
|
},
|
2018-09-20 11:58:47 -07:00
|
|
|
|
[CREATE_STEP.DETAILS]: {
|
|
|
|
|
short: 'Details',
|
|
|
|
|
title: 'Dive into the details',
|
|
|
|
|
subtitle: 'Here’s your chance to lay out the full proposal, in all its glory',
|
|
|
|
|
help:
|
|
|
|
|
'Make sure people know what you’re building, why you’re qualified, and where the money’s going',
|
|
|
|
|
component: Details,
|
|
|
|
|
},
|
|
|
|
|
[CREATE_STEP.MILESTONES]: {
|
|
|
|
|
short: 'Milestones',
|
|
|
|
|
title: 'Set up milestones for deliverables',
|
|
|
|
|
subtitle: 'Make a timeline of when you’ll complete tasks, and receive funds',
|
|
|
|
|
help:
|
2019-02-19 13:42:40 -08:00
|
|
|
|
'Contributors are more willing to fund proposals with funding spread across multiple milestones',
|
2018-09-20 11:58:47 -07:00
|
|
|
|
component: Milestones,
|
|
|
|
|
},
|
2018-12-28 15:05:34 -08:00
|
|
|
|
[CREATE_STEP.PAYMENT]: {
|
|
|
|
|
short: 'Payment',
|
2019-12-02 08:40:24 -08:00
|
|
|
|
title: 'Set your payout and tip addresses',
|
2019-10-16 20:43:20 -07:00
|
|
|
|
subtitle: '',
|
2018-09-20 11:58:47 -07:00
|
|
|
|
help:
|
2019-12-02 08:40:24 -08:00
|
|
|
|
'Double check your addresses, and make sure they’re secure. Once sent, transactions are irreversible!',
|
2018-12-28 15:05:34 -08:00
|
|
|
|
component: Payment,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
},
|
|
|
|
|
[CREATE_STEP.REVIEW]: {
|
|
|
|
|
short: 'Review',
|
|
|
|
|
title: 'Review your proposal',
|
|
|
|
|
subtitle: 'Feel free to edit any field that doesn’t look right',
|
|
|
|
|
help: 'You’ll get a chance to preview your proposal next before you publish it',
|
|
|
|
|
component: Review,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
interface StateProps {
|
|
|
|
|
form: AppState['create']['form'];
|
|
|
|
|
isSavingDraft: AppState['create']['isSavingDraft'];
|
|
|
|
|
hasSavedDraft: AppState['create']['hasSavedDraft'];
|
2019-03-18 11:35:08 -07:00
|
|
|
|
saveDraftError: AppState['create']['saveDraftError'];
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface DispatchProps {
|
|
|
|
|
updateForm: typeof createActions['updateForm'];
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-03 18:08:29 -08:00
|
|
|
|
type Props = StateProps & DispatchProps & RouteComponentProps<any>;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
|
|
|
|
interface State {
|
|
|
|
|
step: CREATE_STEP;
|
|
|
|
|
isPreviewing: boolean;
|
2019-01-31 14:56:16 -08:00
|
|
|
|
isShowingSubmitWarning: boolean;
|
|
|
|
|
isSubmitting: boolean;
|
2019-11-13 13:37:12 -08:00
|
|
|
|
isExplaining: boolean;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
isExample: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class CreateFlow extends React.Component<Props, State> {
|
|
|
|
|
private historyUnlisten: () => void;
|
2018-11-14 08:43:00 -08:00
|
|
|
|
private debouncedUpdateForm: (form: Partial<ProposalDraft>) => void;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
|
|
|
|
constructor(props: Props) {
|
|
|
|
|
super(props);
|
|
|
|
|
const searchValues = qs.parse(props.location.search);
|
2019-02-04 22:25:02 -08:00
|
|
|
|
const queryStep = searchValues.step ? searchValues.step.toUpperCase() : null;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
const step =
|
2019-02-04 22:25:02 -08:00
|
|
|
|
queryStep && CREATE_STEP[queryStep]
|
|
|
|
|
? (CREATE_STEP[queryStep] as CREATE_STEP)
|
2018-09-20 11:58:47 -07:00
|
|
|
|
: CREATE_STEP.BASICS;
|
2019-11-13 13:37:12 -08:00
|
|
|
|
const noExplain = !!ls<LSExplainer>('noExplain');
|
|
|
|
|
|
2018-09-20 11:58:47 -07:00
|
|
|
|
this.state = {
|
|
|
|
|
step,
|
|
|
|
|
isPreviewing: false,
|
2019-01-31 14:56:16 -08:00
|
|
|
|
isSubmitting: false,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
isExample: false,
|
2019-01-31 14:56:16 -08:00
|
|
|
|
isShowingSubmitWarning: false,
|
2019-11-13 13:37:12 -08:00
|
|
|
|
isExplaining: !noExplain,
|
2018-09-20 11:58:47 -07:00
|
|
|
|
};
|
|
|
|
|
this.debouncedUpdateForm = debounce(this.updateForm, 800);
|
2018-10-19 15:03:37 -07:00
|
|
|
|
this.historyUnlisten = this.props.history.listen(this.handlePop);
|
2018-09-20 11:58:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
componentWillUnmount() {
|
|
|
|
|
if (this.historyUnlisten) {
|
|
|
|
|
this.historyUnlisten();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render() {
|
2019-03-18 11:35:08 -07:00
|
|
|
|
const { isSavingDraft, saveDraftError } = this.props;
|
2019-11-13 13:37:12 -08:00
|
|
|
|
const {
|
|
|
|
|
step,
|
|
|
|
|
isPreviewing,
|
|
|
|
|
isSubmitting,
|
|
|
|
|
isShowingSubmitWarning,
|
|
|
|
|
isExplaining,
|
|
|
|
|
} = this.state;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
|
|
|
|
|
const info = STEP_INFO[step];
|
|
|
|
|
const currentIndex = STEP_ORDER.indexOf(step);
|
2019-11-24 07:07:03 -08:00
|
|
|
|
const isLastStep = currentIndex === STEP_ORDER.length - 1;
|
|
|
|
|
const isSecondToLastStep = currentIndex === STEP_ORDER.length - 2;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
const StepComponent = info.component;
|
|
|
|
|
|
|
|
|
|
let content;
|
|
|
|
|
let showFooter = true;
|
2019-01-31 14:56:16 -08:00
|
|
|
|
if (isSubmitting) {
|
2019-02-05 12:26:37 -08:00
|
|
|
|
content = <Final goBack={this.cancelSubmit} />;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
showFooter = false;
|
|
|
|
|
} else if (isPreviewing) {
|
|
|
|
|
content = <Preview />;
|
2019-11-13 13:37:12 -08:00
|
|
|
|
} else if (isExplaining) {
|
|
|
|
|
content = <Explainer startSteps={this.startSteps} />;
|
|
|
|
|
showFooter = false;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
} else {
|
2019-02-01 11:13:30 -08:00
|
|
|
|
// Antd definitions are missing `onClick` for step, even though it works.
|
|
|
|
|
const Step = Steps.Step as any;
|
2018-09-20 11:58:47 -07:00
|
|
|
|
content = (
|
|
|
|
|
<div className="CreateFlow">
|
|
|
|
|
<div className="CreateFlow-header">
|
|
|
|
|
<Steps current={currentIndex}>
|
2019-11-24 07:07:03 -08:00
|
|
|
|
{STEP_ORDER.map(s => (
|
2019-02-01 11:13:30 -08:00
|
|
|
|
<Step
|
2018-09-20 11:58:47 -07:00
|
|
|
|
key={s}
|
|
|
|
|
title={STEP_INFO[s].short}
|
|
|
|
|
onClick={() => this.setStep(s)}
|
|
|
|
|
style={{ cursor: 'pointer' }}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</Steps>
|
|
|
|
|
<h1 className="CreateFlow-header-title">{info.title}</h1>
|
|
|
|
|
<div className="CreateFlow-header-subtitle">{info.subtitle}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="CreateFlow-content">
|
|
|
|
|
<StepComponent
|
2018-11-16 08:57:03 -08:00
|
|
|
|
proposalId={this.props.form && this.props.form.proposalId}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
initialState={this.props.form}
|
|
|
|
|
updateForm={this.debouncedUpdateForm}
|
|
|
|
|
setStep={this.setStep}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
{content}
|
|
|
|
|
{showFooter && (
|
|
|
|
|
<div className="CreateFlow-footer">
|
|
|
|
|
{isLastStep ? (
|
|
|
|
|
<>
|
|
|
|
|
<button
|
|
|
|
|
className="CreateFlow-footer-button"
|
|
|
|
|
key="preview"
|
|
|
|
|
onClick={this.togglePreview}
|
|
|
|
|
>
|
|
|
|
|
{isPreviewing ? 'Back to Edit' : 'Preview'}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
className="CreateFlow-footer-button is-primary"
|
2019-01-09 10:23:08 -08:00
|
|
|
|
key="submit"
|
2018-11-16 08:57:03 -08:00
|
|
|
|
onClick={this.openPublishWarning}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
disabled={this.checkFormErrors()}
|
|
|
|
|
>
|
2019-01-09 10:23:08 -08:00
|
|
|
|
Submit
|
2018-09-20 11:58:47 -07:00
|
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<div className="CreateFlow-footer-help">{info.help}</div>
|
|
|
|
|
<button
|
|
|
|
|
className="CreateFlow-footer-button"
|
|
|
|
|
key="next"
|
|
|
|
|
onClick={this.nextStep}
|
|
|
|
|
>
|
2019-11-24 07:07:03 -08:00
|
|
|
|
{isSecondToLastStep ? 'Review' : 'Continue' } <Icon type="right-circle-o" />
|
2018-09-20 11:58:47 -07:00
|
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
|
2019-03-12 16:04:32 -07:00
|
|
|
|
{process.env.NODE_ENV !== 'production' && (
|
|
|
|
|
<button className="CreateFlow-footer-example" onClick={this.fillInExample}>
|
|
|
|
|
<Icon type="fast-forward" />
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
</div>
|
|
|
|
|
)}
|
2019-03-18 11:35:08 -07:00
|
|
|
|
{isSavingDraft ? (
|
2018-09-20 11:58:47 -07:00
|
|
|
|
<div className="CreateFlow-draftNotification">Saving draft...</div>
|
2019-03-18 11:35:08 -07:00
|
|
|
|
) : (
|
|
|
|
|
saveDraftError && (
|
|
|
|
|
<div className="CreateFlow-draftNotification is-error">
|
|
|
|
|
Failed to save draft!
|
|
|
|
|
<br />
|
|
|
|
|
{saveDraftError}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
2018-09-20 11:58:47 -07:00
|
|
|
|
)}
|
2019-01-31 14:56:16 -08:00
|
|
|
|
<SubmitWarningModal
|
2018-11-16 08:57:03 -08:00
|
|
|
|
proposal={this.props.form}
|
2019-01-31 14:56:16 -08:00
|
|
|
|
isVisible={isShowingSubmitWarning}
|
2018-11-16 08:57:03 -08:00
|
|
|
|
handleClose={this.closePublishWarning}
|
2019-01-31 14:56:16 -08:00
|
|
|
|
handleSubmit={this.startSubmit}
|
2018-11-16 08:57:03 -08:00
|
|
|
|
/>
|
2018-09-20 11:58:47 -07:00
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-14 08:43:00 -08:00
|
|
|
|
private updateForm = (form: Partial<ProposalDraft>) => {
|
2018-09-20 11:58:47 -07:00
|
|
|
|
this.props.updateForm(form);
|
|
|
|
|
};
|
|
|
|
|
|
2019-11-13 13:37:12 -08:00
|
|
|
|
private startSteps = () => {
|
|
|
|
|
this.setState({ step: CREATE_STEP.BASICS, isExplaining: false });
|
|
|
|
|
};
|
|
|
|
|
|
2018-09-20 11:58:47 -07:00
|
|
|
|
private setStep = (step: CREATE_STEP, skipHistory?: boolean) => {
|
|
|
|
|
this.setState({ step });
|
|
|
|
|
if (!skipHistory) {
|
|
|
|
|
const { history, location } = this.props;
|
|
|
|
|
history.push(`${location.pathname}?step=${step.toLowerCase()}`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private nextStep = () => {
|
|
|
|
|
const idx = STEP_ORDER.indexOf(this.state.step);
|
|
|
|
|
if (idx !== STEP_ORDER.length - 1) {
|
|
|
|
|
this.setStep(STEP_ORDER[idx + 1]);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private togglePreview = () => {
|
|
|
|
|
this.setState({ isPreviewing: !this.state.isPreviewing });
|
|
|
|
|
};
|
|
|
|
|
|
2019-01-31 14:56:16 -08:00
|
|
|
|
private startSubmit = () => {
|
2018-11-16 08:57:03 -08:00
|
|
|
|
this.setState({
|
2019-01-31 14:56:16 -08:00
|
|
|
|
isSubmitting: true,
|
|
|
|
|
isShowingSubmitWarning: false,
|
2018-11-16 08:57:03 -08:00
|
|
|
|
});
|
2018-09-20 11:58:47 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private checkFormErrors = () => {
|
2018-11-14 08:43:00 -08:00
|
|
|
|
if (!this.props.form) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
2018-09-20 11:58:47 -07:00
|
|
|
|
const errors = getCreateErrors(this.props.form);
|
|
|
|
|
return !!Object.keys(errors).length;
|
|
|
|
|
};
|
|
|
|
|
|
2018-10-19 15:03:37 -07:00
|
|
|
|
private handlePop: History.LocationListener = (location, action) => {
|
|
|
|
|
if (action === 'POP') {
|
2019-03-14 20:38:04 -07:00
|
|
|
|
this.setState({ isPreviewing: false });
|
2018-10-19 15:03:37 -07:00
|
|
|
|
const searchValues = qs.parse(location.search);
|
|
|
|
|
const urlStep = searchValues.step && searchValues.step.toUpperCase();
|
|
|
|
|
if (urlStep && CREATE_STEP[urlStep]) {
|
|
|
|
|
this.setStep(urlStep as CREATE_STEP, true);
|
|
|
|
|
} else {
|
|
|
|
|
this.setStep(CREATE_STEP.BASICS, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2018-11-16 08:57:03 -08:00
|
|
|
|
private openPublishWarning = () => {
|
2019-01-31 14:56:16 -08:00
|
|
|
|
this.setState({ isShowingSubmitWarning: true });
|
2018-11-16 08:57:03 -08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private closePublishWarning = () => {
|
2019-01-31 14:56:16 -08:00
|
|
|
|
this.setState({ isShowingSubmitWarning: false });
|
2018-11-16 08:57:03 -08:00
|
|
|
|
};
|
|
|
|
|
|
2019-02-05 12:26:37 -08:00
|
|
|
|
private cancelSubmit = () => {
|
|
|
|
|
this.setState({ isSubmitting: false });
|
|
|
|
|
};
|
2018-09-25 12:49:47 -07:00
|
|
|
|
|
2019-02-05 12:26:37 -08:00
|
|
|
|
private fillInExample = () => {
|
|
|
|
|
this.updateForm(createExampleProposal());
|
2018-09-25 12:49:47 -07:00
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.setState({
|
|
|
|
|
isExample: true,
|
|
|
|
|
step: CREATE_STEP.REVIEW,
|
|
|
|
|
});
|
|
|
|
|
}, 50);
|
2018-09-20 11:58:47 -07:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2018-12-03 18:08:29 -08:00
|
|
|
|
const withConnect = connect<StateProps, DispatchProps, {}, AppState>(
|
2019-03-18 11:35:08 -07:00
|
|
|
|
(state: AppState) => ({
|
|
|
|
|
form: state.create.form,
|
|
|
|
|
isSavingDraft: state.create.isSavingDraft,
|
|
|
|
|
hasSavedDraft: state.create.hasSavedDraft,
|
|
|
|
|
saveDraftError: state.create.saveDraftError,
|
|
|
|
|
}),
|
2018-09-20 11:58:47 -07:00
|
|
|
|
{
|
|
|
|
|
updateForm: createActions.updateForm,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
2018-12-03 18:08:29 -08:00
|
|
|
|
export default compose<Props, {}>(
|
2018-09-20 11:58:47 -07:00
|
|
|
|
withRouter,
|
|
|
|
|
withConnect,
|
|
|
|
|
)(CreateFlow);
|