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.
Author not important
Created SEPTEMBER 12, 2018
License Open
Compatible browsers Chrome, 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(values)
})).reduce((p, {name, error}) => (
Object.keys(name).length ? {...p, : error} : p
), {})
Object.keys(requiredFields).forEach(fieldName => {
Object.assign(validationErrors, requiredFields(values))
})
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;
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 && 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
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
return ({
label: professionConfig.label,
value: professionConfig.value
})
})
}
const getSpecializationOptions = (state, ownProps) => {
const jobsConfig = getJobsConfig(state, ownProps)
const profession = getParamsProfession(state, ownProps)
const {
specializations
} = jobsConfig
return Object.keys(specializations).map((specializationKey) => {
const specializationConfig = specializations
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.specializations
return Object.keys(roles).map((roleKey) => {
const roleConfig = roles
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.specializations.roles
return Object.keys(concentrations).map((concentrationKey) => {
const concentrationConfig = concentrations
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.wizardProgress, 10) // TODO refactor, i don't trust this...
for (let i = 0; i < index; i++) {
lookup.complete = true
}
lookup.active = true
return lookup
}
// END SELECTORS
`
Preview