Merge branch 'staging' of https://github.com/freecodecamp/freecodecamp into staging

This commit is contained in:
Quincy Larson
2015-09-29 13:20:29 -07:00
28 changed files with 692 additions and 130 deletions

View File

@ -4,7 +4,7 @@ import React from 'react';
import Fetchr from 'fetchr';
import debugFactory from 'debug';
import { Router } from 'react-router';
import { history } from 'react-router/lib/BrowserHistory';
import { createLocation, createHistory } from 'history';
import { hydrate } from 'thundercats';
import { Render } from 'thundercats-react';
@ -18,21 +18,29 @@ const services = new Fetchr({
});
Rx.config.longStackSupport = !!debug.enabled;
const history = createHistory();
const appLocation = createLocation(
location.pathname + location.search
);
// returns an observable
app$(history)
app$({ history, location: appLocation })
.flatMap(
({ AppCat }) => {
// instantiate the cat with service
const appCat = AppCat(null, services);
// hydrate the stores
return hydrate(appCat, catState)
.map(() => appCat);
},
({ initialState }, appCat) => ({ initialState, appCat })
// not using nextLocation at the moment but will be used for
// redirects in the future
({ nextLocation, props }, appCat) => ({ nextLocation, props, appCat })
)
.flatMap(({ initialState, appCat }) => {
.flatMap(({ props, appCat }) => {
props.history = history;
return Render(
appCat,
React.createElement(Router, initialState),
React.createElement(Router, props),
DOMContianer
);
})

View File

@ -1,3 +1,32 @@
var mapShareKey = 'map-shares';
var lastCompleted = typeof lastCompleted !== 'undefined' ?
lastCompleted :
'';
function getMapShares() {
var alreadyShared = JSON.parse(localStorage.getItem(mapShareKey) || '[]');
if (!alreadyShared || !Array.isArray(alreadyShared)) {
localStorage.setItem(mapShareKey, JSON.stringify([]));
alreadyShared = [];
}
return alreadyShared;
}
function setMapShare(id) {
var alreadyShared = getMapShares();
var found = false;
alreadyShared.forEach(function(_id) {
if (_id === id) {
found = true;
}
});
if (!found) {
alreadyShared.push(id);
}
localStorage.setItem(mapShareKey, JSON.stringify(alreadyShared));
return alreadyShared;
}
$(document).ready(function() {
var challengeName = typeof challengeName !== 'undefined' ?
@ -383,6 +412,40 @@ $(document).ready(function() {
}
}, false);
}
// map sharing
var alreadyShared = getMapShares();
if (lastCompleted && alreadyShared.indexOf(lastCompleted) === -1) {
$('div[id="' + lastCompleted + '"]')
.parent()
.parent()
.removeClass('hidden');
}
// on map view
$('.map-challenge-block-share').on('click', function(e) {
e.preventDefault();
var challengeBlockName = $(this).children().attr('id');
var challengeBlockEscapedName = challengeBlockName.replace(/\s/, '%20');
var username = typeof window.username !== 'undefined' ?
window.username :
'';
var link = 'https://www.facebook.com/dialog/feed?' +
'app_id=1644598365767721' +
'&display=page&' +
'caption=I%20just%20completed%20the%20' +
challengeBlockEscapedName +
'%20section%20on%20Free%20Code%20Camp%2E' +
'&link=http%3A%2F%2Ffreecodecamp%2Ecom%2F' +
username +
'&redirect_uri=http%3A%2F%2Ffreecodecamp%2Ecom%2Fmap';
setMapShare(challengeBlockName);
window.location.href = link;
});
});
function defCheck(a){

View File

@ -1,17 +1,17 @@
import Rx from 'rx';
import { Router } from 'react-router';
import { match } from 'react-router';
import App from './App.jsx';
import AppCat from './Cat';
import childRoutes from './routes';
const router$ = Rx.Observable.fromNodeCallback(Router.run, Router);
const route$ = Rx.Observable.fromNodeCallback(match);
const routes = Object.assign({ components: App }, childRoutes);
export default function app$(location) {
return router$(routes, location)
.map(([initialState, transistion]) => {
return { initialState, transistion, AppCat };
export default function app$({ location, history }) {
return route$({ routes, location, history })
.map(([nextLocation, props]) => {
return { nextLocation, props, AppCat };
});
}

View File

@ -0,0 +1,43 @@
import React, { PropTypes } from 'react';
import { History } from 'react-router';
import { Button, Modal } from 'react-bootstrap';
export default React.createClass({
displayName: 'CreateJobsModal',
propTypes: {
onHide: PropTypes.func,
showModal: PropTypes.bool
},
mixins: [History],
goToNewJob(onHide) {
onHide();
this.history.pushState(null, '/jobs/new');
},
render() {
const {
showModal,
onHide
} = this.props;
return (
<Modal
onHide={ onHide }
show={ showModal }>
<Modal.Body>
<h4>Welcome to Free Code Camp's board</h4>
<p>We post jobs specifically target to our junior developers.</p>
<Button
block={ true }
className='signup-btn'
onClick={ () => this.goToNewJob(onHide) }>
Post a Job
</Button>
</Modal.Body>
</Modal>
);
}
});

View File

@ -1,7 +1,9 @@
import React, { cloneElement, PropTypes } from 'react';
import { contain } from 'thundercats-react';
import { Navigation } from 'react-router';
import { History } from 'react-router';
import { Button, Jumbotron, Row } from 'react-bootstrap';
import CreateJobModal from './CreateJobModal.jsx';
import ListJobs from './List.jsx';
export default contain(
@ -13,12 +15,14 @@ export default contain(
React.createClass({
displayName: 'Jobs',
mixins: [History],
propTypes: {
children: PropTypes.element,
jobActions: PropTypes.object,
jobs: PropTypes.array
jobs: PropTypes.array,
showModal: PropTypes.bool
},
mixins: [Navigation],
handleJobClick(id) {
const { jobActions } = this.props;
@ -26,7 +30,7 @@ export default contain(
return null;
}
jobActions.findJob(id);
this.transitionTo(`/jobs/${id}`);
this.history.pushState(null, `/jobs/${id}`);
},
renderList(handleJobClick, jobs) {
@ -48,7 +52,12 @@ export default contain(
},
render() {
const { children, jobs } = this.props;
const {
children,
jobs,
showModal,
jobActions
} = this.props;
return (
<div>
@ -62,7 +71,8 @@ export default contain(
</p>
<Button
bsSize='large'
className='signup-btn'>
className='signup-btn'
onClick={ jobActions.openModal }>
Try the first month 20% off!
</Button>
</Jumbotron>
@ -70,7 +80,10 @@ export default contain(
<Row>
{ this.renderChild(children, jobs) ||
this.renderList(this.handleJobClick, jobs) }
</Row>
</Row>
<CreateJobModal
onHide={ jobActions.closeModal }
showModal={ showModal } />
</div>
);
}

View File

@ -22,6 +22,7 @@ export default React.createClass({
id,
company,
position,
isHighlighted,
description,
logo,
city,
@ -44,6 +45,7 @@ export default React.createClass({
);
return (
<Panel
bsStyle={ isHighlighted ? 'warning' : 'default' }
collapsible={ true }
eventKey={ index }
header={ header }

View File

@ -0,0 +1,319 @@
import React, { PropTypes } from 'react';
import { History } from 'react-router';
import { contain } from 'thundercats-react';
import debugFactory from 'debug';
import { getDefaults } from '../utils';
import {
inHTMLData,
uriInSingleQuotedAttr
} from 'xss-filters';
import {
Button,
Col,
Input,
Row,
Well
} from 'react-bootstrap';
import {
isAscii,
isEmail,
isMobilePhone,
isURL
} from 'validator';
const debug = debugFactory('freecc:jobs:newForm');
const checkValidity = [
'position',
'locale',
'description',
'email',
'phone',
'url',
'logo',
'name',
'highlight'
];
function formatValue(value, validator, type = 'string') {
const formated = getDefaults(type);
if (validator && type === 'string') {
formated.valid = validator(value);
}
if (value) {
formated.value = value;
formated.bsStyle = formated.valid ? 'success' : 'error';
}
return formated;
}
function isValidURL(data) {
return isURL(data, { 'require_protocol': true });
}
function isValidPhone(data) {
return isMobilePhone(data, 'en-US');
}
export default contain({
actions: 'jobActions',
store: 'jobsStore',
map({ form = {} }) {
const {
position,
locale,
description,
email,
phone,
url,
logo,
name,
highlight
} = form;
return {
position: formatValue(position, isAscii),
locale: formatValue(locale, isAscii),
description: formatValue(description, isAscii),
email: formatValue(email, isEmail),
phone: formatValue(phone, isValidPhone),
url: formatValue(url, isValidURL),
logo: formatValue(logo, isValidURL),
name: formatValue(name, isAscii),
highlight: formatValue(highlight, null, 'bool')
};
},
subscribeOnWillMount() {
return typeof window !== 'undefined';
}
},
React.createClass({
displayName: 'NewJob',
propTypes: {
jobActions: PropTypes.object,
position: PropTypes.object,
locale: PropTypes.object,
description: PropTypes.object,
email: PropTypes.object,
phone: PropTypes.object,
url: PropTypes.object,
logo: PropTypes.object,
name: PropTypes.object,
highlight: PropTypes.object
},
mixins: [History],
handleSubmit(e) {
e.preventDefault();
const props = this.props;
let valid = true;
checkValidity.forEach((prop) => {
// if value exist, check if it is valid
if (props[prop].value && props[prop].type !== 'boolean') {
valid = valid && !!props[prop].valid;
}
});
if (!valid) {
debug('form not valid');
return;
}
const {
position,
locale,
description,
email,
phone,
url,
logo,
name,
highlight,
jobActions
} = this.props;
// sanitize user output
const jobValues = {
position: inHTMLData(position.value),
location: inHTMLData(locale.value),
description: inHTMLData(description.value),
email: inHTMLData(email.value),
phone: inHTMLData(phone.value),
url: uriInSingleQuotedAttr(url.value),
logo: uriInSingleQuotedAttr(logo.value),
name: inHTMLData(name.value),
highlight: !!highlight.value
};
const job = Object.keys(jobValues).reduce((accu, prop) => {
if (jobValues[prop]) {
accu[prop] = jobValues[prop];
}
return accu;
}, {});
job.postedOn = new Date();
debug('job sanitized', job);
jobActions.saveForm(job);
this.history.pushState(null, '/jobs/new/preview');
},
componentDidMount() {
const { jobActions } = this.props;
jobActions.getSavedForm();
},
handleChange(name, { target: { value } }) {
const { jobActions: { handleForm } } = this.props;
handleForm({ [name]: value });
},
render() {
const {
position,
locale,
description,
email,
phone,
url,
logo,
name,
highlight,
jobActions: { handleForm }
} = this.props;
const { handleChange } = this;
const labelClass = 'col-sm-offset-1 col-sm-2';
const inputClass = 'col-sm-6';
return (
<div>
<Row>
<Col>
<Well className='text-center'>
<h1>Create Your Job Post</h1>
<form
className='form-horizontal'
onSubmit={ this.handleSubmit }>
<div className='spacer'>
<h2>Job Information</h2>
</div>
<Input
bsStyle={ position.bsStyle }
label='Position'
labelClassName={ labelClass }
onChange={ (e) => handleChange('position', e) }
placeholder='Position'
type='text'
value={ position.value }
wrapperClassName={ inputClass } />
<Input
bsStyle={ locale.bsStyle }
label='Location'
labelClassName={ labelClass }
onChange={ (e) => handleChange('locale', e) }
placeholder='Location'
type='text'
value={ locale.value }
wrapperClassName={ inputClass } />
<Input
bsStyle={ description.bsStyle }
label='Description'
labelClassName={ labelClass }
onChange={ (e) => handleChange('description', e) }
placeholder='Description'
rows='10'
type='textarea'
value={ description.value }
wrapperClassName={ inputClass } />
<div className='divider'>
<h2>Company Information</h2>
</div>
<Input
bsStyle={ name.bsStyle }
label='Company Name'
labelClassName={ labelClass }
onChange={ (e) => handleChange('name', e) }
placeholder='Foo, INC'
type='text'
value={ name.value }
wrapperClassName={ inputClass } />
<Input
bsStyle={ email.bsStyle }
label='Email'
labelClassName={ labelClass }
onChange={ (e) => handleChange('email', e) }
placeholder='Email'
type='email'
value={ email.value }
wrapperClassName={ inputClass } />
<Input
bsStyle={ phone.bsStyle }
label='Phone'
labelClassName={ labelClass }
onChange={ (e) => handleChange('phone', e) }
placeholder='555-123-1234'
type='tel'
value={ phone.value }
wrapperClassName={ inputClass } />
<Input
bsStyle={ url.bsStyle }
label='URL'
labelClassName={ labelClass }
onChange={ (e) => handleChange('url', e) }
placeholder='http://freecatphotoapp.com'
type='url'
value={ url.value }
wrapperClassName={ inputClass } />
<Input
bsStyle={ logo.bsStyle }
label='Logo'
labelClassName={ labelClass }
onChange={ (e) => handleChange('logo', e) }
placeholder='http://freecatphotoapp.com/logo.png'
type='url'
value={ logo.value }
wrapperClassName={ inputClass } />
<div className='divider'>
<h2>Make it stand out</h2>
</div>
<Input
checked={ highlight.value }
label='Highlight your ad'
labelClassName={ 'col-sm-offset-1 col-sm-6'}
onChange={
({ target: { checked } }) => handleForm({
highlight: !!checked
})
}
type='checkbox' />
<div className='spacer' />
<Row>
<Col
lg={ 6 }
lgOffset={ 3 }>
<Button
block={ true }
bsSize='large'
bsStyle='primary'
type='submit'>
Preview My Ad
</Button>
</Col>
</Row>
</form>
</Well>
</Col>
</Row>
</div>
);
}
})
);

View File

@ -0,0 +1,14 @@
// import React, { PropTypes } from 'react';
import { contain } from 'thundercats-react';
import ShowJob from './ShowJob.jsx';
export default contain(
{
store: 'JobsStore',
actions: 'JobActions',
map({ form: job = {} }) {
return { job };
}
},
ShowJob
);

View File

@ -1,13 +1,5 @@
import React, { PropTypes } from 'react';
import { contain } from 'thundercats-react';
import { Row, Thumbnail, Panel, Well } from 'react-bootstrap';
import moment from 'moment';
const thumbnailStyle = {
backgroundColor: 'white',
maxHeight: '100px',
maxWidth: '100px'
};
import ShowJob from './ShowJob.jsx';
export default contain(
{
@ -28,61 +20,5 @@ export default contain(
return job.id !== id;
}
},
React.createClass({
displayName: 'ShowJob',
propTypes: {
job: PropTypes.object,
params: PropTypes.object
},
renderHeader({ company, position }) {
return (
<div>
<h4 style={{ display: 'inline-block' }}>{ company }</h4>
<h5
className='pull-right hidden-xs hidden-md'
style={{ display: 'inline-block' }}>
{ position }
</h5>
</div>
);
},
render() {
const { job = {} } = this.props;
const {
logo,
position,
city,
company,
state,
email,
phone,
postedOn,
description
} = job;
return (
<div>
<Row>
<Well>
<Thumbnail
alt={ company + 'company logo' }
src={ logo }
style={ thumbnailStyle } />
<Panel>
Position: { position }
Location: { city }, { state }
<br />
Contact: { email || phone || 'N/A' }
<br />
Posted On: { moment(postedOn).format('MMMM Do, YYYY') }
</Panel>
<p>{ description }</p>
</Well>
</Row>
</div>
);
}
})
ShowJob
);

View File

@ -0,0 +1,67 @@
import React, { PropTypes } from 'react';
import { Row, Thumbnail, Panel, Well } from 'react-bootstrap';
import moment from 'moment';
const thumbnailStyle = {
backgroundColor: 'white',
maxHeight: '100px',
maxWidth: '100px'
};
export default React.createClass({
displayName: 'ShowJob',
propTypes: {
job: PropTypes.object,
params: PropTypes.object
},
renderHeader({ company, position }) {
return (
<div>
<h4 style={{ display: 'inline-block' }}>{ company }</h4>
<h5
className='pull-right hidden-xs hidden-md'
style={{ display: 'inline-block' }}>
{ position }
</h5>
</div>
);
},
render() {
const { job = {} } = this.props;
const {
logo,
position,
city,
company,
state,
email,
phone,
postedOn,
description
} = job;
return (
<div>
<Row>
<Well>
<Thumbnail
alt={ company + 'company logo' }
src={ logo }
style={ thumbnailStyle } />
<Panel>
Position: { position }
Location: { city }, { state }
<br />
Contact: { email || phone || 'N/A' }
<br />
Posted On: { moment(postedOn).format('MMMM Do, YYYY') }
</Panel>
<p>{ description }</p>
</Well>
</Row>
</div>
);
}
});

View File

@ -1,7 +1,9 @@
import { Actions } from 'thundercats';
import store from 'store';
import debugFactory from 'debug';
const debug = debugFactory('freecc:jobs:actions');
const assign = Object.assign;
export default Actions({
setJobs: null,
@ -23,7 +25,7 @@ export default Actions({
// if no job found this will be null which is a op noop
return foundJob ?
Object.assign({}, oldState, { currentJob: foundJob }) :
assign({}, oldState, { currentJob: foundJob }) :
null;
};
},
@ -31,6 +33,31 @@ export default Actions({
getJob: null,
getJobs(params) {
return { params };
},
openModal() {
return { showModal: true };
},
closeModal() {
return { showModal: false };
},
handleForm(value) {
return {
transform(oldState) {
const { form } = oldState;
const newState = assign({}, oldState);
newState.form = assign(
{},
form,
value
);
return newState;
}
};
},
saveForm: null,
getSavedForm: null,
setForm(form) {
return { form };
}
})
.refs({ displayName: 'JobActions' })
@ -56,8 +83,22 @@ export default Actions({
debug('job services experienced an issue', err);
return jobActions.setError({ err });
}
jobActions.setJobs({ currentJob: job });
if (job) {
jobActions.setJobs({ currentJob: job });
}
jobActions.setJobs({});
});
});
jobActions.saveForm.subscribe((form) => {
store.set('newJob', form);
});
jobActions.getSavedForm.subscribe(() => {
const job = store.get('newJob');
if (job && !Array.isArray(job) && typeof job === 'object') {
jobActions.setForm(job);
}
});
return jobActions;
});

View File

@ -6,12 +6,25 @@ const {
transformer
} = Store;
export default Store()
export default Store({ showModal: false })
.refs({ displayName: 'JobsStore' })
.init(({ instance: jobsStore, args: [cat] }) => {
const { setJobs, findJob, setError } = cat.getActions('JobActions');
const {
setJobs,
findJob,
setError,
openModal,
closeModal,
handleForm,
setForm
} = cat.getActions('JobActions');
const register = createRegistrar(jobsStore);
register(setter(setJobs));
register(transformer(findJob));
register(setter(setError));
register(setter(openModal));
register(setter(closeModal));
register(setter(setForm));
register(transformer(findJob));
register(handleForm);
});

View File

@ -1,5 +1,7 @@
import Jobs from './components/Jobs.jsx';
import NewJob from './components/NewJob.jsx';
import Show from './components/Show.jsx';
import Preview from './components/Preview.jsx';
/*
* index: /jobs list jobs
@ -11,6 +13,12 @@ export default {
childRoutes: [{
path: '/jobs',
component: Jobs
}, {
path: 'jobs/new',
component: NewJob
}, {
path: 'jobs/new/preview',
component: Preview
}, {
path: 'jobs/:id',
component: Show

View File

@ -0,0 +1,22 @@
const defaults = {
'string': {
value: '',
valid: false,
pristine: true,
type: 'string'
},
bool: {
value: false,
type: 'boolean'
}
};
export function getDefaults(type, value) {
if (!type) {
return defaults['string'];
}
if (value) {
return Object.assign({}, defaults[type], { value });
}
return Object.assign({}, defaults[type]);
}

View File

@ -3,12 +3,8 @@ import Hikes from './Hikes';
export default {
path: '/',
getChildRoutes(locationState, cb) {
setTimeout(() => {
cb(null, [
Jobs,
Hikes
]);
}, 0);
}
childRoutes: [
Jobs,
Hikes
]
};

View File

@ -1,6 +1,7 @@
{
"name": "job",
"base": "PersistedModel",
"strict": true,
"idInjection": true,
"trackChanges": false,
"properties": {
@ -29,6 +30,9 @@
"state": {
"type": "string"
},
"url": {
"type": "string"
},
"country": {
"type": "string"
},
@ -38,7 +42,7 @@
"description": {
"type": "string"
},
"isApproverd": {
"isApproved": {
"type": "boolean"
},
"isHighlighted": {

View File

@ -49,6 +49,7 @@ var paths = {
'!public/js/bundle*',
'node_modules/',
'client/',
'seed',
'server/manifests/*.json',
'server/rev-manifest.json'
],

View File

@ -58,10 +58,10 @@
"gulp-webpack": "^1.5.0",
"helmet": "~0.9.0",
"helmet-csp": "^0.2.3",
"history": "^1.9.0",
"jade": "~1.8.0",
"json-loader": "^0.5.2",
"less": "~1.7.5",
"less-middleware": "~2.0.1",
"less": "~2.5.1",
"lodash": "^3.9.3",
"loopback": "https://github.com/FreeCodeCamp/loopback.git#fix/no-password",
"loopback-boot": "2.8.2",
@ -89,7 +89,7 @@
"react": "^0.13.3",
"react-bootstrap": "~0.23.7",
"react-motion": "~0.1.0",
"react-router": "https://github.com/BerkeleyTrue/react-router#freecodecamp",
"react-router": "^1.0.0-rc1",
"react-vimeo": "^0.0.3",
"request": "~2.53.0",
"rev-del": "^1.0.5",
@ -97,12 +97,14 @@
"sanitize-html": "~1.6.1",
"sort-keys": "^1.1.1",
"source-map-support": "^0.3.2",
"store": "https://github.com/berkeleytrue/store.js.git#feature/noop-server",
"thundercats": "^2.1.0",
"thundercats-react": "^0.1.0",
"twit": "~1.1.20",
"uglify-js": "~2.4.15",
"validator": "~3.22.1",
"validator": "^3.22.1",
"webpack": "^1.9.12",
"xss-filters": "^1.2.6",
"yui": "~3.18.1"
},
"devDependencies": {

View File

@ -464,9 +464,9 @@
"slasher([1, 2, 3], 2, \"\");"
],
"tests": [
"assert.deepEqual(slasher([1, 2, 3], 2), [3], '<code>[1&#44; 2&#44; 3]&#44; 2&#44; [3]</code> should return <code>[3]</code>.');",
"assert.deepEqual(slasher([1, 2, 3], 0), [1, 2, 3], '<code>[1&#44; 2&#44; 3]&#44; 0</code> should return <code>[1&#44; 2&#44; 3]</code>.');",
"assert.deepEqual(slasher([1, 2, 3], 9), [], '<code>[1&#44; 2&#44; 3]&#44; 9</code> should return <code>[]</code>.');"
"assert.deepEqual(slasher([1, 2, 3], 2), [3], '<code>slasher&#40;[1&#44; 2&#44; 3]&#44; 2&#41;</code> should return <code>[3]</code>.');",
"assert.deepEqual(slasher([1, 2, 3], 0), [1, 2, 3], '<code>slasher&#40;[1&#44; 2&#44; 3]&#44; 0&#41;</code> should return <code>[1&#44; 2&#44; 3]</code>.');",
"assert.deepEqual(slasher([1, 2, 3], 9), [], '<code>slasher&#40;[1&#44; 2&#44; 3]&#44; 9&#41;</code> should return <code>[]</code>.');"
],
"MDNlinks": [
"Array.slice()",

View File

@ -643,8 +643,9 @@
"challengeSeed": [
"var ourArray = [\"Stimpson\", \"J\", [\"cat\"]];",
"ourArray.shift();",
"// ourArray now equals [\"happy\", \"J\", [\"cat\"]]",
"// ourArray now equals [\"J\", [\"cat\"]]",
"ourArray.unshift(\"happy\");",
"// ourArray now equals [\"happy\", \"J\", [\"cat\"]]",
"",
"var myArray = [\"John\", 23, [\"dog\", 3]];",
"myArray.shift();",

View File

@ -1142,7 +1142,8 @@
"assert($(\"a\").text().match(/cat\\sphotos/gi), 'Your <code>a</code> element should have the anchor text of \"cat photos\"')",
"assert($(\"p\") && $(\"p\").length > 2, 'Create a new <code>p</code> element around your <code>a</code> element.')",
"assert($(\"a[href=\\\"http://www.freecatphotoapp.com\\\"]\").parent().is(\"p\"), 'Your <code>a</code> element should be nested within your new <code>p</code> element.')",
"assert($(\"p\").text().match(/View\\smore/gi), 'Your <code>p</code> element should have the text \"View more\".')",
"assert($(\"p\").text().match(/^View\\smore\\s/gi), 'Your <code>p</code> element should have the text \"View more \" (with a space after it).')",
"assert(!$(\"a\").text().match(/View\\smore/gi), 'Your <code>a</code> element should <em>not</em> have the text \"View more\".')",
"assert(editor.match(/<\\/p>/g) && editor.match(/<p/g) && editor.match(/<\\/p>/g).length === editor.match(/<p/g).length, 'Make sure each of your <code>p</code> elements has a closing tag.')",
"assert(editor.match(/<\\/a>/g) && editor.match(/<a/g) && editor.match(/<\\/a>/g).length === editor.match(/<a/g).length, 'Make sure each of your <code>a</code> elements has a closing tag.')"
],

View File

@ -153,7 +153,7 @@
"assert.deepEqual(where([{ first: 'Romeo', last: 'Montague' }, { first: 'Mercutio', last: null }, { first: 'Tybalt', last: 'Capulet' }], { last: 'Capulet' }), [{ first: 'Tybalt', last: 'Capulet' }], 'should return an array of objects');",
"assert.deepEqual(where([{ 'a': 1 }, { 'a': 1 }, { 'a': 1, 'b': 2 }], { 'a': 1 }), [{ 'a': 1 }, { 'a': 1 }, { 'a': 1, 'b': 2 }], 'should return with multiples');",
"assert.deepEqual(where([{ 'a': 1, 'b': 2 }, { 'a': 1 }, { 'a': 1, 'b': 2, 'c': 2 }], { 'a': 1, 'b': 2 }), [{ 'a': 1, 'b': 2 }, { 'a': 1, 'b': 2, 'c': 2 }], 'should return two objects in array');",
"assert.deepEqual(where([{ 'a': 5 }, { 'a': 5 }, { 'a': 5, 'b': 10 }], { 'a': 5, 'b': 10 }), [{ 'a': 5, 'b': 10 }], 'should return a single object in array');"
"assert.deepEqual(where([{ 'a': 5 }, { 'b': 10 }, { 'a': 5, 'b': 10 }], { 'a': 5, 'b': 10 }), [{ 'a': 5, 'b': 10 }], 'should return a single object in array');"
],
"MDNlinks": [
"Global Object",

View File

@ -791,6 +791,7 @@
"description": [
"You can also target all the even-numbered elements.",
"Here's how you would target all the odd-numbered elements with class <code>target</code> and give them classes: <code>$(\".target:odd\").addClass(\"animated shake\");</code>",
"Note that jQuery is zero-indexed, meaning that, counter-intuitively, <code>:odd</code> selects the second element, fourth element, and so on.",
"Try selecting all the even-numbered elements - that is, what your browser will consider even-numbered elements - and giving them the classes of <code>animated</code> and <code>shake</code>."
],
"tests": [

View File

@ -1,7 +1,7 @@
import React from 'react';
import Router from 'react-router';
import { RoutingContext } from 'react-router';
import Fetchr from 'fetchr';
import Location from 'react-router/lib/Location';
import { createLocation } from 'history';
import debugFactory from 'debug';
import { app$ } from '../../common/app';
import { RenderToString } from 'thundercats-react';
@ -30,25 +30,25 @@ export default function reactSubRouter(app) {
function serveReactApp(req, res, next) {
const services = new Fetchr({ req });
const location = new Location(req.path, req.query);
const location = createLocation(req.path);
// returns a router wrapped app
app$(location)
app$({ location })
// if react-router does not find a route send down the chain
.filter(function({ initialState }) {
if (!initialState) {
.filter(function({ props}) {
if (!props) {
debug('react tried to find %s but got 404', location.pathname);
return next();
}
return !!initialState;
return !!props;
})
.flatMap(function({ initialState, AppCat }) {
.flatMap(function({ props, AppCat }) {
// call thundercats renderToString
// prefetches data and sets up it up for current state
debug('rendering to string');
return RenderToString(
AppCat(null, services),
React.createElement(Router, initialState)
React.createElement(RoutingContext, props)
);
})
// makes sure we only get one onNext and closes subscription

View File

@ -471,6 +471,7 @@ module.exports = function(app) {
}
function challengeMap({ user = {} }, res, next) {
let lastCompleted;
const daysRunning = moment().diff(new Date('10/15/2014'), 'days');
// if user
@ -513,7 +514,13 @@ module.exports = function(app) {
})
.filter(({ name }) => name !== 'Hikes')
// turn stream of blocks into a stream of an array
.toArray();
.toArray()
.doOnNext((blocks) => {
const lastCompletedBlock = _.findLast(blocks, (block) => {
return block.completed === 100;
});
lastCompleted = lastCompletedBlock.name;
});
Observable.combineLatest(
camperCount$,
@ -526,6 +533,7 @@ module.exports = function(app) {
blocks,
daysRunning,
camperCount,
lastCompleted,
title: "A map of all Free Code Camp's Challenges"
});
},

View File

@ -131,7 +131,16 @@ block content
span= challenge.title
span.sr-only= " Incomplete"
//#announcementModal.modal(tabindex='-1')
if (challengeBlock.completed === 100)
.button-spacer
.row
.col-xs-12.col-sm-8.col-md-6.col-sm-offset-3.col-md-offset-2.hidden
a.btn.btn-lg.btn-block.signup-btn.map-challenge-block-share Section complete. Share your Portfolio with your friends.
.hidden(id="#{challengeBlock.name}")
script.
var username = !{JSON.stringify(user && user.username || '')};
var lastCompleted = !{JSON.stringify(lastCompleted || false)}
// #announcementModal.modal(tabindex='-1')
// .modal-dialog.animated.fadeInUp.fast-animation
// .modal-content
// .modal-header.challenge-list-header Add us to your LinkedIn profile

View File

@ -94,10 +94,6 @@ block content
.row
if (user)
#submit-challenge.animated.fadeIn.btn.btn-lg.btn-primary.btn-block Submit and go to my next challenge (ctrl + enter)
if (user.progressTimestamps.length > 2)
a.btn.btn-lg.btn-block.btn-twitter(target="_blank", href="https://twitter.com/intent/tweet?text=I%20just%20#{verb}%20%40FreeCodeCamp%20#{name}&url=http%3A%2F%2Ffreecodecamp.com/challenges/#{dashedName}&hashtags=LearnToCode, JavaScript")
i.fa.fa-twitter &thinsp;
= phrase
else
a.animated.fadeIn.btn.btn-lg.btn-primary.btn-block(href='/challenges/next-challenge?id=' + challengeId) Go to my next challenge
include ../partials/challenge-modals

View File

@ -69,12 +69,6 @@ block content
a.btn.btn-lg.btn-primary.btn-block#next-courseware-button(name='_csrf', value=_csrf) I've completed this challenge (ctrl + enter)
script.
$('#complete-courseware-editorless-dialog').bind('keypress', modalControlEnterHandler);
if (user.progressTimestamps.length > 2)
.button-spacer
a.btn.btn-lg.btn-block.btn-twitter(href="https://twitter.com/intent/tweet?text=I%20just%20#{verb}%20%40FreeCodeCamp%20#{name}&url=http%3A%2F%2Ffreecodecamp.com/challenges/#{dashedName}&hashtags=LearnToCode, JavaScript" target="_blank")
i.fa.fa-twitter &thinsp;
= phrase
else
a.animated.fadeIn.btn.btn-lg.btn-primary.btn-block(href='/challenges/next-challenge?id=' + challengeId) I've completed this challenge (ctrl + enter)
script.