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: '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, }, [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, }, [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: '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 they’re 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 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']; saveDraftError: AppState['create']['saveDraftError']; } interface DispatchProps { updateForm: typeof createActions['updateForm']; } type Props = StateProps & DispatchProps & RouteComponentProps; interface State { step: CREATE_STEP; isPreviewing: boolean; isShowingSubmitWarning: boolean; isSubmitting: boolean; isExplaining: boolean; isExample: boolean; } class CreateFlow extends React.Component { private historyUnlisten: () => void; private debouncedUpdateForm: (form: Partial) => 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('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 = ; showFooter = false; } else if (isPreviewing) { content = ; } else if (isExplaining) { content = ; showFooter = false; } else { // Antd definitions are missing `onClick` for step, even though it works. const Step = Steps.Step as any; content = (
{STEP_ORDER.map(s => ( this.setStep(s)} style={{ cursor: 'pointer' }} /> ))}

{info.title}

{info.subtitle}
); } return (
{content} {showFooter && (
{isLastStep ? ( <> ) : ( <>
{info.help}
)} {process.env.NODE_ENV !== 'production' && ( )}
)} {isSavingDraft ? (
Saving draft...
) : ( saveDraftError && (
Failed to save draft!
{saveDraftError}
) )}
); } private updateForm = (form: Partial) => { 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( (state: AppState) => ({ form: state.create.form, isSavingDraft: state.create.isSavingDraft, hasSavedDraft: state.create.hasSavedDraft, saveDraftError: state.create.saveDraftError, }), { updateForm: createActions.updateForm, }, ); export default compose( withRouter, withConnect, )(CreateFlow);