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.
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
Leave a Reply