Add view logic for all projects

This commit is contained in:
Berkeley Martinez
2016-06-07 20:41:42 -07:00
parent 1b301b0c0d
commit dc36396369
19 changed files with 546 additions and 229 deletions

View File

@@ -4,7 +4,10 @@ import { reducer as formReducer } from 'redux-form';
import { reducer as app } from './redux';
import entitiesReducer from './redux/entities-reducer';
import { reducer as hikesApp } from './routes/Hikes/redux';
import { reducer as challengesApp } from './routes/challenges/redux';
import {
reducer as challengesApp,
projectNormalizer
} from './routes/challenges/redux';
import {
reducer as jobsApp,
formNormalizer as jobsNormalizer
@@ -18,6 +21,9 @@ export default function createReducer(sideReducers = {}) {
hikesApp,
jobsApp,
challengesApp,
form: formReducer.normalize(jobsNormalizer)
form: formReducer.normalize({
...jobsNormalizer,
...projectNormalizer
})
});
}

View File

@@ -25,7 +25,11 @@ export default handleActions(
toast
}),
[types.setUser]: (state, { payload: user }) => ({ ...state, ...user }),
[types.setUser]: (state, { payload: user }) => ({
...state,
...user,
isSignedIn: true
}),
[types.challengeSaved]: (state, { payload: { points = 0 } }) => ({
...state,

View File

@@ -5,12 +5,7 @@ import { push } from 'react-router-redux';
import { reduxForm } from 'redux-form';
// import debug from 'debug';
import dedent from 'dedent';
import {
isAscii,
isEmail,
isURL
} from 'validator';
import { isAscii, isEmail } from 'validator';
import {
Button,
@@ -19,6 +14,13 @@ import {
Row
} from 'react-bootstrap';
import {
isValidURL,
makeOptional,
makeRequired,
createFormValidator,
getValidationState
} from '../../../utils/form';
import { saveForm, loadSavedForm } from '../redux/actions';
// const log = debug('fcc:jobs:newForm');
@@ -48,10 +50,6 @@ const certTypes = {
isBackEndCert: 'isBackEndCert'
};
function isValidURL(data) {
return isURL(data, { require_protocol: true });
}
const fields = [
'position',
'locale',
@@ -78,35 +76,6 @@ const fieldValidators = {
howToApply: makeRequired(isAscii)
};
function makeOptional(validator) {
return val => val ? validator(val) : true;
}
function makeRequired(validator) {
return (val) => val ? validator(val) : false;
}
function validateForm(values) {
return Object.keys(fieldValidators)
.map(field => {
if (fieldValidators[field](values[field])) {
return null;
}
return { [field]: !fieldValidators[field](values[field]) };
})
.filter(Boolean)
.reduce((errors, error) => ({ ...errors, ...error }), {});
}
function getBsStyle(field) {
if (field.pristine) {
return null;
}
return field.error ?
'error' :
'success';
}
export class NewJob extends PureComponent {
static displayName = 'NewJob';
@@ -223,7 +192,7 @@ export class NewJob extends PureComponent {
</div>
<hr />
<Input
bsStyle={ getBsStyle(position) }
bsStyle={ getValidationState(position) }
label='Job Title'
labelClassName={ labelClass }
placeholder={
@@ -235,7 +204,7 @@ export class NewJob extends PureComponent {
{ ...position }
/>
<Input
bsStyle={ getBsStyle(locale) }
bsStyle={ getValidationState(locale) }
label='Location'
labelClassName={ labelClass }
placeholder='e.g. San Francisco, Remote, etc.'
@@ -245,7 +214,7 @@ export class NewJob extends PureComponent {
{ ...locale }
/>
<Input
bsStyle={ getBsStyle(description) }
bsStyle={ getValidationState(description) }
label='Description'
labelClassName={ labelClass }
required={ true }
@@ -268,7 +237,7 @@ export class NewJob extends PureComponent {
<h2>How should they apply?</h2>
</div>
<Input
bsStyle={ getBsStyle(howToApply) }
bsStyle={ getValidationState(howToApply) }
label=' '
labelClassName={ labelClass }
placeholder={ howToApplyCopy }
@@ -286,7 +255,7 @@ export class NewJob extends PureComponent {
<h2>Tell us about your organization</h2>
</div>
<Input
bsStyle={ getBsStyle(company) }
bsStyle={ getValidationState(company) }
label='Company Name'
labelClassName={ labelClass }
onChange={ (e) => handleChange('company', e) }
@@ -295,7 +264,7 @@ export class NewJob extends PureComponent {
{ ...company }
/>
<Input
bsStyle={ getBsStyle(email) }
bsStyle={ getValidationState(email) }
label='Email'
labelClassName={ labelClass }
placeholder='This is how we will contact you'
@@ -305,7 +274,7 @@ export class NewJob extends PureComponent {
{ ...email }
/>
<Input
bsStyle={ getBsStyle(url) }
bsStyle={ getValidationState(url) }
label='URL'
labelClassName={ labelClass }
placeholder='http://yourcompany.com'
@@ -314,7 +283,7 @@ export class NewJob extends PureComponent {
{ ...url }
/>
<Input
bsStyle={ getBsStyle(logo) }
bsStyle={ getValidationState(logo) }
label='Logo'
labelClassName={ labelClass }
placeholder='http://yourcompany.com/logo.png'
@@ -381,7 +350,7 @@ export default reduxForm(
{
form: 'NewJob',
fields,
validate: validateForm
validate: createFormValidator(fieldValidators)
},
state => ({ initialValues: state.jobsApp.initialValues }),
{

View File

@@ -1,42 +1,19 @@
import normalizeUrl from 'normalize-url';
import {
inHTMLData,
uriInSingleQuotedAttr
} from 'xss-filters';
const normalizeOptions = {
stripWWW: false
};
function ifDefinedNormalize(normalizer) {
return value => value ? normalizer(value) : value;
}
function formatUrl(url) {
if (
typeof url === 'string' &&
url.length > 4 &&
url.indexOf('.') !== -1
) {
// prevent trailing / from being stripped during typing
let lastChar = '';
if (url.substring(url.length - 1) === '/') {
lastChar = '/';
}
return normalizeUrl(url, normalizeOptions) + lastChar;
}
return url;
}
import { callIfDefined, formatUrl } from '../../../utils/form';
export default {
NewJob: {
position: ifDefinedNormalize(inHTMLData),
locale: ifDefinedNormalize(inHTMLData),
description: ifDefinedNormalize(inHTMLData),
email: ifDefinedNormalize(inHTMLData),
url: ifDefinedNormalize(value => formatUrl(uriInSingleQuotedAttr(value))),
logo: ifDefinedNormalize(value => formatUrl(uriInSingleQuotedAttr(value))),
company: ifDefinedNormalize(inHTMLData),
howToApply: ifDefinedNormalize(inHTMLData)
position: callIfDefined(inHTMLData),
locale: callIfDefined(inHTMLData),
description: callIfDefined(inHTMLData),
email: callIfDefined(inHTMLData),
url: callIfDefined(value => formatUrl(uriInSingleQuotedAttr(value))),
logo: callIfDefined(value => formatUrl(uriInSingleQuotedAttr(value))),
company: callIfDefined(inHTMLData),
howToApply: callIfDefined(inHTMLData)
}
};

View File

@@ -15,7 +15,8 @@ import { challengeSelector } from '../redux/selectors';
const views = {
step: Step,
classic: Classic,
project: Project
project: Project,
simple: Project
};
const bindableActions = {

View File

@@ -0,0 +1,154 @@
import React, { PropTypes } from 'react';
import { reduxForm } from 'redux-form';
import {
Button,
FormGroup,
FormControl
} from 'react-bootstrap';
import {
isValidURL,
makeRequired,
createFormValidator,
getValidationState
} from '../../../../utils/form';
import { submitChallenge, showProjectSubmit } from '../../redux/actions';
const propTypes = {
isSignedIn: PropTypes.bool,
isSubmitting: PropTypes.bool,
showProjectSubmit: PropTypes.func,
fields: PropTypes.object,
handleSubmit: PropTypes.func,
submitChallenge: PropTypes.func
};
const bindableActions = { submitChallenge, showProjectSubmit };
const frontEndFields = [ 'solution' ];
const backEndFields = [
'solution',
'githubLink'
];
const fieldValidators = {
solution: makeRequired(isValidURL)
};
const backEndFieldValidators = {
...fieldValidators,
githubLink: makeRequired(isValidURL)
};
export function SolutionInput({ solution }) {
return (
<FormGroup
controlId='solution'
validationState={ getValidationState(solution) }
>
<FormControl
name='solution'
placeholder='https://codepen.io/your-pen-here'
type='url'
{ ...solution}
/>
</FormGroup>
);
}
SolutionInput.propTypes = { solution: PropTypes.object };
export function _FrontEndForm({
fields,
handleSubmit,
submitChallenge,
isSubmitting,
showProjectSubmit
}) {
const buttonCopy = isSubmitting ?
'Submit and go to my next challenge' :
"I've completed this challenge";
return (
<form
name='NewFrontEndProject'
onSubmit={ handleSubmit(submitChallenge)}
>
{ isSubmitting ? <SolutionInput { ...fields }/> : null }
<Button
block={ true }
bsStyle='primary'
className='btn-big'
onClick={ isSubmitting ? null : showProjectSubmit }
type={ isSubmitting ? 'submit' : null }
>
{ buttonCopy } (ctrl + enter)
</Button>
</form>
);
}
_FrontEndForm.propTypes = propTypes;
export const FrontEndForm = reduxForm(
{
form: 'NewFrontEndProject',
fields: frontEndFields,
validate: createFormValidator(fieldValidators)
},
null,
bindableActions
)(_FrontEndForm);
export function _BackEndForm({
fields: { solution, githubLink },
handleSubmit,
submitChallenge,
isSubmitting,
showProjectSubmit
}) {
const buttonCopy = isSubmitting ?
'Submit and go to my next challenge' :
"I've completed this challenge";
return (
<form
name='NewBackEndProject'
onSubmit={ handleSubmit(submitChallenge)}
>
{ isSubmitting ? <SolutionInput solution={ solution }/> : null }
{ isSubmitting ?
<FormGroup
controlId='githubLink'
validationState={ getValidationState(githubLink) }
>
<FormControl
name='githubLink'
placeholder='https://github.com/your-username/your-project'
type='url'
{ ...githubLink }
/>
</FormGroup> :
null
}
<Button
block={ true }
bsStyle='primary'
className='btn-big'
onClick={ isSubmitting ? null : showProjectSubmit }
type={ isSubmitting ? 'submit' : null }
>
{ buttonCopy } (ctrl + enter)
</Button>
</form>
);
}
_BackEndForm.propTypes = propTypes;
export const BackEndForm = reduxForm(
{
form: 'NewBackEndProject',
fields: backEndFields,
validate: createFormValidator(backEndFieldValidators)
},
null,
bindableActions
)(_BackEndForm);

View File

@@ -4,42 +4,28 @@ import { connect } from 'react-redux';
import Youtube from 'react-youtube';
import PureComponent from 'react-pure-render/component';
import { Button, ButtonGroup, Col } from 'react-bootstrap';
import { Col } from 'react-bootstrap';
import SidePanel from './Side-Panel.jsx';
import ToolPanel from './Tool-Panel.jsx';
import { challengeSelector } from '../../redux/selectors';
const bindableActions = {};
const mapStateToProps = createSelector(
challengeSelector,
state => state.app.windowHeight,
state => state.app.navHeight,
state => state.app.isSignedIn,
state => state.challengesApp.tests,
state => state.challengesApp.output,
(
{
challenge: {
id,
title,
description,
challengeSeed: [ videoId = ''] = []
challengeSeed: [ videoId = '' ] = []
} = {}
},
windowHeight,
navHeight,
isSignedIn,
tests,
output
}
) => ({
id,
videoId,
title,
description,
height: windowHeight - navHeight - 20,
tests,
output,
isSignedIn
description
})
);
@@ -50,32 +36,9 @@ export class Project extends PureComponent {
videoId: PropTypes.string,
title: PropTypes.string,
description: PropTypes.arrayOf(PropTypes.string),
isCompleted: PropTypes.bool,
isSignedIn: PropTypes.bool
isCompleted: PropTypes.bool
};
renderIcon(isCompleted) {
if (!isCompleted) {
return null;
}
return (
<i
className='ion-checkmark-circled text-primary'
title='Completed'
/>
);
}
renderDescription(title = '', description = []) {
return description
.map((line, index) => (
<li
className='step-text wrappable'
dangerouslySetInnerHTML={{ __html: line }}
key={ title.slice(6) + index }
/>
));
}
render() {
const {
@@ -83,30 +46,22 @@ export class Project extends PureComponent {
title,
videoId,
isCompleted,
description,
isSignedIn
description
} = this.props;
const buttonCopy = isSignedIn ?
"I've completed this challenge" :
'Go to my next challenge';
return (
<div>
<Col md={ 4 }>
<h4 className='text-center challenge-instructions-title'>
{ title }
{ this.renderIcon(isCompleted) }
</h4>
<hr />
<ul>
{ this.renderDescription(title, description) }
</ul>
<SidePanel
description={ description }
isCompleted={ isCompleted }
title={ title }
/>
</Col>
<Col
md={ 8 }
xs={ 12 }
>
>
<div className='embed-responsive embed-responsive-16by9'>
<Youtube
className='embed-responsive-item'
@@ -115,30 +70,7 @@ export class Project extends PureComponent {
/>
</div>
<br />
<Button
block={ true }
bsStyle='primary'
className='btn-big'
>
{ buttonCopy } (ctrl + enter)
</Button>
<div className='button-spacer' />
<ButtonGroup justified={ true }>
<Button
bsStyle='primary'
className='btn-primary-ghost btn-big'
componentClass='div'
>
Help
</Button>
<Button
bsStyle='primary'
className='btn-primary-ghost btn-big'
componentClass='div'
>
Bug
</Button>
</ButtonGroup>
<ToolPanel />
<br />
</Col>
</div>
@@ -147,6 +79,5 @@ export class Project extends PureComponent {
}
export default connect(
mapStateToProps,
bindableActions
mapStateToProps
)(Project);

View File

@@ -0,0 +1,50 @@
import React, { PropTypes } from 'react';
import PureComponent from 'react-pure-render/component';
export default class SidePanel extends PureComponent {
static propTypes = {
title: PropTypes.string,
description: PropTypes.arrayOf(PropTypes.string),
isCompleted: PropTypes.bool,
isSignedIn: PropTypes.bool
};
renderIcon(isCompleted) {
if (!isCompleted) {
return null;
}
return (
<i
className='ion-checkmark-circled text-primary'
title='Completed'
/>
);
}
renderDescription(title = '', description = []) {
return description.map((line, index) => (
<li
className='step-text wrappable'
dangerouslySetInnerHTML={{ __html: line }}
key={ title.slice(6) + index }
/>
));
}
render() {
const { title, description, isCompleted } = this.props;
return (
<div>
<h4 className='text-center challenge-instructions-title'>
{ title }
{ this.renderIcon(isCompleted) }
</h4>
<hr />
<ul>
{ this.renderDescription(title, description) }
</ul>
</div>
);
}
}

View File

@@ -0,0 +1,101 @@
import React, { PropTypes } from 'react';
import PureComponent from 'react-pure-render/component';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Button, ButtonGroup } from 'react-bootstrap';
import {
FrontEndForm,
BackEndForm
} from './Forms.jsx';
import { submitChallenge } from '../../redux/actions';
import { challengeSelector } from '../../redux/selectors';
import {
simpleProject,
frontEndProject
} from '../../../../utils/challengeTypes';
const mapStateToProps = createSelector(
challengeSelector,
state => state.app.isSignedIn,
state => state.challengesApp.isSubmitting,
(
{ challenge: { challengeType = simpleProject } },
isSignedIn,
isSubmitting
) => ({
isSignedIn,
isSubmitting,
isSimple: challengeType === simpleProject,
isFrontEnd: challengeType === frontEndProject
})
);
export class ToolPanel extends PureComponent {
static propTypes = {
isSignedIn: PropTypes.bool,
isSimple: PropTypes.bool,
isFrontEnd: PropTypes.bool,
isSubmitting: PropTypes.bool
};
renderSubmitButton(isSignedIn, submitChallenge) {
const buttonCopy = isSignedIn ?
'Submit and go to my next challenge' :
"I've completed this challenge";
return (
<Button
block={ true }
bsStyle='primary'
className='btn-big'
onClick={ submitChallenge }
>
{ buttonCopy } (ctrl + enter)
</Button>
);
}
render() {
const {
isFrontEnd,
isSimple,
isSignedIn,
isSubmitting,
submitChallenge
} = this.props;
const FormElement = isFrontEnd ? FrontEndForm : BackEndForm;
return (
<div>
{
isSimple ?
this.renderSubmitButton(isSignedIn, submitChallenge) :
<FormElement isSubmitting={ isSubmitting }/>
}
<div className='button-spacer' />
<ButtonGroup justified={ true }>
<Button
bsStyle='primary'
className='btn-primary-ghost btn-big'
componentClass='div'
>
Help
</Button>
<Button
bsStyle='primary'
className='btn-primary-ghost btn-big'
componentClass='div'
>
Bug
</Button>
</ButtonGroup>
</div>
);
}
}
export default connect(
mapStateToProps,
{ submitChallenge }
)(ToolPanel);

View File

@@ -58,6 +58,7 @@ export const updateOutput = createAction(types.updateOutput, loggerToStr);
export const checkChallenge = createAction(types.checkChallenge);
export const showProjectSubmit = createAction(types.showProjectSubmit);
let id = 0;
export const showChallengeComplete = createAction(
types.showChallengeComplete,

View File

@@ -64,6 +64,35 @@ function completedChallenge(state) {
return Observable.merge(saveChallenge$, challengeCompleted$);
}
function submitModern(type, state) {
const { tests } = state.challengesApp;
if (tests.length > 0 && tests.every(test => test.pass && !test.err)) {
if (type === types.checkChallenge) {
return Observable.of(
showChallengeComplete()
);
}
if (type === types.submitChallenge) {
return completedChallenge(state);
}
}
return Observable.just(makeToast({
message: 'Not all tests are passing, yet.',
title: 'Almost There!',
type: 'info'
}));
}
function submitFrontEnd() {
return Observable.just(null);
}
const submitTypes = {
tests: submitModern,
'project.frontEnd': submitFrontEnd
};
export default function completionSaga(actions$, getState) {
return actions$
.filter(({ type }) => (
@@ -71,36 +100,22 @@ export default function completionSaga(actions$, getState) {
type === types.submitChallenge ||
type === types.moveToNextChallenge
))
.flatMap(({ type }) => {
.flatMap(({ type, payload }) => {
const state = getState();
const { tests } = state.challengesApp;
if (tests.length > 0 && tests.every(test => test.pass && !test.err)) {
if (type === types.checkChallenge) {
return Observable.of(
showChallengeComplete()
);
}
if (type === types.submitChallenge) {
return completedChallenge(state);
}
if (type === types.moveToNextChallenge) {
const nextChallenge = getNextChallenge(
state.challengesApp.challenge,
state.entities,
state.challengesApp.superBlocks
);
return Observable.of(
updateCurrentChallenge(nextChallenge),
push(`/challenges/${nextChallenge.dashedName}`)
);
}
const { submitType } = challengeSelector(state);
const submitter = submitTypes[submitType] ||
(() => Observable.just(null));
if (type === types.moveToNextChallenge) {
const nextChallenge = getNextChallenge(
state.challengesApp.challenge,
state.entities,
state.challengesApp.superBlocks
);
return Observable.of(
updateCurrentChallenge(nextChallenge),
push(`/challenges/${nextChallenge.dashedName}`)
);
}
return Observable.just(makeToast({
message: 'Not all tests are passing, yet.',
title: 'Almost There!',
type: 'info'
}));
return submitter(type, state, payload);
});
}

View File

@@ -5,4 +5,6 @@ export types from './types';
import fetchChallengesSaga from './fetch-challenges-saga';
import completionSaga from './completion-saga';
export projectNormalizer from './project-normalizer';
export const sagas = [ fetchChallengesSaga, completionSaga ];

View File

@@ -0,0 +1,11 @@
import { callIfDefined, formatUrl } from '../../../utils/form';
export default {
NewFrontEndProject: {
solution: callIfDefined(formatUrl)
},
NewBackEndProject: {
githubLink: callIfDefined(formatUrl),
solution: callIfDefined(formatUrl)
}
};

View File

@@ -2,7 +2,7 @@ import { handleActions } from 'redux-actions';
import { createPoly } from '../../../../utils/polyvinyl';
import types from './types';
import { BONFIRE, HTML, JS } from '../../../utils/challengeTypes';
import { bonfire, html, js } from '../../../utils/challengeTypes';
import {
arrayToString,
buildSeed,
@@ -50,6 +50,10 @@ const mainReducer = handleActions(
...state,
toast
}),
[types.showProjectSubmit]: state => ({
...state,
isSubmitting: true
}),
// map
[types.updateFilter]: (state, { payload = ''}) => ({
@@ -105,9 +109,9 @@ const filesReducer = handleActions(
return challenge.files;
}
if (
challenge.challengeType !== HTML &&
challenge.challengeType !== JS &&
challenge.challengeType !== BONFIRE
challenge.challengeType !== html &&
challenge.challengeType !== js &&
challenge.challengeType !== bonfire
) {
return {};
}

View File

@@ -2,16 +2,34 @@ import * as challengeTypes from '../../../utils/challengeTypes';
import { createSelector } from 'reselect';
const viewTypes = {
[ challengeTypes.HTML ]: 'classic',
[ challengeTypes.JS ]: 'classic',
[ challengeTypes.BONFIRE ]: 'classic',
[ challengeTypes.ZIPLINE ]: 'project',
[ challengeTypes.BASEJUMP ]: 'project',
[ challengeTypes.html]: 'classic',
[ challengeTypes.js ]: 'classic',
[ challengeTypes.bonfire ]: 'classic',
[ challengeTypes.frontEndProject]: 'project',
[ challengeTypes.backEndProject]: 'project',
// might not be used anymore
[ challengeTypes.OLDVIDEO ]: 'video',
[ challengeTypes.simpleProject]: 'project',
// formally hikes
[ challengeTypes.VIDEO ]: 'video',
[ challengeTypes.STEP ]: 'step'
[ challengeTypes.video ]: 'video',
[ challengeTypes.step ]: 'step'
};
const submitTypes = {
[ challengeTypes.html ]: 'tests',
[ challengeTypes.js ]: 'tests',
[ challengeTypes.bonfire ]: 'tests',
// requires just a button press
[ challengeTypes.simpleProject ]: 'project.simple',
// requires just a single url
// like codepen.com/my-project
[ challengeTypes.frontEndProject ]: 'project.frontEnd',
// requires two urls
// a hosted URL where the app is running live
// project code url like GitHub
[ challengeTypes.backEndProject ]: 'project.backEnd',
// formally hikes
[ challengeTypes.video ]: 'video',
[ challengeTypes.step ]: 'step'
};
export const challengeSelector = createSelector(
@@ -22,14 +40,13 @@ export const challengeSelector = createSelector(
return {};
}
const challenge = challengeMap[challengeName];
const challengeType = challenge && challenge.challengeType;
return {
challenge: challenge,
viewType: viewTypes[challenge.challengeType] || 'classic',
showPreview: challenge &&
challenge.challengeType === challengeTypes.HTML,
mode: challenge && challenge.challengeType === challengeTypes.HTML ?
challenge,
viewType: viewTypes[challengeType] || 'classic',
submitType: submitTypes[challengeType] || 'tests',
showPreview: challengeType === challengeTypes.html,
mode: challenge && challengeType === challengeTypes.html ?
'text/html' :
'javascript'
};

View File

@@ -30,6 +30,7 @@ export default createTypes([
'updateTests',
'checkChallenge',
'showChallengeComplete',
'showProjectSubmit',
'submitChallenge',
'moveToNextChallenge',

View File

@@ -1,5 +1,5 @@
import { compose } from 'redux';
import { BONFIRE, HTML, JS } from '../../utils/challengeTypes';
import { bonfire, html, js } from '../../utils/challengeTypes';
import { dashify } from '../../../utils';
export function encodeScriptTags(value) {
@@ -41,9 +41,9 @@ export function buildSeed({ challengeSeed = [] } = {}) {
}
const pathsMap = {
[HTML]: 'html',
[JS]: 'js',
[BONFIRE]: 'js'
[html]: 'html',
[js]: 'js',
[bonfire]: 'js'
};
export function getPreFile({ challengeType }) {

View File

@@ -1,8 +1,11 @@
export const HTML = '0';
export const JS = '1';
export const OLDVIDEO = '2';
export const ZIPLINE = '3';
export const BASEJUMP = '4';
export const BONFIRE = '5';
export const VIDEO = '6';
export const STEP = '7';
export const html = '0';
export const js = '1';
export const oldVideo = '2';
export const simpleProject = '2';
export const zipline = '3';
export const frontEndProject = '3';
export const basejump = '4';
export const backEndProject = '4';
export const bonfire = '5';
export const video = '6';
export const step = '7';

70
common/app/utils/form.js Normal file
View File

@@ -0,0 +1,70 @@
import normalizeUrl from 'normalize-url';
import { isURL } from 'validator';
const normalizeOptions = {
stripWWW: false
};
// callIfDefined(fn: (Any) => Any) => (value: Any) => Any
export function callIfDefined(fn) {
return value => value ? fn(value) : value;
}
// formatUrl(url: String) => String
export function formatUrl(url) {
if (
typeof url === 'string' &&
url.length > 4 &&
url.indexOf('.') !== -1
) {
// prevent trailing / from being stripped during typing
let lastChar = '';
if (url.substring(url.length - 1) === '/') {
lastChar = '/';
}
// prevent normalize-url from stripping last dot during typing
if (url.substring(url.length - 1) === '.') {
lastChar = '.';
}
return normalizeUrl(url, normalizeOptions) + lastChar;
}
return url;
}
export function isValidURL(data) {
/* eslint-disable quote-props */
return isURL(data, { 'require_protocol': true });
/* eslint-enable quote-props */
}
export function makeOptional(validator) {
return val => val ? validator(val) : true;
}
export function makeRequired(validator) {
return (val) => val ? validator(val) : false;
}
export function createFormValidator(fieldValidators) {
const fieldKeys = Object.keys(fieldValidators);
return values => fieldKeys
.map(field => {
if (fieldValidators[field](values[field])) {
return null;
}
return { [field]: !fieldValidators[field](values[field]) };
})
.filter(Boolean)
.reduce((errors, error) => ({ ...errors, ...error }), {});
}
export function getValidationState(field) {
if (field.pristine) {
return null;
}
return field.error ?
'error' :
'success';
}