diff --git a/client/index.js b/client/index.js
index 79c5c541c1..44bdb0a82c 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,29 @@ 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 }) => {
+ props.history = history;
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/CreateJobModal.jsx b/common/app/routes/Jobs/components/CreateJobModal.jsx
new file mode 100644
index 0000000000..446ed957d6
--- /dev/null
+++ b/common/app/routes/Jobs/components/CreateJobModal.jsx
@@ -0,0 +1,43 @@
+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(onHide) {
+ onHide();
+ this.history.pushState(null, '/jobs/new');
+ },
+
+ 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 a6bc6a9a9f..a4d8354b3f 100644
--- a/common/app/routes/Jobs/components/Jobs.jsx
+++ b/common/app/routes/Jobs/components/Jobs.jsx
@@ -1,7 +1,9 @@
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 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: [Navigation],
handleJobClick(id) {
const { jobActions } = this.props;
@@ -26,7 +30,7 @@ export default contain(
return null;
}
jobActions.findJob(id);
- this.transitionTo(`/jobs/${id}`);
+ this.history.pushState(null, `/jobs/${id}`);
},
renderList(handleJobClick, jobs) {
@@ -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/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 (
{
+ // if value exist, check if it is valid
+ if (props[prop].value && props[prop].type !== 'boolean') {
+ valid = valid && !!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;
+ }, {});
+
+ job.postedOn = new Date();
+ debug('job sanitized', job);
+ jobActions.saveForm(job);
+
+ this.history.pushState(null, '/jobs/new/preview');
+ },
+
+ componentDidMount() {
+ const { jobActions } = this.props;
+ jobActions.getSavedForm();
+ },
+
+ handleChange(name, { target: { value } }) {
+ const { jobActions: { handleForm } } = this.props;
+ handleForm({ [name]: value });
+ },
+
+ render() {
+ const {
+ position,
+ locale,
+ description,
+ email,
+ phone,
+ url,
+ logo,
+ name,
+ highlight,
+ jobActions: { handleForm }
+ } = this.props;
+ const { handleChange } = this;
+ const labelClass = 'col-sm-offset-1 col-sm-2';
+ const inputClass = 'col-sm-6';
+
+ return (
+
+
+
+
+ Create Your Job Post
+
+
+
+
+
+ );
+ }
+ })
+);
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/flux/Actions.js b/common/app/routes/Jobs/flux/Actions.js
index 4df2ae42c6..5900e6dda1 100644
--- a/common/app/routes/Jobs/flux/Actions.js
+++ b/common/app/routes/Jobs/flux/Actions.js
@@ -1,7 +1,9 @@
import { Actions } from 'thundercats';
+import store from 'store';
import debugFactory from 'debug';
const debug = debugFactory('freecc:jobs:actions');
+const assign = Object.assign;
export default Actions({
setJobs: null,
@@ -23,7 +25,7 @@ export default Actions({
// if no job found this will be null which is a op noop
return foundJob ?
- Object.assign({}, oldState, { currentJob: foundJob }) :
+ assign({}, oldState, { currentJob: foundJob }) :
null;
};
},
@@ -31,6 +33,31 @@ export default Actions({
getJob: null,
getJobs(params) {
return { params };
+ },
+ openModal() {
+ return { showModal: true };
+ },
+ closeModal() {
+ return { showModal: false };
+ },
+ handleForm(value) {
+ return {
+ transform(oldState) {
+ const { form } = oldState;
+ const newState = assign({}, oldState);
+ newState.form = assign(
+ {},
+ form,
+ value
+ );
+ return newState;
+ }
+ };
+ },
+ saveForm: null,
+ getSavedForm: null,
+ setForm(form) {
+ return { form };
}
})
.refs({ displayName: 'JobActions' })
@@ -56,8 +83,22 @@ 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({});
});
});
+
+ jobActions.saveForm.subscribe((form) => {
+ store.set('newJob', form);
+ });
+
+ jobActions.getSavedForm.subscribe(() => {
+ const job = store.get('newJob');
+ if (job && !Array.isArray(job) && typeof job === 'object') {
+ jobActions.setForm(job);
+ }
+ });
return jobActions;
});
diff --git a/common/app/routes/Jobs/flux/Store.js b/common/app/routes/Jobs/flux/Store.js
index 2fdfa50207..b2f5132013 100644
--- a/common/app/routes/Jobs/flux/Store.js
+++ b/common/app/routes/Jobs/flux/Store.js
@@ -6,12 +6,25 @@ 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,
+ handleForm,
+ setForm
+ } = cat.getActions('JobActions');
const register = createRegistrar(jobsStore);
register(setter(setJobs));
- register(transformer(findJob));
register(setter(setError));
+ register(setter(openModal));
+ register(setter(closeModal));
+ register(setter(setForm));
+
+ register(transformer(findJob));
+ register(handleForm);
});
diff --git a/common/app/routes/Jobs/index.js b/common/app/routes/Jobs/index.js
index ac6b07f866..6c556c994e 100644
--- a/common/app/routes/Jobs/index.js
+++ b/common/app/routes/Jobs/index.js
@@ -1,5 +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
@@ -11,6 +13,12 @@ export default {
childRoutes: [{
path: '/jobs',
component: Jobs
+ }, {
+ path: 'jobs/new',
+ component: NewJob
+ }, {
+ path: 'jobs/new/preview',
+ component: Preview
}, {
path: 'jobs/:id',
component: Show
diff --git a/common/app/routes/Jobs/utils.js b/common/app/routes/Jobs/utils.js
new file mode 100644
index 0000000000..aeb0396c12
--- /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 Object.assign({}, defaults[type]);
+}
diff --git a/common/app/routes/index.js b/common/app/routes/index.js
index 973e9f2bf0..1f14733f82 100644
--- a/common/app/routes/index.js
+++ b/common/app/routes/index.js
@@ -3,12 +3,8 @@ import Hikes from './Hikes';
export default {
path: '/',
- getChildRoutes(locationState, cb) {
- setTimeout(() => {
- cb(null, [
- Jobs,
- Hikes
- ]);
- }, 0);
- }
+ childRoutes: [
+ Jobs,
+ Hikes
+ ]
};
diff --git a/common/models/job.json b/common/models/job.json
index a3392fee82..f77fc6defa 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": {
@@ -29,6 +30,9 @@
"state": {
"type": "string"
},
+ "url": {
+ "type": "string"
+ },
"country": {
"type": "string"
},
@@ -38,7 +42,7 @@
"description": {
"type": "string"
},
- "isApproverd": {
+ "isApproved": {
"type": "boolean"
},
"isHighlighted": {
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'
],
diff --git a/package.json b/package.json
index 901b54ea3a..f16e4bb9db 100644
--- a/package.json
+++ b/package.json
@@ -58,10 +58,10 @@
"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",
- "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",
@@ -89,7 +89,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",
@@ -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",
+ "validator": "^3.22.1",
"webpack": "^1.9.12",
+ "xss-filters": "^1.2.6",
"yui": "~3.18.1"
},
"devDependencies": {
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