Merge branch 'feature/jobs' into staging
This commit is contained in:
@ -22,6 +22,19 @@ const history = createHistory();
|
||||
const appLocation = createLocation(
|
||||
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
|
||||
app$({ history, location: appLocation })
|
||||
.flatMap(
|
||||
@ -36,6 +49,27 @@ app$({ history, location: appLocation })
|
||||
// redirects in the future
|
||||
({ 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 }) => {
|
||||
props.history = history;
|
||||
return Render(
|
||||
@ -49,7 +83,7 @@ app$({ history, location: appLocation })
|
||||
debug('react rendered');
|
||||
},
|
||||
err => {
|
||||
debug('an error has occured', err.stack);
|
||||
throw err;
|
||||
},
|
||||
() => {
|
||||
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,
|
||||
&:focus {
|
||||
color: @navbar-default-link-active-color;
|
||||
background-color: @navbar-default-link-active-bg;
|
||||
}
|
||||
}
|
||||
> .disabled > a {
|
||||
|
@ -9,6 +9,11 @@ html,body,div,span,a,li,td,th {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
bold {
|
||||
font-family: 'Lato-Bold', sans-serif;
|
||||
font-weight: Bold;
|
||||
}
|
||||
|
||||
li, .wrappable {
|
||||
white-space: pre; /* CSS 2.0 */
|
||||
white-space: pre-wrap; /* CSS 2.1 */
|
||||
@ -973,11 +978,11 @@ code {
|
||||
margin: 0!important;
|
||||
}
|
||||
|
||||
// gitter chat
|
||||
.gitter-chat-embed {
|
||||
z-index: 20000 !important;
|
||||
}
|
||||
|
||||
|
||||
//uncomment this to see the dimensions of all elements outlined in red
|
||||
//* {
|
||||
// border-color: red;
|
||||
@ -1087,3 +1092,4 @@ code {
|
||||
}
|
||||
|
||||
@import "chat.less";
|
||||
@import "jobs.less";
|
||||
|
@ -12,6 +12,6 @@ export default Cat()
|
||||
cat.register(HikesActions, null, services);
|
||||
cat.register(HikesStore, null, cat);
|
||||
|
||||
cat.register(JobActions, null, services);
|
||||
cat.register(JobActions, null, cat, services);
|
||||
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 { LinkContainer } from 'react-router-bootstrap';
|
||||
import {
|
||||
Col,
|
||||
CollapsibleNav,
|
||||
@ -11,16 +12,6 @@ import navLinks from './links.json';
|
||||
import FCCNavItem from './NavItem.jsx';
|
||||
|
||||
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 = (
|
||||
<a href='/'>
|
||||
@ -39,18 +30,40 @@ const toggleButton = (
|
||||
</button>
|
||||
);
|
||||
|
||||
export default class extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
export default React.createClass({
|
||||
displayName: 'Nav',
|
||||
|
||||
static displayName = 'Nav'
|
||||
static propTypes = {
|
||||
propTypes: {
|
||||
points: PropTypes.number,
|
||||
picture: PropTypes.string,
|
||||
signedIn: PropTypes.bool,
|
||||
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) {
|
||||
if (!username) {
|
||||
@ -62,7 +75,7 @@ export default class extends React.Component {
|
||||
[ { points } ]
|
||||
</NavItem>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
renderSignin(username, picture) {
|
||||
if (username) {
|
||||
@ -87,7 +100,7 @@ export default class extends React.Component {
|
||||
</FCCNavItem>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const { username, points, picture } = this.props;
|
||||
@ -103,12 +116,12 @@ export default class extends React.Component {
|
||||
className='hamburger-dropdown'
|
||||
navbar={ true }
|
||||
right={ true }>
|
||||
{ navElements }
|
||||
{ this.renderPoints(username, points)}
|
||||
{ this.renderLinks() }
|
||||
{ this.renderPoints(username, points) }
|
||||
{ this.renderSignin(username, picture) }
|
||||
</Nav>
|
||||
</CollapsibleNav>
|
||||
</Navbar>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -4,11 +4,14 @@ import BootstrapMixin from 'react-bootstrap/lib/BootstrapMixin';
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'FCCNavItem',
|
||||
|
||||
mixins: [BootstrapMixin],
|
||||
|
||||
propTypes: {
|
||||
active: React.PropTypes.bool,
|
||||
'aria-controls': React.PropTypes.string,
|
||||
children: React.PropTypes.node,
|
||||
className: React.PropTypes.string,
|
||||
disabled: React.PropTypes.bool,
|
||||
eventKey: React.PropTypes.any,
|
||||
href: React.PropTypes.string,
|
||||
@ -30,7 +33,11 @@ export default React.createClass({
|
||||
e.preventDefault();
|
||||
|
||||
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
|
||||
} = this.props;
|
||||
|
||||
let classes = {
|
||||
active,
|
||||
disabled
|
||||
};
|
||||
const linkClassName = classNames(className, {
|
||||
// 'active': active, we don't actually use the active class
|
||||
// but it is used for a11y below
|
||||
'disabled': disabled
|
||||
});
|
||||
|
||||
let linkProps = {
|
||||
role,
|
||||
@ -75,9 +83,9 @@ export default React.createClass({
|
||||
role='presentation'>
|
||||
<a
|
||||
{ ...linkProps }
|
||||
aria-selected={ active }
|
||||
aria-controls={ ariaControls }
|
||||
className={ className }>
|
||||
aria-selected={ active }
|
||||
className={ linkClassName }>
|
||||
{ children }
|
||||
</a>
|
||||
</li>
|
||||
|
@ -9,5 +9,6 @@
|
||||
"link": "/news"
|
||||
},{
|
||||
"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' })
|
||||
.init(({ instance: appActions, args: [services] }) => {
|
||||
|
@ -14,10 +14,10 @@ export default Store({
|
||||
value: initValue
|
||||
},
|
||||
init({ instance: appStore, args: [cat] }) {
|
||||
const { setUser, setTitle } = cat.getActions('appActions');
|
||||
const { updateRoute, setUser, setTitle } = cat.getActions('appActions');
|
||||
const register = createRegistrar(appStore);
|
||||
|
||||
register(setter(fromMany(setUser, setTitle)));
|
||||
register(setter(fromMany(setUser, setTitle, updateRoute)));
|
||||
|
||||
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 { contain } from 'thundercats-react';
|
||||
import { History } from 'react-router';
|
||||
import { Button, Jumbotron, Row } from 'react-bootstrap';
|
||||
import { Button, Panel, Row, Col } from 'react-bootstrap';
|
||||
|
||||
import CreateJobModal from './CreateJobModal.jsx';
|
||||
import ListJobs from './List.jsx';
|
||||
import TwitterBtn from './TwitterBtn.jsx';
|
||||
|
||||
export default contain(
|
||||
{
|
||||
store: 'jobsStore',
|
||||
fetchAction: 'jobActions.getJobs',
|
||||
actions: 'jobActions'
|
||||
actions: [
|
||||
'appActions',
|
||||
'jobActions'
|
||||
]
|
||||
},
|
||||
React.createClass({
|
||||
displayName: 'Jobs',
|
||||
|
||||
mixins: [History],
|
||||
|
||||
propTypes: {
|
||||
children: PropTypes.element,
|
||||
numOfFollowers: PropTypes.number,
|
||||
appActions: PropTypes.object,
|
||||
jobActions: PropTypes.object,
|
||||
jobs: PropTypes.array,
|
||||
showModal: PropTypes.bool
|
||||
},
|
||||
|
||||
handleJobClick(id) {
|
||||
componentDidMount() {
|
||||
const { jobActions } = this.props;
|
||||
jobActions.getFollowers();
|
||||
},
|
||||
|
||||
handleJobClick(id) {
|
||||
const { appActions, jobActions } = this.props;
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
jobActions.findJob(id);
|
||||
this.history.pushState(null, `/jobs/${id}`);
|
||||
appActions.updateRoute(`/jobs/${id}`);
|
||||
},
|
||||
|
||||
renderList(handleJobClick, jobs) {
|
||||
@ -54,37 +61,47 @@ export default contain(
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
numOfFollowers,
|
||||
jobs,
|
||||
showModal,
|
||||
jobActions
|
||||
appActions
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Panel>
|
||||
<Row>
|
||||
<Jumbotron>
|
||||
<h1>Free Code Camps' Job Board</h1>
|
||||
<p>
|
||||
Need to find the best junior developers?
|
||||
Want to find dedicated developers eager to join your company?
|
||||
Sign up now to post your job!
|
||||
</p>
|
||||
<Col
|
||||
md={ 10 }
|
||||
mdOffset= { 1 }
|
||||
xs={ 12 }>
|
||||
<h1 className='text-center'>
|
||||
Talented web developers with strong portfolios are eager
|
||||
to work for your company
|
||||
</h1>
|
||||
<Row className='text-center'>
|
||||
<Col
|
||||
sm={ 8 }
|
||||
smOffset={ 2 }
|
||||
xs={ 12 }>
|
||||
<Button
|
||||
bsSize='large'
|
||||
className='signup-btn'
|
||||
onClick={ jobActions.openModal }>
|
||||
Try the first month 20% off!
|
||||
className='signup-btn btn-block'
|
||||
onClick={ ()=> {
|
||||
appActions.updateRoute('/jobs/new');
|
||||
}}>
|
||||
Post a job: $200 for 30 days + weekly tweets
|
||||
</Button>
|
||||
</Jumbotron>
|
||||
<div className='button-spacer' />
|
||||
<TwitterBtn count={ numOfFollowers || 0 } />
|
||||
<div className='spacer' />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
{ this.renderChild(children, jobs) ||
|
||||
this.renderList(this.handleJobClick, jobs) }
|
||||
</Row>
|
||||
<CreateJobModal
|
||||
onHide={ jobActions.closeModal }
|
||||
showModal={ showModal } />
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
})
|
||||
|
@ -1,5 +1,6 @@
|
||||
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';
|
||||
|
||||
export default React.createClass({
|
||||
@ -10,62 +11,57 @@ export default React.createClass({
|
||||
jobs: PropTypes.array
|
||||
},
|
||||
|
||||
renderJobs(handleClick, jobs =[]) {
|
||||
const thumbnailStyle = {
|
||||
backgroundColor: 'white',
|
||||
maxHeight: '100px',
|
||||
maxWidth: '100px'
|
||||
};
|
||||
addLocation(locale) {
|
||||
if (!locale) {
|
||||
return null;
|
||||
}
|
||||
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,
|
||||
company,
|
||||
position,
|
||||
isHighlighted,
|
||||
description,
|
||||
logo,
|
||||
city,
|
||||
state,
|
||||
email,
|
||||
phone,
|
||||
postedOn
|
||||
},
|
||||
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>
|
||||
);
|
||||
postedOn,
|
||||
locale
|
||||
}) => {
|
||||
|
||||
const className = classnames({
|
||||
'jobs-list': true,
|
||||
'jobs-list-highlight': isHighlighted
|
||||
});
|
||||
|
||||
return (
|
||||
<Panel
|
||||
bsStyle={ isHighlighted ? 'warning' : 'default' }
|
||||
collapsible={ true }
|
||||
eventKey={ index }
|
||||
header={ header }
|
||||
key={ id }>
|
||||
<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 onClick={ () => handleClick(id) }>{ description }</p>
|
||||
</Well>
|
||||
</Panel>
|
||||
<ListGroupItem
|
||||
className={ className }
|
||||
key={ id }
|
||||
onClick={ () => handleClick(id) }>
|
||||
<div>
|
||||
<h4 style={{ display: 'inline-block' }}>
|
||||
<bold>{ company }</bold>
|
||||
{' '}
|
||||
<span className='hidden-xs hidden-sm'>
|
||||
- { position }
|
||||
</span>
|
||||
</h4>
|
||||
<h4
|
||||
className='pull-right'
|
||||
style={{ display: 'inline-block' }}>
|
||||
{ this.addLocation(locale) }
|
||||
{ moment(new Date(postedOn)).format('MMM Do') }
|
||||
</h4>
|
||||
</div>
|
||||
</ListGroupItem>
|
||||
);
|
||||
});
|
||||
},
|
||||
@ -77,9 +73,9 @@ export default React.createClass({
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<PanelGroup>
|
||||
<ListGroup>
|
||||
{ this.renderJobs(handleClick, jobs) }
|
||||
</PanelGroup>
|
||||
</ListGroup>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { helpers } from 'rx';
|
||||
import React, { PropTypes } from 'react';
|
||||
import { History } from 'react-router';
|
||||
import { contain } from 'thundercats-react';
|
||||
import debugFactory from 'debug';
|
||||
import dedent from 'dedent';
|
||||
import normalizeUrl from 'normalize-url';
|
||||
|
||||
import { getDefaults } from '../utils';
|
||||
|
||||
import {
|
||||
@ -14,13 +18,13 @@ import {
|
||||
Col,
|
||||
Input,
|
||||
Row,
|
||||
Panel,
|
||||
Well
|
||||
} from 'react-bootstrap';
|
||||
|
||||
import {
|
||||
isAscii,
|
||||
isEmail,
|
||||
isMobilePhone,
|
||||
isURL
|
||||
} from 'validator';
|
||||
|
||||
@ -31,12 +35,43 @@ const checkValidity = [
|
||||
'locale',
|
||||
'description',
|
||||
'email',
|
||||
'phone',
|
||||
'url',
|
||||
'logo',
|
||||
'name',
|
||||
'highlight'
|
||||
'company',
|
||||
'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') {
|
||||
const formated = getDefaults(type);
|
||||
@ -50,12 +85,32 @@ function formatValue(value, validator, type = 'string') {
|
||||
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) {
|
||||
return isURL(data, { 'require_protocol': true });
|
||||
}
|
||||
|
||||
function isValidPhone(data) {
|
||||
return isMobilePhone(data, 'en-US');
|
||||
function makeRequired(validator) {
|
||||
return (val) => !!val && validator(val);
|
||||
}
|
||||
|
||||
export default contain({
|
||||
@ -67,22 +122,28 @@ export default contain({
|
||||
locale,
|
||||
description,
|
||||
email,
|
||||
phone,
|
||||
url,
|
||||
logo,
|
||||
name,
|
||||
highlight
|
||||
company,
|
||||
isHighlighted,
|
||||
isFullStackCert,
|
||||
isFrontEndCert,
|
||||
isRemoteOk,
|
||||
howToApply
|
||||
} = 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')
|
||||
position: formatValue(position, makeRequired(isAscii)),
|
||||
locale: formatValue(locale, makeRequired(isAscii)),
|
||||
description: formatValue(description, makeRequired(helpers.identity)),
|
||||
email: formatValue(email, makeRequired(isEmail)),
|
||||
url: formatValue(formatUrl(url), isValidURL),
|
||||
logo: formatValue(formatUrl(logo), isValidURL),
|
||||
company: formatValue(company, makeRequired(isAscii)),
|
||||
isHighlighted: formatValue(isHighlighted, null, 'bool'),
|
||||
isFullStackCert: formatValue(isFullStackCert, null, 'bool'),
|
||||
isFrontEndCert: formatValue(isFrontEndCert, null, 'bool'),
|
||||
isRemoteOk: formatValue(isRemoteOk, null, 'bool'),
|
||||
howToApply: formatValue(howToApply, makeRequired(isAscii))
|
||||
};
|
||||
},
|
||||
subscribeOnWillMount() {
|
||||
@ -98,11 +159,14 @@ export default contain({
|
||||
locale: PropTypes.object,
|
||||
description: PropTypes.object,
|
||||
email: PropTypes.object,
|
||||
phone: PropTypes.object,
|
||||
url: PropTypes.object,
|
||||
logo: PropTypes.object,
|
||||
name: PropTypes.object,
|
||||
highlight: PropTypes.object
|
||||
company: PropTypes.object,
|
||||
isHighlighted: PropTypes.object,
|
||||
isFullStackCert: PropTypes.object,
|
||||
isFrontEndCert: PropTypes.object,
|
||||
isRemoteOk: PropTypes.object,
|
||||
howToApply: PropTypes.object
|
||||
},
|
||||
|
||||
mixins: [History],
|
||||
@ -124,29 +188,37 @@ export default contain({
|
||||
}
|
||||
|
||||
const {
|
||||
jobActions,
|
||||
|
||||
// form values
|
||||
position,
|
||||
locale,
|
||||
description,
|
||||
email,
|
||||
phone,
|
||||
url,
|
||||
logo,
|
||||
name,
|
||||
highlight,
|
||||
jobActions
|
||||
company,
|
||||
isHighlighted,
|
||||
isFullStackCert,
|
||||
isFrontEndCert,
|
||||
isRemoteOk,
|
||||
howToApply
|
||||
} = this.props;
|
||||
|
||||
// sanitize user output
|
||||
const jobValues = {
|
||||
position: inHTMLData(position.value),
|
||||
location: inHTMLData(locale.value),
|
||||
locale: 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
|
||||
url: formatUrl(uriInSingleQuotedAttr(url.value), false),
|
||||
logo: formatUrl(uriInSingleQuotedAttr(logo.value), false),
|
||||
company: inHTMLData(company.value),
|
||||
isHighlighted: !!isHighlighted.value,
|
||||
isFrontEndCert: !!isFrontEndCert.value,
|
||||
isFullStackCert: !!isFullStackCert.value,
|
||||
isRemoteOk: !!isRemoteOk.value,
|
||||
howToApply: inHTMLData(howToApply.value)
|
||||
};
|
||||
|
||||
const job = Object.keys(jobValues).reduce((accu, prop) => {
|
||||
@ -179,11 +251,14 @@ export default contain({
|
||||
locale,
|
||||
description,
|
||||
email,
|
||||
phone,
|
||||
url,
|
||||
logo,
|
||||
name,
|
||||
highlight,
|
||||
company,
|
||||
isHighlighted,
|
||||
isFrontEndCert,
|
||||
isFullStackCert,
|
||||
isRemoteOk,
|
||||
howToApply,
|
||||
jobActions: { handleForm }
|
||||
} = this.props;
|
||||
const { handleChange } = this;
|
||||
@ -193,22 +268,26 @@ export default contain({
|
||||
return (
|
||||
<div>
|
||||
<Row>
|
||||
<Col>
|
||||
<Well className='text-center'>
|
||||
<h1>Create Your Job Post</h1>
|
||||
<Col
|
||||
md={ 10 }
|
||||
mdOffset={ 1 }>
|
||||
<Panel className='text-center'>
|
||||
<form
|
||||
className='form-horizontal'
|
||||
onSubmit={ this.handleSubmit }>
|
||||
|
||||
<div className='spacer'>
|
||||
<h2>Job Information</h2>
|
||||
<h2>First, tell us about the position</h2>
|
||||
</div>
|
||||
<Input
|
||||
bsStyle={ position.bsStyle }
|
||||
label='Position'
|
||||
label='Job Title'
|
||||
labelClassName={ labelClass }
|
||||
onChange={ (e) => handleChange('position', e) }
|
||||
placeholder='Position'
|
||||
placeholder={
|
||||
'e.g. Full Stack Developer, Front End Developer, etc.'
|
||||
}
|
||||
required={ true }
|
||||
type='text'
|
||||
value={ position.value }
|
||||
wrapperClassName={ inputClass } />
|
||||
@ -217,7 +296,8 @@ export default contain({
|
||||
label='Location'
|
||||
labelClassName={ labelClass }
|
||||
onChange={ (e) => handleChange('locale', e) }
|
||||
placeholder='Location'
|
||||
placeholder='e.g. San Francisco, Remote, etc.'
|
||||
required={ true }
|
||||
type='text'
|
||||
value={ locale.value }
|
||||
wrapperClassName={ inputClass } />
|
||||
@ -226,48 +306,90 @@ export default contain({
|
||||
label='Description'
|
||||
labelClassName={ labelClass }
|
||||
onChange={ (e) => handleChange('description', e) }
|
||||
placeholder='Description'
|
||||
required={ true }
|
||||
rows='10'
|
||||
type='textarea'
|
||||
value={ description.value }
|
||||
wrapperClassName={ inputClass } />
|
||||
|
||||
<div className='divider'>
|
||||
<h2>Company Information</h2>
|
||||
<Input
|
||||
checked={ isFrontEndCert.value }
|
||||
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>
|
||||
<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'
|
||||
labelClassName={ labelClass }
|
||||
onChange={ (e) => handleChange('name', e) }
|
||||
placeholder='Foo, INC'
|
||||
onChange={ (e) => handleChange('company', e) }
|
||||
type='text'
|
||||
value={ name.value }
|
||||
value={ company.value }
|
||||
wrapperClassName={ inputClass } />
|
||||
<Input
|
||||
bsStyle={ email.bsStyle }
|
||||
label='Email'
|
||||
labelClassName={ labelClass }
|
||||
onChange={ (e) => handleChange('email', e) }
|
||||
placeholder='Email'
|
||||
placeholder='This is how we will contact you'
|
||||
required={ true }
|
||||
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'
|
||||
placeholder='http://yourcompany.com'
|
||||
type='url'
|
||||
value={ url.value }
|
||||
wrapperClassName={ inputClass } />
|
||||
@ -276,27 +398,48 @@ export default contain({
|
||||
label='Logo'
|
||||
labelClassName={ labelClass }
|
||||
onChange={ (e) => handleChange('logo', e) }
|
||||
placeholder='http://freecatphotoapp.com/logo.png'
|
||||
placeholder='http://yourcompany.com/logo.png'
|
||||
type='url'
|
||||
value={ logo.value }
|
||||
wrapperClassName={ inputClass } />
|
||||
|
||||
<div className='divider'>
|
||||
<div className='spacer' />
|
||||
<Well>
|
||||
<div>
|
||||
<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
|
||||
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 }
|
||||
lgOffset={ 3 }>
|
||||
<Button
|
||||
@ -309,7 +452,7 @@ export default contain({
|
||||
</Col>
|
||||
</Row>
|
||||
</form>
|
||||
</Well>
|
||||
</Panel>
|
||||
</Col>
|
||||
</Row>
|
||||
</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 ShowJob from './ShowJob.jsx';
|
||||
import JobNotFound from './JobNotFound.jsx';
|
||||
|
||||
export default contain(
|
||||
{
|
||||
store: 'JobsStore',
|
||||
actions: 'JobActions',
|
||||
actions: [
|
||||
'appActions',
|
||||
'jobActions'
|
||||
],
|
||||
map({ form: 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 ShowJob from './ShowJob.jsx';
|
||||
import JobNotFound from './JobNotFound.jsx';
|
||||
import { isJobValid } from '../utils';
|
||||
|
||||
export default contain(
|
||||
{
|
||||
@ -20,5 +25,26 @@ export default contain(
|
||||
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 { Row, Thumbnail, Panel, Well } from 'react-bootstrap';
|
||||
import moment from 'moment';
|
||||
import { Well, Row, Col, Thumbnail, Panel } from 'react-bootstrap';
|
||||
import urlRegexFactory from 'url-regex';
|
||||
|
||||
const urlRegex = urlRegexFactory();
|
||||
const defaultImage =
|
||||
'https://s3.amazonaws.com/freecodecamp/camper-image-placeholder.png';
|
||||
|
||||
const thumbnailStyle = {
|
||||
backgroundColor: 'white',
|
||||
@ -8,6 +12,12 @@ const thumbnailStyle = {
|
||||
maxWidth: '100px'
|
||||
};
|
||||
|
||||
function addATags(text) {
|
||||
return text.replace(urlRegex, function(match) {
|
||||
return `<a href=${match}>${match}</a>`;
|
||||
});
|
||||
}
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'ShowJob',
|
||||
propTypes: {
|
||||
@ -36,30 +46,69 @@ export default React.createClass({
|
||||
city,
|
||||
company,
|
||||
state,
|
||||
email,
|
||||
phone,
|
||||
postedOn,
|
||||
description
|
||||
locale,
|
||||
description,
|
||||
howToApply
|
||||
} = job;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Row>
|
||||
<Well>
|
||||
<Thumbnail
|
||||
alt={ company + 'company logo' }
|
||||
src={ logo }
|
||||
style={ thumbnailStyle } />
|
||||
<Col
|
||||
md={ 10 }
|
||||
mdOffset={ 1 }
|
||||
xs={ 12 }>
|
||||
<Panel>
|
||||
Position: { position }
|
||||
Location: { city }, { state }
|
||||
<Row>
|
||||
<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 />
|
||||
Contact: { email || phone || 'N/A' }
|
||||
<br />
|
||||
Posted On: { moment(postedOn).format('MMMM Do, YYYY') }
|
||||
</Panel>
|
||||
<bold>Location: </bold>
|
||||
{ locale ? locale : `${city}, ${state}` }
|
||||
</Col>
|
||||
</Row>
|
||||
<div className='spacer' />
|
||||
<Row>
|
||||
<Col
|
||||
md={ 6 }
|
||||
mdOffset={ 3 }
|
||||
style={{ whiteSpace: 'pre-line' }}
|
||||
xs={ 12 }>
|
||||
<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>
|
||||
</Panel>
|
||||
</Col>
|
||||
</Row>
|
||||
</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 store from 'store';
|
||||
import debugFactory from 'debug';
|
||||
import { jsonp$ } from '../../../../utils/jsonp$';
|
||||
import { postJSON$ } from '../../../../utils/ajax-stream';
|
||||
|
||||
const debug = debugFactory('freecc:jobs:actions');
|
||||
const assign = Object.assign;
|
||||
@ -31,6 +33,7 @@ export default Actions({
|
||||
},
|
||||
setError: null,
|
||||
getJob: null,
|
||||
saveJobToDb: null,
|
||||
getJobs(params) {
|
||||
return { params };
|
||||
},
|
||||
@ -56,12 +59,47 @@ export default Actions({
|
||||
},
|
||||
saveForm: null,
|
||||
getSavedForm: null,
|
||||
clearSavedForm: null,
|
||||
setForm(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' })
|
||||
.init(({ instance: jobActions, args: [services] }) => {
|
||||
.init(({ instance: jobActions, args: [cat, services] }) => {
|
||||
jobActions.getJobs.subscribe(() => {
|
||||
services.read('jobs', null, null, (err, jobs) => {
|
||||
if (err) {
|
||||
@ -100,5 +138,55 @@ export default Actions({
|
||||
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;
|
||||
});
|
||||
|
@ -19,7 +19,11 @@ export default Store({
|
||||
openModal,
|
||||
closeModal,
|
||||
handleForm,
|
||||
setForm
|
||||
setForm,
|
||||
setFollowersCount,
|
||||
setPromoCode,
|
||||
applyPromo,
|
||||
clearPromo
|
||||
} = cat.getActions('JobActions');
|
||||
const register = createRegistrar(jobsStore);
|
||||
register(setter(setJobs));
|
||||
@ -27,6 +31,10 @@ export default Store({
|
||||
register(setter(openModal));
|
||||
register(setter(closeModal));
|
||||
register(setter(setForm));
|
||||
register(setter(setPromoCode));
|
||||
register(setter(applyPromo));
|
||||
register(setter(clearPromo));
|
||||
register(setter(setFollowersCount));
|
||||
|
||||
register(transformer(findJob));
|
||||
register(handleForm);
|
||||
|
@ -2,6 +2,8 @@ import Jobs from './components/Jobs.jsx';
|
||||
import NewJob from './components/NewJob.jsx';
|
||||
import Show from './components/Show.jsx';
|
||||
import Preview from './components/Preview.jsx';
|
||||
import GoToPayPal from './components/GoToPayPal.jsx';
|
||||
import NewJobCompleted from './components/NewJobCompleted.jsx';
|
||||
|
||||
/*
|
||||
* index: /jobs list jobs
|
||||
@ -19,6 +21,12 @@ export default {
|
||||
}, {
|
||||
path: 'jobs/new/preview',
|
||||
component: Preview
|
||||
}, {
|
||||
path: 'jobs/new/check-out',
|
||||
component: GoToPayPal
|
||||
}, {
|
||||
path: 'jobs/new/completed',
|
||||
component: NewJobCompleted
|
||||
}, {
|
||||
path: 'jobs/:id',
|
||||
component: Show
|
||||
|
@ -20,3 +20,10 @@ export function getDefaults(type, value) {
|
||||
}
|
||||
return Object.assign({}, defaults[type]);
|
||||
}
|
||||
|
||||
export function isJobValid(job) {
|
||||
return job &&
|
||||
!job.isFilled &&
|
||||
job.isApproved &&
|
||||
job.isPaid;
|
||||
}
|
||||
|
@ -22,7 +22,8 @@
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
"phone": {
|
||||
"type": "string"
|
||||
@ -36,24 +37,57 @@
|
||||
"country": {
|
||||
"type": "string"
|
||||
},
|
||||
"locale": {
|
||||
"type": "string",
|
||||
"required": true,
|
||||
"description": "format: city, state"
|
||||
},
|
||||
"location": {
|
||||
"type": "geopoint"
|
||||
"type": "geopoint",
|
||||
"description": "location in lat, long"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"isApproved": {
|
||||
"type": "boolean"
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"isHighlighted": {
|
||||
"type": "boolean"
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"isPaid": {
|
||||
"type": "boolean"
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"isFilled": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"postedOn": {
|
||||
"type": "date",
|
||||
"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": [],
|
||||
|
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,
|
||||
body: JSON.stringify(body),
|
||||
method: 'POST',
|
||||
responseType: 'json',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
@ -277,10 +278,7 @@ export function get$(url) {
|
||||
* @returns {Observable} The observable sequence which contains the parsed JSON
|
||||
*/
|
||||
export function getJSON$(url) {
|
||||
if (!root.JSON && typeof root.JSON.parse !== 'function') {
|
||||
throw new TypeError('JSON is not supported in your runtime.');
|
||||
}
|
||||
return ajax$({url: url, responseType: 'json'}).map(function(x) {
|
||||
return ajax$({ url: url, responseType: 'json' }).map(function(x) {
|
||||
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-inject": "~1.0.2",
|
||||
"gulp-jsonlint": "^1.1.0",
|
||||
"gulp-less": "^3.0.3",
|
||||
"gulp-minify-css": "~0.5.1",
|
||||
"gulp-nodemon": "^2.0.3",
|
||||
"gulp-notify": "^2.2.0",
|
||||
"gulp-plumber": "^1.0.1",
|
||||
"gulp-less": "^3.0.3",
|
||||
"gulp-minify-css": "~0.5.1",
|
||||
"gulp-reduce-file": "0.0.1",
|
||||
"gulp-rev": "^6.0.1",
|
||||
"gulp-rev-replace": "^0.4.2",
|
||||
@ -89,6 +89,7 @@
|
||||
"node-slack": "0.0.7",
|
||||
"node-uuid": "^1.4.3",
|
||||
"nodemailer": "~1.3.0",
|
||||
"normalize-url": "^1.3.1",
|
||||
"object.assign": "^3.0.0",
|
||||
"passport-facebook": "^2.0.0",
|
||||
"passport-github": "^0.1.5",
|
||||
@ -103,6 +104,7 @@
|
||||
"react-bootstrap": "~0.23.7",
|
||||
"react-motion": "~0.1.0",
|
||||
"react-router": "https://github.com/BerkeleyTrue/react-router.git#freecodecamp",
|
||||
"react-router-bootstrap": "^0.19.2",
|
||||
"react-vimeo": "^0.0.3",
|
||||
"request": "~2.53.0",
|
||||
"rev-del": "^1.0.5",
|
||||
@ -115,6 +117,7 @@
|
||||
"thundercats-react": "^0.3.0",
|
||||
"twit": "~1.1.20",
|
||||
"uglify-js": "~2.4.15",
|
||||
"url-regex": "^3.0.0",
|
||||
"validator": "^3.22.1",
|
||||
"webpack": "^1.9.12",
|
||||
"xss-filters": "^1.2.6",
|
||||
|
@ -7,3 +7,8 @@
|
||||
font-family: "Lato Light";
|
||||
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
|
||||
// remove their individual controllers
|
||||
const routes = [
|
||||
'/hikes',
|
||||
'/hikes/*',
|
||||
'/jobs',
|
||||
'/jobs/*'
|
||||
];
|
||||
|
||||
const devRoutes = [
|
||||
'/hikes',
|
||||
'/hikes/*'
|
||||
];
|
||||
|
||||
export default function reactSubRouter(app) {
|
||||
var router = app.loopback.Router();
|
||||
|
||||
// These routes are in production
|
||||
routes.forEach((route) => {
|
||||
router.get(route, serveReactApp);
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
routes.forEach(function(route) {
|
||||
devRoutes.forEach(function(route) {
|
||||
router.get(route, serveReactApp);
|
||||
});
|
||||
}
|
||||
@ -35,7 +43,7 @@ export default function reactSubRouter(app) {
|
||||
// returns a router wrapped app
|
||||
app$({ location })
|
||||
// if react-router does not find a route send down the chain
|
||||
.filter(function({ props}) {
|
||||
.filter(function({ props }) {
|
||||
if (!props) {
|
||||
debug('react tried to find %s but got 404', location.pathname);
|
||||
return next();
|
||||
|
@ -51,6 +51,10 @@
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"promo": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
},
|
||||
"user": {
|
||||
"dataSource": "db",
|
||||
"public": true
|
||||
|
@ -1,14 +1,36 @@
|
||||
const whereFilt = {
|
||||
where: {
|
||||
isFilled: false,
|
||||
isPaid: true,
|
||||
isApproved: true
|
||||
}
|
||||
};
|
||||
|
||||
export default function getJobServices(app) {
|
||||
const { Job } = app.models;
|
||||
|
||||
return {
|
||||
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;
|
||||
if (id) {
|
||||
return Job.findById(id, cb);
|
||||
}
|
||||
Job.find({}, (err, jobs) => {
|
||||
Job.find(whereFilt, (err, jobs) => {
|
||||
cb(err, jobs);
|
||||
});
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ const protectedUserFields = {
|
||||
profiles: censor
|
||||
};
|
||||
|
||||
export default function userServices(/* app */) {
|
||||
export default function userServices() {
|
||||
return {
|
||||
name: 'user',
|
||||
read: (req, resource, params, config, cb) => {
|
||||
|
@ -1,14 +1,13 @@
|
||||
doctype html
|
||||
html(ng-app='profileValidation', lang='en')
|
||||
html(lang='en')
|
||||
head
|
||||
if title
|
||||
title= title
|
||||
else
|
||||
title redirecting to | Free Code Camp
|
||||
include partials/small-head
|
||||
title Free Code Camp
|
||||
include partials/react-stylesheets
|
||||
body.top-and-bottom-margins(style='overflow: hidden')
|
||||
.container
|
||||
include partials/flash
|
||||
#fcc!= markup
|
||||
script!= state
|
||||
script(src=rev('/js', 'bundle.js'))
|
||||
|
@ -1,9 +1,7 @@
|
||||
script(src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js")
|
||||
script(src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.4/js/bootstrap.min.js")
|
||||
link(rel='stylesheet', type='text/css' href='/css/lato.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')
|
||||
// End **REQUIRED** includes
|
||||
|
||||
include meta
|
||||
meta(charset='utf-8')
|
@ -1,14 +1,30 @@
|
||||
doctype html
|
||||
html(lang='en')
|
||||
head
|
||||
include partials/small-head
|
||||
body.top-and-bottom-margins
|
||||
script(src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js")
|
||||
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='/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
|
||||
.container
|
||||
.row
|
||||
.panel.panel-info
|
||||
p redirecting you... please wait...
|
||||
include partials/footer
|
||||
script.
|
||||
setTimeout(function() {
|
||||
window.location = 'http://freecodecamp.com'
|
||||
|
Reference in New Issue
Block a user