Merge branch 'staging' of github.com:FreeCodeCamp/freecodecamp into staging
Conflicts: server/boot/randomAPIs.js server/views/coursewares/showBonfire.jade server/views/partials/navbar.jade
This commit is contained in:
@ -70,6 +70,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",
|
||||||
|
"object.assign": "^3.0.0",
|
||||||
"passport-facebook": "^2.0.0",
|
"passport-facebook": "^2.0.0",
|
||||||
"passport-google-oauth2": "^0.1.6",
|
"passport-google-oauth2": "^0.1.6",
|
||||||
"passport-linkedin-oauth2": "^1.2.1",
|
"passport-linkedin-oauth2": "^1.2.1",
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
$(document).ready(function() {
|
||||||
|
$('#reset-button').on('click', resetEditor);
|
||||||
|
});
|
||||||
|
|
||||||
var widgets = [];
|
var widgets = [];
|
||||||
var myCodeMirror = CodeMirror.fromTextArea(document.getElementById("codeEditor"), {
|
var myCodeMirror = CodeMirror.fromTextArea(document.getElementById("codeEditor"), {
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
@ -41,73 +45,90 @@ editor.setOption("extraKeys", {
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Local Storage Update System By Andrew Cay(Resto)
|
Local Storage Update System By Andrew Cay(Resto)
|
||||||
localBonfire: singleton object that contains properties and methods related to
|
codeStorage: singleton object that contains properties and methods related to
|
||||||
dealing with the localStorage system.
|
dealing with the localStorage system.
|
||||||
The keys work off of the variable challenge_name to make unique identifiers per bonfire
|
The keys work off of the variable challenge_name to make unique identifiers per bonfire
|
||||||
|
|
||||||
Two extra functionalities:
|
Two extra functionalities:
|
||||||
Added anonymous version checking system incase of future updates to the system
|
Added anonymous version checking system incase of future updates to the system
|
||||||
Added keyup listener to editor(myCodeMirror) so the last update has been saved to storage
|
Added keyup listener to editor(myCodeMirror) so the last update has been saved to storage
|
||||||
*/
|
*/
|
||||||
var localBonfire = {
|
var codeStorage = {
|
||||||
version: 0.01,
|
version: 0.01,
|
||||||
keyVersion:"saveVersion",
|
keyVersion:"saveVersion",
|
||||||
keyStamp: challenge_Name + 'Stamp',
|
keyValue: null,//where the value of the editor is saved
|
||||||
keyValue: challenge_Name + 'Val',
|
updateWait: 2000,// 2 seconds
|
||||||
stampExpireTime: (1000 *60) *60 *24,
|
updateTimeoutId: null,
|
||||||
updateWait: 1500,// 1.5 seconds
|
eventArray: []//for firing saves
|
||||||
updateTimeoutId: null
|
|
||||||
};
|
};
|
||||||
localBonfire.getEditorValue = function(){
|
// Returns true if the editor code was saved since last key press (use this if you want to make a "saved" notification somewhere")
|
||||||
return localStorage.getItem(localBonfire.keyValue);
|
codeStorage.hasSaved = function(){
|
||||||
|
return ( updateTimeoutId === null );
|
||||||
};
|
};
|
||||||
localBonfire.getStampTime = function(){
|
codeStorage.onSave = function(func){
|
||||||
//localstorage always saves as strings.
|
codeStorage.eventArray.push(func);
|
||||||
return Number.parseInt( localStorage.getItem(localBonfire.keyStamp) );
|
|
||||||
};
|
};
|
||||||
localBonfire.isAlive = function(){// returns true if IDE was edited within expire time
|
codeStorage.setSaveKey = function(key){
|
||||||
return ( Date.now() - localBonfire.getStampTime() < localBonfire.stampExpireTime );
|
codeStorage.keyValue = key + 'Val';
|
||||||
};
|
};
|
||||||
localBonfire.updateStorage = function(){
|
codeStorage.getEditorValue = function(){
|
||||||
if(typeof(Storage) !== undefined) {
|
return ('' + localStorage.getItem(codeStorage.keyValue));
|
||||||
var stamp = Date.now(),
|
|
||||||
value = editor.getValue();
|
|
||||||
localStorage.setItem(localBonfire.keyValue, value);
|
|
||||||
localStorage.setItem(localBonfire.keyStamp, stamp);
|
|
||||||
} else {
|
|
||||||
if( debugging ){
|
|
||||||
console.log('no web storage');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
localBonfire.updateTimeoutId = null;
|
|
||||||
};
|
};
|
||||||
// ANONYMOUS 1 TIME UPDATE VERSION
|
|
||||||
|
codeStorage.isAlive = function() {
|
||||||
|
var val = this.getEditorValue()
|
||||||
|
return val !== 'null' &&
|
||||||
|
val !== 'undefined' &&
|
||||||
|
(val && val.length > 0);
|
||||||
|
}
|
||||||
|
codeStorage.updateStorage = function(){
|
||||||
|
if(typeof(Storage) !== undefined) {
|
||||||
|
var value = editor.getValue();
|
||||||
|
localStorage.setItem(codeStorage.keyValue, value);
|
||||||
|
} else {
|
||||||
|
var debugging = false;
|
||||||
|
if( debugging ){
|
||||||
|
console.log('no web storage');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
codeStorage.updateTimeoutId = null;
|
||||||
|
codeStorage.eventArray.forEach(function(func){
|
||||||
|
func();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
//Update Version
|
||||||
(function(){
|
(function(){
|
||||||
var savedVersion = localStorage.getItem('saveVersion');
|
var savedVersion = localStorage.getItem('saveVersion');
|
||||||
if( savedVersion === null ){
|
if( savedVersion === null ){
|
||||||
localStorage.setItem(localBonfire.keyVersion, localBonfire.version);//just write current version
|
localStorage.setItem(codeStorage.keyVersion, codeStorage.version);//just write current version
|
||||||
}else{
|
}else{
|
||||||
//do checking if not current version
|
if( savedVersion !== codeStorage.version ){
|
||||||
if( savedVersion !== localBonfire.version ){
|
//Update version
|
||||||
//update version
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
editor.on('keyup', function(codMir, event){
|
|
||||||
window.clearTimeout(localBonfire.updateTimeoutId);
|
|
||||||
localBonfire.updateTimeoutId = window.setTimeout(localBonfire.updateStorage, localBonfire.updateWait);
|
///Set everything up one page
|
||||||
|
/// Update local save when editor has changed
|
||||||
|
codeStorage.setSaveKey(challenge_Name);
|
||||||
|
editor.on('keyup', function(){
|
||||||
|
window.clearTimeout(codeStorage.updateTimeoutId);
|
||||||
|
codeStorage.updateTimeoutId = window.setTimeout(codeStorage.updateStorage, codeStorage.updateWait);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
var attempts = 0;
|
var attempts = 0;
|
||||||
if (attempts) {
|
if (attempts) {
|
||||||
attempts = 0;
|
attempts = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
var resetEditor = function() {
|
var resetEditor = function resetEditor() {
|
||||||
editor.setValue(allSeeds);
|
editor.setValue(allSeeds);
|
||||||
localBonfire.updateStorage();
|
codeStorage.updateStorage();
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var codeOutput = CodeMirror.fromTextArea(document.getElementById("codeOutput"), {
|
var codeOutput = CodeMirror.fromTextArea(document.getElementById("codeOutput"), {
|
||||||
@ -141,11 +162,11 @@ var tests = tests || [];
|
|||||||
var allSeeds = '';
|
var allSeeds = '';
|
||||||
(function() {
|
(function() {
|
||||||
challengeSeed.forEach(function(elem) {
|
challengeSeed.forEach(function(elem) {
|
||||||
allSeeds += elem + '\n';
|
allSeeds += elem + '\n';
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
editorValue = (localBonfire.isAlive())? localBonfire.getEditorValue() : allSeeds;
|
editorValue = (codeStorage.isAlive())? codeStorage.getEditorValue() : allSeeds;
|
||||||
|
|
||||||
myCodeMirror.setValue(editorValue);
|
myCodeMirror.setValue(editorValue);
|
||||||
|
|
||||||
|
@ -271,6 +271,7 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
$('#story-submit').on('click', storySubmitButtonHandler);
|
$('#story-submit').on('click', storySubmitButtonHandler);
|
||||||
|
|
||||||
|
|
||||||
var commentSubmitButtonHandler = function commentSubmitButtonHandler() {
|
var commentSubmitButtonHandler = function commentSubmitButtonHandler() {
|
||||||
$('#comment-button').unbind('click');
|
$('#comment-button').unbind('click');
|
||||||
var data = $('#comment-box').val();
|
var data = $('#comment-box').val();
|
||||||
|
@ -31,8 +31,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
var R = require('ramda'),
|
var R = require('ramda'),
|
||||||
|
Rx = require('rx'),
|
||||||
|
assign = require('object.assign'),
|
||||||
|
debug = require('debug')('freecc:challenges'),
|
||||||
utils = require('../utils'),
|
utils = require('../utils'),
|
||||||
|
|
||||||
|
// this would be so much cleaner with destructering...
|
||||||
saveUser = require('../utils/rx').saveUser,
|
saveUser = require('../utils/rx').saveUser,
|
||||||
|
observableQueryFromModel = require('../utils/rx').observableQueryFromModel,
|
||||||
|
|
||||||
userMigration = require('../utils/middleware').userMigration,
|
userMigration = require('../utils/middleware').userMigration,
|
||||||
ifNoUserRedirectTo = require('../utils/middleware').ifNoUserRedirectTo;
|
ifNoUserRedirectTo = require('../utils/middleware').ifNoUserRedirectTo;
|
||||||
|
|
||||||
@ -42,6 +49,28 @@ var challengeMapWithDashedNames = utils.getChallengeMapWithDashedNames();
|
|||||||
|
|
||||||
var getMDNLinks = utils.getMDNLinks;
|
var getMDNLinks = utils.getMDNLinks;
|
||||||
|
|
||||||
|
var challangesRegex = /^(bonfire|waypoint|zipline|basejump)/i;
|
||||||
|
function dasherize(name) {
|
||||||
|
return ('' + name)
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s/g, '-')
|
||||||
|
.replace(/[^a-z0-9\-\.]/gi, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function unDasherize(name) {
|
||||||
|
return ('' + name).replace(/\-/g, ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUserProgress(user, challengeId, completedChallenge) {
|
||||||
|
var index = user.uncompletedChallenges.indexOf(challengeId);
|
||||||
|
if (index > -1) {
|
||||||
|
user.progressTimestamps.push(Date.now());
|
||||||
|
user.uncompletedChallenges.splice(index, 1);
|
||||||
|
}
|
||||||
|
user.completedChallenges.push(completedChallenge);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = function(app) {
|
module.exports = function(app) {
|
||||||
var router = app.loopback.Router();
|
var router = app.loopback.Router();
|
||||||
var Challenge = app.models.Challenge;
|
var Challenge = app.models.Challenge;
|
||||||
@ -56,7 +85,7 @@ module.exports = function(app) {
|
|||||||
router.get('/map', challengeMap);
|
router.get('/map', challengeMap);
|
||||||
router.get(
|
router.get(
|
||||||
'/challenges/next-challenge',
|
'/challenges/next-challenge',
|
||||||
ifNoUserRedirectTo('../challenges/learn-how-free-code-camp-works'),
|
ifNoUserRedirectTo('/challenges/learn-how-free-code-camp-works'),
|
||||||
returnNextChallenge
|
returnNextChallenge
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -64,7 +93,7 @@ module.exports = function(app) {
|
|||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/challenges/',
|
'/challenges/',
|
||||||
ifNoUserRedirectTo('../challenges/learn-how-free-code-camp-works'),
|
ifNoUserRedirectTo('/challenges/learn-how-free-code-camp-works'),
|
||||||
returnCurrentChallenge
|
returnCurrentChallenge
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -114,7 +143,7 @@ module.exports = function(app) {
|
|||||||
function() {},
|
function() {},
|
||||||
next,
|
next,
|
||||||
function() {
|
function() {
|
||||||
res.redirect('../challenges/' + nextChallengeName);
|
res.redirect('/challenges/' + nextChallengeName);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -130,6 +159,7 @@ module.exports = function(app) {
|
|||||||
return elem;
|
return elem;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!req.user.currentChallenge) {
|
if (!req.user.currentChallenge) {
|
||||||
req.user.currentChallenge = {};
|
req.user.currentChallenge = {};
|
||||||
req.user.currentChallenge.challengeId = challengeMapWithIds['0'][0];
|
req.user.currentChallenge.challengeId = challengeMapWithIds['0'][0];
|
||||||
@ -146,40 +176,53 @@ module.exports = function(app) {
|
|||||||
function() {},
|
function() {},
|
||||||
next,
|
next,
|
||||||
function() {
|
function() {
|
||||||
res.redirect('../challenges/' + nameString);
|
res.redirect('/challenges/' + nameString);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function returnIndividualChallenge(req, res, next) {
|
function returnIndividualChallenge(req, res, next) {
|
||||||
var dashedName = req.params.challengeName;
|
var origChallengeName = req.params.challengeName;
|
||||||
|
var unDashedName = unDasherize(origChallengeName);
|
||||||
|
|
||||||
|
var challengeName = challangesRegex.test(unDashedName) ?
|
||||||
|
// remove first word if matches
|
||||||
|
unDashedName.split(' ').slice(1).join(' ') :
|
||||||
|
unDashedName;
|
||||||
|
|
||||||
|
debug('looking for ', challengeName);
|
||||||
Challenge.findOne(
|
Challenge.findOne(
|
||||||
{ where: { dashedName: dashedName }},
|
{ where: { name: { like: challengeName, options: 'i' } } },
|
||||||
function(err, challenge) {
|
function(err, challenge) {
|
||||||
if (err) { return next(err); }
|
if (err) { return next(err); }
|
||||||
|
|
||||||
// Handle not found
|
// Handle not found
|
||||||
if (!challenge) {
|
if (!challenge) {
|
||||||
|
debug('did not find challenge for ' + origChallengeName);
|
||||||
req.flash('errors', {
|
req.flash('errors', {
|
||||||
msg:
|
msg:
|
||||||
'404: We couldn\'t find a challenge with the name `' +
|
'404: We couldn\'t find a challenge with the name `' +
|
||||||
dashedName +
|
origChallengeName +
|
||||||
'` Please double check the name.'
|
'` Please double check the name.'
|
||||||
});
|
});
|
||||||
return res.redirect('/challenges');
|
return res.redirect('/challenges');
|
||||||
}
|
}
|
||||||
// Redirect to full name if the user only entered a partial
|
// Redirect to full name if the user only entered a partial
|
||||||
|
if (dasherize(challenge.name) !== origChallengeName) {
|
||||||
|
debug('redirecting to fullname');
|
||||||
|
return res.redirect('/challenges/' + dasherize(challenge.name));
|
||||||
|
}
|
||||||
|
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
req.user.currentChallenge = {
|
req.user.currentChallenge = {
|
||||||
challengeId: challenge.id,
|
challengeId: challenge.id,
|
||||||
challengeName: challenge.name,
|
challengeName: challenge.name,
|
||||||
dashedName: challenge.dashedName,
|
dashedName: challenge.dashedName,
|
||||||
challengeBlock: R.head(R.flatten(Object.keys(challengeMapWithIds).
|
challengeBlock: R.head(R.flatten(Object.keys(challengeMapWithIds)
|
||||||
map(function (key) {
|
.map(function (key) {
|
||||||
return challengeMapWithIds[key]
|
return challengeMapWithIds[key]
|
||||||
.filter(function (elem) {
|
.filter(function (elem) {
|
||||||
return String(elem) === challenge.id;
|
return elem === ('' + challenge.id);
|
||||||
})
|
})
|
||||||
.map(function () {
|
.map(function () {
|
||||||
return key;
|
return key;
|
||||||
@ -191,7 +234,7 @@ module.exports = function(app) {
|
|||||||
|
|
||||||
var commonLocals = {
|
var commonLocals = {
|
||||||
title: challenge.name,
|
title: challenge.name,
|
||||||
dashedName: dashedName,
|
dashedName: origChallengeName,
|
||||||
name: challenge.name,
|
name: challenge.name,
|
||||||
details: challenge.description.slice(1),
|
details: challenge.description.slice(1),
|
||||||
tests: challenge.tests,
|
tests: challenge.tests,
|
||||||
@ -235,143 +278,123 @@ module.exports = function(app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function completedBonfire(req, res, next) {
|
function completedBonfire(req, res, next) {
|
||||||
var isCompletedWith = req.body.challengeInfo.completedWith || '';
|
debug('compltedBonfire');
|
||||||
var isCompletedDate = Math.round(+new Date());
|
var completedWith = req.body.challengeInfo.completedWith || false;
|
||||||
var challengeId = req.body.challengeInfo.challengeId;
|
var challengeId = req.body.challengeInfo.challengeId;
|
||||||
var isSolution = req.body.challengeInfo.solution;
|
|
||||||
var challengeName = req.body.challengeInfo.challengeName;
|
|
||||||
|
|
||||||
if (isCompletedWith) {
|
var challengeData = {
|
||||||
User.find({
|
id: challengeId,
|
||||||
where: { 'profile.username': isCompletedWith.toLowerCase() },
|
name: req.body.challengeInfo.challengeName,
|
||||||
limit: 1
|
completedDate: Math.round(+new Date()),
|
||||||
}, function (err, pairedWith) {
|
solution: req.body.challengeInfo.solution,
|
||||||
if (err) { return next(err); }
|
challengeType: 5
|
||||||
|
};
|
||||||
|
|
||||||
var index = req.user.uncompletedChallenges.indexOf(challengeId);
|
observableQueryFromModel(
|
||||||
if (index > -1) {
|
User,
|
||||||
req.user.progressTimestamps.push(Date.now() || 0);
|
'findOne',
|
||||||
req.user.uncompletedChallenges.splice(index, 1);
|
{ where: { username: ('' + completedWith).toLowerCase() } }
|
||||||
}
|
)
|
||||||
pairedWith = pairedWith.pop();
|
.doOnNext(function(pairedWith) {
|
||||||
|
debug('paired with ', pairedWith);
|
||||||
if (pairedWith) {
|
if (pairedWith) {
|
||||||
|
updateUserProgress(
|
||||||
index = pairedWith.uncompletedChallenges.indexOf(challengeId);
|
pairedWith,
|
||||||
if (index > -1) {
|
challengeId,
|
||||||
pairedWith.progressTimestamps.push(Date.now() || 0);
|
assign({ completedWith: req.user.id }, challengeData)
|
||||||
pairedWith.uncompletedChallenges.splice(index, 1);
|
);
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pairedWith.completedChallenges.push({
|
|
||||||
id: challengeId,
|
|
||||||
name: challengeName,
|
|
||||||
completedWith: req.user.id,
|
|
||||||
completedDate: isCompletedDate,
|
|
||||||
solution: isSolution,
|
|
||||||
challengeType: 5
|
|
||||||
});
|
|
||||||
|
|
||||||
req.user.completedChallenges.push({
|
|
||||||
id: challengeId,
|
|
||||||
name: challengeName,
|
|
||||||
completedWith: pairedWith.id,
|
|
||||||
completedDate: isCompletedDate,
|
|
||||||
solution: isSolution,
|
|
||||||
challengeType: 5
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// User said they paired, but pair wasn't found
|
})
|
||||||
req.user.completedChallenges.push({
|
.withLatestFrom(
|
||||||
id: challengeId,
|
Rx.Observable.just(req.user),
|
||||||
name: challengeName,
|
function(pairedWith, user) {
|
||||||
completedWith: null,
|
debug('yo');
|
||||||
completedDate: isCompletedDate,
|
return {
|
||||||
solution: isSolution,
|
user: user,
|
||||||
challengeType: 5
|
pairedWith: pairedWith
|
||||||
});
|
};
|
||||||
|
}
|
||||||
req.user.save(function (err, user) {
|
)
|
||||||
if (err) { return next(err); }
|
// side effects should always be done in do's and taps
|
||||||
|
.doOnNext(function(dats) {
|
||||||
if (pairedWith) {
|
updateUserProgress(
|
||||||
pairedWith.save(function (err, paired) {
|
dats.user,
|
||||||
if (err) {
|
challengeId,
|
||||||
return next(err);
|
dats.pairedWith ?
|
||||||
}
|
// paired programmer found and adding to data
|
||||||
if (user && paired) {
|
assign({ completedWith: dats.pairedWith.id }, challengeData) :
|
||||||
return res.send(true);
|
// user said they paired, but pair wasn't found
|
||||||
}
|
challengeData
|
||||||
});
|
);
|
||||||
} else if (user) {
|
})
|
||||||
res.send(true);
|
// not iterate users
|
||||||
|
.flatMap(function(dats) {
|
||||||
|
debug('flatmap');
|
||||||
|
return Rx.Observable.from([dats.user, dats.pairedWith]);
|
||||||
|
})
|
||||||
|
// save user
|
||||||
|
.flatMap(function(user) {
|
||||||
|
// save user will do nothing if user is falsey
|
||||||
|
return saveUser(user);
|
||||||
|
})
|
||||||
|
.subscribe(
|
||||||
|
function(user) {
|
||||||
|
debug('onNext');
|
||||||
|
if (user) {
|
||||||
|
debug('user %s saved', user.username);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
});
|
next,
|
||||||
} else {
|
function() {
|
||||||
req.user.completedChallenges.push({
|
debug('completed');
|
||||||
id: challengeId,
|
return res.status(200).send(true);
|
||||||
name: challengeName,
|
}
|
||||||
completedWith: null,
|
);
|
||||||
completedDate: isCompletedDate,
|
|
||||||
solution: isSolution,
|
|
||||||
challengeType: 5
|
|
||||||
});
|
|
||||||
|
|
||||||
var index = req.user.uncompletedChallenges.indexOf(challengeId);
|
|
||||||
if (index > -1) {
|
|
||||||
|
|
||||||
req.user.progressTimestamps.push(Date.now() || 0);
|
|
||||||
req.user.uncompletedChallenges.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.user.save(function (err) {
|
|
||||||
if (err) { return next(err); }
|
|
||||||
res.send(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function completedChallenge(req, res, next) {
|
function completedChallenge(req, res, next) {
|
||||||
|
|
||||||
var isCompletedDate = Math.round(+new Date());
|
var completedDate = Math.round(+new Date());
|
||||||
var challengeId = req.body.challengeInfo.challengeId;
|
var challengeId = req.body.challengeInfo.challengeId;
|
||||||
|
|
||||||
req.user.completedChallenges.push({
|
updateUserProgress(
|
||||||
id: challengeId,
|
req.user,
|
||||||
completedDate: isCompletedDate,
|
challengeId,
|
||||||
name: req.body.challengeInfo.challengeName,
|
{
|
||||||
solution: null,
|
id: challengeId,
|
||||||
githubLink: null,
|
completedDate: completedDate,
|
||||||
verified: true
|
name: req.body.challengeInfo.challengeName,
|
||||||
});
|
solution: null,
|
||||||
var index = req.user.uncompletedChallenges.indexOf(challengeId);
|
githubLink: null,
|
||||||
|
verified: true
|
||||||
if (index > -1) {
|
|
||||||
req.user.progressTimestamps.push(Date.now() || 0);
|
|
||||||
req.user.uncompletedChallenges.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.user.save(function (err, user) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
}
|
||||||
if (user) {
|
);
|
||||||
res.sendStatus(200);
|
|
||||||
}
|
saveUser(req.user)
|
||||||
});
|
.subscribe(
|
||||||
|
function() { },
|
||||||
|
next,
|
||||||
|
function() {
|
||||||
|
res.sendStatus(200);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function completedZiplineOrBasejump(req, res, next) {
|
function completedZiplineOrBasejump(req, res, next) {
|
||||||
|
|
||||||
var isCompletedWith = req.body.challengeInfo.completedWith || false;
|
var completedWith = req.body.challengeInfo.completedWith || false;
|
||||||
var isCompletedDate = Math.round(+new Date());
|
var completedDate = Math.round(+new Date());
|
||||||
var challengeId = req.body.challengeInfo.challengeId;
|
var challengeId = req.body.challengeInfo.challengeId;
|
||||||
var solutionLink = req.body.challengeInfo.publicURL;
|
var solutionLink = req.body.challengeInfo.publicURL;
|
||||||
var githubLink = req.body.challengeInfo.challengeType === '4'
|
|
||||||
? req.body.challengeInfo.githubURL : true;
|
var githubLink = req.body.challengeInfo.challengeType === '4' ?
|
||||||
|
req.body.challengeInfo.githubURL :
|
||||||
|
true;
|
||||||
|
|
||||||
var challengeType = req.body.challengeInfo.challengeType === '4' ?
|
var challengeType = req.body.challengeInfo.challengeType === '4' ?
|
||||||
4 : 3;
|
4 :
|
||||||
|
3;
|
||||||
|
|
||||||
if (!solutionLink || !githubLink) {
|
if (!solutionLink || !githubLink) {
|
||||||
req.flash('errors', {
|
req.flash('errors', {
|
||||||
msg: 'You haven\'t supplied the necessary URLs for us to inspect ' +
|
msg: 'You haven\'t supplied the necessary URLs for us to inspect ' +
|
||||||
@ -380,93 +403,64 @@ module.exports = function(app) {
|
|||||||
return res.sendStatus(403);
|
return res.sendStatus(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCompletedWith) {
|
var challengeData = {
|
||||||
User.find({
|
id: challengeId,
|
||||||
where: { 'profile.username': isCompletedWith.toLowerCase() },
|
name: req.body.challengeInfo.challengeName,
|
||||||
limit: 1
|
completedDate: completedDate,
|
||||||
}, function (err, pairedWithFromMongo) {
|
solution: solutionLink,
|
||||||
if (err) { return next(err); }
|
githubLink: githubLink,
|
||||||
var index = req.user.uncompletedChallenges.indexOf(challengeId);
|
challengeType: challengeType,
|
||||||
if (index > -1) {
|
verified: false
|
||||||
req.user.progressTimestamps.push(Date.now() || 0);
|
};
|
||||||
req.user.uncompletedChallenges.splice(index, 1);
|
|
||||||
|
observableQueryFromModel(
|
||||||
|
User,
|
||||||
|
'findOne',
|
||||||
|
{ where: { username: completedWith.toLowerCase() } }
|
||||||
|
)
|
||||||
|
.doOnNext(function(pairedWith) {
|
||||||
|
if (pairedWith) {
|
||||||
|
updateUserProgress(
|
||||||
|
pairedWith,
|
||||||
|
challengeId,
|
||||||
|
assign({ completedWith: req.user.id }, challengeData)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
var pairedWith = pairedWithFromMongo.pop();
|
})
|
||||||
|
.withLatestFrom(Rx.Observable.just(req.user), function(user, pairedWith) {
|
||||||
req.user.completedChallenges.push({
|
return {
|
||||||
id: challengeId,
|
user: user,
|
||||||
name: req.body.challengeInfo.challengeName,
|
pairedWith: pairedWith
|
||||||
completedWith: pairedWith.id,
|
};
|
||||||
completedDate: isCompletedDate,
|
})
|
||||||
solution: solutionLink,
|
.doOnNext(function(dats) {
|
||||||
githubLink: githubLink,
|
updateUserProgress(
|
||||||
challengeType: challengeType,
|
dats.user,
|
||||||
verified: false
|
challengeId,
|
||||||
});
|
dats.pairedWith ?
|
||||||
|
assign({ completedWith: dats.pairedWith.id }, challengeData) :
|
||||||
req.user.save(function (err, user) {
|
challengeData
|
||||||
if (err) { return next(err); }
|
);
|
||||||
|
})
|
||||||
if (req.user.id.toString() === pairedWith.id.toString()) {
|
.flatMap(function(dats) {
|
||||||
return res.sendStatus(200);
|
return Rx.Observable.from([dats.user, dats.pairedWith]);
|
||||||
|
})
|
||||||
|
// save users
|
||||||
|
.flatMap(function(user) {
|
||||||
|
// save user will do nothing if user is falsey
|
||||||
|
return saveUser(user);
|
||||||
|
})
|
||||||
|
.subscribe(
|
||||||
|
function(user) {
|
||||||
|
if (user) {
|
||||||
|
debug('user %s saved', user.username);
|
||||||
}
|
}
|
||||||
index = pairedWith.uncompletedChallenges.indexOf(challengeId);
|
},
|
||||||
if (index > -1) {
|
next,
|
||||||
pairedWith.progressTimestamps.push(Date.now() || 0);
|
function() {
|
||||||
pairedWith.uncompletedChallenges.splice(index, 1);
|
return res.status(200).send(true);
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
pairedWith.completedChallenges.push({
|
|
||||||
id: challengeId,
|
|
||||||
name: req.body.challengeInfo.coursewareName,
|
|
||||||
completedWith: req.user.id,
|
|
||||||
completedDate: isCompletedDate,
|
|
||||||
solution: solutionLink,
|
|
||||||
githubLink: githubLink,
|
|
||||||
challengeType: challengeType,
|
|
||||||
verified: false
|
|
||||||
});
|
|
||||||
pairedWith.save(function (err, paired) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
if (user && paired) {
|
|
||||||
return res.sendStatus(200);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
|
|
||||||
req.user.completedChallenges.push({
|
|
||||||
id: challengeId,
|
|
||||||
name: req.body.challengeInfo.challengeName,
|
|
||||||
completedWith: null,
|
|
||||||
completedDate: isCompletedDate,
|
|
||||||
solution: solutionLink,
|
|
||||||
githubLink: githubLink,
|
|
||||||
challengeType: challengeType,
|
|
||||||
verified: false
|
|
||||||
});
|
|
||||||
|
|
||||||
var index = req.user.uncompletedChallenges.indexOf(challengeId);
|
|
||||||
if (index > -1) {
|
|
||||||
req.user.progressTimestamps.push(Date.now() || 0);
|
|
||||||
req.user.uncompletedChallenges.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.user.save(function (err, user) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
}
|
||||||
// NOTE(berks): under certain conditions this will not close
|
);
|
||||||
// the response.
|
|
||||||
if (user) {
|
|
||||||
return res.sendStatus(200);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function challengeMap(req, res, next) {
|
function challengeMap(req, res, next) {
|
||||||
|
@ -13,7 +13,7 @@ module.exports = function(app) {
|
|||||||
var router = app.loopback.Router();
|
var router = app.loopback.Router();
|
||||||
var User = app.models.User;
|
var User = app.models.User;
|
||||||
var Challenge = app.models.Challenge;
|
var Challenge = app.models.Challenge;
|
||||||
var Story = app.models.Store;
|
var Story = app.models.Story;
|
||||||
var FieldGuide = app.models.FieldGuide;
|
var FieldGuide = app.models.FieldGuide;
|
||||||
var Nonprofit = app.models.Nonprofit;
|
var Nonprofit = app.models.Nonprofit;
|
||||||
|
|
||||||
@ -76,15 +76,15 @@ module.exports = function(app) {
|
|||||||
users: function(callback) {
|
users: function(callback) {
|
||||||
User.find(
|
User.find(
|
||||||
{
|
{
|
||||||
where: { 'profile.username': { nlike: '' } },
|
where: { username: { nlike: '' } },
|
||||||
fields: { 'profile.username': true }
|
fields: { username: true }
|
||||||
},
|
},
|
||||||
function(err, users) {
|
function(err, users) {
|
||||||
if (err) {
|
if (err) {
|
||||||
debug('User err: ', err);
|
debug('User err: ', err);
|
||||||
callback(err);
|
callback(err);
|
||||||
} else {
|
} else {
|
||||||
Rx.Observable.from(users)
|
Rx.Observable.from(users, null, null, Rx.Scheduler.default)
|
||||||
.map(function(user) {
|
.map(function(user) {
|
||||||
return user.username;
|
return user.username;
|
||||||
})
|
})
|
||||||
@ -107,7 +107,7 @@ module.exports = function(app) {
|
|||||||
debug('Challenge err: ', err);
|
debug('Challenge err: ', err);
|
||||||
callback(err);
|
callback(err);
|
||||||
} else {
|
} else {
|
||||||
Rx.Observable.from(challenges)
|
Rx.Observable.from(challenges, null, null, Rx.Scheduler.default)
|
||||||
.map(function(challenge) {
|
.map(function(challenge) {
|
||||||
return challenge.name;
|
return challenge.name;
|
||||||
})
|
})
|
||||||
@ -127,7 +127,7 @@ module.exports = function(app) {
|
|||||||
debug('Story err: ', err);
|
debug('Story err: ', err);
|
||||||
callback(err);
|
callback(err);
|
||||||
} else {
|
} else {
|
||||||
Rx.Observable.from(stories)
|
Rx.Observable.from(stories, null, null, Rx.Scheduler.default)
|
||||||
.map(function(story) {
|
.map(function(story) {
|
||||||
return story.link;
|
return story.link;
|
||||||
})
|
})
|
||||||
@ -148,7 +148,7 @@ module.exports = function(app) {
|
|||||||
debug('User err: ', err);
|
debug('User err: ', err);
|
||||||
callback(err);
|
callback(err);
|
||||||
} else {
|
} else {
|
||||||
Rx.Observable.from(nonprofits)
|
Rx.Observable.from(nonprofits, null, null, Rx.Scheduler.default)
|
||||||
.map(function(nonprofit) {
|
.map(function(nonprofit) {
|
||||||
return nonprofit.name;
|
return nonprofit.name;
|
||||||
})
|
})
|
||||||
@ -168,7 +168,12 @@ module.exports = function(app) {
|
|||||||
debug('User err: ', err);
|
debug('User err: ', err);
|
||||||
callback(err);
|
callback(err);
|
||||||
} else {
|
} else {
|
||||||
Rx.Observable.from(fieldGuides)
|
Rx.Observable.from(
|
||||||
|
fieldGuides,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
Rx.Scheduler.default
|
||||||
|
)
|
||||||
.map(function(fieldGuide) {
|
.map(function(fieldGuide) {
|
||||||
return fieldGuide.name;
|
return fieldGuide.name;
|
||||||
})
|
})
|
||||||
@ -184,7 +189,7 @@ module.exports = function(app) {
|
|||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
setTimeout(function() {
|
process.nextTick(function() {
|
||||||
res.header('Content-Type', 'application/xml');
|
res.header('Content-Type', 'application/xml');
|
||||||
res.render('resources/sitemap', {
|
res.render('resources/sitemap', {
|
||||||
appUrl: appUrl,
|
appUrl: appUrl,
|
||||||
@ -195,11 +200,15 @@ module.exports = function(app) {
|
|||||||
nonprofits: results.nonprofits,
|
nonprofits: results.nonprofits,
|
||||||
fieldGuides: results.fieldGuides
|
fieldGuides: results.fieldGuides
|
||||||
});
|
});
|
||||||
}, 0);
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function chat(req, res) {
|
||||||
|
res.redirect('//gitter.im/FreeCodeCamp/FreeCodeCamp');
|
||||||
|
}
|
||||||
|
|
||||||
function bootcampCalculator(req, res) {
|
function bootcampCalculator(req, res) {
|
||||||
res.render('resources/calculator', {
|
res.render('resources/calculator', {
|
||||||
title: 'Coding Bootcamp Cost Calculator',
|
title: 'Coding Bootcamp Cost Calculator',
|
||||||
@ -260,7 +269,7 @@ module.exports = function(app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function unsubscribe(req, res, next) {
|
function unsubscribe(req, res, next) {
|
||||||
User.findOne({ email: req.params.email }, function(err, user) {
|
User.findOne({ where: { email: req.params.email } }, function(err, user) {
|
||||||
if (user) {
|
if (user) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return next(err);
|
return next(err);
|
||||||
|
@ -4,7 +4,7 @@ var _ = require('lodash'),
|
|||||||
crypto = require('crypto'),
|
crypto = require('crypto'),
|
||||||
nodemailer = require('nodemailer'),
|
nodemailer = require('nodemailer'),
|
||||||
moment = require('moment'),
|
moment = require('moment'),
|
||||||
//debug = require('debug')('freecc:cntr:userController'),
|
// debug = require('debug')('freecc:cntr:userController'),
|
||||||
|
|
||||||
secrets = require('../../config/secrets');
|
secrets = require('../../config/secrets');
|
||||||
|
|
||||||
|
@ -17,4 +17,4 @@ module.exports = {
|
|||||||
clientID: process.env.GITHUB_ID,
|
clientID: process.env.GITHUB_ID,
|
||||||
clientSecret: process.env.GITHUB_SECRET
|
clientSecret: process.env.GITHUB_SECRET
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -3,8 +3,8 @@ var secrets = require('../config/secrets');
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
db: {
|
db: {
|
||||||
connector: 'mongodb',
|
connector: 'mongodb',
|
||||||
connectionTimeout: 15000,
|
connectionTimeout: 5000,
|
||||||
url: process.env.MONGOHQ_URL
|
url: secrets.db
|
||||||
},
|
},
|
||||||
mail: {
|
mail: {
|
||||||
connector: 'mail',
|
connector: 'mail',
|
||||||
|
25
server/utils/rx.js
Normal file
25
server/utils/rx.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
var Rx = require('rx');
|
||||||
|
var debug = require('debug')('freecc:rxUtils');
|
||||||
|
|
||||||
|
exports.saveUser = function saveUser(user) {
|
||||||
|
return new Rx.Observable.create(function(observer) {
|
||||||
|
if (!user || typeof user.save !== 'function') {
|
||||||
|
debug('no user or save method');
|
||||||
|
observer.onNext();
|
||||||
|
return observer.onCompleted();
|
||||||
|
}
|
||||||
|
user.save(function(err, savedUser) {
|
||||||
|
if (err) {
|
||||||
|
return observer.onError(err);
|
||||||
|
}
|
||||||
|
debug('user saved');
|
||||||
|
observer.onNext(savedUser);
|
||||||
|
observer.onCompleted();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.observableQueryFromModel =
|
||||||
|
function observableQueryFromModel(Model, method, query) {
|
||||||
|
return Rx.Observable.fromNodeCallback(Model[method], Model)(query);
|
||||||
|
};
|
@ -47,88 +47,32 @@ block content
|
|||||||
.background-svg.img-center
|
.background-svg.img-center
|
||||||
.points-on-top
|
.points-on-top
|
||||||
= "[ " + (progressTimestamps.length) + " ]"
|
= "[ " + (progressTimestamps.length) + " ]"
|
||||||
|
|
||||||
|
|
||||||
.row
|
|
||||||
.col-xs-12
|
|
||||||
if (website1Title && website1Link && website1Image)
|
|
||||||
.row
|
|
||||||
.col-xs-12.col-md-5
|
|
||||||
img.img-center.img-responsive.portfolio-image(src=website1Image, alt="@#{username}'s #{website1Title}")
|
|
||||||
.col-xs-12.col-md-7
|
|
||||||
h3.text-center.wrappable.flat-top= website1Title
|
|
||||||
a.btn.btn-lg.btn-block.btn-info(href=website1Link, target='_blank')
|
|
||||||
i.fa.icon-beaker
|
|
||||||
| Try it out
|
|
||||||
br
|
|
||||||
if (website1Title && website1Link && !website1Image)
|
|
||||||
.col-xs-12.col-md-12
|
|
||||||
h3.text-center.wrappable.flat-top= website1Title
|
|
||||||
a.btn.btn-lg.btn-block.btn-info(href=website1Link, target='_blank')
|
|
||||||
i.fa.icon-beaker
|
|
||||||
| Try it out
|
|
||||||
br
|
|
||||||
if (website2Title && website2Link && website2Image)
|
|
||||||
.row
|
|
||||||
.col-xs-12.col-md-5
|
|
||||||
img.img-responsive.portfolio-image.img-center(src=website2Image, alt="@#{username}'s #{website2Title}")
|
|
||||||
.col-xs-12.col-md-7
|
|
||||||
h3.text-center.wrappable.flat-top= website2Title
|
|
||||||
a.btn.btn-lg.btn-block.btn-info(href=website2Link, target='_blank')
|
|
||||||
i.fa.icon-beaker
|
|
||||||
| Try it out
|
|
||||||
br
|
|
||||||
if (website2Title && website2Link && !website2Image)
|
|
||||||
.col-xs-12.col-md-12
|
|
||||||
h3.text-center.wrappable.flat-top= website2Title
|
|
||||||
a.btn.btn-lg.btn-block.btn-info(href=website2Link, target='_blank')
|
|
||||||
i.fa.icon-beaker
|
|
||||||
| Try it out
|
|
||||||
br
|
|
||||||
if (website3Title && website3Link && website3Image)
|
|
||||||
.row
|
|
||||||
.col-xs-12.col-md-5
|
|
||||||
img.img-responsive.portfolio-image.img-center(src=website3Image, alt="@#{username}'s #{website1Title}")
|
|
||||||
.col-xs-12.col-md-7
|
|
||||||
h3.text-center.wrappable.flat-top= website3Title
|
|
||||||
a.btn.btn-lg.btn-block.btn-info(href=website3Link, target='_blank')
|
|
||||||
i.fa.icon-beaker
|
|
||||||
| Try it out
|
|
||||||
if (website3Title && website3Link && !website3Image)
|
|
||||||
.col-xs-12.col-md-12
|
|
||||||
h3.text-center.wrappable.flat-top= website3Title
|
|
||||||
a.btn.btn-lg.btn-block.btn-info(href=website3Link, target='_blank')
|
|
||||||
i.fa.icon-beaker
|
|
||||||
| Try it out
|
|
||||||
|
|
||||||
.spacer
|
.spacer
|
||||||
.hidden-xs.hidden-sm.col-md-12
|
.hidden-xs.hidden-sm.col-md-12
|
||||||
#cal-heatmap.d3-centered
|
#cal-heatmap.d3-centered
|
||||||
script.
|
script.
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
setTimeout(function () {
|
var cal = new CalHeatMap();
|
||||||
var cal = new CalHeatMap();
|
var calendar = !{JSON.stringify(calender)};
|
||||||
var calendar = !{JSON.stringify(calender)};
|
cal.init({
|
||||||
cal.init({
|
itemSelector: "#cal-heatmap",
|
||||||
itemSelector: "#cal-heatmap",
|
domain: "month",
|
||||||
domain: "month",
|
subDomain: "x_day",
|
||||||
subDomain: "x_day",
|
domainGutter: 10,
|
||||||
domainGutter: 10,
|
data: calendar,
|
||||||
data: calendar,
|
cellSize: 15,
|
||||||
cellSize: 15,
|
align: 'center',
|
||||||
align: 'center',
|
cellRadius: 3,
|
||||||
cellRadius: 3,
|
cellPadding: 2,
|
||||||
cellPadding: 2,
|
tooltip: true,
|
||||||
tooltip: true,
|
range: 6,
|
||||||
range: 6,
|
start: new Date().setDate(new Date().getDate() - 150),
|
||||||
start: new Date().setDate(new Date().getDate() - 150),
|
legendColors: ["#cccccc", "#215f1e"],
|
||||||
legendColors: ["#cccccc", "#215f1e"],
|
legend: [1, 2, 3],
|
||||||
legend: [1, 2, 3],
|
label: {
|
||||||
label: {
|
position: "top"
|
||||||
position: "top"
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}, 300);
|
|
||||||
});
|
});
|
||||||
.row
|
.row
|
||||||
.hidden-xs.col-sm-12.text-center
|
.hidden-xs.col-sm-12.text-center
|
||||||
|
@ -84,20 +84,20 @@ block content
|
|||||||
label.negative-10.btn.btn-primary.btn-block#submitButton
|
label.negative-10.btn.btn-primary.btn-block#submitButton
|
||||||
i.fa.fa-play
|
i.fa.fa-play
|
||||||
| Run code (ctrl + enter)
|
| Run code (ctrl + enter)
|
||||||
.button-spacer
|
.button-spacer
|
||||||
.btn-group.input-group.btn-group-justified
|
.btn-group.input-group.btn-group-justified
|
||||||
label.btn.btn-success#resetButton
|
label.btn.btn-success#trigger-reset-modal
|
||||||
i.fa.fa-refresh
|
i.fa.fa-refresh
|
||||||
| Reset
|
| Reset
|
||||||
label.btn.btn-success#trigger-help-modal
|
label.btn.btn-success#trigger-help-modal
|
||||||
i.fa.fa-medkit
|
i.fa.fa-medkit
|
||||||
| Help
|
| Help
|
||||||
label.btn.btn-success#trigger-pair-modal
|
label.btn.btn-success#trigger-pair-modal
|
||||||
i.fa.fa-user-plus
|
i.fa.fa-user-plus
|
||||||
| Pair
|
| Pair
|
||||||
label.btn.btn-success#trigger-issue-modal
|
label.btn.btn-success#trigger-issue-modal
|
||||||
i.fa.fa-bug
|
i.fa.fa-bug
|
||||||
| Bug
|
| Bug
|
||||||
.button-spacer
|
.button-spacer
|
||||||
form.code
|
form.code
|
||||||
.form-group.codeMirrorView
|
.form-group.codeMirrorView
|
||||||
|
@ -14,7 +14,7 @@ nav.navbar.navbar-default.navbar-fixed-top.nav-height
|
|||||||
li
|
li
|
||||||
a(href='/map') Map
|
a(href='/map') Map
|
||||||
li
|
li
|
||||||
a(href='gitter.im/FreeCodeCamp/FreeCodeCamp', target='_blank') Chat
|
a(href='//gitter.im/FreeCodeCamp/FreeCodeCamp', target='_blank') Chat
|
||||||
li
|
li
|
||||||
a(href='/stories') News
|
a(href='/stories') News
|
||||||
li
|
li
|
||||||
|
Reference in New Issue
Block a user