Merge branch 'feature/jobs' into staging
This commit is contained in:
@ -22,6 +22,19 @@ const history = createHistory();
|
|||||||
const appLocation = createLocation(
|
const appLocation = createLocation(
|
||||||
location.pathname + location.search
|
location.pathname + location.search
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function location$(history) {
|
||||||
|
return Rx.Observable.create(function(observer) {
|
||||||
|
const dispose = history.listen(function(location) {
|
||||||
|
observer.onNext(location.pathname);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Rx.Disposable.create(() => {
|
||||||
|
dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// returns an observable
|
// returns an observable
|
||||||
app$({ history, location: appLocation })
|
app$({ history, location: appLocation })
|
||||||
.flatMap(
|
.flatMap(
|
||||||
@ -36,6 +49,27 @@ app$({ history, location: appLocation })
|
|||||||
// redirects in the future
|
// redirects in the future
|
||||||
({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat })
|
({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat })
|
||||||
)
|
)
|
||||||
|
.doOnNext(({ appCat }) => {
|
||||||
|
const appActions = appCat.getActions('appActions');
|
||||||
|
|
||||||
|
location$(history)
|
||||||
|
.pluck('pathname')
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.doOnNext(route => debug('route change', route))
|
||||||
|
.subscribe(route => appActions.updateRoute(route));
|
||||||
|
|
||||||
|
appActions.goBack.subscribe(function() {
|
||||||
|
history.goBack();
|
||||||
|
});
|
||||||
|
|
||||||
|
appActions
|
||||||
|
.updateRoute
|
||||||
|
.pluck('route')
|
||||||
|
.doOnNext(route => debug('update route', route))
|
||||||
|
.subscribe(function(route) {
|
||||||
|
history.pushState(null, route);
|
||||||
|
});
|
||||||
|
})
|
||||||
.flatMap(({ props, appCat }) => {
|
.flatMap(({ props, appCat }) => {
|
||||||
props.history = history;
|
props.history = history;
|
||||||
return Render(
|
return Render(
|
||||||
@ -49,7 +83,7 @@ app$({ history, location: appLocation })
|
|||||||
debug('react rendered');
|
debug('react rendered');
|
||||||
},
|
},
|
||||||
err => {
|
err => {
|
||||||
debug('an error has occured', err.stack);
|
throw err;
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
debug('react closed subscription');
|
debug('react closed subscription');
|
||||||
|
16
client/less/jobs.less
Normal file
16
client/less/jobs.less
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.jobs-list-highlight {
|
||||||
|
background-color: #ffc
|
||||||
|
}
|
||||||
|
|
||||||
|
a.jobs-list-highlight:hover {
|
||||||
|
background-color: #ffc
|
||||||
|
}
|
||||||
|
|
||||||
|
.jobs-list {
|
||||||
|
cursor: pointer;
|
||||||
|
cursor: hand;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jobs-checkbox-spacer input[type="checkbox"] {
|
||||||
|
margin-left: -23px
|
||||||
|
}
|
@ -424,7 +424,6 @@
|
|||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
color: @navbar-default-link-active-color;
|
color: @navbar-default-link-active-color;
|
||||||
background-color: @navbar-default-link-active-bg;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
> .disabled > a {
|
> .disabled > a {
|
||||||
|
@ -9,6 +9,11 @@ html,body,div,span,a,li,td,th {
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bold {
|
||||||
|
font-family: 'Lato-Bold', sans-serif;
|
||||||
|
font-weight: Bold;
|
||||||
|
}
|
||||||
|
|
||||||
li, .wrappable {
|
li, .wrappable {
|
||||||
white-space: pre; /* CSS 2.0 */
|
white-space: pre; /* CSS 2.0 */
|
||||||
white-space: pre-wrap; /* CSS 2.1 */
|
white-space: pre-wrap; /* CSS 2.1 */
|
||||||
@ -973,11 +978,11 @@ code {
|
|||||||
margin: 0!important;
|
margin: 0!important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// gitter chat
|
|
||||||
.gitter-chat-embed {
|
.gitter-chat-embed {
|
||||||
z-index: 20000 !important;
|
z-index: 20000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//uncomment this to see the dimensions of all elements outlined in red
|
//uncomment this to see the dimensions of all elements outlined in red
|
||||||
//* {
|
//* {
|
||||||
// border-color: red;
|
// border-color: red;
|
||||||
@ -1087,3 +1092,4 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@import "chat.less";
|
@import "chat.less";
|
||||||
|
@import "jobs.less";
|
||||||
|
@ -12,6 +12,6 @@ export default Cat()
|
|||||||
cat.register(HikesActions, null, services);
|
cat.register(HikesActions, null, services);
|
||||||
cat.register(HikesStore, null, cat);
|
cat.register(HikesStore, null, cat);
|
||||||
|
|
||||||
cat.register(JobActions, null, services);
|
cat.register(JobActions, null, cat, services);
|
||||||
cat.register(JobsStore, null, cat);
|
cat.register(JobsStore, null, cat);
|
||||||
});
|
});
|
||||||
|
27
common/app/components/Flash/Queue.jsx
Normal file
27
common/app/components/Flash/Queue.jsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React, { createClass, PropTypes } from 'react';
|
||||||
|
import { Alert } from 'react-bootstrap';
|
||||||
|
|
||||||
|
export default createClass({
|
||||||
|
displayName: 'FlashQueue',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
messages: PropTypes.array
|
||||||
|
},
|
||||||
|
|
||||||
|
renderMessages(messages) {
|
||||||
|
return messages.map(message => {
|
||||||
|
return (
|
||||||
|
<Alert>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { messages = [] } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{ this.renderMessages(messages) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
0
common/app/components/Flash/index.jsx
Normal file
0
common/app/components/Flash/index.jsx
Normal file
@ -1,4 +1,5 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
|
import { LinkContainer } from 'react-router-bootstrap';
|
||||||
import {
|
import {
|
||||||
Col,
|
Col,
|
||||||
CollapsibleNav,
|
CollapsibleNav,
|
||||||
@ -11,16 +12,6 @@ import navLinks from './links.json';
|
|||||||
import FCCNavItem from './NavItem.jsx';
|
import FCCNavItem from './NavItem.jsx';
|
||||||
|
|
||||||
const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg';
|
const fCClogo = 'https://s3.amazonaws.com/freecodecamp/freecodecamp_logo.svg';
|
||||||
const navElements = navLinks.map((navItem, index) => {
|
|
||||||
return (
|
|
||||||
<NavItem
|
|
||||||
eventKey={ index + 1 }
|
|
||||||
href={ navItem.link }
|
|
||||||
key={ index }>
|
|
||||||
{ navItem.content }
|
|
||||||
</NavItem>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const logoElement = (
|
const logoElement = (
|
||||||
<a href='/'>
|
<a href='/'>
|
||||||
@ -39,18 +30,40 @@ const toggleButton = (
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default class extends React.Component {
|
export default React.createClass({
|
||||||
constructor(props) {
|
displayName: 'Nav',
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
static displayName = 'Nav'
|
propTypes: {
|
||||||
static propTypes = {
|
|
||||||
points: PropTypes.number,
|
points: PropTypes.number,
|
||||||
picture: PropTypes.string,
|
picture: PropTypes.string,
|
||||||
signedIn: PropTypes.bool,
|
signedIn: PropTypes.bool,
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
|
},
|
||||||
|
|
||||||
|
renderLinks() {
|
||||||
|
return navLinks.map(({ content, link, react }, index) => {
|
||||||
|
if (react) {
|
||||||
|
return (
|
||||||
|
<LinkContainer
|
||||||
|
eventKey={ index + 1 }
|
||||||
|
key={ content }
|
||||||
|
to={ link }>
|
||||||
|
<NavItem>
|
||||||
|
{ content }
|
||||||
|
</NavItem>
|
||||||
|
</LinkContainer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
<NavItem
|
||||||
|
eventKey={ index + 1 }
|
||||||
|
href={ link }
|
||||||
|
key={ content }>
|
||||||
|
{ content }
|
||||||
|
</NavItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
renderPoints(username, points) {
|
renderPoints(username, points) {
|
||||||
if (!username) {
|
if (!username) {
|
||||||
@ -62,7 +75,7 @@ export default class extends React.Component {
|
|||||||
[ { points } ]
|
[ { points } ]
|
||||||
</NavItem>
|
</NavItem>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
|
||||||
renderSignin(username, picture) {
|
renderSignin(username, picture) {
|
||||||
if (username) {
|
if (username) {
|
||||||
@ -87,7 +100,7 @@ export default class extends React.Component {
|
|||||||
</FCCNavItem>
|
</FCCNavItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { username, points, picture } = this.props;
|
const { username, points, picture } = this.props;
|
||||||
@ -103,12 +116,12 @@ export default class extends React.Component {
|
|||||||
className='hamburger-dropdown'
|
className='hamburger-dropdown'
|
||||||
navbar={ true }
|
navbar={ true }
|
||||||
right={ true }>
|
right={ true }>
|
||||||
{ navElements }
|
{ this.renderLinks() }
|
||||||
{ this.renderPoints(username, points)}
|
{ this.renderPoints(username, points) }
|
||||||
{ this.renderSignin(username, picture) }
|
{ this.renderSignin(username, picture) }
|
||||||
</Nav>
|
</Nav>
|
||||||
</CollapsibleNav>
|
</CollapsibleNav>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
@ -4,11 +4,14 @@ import BootstrapMixin from 'react-bootstrap/lib/BootstrapMixin';
|
|||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'FCCNavItem',
|
displayName: 'FCCNavItem',
|
||||||
|
|
||||||
mixins: [BootstrapMixin],
|
mixins: [BootstrapMixin],
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
active: React.PropTypes.bool,
|
active: React.PropTypes.bool,
|
||||||
'aria-controls': React.PropTypes.string,
|
'aria-controls': React.PropTypes.string,
|
||||||
|
children: React.PropTypes.node,
|
||||||
|
className: React.PropTypes.string,
|
||||||
disabled: React.PropTypes.bool,
|
disabled: React.PropTypes.bool,
|
||||||
eventKey: React.PropTypes.any,
|
eventKey: React.PropTypes.any,
|
||||||
href: React.PropTypes.string,
|
href: React.PropTypes.string,
|
||||||
@ -30,7 +33,11 @@ export default React.createClass({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!this.props.disabled) {
|
if (!this.props.disabled) {
|
||||||
this.props.onSelect(this.props.eventKey, this.props.href, this.props.target);
|
this.props.onSelect(
|
||||||
|
this.props.eventKey,
|
||||||
|
this.props.href,
|
||||||
|
this.props.target
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -50,10 +57,11 @@ export default React.createClass({
|
|||||||
...props
|
...props
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
let classes = {
|
const linkClassName = classNames(className, {
|
||||||
active,
|
// 'active': active, we don't actually use the active class
|
||||||
disabled
|
// but it is used for a11y below
|
||||||
};
|
'disabled': disabled
|
||||||
|
});
|
||||||
|
|
||||||
let linkProps = {
|
let linkProps = {
|
||||||
role,
|
role,
|
||||||
@ -75,9 +83,9 @@ export default React.createClass({
|
|||||||
role='presentation'>
|
role='presentation'>
|
||||||
<a
|
<a
|
||||||
{ ...linkProps }
|
{ ...linkProps }
|
||||||
aria-selected={ active }
|
|
||||||
aria-controls={ ariaControls }
|
aria-controls={ ariaControls }
|
||||||
className={ className }>
|
aria-selected={ active }
|
||||||
|
className={ linkClassName }>
|
||||||
{ children }
|
{ children }
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
@ -9,5 +9,6 @@
|
|||||||
"link": "/news"
|
"link": "/news"
|
||||||
},{
|
},{
|
||||||
"content": "Jobs",
|
"content": "Jobs",
|
||||||
"link": "/jobs"
|
"link": "/jobs",
|
||||||
|
"react": true
|
||||||
}]
|
}]
|
||||||
|
@ -16,7 +16,11 @@ export default Actions({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getUser: null
|
getUser: null,
|
||||||
|
updateRoute(route) {
|
||||||
|
return { route };
|
||||||
|
},
|
||||||
|
goBack: null
|
||||||
})
|
})
|
||||||
.refs({ displayName: 'AppActions' })
|
.refs({ displayName: 'AppActions' })
|
||||||
.init(({ instance: appActions, args: [services] }) => {
|
.init(({ instance: appActions, args: [services] }) => {
|
||||||
|
@ -14,10 +14,10 @@ export default Store({
|
|||||||
value: initValue
|
value: initValue
|
||||||
},
|
},
|
||||||
init({ instance: appStore, args: [cat] }) {
|
init({ instance: appStore, args: [cat] }) {
|
||||||
const { setUser, setTitle } = cat.getActions('appActions');
|
const { updateRoute, setUser, setTitle } = cat.getActions('appActions');
|
||||||
const register = createRegistrar(appStore);
|
const register = createRegistrar(appStore);
|
||||||
|
|
||||||
register(setter(fromMany(setUser, setTitle)));
|
register(setter(fromMany(setUser, setTitle, updateRoute)));
|
||||||
|
|
||||||
return appStore;
|
return appStore;
|
||||||
}
|
}
|
||||||
|
270
common/app/routes/Jobs/components/GoToPayPal.jsx
Normal file
270
common/app/routes/Jobs/components/GoToPayPal.jsx
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
import React, { PropTypes } from 'react';
|
||||||
|
import { Button, Input, Col, Panel, Row, Well } from 'react-bootstrap';
|
||||||
|
import { contain } from 'thundercats-react';
|
||||||
|
|
||||||
|
// real paypal buttons
|
||||||
|
// will take your money
|
||||||
|
const paypalIds = {
|
||||||
|
regular: 'Q8Z82ZLAX3Q8N',
|
||||||
|
highlighted: 'VC8QPSKCYMZLN'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default contain(
|
||||||
|
{
|
||||||
|
store: 'JobsStore',
|
||||||
|
actions: [
|
||||||
|
'jobActions',
|
||||||
|
'appActions'
|
||||||
|
],
|
||||||
|
map({
|
||||||
|
job: { id, isHighlighted } = {},
|
||||||
|
buttonId = isHighlighted ?
|
||||||
|
paypalIds.highlighted :
|
||||||
|
paypalIds.regular,
|
||||||
|
price = 200,
|
||||||
|
discountAmount = 0,
|
||||||
|
promoCode = '',
|
||||||
|
promoApplied = false,
|
||||||
|
promoName
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
isHighlighted,
|
||||||
|
buttonId,
|
||||||
|
price,
|
||||||
|
discountAmount,
|
||||||
|
promoName,
|
||||||
|
promoCode,
|
||||||
|
promoApplied
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
React.createClass({
|
||||||
|
displayName: 'GoToPayPal',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
appActions: PropTypes.object,
|
||||||
|
id: PropTypes.string,
|
||||||
|
isHighlighted: PropTypes.bool,
|
||||||
|
buttonId: PropTypes.string,
|
||||||
|
price: PropTypes.number,
|
||||||
|
discountAmount: PropTypes.number,
|
||||||
|
promoName: PropTypes.string,
|
||||||
|
promoCode: PropTypes.string,
|
||||||
|
promoApplied: PropTypes.bool,
|
||||||
|
jobActions: PropTypes.object
|
||||||
|
},
|
||||||
|
|
||||||
|
goToJobBoard() {
|
||||||
|
const { appActions } = this.props;
|
||||||
|
appActions.updateRoute('/jobs');
|
||||||
|
},
|
||||||
|
|
||||||
|
renderDiscount(discountAmount) {
|
||||||
|
if (!discountAmount) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 3 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<h4>Promo Discount</h4>
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
md={ 3 }>
|
||||||
|
<h4>-{ discountAmount }</h4>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderHighlightPrice(isHighlighted) {
|
||||||
|
if (!isHighlighted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 3 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<h4>Highlighting</h4>
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
md={ 3 }>
|
||||||
|
<h4>+ 50</h4>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderPromo() {
|
||||||
|
const {
|
||||||
|
promoApplied,
|
||||||
|
promoCode,
|
||||||
|
promoName,
|
||||||
|
isHighlighted,
|
||||||
|
jobActions
|
||||||
|
} = this.props;
|
||||||
|
if (promoApplied) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 3 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
{ promoName } applied
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 3 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
Have a promo code?
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 3 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<Input
|
||||||
|
onChange={ jobActions.setPromoCode }
|
||||||
|
type='text'
|
||||||
|
value={ promoCode } />
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
md={ 3 }>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
onClick={ () => {
|
||||||
|
jobActions.applyCode({
|
||||||
|
code: promoCode,
|
||||||
|
type: isHighlighted ? 'isHighlighted' : null
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
Apply Promo Code
|
||||||
|
</Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
isHighlighted,
|
||||||
|
buttonId,
|
||||||
|
price,
|
||||||
|
discountAmount
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 10 }
|
||||||
|
mdOffset={ 1 }
|
||||||
|
sm={ 8 }
|
||||||
|
smOffset={ 2 }
|
||||||
|
xs={ 12 }>
|
||||||
|
<Panel>
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 6 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<h2 className='text-center'>
|
||||||
|
One more step
|
||||||
|
</h2>
|
||||||
|
<div className='spacer' />
|
||||||
|
You're Awesome! just one more step to go.
|
||||||
|
Clicking on the link below will redirect to paypal.
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div className='spacer' />
|
||||||
|
<Well>
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 3 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<h4>Job Posting</h4>
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
md={ 6 }>
|
||||||
|
<h4>+ { price }</h4>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{ this.renderHighlightPrice(isHighlighted) }
|
||||||
|
{ this.renderDiscount(discountAmount) }
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 3 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<h4>Total</h4>
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
md={ 6 }>
|
||||||
|
<h4>${
|
||||||
|
price - discountAmount + (isHighlighted ? 50 : 0)
|
||||||
|
}</h4>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Well>
|
||||||
|
{ this.renderPromo() }
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 6 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<form
|
||||||
|
action='https://www.paypal.com/cgi-bin/webscr'
|
||||||
|
method='post'
|
||||||
|
onClick={ this.goToJobBoard }
|
||||||
|
target='_blank'>
|
||||||
|
<input
|
||||||
|
name='cmd'
|
||||||
|
type='hidden'
|
||||||
|
value='_s-xclick' />
|
||||||
|
<input
|
||||||
|
name='hosted_button_id'
|
||||||
|
type='hidden'
|
||||||
|
value={ buttonId } />
|
||||||
|
<input
|
||||||
|
name='custom'
|
||||||
|
type='hidden'
|
||||||
|
value={ '' + id } />
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='large'
|
||||||
|
className='signup-btn'
|
||||||
|
type='submit'>
|
||||||
|
<i className='fa fa-paypal' />
|
||||||
|
Continue to PayPal
|
||||||
|
</Button>
|
||||||
|
<div className='spacer' />
|
||||||
|
<img
|
||||||
|
alt='An array of credit cards'
|
||||||
|
border='0'
|
||||||
|
src='http://i.imgur.com/Q2SdSZG.png'
|
||||||
|
style={{
|
||||||
|
width: '100%'
|
||||||
|
}} />
|
||||||
|
</form>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div className='spacer' />
|
||||||
|
</Panel>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
31
common/app/routes/Jobs/components/JobNotFound.jsx
Normal file
31
common/app/routes/Jobs/components/JobNotFound.jsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React, { createClass } from 'react';
|
||||||
|
import { LinkContainer } from 'react-router-bootstrap';
|
||||||
|
import { Button, Row, Col, Panel } from 'react-bootstrap';
|
||||||
|
|
||||||
|
export default createClass({
|
||||||
|
displayName: 'NoJobFound',
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 6 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<Panel>
|
||||||
|
No job found...
|
||||||
|
<LinkContainer to='/jobs'>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='large'
|
||||||
|
bsStyle='primary'>
|
||||||
|
Go to the job board
|
||||||
|
</Button>
|
||||||
|
</LinkContainer>
|
||||||
|
</Panel>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -1,36 +1,43 @@
|
|||||||
import React, { cloneElement, PropTypes } from 'react';
|
import React, { cloneElement, PropTypes } from 'react';
|
||||||
import { contain } from 'thundercats-react';
|
import { contain } from 'thundercats-react';
|
||||||
import { History } from 'react-router';
|
import { Button, Panel, Row, Col } 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';
|
||||||
|
import TwitterBtn from './TwitterBtn.jsx';
|
||||||
|
|
||||||
export default contain(
|
export default contain(
|
||||||
{
|
{
|
||||||
store: 'jobsStore',
|
store: 'jobsStore',
|
||||||
fetchAction: 'jobActions.getJobs',
|
fetchAction: 'jobActions.getJobs',
|
||||||
actions: 'jobActions'
|
actions: [
|
||||||
|
'appActions',
|
||||||
|
'jobActions'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
React.createClass({
|
React.createClass({
|
||||||
displayName: 'Jobs',
|
displayName: 'Jobs',
|
||||||
|
|
||||||
mixins: [History],
|
|
||||||
|
|
||||||
propTypes: {
|
propTypes: {
|
||||||
children: PropTypes.element,
|
children: PropTypes.element,
|
||||||
|
numOfFollowers: PropTypes.number,
|
||||||
|
appActions: PropTypes.object,
|
||||||
jobActions: PropTypes.object,
|
jobActions: PropTypes.object,
|
||||||
jobs: PropTypes.array,
|
jobs: PropTypes.array,
|
||||||
showModal: PropTypes.bool
|
showModal: PropTypes.bool
|
||||||
},
|
},
|
||||||
|
|
||||||
handleJobClick(id) {
|
componentDidMount() {
|
||||||
const { jobActions } = this.props;
|
const { jobActions } = this.props;
|
||||||
|
jobActions.getFollowers();
|
||||||
|
},
|
||||||
|
|
||||||
|
handleJobClick(id) {
|
||||||
|
const { appActions, jobActions } = this.props;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
jobActions.findJob(id);
|
jobActions.findJob(id);
|
||||||
this.history.pushState(null, `/jobs/${id}`);
|
appActions.updateRoute(`/jobs/${id}`);
|
||||||
},
|
},
|
||||||
|
|
||||||
renderList(handleJobClick, jobs) {
|
renderList(handleJobClick, jobs) {
|
||||||
@ -54,37 +61,47 @@ export default contain(
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
|
numOfFollowers,
|
||||||
jobs,
|
jobs,
|
||||||
showModal,
|
appActions
|
||||||
jobActions
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Panel>
|
||||||
<Row>
|
<Row>
|
||||||
<Jumbotron>
|
<Col
|
||||||
<h1>Free Code Camps' Job Board</h1>
|
md={ 10 }
|
||||||
<p>
|
mdOffset= { 1 }
|
||||||
Need to find the best junior developers?
|
xs={ 12 }>
|
||||||
Want to find dedicated developers eager to join your company?
|
<h1 className='text-center'>
|
||||||
Sign up now to post your job!
|
Talented web developers with strong portfolios are eager
|
||||||
</p>
|
to work for your company
|
||||||
|
</h1>
|
||||||
|
<Row className='text-center'>
|
||||||
|
<Col
|
||||||
|
sm={ 8 }
|
||||||
|
smOffset={ 2 }
|
||||||
|
xs={ 12 }>
|
||||||
<Button
|
<Button
|
||||||
bsSize='large'
|
bsSize='large'
|
||||||
className='signup-btn'
|
className='signup-btn btn-block'
|
||||||
onClick={ jobActions.openModal }>
|
onClick={ ()=> {
|
||||||
Try the first month 20% off!
|
appActions.updateRoute('/jobs/new');
|
||||||
|
}}>
|
||||||
|
Post a job: $200 for 30 days + weekly tweets
|
||||||
</Button>
|
</Button>
|
||||||
</Jumbotron>
|
<div className='button-spacer' />
|
||||||
|
<TwitterBtn count={ numOfFollowers || 0 } />
|
||||||
|
<div className='spacer' />
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
{ this.renderChild(children, jobs) ||
|
{ this.renderChild(children, jobs) ||
|
||||||
this.renderList(this.handleJobClick, jobs) }
|
this.renderList(this.handleJobClick, jobs) }
|
||||||
</Row>
|
</Row>
|
||||||
<CreateJobModal
|
</Col>
|
||||||
onHide={ jobActions.closeModal }
|
</Row>
|
||||||
showModal={ showModal } />
|
</Panel>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { PanelGroup, Thumbnail, Panel, Well } from 'react-bootstrap';
|
import classnames from 'classnames';
|
||||||
|
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
@ -10,62 +11,57 @@ export default React.createClass({
|
|||||||
jobs: PropTypes.array
|
jobs: PropTypes.array
|
||||||
},
|
},
|
||||||
|
|
||||||
renderJobs(handleClick, jobs =[]) {
|
addLocation(locale) {
|
||||||
const thumbnailStyle = {
|
if (!locale) {
|
||||||
backgroundColor: 'white',
|
return null;
|
||||||
maxHeight: '100px',
|
}
|
||||||
maxWidth: '100px'
|
return (
|
||||||
};
|
<span className='hidden-xs hidden-sm'>
|
||||||
|
{ locale } - {' '}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
return jobs.map((
|
renderJobs(handleClick, jobs = []) {
|
||||||
{
|
return jobs
|
||||||
|
.filter(({ isPaid, isApproved, isFilled }) => {
|
||||||
|
return isPaid && isApproved && !isFilled;
|
||||||
|
})
|
||||||
|
.map(({
|
||||||
id,
|
id,
|
||||||
company,
|
company,
|
||||||
position,
|
position,
|
||||||
isHighlighted,
|
isHighlighted,
|
||||||
description,
|
postedOn,
|
||||||
logo,
|
locale
|
||||||
city,
|
}) => {
|
||||||
state,
|
|
||||||
email,
|
const className = classnames({
|
||||||
phone,
|
'jobs-list': true,
|
||||||
postedOn
|
'jobs-list-highlight': isHighlighted
|
||||||
},
|
});
|
||||||
index
|
|
||||||
) => {
|
|
||||||
const header = (
|
|
||||||
<div>
|
|
||||||
<h4 style={{ display: 'inline-block' }}>{ company }</h4>
|
|
||||||
<h5
|
|
||||||
className='pull-right hidden-xs hidden-md'
|
|
||||||
style={{ display: 'inline-block' }}>
|
|
||||||
{ position }
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<Panel
|
<ListGroupItem
|
||||||
bsStyle={ isHighlighted ? 'warning' : 'default' }
|
className={ className }
|
||||||
collapsible={ true }
|
key={ id }
|
||||||
eventKey={ index }
|
onClick={ () => handleClick(id) }>
|
||||||
header={ header }
|
<div>
|
||||||
key={ id }>
|
<h4 style={{ display: 'inline-block' }}>
|
||||||
<Well>
|
<bold>{ company }</bold>
|
||||||
<Thumbnail
|
{' '}
|
||||||
alt={ company + 'company logo' }
|
<span className='hidden-xs hidden-sm'>
|
||||||
src={ logo }
|
- { position }
|
||||||
style={ thumbnailStyle } />
|
</span>
|
||||||
<Panel>
|
</h4>
|
||||||
Position: { position }
|
<h4
|
||||||
Location: { city }, { state }
|
className='pull-right'
|
||||||
<br />
|
style={{ display: 'inline-block' }}>
|
||||||
Contact: { email || phone || 'N/A' }
|
{ this.addLocation(locale) }
|
||||||
<br />
|
{ moment(new Date(postedOn)).format('MMM Do') }
|
||||||
Posted On: { moment(postedOn).format('MMMM Do, YYYY') }
|
</h4>
|
||||||
</Panel>
|
</div>
|
||||||
<p onClick={ () => handleClick(id) }>{ description }</p>
|
</ListGroupItem>
|
||||||
</Well>
|
|
||||||
</Panel>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -77,9 +73,9 @@ export default React.createClass({
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PanelGroup>
|
<ListGroup>
|
||||||
{ this.renderJobs(handleClick, jobs) }
|
{ this.renderJobs(handleClick, jobs) }
|
||||||
</PanelGroup>
|
</ListGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
import { helpers } from 'rx';
|
||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { History } from 'react-router';
|
import { History } from 'react-router';
|
||||||
import { contain } from 'thundercats-react';
|
import { contain } from 'thundercats-react';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
|
import dedent from 'dedent';
|
||||||
|
import normalizeUrl from 'normalize-url';
|
||||||
|
|
||||||
import { getDefaults } from '../utils';
|
import { getDefaults } from '../utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -14,13 +18,13 @@ import {
|
|||||||
Col,
|
Col,
|
||||||
Input,
|
Input,
|
||||||
Row,
|
Row,
|
||||||
|
Panel,
|
||||||
Well
|
Well
|
||||||
} from 'react-bootstrap';
|
} from 'react-bootstrap';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isAscii,
|
isAscii,
|
||||||
isEmail,
|
isEmail,
|
||||||
isMobilePhone,
|
|
||||||
isURL
|
isURL
|
||||||
} from 'validator';
|
} from 'validator';
|
||||||
|
|
||||||
@ -31,12 +35,43 @@ const checkValidity = [
|
|||||||
'locale',
|
'locale',
|
||||||
'description',
|
'description',
|
||||||
'email',
|
'email',
|
||||||
'phone',
|
|
||||||
'url',
|
'url',
|
||||||
'logo',
|
'logo',
|
||||||
'name',
|
'company',
|
||||||
'highlight'
|
'isHighlighted',
|
||||||
|
'howToApply'
|
||||||
];
|
];
|
||||||
|
const hightlightCopy = `
|
||||||
|
Highlight my post to make it stand out. (+$50)
|
||||||
|
`;
|
||||||
|
|
||||||
|
const foo = `
|
||||||
|
This will narrow the field substantially with higher quality applicants
|
||||||
|
`;
|
||||||
|
|
||||||
|
const isFullStackCopy = `
|
||||||
|
Applicants must have earned Free Code Camp’s Full Stack Certification to apply.*
|
||||||
|
`;
|
||||||
|
|
||||||
|
const isFrontEndCopy = `
|
||||||
|
Applicants must have earned Free Code Camp’s Front End Certification to apply.*
|
||||||
|
`;
|
||||||
|
|
||||||
|
const isRemoteCopy = `
|
||||||
|
This job can be performed remotely.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const howToApplyCopy = dedent`
|
||||||
|
Examples: click here to apply yourcompany.com/jobs/33
|
||||||
|
Or email jobs@yourcompany.com
|
||||||
|
`;
|
||||||
|
|
||||||
|
const checkboxClass = dedent`
|
||||||
|
text-left
|
||||||
|
jobs-checkbox-spacer
|
||||||
|
col-sm-offset-2
|
||||||
|
col-sm-6 col-md-offset-3
|
||||||
|
`;
|
||||||
|
|
||||||
function formatValue(value, validator, type = 'string') {
|
function formatValue(value, validator, type = 'string') {
|
||||||
const formated = getDefaults(type);
|
const formated = getDefaults(type);
|
||||||
@ -50,12 +85,32 @@ function formatValue(value, validator, type = 'string') {
|
|||||||
return formated;
|
return formated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizeOptions = {
|
||||||
|
stripWWW: false
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatUrl(url, shouldKeepTrailingSlash = true) {
|
||||||
|
if (
|
||||||
|
typeof url === 'string' &&
|
||||||
|
url.length > 4 &&
|
||||||
|
url.indexOf('.') !== -1
|
||||||
|
) {
|
||||||
|
// prevent trailing / from being stripped during typing
|
||||||
|
let lastChar = '';
|
||||||
|
if (shouldKeepTrailingSlash && url.substring(url.length - 1) === '/') {
|
||||||
|
lastChar = '/';
|
||||||
|
}
|
||||||
|
return normalizeUrl(url, normalizeOptions) + lastChar;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
function isValidURL(data) {
|
function isValidURL(data) {
|
||||||
return isURL(data, { 'require_protocol': true });
|
return isURL(data, { 'require_protocol': true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidPhone(data) {
|
function makeRequired(validator) {
|
||||||
return isMobilePhone(data, 'en-US');
|
return (val) => !!val && validator(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default contain({
|
export default contain({
|
||||||
@ -67,22 +122,28 @@ export default contain({
|
|||||||
locale,
|
locale,
|
||||||
description,
|
description,
|
||||||
email,
|
email,
|
||||||
phone,
|
|
||||||
url,
|
url,
|
||||||
logo,
|
logo,
|
||||||
name,
|
company,
|
||||||
highlight
|
isHighlighted,
|
||||||
|
isFullStackCert,
|
||||||
|
isFrontEndCert,
|
||||||
|
isRemoteOk,
|
||||||
|
howToApply
|
||||||
} = form;
|
} = form;
|
||||||
return {
|
return {
|
||||||
position: formatValue(position, isAscii),
|
position: formatValue(position, makeRequired(isAscii)),
|
||||||
locale: formatValue(locale, isAscii),
|
locale: formatValue(locale, makeRequired(isAscii)),
|
||||||
description: formatValue(description, isAscii),
|
description: formatValue(description, makeRequired(helpers.identity)),
|
||||||
email: formatValue(email, isEmail),
|
email: formatValue(email, makeRequired(isEmail)),
|
||||||
phone: formatValue(phone, isValidPhone),
|
url: formatValue(formatUrl(url), isValidURL),
|
||||||
url: formatValue(url, isValidURL),
|
logo: formatValue(formatUrl(logo), isValidURL),
|
||||||
logo: formatValue(logo, isValidURL),
|
company: formatValue(company, makeRequired(isAscii)),
|
||||||
name: formatValue(name, isAscii),
|
isHighlighted: formatValue(isHighlighted, null, 'bool'),
|
||||||
highlight: formatValue(highlight, null, 'bool')
|
isFullStackCert: formatValue(isFullStackCert, null, 'bool'),
|
||||||
|
isFrontEndCert: formatValue(isFrontEndCert, null, 'bool'),
|
||||||
|
isRemoteOk: formatValue(isRemoteOk, null, 'bool'),
|
||||||
|
howToApply: formatValue(howToApply, makeRequired(isAscii))
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
subscribeOnWillMount() {
|
subscribeOnWillMount() {
|
||||||
@ -98,11 +159,14 @@ export default contain({
|
|||||||
locale: PropTypes.object,
|
locale: PropTypes.object,
|
||||||
description: PropTypes.object,
|
description: PropTypes.object,
|
||||||
email: PropTypes.object,
|
email: PropTypes.object,
|
||||||
phone: PropTypes.object,
|
|
||||||
url: PropTypes.object,
|
url: PropTypes.object,
|
||||||
logo: PropTypes.object,
|
logo: PropTypes.object,
|
||||||
name: PropTypes.object,
|
company: PropTypes.object,
|
||||||
highlight: PropTypes.object
|
isHighlighted: PropTypes.object,
|
||||||
|
isFullStackCert: PropTypes.object,
|
||||||
|
isFrontEndCert: PropTypes.object,
|
||||||
|
isRemoteOk: PropTypes.object,
|
||||||
|
howToApply: PropTypes.object
|
||||||
},
|
},
|
||||||
|
|
||||||
mixins: [History],
|
mixins: [History],
|
||||||
@ -124,29 +188,37 @@ export default contain({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
jobActions,
|
||||||
|
|
||||||
|
// form values
|
||||||
position,
|
position,
|
||||||
locale,
|
locale,
|
||||||
description,
|
description,
|
||||||
email,
|
email,
|
||||||
phone,
|
|
||||||
url,
|
url,
|
||||||
logo,
|
logo,
|
||||||
name,
|
company,
|
||||||
highlight,
|
isHighlighted,
|
||||||
jobActions
|
isFullStackCert,
|
||||||
|
isFrontEndCert,
|
||||||
|
isRemoteOk,
|
||||||
|
howToApply
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// sanitize user output
|
// sanitize user output
|
||||||
const jobValues = {
|
const jobValues = {
|
||||||
position: inHTMLData(position.value),
|
position: inHTMLData(position.value),
|
||||||
location: inHTMLData(locale.value),
|
locale: inHTMLData(locale.value),
|
||||||
description: inHTMLData(description.value),
|
description: inHTMLData(description.value),
|
||||||
email: inHTMLData(email.value),
|
email: inHTMLData(email.value),
|
||||||
phone: inHTMLData(phone.value),
|
url: formatUrl(uriInSingleQuotedAttr(url.value), false),
|
||||||
url: uriInSingleQuotedAttr(url.value),
|
logo: formatUrl(uriInSingleQuotedAttr(logo.value), false),
|
||||||
logo: uriInSingleQuotedAttr(logo.value),
|
company: inHTMLData(company.value),
|
||||||
name: inHTMLData(name.value),
|
isHighlighted: !!isHighlighted.value,
|
||||||
highlight: !!highlight.value
|
isFrontEndCert: !!isFrontEndCert.value,
|
||||||
|
isFullStackCert: !!isFullStackCert.value,
|
||||||
|
isRemoteOk: !!isRemoteOk.value,
|
||||||
|
howToApply: inHTMLData(howToApply.value)
|
||||||
};
|
};
|
||||||
|
|
||||||
const job = Object.keys(jobValues).reduce((accu, prop) => {
|
const job = Object.keys(jobValues).reduce((accu, prop) => {
|
||||||
@ -179,11 +251,14 @@ export default contain({
|
|||||||
locale,
|
locale,
|
||||||
description,
|
description,
|
||||||
email,
|
email,
|
||||||
phone,
|
|
||||||
url,
|
url,
|
||||||
logo,
|
logo,
|
||||||
name,
|
company,
|
||||||
highlight,
|
isHighlighted,
|
||||||
|
isFrontEndCert,
|
||||||
|
isFullStackCert,
|
||||||
|
isRemoteOk,
|
||||||
|
howToApply,
|
||||||
jobActions: { handleForm }
|
jobActions: { handleForm }
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { handleChange } = this;
|
const { handleChange } = this;
|
||||||
@ -193,22 +268,26 @@ export default contain({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Row>
|
<Row>
|
||||||
<Col>
|
<Col
|
||||||
<Well className='text-center'>
|
md={ 10 }
|
||||||
<h1>Create Your Job Post</h1>
|
mdOffset={ 1 }>
|
||||||
|
<Panel className='text-center'>
|
||||||
<form
|
<form
|
||||||
className='form-horizontal'
|
className='form-horizontal'
|
||||||
onSubmit={ this.handleSubmit }>
|
onSubmit={ this.handleSubmit }>
|
||||||
|
|
||||||
<div className='spacer'>
|
<div className='spacer'>
|
||||||
<h2>Job Information</h2>
|
<h2>First, tell us about the position</h2>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
bsStyle={ position.bsStyle }
|
bsStyle={ position.bsStyle }
|
||||||
label='Position'
|
label='Job Title'
|
||||||
labelClassName={ labelClass }
|
labelClassName={ labelClass }
|
||||||
onChange={ (e) => handleChange('position', e) }
|
onChange={ (e) => handleChange('position', e) }
|
||||||
placeholder='Position'
|
placeholder={
|
||||||
|
'e.g. Full Stack Developer, Front End Developer, etc.'
|
||||||
|
}
|
||||||
|
required={ true }
|
||||||
type='text'
|
type='text'
|
||||||
value={ position.value }
|
value={ position.value }
|
||||||
wrapperClassName={ inputClass } />
|
wrapperClassName={ inputClass } />
|
||||||
@ -217,7 +296,8 @@ export default contain({
|
|||||||
label='Location'
|
label='Location'
|
||||||
labelClassName={ labelClass }
|
labelClassName={ labelClass }
|
||||||
onChange={ (e) => handleChange('locale', e) }
|
onChange={ (e) => handleChange('locale', e) }
|
||||||
placeholder='Location'
|
placeholder='e.g. San Francisco, Remote, etc.'
|
||||||
|
required={ true }
|
||||||
type='text'
|
type='text'
|
||||||
value={ locale.value }
|
value={ locale.value }
|
||||||
wrapperClassName={ inputClass } />
|
wrapperClassName={ inputClass } />
|
||||||
@ -226,48 +306,90 @@ export default contain({
|
|||||||
label='Description'
|
label='Description'
|
||||||
labelClassName={ labelClass }
|
labelClassName={ labelClass }
|
||||||
onChange={ (e) => handleChange('description', e) }
|
onChange={ (e) => handleChange('description', e) }
|
||||||
placeholder='Description'
|
required={ true }
|
||||||
rows='10'
|
rows='10'
|
||||||
type='textarea'
|
type='textarea'
|
||||||
value={ description.value }
|
value={ description.value }
|
||||||
wrapperClassName={ inputClass } />
|
wrapperClassName={ inputClass } />
|
||||||
|
<Input
|
||||||
<div className='divider'>
|
checked={ isFrontEndCert.value }
|
||||||
<h2>Company Information</h2>
|
label={ isFrontEndCopy }
|
||||||
|
onChange={
|
||||||
|
({ target: { checked } }) => handleForm({
|
||||||
|
isFrontEndCert: !!checked
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type='checkbox'
|
||||||
|
wrapperClassName={ checkboxClass } />
|
||||||
|
<Input
|
||||||
|
checked={ isFullStackCert.value }
|
||||||
|
label={ isFullStackCopy }
|
||||||
|
onChange={
|
||||||
|
({ target: { checked } }) => handleForm({
|
||||||
|
isFullStackCert: !!checked
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type='checkbox'
|
||||||
|
wrapperClassName={ checkboxClass } />
|
||||||
|
<Input
|
||||||
|
checked={ isRemoteOk.value }
|
||||||
|
label={ isRemoteCopy }
|
||||||
|
onChange={
|
||||||
|
({ target: { checked } }) => handleForm({
|
||||||
|
isRemoteOk: !!checked
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type='checkbox'
|
||||||
|
wrapperClassName={ checkboxClass } />
|
||||||
|
<Row>
|
||||||
|
<small>* { foo }</small>
|
||||||
|
</Row>
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<div>
|
||||||
|
<h2>How should they apply?</h2>
|
||||||
</div>
|
</div>
|
||||||
<Input
|
<Input
|
||||||
bsStyle={ name.bsStyle }
|
bsStyle={ howToApply.bsStyle }
|
||||||
|
label=' '
|
||||||
|
labelClassName={ labelClass }
|
||||||
|
onChange={ (e) => handleChange('howToApply', e) }
|
||||||
|
placeholder={ howToApplyCopy }
|
||||||
|
required={ true }
|
||||||
|
rows='2'
|
||||||
|
type='textarea'
|
||||||
|
value={ howToApply.value }
|
||||||
|
wrapperClassName={ inputClass } />
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<div className='spacer' />
|
||||||
|
<div>
|
||||||
|
<h2>Tell us about your organization</h2>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
bsStyle={ company.bsStyle }
|
||||||
label='Company Name'
|
label='Company Name'
|
||||||
labelClassName={ labelClass }
|
labelClassName={ labelClass }
|
||||||
onChange={ (e) => handleChange('name', e) }
|
onChange={ (e) => handleChange('company', e) }
|
||||||
placeholder='Foo, INC'
|
|
||||||
type='text'
|
type='text'
|
||||||
value={ name.value }
|
value={ company.value }
|
||||||
wrapperClassName={ inputClass } />
|
wrapperClassName={ inputClass } />
|
||||||
<Input
|
<Input
|
||||||
bsStyle={ email.bsStyle }
|
bsStyle={ email.bsStyle }
|
||||||
label='Email'
|
label='Email'
|
||||||
labelClassName={ labelClass }
|
labelClassName={ labelClass }
|
||||||
onChange={ (e) => handleChange('email', e) }
|
onChange={ (e) => handleChange('email', e) }
|
||||||
placeholder='Email'
|
placeholder='This is how we will contact you'
|
||||||
|
required={ true }
|
||||||
type='email'
|
type='email'
|
||||||
value={ email.value }
|
value={ email.value }
|
||||||
wrapperClassName={ inputClass } />
|
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
|
<Input
|
||||||
bsStyle={ url.bsStyle }
|
bsStyle={ url.bsStyle }
|
||||||
label='URL'
|
label='URL'
|
||||||
labelClassName={ labelClass }
|
labelClassName={ labelClass }
|
||||||
onChange={ (e) => handleChange('url', e) }
|
onChange={ (e) => handleChange('url', e) }
|
||||||
placeholder='http://freecatphotoapp.com'
|
placeholder='http://yourcompany.com'
|
||||||
type='url'
|
type='url'
|
||||||
value={ url.value }
|
value={ url.value }
|
||||||
wrapperClassName={ inputClass } />
|
wrapperClassName={ inputClass } />
|
||||||
@ -276,27 +398,48 @@ export default contain({
|
|||||||
label='Logo'
|
label='Logo'
|
||||||
labelClassName={ labelClass }
|
labelClassName={ labelClass }
|
||||||
onChange={ (e) => handleChange('logo', e) }
|
onChange={ (e) => handleChange('logo', e) }
|
||||||
placeholder='http://freecatphotoapp.com/logo.png'
|
placeholder='http://yourcompany.com/logo.png'
|
||||||
type='url'
|
type='url'
|
||||||
value={ logo.value }
|
value={ logo.value }
|
||||||
wrapperClassName={ inputClass } />
|
wrapperClassName={ inputClass } />
|
||||||
|
|
||||||
<div className='divider'>
|
<div className='spacer' />
|
||||||
|
<Well>
|
||||||
|
<div>
|
||||||
<h2>Make it stand out</h2>
|
<h2>Make it stand out</h2>
|
||||||
</div>
|
</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' />
|
<div className='spacer' />
|
||||||
<Row>
|
<Row>
|
||||||
<Col
|
<Col
|
||||||
|
md={ 6 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
Highlight this ad to give it extra attention.
|
||||||
|
<br />
|
||||||
|
Featured listings receive more clicks and more applications.
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<Input
|
||||||
|
bsSize='large'
|
||||||
|
bsStyle='success'
|
||||||
|
checked={ isHighlighted.value }
|
||||||
|
label={ hightlightCopy }
|
||||||
|
onChange={
|
||||||
|
({ target: { checked } }) => handleForm({
|
||||||
|
isHighlighted: !!checked
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type='checkbox'
|
||||||
|
wrapperClassName={
|
||||||
|
checkboxClass.replace('text-left', '')
|
||||||
|
} />
|
||||||
|
</Row>
|
||||||
|
</Well>
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
className='text-left'
|
||||||
lg={ 6 }
|
lg={ 6 }
|
||||||
lgOffset={ 3 }>
|
lgOffset={ 3 }>
|
||||||
<Button
|
<Button
|
||||||
@ -309,7 +452,7 @@ export default contain({
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</form>
|
</form>
|
||||||
</Well>
|
</Panel>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
|
39
common/app/routes/Jobs/components/NewJobCompleted.jsx
Normal file
39
common/app/routes/Jobs/components/NewJobCompleted.jsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { LinkContainer } from 'react-router-bootstrap';
|
||||||
|
import { Button, Panel, Col, Row } from 'react-bootstrap';
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
displayName: 'NewJobCompleted',
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className='text-center'>
|
||||||
|
<Panel>
|
||||||
|
<Row>
|
||||||
|
<h1>
|
||||||
|
Your Position has Been Submitted
|
||||||
|
</h1>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 6 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
We’ll review your listing and email you when it’s live.
|
||||||
|
<br />
|
||||||
|
Thank you for listing this job with Free Code Camp.
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<div className='spacer' />
|
||||||
|
<LinkContainer to={ '/jobs' }>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='large'
|
||||||
|
bsStyle='primary'>
|
||||||
|
Go to the job board
|
||||||
|
</Button>
|
||||||
|
</LinkContainer>
|
||||||
|
</Panel>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -1,14 +1,85 @@
|
|||||||
// import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
|
import { Lifecycle } from 'react-router';
|
||||||
|
import { Panel, Button, Row, Col } from 'react-bootstrap';
|
||||||
import { contain } from 'thundercats-react';
|
import { contain } from 'thundercats-react';
|
||||||
|
|
||||||
import ShowJob from './ShowJob.jsx';
|
import ShowJob from './ShowJob.jsx';
|
||||||
|
import JobNotFound from './JobNotFound.jsx';
|
||||||
|
|
||||||
export default contain(
|
export default contain(
|
||||||
{
|
{
|
||||||
store: 'JobsStore',
|
store: 'JobsStore',
|
||||||
actions: 'JobActions',
|
actions: [
|
||||||
|
'appActions',
|
||||||
|
'jobActions'
|
||||||
|
],
|
||||||
map({ form: job = {} }) {
|
map({ form: job = {} }) {
|
||||||
return { job };
|
return { job };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ShowJob
|
React.createClass({
|
||||||
|
displayName: 'Preview',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
appActions: PropTypes.object,
|
||||||
|
job: PropTypes.object,
|
||||||
|
jobActions: PropTypes.object
|
||||||
|
},
|
||||||
|
|
||||||
|
mixins: [Lifecycle],
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { appActions, job } = this.props;
|
||||||
|
// redirect user in client
|
||||||
|
if (!job || !job.position || !job.description) {
|
||||||
|
appActions.updateRoute('/jobs/new');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
routerWillLeave() {
|
||||||
|
const { jobActions } = this.props;
|
||||||
|
jobActions.clearPromo();
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { appActions, job, jobActions } = this.props;
|
||||||
|
|
||||||
|
if (!job || !job.position || !job.description) {
|
||||||
|
return <JobNotFound />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ShowJob job={ job } />
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 10 }
|
||||||
|
mdOffset={ 1 }
|
||||||
|
xs={ 12 }>
|
||||||
|
<Panel>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
className='signup-btn'
|
||||||
|
onClick={ () => {
|
||||||
|
jobActions.clearSavedForm();
|
||||||
|
jobActions.saveJobToDb({
|
||||||
|
goTo: '/jobs/new/check-out',
|
||||||
|
job
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
|
||||||
|
Looks great! Let's Check Out
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
onClick={ () => appActions.goBack() } >
|
||||||
|
Head back and make edits
|
||||||
|
</Button>
|
||||||
|
</Panel>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
|
import React, { createClass } from 'react';
|
||||||
|
import { History } from 'react-router';
|
||||||
import { contain } from 'thundercats-react';
|
import { contain } from 'thundercats-react';
|
||||||
|
|
||||||
import ShowJob from './ShowJob.jsx';
|
import ShowJob from './ShowJob.jsx';
|
||||||
|
import JobNotFound from './JobNotFound.jsx';
|
||||||
|
import { isJobValid } from '../utils';
|
||||||
|
|
||||||
export default contain(
|
export default contain(
|
||||||
{
|
{
|
||||||
@ -20,5 +25,26 @@ export default contain(
|
|||||||
return job.id !== id;
|
return job.id !== id;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ShowJob
|
createClass({
|
||||||
|
displayName: 'Show',
|
||||||
|
|
||||||
|
mixins: [History],
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { job } = this.props;
|
||||||
|
// redirect user in client
|
||||||
|
if (!isJobValid(job)) {
|
||||||
|
this.history.pushState(null, '/jobs');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { job } = this.props;
|
||||||
|
|
||||||
|
if (!isJobValid(job)) {
|
||||||
|
return <JobNotFound />;
|
||||||
|
}
|
||||||
|
return <ShowJob { ...this.props }/>;
|
||||||
|
}
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import { Row, Thumbnail, Panel, Well } from 'react-bootstrap';
|
import { Well, Row, Col, Thumbnail, Panel } from 'react-bootstrap';
|
||||||
import moment from 'moment';
|
import urlRegexFactory from 'url-regex';
|
||||||
|
|
||||||
|
const urlRegex = urlRegexFactory();
|
||||||
|
const defaultImage =
|
||||||
|
'https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png';
|
||||||
|
|
||||||
const thumbnailStyle = {
|
const thumbnailStyle = {
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
@ -8,6 +12,12 @@ const thumbnailStyle = {
|
|||||||
maxWidth: '100px'
|
maxWidth: '100px'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function addATags(text) {
|
||||||
|
return text.replace(urlRegex, function(match) {
|
||||||
|
return `<a href=${match}>${match}</a>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default React.createClass({
|
export default React.createClass({
|
||||||
displayName: 'ShowJob',
|
displayName: 'ShowJob',
|
||||||
propTypes: {
|
propTypes: {
|
||||||
@ -36,30 +46,69 @@ export default React.createClass({
|
|||||||
city,
|
city,
|
||||||
company,
|
company,
|
||||||
state,
|
state,
|
||||||
email,
|
locale,
|
||||||
phone,
|
description,
|
||||||
postedOn,
|
howToApply
|
||||||
description
|
|
||||||
} = job;
|
} = job;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Row>
|
<Row>
|
||||||
<Well>
|
<Col
|
||||||
<Thumbnail
|
md={ 10 }
|
||||||
alt={ company + 'company logo' }
|
mdOffset={ 1 }
|
||||||
src={ logo }
|
xs={ 12 }>
|
||||||
style={ thumbnailStyle } />
|
|
||||||
<Panel>
|
<Panel>
|
||||||
Position: { position }
|
<Row>
|
||||||
Location: { city }, { state }
|
<h2 className='text-center'>
|
||||||
|
{ company }
|
||||||
|
</h2>
|
||||||
|
</Row>
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 2 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<Thumbnail
|
||||||
|
alt={ logo ? company + 'company logo' : 'stock image' }
|
||||||
|
src={ logo || defaultImage }
|
||||||
|
style={ thumbnailStyle } />
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
md={ 4 }>
|
||||||
|
|
||||||
|
<bold>Position: </bold> { position || 'N/A' }
|
||||||
<br />
|
<br />
|
||||||
Contact: { email || phone || 'N/A' }
|
<bold>Location: </bold>
|
||||||
<br />
|
{ locale ? locale : `${city}, ${state}` }
|
||||||
Posted On: { moment(postedOn).format('MMMM Do, YYYY') }
|
</Col>
|
||||||
</Panel>
|
</Row>
|
||||||
|
<div className='spacer' />
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 6 }
|
||||||
|
mdOffset={ 3 }
|
||||||
|
style={{ whiteSpace: 'pre-line' }}
|
||||||
|
xs={ 12 }>
|
||||||
<p>{ description }</p>
|
<p>{ description }</p>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Well>
|
||||||
|
<Row>
|
||||||
|
<Col
|
||||||
|
md={ 6 }
|
||||||
|
mdOffset={ 3 }>
|
||||||
|
<bold>How do I apply?</bold>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<span dangerouslySetInnerHTML={{
|
||||||
|
__html: addATags(howToApply)
|
||||||
|
}} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</Well>
|
</Well>
|
||||||
|
</Panel>
|
||||||
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
33
common/app/routes/Jobs/components/TwitterBtn.jsx
Normal file
33
common/app/routes/Jobs/components/TwitterBtn.jsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React, { createClass, PropTypes } from 'react';
|
||||||
|
import { Button } from 'react-bootstrap';
|
||||||
|
|
||||||
|
const followLink = 'https://twitter.com/intent/follow?' +
|
||||||
|
'ref_src=twsrc%5Etfw&region=follow_link&screen_name=CamperJobs&' +
|
||||||
|
'amp;tw_p=followbutton';
|
||||||
|
|
||||||
|
function commify(count) {
|
||||||
|
return Number(count).toLocaleString('en');
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createClass({
|
||||||
|
|
||||||
|
displayName: 'FollowButton',
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
count: PropTypes.number
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { count } = this.props;
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
block={ true }
|
||||||
|
bsSize='large'
|
||||||
|
bsStyle='primary'
|
||||||
|
href={ followLink }
|
||||||
|
target='__blank'>
|
||||||
|
Join { commify(count) } followers who see our job postings on Twitter.
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -1,6 +1,8 @@
|
|||||||
import { Actions } from 'thundercats';
|
import { Actions } from 'thundercats';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
import debugFactory from 'debug';
|
import debugFactory from 'debug';
|
||||||
|
import { jsonp$ } from '../../../../utils/jsonp$';
|
||||||
|
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||||
|
|
||||||
const debug = debugFactory('freecc:jobs:actions');
|
const debug = debugFactory('freecc:jobs:actions');
|
||||||
const assign = Object.assign;
|
const assign = Object.assign;
|
||||||
@ -31,6 +33,7 @@ export default Actions({
|
|||||||
},
|
},
|
||||||
setError: null,
|
setError: null,
|
||||||
getJob: null,
|
getJob: null,
|
||||||
|
saveJobToDb: null,
|
||||||
getJobs(params) {
|
getJobs(params) {
|
||||||
return { params };
|
return { params };
|
||||||
},
|
},
|
||||||
@ -56,12 +59,47 @@ export default Actions({
|
|||||||
},
|
},
|
||||||
saveForm: null,
|
saveForm: null,
|
||||||
getSavedForm: null,
|
getSavedForm: null,
|
||||||
|
clearSavedForm: null,
|
||||||
setForm(form) {
|
setForm(form) {
|
||||||
return { form };
|
return { form };
|
||||||
|
},
|
||||||
|
getFollowers: null,
|
||||||
|
setFollowersCount(numOfFollowers) {
|
||||||
|
return { numOfFollowers };
|
||||||
|
},
|
||||||
|
setPromoCode({ target: { value = '' }} = {}) {
|
||||||
|
return { promoCode: value.replace(/[^\d\w\s]/, '') };
|
||||||
|
},
|
||||||
|
applyCode: null,
|
||||||
|
clearPromo(foo, undef) {
|
||||||
|
return {
|
||||||
|
price: undef,
|
||||||
|
buttonId: undef,
|
||||||
|
discountAmount: undef,
|
||||||
|
promoCode: undef,
|
||||||
|
promoApplied: false,
|
||||||
|
promoName: undef
|
||||||
|
};
|
||||||
|
},
|
||||||
|
applyPromo({
|
||||||
|
fullPrice: price,
|
||||||
|
buttonId,
|
||||||
|
discountAmount,
|
||||||
|
code: promoCode,
|
||||||
|
name: promoName
|
||||||
|
} = {}) {
|
||||||
|
return {
|
||||||
|
price,
|
||||||
|
buttonId,
|
||||||
|
discountAmount,
|
||||||
|
promoCode,
|
||||||
|
promoApplied: true,
|
||||||
|
promoName
|
||||||
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.refs({ displayName: 'JobActions' })
|
.refs({ displayName: 'JobActions' })
|
||||||
.init(({ instance: jobActions, args: [services] }) => {
|
.init(({ instance: jobActions, args: [cat, services] }) => {
|
||||||
jobActions.getJobs.subscribe(() => {
|
jobActions.getJobs.subscribe(() => {
|
||||||
services.read('jobs', null, null, (err, jobs) => {
|
services.read('jobs', null, null, (err, jobs) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
@ -100,5 +138,55 @@ export default Actions({
|
|||||||
jobActions.setForm(job);
|
jobActions.setForm(job);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jobActions.clearSavedForm.subscribe(() => {
|
||||||
|
store.remove('newJob');
|
||||||
|
});
|
||||||
|
|
||||||
|
jobActions.saveJobToDb.subscribe(({ goTo, job }) => {
|
||||||
|
const appActions = cat.getActions('appActions');
|
||||||
|
services.create('jobs', { job }, null, (err, job) => {
|
||||||
|
if (err) {
|
||||||
|
debug('job services experienced an issue', err);
|
||||||
|
return jobActions.setError(err);
|
||||||
|
}
|
||||||
|
jobActions.setJobs({ job });
|
||||||
|
appActions.updateRoute(goTo);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
jobActions.getFollowers.subscribe(() => {
|
||||||
|
const url = 'https://cdn.syndication.twimg.com/widgets/followbutton/' +
|
||||||
|
'info.json?lang=en&screen_names=CamperJobs' +
|
||||||
|
'&callback=JSONPCallback';
|
||||||
|
|
||||||
|
jsonp$(url)
|
||||||
|
.map(({ response }) => {
|
||||||
|
return response[0]['followers_count'];
|
||||||
|
})
|
||||||
|
.subscribe(
|
||||||
|
count => jobActions.setFollowersCount(count),
|
||||||
|
err => jobActions.setError(err)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
jobActions.applyCode.subscribe(({ code = '', type = null}) => {
|
||||||
|
const body = { code: code.replace(/[^\d\w\s]/, '') };
|
||||||
|
if (type) {
|
||||||
|
body.type = type;
|
||||||
|
}
|
||||||
|
postJSON$('/api/promos/getButton', body)
|
||||||
|
.pluck('response')
|
||||||
|
.subscribe(
|
||||||
|
({ promo }) => {
|
||||||
|
if (promo && promo.buttonId) {
|
||||||
|
jobActions.applyPromo(promo);
|
||||||
|
}
|
||||||
|
jobActions.setError(new Error('no promo found'));
|
||||||
|
},
|
||||||
|
jobActions.setError
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return jobActions;
|
return jobActions;
|
||||||
});
|
});
|
||||||
|
@ -19,7 +19,11 @@ export default Store({
|
|||||||
openModal,
|
openModal,
|
||||||
closeModal,
|
closeModal,
|
||||||
handleForm,
|
handleForm,
|
||||||
setForm
|
setForm,
|
||||||
|
setFollowersCount,
|
||||||
|
setPromoCode,
|
||||||
|
applyPromo,
|
||||||
|
clearPromo
|
||||||
} = cat.getActions('JobActions');
|
} = cat.getActions('JobActions');
|
||||||
const register = createRegistrar(jobsStore);
|
const register = createRegistrar(jobsStore);
|
||||||
register(setter(setJobs));
|
register(setter(setJobs));
|
||||||
@ -27,6 +31,10 @@ export default Store({
|
|||||||
register(setter(openModal));
|
register(setter(openModal));
|
||||||
register(setter(closeModal));
|
register(setter(closeModal));
|
||||||
register(setter(setForm));
|
register(setter(setForm));
|
||||||
|
register(setter(setPromoCode));
|
||||||
|
register(setter(applyPromo));
|
||||||
|
register(setter(clearPromo));
|
||||||
|
register(setter(setFollowersCount));
|
||||||
|
|
||||||
register(transformer(findJob));
|
register(transformer(findJob));
|
||||||
register(handleForm);
|
register(handleForm);
|
||||||
|
@ -2,6 +2,8 @@ import Jobs from './components/Jobs.jsx';
|
|||||||
import NewJob from './components/NewJob.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';
|
import Preview from './components/Preview.jsx';
|
||||||
|
import GoToPayPal from './components/GoToPayPal.jsx';
|
||||||
|
import NewJobCompleted from './components/NewJobCompleted.jsx';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* index: /jobs list jobs
|
* index: /jobs list jobs
|
||||||
@ -19,6 +21,12 @@ export default {
|
|||||||
}, {
|
}, {
|
||||||
path: 'jobs/new/preview',
|
path: 'jobs/new/preview',
|
||||||
component: Preview
|
component: Preview
|
||||||
|
}, {
|
||||||
|
path: 'jobs/new/check-out',
|
||||||
|
component: GoToPayPal
|
||||||
|
}, {
|
||||||
|
path: 'jobs/new/completed',
|
||||||
|
component: NewJobCompleted
|
||||||
}, {
|
}, {
|
||||||
path: 'jobs/:id',
|
path: 'jobs/:id',
|
||||||
component: Show
|
component: Show
|
||||||
|
@ -20,3 +20,10 @@ export function getDefaults(type, value) {
|
|||||||
}
|
}
|
||||||
return Object.assign({}, defaults[type]);
|
return Object.assign({}, defaults[type]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isJobValid(job) {
|
||||||
|
return job &&
|
||||||
|
!job.isFilled &&
|
||||||
|
job.isApproved &&
|
||||||
|
job.isPaid;
|
||||||
|
}
|
||||||
|
@ -22,7 +22,8 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string"
|
"type": "string",
|
||||||
|
"required": true
|
||||||
},
|
},
|
||||||
"phone": {
|
"phone": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@ -36,24 +37,57 @@
|
|||||||
"country": {
|
"country": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"locale": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "format: city, state"
|
||||||
|
},
|
||||||
"location": {
|
"location": {
|
||||||
"type": "geopoint"
|
"type": "geopoint",
|
||||||
|
"description": "location in lat, long"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"isApproved": {
|
"isApproved": {
|
||||||
"type": "boolean"
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
},
|
},
|
||||||
"isHighlighted": {
|
"isHighlighted": {
|
||||||
"type": "boolean"
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
},
|
},
|
||||||
"isPaid": {
|
"isPaid": {
|
||||||
"type": "boolean"
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"isFilled": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
},
|
},
|
||||||
"postedOn": {
|
"postedOn": {
|
||||||
"type": "date",
|
"type": "date",
|
||||||
"defaultFn": "now"
|
"defaultFn": "now"
|
||||||
|
},
|
||||||
|
"isFrontEndCert": {
|
||||||
|
"type": "boolean",
|
||||||
|
"defaut": false,
|
||||||
|
"description": "Camper must be front end certified to apply"
|
||||||
|
},
|
||||||
|
"isFullStackCert": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Camper must be full stack certified to apply"
|
||||||
|
},
|
||||||
|
"isRemoteOk": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Camper may work remotely"
|
||||||
|
},
|
||||||
|
"howToApply": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "How do campers apply to job"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"validations": [],
|
"validations": [],
|
||||||
|
47
common/models/promo.js
Normal file
47
common/models/promo.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { isAlphanumeric } from 'validator';
|
||||||
|
|
||||||
|
export default function promo(Promo) {
|
||||||
|
Promo.getButton = function getButton(code, type = 'isNot') {
|
||||||
|
if (
|
||||||
|
!isAlphanumeric(code) &&
|
||||||
|
type &&
|
||||||
|
!isAlphanumeric(type)
|
||||||
|
) {
|
||||||
|
return Promise.reject(new Error(
|
||||||
|
'Code or Type should be an alphanumeric'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
where: {
|
||||||
|
and: [{ code }, { type }]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return Promo.findOne(query);
|
||||||
|
};
|
||||||
|
|
||||||
|
Promo.remoteMethod(
|
||||||
|
'getButton',
|
||||||
|
{
|
||||||
|
description: 'Get button id for promocode',
|
||||||
|
accepts: [
|
||||||
|
{
|
||||||
|
arg: 'code',
|
||||||
|
type: 'string',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
arg: 'type',
|
||||||
|
type: 'string'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
returns: [
|
||||||
|
{
|
||||||
|
arg: 'promo',
|
||||||
|
type: 'object'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
59
common/models/promo.json
Normal file
59
common/models/promo.json
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "promo",
|
||||||
|
"base": "PersistedModel",
|
||||||
|
"strict": true,
|
||||||
|
"idInjection": true,
|
||||||
|
"trackChanges": false,
|
||||||
|
"properties": {
|
||||||
|
"code": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "The code to unlock the promotional discount"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "The name of the discount"
|
||||||
|
},
|
||||||
|
"buttonId": {
|
||||||
|
"type": "string",
|
||||||
|
"required": true,
|
||||||
|
"description": "The id of paypal button"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A selector of different types of buttons for the same discount"
|
||||||
|
},
|
||||||
|
"fullPrice": {
|
||||||
|
"type": "number",
|
||||||
|
"required": true,
|
||||||
|
"description": "The original amount"
|
||||||
|
},
|
||||||
|
"discountAmount": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The amount of the discount if applicable"
|
||||||
|
},
|
||||||
|
"discountPercent": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "The amount of discount as a percentage if applicable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"validations": [],
|
||||||
|
"relations": {},
|
||||||
|
"acls": [
|
||||||
|
{
|
||||||
|
"accessType": "*",
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"permission": "DENY"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"accessType": "EXECUTE",
|
||||||
|
"principalType": "ROLE",
|
||||||
|
"principalId": "$everyone",
|
||||||
|
"permission": "ALLOW",
|
||||||
|
"property": "getButton"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"methods": []
|
||||||
|
}
|
@ -255,6 +255,7 @@ export function postJSON$(url, body) {
|
|||||||
url,
|
url,
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
responseType: 'json',
|
||||||
headers: { 'Content-Type': 'application/json' }
|
headers: { 'Content-Type': 'application/json' }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -277,10 +278,7 @@ export function get$(url) {
|
|||||||
* @returns {Observable} The observable sequence which contains the parsed JSON
|
* @returns {Observable} The observable sequence which contains the parsed JSON
|
||||||
*/
|
*/
|
||||||
export function getJSON$(url) {
|
export function getJSON$(url) {
|
||||||
if (!root.JSON && typeof root.JSON.parse !== 'function') {
|
return ajax$({ url: url, responseType: 'json' }).map(function(x) {
|
||||||
throw new TypeError('JSON is not supported in your runtime.');
|
|
||||||
}
|
|
||||||
return ajax$({url: url, responseType: 'json'}).map(function(x) {
|
|
||||||
return x.response;
|
return x.response;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
77
common/utils/jsonp$.js
Normal file
77
common/utils/jsonp$.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import { AnonymousObservable, Disposable } from 'rx';
|
||||||
|
|
||||||
|
const root = typeof window !== 'undefined' ? window : {};
|
||||||
|
const trash = 'document' in root && root.document.createElement('div');
|
||||||
|
|
||||||
|
function destroy(element) {
|
||||||
|
trash.appendChild(element);
|
||||||
|
trash.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jsonp$(options) {
|
||||||
|
let id = 0;
|
||||||
|
if (typeof options === 'string') {
|
||||||
|
options = { url: options };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AnonymousObservable(function(o) {
|
||||||
|
const settings = Object.assign(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
jsonp: 'JSONPCallback',
|
||||||
|
async: true,
|
||||||
|
jsonpCallback: 'rxjsjsonpCallbackscallback_' + (id++).toString(36)
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
let script = root.document.createElement('script');
|
||||||
|
script.type = 'text/javascript';
|
||||||
|
script.async = settings.async;
|
||||||
|
script.src = settings.url.replace(settings.jsonp, settings.jsonpCallback);
|
||||||
|
|
||||||
|
root[settings.jsonpCallback] = function(data) {
|
||||||
|
root[settings.jsonpCallback].called = true;
|
||||||
|
root[settings.jsonpCallback].data = data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = function(e) {
|
||||||
|
if (e.type === 'load' && !root[settings.jsonpCallback].called) {
|
||||||
|
e = { type: 'error' };
|
||||||
|
}
|
||||||
|
const status = e.type === 'error' ? 400 : 200;
|
||||||
|
const data = root[settings.jsonpCallback].data;
|
||||||
|
|
||||||
|
if (status === 200) {
|
||||||
|
o.onNext({
|
||||||
|
status: status,
|
||||||
|
responseType: 'jsonp',
|
||||||
|
response: data,
|
||||||
|
originalEvent: e
|
||||||
|
});
|
||||||
|
|
||||||
|
o.onCompleted();
|
||||||
|
} else {
|
||||||
|
o.onError({
|
||||||
|
type: 'error',
|
||||||
|
status: status,
|
||||||
|
originalEvent: e
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
script.onload = script.onreadystatechanged = script.onerror = handler;
|
||||||
|
|
||||||
|
const head = root.document.getElementsByTagName('head')[0] ||
|
||||||
|
root.document.documentElement;
|
||||||
|
|
||||||
|
head.insertBefore(script, head.firstChild);
|
||||||
|
|
||||||
|
return Disposable.create(() => {
|
||||||
|
script.onload = script.onreadystatechanged = script.onerror = null;
|
||||||
|
|
||||||
|
destroy(script);
|
||||||
|
script = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -59,11 +59,11 @@
|
|||||||
"gulp-eslint": "~0.9.0",
|
"gulp-eslint": "~0.9.0",
|
||||||
"gulp-inject": "~1.0.2",
|
"gulp-inject": "~1.0.2",
|
||||||
"gulp-jsonlint": "^1.1.0",
|
"gulp-jsonlint": "^1.1.0",
|
||||||
|
"gulp-less": "^3.0.3",
|
||||||
|
"gulp-minify-css": "~0.5.1",
|
||||||
"gulp-nodemon": "^2.0.3",
|
"gulp-nodemon": "^2.0.3",
|
||||||
"gulp-notify": "^2.2.0",
|
"gulp-notify": "^2.2.0",
|
||||||
"gulp-plumber": "^1.0.1",
|
"gulp-plumber": "^1.0.1",
|
||||||
"gulp-less": "^3.0.3",
|
|
||||||
"gulp-minify-css": "~0.5.1",
|
|
||||||
"gulp-reduce-file": "0.0.1",
|
"gulp-reduce-file": "0.0.1",
|
||||||
"gulp-rev": "^6.0.1",
|
"gulp-rev": "^6.0.1",
|
||||||
"gulp-rev-replace": "^0.4.2",
|
"gulp-rev-replace": "^0.4.2",
|
||||||
@ -89,6 +89,7 @@
|
|||||||
"node-slack": "0.0.7",
|
"node-slack": "0.0.7",
|
||||||
"node-uuid": "^1.4.3",
|
"node-uuid": "^1.4.3",
|
||||||
"nodemailer": "~1.3.0",
|
"nodemailer": "~1.3.0",
|
||||||
|
"normalize-url": "^1.3.1",
|
||||||
"object.assign": "^3.0.0",
|
"object.assign": "^3.0.0",
|
||||||
"passport-facebook": "^2.0.0",
|
"passport-facebook": "^2.0.0",
|
||||||
"passport-github": "^0.1.5",
|
"passport-github": "^0.1.5",
|
||||||
@ -103,6 +104,7 @@
|
|||||||
"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.git#freecodecamp",
|
"react-router": "https://github.com/BerkeleyTrue/react-router.git#freecodecamp",
|
||||||
|
"react-router-bootstrap": "^0.19.2",
|
||||||
"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",
|
||||||
@ -115,6 +117,7 @@
|
|||||||
"thundercats-react": "^0.3.0",
|
"thundercats-react": "^0.3.0",
|
||||||
"twit": "~1.1.20",
|
"twit": "~1.1.20",
|
||||||
"uglify-js": "~2.4.15",
|
"uglify-js": "~2.4.15",
|
||||||
|
"url-regex": "^3.0.0",
|
||||||
"validator": "^3.22.1",
|
"validator": "^3.22.1",
|
||||||
"webpack": "^1.9.12",
|
"webpack": "^1.9.12",
|
||||||
"xss-filters": "^1.2.6",
|
"xss-filters": "^1.2.6",
|
||||||
|
@ -7,3 +7,8 @@
|
|||||||
font-family: "Lato Light";
|
font-family: "Lato Light";
|
||||||
src: url(/fonts/Lato-Light.ttf) format("truetype");
|
src: url(/fonts/Lato-Light.ttf) format("truetype");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Lato Bold";
|
||||||
|
src: url(/fonts/Lato-Bold.ttf) format("truetype");
|
||||||
|
}
|
||||||
|
BIN
public/fonts/Lato-Bold.ttf
Executable file
BIN
public/fonts/Lato-Bold.ttf
Executable file
Binary file not shown.
@ -11,17 +11,25 @@ const debug = debugFactory('freecc:react-server');
|
|||||||
// add routes here as they slowly get reactified
|
// add routes here as they slowly get reactified
|
||||||
// remove their individual controllers
|
// remove their individual controllers
|
||||||
const routes = [
|
const routes = [
|
||||||
'/hikes',
|
|
||||||
'/hikes/*',
|
|
||||||
'/jobs',
|
'/jobs',
|
||||||
'/jobs/*'
|
'/jobs/*'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const devRoutes = [
|
||||||
|
'/hikes',
|
||||||
|
'/hikes/*'
|
||||||
|
];
|
||||||
|
|
||||||
export default function reactSubRouter(app) {
|
export default function reactSubRouter(app) {
|
||||||
var router = app.loopback.Router();
|
var router = app.loopback.Router();
|
||||||
|
|
||||||
|
// These routes are in production
|
||||||
|
routes.forEach((route) => {
|
||||||
|
router.get(route, serveReactApp);
|
||||||
|
});
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
routes.forEach(function(route) {
|
devRoutes.forEach(function(route) {
|
||||||
router.get(route, serveReactApp);
|
router.get(route, serveReactApp);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -35,7 +43,7 @@ export default function reactSubRouter(app) {
|
|||||||
// 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({ props}) {
|
.filter(function({ props }) {
|
||||||
if (!props) {
|
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();
|
||||||
|
@ -51,6 +51,10 @@
|
|||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": true
|
"public": true
|
||||||
},
|
},
|
||||||
|
"promo": {
|
||||||
|
"dataSource": "db",
|
||||||
|
"public": true
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"dataSource": "db",
|
"dataSource": "db",
|
||||||
"public": true
|
"public": true
|
||||||
|
@ -1,14 +1,36 @@
|
|||||||
|
const whereFilt = {
|
||||||
|
where: {
|
||||||
|
isFilled: false,
|
||||||
|
isPaid: true,
|
||||||
|
isApproved: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export default function getJobServices(app) {
|
export default function getJobServices(app) {
|
||||||
const { Job } = app.models;
|
const { Job } = app.models;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'jobs',
|
name: 'jobs',
|
||||||
read: (req, resource, params, config, cb) => {
|
create(req, resource, { job } = {}, body, config, cb) {
|
||||||
|
if (!job) {
|
||||||
|
return cb(new Error('job creation should get a job object'));
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(job, {
|
||||||
|
isPaid: false,
|
||||||
|
isApproved: false
|
||||||
|
});
|
||||||
|
|
||||||
|
Job.create(job, (err, savedJob) => {
|
||||||
|
cb(err, savedJob);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
read(req, resource, params, config, cb) {
|
||||||
const id = params ? params.id : null;
|
const id = params ? params.id : null;
|
||||||
if (id) {
|
if (id) {
|
||||||
return Job.findById(id, cb);
|
return Job.findById(id, cb);
|
||||||
}
|
}
|
||||||
Job.find({}, (err, jobs) => {
|
Job.find(whereFilt, (err, jobs) => {
|
||||||
cb(err, jobs);
|
cb(err, jobs);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ const protectedUserFields = {
|
|||||||
profiles: censor
|
profiles: censor
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function userServices(/* app */) {
|
export default function userServices() {
|
||||||
return {
|
return {
|
||||||
name: 'user',
|
name: 'user',
|
||||||
read: (req, resource, params, config, cb) => {
|
read: (req, resource, params, config, cb) => {
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
doctype html
|
doctype html
|
||||||
html(ng-app='profileValidation', lang='en')
|
html(lang='en')
|
||||||
head
|
head
|
||||||
if title
|
if title
|
||||||
title= title
|
title= title
|
||||||
else
|
else
|
||||||
title redirecting to | Free Code Camp
|
title Free Code Camp
|
||||||
include partials/small-head
|
include partials/react-stylesheets
|
||||||
body.top-and-bottom-margins(style='overflow: hidden')
|
body.top-and-bottom-margins(style='overflow: hidden')
|
||||||
.container
|
.container
|
||||||
include partials/flash
|
|
||||||
#fcc!= markup
|
#fcc!= markup
|
||||||
script!= state
|
script!= state
|
||||||
script(src=rev('/js', 'bundle.js'))
|
script(src=rev('/js', 'bundle.js'))
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
script(src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js")
|
link(rel='stylesheet', type='text/css' href='/css/lato.css')
|
||||||
script(src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/js/bootstrap.min.js")
|
|
||||||
link(rel='stylesheet', href='/bower_components/font-awesome/css/font-awesome.min.css')
|
link(rel='stylesheet', href='/bower_components/font-awesome/css/font-awesome.min.css')
|
||||||
link(rel='stylesheet', href='/css/main.css')
|
link(rel='stylesheet', href=rev('/css', 'main.css'))
|
||||||
link(rel='stylesheet', href='/css/Vimeo.css')
|
link(rel='stylesheet', href='/css/Vimeo.css')
|
||||||
// End **REQUIRED** includes
|
|
||||||
|
|
||||||
include meta
|
include meta
|
||||||
meta(charset='utf-8')
|
meta(charset='utf-8')
|
@ -1,14 +1,30 @@
|
|||||||
doctype html
|
doctype html
|
||||||
html(lang='en')
|
html(lang='en')
|
||||||
head
|
script(src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js")
|
||||||
include partials/small-head
|
script(src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/js/bootstrap.min.js")
|
||||||
body.top-and-bottom-margins
|
link(rel='stylesheet', href='/bower_components/font-awesome/css/font-awesome.min.css')
|
||||||
|
link(rel='stylesheet', href='/css/main.css')
|
||||||
|
link(rel='stylesheet', href='/css/Vimeo.css')
|
||||||
|
|
||||||
|
include partials/meta
|
||||||
|
meta(charset='utf-8')
|
||||||
|
meta(http-equiv='X-UA-Compatible', content='IE=edge')
|
||||||
|
meta(name='viewport', content='width=device-width, initial-scale=1.0')
|
||||||
|
meta(name='csrf-token', content=_csrf)
|
||||||
|
script.
|
||||||
|
(function(i,s,o,g,r,a,m){ i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||||
|
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||||
|
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||||
|
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||||
|
ga('create', 'UA-55446531-1', 'auto');
|
||||||
|
ga('require', 'displayfeatures');
|
||||||
|
ga('send', 'pageview');
|
||||||
|
body.top-and-bottom-margins
|
||||||
include partials/navbar
|
include partials/navbar
|
||||||
.container
|
.container
|
||||||
.row
|
.row
|
||||||
.panel.panel-info
|
.panel.panel-info
|
||||||
p redirecting you... please wait...
|
p redirecting you... please wait...
|
||||||
include partials/footer
|
|
||||||
script.
|
script.
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
window.location = 'http://freecodecamp.com'
|
window.location = 'http://freecodecamp.com'
|
||||||
|
Reference in New Issue
Block a user