React and Redux Simple Multi-page Form

This is a simple multi-page form built using React + Redux + ReduxForm + Dropzone + ReactRouterRedux + ReduxSaga + others. Form validation and file upload validation are implemented in this code. Demo and download options are available.

Demo Download

Authornot important
CreatedSEPTEMBER 12, 2018
LicenseOpen
Compatible browsersChrome, Firefox, Opera, Safari

HTML Snippet

<div id="js-app"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-polyfill/6.20.0/polyfill.js"></script>
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prop-types/15.6.2/prop-types.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.0/redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.7/react-redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-form/7.4.2/redux-form.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/classnames/2.2.5/dedupe.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-router/3.2.1/ReactRouter.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-router-redux/4.0.8/ReactRouterRedux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-saga/0.16.0/redux-saga.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dropzone/5.1.0/index.js"></script>
<script src="https://codepen.io/clindsey/pen/LbyNre.js"></script> <!-- svg-icon-1.0.0 -->

<script>
const professionsConfig = ({
  alpha: {
    label: 'Alpha',
    value: 'alpha',
    specializations: {
      alpha: {
        label: 'Alpha',
        value: 'alpha',
        roles: {
          alpha: {
            label: 'Alpha',
            value: 'alpha',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          bravo: {
            label: 'Bravo',
            value: 'bravo',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          charlie: {
            label: 'Charlie',
            value: 'charlie',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          }
        }
      },
      bravo: {
        label: 'Bravo',
        value: 'bravo',
        roles: {
          alpha: {
            label: 'Alpha',
            value: 'alpha',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          bravo: {
            label: 'Bravo',
            value: 'bravo',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          charlie: {
            label: 'Charlie',
            value: 'charlie',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          }
        }
      },
      charlie: {
        label: 'Charlie',
        value: 'charlie',
        roles: {
          alpha: {
            label: 'Alpha',
            value: 'alpha',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          bravo: {
            label: 'Bravo',
            value: 'bravo',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          charlie: {
            label: 'Charlie',
            value: 'charlie',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          }
        }
      }
    }
  },
  bravo: {
    label: 'Bravo',
    value: 'bravo',
    specializations: {
      alpha: {
        label: 'Alpha',
        value: 'alpha',
        roles: {
          alpha: {
            label: 'Alpha',
            value: 'alpha',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          bravo: {
            label: 'Bravo',
            value: 'bravo',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          charlie: {
            label: 'Charlie',
            value: 'charlie',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          }
        }
      },
      bravo: {
        label: 'Bravo',
        value: 'bravo',
        roles: {
          alpha: {
            label: 'Alpha',
            value: 'alpha',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          bravo: {
            label: 'Bravo',
            value: 'bravo',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          charlie: {
            label: 'Charlie',
            value: 'charlie',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          }
        }
      },
      charlie: {
        label: 'Charlie',
        value: 'charlie',
        roles: {
          alpha: {
            label: 'Alpha',
            value: 'alpha',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          bravo: {
            label: 'Bravo',
            value: 'bravo',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          charlie: {
            label: 'Charlie',
            value: 'charlie',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          }
        }
      }
    }
  },
  charlie: {
    label: 'Charlie',
    value: 'charlie',
    specializations: {
      alpha: {
        label: 'Alpha',
        value: 'alpha',
        roles: {
          alpha: {
            label: 'Alpha',
            value: 'alpha',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          bravo: {
            label: 'Bravo',
            value: 'bravo',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          charlie: {
            label: 'Charlie',
            value: 'charlie',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          }
        }
      },
      bravo: {
        label: 'Bravo',
        value: 'bravo',
        roles: {
          alpha: {
            label: 'Alpha',
            value: 'alpha',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          bravo: {
            label: 'Bravo',
            value: 'bravo',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          charlie: {
            label: 'Charlie',
            value: 'charlie',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          }
        }
      },
      charlie: {
        label: 'Charlie',
        value: 'charlie',
        roles: {
          alpha: {
            label: 'Alpha',
            value: 'alpha',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          bravo: {
            label: 'Bravo',
            value: 'bravo',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          },
          charlie: {
            label: 'Charlie',
            value: 'charlie',
            concentrations: {
              alpha: {
                label: 'Alpha',
                value: 'alpha'
              },
              bravo: {
                label: 'Bravo',
                value: 'bravo'
              },
              charlie: {
                label: 'Charlie',
                value: 'charlie'
              }
            }
          }
        }
      }
    }
  }
})
</script>

SCSS Code

// begin colors
$brand-secondary: #0698bd;
$brand-red: #e90c27;
$brand-green: #4ab043;
$brand-yellow: #ffd305;
$brand-black: #4a4a4a;
$brand-gray: #9b9b9b;
$brand-lighter-gray: #b0b0b0;
$brand-disabled-gray: #d0d0d0;
$brand-border-gray: #e0e0e0;
$brand-success: #5cb85c;
$brand-info: #5bc0de; // stylelint-disable-line no-indistinguishable-colors
$brand-warning: #f0ad4e;
$brand-inverse: $gray-dark;
$brand-danger: #d9534f;
$brand-primary: darken(#428bca, 6.5%);
$brand-primary-muted: lighten($brand-primary, 10%);
// end colors

html {
  color: $text-base-color;
  font-family: 'Fira Sans Condensed', sans-serif;
  font-size: 14px;

  @include mq($from: tablet) {
    font-size: 16px;
  }
}

body {
  margin-top: $inuit-global-spacing-unit;
  margin-bottom: $inuit-global-spacing-unit;
}

// begin FormField
.c-form-field {
  margin-bottom: $inuit-global-spacing-unit;
}

.c-form-field--error {

  .c-form-field__control {
    border-bottom: solid 1px $border-color;
  }

  .c-form-field__hint {
    color: $brand-danger;
  }
}

.c-form-field__hint {
  font-size: 0.75rem;
  color: $text-muted-color;
  font-weight: 300;
  min-height: $inuit-global-spacing-unit;
}

.c-form-field__img {
  height: 1.375rem; // 22px
  margin-right: 0;
  text-align: center;
  width: 2.125rem; // 34px

  > img {
    margin: 0 auto;
    height: 1.375rem; // 22px
  }
}

.c-form-field__control {
  border-bottom: solid 1px $border-color;
}

.c-form-field__input {
  -webkit-backface-visibility: hidden;
  border: none;
  color: $text-base-color;
  font-family: inherit;
  font-weight: 300;
  padding: 0;
  width: 100%;
}
// end FormField

// begin FormButton
.c-form-button {
  border: solid 1px $white;
  border-radius: $control-radius;
  color: $white;
  cursor: pointer;
  display: inline-block;
  font: inherit;
  font-weight: 600;
  margin: $inuit-global-spacing-unit-small 0;
  padding: round($inuit-global-spacing-unit-small * 0.5) $inuit-global-spacing-unit;
  text-align: center;
  vertical-align: middle;
  text-decoration: none;
  font-weight: normal;
}

.c-form-button--primary {
  background-color: $brand-primary;
  border-color: $brand-primary;

  &.c-form-button--disabled,
  &:disabled {
    background-color: $brand-primary-muted;
    border-color: $brand-primary-muted;
  }

  &.c-form-button--inverse {
    background-color: $white;
    color: $brand-primary;
    border-color: $brand-primary;
  }
}

.c-form-button--block {
  width: 100%;
}

.c-form-button--destructive {
  background-color: $brand-danger;
  border-color: $brand-danger;
}
// end FormButton

// begin FormRadio
.c-form-radio__label {
  border-radius: $control-radius;
  border: solid 1px $border-color;
  color: $text-muted-color;
  cursor: pointer;
  display: block;
  margin: 0 auto;
  overflow-x: hidden;
  padding: $inuit-global-spacing-unit-tiny;
  text-overflow: ellipsis;
  white-space: nowrap;
  width: 100%;
}

.c-form-radio__item {
  margin-bottom: $inuit-global-spacing-unit-tiny;
  text-align: center;
}

.c-form-radio__field:checked + label {
  border-color: $brand-primary;
  color: $brand-primary;
}

.c-form-radio--error {

  .c-form-radio__hint {
    color: $brand-danger;
  }
}

.c-form-radio__hint {
  color: $text-muted-color;
  font-size: 0.75rem;
  min-height: 2rem;
}
// end FormRadio

.u-red {
  color: $brand-red;
}

// begin Typography
.c-h1 {
  font-size: 2rem;
}

.c-h2 {
  font-size: 1.5rem;
  font-weight: 600;
}

.c-h3 {
  font-size: 1.25rem;
}

.c-h4 {
  font-size: 0.875rem;
  font-weight: 600;
  text-transform: uppercase;
}

.c-h5 {
  font-size: 0.9375rem;
  font-weight: 600;
}

.c-text-strong {
  color: $brand-secondary;
}

.c-text-strong--inverse {
  color: $white;
}

.c-text-strong--stronger {
  font-weight: 600;
  color: $text-base-color;
}

.c-text-strong--super-strong {
  font-weight: 600;
  font-size: 1.25rem;
}

.c-text-small {
  font-size: 0.875rem;
}

.c-text-small--muted {
  color: $brand-gray;
}

.c-text-small--strong {
  color: $brand-secondary;
}

.c-text-small--stronger {
  font-weight: 600;
}
// end Typography

.c-form-progress__item {
  margin-right: 16px;
  color: $brand-gray;
}

.c-form-progress__item--complete {
  color: $brand-green;
}

.c-form-progress__item--active {
  color: $brand-black;
  font-weight: 600;
}

JavaScript Snippet


const {
  select,
  call,
  takeEvery
} = ReduxSaga.effects
const {
  Provider,
  connect
} = ReactRedux
const {
  routerMiddleware,
  routerReducer,
  syncHistoryWithStore
} = ReactRouterRedux
const {
  Field,
  FieldArray,
  Fields,
  FormSection,
  reduxForm
} = ReduxForm
const {
  IndexRedirect,
  Link,
  Route,
  Router,
  hashHistory
} = ReactRouter
const sagaMiddleware = ReduxSaga.default()
const DropzoneComponent = Dropzone
// END 3rd PARTY LIBRARY IMPORTS

// BEGIN HOOKS
const updateParamsHook = store => (nextState, replace, next) => {
  store.dispatch(updateParamsAction(nextState.params))
  next()
}

const ensureContactDetails = store => (nextState, replace, next) => {
  if (getIsContactDetailsComplete(store.getState()) === false) {
    replace(`/${CONTACT_PAGE}`)
  }
  next()
}
// END HOOKS

// BEGIN ROUTES
setTimeout(() => {
  const store = configureStore()
  const history = syncHistoryWithStore(hashHistory, store)
  ReactDOM.render((
    <Provider {...{ store }}>
      <Router {...{ history }}>
        <Route
            component={AppContainer}
            path="/">
          <Route
              path={CONTACT_PAGE}
              component={ContactPage}
              wizardProgress="0"
          />
          <Route
              path={PROFESSION_PAGE}
              onEnter={composeEnterHooksSeries(ensureContactDetails(store))}
              component={ProfessionPage}
              wizardProgress="1"
          />
          <Route
              path={`:profession/${SPECIALIZATION_PAGE}`}
              onEnter={composeEnterHooksSeries(ensureContactDetails(store), updateParamsHook(store))}
              component={SpecializationPage}
              wizardProgress="2"
          />
          <Route
              path={`:profession/:specialization/${ROLE_PAGE}`}
              onEnter={composeEnterHooksSeries(ensureContactDetails(store), updateParamsHook(store))}
              component={RolePage}
              wizardProgress="3"
          />
          <Route
              path={`:profession/:specialization/:role/${CONCENTRATION_PAGE}`}
              onEnter={composeEnterHooksSeries(ensureContactDetails(store), updateParamsHook(store))}
              component={ConcentrationPage}
              wizardProgress="4"
          />
          <Route
              path={`:profession/:specialization/:role/:concentration/${FILE_UPLOAD_PAGE}`}
              onEnter={composeEnterHooksSeries(ensureContactDetails(store), updateParamsHook(store))}
              component={FileUploadPage}
              wizardProgress="5"
          />
          <Route
              path={SUCCESS_PAGE}
              component={SuccessPage}
              wizardProgress="6"
          />
          <IndexRedirect to={LANDING_PAGE} />
        </Route>
      </Router>
    </Provider>
  ), document.getElementById('js-app'))
}, 0)
// END ROUTES

// BEGIN REDUX CONFIG
function configureStore (initialState) {
  const reducers = Redux.combineReducers({
    ui: uiReducer,
    jobApplications: jobApplicationsReducer,
    form: ReduxForm.reducer,
    routing: routerReducer
  })
  const router = routerMiddleware(hashHistory)
  const store = Redux.createStore(
    reducers,
    initialState,
    Redux.applyMiddleware(router, sagaMiddleware)
  )
  sagaMiddleware.run(rootSaga)
  return store
}
// END REDUX CONFIG

// BEGIN NAVIGATION CONSTANTS
const CONTACT_PAGE = 'contact'
const PROFESSION_PAGE = 'profession'
const SPECIALIZATION_PAGE = 'specialization'
const ROLE_PAGE = 'role'
const CONCENTRATION_PAGE = 'concentration'
const FILE_UPLOAD_PAGE = 'file-upload'
const SUCCESS_PAGE = 'success'
const LANDING_PAGE = CONTACT_PAGE
// END NAVIGATION CONSTANTS

// BEGIN REDUCERS
const getUiDefaultState = () => ({
  params: {}
})

const uiReducer = (state = getUiDefaultState(), action) => {
  if (action.type === UPDATE_PARAMS) {
    return {
      ...state,
      params: {
        ...state.params,
        ...action.params
      }
    }
  }
  return state
}

const getContactDetailsDefaultState = () => ({
  username: null
})

const jobApplicationsReducer = (state = getContactDetailsDefaultState(), action) => {
  if (action.type === SUBMIT_CONTACT_DETAILS) {
    return {
      ...state,
      username: action.username
    }
  }
  return state
}
// END REDUCERS

// BEGIN CONSTANTS
const UPDATE_PARAMS = 'ui/updateParams'
const SUBMIT_CONTACT_DETAILS = 'jobApplication/contactDetails/submit'
const SUBMIT_PROFESSION = 'jobApplication/profession/submit'
const SUBMIT_SPECIALIZATION = 'jobApplication/specialization/submit'
const SUBMIT_ROLE = 'jobApplication/role/submit'
const SUBMIT_CONCENTRATION = 'jobApplication/concentration/submit'
const SUBMIT_FILE_UPLOAD = 'jobApplication/fileUpload/submit'
// END CONSTANTS

// BEGIN SAGAS
function * rootSaga () {
  yield ReduxSaga.effects.all([
    contactDetailsSubmitSaga(),
    professionSubmitSaga(),
    specializationSubmitSaga(),
    roleSubmitSaga(),
    concentrationSubmitSaga(),
    fileUploadSubmitSaga()
  ])
}

function * fileUploadSubmitSaga () {
  yield takeEvery(SUBMIT_FILE_UPLOAD, fileUploadSubmitEffect)
}

function * fileUploadSubmitEffect (action) {
  yield call(action.form.resolve)
  const username = yield select(getUsername)
  const profession = yield select(getParamsProfession)
  const specialization = yield select(getParamsSpecialization)
  const role = yield select(getParamsRole)
  const concentration = yield select(getParamsConcentration)
  const files = action.files
  yield call(console.log, 'successful job app submit!', {
    username,
    profession,
    specialization,
    role,
    concentration,
    files
  })
  yield call(hashHistory.push, `/${SUCCESS_PAGE}`)
}

function * concentrationSubmitSaga () {
  yield takeEvery(SUBMIT_CONCENTRATION, concentrationSubmitEffect)
}

function * concentrationSubmitEffect (action) {
  yield call(action.form.resolve)
  const profession = yield select(getParamsProfession)
  const specialization = yield select(getParamsSpecialization)
  const role = yield select(getParamsRole)
  yield call(hashHistory.push, `/${profession}/${specialization}/${role}/${action.concentration}/${FILE_UPLOAD_PAGE}`)
}

function * roleSubmitSaga () {
  yield takeEvery(SUBMIT_ROLE, roleSubmitEffect)
}

function * roleSubmitEffect (action) {
  yield call(action.form.resolve)
  const profession = yield select(getParamsProfession)
  const specialization = yield select(getParamsSpecialization)
  yield call(hashHistory.push, `/${profession}/${specialization}/${action.role}/${CONCENTRATION_PAGE}`)
}

function * specializationSubmitSaga () {
  yield takeEvery(SUBMIT_SPECIALIZATION, specializationSubmitEffect)
}

function * specializationSubmitEffect (action) {
  yield call(action.form.resolve)
  const profession = yield select(getParamsProfession)
  yield call(hashHistory.push, `/${profession}/${action.specialization}/${ROLE_PAGE}`)
}

function * professionSubmitSaga () {
  yield takeEvery(SUBMIT_PROFESSION, professionSubmitEffect)
}

function * professionSubmitEffect (action) {
  yield call(action.form.resolve)
  yield call(hashHistory.push, `/${action.profession}/${SPECIALIZATION_PAGE}`)
}

function * contactDetailsSubmitSaga () {
  yield takeEvery(SUBMIT_CONTACT_DETAILS, contactDetailsSubmitEffect)
}

function * contactDetailsSubmitEffect (action) {
  yield call(action.form.resolve)
  yield call(hashHistory.push, `/${PROFESSION_PAGE}`)
}
// END SAGAS

// BEGIN GENERIC STORE-CONNECTED COMPONENTS
const AppContainer = connect((state, ownProps) => ({
  pages: getProgressPages(state, ownProps)
}), {
})(class extends React.Component {
  render () {
    return (
      <div className="o-wrapper">
        <div className="o-layout">
          <div className="o-layout__item">
            <ul className="o-list-bare o-list-inline c-form-progress">
              {this.props.pages.map((page, index) => (
                <li
                    key={index}
                    className={classNames({
                      'o-list-inline__item': true,
                      'c-form-progress__item': true,
                      'c-form-progress__item--complete': page.complete,
                      'c-form-progress__item--active': page.active
                    })}
                >{page.label}</li>
              ))}
            </ul>
          </div>
        </div>
        <div className="o-layout">
          <div className="o-layout__item">
            {this.props.children}
          </div>
        </div>
      </div>
    )
  }
})
// END GENERIC STORE-CONNECTED COMPONENTS

// BEGIN FORM SUBMISSIONS
const fileUploadSubmission = (callbackAction) => {
  return (values) => {
    const {
      fileUpload
    } = values
    return new Promise((resolve, reject) => {
      callbackAction(fileUpload.files, resolve, reject)
    })
  }
}

const contactDetailsSubmission = (callbackAction) => {
  return ({ contact }) => {
    return new Promise((resolve, reject) => {
      callbackAction(contact.username, resolve, reject)
    })
  }
}

const professionSubmission = (callbackAction) => {
  return ({ profession }) => {
    return new Promise((resolve, reject) => {
      callbackAction(profession.title, resolve, reject)
    })
  }
}

const specializationSubmission = (callbackAction) => {
  return ({ specialization }) => {
    return new Promise((resolve, reject) => {
      callbackAction(specialization.title, resolve, reject)
    })
  }
}

const roleSubmission = (callbackAction) => {
  return ({ role }) => {
    return new Promise((resolve, reject) => {
      callbackAction(role.title, resolve, reject)
    })
  }
}

const concentrationSubmission = (callbackAction) => {
  return ({ concentration }) => {
    return new Promise((resolve, reject) => {
      callbackAction(concentration.title, resolve, reject)
    })
  }
}
// END FORM SUBMISSIONS

// BEGIN ACTIONS
const updateParamsAction = params => ({
  type: UPDATE_PARAMS,
  params
})

const submitContactDetailsAction = (username, resolve, reject) => ({
  type: SUBMIT_CONTACT_DETAILS,
  username,
  form: {
    resolve,
    reject
  }
})

const submitProfessionAction = (profession, resolve, reject) => ({
  type: SUBMIT_PROFESSION,
  profession,
  form: {
    resolve,
    reject
  }
})

const submitSpecializationAction = (specialization, resolve, reject) => ({
  type: SUBMIT_SPECIALIZATION,
  specialization,
  form: {
    resolve,
    reject
  }
})

const submitRoleAction = (role, resolve, reject) => ({
  type: SUBMIT_ROLE,
  role,
  form: {
    resolve,
    reject
  }
})

const submitFileUploadAction = (files, resolve, reject) => ({
  type: SUBMIT_FILE_UPLOAD,
  files,
  form: {
    resolve,
    reject
  }
})

const submitConcentrationAction = (concentration, resolve, reject) => ({
  type: SUBMIT_CONCENTRATION,
  concentration,
  form: {
    resolve,
    reject
  }
})
// END ACTIONS

// BEGIN PAGES, ROUTE-SPECIFIC STORE-CONNECTED COMPONENTS
const ContactPage = connect(() => ({
}), {
  submitContactDetails: submitContactDetailsAction
})(class extends React.Component {
  static propTypes = {
    submitContactDetails: PropTypes.func.isRequired
  }

  render () {
    return (
      <div className="o-layout">
        <div className="o-layout__item u-1/2@tablet">
          <h1>{'Contact Details'}</h1>
          <p>{'Please provide your contact details.'}</p>
          <ContactDetailsForm onSubmit={contactDetailsSubmission(this.props.submitContactDetails)} />
        </div>
      </div>
    )
  }
})

const ProfessionPage = connect((state, ownProps) => ({
  professionOptions: getProfessionOptions(state, ownProps)
}), {
  submitProfession: submitProfessionAction
})(class extends React.Component {
  static propTypes = {
    submitProfession: PropTypes.func.isRequired
  }

  render () {
    return (
      <div className="o-layout">
        <div className="o-layout__item u-1/2@tablet">
          <h1>{'Profession Page'}</h1>
          <p>{'Pick your profession'}</p>
          <ProfessionForm
              onSubmit={professionSubmission(this.props.submitProfession)}
              professionOptions={this.props.professionOptions}
          />
        </div>
      </div>
    )
  }
})

const SpecializationPage = connect((state, ownProps) => ({
  specializationOptions: getSpecializationOptions(state, ownProps)
}), {
  submitSpecialization: submitSpecializationAction
})(class extends React.Component {
  static propTypes = {
    submitSpecialization: PropTypes.func.isRequired,
    specializationOptions: PropTypes.arrayOf(PropTypes.shape({
      label: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    })).isRequired
  }

  render () {
    return (
      <div className="o-layout">
        <div className="o-layout__item u-1/2@tablet">
          <h1>{'Specialization Page'}</h1>
          <p>{'Choose a specialization'}</p>
          <SpecializationForm
              specializationOptions={this.props.specializationOptions}
              onSubmit={specializationSubmission(this.props.submitSpecialization)}
          />
        </div>
      </div>
    )
  }
})

const RolePage = connect((state, ownProps) => ({
  roleOptions: getRoleOptions(state, ownProps)
}), {
  submitRole: submitRoleAction
})(class extends React.Component {
  static propTypes = {
    submitRole: PropTypes.func.isRequired,
    roleOptions: PropTypes.arrayOf(PropTypes.shape({
      label: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    })).isRequired
  }

  render () {
    return (
      <div className="o-layout">
        <div className="o-layout__item u-1/2@tablet">
          <h1>{'Role Page'}</h1>
          <p>{'What role would you like?'}</p>
          <RoleForm
              roleOptions={this.props.roleOptions}
              onSubmit={roleSubmission(this.props.submitRole)}
          />
        </div>
      </div>
    )
  }
})

const ConcentrationPage = connect((state, ownProps) => ({
  concentrationOptions: getConcentrationOptions(state, ownProps)
}), {
  submitConcentration: submitConcentrationAction
})(class extends React.Component {
  static propTypes = {
    submitConcentration: PropTypes.func.isRequired,
    concentrationOptions: PropTypes.arrayOf(PropTypes.shape({
      label: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    })).isRequired
  }

  render () {
    return (
      <div className="o-layout">
        <div className="o-layout__item u-1/2@tablet">
          <h1>{'Concentration Page'}</h1>
          <p>{'How about a specific concentration?'}</p>
          <ConcentrationForm
              concentrationOptions={this.props.concentrationOptions}
              onSubmit={concentrationSubmission(this.props.submitConcentration)}
          />
        </div>
      </div>
    )
  }
})

const FileUploadPage = connect((state, ownProps) => ({
}), {
  submitFileUpload: submitFileUploadAction
})(class extends React.Component {
  static propTypes = {
    submitFileUpload: PropTypes.func.isRequired
  }

  render () {
    return (
      <div className="o-layout">
        <div className="o-layout__item u-1/2@tablet">
          <h1>{'File Upload Page'}</h1>
          <p>{'Attach some files for this application.'}</p>
          <FileUploadForm onSubmit={fileUploadSubmission(this.props.submitFileUpload)} />
        </div>
      </div>
    )
  }
})

const SuccessPage = connect(() => ({
}), {
})(class extends React.Component {
  render () {
    return (
      <div className="o-layout">
        <div className="o-layout__item u-1/2@tablet">
          <h1>{'Success Page'}</h1>
          <p>{'You successfully completed the form!'}</p>
        </div>
      </div>
    )
  }
})
// END PAGES, ROUTE-SPECIFIC STORE-CONNECTED COMPONENTS

// BEGIN UTILS/ROUTES
const composeEnterHooksSeries = (...hooks) => {
  return (nextState, originalReplace, executeTransition) => {
    let cancelSeries = false
    const replace = location => {
      cancelSeries = true
      originalReplace(location)
    }
    (function executeHooksSynchronously (remainingHooks) {
      if (cancelSeries || !remainingHooks.length) {
        return executeTransition()
      }
      let nextHook = remainingHooks[0]
      if (nextHook.length >= 3) {
        nextHook.call(this, nextState, replace, () => {
          executeHooksSynchronously(remainingHooks.slice(1))
        })
      } else {
        nextHook.call(this, nextState, replace)
        executeHooksSynchronously(remainingHooks.slice(1))
      }
    }(hooks))
  }
}
// END UTILS/ROUTES

// BEGIN UTILS/FORMS
const validateFields = (validators, requiredFields = {}) => values => {
  const validationErrors = Object.keys(validators).map(name => ({
    name,
    error: validators[name](values[name])
  })).reduce((p, {name, error}) => (
    Object.keys(name).length ? {...p, [name]: error} : p
  ), {})
  Object.keys(requiredFields).forEach(fieldName => {
    Object.assign(validationErrors[fieldName], requiredFields[fieldName](values[fieldName]))
  })
  return validationErrors
}
// END UTILS/FORMS

// BEGIN FORM VALIDATIONS
const usernameValidation = (values) => {
  const errors = {}
  if (!values || !values.username) {
    errors.username = 'Required'
  }
  return errors
}

const titleValidation = (values) => {
  const errors = {}
  if (!values || !values.title) {
    errors.title = 'Required'
  }
  return errors
}

const contactValidation = values => ({
  ...usernameValidation(values)
})

const professionValidation = values => ({
  ...titleValidation(values)
})

const specializationValidation = values => ({
  ...titleValidation(values)
})

const roleValidation = values => ({
  ...titleValidation(values)
})

const concentrationValidation = values => ({
  ...titleValidation(values)
})

const filesValidation = (values) => {
  const errors = {}
  if (!values || !values.files) {
    errors.files = {
      _error: 'Required'
    }
  }
  return errors
}

const fileUploadValidation = values => ({
  ...filesValidation(values)
})
// END FORM VALIDATIONS

// BEGIN FORMS
const ContactDetailsForm = reduxForm({
  form: 'contentDetails',
  validate: validateFields({
    contact: contactValidation
  })
})(class extends React.Component {
  render () {
    return (
      <form onSubmit={this.props.handleSubmit}>
        <FormSection name="contact">
          <UsernameField />
        </FormSection>
        <SubmitButton disabled={this.props.submitting} />
      </form>
    )
  }
})

const ProfessionForm = reduxForm({
  form: 'profession',
  validate: validateFields({
    profession: professionValidation
  })
})(class extends React.Component {
  static propTypes = {
    professionOptions: PropTypes.arrayOf(PropTypes.shape({
      label: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    })).isRequired
  }

  render () {
    return (
      <form onSubmit={this.props.handleSubmit}>
        <FormSection name="profession">
          <ProfessionTitleField options={this.props.professionOptions} />
        </FormSection>
        <SubmitButton disabled={this.props.submitting} />
      </form>
    )
  }
})

const SpecializationForm = reduxForm({
  form: 'specialization',
  validate: validateFields({
    specialization: specializationValidation
  })
})(class extends React.Component {
  static propTypes = {
    specializationOptions: PropTypes.arrayOf(PropTypes.shape({
      label: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    })).isRequired
  }

  render () {
    return (
      <form onSubmit={this.props.handleSubmit}>
        <FormSection name="specialization">
          <SpecializationTitleField options={this.props.specializationOptions} />
        </FormSection>
        <SubmitButton disabled={this.props.submitting} />
      </form>
    )
  }
})

const RoleForm = reduxForm({
  form: 'role',
  validate: validateFields({
    role: roleValidation
  })
})(class extends React.Component {
  static propTypes = {
    roleOptions: PropTypes.arrayOf(PropTypes.shape({
      label: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    })).isRequired
  }

  render () {
    return (
      <form onSubmit={this.props.handleSubmit}>
        <FormSection name="role">
          <RoleTitleField options={this.props.roleOptions} />
        </FormSection>
        <SubmitButton disabled={this.props.submitting} />
      </form>
    )
  }
})

const ConcentrationForm = reduxForm({
  form: 'concentration',
  validate: validateFields({
    concentration: concentrationValidation
  })
})(class extends React.Component {
  static propTypes = {
    concentrationOptions: PropTypes.arrayOf(PropTypes.shape({
      label: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    })).isRequired
  }

  render () {
    return (
      <form onSubmit={this.props.handleSubmit}>
        <FormSection name="concentration">
          <ConcentrationTitleField options={this.props.concentrationOptions} />
        </FormSection>
        <SubmitButton disabled={this.props.submitting} />
      </form>
    )
  }
})

const FileUploadForm = reduxForm({
  form: 'fileUpload',
  validate: validateFields({
    fileUpload: fileUploadValidation
  })
})(class extends React.Component {
  static propTypes = {
  }

  render () {
    return (
      <form onSubmit={this.props.handleSubmit}>
        <FormSection name="fileUpload">
          <FileUploadField />
        </FormSection>
        <SubmitButton disabled={this.props.submitting} />
      </form>
    )
  }
})
// END FORMS

// BEGIN FORM FIELDS
class SubmitButton extends React.Component {
  render () {
    return (
      <button
          disabled={this.props.disabled}
          className="c-form-button c-form-button--primary c-form-button--block"
          type="submit"
      >{'Submit'}</button>
    )
  }
}

class FileFields extends React.Component {
  render () {
    return (
      <ul>
        <li>
          <DropzoneComponent
              onDrop={(files) => {
                this.props.fields.map((_, i) => ths.props.fields.remove(i))
                files.map(file => this.props.fields.push(file))
              }}
          >{'Drop files here'}</DropzoneComponent>
        </li>
        {this.props.meta.error && (
          <li className="u-red">{this.props.meta.error}</li>
        )}
        {this.props.fields.map((file, index) => (
          <li key={index}>
            <button
                onClick={() => this.props.fields.remove(index)}
                type="button"
            >{'X'}</button>
            <Field
                name={`${file}.name`}
                component={TextDisplayControl}
            />
          </li>
        ))}
      </ul>
    )
  }
}

class FileUploadField extends React.Component {
  render () {
    return (
      <FieldArray
          name="files"
          component={FileFields}
          label="Upload files"
      />
    )
  }
}

class UsernameField extends React.Component {
  render () {
    return (
      <FormField
          icon="Password"
          fields={[
            {
              name: 'username',
              placeholder: 'Username',
              type: 'text',
            }
          ]}
      />
    )
  }
}

class ConcentrationTitleField extends React.Component {
  static propTypes = {
    options: PropTypes.arrayOf(PropTypes.shape({
      label: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    })).isRequired
  }

  render () {
    return (
      <Field
          component={RadioControl}
          hint="Pick a concentration"
          label="Concentration"
          name="title"
          options={this.props.options.map(o => ({
            ...o,
            classes: 'u-1/3',
            icon: 'Password'
          }))}
      />
    )
  }
}

class RoleTitleField extends React.Component {
  static propTypes = {
    options: PropTypes.arrayOf(PropTypes.shape({
      label: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    })).isRequired
  }

  render () {
    return (
      <Field
          component={RadioControl}
          hint="Pick a role"
          label="Role"
          name="title"
          options={this.props.options.map(o => ({
            ...o,
            classes: 'u-1/3',
            icon: 'Password'
          }))}
      />
    )
  }
}

class SpecializationTitleField extends React.Component {
  static propTypes = {
    options: PropTypes.arrayOf(PropTypes.shape({
      label: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    })).isRequired
  }

  render () {
    return (
      <Field
          component={RadioControl}
          hint="Pick a specialization"
          label="Specialization"
          name="title"
          options={this.props.options.map(o => ({
            ...o,
            classes: 'u-1/3',
            icon: 'Password'
          }))}
      />
    )
  }
}

class ProfessionTitleField extends React.Component {
  static propTypes = {
    options: PropTypes.arrayOf(PropTypes.shape({
      label: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    })).isRequired
  }

  render () {
    return (
      <Field
          component={RadioControl}
          hint="Pick a profession"
          label="Profession"
          name="title"
          options={this.props.options.map(o => ({
            ...o,
            classes: 'u-1/3',
            icon: 'Password'
          }))}
      />
    )
  }
}
// END FORM FIELDS

// BEGIN FORM FIELD CONTROLS
class FormField extends React.Component { // refactor, missing propTypes
  static propTypes = {
    icon: PropTypes.string,
    fields: PropTypes.arrayOf(PropTypes.shape({
      className: PropTypes.string,
      format: PropTypes.func,
      name: PropTypes.string.isRequired,
      normalize: PropTypes.func,
      parse: PropTypes.func,
      placeholder: PropTypes.string.isRequired, // refactor, is this really required?  Is this used for making checkboxes/radios?
      type: PropTypes.string.isRequired
    })),
    hint: PropTypes.string
  };

  renderFields (props) {
    const {
      fields,
      hint,
      icon
    } = props;
    const errors = fields.map(({ name }) => {
      const {
        meta: {
          error,
          touched
        } // refactor, ¯\_(ãÄ)_/¯ @ next line
      } = eval(`props.${name}`); // eslint-disable-line no-eval
      return touched && error; // refactor, `touched` might not be behaving as expected?
    }).filter(i => i);
    const error = errors[0] || props.error;
    const message = error || hint;
    const className = classNames({
      'c-form-field': true,
      'c-form-field--error': !!error
    });
    const Icon = SVGIcon[icon];
    return (
      <div {...{ className }}>
        <div className="o-media">
          <div className="o-media__img c-form-field__img">
            {icon && (<Icon />)}
          </div>
          <div className="o-media__body">
            <div className="c-form-field__control">
              {fields.map((field, key) => {
                const {
                  format,
                  name,
                  normalize,
                  parse,
                  placeholder,
                  type
                } = field; // refactor, what else is in `props.${name}`?
                // refactor, I had to comment out the line below to make this work with FormSection
                // const {input} = eval(`props.${name}`); // eslint-disable-line no-eval
                const inputClassName = classNames({
                  'c-form-field__input': true,
                  '5625463739': false,
                  [field.className]: field.className && true // refactor, `&& true`?  I don't get it
                });
                return (
                  <Field
                      className={inputClassName}
                      component="input"
                      {...{key, placeholder, type, format, normalize, parse, name}}
                  />
                );
              })}
            </div>
            <div className="c-form-field__hint">{message}</div>
          </div>
        </div>
      </div>
    );
  }

  render () {
    const {
      fields,
      hint,
      icon
    } = this.props;
    return (
      <Fields
          component={this.renderFields}
          names={fields.map(({name}) => name)}
          {...{fields, hint, icon}}
      />
    );
  }
}

class TextDisplayControl extends React.Component {
  render () {
    return (
      <span>{this.props.input.value}</span>
    )
  }
}

class TextInputControl extends React.Component {
  static propTypes = {
    placeholder: PropTypes.string,
    type: PropTypes.string.isRequired
  }

  render () {
    const {
      input,
      type,
      placeholder,
      meta: {
        error,
        touched
      }
    } = this.props
    const className = classNames({
      'c-input-control': true,
      'c-input-control--error': touched && error
    })
    return (
      <div {...{className}}>
        <input
            className="c-input-control__input"
            {...input}
            {...{type, placeholder}}
        />
        <div className="c-input-control__hint c-text-small">{touched && error}</div>
      </div>
    )
  }
}

class RadioControl extends React.Component {
  handleChange (value) {
    return () => {
      this.props.input.onChange(value)
    }
  }

  render () {
    const {
      hint,
      input: {
        value,
        name
      },
      meta: {
        error,
        touched
      },
      options
    } = this.props
    const message = (touched && error) || hint
    const className = classNames({
      'c-form-radio': true,
      'c-form-radio--error': (touched && !!error)
    })
    return (
      <div {...{className}}>
        <div className="o-layout">
          {options.map((field, key) => {
            const Icon = SVGIcon[field.icon]
            const fieldClasses = field.classes || ''
            return (
              <div
                  className={`c-form-radio__item o-layout__item ${fieldClasses}`}
                  {...{key}}
              >
                <input
                    checked={value === field.value}
                    className="c-form-radio__field u-hidden-visually"
                    id={`${name}-${key}`}
                    onChange={this.handleChange(field.value)}
                    type="radio"
                    value={field.value}
                    {...{name}}
                />
                <label
                    className="c-form-radio__label"
                    htmlFor={`${name}-${key}`}
                >
                  {field.icon && Icon && (<Icon active={value === field.value} />)}
                  {field.label}
                </label>
              </div>
            )
          })}
          <div className="o-layout__item u-1/1 c-form-radio__message">
            <div className="c-form-radio__hint">{message}</div>
          </div>
        </div>
      </div>
    )
  }
}
// END FORM FIELD CONTROLS

// BEGIN SELECTORS
const getUiState = state => state.ui

const getJobApplicationsState = state => state.jobApplications

const getUsername = (state, ownProps) => {
  const jobApplicationsState = getJobApplicationsState(state, ownProps)
  return jobApplicationsState.username
}

const getIsContactDetailsComplete = (state, ownProps) => {
  const username = getUsername(state, ownProps)
  return !!username
}

const getParams = (state, ownProps) => {
  const uiState = getUiState(state, ownProps)
  return uiState.params
}

const getParamsProfession = (state, ownProps) => {
  const params = getParams(state, ownProps)
  return params.profession
}

const getParamsSpecialization = (state, ownProps) => {
  const params = getParams(state, ownProps)
  return params.specialization
}

const getParamsRole = (state, ownProps) => {
  const params = getParams(state, ownProps)
  return params.role
}

const getParamsConcentration = (state, ownProps) => {
  const params = getParams(state, ownProps)
  return params.concentration
}

const getJobsConfig = () => professionsConfig

const getProfessionOptions = (state, ownProps) => {
  const jobsConfig = getJobsConfig(state, ownProps)
  return Object.keys(jobsConfig).map((professionKey) => {
    const professionConfig = jobsConfig[professionKey]
    return ({
      label: professionConfig.label,
      value: professionConfig.value
    })
  })
}

const getSpecializationOptions = (state, ownProps) => {
  const jobsConfig = getJobsConfig(state, ownProps)
  const profession = getParamsProfession(state, ownProps)
  const {
    specializations
  } = jobsConfig[profession]
  return Object.keys(specializations).map((specializationKey) => {
    const specializationConfig = specializations[specializationKey]
    return ({
      label: specializationConfig.label,
      value: specializationConfig.value
    })
  })
}

const getRoleOptions = (state, ownProps) => {
  const jobsConfig = getJobsConfig(state, ownProps)
  const profession = getParamsProfession(state, ownProps)
  const specialization = getParamsSpecialization(state, ownProps)
  const {
    roles
  } = jobsConfig[profession].specializations[specialization]
  return Object.keys(roles).map((roleKey) => {
    const roleConfig = roles[roleKey]
    return ({
      label: roleConfig.label,
      value: roleConfig.value
    })
  })
}

const getConcentrationOptions = (state, ownProps) => {
  const jobsConfig = getJobsConfig(state, ownProps)
  const profession = getParamsProfession(state, ownProps)
  const specialization = getParamsSpecialization(state, ownProps)
  const role = getParamsRole(state, ownProps)
  const {
    concentrations
  } = jobsConfig[profession].specializations[specialization].roles[role]
  return Object.keys(concentrations).map((concentrationKey) => {
    const concentrationConfig = concentrations[concentrationKey]
    return ({
      label: concentrationConfig.label,
      value: concentrationConfig.value
    })
  })
}

const getProgressPages = (state, ownProps) => {
  const lookup = [
    {
      label: 'Contact',
      complete: false,
      active: false
    }, {
      label: 'Profession',
      complete: false,
      active: false
    }, {
      label: 'Specialization',
      complete: false,
      active: false
    }, {
      label: 'Role',
      complete: false,
      active: false
    }, {
      label: 'Concentration',
      complete: false,
      active: false
    }, {
      label: 'File Upload',
      complete: false,
      active: false
    }, {
      label: 'Success',
      complete: false,
      active: false
    }
  ]
  const index = parseInt(ownProps.routes[ownProps.routes.length - 1].wizardProgress, 10) // TODO refactor, i don't trust this...
  for (let i = 0; i < index; i++) {
    lookup[i].complete = true
  }
  lookup[index].active = true
  return lookup
}
// END SELECTORS

Preview

W3TWEAKS
Latest posts by W3TWEAKS (see all)

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *