From 6d8835ba5605fc476ddc8c920b9e9a8ed062b785 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 11 Sep 2015 10:58:24 -0700 Subject: [PATCH 01/20] return undefined if job is not found null values count as values when using default values so properties must be undefined when expecting default value to work --- common/app/routes/Jobs/flux/Actions.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index 4df2ae42c6..ce31070736 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -56,7 +56,10 @@ export default Actions({ debug('job services experienced an issue', err); return jobActions.setError({ err }); } - jobActions.setJobs({ currentJob: job }); + if (job) { + jobActions.setJobs({ currentJob: job }); + } + jobActions.setJobs({}); }); }); return jobActions; From e579cbd778433421b6eee03fb2c12c5aab1785c8 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sun, 13 Sep 2015 18:12:22 -0700 Subject: [PATCH 02/20] update to react-router 1.0.0-rc1 --- client/index.js | 19 +++++++++++++------ common/app/app-stream.jsx | 12 ++++++------ common/app/routes/Jobs/components/Jobs.jsx | 6 +++--- package.json | 3 ++- server/boot/a-react.js | 18 +++++++++--------- 5 files changed, 33 insertions(+), 25 deletions(-) diff --git a/client/index.js b/client/index.js index 79c5c541c1..a7818eece5 100644 --- a/client/index.js +++ b/client/index.js @@ -4,7 +4,7 @@ import React from 'react'; import Fetchr from 'fetchr'; import debugFactory from 'debug'; import { Router } from 'react-router'; -import { history } from 'react-router/lib/BrowserHistory'; +import { createLocation, createHistory } from 'history'; import { hydrate } from 'thundercats'; import { Render } from 'thundercats-react'; @@ -18,21 +18,28 @@ const services = new Fetchr({ }); Rx.config.longStackSupport = !!debug.enabled; - +const history = createHistory(); +const appLocation = createLocation( + location.pathname + location.search +); // returns an observable -app$(history) +app$({ history, location: appLocation }) .flatMap( ({ AppCat }) => { + // instantiate the cat with service const appCat = AppCat(null, services); + // hydrate the stores return hydrate(appCat, catState) .map(() => appCat); }, - ({ initialState }, appCat) => ({ initialState, appCat }) + // not using nextLocation at the moment but will be used for + // redirects in the future + ({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat }) ) - .flatMap(({ initialState, appCat }) => { + .flatMap(({ props, appCat }) => { return Render( appCat, - React.createElement(Router, initialState), + React.createElement(Router, props), DOMContianer ); }) diff --git a/common/app/app-stream.jsx b/common/app/app-stream.jsx index 25ae2a6300..82d5568c09 100644 --- a/common/app/app-stream.jsx +++ b/common/app/app-stream.jsx @@ -1,17 +1,17 @@ import Rx from 'rx'; -import { Router } from 'react-router'; +import { match } from 'react-router'; import App from './App.jsx'; import AppCat from './Cat'; import childRoutes from './routes'; -const router$ = Rx.Observable.fromNodeCallback(Router.run, Router); +const route$ = Rx.Observable.fromNodeCallback(match); const routes = Object.assign({ components: App }, childRoutes); -export default function app$(location) { - return router$(routes, location) - .map(([initialState, transistion]) => { - return { initialState, transistion, AppCat }; +export default function app$({ location, history }) { + return route$({ routes, location, history }) + .map(([nextLocation, props]) => { + return { nextLocation, props, AppCat }; }); } diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index a6bc6a9a9f..6c40cf6b0c 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -1,6 +1,6 @@ import React, { cloneElement, PropTypes } from 'react'; import { contain } from 'thundercats-react'; -import { Navigation } from 'react-router'; +import { History } from 'react-router'; import { Button, Jumbotron, Row } from 'react-bootstrap'; import ListJobs from './List.jsx'; @@ -18,7 +18,7 @@ export default contain( jobActions: PropTypes.object, jobs: PropTypes.array }, - mixins: [Navigation], + mixins: [History], handleJobClick(id) { const { jobActions } = this.props; @@ -26,7 +26,7 @@ export default contain( return null; } jobActions.findJob(id); - this.transitionTo(`/jobs/${id}`); + this.history.pushState(null, `/jobs/${id}`); }, renderList(handleJobClick, jobs) { diff --git a/package.json b/package.json index 901b54ea3a..4b25530278 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "gulp-webpack": "^1.5.0", "helmet": "~0.9.0", "helmet-csp": "^0.2.3", + "history": "^1.9.0", "jade": "~1.8.0", "json-loader": "^0.5.2", "less": "~1.7.5", @@ -89,7 +90,7 @@ "react": "^0.13.3", "react-bootstrap": "~0.23.7", "react-motion": "~0.1.0", - "react-router": "https://github.com/BerkeleyTrue/react-router#freecodecamp", + "react-router": "^1.0.0-rc1", "react-vimeo": "^0.0.3", "request": "~2.53.0", "rev-del": "^1.0.5", diff --git a/server/boot/a-react.js b/server/boot/a-react.js index 6c0cd04819..9b1f4926c9 100644 --- a/server/boot/a-react.js +++ b/server/boot/a-react.js @@ -1,7 +1,7 @@ import React from 'react'; -import Router from 'react-router'; +import { RoutingContext } from 'react-router'; import Fetchr from 'fetchr'; -import Location from 'react-router/lib/Location'; +import { createLocation } from 'history'; import debugFactory from 'debug'; import { app$ } from '../../common/app'; import { RenderToString } from 'thundercats-react'; @@ -30,25 +30,25 @@ export default function reactSubRouter(app) { function serveReactApp(req, res, next) { const services = new Fetchr({ req }); - const location = new Location(req.path, req.query); + const location = createLocation(req.path); // returns a router wrapped app - app$(location) + app$({ location }) // if react-router does not find a route send down the chain - .filter(function({ initialState }) { - if (!initialState) { + .filter(function({ props}) { + if (!props) { debug('react tried to find %s but got 404', location.pathname); return next(); } - return !!initialState; + return !!props; }) - .flatMap(function({ initialState, AppCat }) { + .flatMap(function({ props, AppCat }) { // call thundercats renderToString // prefetches data and sets up it up for current state debug('rendering to string'); return RenderToString( AppCat(null, services), - React.createElement(Router, initialState) + React.createElement(RoutingContext, props) ); }) // makes sure we only get one onNext and closes subscription From a34bcc2266cd33c6afbea1a224c1d6f999cba6a0 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sun, 13 Sep 2015 22:14:49 -0700 Subject: [PATCH 03/20] fix (hack) override history object with original --- client/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/index.js b/client/index.js index a7818eece5..44bdb0a82c 100644 --- a/client/index.js +++ b/client/index.js @@ -37,6 +37,7 @@ app$({ history, location: appLocation }) ({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat }) ) .flatMap(({ props, appCat }) => { + props.history = history; return Render( appCat, React.createElement(Router, props), From fe144f7297deafbb88ea181a1db7590457d2f559 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 14 Sep 2015 12:12:31 -0700 Subject: [PATCH 04/20] add highlighting to jobs --- common/app/routes/Jobs/components/List.jsx | 2 ++ common/app/routes/index.js | 12 ++++-------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/common/app/routes/Jobs/components/List.jsx b/common/app/routes/Jobs/components/List.jsx index ec8325a98b..2457bcb7f0 100644 --- a/common/app/routes/Jobs/components/List.jsx +++ b/common/app/routes/Jobs/components/List.jsx @@ -22,6 +22,7 @@ export default React.createClass({ id, company, position, + isHighlighted, description, logo, city, @@ -44,6 +45,7 @@ export default React.createClass({ ); return ( { - cb(null, [ - Jobs, - Hikes - ]); - }, 0); - } + childRoutes: [ + Jobs, + Hikes + ] }; From 523af406417025dbbb42d0346bb728a1d7fdd3fc Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 14 Sep 2015 12:17:23 -0700 Subject: [PATCH 05/20] bump less remove old less middleware --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 4b25530278..16c58aae82 100644 --- a/package.json +++ b/package.json @@ -61,8 +61,7 @@ "history": "^1.9.0", "jade": "~1.8.0", "json-loader": "^0.5.2", - "less": "~1.7.5", - "less-middleware": "~2.0.1", + "less": "~2.5.1", "lodash": "^3.9.3", "loopback": "https://github.com/FreeCodeCamp/loopback.git#fix/no-password", "loopback-boot": "2.8.2", From d8e8f3bb67982ffab614de6236695a6470b0eb3b Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 14 Sep 2015 13:06:27 -0700 Subject: [PATCH 06/20] add `create job` modal --- .../routes/Jobs/components/CreateJobModal.jsx | 33 +++++++++++++++++++ common/app/routes/Jobs/components/Jobs.jsx | 23 ++++++++++--- common/app/routes/Jobs/flux/Actions.js | 6 ++++ common/app/routes/Jobs/flux/Store.js | 12 +++++-- 4 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 common/app/routes/Jobs/components/CreateJobModal.jsx diff --git a/common/app/routes/Jobs/components/CreateJobModal.jsx b/common/app/routes/Jobs/components/CreateJobModal.jsx new file mode 100644 index 0000000000..d3e6d34581 --- /dev/null +++ b/common/app/routes/Jobs/components/CreateJobModal.jsx @@ -0,0 +1,33 @@ +import React, { PropTypes } from 'react'; +import { Button, Modal } from 'react-bootstrap'; + +export default React.createClass({ + displayName: 'CreateJobsModal', + propTypes: { + onHide: PropTypes.func, + showModal: PropTypes.bool + }, + + render() { + const { + showModal, + onHide + } = this.props; + + return ( + + +

Welcome to Free Code Camp's board

+

We post jobs specifically target to our junior developers.

+ +
+
+ ); + } +}); diff --git a/common/app/routes/Jobs/components/Jobs.jsx b/common/app/routes/Jobs/components/Jobs.jsx index 6c40cf6b0c..a4d8354b3f 100644 --- a/common/app/routes/Jobs/components/Jobs.jsx +++ b/common/app/routes/Jobs/components/Jobs.jsx @@ -2,6 +2,8 @@ import React, { cloneElement, PropTypes } from 'react'; import { contain } from 'thundercats-react'; import { History } from 'react-router'; import { Button, Jumbotron, Row } from 'react-bootstrap'; + +import CreateJobModal from './CreateJobModal.jsx'; import ListJobs from './List.jsx'; export default contain( @@ -13,12 +15,14 @@ export default contain( React.createClass({ displayName: 'Jobs', + mixins: [History], + propTypes: { children: PropTypes.element, jobActions: PropTypes.object, - jobs: PropTypes.array + jobs: PropTypes.array, + showModal: PropTypes.bool }, - mixins: [History], handleJobClick(id) { const { jobActions } = this.props; @@ -48,7 +52,12 @@ export default contain( }, render() { - const { children, jobs } = this.props; + const { + children, + jobs, + showModal, + jobActions + } = this.props; return (
@@ -62,7 +71,8 @@ export default contain(

@@ -70,7 +80,10 @@ export default contain( { this.renderChild(children, jobs) || this.renderList(this.handleJobClick, jobs) } - + +
); } diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index ce31070736..40c78025d0 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -31,6 +31,12 @@ export default Actions({ getJob: null, getJobs(params) { return { params }; + }, + openModal() { + return { showModal: true }; + }, + closeModal() { + return { showModal: false }; } }) .refs({ displayName: 'JobActions' }) diff --git a/common/app/routes/Jobs/flux/Store.js b/common/app/routes/Jobs/flux/Store.js index 2fdfa50207..abe3eb61cc 100644 --- a/common/app/routes/Jobs/flux/Store.js +++ b/common/app/routes/Jobs/flux/Store.js @@ -6,12 +6,20 @@ const { transformer } = Store; -export default Store() +export default Store({ showModal: false }) .refs({ displayName: 'JobsStore' }) .init(({ instance: jobsStore, args: [cat] }) => { - const { setJobs, findJob, setError } = cat.getActions('JobActions'); + const { + setJobs, + findJob, + setError, + openModal, + closeModal + } = cat.getActions('JobActions'); const register = createRegistrar(jobsStore); register(setter(setJobs)); register(transformer(findJob)); register(setter(setError)); + register(setter(openModal)); + register(setter(closeModal)); }); From d3f2d603df7d2f36780624da2eb4f3e70e2730ca Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 14 Sep 2015 17:31:24 -0700 Subject: [PATCH 07/20] fix nodemon should ignore seed files --- gulpfile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/gulpfile.js b/gulpfile.js index afe9b421f5..fd5fdda38d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -49,6 +49,7 @@ var paths = { '!public/js/bundle*', 'node_modules/', 'client/', + 'seed', 'server/manifests/*.json', 'server/rev-manifest.json' ], From 41933a83604a4ebb0bcbca577b68e4cc378fe227 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 14 Sep 2015 17:31:48 -0700 Subject: [PATCH 08/20] initial job form and job form nav --- .../routes/Jobs/components/CreateJobModal.jsx | 11 +++- common/app/routes/Jobs/components/NewJob.jsx | 56 +++++++++++++++++++ common/app/routes/Jobs/index.js | 4 ++ 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 common/app/routes/Jobs/components/NewJob.jsx diff --git a/common/app/routes/Jobs/components/CreateJobModal.jsx b/common/app/routes/Jobs/components/CreateJobModal.jsx index d3e6d34581..8a02400293 100644 --- a/common/app/routes/Jobs/components/CreateJobModal.jsx +++ b/common/app/routes/Jobs/components/CreateJobModal.jsx @@ -1,13 +1,21 @@ import React, { PropTypes } from 'react'; +import { History } from 'react-router'; import { Button, Modal } from 'react-bootstrap'; export default React.createClass({ displayName: 'CreateJobsModal', + propTypes: { onHide: PropTypes.func, showModal: PropTypes.bool }, + mixins: [History], + + goToNewJob() { + this.history.pushState(null, '/jobs/new'); + }, + render() { const { showModal, @@ -23,7 +31,8 @@ export default React.createClass({

We post jobs specifically target to our junior developers.

diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx new file mode 100644 index 0000000000..a412ff6d7d --- /dev/null +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -0,0 +1,56 @@ +import React, { PropTypes } from 'react'; +import { contain } from 'thundercats-react'; +import { + Col, + Input, + Row, + Well +} from 'react-bootstrap'; + +export default contain({ + actions: 'jobActions', + store: 'jobsStore', + map({ form = {} }) { + return form; + } + }, + React.createClass({ + displayName: 'NewJob', + propTypes: { + jobActions: PropTypes.object + }, + render() { + return ( +
+ + + +

Create You Job Post

+
+ + + +
+
+ +
+
+ ); + } + }) +); diff --git a/common/app/routes/Jobs/index.js b/common/app/routes/Jobs/index.js index ac6b07f866..3564332d32 100644 --- a/common/app/routes/Jobs/index.js +++ b/common/app/routes/Jobs/index.js @@ -1,4 +1,5 @@ import Jobs from './components/Jobs.jsx'; +import NewJob from './components/NewJob.jsx'; import Show from './components/Show.jsx'; /* @@ -11,6 +12,9 @@ export default { childRoutes: [{ path: '/jobs', component: Jobs + }, { + path: 'jobs/new', + component: NewJob }, { path: 'jobs/:id', component: Show From 65572d65c8a234d388b059811dda32ca94a003db Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 21 Sep 2015 20:38:09 -0700 Subject: [PATCH 09/20] close modal before transition to job form --- common/app/routes/Jobs/components/CreateJobModal.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/common/app/routes/Jobs/components/CreateJobModal.jsx b/common/app/routes/Jobs/components/CreateJobModal.jsx index 8a02400293..446ed957d6 100644 --- a/common/app/routes/Jobs/components/CreateJobModal.jsx +++ b/common/app/routes/Jobs/components/CreateJobModal.jsx @@ -12,7 +12,8 @@ export default React.createClass({ mixins: [History], - goToNewJob() { + goToNewJob(onHide) { + onHide(); this.history.pushState(null, '/jobs/new'); }, @@ -32,7 +33,7 @@ export default React.createClass({ From 10b3b8d75854187741fa008e1b5830377bd35bcf Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Mon, 21 Sep 2015 22:41:12 -0700 Subject: [PATCH 10/20] add validation to one input not sure this is the best approach --- common/app/routes/Jobs/components/NewJob.jsx | 55 +++++++++++++++++++- common/app/routes/Jobs/flux/Actions.js | 35 ++++++++++++- common/app/routes/Jobs/flux/Store.js | 7 ++- 3 files changed, 92 insertions(+), 5 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index a412ff6d7d..61bf46530a 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -7,19 +7,57 @@ import { Well } from 'react-bootstrap'; +const defaults = { + 'string': { + value: '', + valid: false, + pristine: true + } +}; + +function defaultValue(type) { + return defaults[type]; +} + +function validatePosition(value) { + if (!value && typeof value !== 'string') { + return false; + } + return true; +} + export default contain({ actions: 'jobActions', store: 'jobsStore', map({ form = {} }) { - return form; + const { + position = defaultValue('string'), + location = defaultValue('string'), + description = defaultValue('string') + } = form; + return { + position, + location, + description + }; } }, React.createClass({ displayName: 'NewJob', + propTypes: { - jobActions: PropTypes.object + jobActions: PropTypes.object, + position: PropTypes.object, + location: PropTypes.object, + description: PropTypes.object }, + render() { + const { + jobActions, + position + } = this.props; + return (
@@ -28,10 +66,23 @@ export default contain({

Create You Job Post

{ + jobActions.handleForm({ + name: 'position', + value, + validator: validatePosition + }); + }} placeholder='Position' type='text' + value={ position.value } wrapperClassName='col-xs-10' /> {} }) { + if (!name) { + // operation noop + return { replace: null }; + } + if (!validator(value)) { + return { + transform(oldState) { + const { oldForm } = oldState; + const newState = assign({}, oldState); + newState.form = assign( + {}, + oldForm, + { [name]: { value, valid: false, pristine: false }} + ); + return newState; + } + }; + } + return { + transform(oldState) { + const { oldForm } = oldState; + const newState = assign({}, oldState); + newState.form = assign( + {}, + oldForm, + { [name]: { value, valid: true, pristine: false }} + ); + return newState; + } + }; } }) .refs({ displayName: 'JobActions' }) diff --git a/common/app/routes/Jobs/flux/Store.js b/common/app/routes/Jobs/flux/Store.js index abe3eb61cc..a73235f1aa 100644 --- a/common/app/routes/Jobs/flux/Store.js +++ b/common/app/routes/Jobs/flux/Store.js @@ -14,12 +14,15 @@ export default Store({ showModal: false }) findJob, setError, openModal, - closeModal + closeModal, + handleForm } = cat.getActions('JobActions'); const register = createRegistrar(jobsStore); register(setter(setJobs)); - register(transformer(findJob)); register(setter(setError)); register(setter(openModal)); register(setter(closeModal)); + + register(transformer(findJob)); + register(handleForm); }); From 5258145ef601d607ef2f2a90e9f04b20a7b2bc8d Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Sep 2015 13:56:55 -0700 Subject: [PATCH 11/20] add validation to all current inputs validation right now is simply validating that the value is indeed a string --- common/app/routes/Jobs/components/NewJob.jsx | 41 +++++++++++++++++--- common/app/routes/Jobs/flux/Actions.js | 8 ++-- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 61bf46530a..b1b1ff30c3 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -19,7 +19,7 @@ function defaultValue(type) { return defaults[type]; } -function validatePosition(value) { +function validateString(value) { if (!value && typeof value !== 'string') { return false; } @@ -32,12 +32,12 @@ export default contain({ map({ form = {} }) { const { position = defaultValue('string'), - location = defaultValue('string'), + locale = defaultValue('string'), description = defaultValue('string') } = form; return { position, - location, + locale, description }; } @@ -48,14 +48,16 @@ export default contain({ propTypes: { jobActions: PropTypes.object, position: PropTypes.object, - location: PropTypes.object, + locale: PropTypes.object, description: PropTypes.object }, render() { const { jobActions, - position + position, + locale, + description } = this.props; return ( @@ -77,7 +79,7 @@ export default contain({ jobActions.handleForm({ name: 'position', value, - validator: validatePosition + validator: validateString }); }} placeholder='Position' @@ -85,16 +87,43 @@ export default contain({ value={ position.value } wrapperClassName='col-xs-10' /> { + jobActions.handleForm({ + name: 'locale', + value, + validator: validateString + }); + }} placeholder='Location' type='text' + value={ locale.value } wrapperClassName='col-xs-10' /> { + jobActions.handleForm({ + name: 'description', + value, + validator: validateString + }); + }} placeholder='Description' + rows='10' type='textarea' + value={ description.value } wrapperClassName='col-xs-10' /> diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index 664d04caf3..ccaab9c755 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -47,11 +47,11 @@ export default Actions({ if (!validator(value)) { return { transform(oldState) { - const { oldForm } = oldState; + const { form } = oldState; const newState = assign({}, oldState); newState.form = assign( {}, - oldForm, + form, { [name]: { value, valid: false, pristine: false }} ); return newState; @@ -60,11 +60,11 @@ export default Actions({ } return { transform(oldState) { - const { oldForm } = oldState; + const { form } = oldState; const newState = assign({}, oldState); newState.form = assign( {}, - oldForm, + form, { [name]: { value, valid: true, pristine: false }} ); return newState; From 98af05256a7c838461394cbd65c410a0b3cc176e Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Sep 2015 16:10:12 -0700 Subject: [PATCH 12/20] switch to validator add email field --- common/app/routes/Jobs/components/NewJob.jsx | 64 +++++++++++--------- common/app/routes/Jobs/flux/Actions.js | 14 ++++- 2 files changed, 46 insertions(+), 32 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index b1b1ff30c3..83f773e05d 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -6,6 +6,10 @@ import { Row, Well } from 'react-bootstrap'; +import { + isAscii, + isEmail +} from 'validator'; const defaults = { 'string': { @@ -19,13 +23,6 @@ function defaultValue(type) { return defaults[type]; } -function validateString(value) { - if (!value && typeof value !== 'string') { - return false; - } - return true; -} - export default contain({ actions: 'jobActions', store: 'jobsStore', @@ -33,12 +30,14 @@ export default contain({ const { position = defaultValue('string'), locale = defaultValue('string'), - description = defaultValue('string') + description = defaultValue('string'), + email = defaultValue('string') } = form; return { position, locale, - description + description, + email }; } }, @@ -49,7 +48,8 @@ export default contain({ jobActions: PropTypes.object, position: PropTypes.object, locale: PropTypes.object, - description: PropTypes.object + description: PropTypes.object, + email: PropTypes.object }, render() { @@ -57,7 +57,8 @@ export default contain({ jobActions, position, locale, - description + description, + email } = this.props; return ( @@ -65,21 +66,17 @@ export default contain({ -

Create You Job Post

+

Create Your Job Post

{ jobActions.handleForm({ name: 'position', value, - validator: validateString + validator: isAscii }); }} placeholder='Position' @@ -87,18 +84,14 @@ export default contain({ value={ position.value } wrapperClassName='col-xs-10' /> { jobActions.handleForm({ name: 'locale', value, - validator: validateString + validator: isAscii }); }} placeholder='Location' @@ -106,18 +99,14 @@ export default contain({ value={ locale.value } wrapperClassName='col-xs-10' /> { jobActions.handleForm({ name: 'description', value, - validator: validateString + validator: isAscii }); }} placeholder='Description' @@ -125,6 +114,21 @@ export default contain({ type='textarea' value={ description.value } wrapperClassName='col-xs-10' /> + { + jobActions.handleForm({ + name: 'email', + value, + validator: isEmail + }); + }} + placeholder='Email' + type='email' + value={ email.value } + wrapperClassName='col-xs-10' />
diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index ccaab9c755..5b284e4b8c 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -52,7 +52,12 @@ export default Actions({ newState.form = assign( {}, form, - { [name]: { value, valid: false, pristine: false }} + { [name]: { + value, + valid: false, + pristine: false, + bsStyle: value ? 'error' : null + }} ); return newState; } @@ -65,7 +70,12 @@ export default Actions({ newState.form = assign( {}, form, - { [name]: { value, valid: true, pristine: false }} + { [name]: { + value, + valid: true, + pristine: false, + bsStyle: value ? 'success' : null + }} ); return newState; } From 70b823ca63b7a3d377af7c0dca9800177020b5c4 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Sep 2015 17:19:14 -0700 Subject: [PATCH 13/20] add phone number input change validation function scheme update validator --- common/app/routes/Jobs/components/NewJob.jsx | 168 +++++++++++-------- common/models/job.json | 1 + package.json | 2 +- 3 files changed, 103 insertions(+), 68 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 83f773e05d..ae1e5eab51 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -8,7 +8,8 @@ import { } from 'react-bootstrap'; import { isAscii, - isEmail + isEmail, + isMobilePhone } from 'validator'; const defaults = { @@ -31,13 +32,15 @@ export default contain({ position = defaultValue('string'), locale = defaultValue('string'), description = defaultValue('string'), - email = defaultValue('string') + email = defaultValue('string'), + phone = defaultValue('string') } = form; return { position, locale, description, - email + email, + phone }; } }, @@ -49,17 +52,25 @@ export default contain({ position: PropTypes.object, locale: PropTypes.object, description: PropTypes.object, - email: PropTypes.object + email: PropTypes.object, + phone: PropTypes.object + }, + + handleChange(name, validator, { target: { value } }) { + const { jobActions: { handleForm } } = this.props; + handleForm({ name, value, validator }); }, render() { const { - jobActions, position, locale, description, - email + email, + phone } = this.props; + const labelClass = 'col-sm-offset-1 col-sm-2'; + const inputClass = 'col-sm-6'; return (
@@ -68,67 +79,90 @@ export default contain({

Create Your Job Post

- { - jobActions.handleForm({ - name: 'position', - value, - validator: isAscii - }); - }} - placeholder='Position' - type='text' - value={ position.value } - wrapperClassName='col-xs-10' /> - { - jobActions.handleForm({ - name: 'locale', - value, - validator: isAscii - }); - }} - placeholder='Location' - type='text' - value={ locale.value } - wrapperClassName='col-xs-10' /> - { - jobActions.handleForm({ - name: 'description', - value, - validator: isAscii - }); - }} - placeholder='Description' - rows='10' - type='textarea' - value={ description.value } - wrapperClassName='col-xs-10' /> - { - jobActions.handleForm({ - name: 'email', - value, - validator: isEmail - }); - }} - placeholder='Email' - type='email' - value={ email.value } - wrapperClassName='col-xs-10' /> + +
+

Job Information

+
+ { + this.handleChange( + 'position', + isAscii, + e + ); + }} + placeholder='Position' + type='text' + value={ position.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'locale', + isAscii, + e, + ); + }} + placeholder='Location' + type='text' + value={ locale.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'description', + isAscii, + e + ); + }} + placeholder='Description' + rows='10' + type='textarea' + value={ description.value } + wrapperClassName={ inputClass } /> +
+

Company Information

+
+ { + this.handleChange( + 'email', + isEmail, + e + ); + }} + placeholder='Email' + type='email' + value={ email.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'phone', + (data) => isMobilePhone(data, 'en-US'), + e + ); + }} + placeholder='555-123-1234' + type='tel' + value={ phone.value } + wrapperClassName={ inputClass } /> +
diff --git a/common/models/job.json b/common/models/job.json index a3392fee82..197f0619bf 100644 --- a/common/models/job.json +++ b/common/models/job.json @@ -1,6 +1,7 @@ { "name": "job", "base": "PersistedModel", + "strict": true, "idInjection": true, "trackChanges": false, "properties": { diff --git a/package.json b/package.json index 16c58aae82..df579eb2bc 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "thundercats-react": "^0.1.0", "twit": "~1.1.20", "uglify-js": "~2.4.15", - "validator": "~3.22.1", + "validator": "^3.22.1", "webpack": "^1.9.12", "yui": "~3.18.1" }, From 2ee22340503511d6671ac5c5c837f13f768a1e1f Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Sep 2015 17:26:53 -0700 Subject: [PATCH 14/20] add company URL --- common/app/routes/Jobs/components/NewJob.jsx | 30 ++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index ae1e5eab51..fd5e2aafa9 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -9,7 +9,8 @@ import { import { isAscii, isEmail, - isMobilePhone + isMobilePhone, + isURL } from 'validator'; const defaults = { @@ -33,14 +34,16 @@ export default contain({ locale = defaultValue('string'), description = defaultValue('string'), email = defaultValue('string'), - phone = defaultValue('string') + phone = defaultValue('string'), + url = defaultValue('string') } = form; return { position, locale, description, email, - phone + phone, + url }; } }, @@ -53,7 +56,8 @@ export default contain({ locale: PropTypes.object, description: PropTypes.object, email: PropTypes.object, - phone: PropTypes.object + phone: PropTypes.object, + url: PropTypes.object }, handleChange(name, validator, { target: { value } }) { @@ -67,7 +71,8 @@ export default contain({ locale, description, email, - phone + phone, + url } = this.props; const labelClass = 'col-sm-offset-1 col-sm-2'; const inputClass = 'col-sm-6'; @@ -162,6 +167,21 @@ export default contain({ type='tel' value={ phone.value } wrapperClassName={ inputClass } /> + { + this.handleChange( + 'url', + (data) => isURL(data, { 'require_protocol': true }), + e + ); + }} + placeholder='http://freecatphotoapp.com' + type='url' + value={ url.value } + wrapperClassName={ inputClass } /> From 01a40500591539c75aaa5070354725efee5e0809 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Tue, 22 Sep 2015 18:25:09 -0700 Subject: [PATCH 15/20] Add higlight, company name --- common/app/routes/Jobs/components/NewJob.jsx | 86 ++++++++++++++++---- 1 file changed, 72 insertions(+), 14 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index fd5e2aafa9..f76113959c 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -18,24 +18,26 @@ const defaults = { value: '', valid: false, pristine: true + }, + bool: { + value: false } }; -function defaultValue(type) { - return defaults[type]; -} - export default contain({ actions: 'jobActions', store: 'jobsStore', map({ form = {} }) { const { - position = defaultValue('string'), - locale = defaultValue('string'), - description = defaultValue('string'), - email = defaultValue('string'), - phone = defaultValue('string'), - url = defaultValue('string') + position = defaults['string'], + locale = defaults['string'], + description = defaults['string'], + email = defaults['string'], + phone = defaults['string'], + url = defaults['string'], + logo = defaults['string'], + name = defaults['string'], + highlight = defaults['bool'] } = form; return { position, @@ -43,7 +45,10 @@ export default contain({ description, email, phone, - url + url, + logo, + name, + highlight }; } }, @@ -57,7 +62,10 @@ export default contain({ description: PropTypes.object, email: PropTypes.object, phone: PropTypes.object, - url: PropTypes.object + url: PropTypes.object, + logo: PropTypes.object, + name: PropTypes.object, + highlight: PropTypes.object }, handleChange(name, validator, { target: { value } }) { @@ -72,7 +80,10 @@ export default contain({ description, email, phone, - url + url, + logo, + name, + highlight } = this.props; const labelClass = 'col-sm-offset-1 col-sm-2'; const inputClass = 'col-sm-6'; @@ -134,9 +145,25 @@ export default contain({ type='textarea' value={ description.value } wrapperClassName={ inputClass } /> +

Company Information

+ { + this.handleChange( + 'name', + isAscii, + e, + ); + }} + placeholder='Foo, INC' + type='text' + value={ name.value } + wrapperClassName={ inputClass } /> { this.handleChange( @@ -182,6 +209,37 @@ export default contain({ type='url' value={ url.value } wrapperClassName={ inputClass } /> + { + this.handleChange( + 'logo', + (data) => isURL(data, { 'require_protocol': true }), + e + ); + }} + placeholder='http://freecatphotoapp.com/logo.png' + type='url' + value={ logo.value } + wrapperClassName={ inputClass } /> + +
+

Make it stand out

+
+ { + this.handleChange( + 'highlight', + () => { return true; }, + e + ); + }} + type='checkbox' + value={ highlight.value } /> From d8a6373b1ec46545d7f2435be64ecb4aeddf525a Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Wed, 23 Sep 2015 13:31:27 -0700 Subject: [PATCH 16/20] add submit button --- common/app/routes/Jobs/components/NewJob.jsx | 301 ++++++++++--------- 1 file changed, 157 insertions(+), 144 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index f76113959c..41d20db840 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -1,6 +1,7 @@ import React, { PropTypes } from 'react'; import { contain } from 'thundercats-react'; import { + Button, Col, Input, Row, @@ -95,151 +96,163 @@ export default contain({

Create Your Job Post

+ +
+

Job Information

+
+ { + this.handleChange( + 'position', + isAscii, + e + ); + }} + placeholder='Position' + type='text' + value={ position.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'locale', + isAscii, + e, + ); + }} + placeholder='Location' + type='text' + value={ locale.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'description', + isAscii, + e + ); + }} + placeholder='Description' + rows='10' + type='textarea' + value={ description.value } + wrapperClassName={ inputClass } /> + +
+

Company Information

+
+ { + this.handleChange( + 'name', + isAscii, + e, + ); + }} + placeholder='Foo, INC' + type='text' + value={ name.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'email', + isEmail, + e + ); + }} + placeholder='Email' + type='email' + value={ email.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'phone', + (data) => isMobilePhone(data, 'en-US'), + e + ); + }} + placeholder='555-123-1234' + type='tel' + value={ phone.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'url', + (data) => isURL(data, { 'require_protocol': true }), + e + ); + }} + placeholder='http://freecatphotoapp.com' + type='url' + value={ url.value } + wrapperClassName={ inputClass } /> + { + this.handleChange( + 'logo', + (data) => isURL(data, { 'require_protocol': true }), + e + ); + }} + placeholder='http://freecatphotoapp.com/logo.png' + type='url' + value={ logo.value } + wrapperClassName={ inputClass } /> + +
+

Make it stand out

+
+ { + this.handleChange( + 'highlight', + () => { return true; }, + e + ); + }} + type='checkbox' + value={ highlight.value } /> +
-
-

Job Information

-
- { - this.handleChange( - 'position', - isAscii, - e - ); - }} - placeholder='Position' - type='text' - value={ position.value } - wrapperClassName={ inputClass } /> - { - this.handleChange( - 'locale', - isAscii, - e, - ); - }} - placeholder='Location' - type='text' - value={ locale.value } - wrapperClassName={ inputClass } /> - { - this.handleChange( - 'description', - isAscii, - e - ); - }} - placeholder='Description' - rows='10' - type='textarea' - value={ description.value } - wrapperClassName={ inputClass } /> - -
-

Company Information

-
- { - this.handleChange( - 'name', - isAscii, - e, - ); - }} - placeholder='Foo, INC' - type='text' - value={ name.value } - wrapperClassName={ inputClass } /> - { - this.handleChange( - 'email', - isEmail, - e - ); - }} - placeholder='Email' - type='email' - value={ email.value } - wrapperClassName={ inputClass } /> - { - this.handleChange( - 'phone', - (data) => isMobilePhone(data, 'en-US'), - e - ); - }} - placeholder='555-123-1234' - type='tel' - value={ phone.value } - wrapperClassName={ inputClass } /> - { - this.handleChange( - 'url', - (data) => isURL(data, { 'require_protocol': true }), - e - ); - }} - placeholder='http://freecatphotoapp.com' - type='url' - value={ url.value } - wrapperClassName={ inputClass } /> - { - this.handleChange( - 'logo', - (data) => isURL(data, { 'require_protocol': true }), - e - ); - }} - placeholder='http://freecatphotoapp.com/logo.png' - type='url' - value={ logo.value } - wrapperClassName={ inputClass } /> - -
-

Make it stand out

-
- { - this.handleChange( - 'highlight', - () => { return true; }, - e - ); - }} - type='checkbox' - value={ highlight.value } /> + + +
From 8148c1a19cedbd1cae09f11d799576f1e099be6f Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 24 Sep 2015 20:28:04 -0700 Subject: [PATCH 17/20] save form to localStorage --- common/app/routes/Jobs/components/NewJob.jsx | 121 +++++++++++++++---- common/app/routes/Jobs/flux/Actions.js | 52 ++++++-- common/app/routes/Jobs/flux/Store.js | 4 +- common/app/routes/Jobs/utils.js | 22 ++++ common/models/job.json | 5 +- package.json | 2 + 6 files changed, 169 insertions(+), 37 deletions(-) create mode 100644 common/app/routes/Jobs/utils.js diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 41d20db840..b160671c4b 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -1,5 +1,13 @@ import React, { PropTypes } from 'react'; import { contain } from 'thundercats-react'; +import debugFactory from 'debug'; +import { getDefaults } from '../utils'; + +import { + inHTMLData, + uriInSingleQuotedAttr +} from 'xss-filters'; + import { Button, Col, @@ -7,6 +15,7 @@ import { Row, Well } from 'react-bootstrap'; + import { isAscii, isEmail, @@ -14,31 +23,34 @@ import { isURL } from 'validator'; -const defaults = { - 'string': { - value: '', - valid: false, - pristine: true - }, - bool: { - value: false - } -}; +const debug = debugFactory('freecc:jobs:newForm'); + +const checkValidity = [ + 'position', + 'locale', + 'description', + 'email', + 'phone', + 'url', + 'logo', + 'name', + 'highlight' +]; export default contain({ actions: 'jobActions', store: 'jobsStore', map({ form = {} }) { const { - position = defaults['string'], - locale = defaults['string'], - description = defaults['string'], - email = defaults['string'], - phone = defaults['string'], - url = defaults['string'], - logo = defaults['string'], - name = defaults['string'], - highlight = defaults['bool'] + position = getDefaults('string'), + locale = getDefaults('string'), + description = getDefaults('string'), + email = getDefaults('string'), + phone = getDefaults('string'), + url = getDefaults('string'), + logo = getDefaults('string'), + name = getDefaults('string'), + highlight = getDefaults('bool') } = form; return { position, @@ -51,6 +63,9 @@ export default contain({ name, highlight }; + }, + subscribeOnWillMount() { + return typeof window !== 'undefined'; } }, React.createClass({ @@ -69,6 +84,63 @@ export default contain({ highlight: PropTypes.object }, + handleSubmit(e) { + e.preventDefault(); + let valid = true; + checkValidity.forEach((prop) => { + // if value exist, check if it is valid + if (this.props[prop].value) { + valid = valid && !!this.props[prop].valid; + } + }); + + if (!valid) { + debug('form not valid'); + return; + } + + const { + position, + locale, + description, + email, + phone, + url, + logo, + name, + highlight, + jobActions + } = this.props; + + // sanitize user output + const jobValues = { + position: inHTMLData(position.value), + location: inHTMLData(locale.value), + description: inHTMLData(description.value), + email: inHTMLData(email.value), + phone: inHTMLData(phone.value), + url: uriInSingleQuotedAttr(url.value), + logo: uriInSingleQuotedAttr(logo.value), + name: inHTMLData(name.value), + highlight: !!highlight.value + }; + + const job = Object.keys(jobValues).reduce((accu, prop) => { + if (jobValues[prop]) { + accu[prop] = jobValues[prop]; + } + return accu; + }, {}); + + debug('job sanitized', job); + jobActions.saveForm(job); + }, + + componentDidMount() { + const { jobActions } = this.props; + jobActions.getSavedForm(); + }, + handleChange(name, validator, { target: { value } }) { const { jobActions: { handleForm } } = this.props; handleForm({ name, value, validator }); @@ -95,7 +167,9 @@ export default contain({

Create Your Job Post

-
+

Job Information

@@ -151,7 +225,7 @@ export default contain({

Company Information

{ @@ -248,8 +322,9 @@ export default contain({ lgOffset={ 3 }> diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index 5b284e4b8c..771c520013 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -1,4 +1,6 @@ import { Actions } from 'thundercats'; +import store from 'store'; +import { getDefaults } from '../utils'; import debugFactory from 'debug'; const debug = debugFactory('freecc:jobs:actions'); @@ -52,12 +54,14 @@ export default Actions({ newState.form = assign( {}, form, - { [name]: { - value, - valid: false, - pristine: false, - bsStyle: value ? 'error' : null - }} + { + [name]: { + value, + valid: false, + pristine: false, + bsStyle: value ? 'error' : null + } + } ); return newState; } @@ -70,16 +74,31 @@ export default Actions({ newState.form = assign( {}, form, - { [name]: { - value, - valid: true, - pristine: false, - bsStyle: value ? 'success' : null - }} + { + [name]: { + value, + valid: true, + pristine: false, + bsStyle: value ? 'success' : null + } + } ); return newState; } }; + }, + saveForm: null, + getSavedForm: null, + setForm(job) { + const form = Object.keys(job).reduce((accu, prop) => { + console.log('form', accu); + return Object.assign( + accu, + { [prop]: getDefaults(typeof prop, job[prop]) } + ); + }, {}); + + return { form }; } }) .refs({ displayName: 'JobActions' }) @@ -111,5 +130,14 @@ export default Actions({ jobActions.setJobs({}); }); }); + + jobActions.saveForm.subscribe((form) => { + store.set('newJob', form); + }); + + jobActions.getSavedForm.subscribe(() => { + const job = store.get('newJob'); + jobActions.setForm(job); + }); return jobActions; }); diff --git a/common/app/routes/Jobs/flux/Store.js b/common/app/routes/Jobs/flux/Store.js index a73235f1aa..b2f5132013 100644 --- a/common/app/routes/Jobs/flux/Store.js +++ b/common/app/routes/Jobs/flux/Store.js @@ -15,13 +15,15 @@ export default Store({ showModal: false }) setError, openModal, closeModal, - handleForm + handleForm, + setForm } = cat.getActions('JobActions'); const register = createRegistrar(jobsStore); register(setter(setJobs)); register(setter(setError)); register(setter(openModal)); register(setter(closeModal)); + register(setter(setForm)); register(transformer(findJob)); register(handleForm); diff --git a/common/app/routes/Jobs/utils.js b/common/app/routes/Jobs/utils.js new file mode 100644 index 0000000000..3a60c373a8 --- /dev/null +++ b/common/app/routes/Jobs/utils.js @@ -0,0 +1,22 @@ +const defaults = { + 'string': { + value: '', + valid: false, + pristine: true, + type: 'string' + }, + bool: { + value: false, + type: 'boolean' + } +}; + +export function getDefaults(type, value) { + if (!type) { + return defaults['string']; + } + if (value) { + return Object.assign({}, defaults[type], { value }); + } + return defaults[type]; +} diff --git a/common/models/job.json b/common/models/job.json index 197f0619bf..f77fc6defa 100644 --- a/common/models/job.json +++ b/common/models/job.json @@ -30,6 +30,9 @@ "state": { "type": "string" }, + "url": { + "type": "string" + }, "country": { "type": "string" }, @@ -39,7 +42,7 @@ "description": { "type": "string" }, - "isApproverd": { + "isApproved": { "type": "boolean" }, "isHighlighted": { diff --git a/package.json b/package.json index df579eb2bc..f16e4bb9db 100644 --- a/package.json +++ b/package.json @@ -97,12 +97,14 @@ "sanitize-html": "~1.6.1", "sort-keys": "^1.1.1", "source-map-support": "^0.3.2", + "store": "https://github.com/berkeleytrue/store.js.git#feature/noop-server", "thundercats": "^2.1.0", "thundercats-react": "^0.1.0", "twit": "~1.1.20", "uglify-js": "~2.4.15", "validator": "^3.22.1", "webpack": "^1.9.12", + "xss-filters": "^1.2.6", "yui": "~3.18.1" }, "devDependencies": { From c63a983fb93a5352b82eedeaf5e9c4e51379d064 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 25 Sep 2015 00:04:38 -0700 Subject: [PATCH 18/20] filter output from localStorage --- common/app/routes/Jobs/flux/Actions.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/app/routes/Jobs/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js index 771c520013..15229e039d 100644 --- a/common/app/routes/Jobs/flux/Actions.js +++ b/common/app/routes/Jobs/flux/Actions.js @@ -137,7 +137,9 @@ export default Actions({ jobActions.getSavedForm.subscribe(() => { const job = store.get('newJob'); - jobActions.setForm(job); + if (job && !Array.isArray(job) && typeof job === 'object') { + jobActions.setForm(job); + } }); return jobActions; }); From 891341532b95953d028edf7250c1798c973903da Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Fri, 25 Sep 2015 12:53:29 -0700 Subject: [PATCH 19/20] refactor form to do validation right in component --- common/app/routes/Jobs/components/NewJob.jsx | 135 ++++++++----------- common/app/routes/Jobs/flux/Actions.js | 47 +------ common/app/routes/Jobs/utils.js | 2 +- 3 files changed, 58 insertions(+), 126 deletions(-) diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index b160671c4b..12f9b425a0 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -37,22 +37,31 @@ const checkValidity = [ 'highlight' ]; +function formatValue(value, validator, type = 'string') { + const formated = getDefaults(type); + if (validator && type === 'string') { + formated.valid = validator(value); + } + if (value) { + formated.value = value; + formated.bsStyle = formated.valid ? 'success' : 'error'; + } + return formated; +} + +function isValidURL(data) { + return isURL(data, { 'require_protocol': true }); +} + +function isValidPhone(data) { + return isMobilePhone(data, 'en-US'); +} + export default contain({ actions: 'jobActions', store: 'jobsStore', map({ form = {} }) { const { - position = getDefaults('string'), - locale = getDefaults('string'), - description = getDefaults('string'), - email = getDefaults('string'), - phone = getDefaults('string'), - url = getDefaults('string'), - logo = getDefaults('string'), - name = getDefaults('string'), - highlight = getDefaults('bool') - } = form; - return { position, locale, description, @@ -62,6 +71,17 @@ export default contain({ logo, name, highlight + } = form; + return { + position: formatValue(position, isAscii), + locale: formatValue(locale, isAscii), + description: formatValue(description, isAscii), + email: formatValue(email, isEmail), + phone: formatValue(phone, isValidPhone), + url: formatValue(url, isValidURL), + logo: formatValue(logo, isValidURL), + name: formatValue(name, isAscii), + highlight: formatValue(highlight, null, 'bool') }; }, subscribeOnWillMount() { @@ -86,11 +106,12 @@ export default contain({ handleSubmit(e) { e.preventDefault(); + const props = this.props; let valid = true; checkValidity.forEach((prop) => { // if value exist, check if it is valid - if (this.props[prop].value) { - valid = valid && !!this.props[prop].valid; + if (props[prop].value && props[prop].type !== 'boolean') { + valid = valid && !!props[prop].valid; } }); @@ -141,9 +162,9 @@ export default contain({ jobActions.getSavedForm(); }, - handleChange(name, validator, { target: { value } }) { + handleChange(name, { target: { value } }) { const { jobActions: { handleForm } } = this.props; - handleForm({ name, value, validator }); + handleForm({ [name]: value }); }, render() { @@ -156,8 +177,10 @@ export default contain({ url, logo, name, - highlight + highlight, + jobActions: { handleForm } } = this.props; + const { handleChange } = this; const labelClass = 'col-sm-offset-1 col-sm-2'; const inputClass = 'col-sm-6'; @@ -178,13 +201,7 @@ export default contain({ bsStyle={ position.bsStyle } label='Position' labelClassName={ labelClass } - onChange={ (e) => { - this.handleChange( - 'position', - isAscii, - e - ); - }} + onChange={ (e) => handleChange('position', e) } placeholder='Position' type='text' value={ position.value } @@ -193,13 +210,7 @@ export default contain({ bsStyle={ locale.bsStyle } label='Location' labelClassName={ labelClass } - onChange={ (e) => { - this.handleChange( - 'locale', - isAscii, - e, - ); - }} + onChange={ (e) => handleChange('locale', e) } placeholder='Location' type='text' value={ locale.value } @@ -208,13 +219,7 @@ export default contain({ bsStyle={ description.bsStyle } label='Description' labelClassName={ labelClass } - onChange={ (e) => { - this.handleChange( - 'description', - isAscii, - e - ); - }} + onChange={ (e) => handleChange('description', e) } placeholder='Description' rows='10' type='textarea' @@ -228,13 +233,7 @@ export default contain({ bsStyle={ name.bsStyle } label='Company Name' labelClassName={ labelClass } - onChange={ (e) => { - this.handleChange( - 'name', - isAscii, - e, - ); - }} + onChange={ (e) => handleChange('name', e) } placeholder='Foo, INC' type='text' value={ name.value } @@ -243,13 +242,7 @@ export default contain({ bsStyle={ email.bsStyle } label='Email' labelClassName={ labelClass } - onChange={ (e) => { - this.handleChange( - 'email', - isEmail, - e - ); - }} + onChange={ (e) => handleChange('email', e) } placeholder='Email' type='email' value={ email.value } @@ -258,13 +251,7 @@ export default contain({ bsStyle={ phone.bsStyle } label='Phone' labelClassName={ labelClass } - onChange={ (e) => { - this.handleChange( - 'phone', - (data) => isMobilePhone(data, 'en-US'), - e - ); - }} + onChange={ (e) => handleChange('phone', e) } placeholder='555-123-1234' type='tel' value={ phone.value } @@ -273,13 +260,7 @@ export default contain({ bsStyle={ url.bsStyle } label='URL' labelClassName={ labelClass } - onChange={ (e) => { - this.handleChange( - 'url', - (data) => isURL(data, { 'require_protocol': true }), - e - ); - }} + onChange={ (e) => handleChange('url', e) } placeholder='http://freecatphotoapp.com' type='url' value={ url.value } @@ -288,13 +269,7 @@ export default contain({ bsStyle={ logo.bsStyle } label='Logo' labelClassName={ labelClass } - onChange={ (e) => { - this.handleChange( - 'logo', - (data) => isURL(data, { 'require_protocol': true }), - e - ); - }} + onChange={ (e) => handleChange('logo', e) } placeholder='http://freecatphotoapp.com/logo.png' type='url' value={ logo.value } @@ -304,17 +279,15 @@ export default contain({

Make it stand out

{ - this.handleChange( - 'highlight', - () => { return true; }, - e - ); - }} - type='checkbox' - value={ highlight.value } /> + onChange={ + ({ target: { checked } }) => handleForm({ + highlight: !!checked + }) + } + type='checkbox' />
{} }) { - if (!name) { - // operation noop - return { replace: null }; - } - if (!validator(value)) { - return { - transform(oldState) { - const { form } = oldState; - const newState = assign({}, oldState); - newState.form = assign( - {}, - form, - { - [name]: { - value, - valid: false, - pristine: false, - bsStyle: value ? 'error' : null - } - } - ); - return newState; - } - }; - } + handleForm(value) { return { transform(oldState) { const { form } = oldState; @@ -74,14 +48,7 @@ export default Actions({ newState.form = assign( {}, form, - { - [name]: { - value, - valid: true, - pristine: false, - bsStyle: value ? 'success' : null - } - } + value ); return newState; } @@ -89,15 +56,7 @@ export default Actions({ }, saveForm: null, getSavedForm: null, - setForm(job) { - const form = Object.keys(job).reduce((accu, prop) => { - console.log('form', accu); - return Object.assign( - accu, - { [prop]: getDefaults(typeof prop, job[prop]) } - ); - }, {}); - + setForm(form) { return { form }; } }) diff --git a/common/app/routes/Jobs/utils.js b/common/app/routes/Jobs/utils.js index 3a60c373a8..aeb0396c12 100644 --- a/common/app/routes/Jobs/utils.js +++ b/common/app/routes/Jobs/utils.js @@ -18,5 +18,5 @@ export function getDefaults(type, value) { if (value) { return Object.assign({}, defaults[type], { value }); } - return defaults[type]; + return Object.assign({}, defaults[type]); } From 90f6d986d783a9d5b6931dff89d1ef846b1766b4 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Sat, 26 Sep 2015 22:23:56 -0700 Subject: [PATCH 20/20] show preview from new job --- common/app/routes/Jobs/components/NewJob.jsx | 6 ++ common/app/routes/Jobs/components/Preview.jsx | 14 ++++ common/app/routes/Jobs/components/Show.jsx | 68 +------------------ common/app/routes/Jobs/components/ShowJob.jsx | 67 ++++++++++++++++++ common/app/routes/Jobs/index.js | 4 ++ 5 files changed, 93 insertions(+), 66 deletions(-) create mode 100644 common/app/routes/Jobs/components/Preview.jsx create mode 100644 common/app/routes/Jobs/components/ShowJob.jsx diff --git a/common/app/routes/Jobs/components/NewJob.jsx b/common/app/routes/Jobs/components/NewJob.jsx index 12f9b425a0..4c369411f0 100644 --- a/common/app/routes/Jobs/components/NewJob.jsx +++ b/common/app/routes/Jobs/components/NewJob.jsx @@ -1,4 +1,5 @@ import React, { PropTypes } from 'react'; +import { History } from 'react-router'; import { contain } from 'thundercats-react'; import debugFactory from 'debug'; import { getDefaults } from '../utils'; @@ -104,6 +105,8 @@ export default contain({ highlight: PropTypes.object }, + mixins: [History], + handleSubmit(e) { e.preventDefault(); const props = this.props; @@ -153,8 +156,11 @@ export default contain({ return accu; }, {}); + job.postedOn = new Date(); debug('job sanitized', job); jobActions.saveForm(job); + + this.history.pushState(null, '/jobs/new/preview'); }, componentDidMount() { diff --git a/common/app/routes/Jobs/components/Preview.jsx b/common/app/routes/Jobs/components/Preview.jsx new file mode 100644 index 0000000000..5b6081be5c --- /dev/null +++ b/common/app/routes/Jobs/components/Preview.jsx @@ -0,0 +1,14 @@ +// import React, { PropTypes } from 'react'; +import { contain } from 'thundercats-react'; +import ShowJob from './ShowJob.jsx'; + +export default contain( + { + store: 'JobsStore', + actions: 'JobActions', + map({ form: job = {} }) { + return { job }; + } + }, + ShowJob +); diff --git a/common/app/routes/Jobs/components/Show.jsx b/common/app/routes/Jobs/components/Show.jsx index 0baedb82b3..ce2512c27d 100644 --- a/common/app/routes/Jobs/components/Show.jsx +++ b/common/app/routes/Jobs/components/Show.jsx @@ -1,13 +1,5 @@ -import React, { PropTypes } from 'react'; import { contain } from 'thundercats-react'; -import { Row, Thumbnail, Panel, Well } from 'react-bootstrap'; -import moment from 'moment'; - -const thumbnailStyle = { - backgroundColor: 'white', - maxHeight: '100px', - maxWidth: '100px' -}; +import ShowJob from './ShowJob.jsx'; export default contain( { @@ -28,61 +20,5 @@ export default contain( return job.id !== id; } }, - React.createClass({ - displayName: 'ShowJob', - propTypes: { - job: PropTypes.object, - params: PropTypes.object - }, - - renderHeader({ company, position }) { - return ( -
-

{ company }

-
- { position } -
-
- ); - }, - - render() { - const { job = {} } = this.props; - const { - logo, - position, - city, - company, - state, - email, - phone, - postedOn, - description - } = job; - - return ( -
- - - - - Position: { position } - Location: { city }, { state } -
- Contact: { email || phone || 'N/A' } -
- Posted On: { moment(postedOn).format('MMMM Do, YYYY') } -
-

{ description }

-
-
-
- ); - } - }) + ShowJob ); diff --git a/common/app/routes/Jobs/components/ShowJob.jsx b/common/app/routes/Jobs/components/ShowJob.jsx new file mode 100644 index 0000000000..1a048a3fff --- /dev/null +++ b/common/app/routes/Jobs/components/ShowJob.jsx @@ -0,0 +1,67 @@ +import React, { PropTypes } from 'react'; +import { Row, Thumbnail, Panel, Well } from 'react-bootstrap'; +import moment from 'moment'; + +const thumbnailStyle = { + backgroundColor: 'white', + maxHeight: '100px', + maxWidth: '100px' +}; + +export default React.createClass({ + displayName: 'ShowJob', + propTypes: { + job: PropTypes.object, + params: PropTypes.object + }, + + renderHeader({ company, position }) { + return ( +
+

{ company }

+
+ { position } +
+
+ ); + }, + + render() { + const { job = {} } = this.props; + const { + logo, + position, + city, + company, + state, + email, + phone, + postedOn, + description + } = job; + + return ( +
+ + + + + Position: { position } + Location: { city }, { state } +
+ Contact: { email || phone || 'N/A' } +
+ Posted On: { moment(postedOn).format('MMMM Do, YYYY') } +
+

{ description }

+
+
+
+ ); + } +}); diff --git a/common/app/routes/Jobs/index.js b/common/app/routes/Jobs/index.js index 3564332d32..6c556c994e 100644 --- a/common/app/routes/Jobs/index.js +++ b/common/app/routes/Jobs/index.js @@ -1,6 +1,7 @@ import Jobs from './components/Jobs.jsx'; import NewJob from './components/NewJob.jsx'; import Show from './components/Show.jsx'; +import Preview from './components/Preview.jsx'; /* * index: /jobs list jobs @@ -15,6 +16,9 @@ export default { }, { path: 'jobs/new', component: NewJob + }, { + path: 'jobs/new/preview', + component: Preview }, { path: 'jobs/:id', component: Show