zcash-grant-system/frontend/client/components/CreateFlow/index.tsx

378 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import { connect } from 'react-redux';
import { compose } from 'recompose';
import { Steps, Icon } from 'antd';
import qs from 'query-string';
import { withRouter, RouteComponentProps } from 'react-router';
import { History } from 'history';
import { debounce } from 'underscore';
import Basics from './Basics';
import Team from './Team';
import Details from './Details';
import Milestones from './Milestones';
import Payment from './Payment';
import Review from './Review';
import Preview from './Preview';
import Final from './Final';
import Explainer from './Explainer';
import SubmitWarningModal from './SubmitWarningModal';
import createExampleProposal from './example';
import { createActions } from 'modules/create';
import { ProposalDraft } from 'types';
import { getCreateErrors } from 'modules/create/utils';
import ls from 'local-storage';
import { AppState } from 'store/reducers';
import './index.less';
export enum CREATE_STEP {
BASICS = 'BASICS',
TEAM = 'TEAM',
DETAILS = 'DETAILS',
MILESTONES = 'MILESTONES',
PAYMENT = 'PAYMENT',
REVIEW = 'REVIEW',
}
const STEP_ORDER = [
CREATE_STEP.BASICS,
CREATE_STEP.TEAM,
CREATE_STEP.DETAILS,
CREATE_STEP.MILESTONES,
CREATE_STEP.PAYMENT,
CREATE_STEP.REVIEW,
];
interface StepInfo {
short: string;
title: React.ReactNode;
subtitle: React.ReactNode;
help: React.ReactNode;
component: any;
}
interface LSExplainer {
noExplain: boolean;
}
const STEP_INFO: { [key in CREATE_STEP]: StepInfo } = {
[CREATE_STEP.BASICS]: {
short: 'Basics',
title: 'Lets start with the basics',
subtitle: 'Dont worry, you can come back and change things before publishing',
help:
'You dont have to fill out everything at once right now, you can come back later.',
component: Basics,
},
[CREATE_STEP.TEAM]: {
short: 'Team',
title: 'Assemble your team',
subtitle: 'Let everyone know if youre flying solo, or who youre working with',
help:
'More team members, real names, and linked social accounts adds legitimacy to your proposal',
component: Team,
},
[CREATE_STEP.DETAILS]: {
short: 'Details',
title: 'Dive into the details',
subtitle: 'Heres your chance to lay out the full proposal, in all its glory',
help:
'Make sure people know what youre building, why youre qualified, and where the moneys going',
component: Details,
},
[CREATE_STEP.MILESTONES]: {
short: 'Milestones',
title: 'Set up milestones for deliverables',
subtitle: 'Make a timeline of when youll complete tasks, and receive funds',
help:
'Contributors are more willing to fund proposals with funding spread across multiple milestones',
component: Milestones,
},
[CREATE_STEP.PAYMENT]: {
short: 'Payment',
title: 'Set your payout and tip addresses',
subtitle: '',
help:
'Double check your addresses, and make sure theyre secure. Once sent, transactions are irreversible!',
component: Payment,
},
[CREATE_STEP.REVIEW]: {
short: 'Review',
title: 'Review your proposal',
subtitle: 'Feel free to edit any field that doesnt look right',
help: 'Youll 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'];
saveDraftError: AppState['create']['saveDraftError'];
}
interface DispatchProps {
updateForm: typeof createActions['updateForm'];
}
type Props = StateProps & DispatchProps & RouteComponentProps<any>;
interface State {
step: CREATE_STEP;
isPreviewing: boolean;
isShowingSubmitWarning: boolean;
isSubmitting: boolean;
isExplaining: boolean;
isExample: boolean;
}
class CreateFlow extends React.Component<Props, State> {
private historyUnlisten: () => void;
private debouncedUpdateForm: (form: Partial<ProposalDraft>) => void;
constructor(props: Props) {
super(props);
const searchValues = qs.parse(props.location.search);
const queryStep = searchValues.step ? searchValues.step.toUpperCase() : null;
const step =
queryStep && CREATE_STEP[queryStep]
? (CREATE_STEP[queryStep] as CREATE_STEP)
: CREATE_STEP.BASICS;
const noExplain = !!ls<LSExplainer>('noExplain');
this.state = {
step,
isPreviewing: false,
isSubmitting: false,
isExample: false,
isShowingSubmitWarning: false,
isExplaining: !noExplain,
};
this.debouncedUpdateForm = debounce(this.updateForm, 800);
this.historyUnlisten = this.props.history.listen(this.handlePop);
}
componentWillUnmount() {
if (this.historyUnlisten) {
this.historyUnlisten();
}
}
render() {
const { isSavingDraft, saveDraftError } = this.props;
const {
step,
isPreviewing,
isSubmitting,
isShowingSubmitWarning,
isExplaining,
} = this.state;
const info = STEP_INFO[step];
const currentIndex = STEP_ORDER.indexOf(step);
const isLastStep = currentIndex === STEP_ORDER.length - 1;
const isSecondToLastStep = currentIndex === STEP_ORDER.length - 2;
const StepComponent = info.component;
let content;
let showFooter = true;
if (isSubmitting) {
content = <Final goBack={this.cancelSubmit} />;
showFooter = false;
} else if (isPreviewing) {
content = <Preview />;
} else if (isExplaining) {
content = <Explainer startSteps={this.startSteps} />;
showFooter = false;
} else {
// Antd definitions are missing `onClick` for step, even though it works.
const Step = Steps.Step as any;
content = (
<div className="CreateFlow">
<div className="CreateFlow-header">
<Steps current={currentIndex}>
{STEP_ORDER.map(s => (
<Step
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
proposalId={this.props.form && this.props.form.proposalId}
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"
key="submit"
onClick={this.openPublishWarning}
disabled={this.checkFormErrors()}
>
Submit
</button>
</>
) : (
<>
<div className="CreateFlow-footer-help">{info.help}</div>
<button
className="CreateFlow-footer-button"
key="next"
onClick={this.nextStep}
>
{isSecondToLastStep ? 'Review' : 'Continue' } <Icon type="right-circle-o" />
</button>
</>
)}
{process.env.NODE_ENV !== 'production' && (
<button className="CreateFlow-footer-example" onClick={this.fillInExample}>
<Icon type="fast-forward" />
</button>
)}
</div>
)}
{isSavingDraft ? (
<div className="CreateFlow-draftNotification">Saving draft...</div>
) : (
saveDraftError && (
<div className="CreateFlow-draftNotification is-error">
Failed to save draft!
<br />
{saveDraftError}
</div>
)
)}
<SubmitWarningModal
proposal={this.props.form}
isVisible={isShowingSubmitWarning}
handleClose={this.closePublishWarning}
handleSubmit={this.startSubmit}
/>
</div>
);
}
private updateForm = (form: Partial<ProposalDraft>) => {
this.props.updateForm(form);
};
private startSteps = () => {
this.setState({ step: CREATE_STEP.BASICS, isExplaining: false });
};
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 });
};
private startSubmit = () => {
this.setState({
isSubmitting: true,
isShowingSubmitWarning: false,
});
};
private checkFormErrors = () => {
if (!this.props.form) {
return true;
}
const errors = getCreateErrors(this.props.form);
return !!Object.keys(errors).length;
};
private handlePop: History.LocationListener = (location, action) => {
if (action === 'POP') {
this.setState({ isPreviewing: false });
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);
}
}
};
private openPublishWarning = () => {
this.setState({ isShowingSubmitWarning: true });
};
private closePublishWarning = () => {
this.setState({ isShowingSubmitWarning: false });
};
private cancelSubmit = () => {
this.setState({ isSubmitting: false });
};
private fillInExample = () => {
this.updateForm(createExampleProposal());
setTimeout(() => {
this.setState({
isExample: true,
step: CREATE_STEP.REVIEW,
});
}, 50);
};
}
const withConnect = connect<StateProps, DispatchProps, {}, AppState>(
(state: AppState) => ({
form: state.create.form,
isSavingDraft: state.create.isSavingDraft,
hasSavedDraft: state.create.hasSavedDraft,
saveDraftError: state.create.saveDraftError,
}),
{
updateForm: createActions.updateForm,
},
);
export default compose<Props, {}>(
withRouter,
withConnect,
)(CreateFlow);