Merge pull request #3525 from FreeCodeCamp/feature/jobs
intermediate pull
This commit is contained in:
@ -4,7 +4,7 @@ import React from 'react';
|
|||||||
import Fetchr from 'fetchr';
|
import Fetchr from 'fetchr';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
import { Router } from 'react-router';
|
import { Router } from 'react-router';
|
||||||
import { history } from 'react-router/lib/BrowserHistory';
|
import { createLocation, createHistory } from 'history';
|
||||||
import { hydrate } from 'thundercats';
|
import { hydrate } from 'thundercats';
|
||||||
import { Render } from 'thundercats-react';
|
import { Render } from 'thundercats-react';
|
||||||
|
|
||||||
@ -18,21 +18,29 @@ const services = new Fetchr({
|
|||||||
});
|
});
|
||||||
|
|
||||||
Rx.config.longStackSupport = !!debug.enabled;
|
Rx.config.longStackSupport = !!debug.enabled;
|
||||||
|
const history = createHistory();
|
||||||
|
const appLocation = createLocation(
|
||||||
|
location.pathname + location.search
|
||||||
|
);
|
||||||
// returns an observable
|
// returns an observable
|
||||||
app$(history)
|
app$({ history, location: appLocation })
|
||||||
.flatMap(
|
.flatMap(
|
||||||
({ AppCat }) => {
|
({ AppCat }) => {
|
||||||
|
// instantiate the cat with service
|
||||||
const appCat = AppCat(null, services);
|
const appCat = AppCat(null, services);
|
||||||
|
// hydrate the stores
|
||||||
return hydrate(appCat, catState)
|
return hydrate(appCat, catState)
|
||||||
.map(() => appCat);
|
.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(
|
return Render(
|
||||||
appCat,
|
appCat,
|
||||||
React.createElement(Router, initialState),
|
React.createElement(Router, props),
|
||||||
DOMContianer
|
DOMContianer
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import Rx from 'rx';
|
import Rx from 'rx';
|
||||||
import { Router } from 'react-router';
|
import { match } from 'react-router';
|
||||||
import App from './App.jsx';
|
import App from './App.jsx';
|
||||||
import AppCat from './Cat';
|
import AppCat from './Cat';
|
||||||
|
|
||||||
import childRoutes from './routes';
|
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);
|
const routes = Object.assign({ components: App }, childRoutes);
|
||||||
|
|
||||||
export default function app$(location) {
|
export default function app$({ location, history }) {
|
||||||
return router$(routes, location)
|
return route$({ routes, location, history })
|
||||||
.map(([initialState, transistion]) => {
|
.map(([nextLocation, props]) => {
|
||||||
return { initialState, transistion, AppCat };
|
return { nextLocation, props, AppCat };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
43
common/app/routes/Jobs/components/CreateJobModal.jsx
Normal file
43
common/app/routes/Jobs/components/CreateJobModal.jsx
Normal file
@ -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 (
|
||||||
|
<Modal
|
||||||
|
onHide={ onHide }
|
||||||
|
show={ showModal }>
|
||||||
|
<Modal.Body>
|
||||||
|
<h4>Welcome to Free Code Camp's board</h4>
|
||||||
|
<p>We post jobs specifically target to our junior developers.</p>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
className='signup-btn'
|
||||||
|
onClick={ () => this.goToNewJob(onHide) }>
|
||||||
|
Post a Job
|
||||||
|
</Button>
|
||||||
|
</Modal.Body>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -1,7 +1,9 @@
|
|||||||
import React, { cloneElement, PropTypes } from 'react';
|
import React, { cloneElement, PropTypes } from 'react';
|
||||||
import { contain } from 'thundercats-react';
|
import { contain } from 'thundercats-react';
|
||||||
import { Navigation } from 'react-router';
|
import { History } from 'react-router';
|
||||||
import { Button, Jumbotron, Row } from 'react-bootstrap';
|
import { Button, Jumbotron, Row } from 'react-bootstrap';
|
||||||
|
|
||||||
|
import CreateJobModal from './CreateJobModal.jsx';
|
||||||
import ListJobs from './List.jsx';
|
import ListJobs from './List.jsx';
|
||||||
|
|
||||||
export default contain(
|
export default contain(
|
||||||
@ -13,12 +15,14 @@ export default contain(
|
|||||||
React.createClass({
|
React.createClass({
|
||||||
displayName: 'Jobs',
|
displayName: 'Jobs',
|
||||||
|
|
||||||
|
mixins: [History],
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
children: PropTypes.element,
|
children: PropTypes.element,
|
||||||
jobActions: PropTypes.object,
|
jobActions: PropTypes.object,
|
||||||
jobs: PropTypes.array
|
jobs: PropTypes.array,
|
||||||
|
showModal: PropTypes.bool
|
||||||
},
|
},
|
||||||
mixins: [Navigation],
|
|
||||||
|
|
||||||
handleJobClick(id) {
|
handleJobClick(id) {
|
||||||
const { jobActions } = this.props;
|
const { jobActions } = this.props;
|
||||||
@ -26,7 +30,7 @@ export default contain(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
jobActions.findJob(id);
|
jobActions.findJob(id);
|
||||||
this.transitionTo(`/jobs/${id}`);
|
this.history.pushState(null, `/jobs/${id}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderList(handleJobClick, jobs) {
|
renderList(handleJobClick, jobs) {
|
||||||
@ -48,7 +52,12 @@ export default contain(
|
|||||||
},
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { children, jobs } = this.props;
|
const {
|
||||||
|
children,
|
||||||
|
jobs,
|
||||||
|
showModal,
|
||||||
|
jobActions
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -62,7 +71,8 @@ export default contain(
|
|||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
bsSize='large'
|
bsSize='large'
|
||||||
className='signup-btn'>
|
className='signup-btn'
|
||||||
|
onClick={ jobActions.openModal }>
|
||||||
Try the first month 20% off!
|
Try the first month 20% off!
|
||||||
</Button>
|
</Button>
|
||||||
</Jumbotron>
|
</Jumbotron>
|
||||||
@ -71,6 +81,9 @@ export default contain(
|
|||||||
{ this.renderChild(children, jobs) ||
|
{ this.renderChild(children, jobs) ||
|
||||||
this.renderList(this.handleJobClick, jobs) }
|
this.renderList(this.handleJobClick, jobs) }
|
||||||
</Row>
|
</Row>
|
||||||
|
<CreateJobModal
|
||||||
|
onHide={ jobActions.closeModal }
|
||||||
|
showModal={ showModal } />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ export default React.createClass({
|
|||||||
id,
|
id,
|
||||||
company,
|
company,
|
||||||
position,
|
position,
|
||||||
|
isHighlighted,
|
||||||
description,
|
description,
|
||||||
logo,
|
logo,
|
||||||
city,
|
city,
|
||||||
@ -44,6 +45,7 @@ export default React.createClass({
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Panel
|
<Panel
|
||||||
|
bsStyle={ isHighlighted ? 'warning' : 'default' }
|
||||||
collapsible={ true }
|
collapsible={ true }
|
||||||
eventKey={ index }
|
eventKey={ index }
|
||||||
header={ header }
|
header={ header }
|
||||||
|
319
common/app/routes/Jobs/components/NewJob.jsx
Normal file
319
common/app/routes/Jobs/components/NewJob.jsx
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { History } from 'react-router';
|
||||||
|
import { contain } from 'thundercats-react';
|
||||||
|
import debugFactory from 'debug';
|
||||||
|
import { getDefaults } from '../utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
inHTMLData,
|
||||||
|
uriInSingleQuotedAttr
|
||||||
|
} from 'xss-filters';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
Input,
|
||||||
|
Row,
|
||||||
|
Well
|
||||||
|
} from 'react-bootstrap';
|
||||||
|
|
||||||
|
import {
|
||||||
|
isAscii,
|
||||||
|
isEmail,
|
||||||
|
isMobilePhone,
|
||||||
|
isURL
|
||||||
|
} from 'validator';
|
||||||
|
|
||||||
|
const debug = debugFactory('freecc:jobs:newForm');
|
||||||
|
|
||||||
|
const checkValidity = [
|
||||||
|
'position',
|
||||||
|
'locale',
|
||||||
|
'description',
|
||||||
|
'email',
|
||||||
|
'phone',
|
||||||
|
'url',
|
||||||
|
'logo',
|
||||||
|
'name',
|
||||||
|
'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,
|
||||||
|
locale,
|
||||||
|
description,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
url,
|
||||||
|
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() {
|
||||||
|
return typeof window !== 'undefined';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
React.createClass({
|
||||||
|
displayName: 'NewJob',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
jobActions: PropTypes.object,
|
||||||
|
position: PropTypes.object,
|
||||||
|
locale: PropTypes.object,
|
||||||
|
description: PropTypes.object,
|
||||||
|
email: PropTypes.object,
|
||||||
|
phone: PropTypes.object,
|
||||||
|
url: PropTypes.object,
|
||||||
|
logo: PropTypes.object,
|
||||||
|
name: PropTypes.object,
|
||||||
|
highlight: PropTypes.object
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [History],
|
||||||
|
|
||||||
|
handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const props = this.props;
|
||||||
|
let valid = true;
|
||||||
|
checkValidity.forEach((prop) => {
|
||||||
|
// 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 (
|
||||||
|
<div>
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<Well className='text-center'>
|
||||||
|
<h1>Create Your Job Post</h1>
|
||||||
|
<form
|
||||||
|
className='form-horizontal'
|
||||||
|
onSubmit={ this.handleSubmit }>
|
||||||
|
|
||||||
|
<div className='spacer'>
|
||||||
|
<h2>Job Information</h2>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
bsStyle={ position.bsStyle }
|
||||||
|
label='Position'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('position', e) }
|
||||||
|
placeholder='Position'
|
||||||
|
type='text'
|
||||||
|
value={ position.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
<Input
|
||||||
|
bsStyle={ locale.bsStyle }
|
||||||
|
label='Location'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('locale', e) }
|
||||||
|
placeholder='Location'
|
||||||
|
type='text'
|
||||||
|
value={ locale.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
<Input
|
||||||
|
bsStyle={ description.bsStyle }
|
||||||
|
label='Description'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('description', e) }
|
||||||
|
placeholder='Description'
|
||||||
|
rows='10'
|
||||||
|
type='textarea'
|
||||||
|
value={ description.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
|
||||||
|
<div className='divider'>
|
||||||
|
<h2>Company Information</h2>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
bsStyle={ name.bsStyle }
|
||||||
|
label='Company Name'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('name', e) }
|
||||||
|
placeholder='Foo, INC'
|
||||||
|
type='text'
|
||||||
|
value={ name.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
<Input
|
||||||
|
bsStyle={ email.bsStyle }
|
||||||
|
label='Email'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('email', e) }
|
||||||
|
placeholder='Email'
|
||||||
|
type='email'
|
||||||
|
value={ email.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
<Input
|
||||||
|
bsStyle={ phone.bsStyle }
|
||||||
|
label='Phone'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('phone', e) }
|
||||||
|
placeholder='555-123-1234'
|
||||||
|
type='tel'
|
||||||
|
value={ phone.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
<Input
|
||||||
|
bsStyle={ url.bsStyle }
|
||||||
|
label='URL'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('url', e) }
|
||||||
|
placeholder='http://freecatphotoapp.com'
|
||||||
|
type='url'
|
||||||
|
value={ url.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
<Input
|
||||||
|
bsStyle={ logo.bsStyle }
|
||||||
|
label='Logo'
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('logo', e) }
|
||||||
|
placeholder='http://freecatphotoapp.com/logo.png'
|
||||||
|
type='url'
|
||||||
|
value={ logo.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
|
||||||
|
<div className='divider'>
|
||||||
|
<h2>Make it stand out</h2>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
checked={ highlight.value }
|
||||||
|
label='Highlight your ad'
|
||||||
|
labelClassName={ 'col-sm-offset-1 col-sm-6'}
|
||||||
|
onChange={
|
||||||
|
({ target: { checked } }) => handleForm({
|
||||||
|
highlight: !!checked
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type='checkbox' />
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
lg={ 6 }
|
||||||
|
lgOffset={ 3 }>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='large'
|
||||||
|
bsStyle='primary'
|
||||||
|
type='submit'>
|
||||||
|
Preview My Ad
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</form>
|
||||||
|
</Well>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
14
common/app/routes/Jobs/components/Preview.jsx
Normal file
14
common/app/routes/Jobs/components/Preview.jsx
Normal file
@ -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
|
||||||
|
);
|
@ -1,13 +1,5 @@
|
|||||||
import React, { PropTypes } from 'react';
|
|
||||||
import { contain } from 'thundercats-react';
|
import { contain } from 'thundercats-react';
|
||||||
import { Row, Thumbnail, Panel, Well } from 'react-bootstrap';
|
import ShowJob from './ShowJob.jsx';
|
||||||
import moment from 'moment';
|
|
||||||
|
|
||||||
const thumbnailStyle = {
|
|
||||||
backgroundColor: 'white',
|
|
||||||
maxHeight: '100px',
|
|
||||||
maxWidth: '100px'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default contain(
|
export default contain(
|
||||||
{
|
{
|
||||||
@ -28,61 +20,5 @@ export default contain(
|
|||||||
return job.id !== id;
|
return job.id !== id;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
React.createClass({
|
ShowJob
|
||||||
displayName: 'ShowJob',
|
|
||||||
propTypes: {
|
|
||||||
job: PropTypes.object,
|
|
||||||
params: PropTypes.object
|
|
||||||
},
|
|
||||||
|
|
||||||
renderHeader({ company, position }) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h4 style={{ display: 'inline-block' }}>{ company }</h4>
|
|
||||||
<h5
|
|
||||||
className='pull-right hidden-xs hidden-md'
|
|
||||||
style={{ display: 'inline-block' }}>
|
|
||||||
{ position }
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { job = {} } = this.props;
|
|
||||||
const {
|
|
||||||
logo,
|
|
||||||
position,
|
|
||||||
city,
|
|
||||||
company,
|
|
||||||
state,
|
|
||||||
email,
|
|
||||||
phone,
|
|
||||||
postedOn,
|
|
||||||
description
|
|
||||||
} = job;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Row>
|
|
||||||
<Well>
|
|
||||||
<Thumbnail
|
|
||||||
alt={ company + 'company logo' }
|
|
||||||
src={ logo }
|
|
||||||
style={ thumbnailStyle } />
|
|
||||||
<Panel>
|
|
||||||
Position: { position }
|
|
||||||
Location: { city }, { state }
|
|
||||||
<br />
|
|
||||||
Contact: { email || phone || 'N/A' }
|
|
||||||
<br />
|
|
||||||
Posted On: { moment(postedOn).format('MMMM Do, YYYY') }
|
|
||||||
</Panel>
|
|
||||||
<p>{ description }</p>
|
|
||||||
</Well>
|
|
||||||
</Row>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
67
common/app/routes/Jobs/components/ShowJob.jsx
Normal file
67
common/app/routes/Jobs/components/ShowJob.jsx
Normal file
@ -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 (
|
||||||
|
<div>
|
||||||
|
<h4 style={{ display: 'inline-block' }}>{ company }</h4>
|
||||||
|
<h5
|
||||||
|
className='pull-right hidden-xs hidden-md'
|
||||||
|
style={{ display: 'inline-block' }}>
|
||||||
|
{ position }
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { job = {} } = this.props;
|
||||||
|
const {
|
||||||
|
logo,
|
||||||
|
position,
|
||||||
|
city,
|
||||||
|
company,
|
||||||
|
state,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
postedOn,
|
||||||
|
description
|
||||||
|
} = job;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Row>
|
||||||
|
<Well>
|
||||||
|
<Thumbnail
|
||||||
|
alt={ company + 'company logo' }
|
||||||
|
src={ logo }
|
||||||
|
style={ thumbnailStyle } />
|
||||||
|
<Panel>
|
||||||
|
Position: { position }
|
||||||
|
Location: { city }, { state }
|
||||||
|
<br />
|
||||||
|
Contact: { email || phone || 'N/A' }
|
||||||
|
<br />
|
||||||
|
Posted On: { moment(postedOn).format('MMMM Do, YYYY') }
|
||||||
|
</Panel>
|
||||||
|
<p>{ description }</p>
|
||||||
|
</Well>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -1,7 +1,9 @@
|
|||||||
import { Actions } from 'thundercats';
|
import { Actions } from 'thundercats';
|
||||||
|
import store from 'store';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
|
|
||||||
const debug = debugFactory('freecc:jobs:actions');
|
const debug = debugFactory('freecc:jobs:actions');
|
||||||
|
const assign = Object.assign;
|
||||||
|
|
||||||
export default Actions({
|
export default Actions({
|
||||||
setJobs: null,
|
setJobs: null,
|
||||||
@ -23,7 +25,7 @@ export default Actions({
|
|||||||
|
|
||||||
// if no job found this will be null which is a op noop
|
// if no job found this will be null which is a op noop
|
||||||
return foundJob ?
|
return foundJob ?
|
||||||
Object.assign({}, oldState, { currentJob: foundJob }) :
|
assign({}, oldState, { currentJob: foundJob }) :
|
||||||
null;
|
null;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -31,6 +33,31 @@ export default Actions({
|
|||||||
getJob: null,
|
getJob: null,
|
||||||
getJobs(params) {
|
getJobs(params) {
|
||||||
return { 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' })
|
.refs({ displayName: 'JobActions' })
|
||||||
@ -56,8 +83,22 @@ export default Actions({
|
|||||||
debug('job services experienced an issue', err);
|
debug('job services experienced an issue', err);
|
||||||
return jobActions.setError({ err });
|
return jobActions.setError({ err });
|
||||||
}
|
}
|
||||||
|
if (job) {
|
||||||
jobActions.setJobs({ currentJob: 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;
|
return jobActions;
|
||||||
});
|
});
|
||||||
|
@ -6,12 +6,25 @@ const {
|
|||||||
transformer
|
transformer
|
||||||
} = Store;
|
} = Store;
|
||||||
|
|
||||||
export default Store()
|
export default Store({ showModal: false })
|
||||||
.refs({ displayName: 'JobsStore' })
|
.refs({ displayName: 'JobsStore' })
|
||||||
.init(({ instance: jobsStore, args: [cat] }) => {
|
.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);
|
const register = createRegistrar(jobsStore);
|
||||||
register(setter(setJobs));
|
register(setter(setJobs));
|
||||||
register(transformer(findJob));
|
|
||||||
register(setter(setError));
|
register(setter(setError));
|
||||||
|
register(setter(openModal));
|
||||||
|
register(setter(closeModal));
|
||||||
|
register(setter(setForm));
|
||||||
|
|
||||||
|
register(transformer(findJob));
|
||||||
|
register(handleForm);
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import Jobs from './components/Jobs.jsx';
|
import Jobs from './components/Jobs.jsx';
|
||||||
|
import NewJob from './components/NewJob.jsx';
|
||||||
import Show from './components/Show.jsx';
|
import Show from './components/Show.jsx';
|
||||||
|
import Preview from './components/Preview.jsx';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* index: /jobs list jobs
|
* index: /jobs list jobs
|
||||||
@ -11,6 +13,12 @@ export default {
|
|||||||
childRoutes: [{
|
childRoutes: [{
|
||||||
path: '/jobs',
|
path: '/jobs',
|
||||||
component: Jobs
|
component: Jobs
|
||||||
|
}, {
|
||||||
|
path: 'jobs/new',
|
||||||
|
component: NewJob
|
||||||
|
}, {
|
||||||
|
path: 'jobs/new/preview',
|
||||||
|
component: Preview
|
||||||
}, {
|
}, {
|
||||||
path: 'jobs/:id',
|
path: 'jobs/:id',
|
||||||
component: Show
|
component: Show
|
||||||
|
22
common/app/routes/Jobs/utils.js
Normal file
22
common/app/routes/Jobs/utils.js
Normal file
@ -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]);
|
||||||
|
}
|
@ -3,12 +3,8 @@ import Hikes from './Hikes';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
path: '/',
|
path: '/',
|
||||||
getChildRoutes(locationState, cb) {
|
childRoutes: [
|
||||||
setTimeout(() => {
|
|
||||||
cb(null, [
|
|
||||||
Jobs,
|
Jobs,
|
||||||
Hikes
|
Hikes
|
||||||
]);
|
]
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "job",
|
"name": "job",
|
||||||
"base": "PersistedModel",
|
"base": "PersistedModel",
|
||||||
|
"strict": true,
|
||||||
"idInjection": true,
|
"idInjection": true,
|
||||||
"trackChanges": false,
|
"trackChanges": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -29,6 +30,9 @@
|
|||||||
"state": {
|
"state": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"country": {
|
"country": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -38,7 +42,7 @@
|
|||||||
"description": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isApproverd": {
|
"isApproved": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"isHighlighted": {
|
"isHighlighted": {
|
||||||
|
@ -49,6 +49,7 @@ var paths = {
|
|||||||
'!public/js/bundle*',
|
'!public/js/bundle*',
|
||||||
'node_modules/',
|
'node_modules/',
|
||||||
'client/',
|
'client/',
|
||||||
|
'seed',
|
||||||
'server/manifests/*.json',
|
'server/manifests/*.json',
|
||||||
'server/rev-manifest.json'
|
'server/rev-manifest.json'
|
||||||
],
|
],
|
||||||
|
10
package.json
10
package.json
@ -58,10 +58,10 @@
|
|||||||
"gulp-webpack": "^1.5.0",
|
"gulp-webpack": "^1.5.0",
|
||||||
"helmet": "~0.9.0",
|
"helmet": "~0.9.0",
|
||||||
"helmet-csp": "^0.2.3",
|
"helmet-csp": "^0.2.3",
|
||||||
|
"history": "^1.9.0",
|
||||||
"jade": "~1.8.0",
|
"jade": "~1.8.0",
|
||||||
"json-loader": "^0.5.2",
|
"json-loader": "^0.5.2",
|
||||||
"less": "~1.7.5",
|
"less": "~2.5.1",
|
||||||
"less-middleware": "~2.0.1",
|
|
||||||
"lodash": "^3.9.3",
|
"lodash": "^3.9.3",
|
||||||
"loopback": "https://github.com/FreeCodeCamp/loopback.git#fix/no-password",
|
"loopback": "https://github.com/FreeCodeCamp/loopback.git#fix/no-password",
|
||||||
"loopback-boot": "2.8.2",
|
"loopback-boot": "2.8.2",
|
||||||
@ -89,7 +89,7 @@
|
|||||||
"react": "^0.13.3",
|
"react": "^0.13.3",
|
||||||
"react-bootstrap": "~0.23.7",
|
"react-bootstrap": "~0.23.7",
|
||||||
"react-motion": "~0.1.0",
|
"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",
|
"react-vimeo": "^0.0.3",
|
||||||
"request": "~2.53.0",
|
"request": "~2.53.0",
|
||||||
"rev-del": "^1.0.5",
|
"rev-del": "^1.0.5",
|
||||||
@ -97,12 +97,14 @@
|
|||||||
"sanitize-html": "~1.6.1",
|
"sanitize-html": "~1.6.1",
|
||||||
"sort-keys": "^1.1.1",
|
"sort-keys": "^1.1.1",
|
||||||
"source-map-support": "^0.3.2",
|
"source-map-support": "^0.3.2",
|
||||||
|
"store": "https://github.com/berkeleytrue/store.js.git#feature/noop-server",
|
||||||
"thundercats": "^2.1.0",
|
"thundercats": "^2.1.0",
|
||||||
"thundercats-react": "^0.1.0",
|
"thundercats-react": "^0.1.0",
|
||||||
"twit": "~1.1.20",
|
"twit": "~1.1.20",
|
||||||
"uglify-js": "~2.4.15",
|
"uglify-js": "~2.4.15",
|
||||||
"validator": "~3.22.1",
|
"validator": "^3.22.1",
|
||||||
"webpack": "^1.9.12",
|
"webpack": "^1.9.12",
|
||||||
|
"xss-filters": "^1.2.6",
|
||||||
"yui": "~3.18.1"
|
"yui": "~3.18.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Router from 'react-router';
|
import { RoutingContext } from 'react-router';
|
||||||
import Fetchr from 'fetchr';
|
import Fetchr from 'fetchr';
|
||||||
import Location from 'react-router/lib/Location';
|
import { createLocation } from 'history';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
import { app$ } from '../../common/app';
|
import { app$ } from '../../common/app';
|
||||||
import { RenderToString } from 'thundercats-react';
|
import { RenderToString } from 'thundercats-react';
|
||||||
@ -30,25 +30,25 @@ export default function reactSubRouter(app) {
|
|||||||
|
|
||||||
function serveReactApp(req, res, next) {
|
function serveReactApp(req, res, next) {
|
||||||
const services = new Fetchr({ req });
|
const services = new Fetchr({ req });
|
||||||
const location = new Location(req.path, req.query);
|
const location = createLocation(req.path);
|
||||||
|
|
||||||
// returns a router wrapped app
|
// returns a router wrapped app
|
||||||
app$(location)
|
app$({ location })
|
||||||
// if react-router does not find a route send down the chain
|
// if react-router does not find a route send down the chain
|
||||||
.filter(function({ initialState }) {
|
.filter(function({ props}) {
|
||||||
if (!initialState) {
|
if (!props) {
|
||||||
debug('react tried to find %s but got 404', location.pathname);
|
debug('react tried to find %s but got 404', location.pathname);
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
return !!initialState;
|
return !!props;
|
||||||
})
|
})
|
||||||
.flatMap(function({ initialState, AppCat }) {
|
.flatMap(function({ props, AppCat }) {
|
||||||
// call thundercats renderToString
|
// call thundercats renderToString
|
||||||
// prefetches data and sets up it up for current state
|
// prefetches data and sets up it up for current state
|
||||||
debug('rendering to string');
|
debug('rendering to string');
|
||||||
return RenderToString(
|
return RenderToString(
|
||||||
AppCat(null, services),
|
AppCat(null, services),
|
||||||
React.createElement(Router, initialState)
|
React.createElement(RoutingContext, props)
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
// makes sure we only get one onNext and closes subscription
|
// makes sure we only get one onNext and closes subscription
|
||||||
|
Reference in New Issue
Block a user