diff --git a/seed/index.js b/seed/index.js index 121a9ba953..0920aeb8f0 100644 --- a/seed/index.js +++ b/seed/index.js @@ -31,11 +31,7 @@ destroy() var challenges = challengeSpec.challenges .map(function(challenge, index) { - // NOTE(berks): add title for displaying in views - challenge.name = - _.capitalize(challenge.type) + - ': ' + - challenge.title.replace(/[^a-zA-Z0-9\s]/g, ''); + challenge.name = challenge.title.replace(/[^a-zA-Z0-9\s]/g, ''); challenge.dashedName = challenge.name .toLowerCase() diff --git a/server/boot/challenge.js b/server/boot/challenge.js index c88e377b04..34fc38f5b3 100644 --- a/server/boot/challenge.js +++ b/server/boot/challenge.js @@ -33,13 +33,13 @@ const debug = debugFactory('freecc:challenges'); const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i; const firstChallenge = 'waypoint-learn-how-free-code-camp-works'; const challengeView = { - 0: 'coursewares/showHTML', - 1: 'coursewares/showJS', - 2: 'coursewares/showVideo', - 3: 'coursewares/showZiplineOrBasejump', - 4: 'coursewares/showZiplineOrBasejump', - 5: 'coursewares/showBonfire', - 7: 'coursewares/showStep' + 0: 'challenges/showHTML', + 1: 'challenges/showJS', + 2: 'challenges/showVideo', + 3: 'challenges/showZiplineOrBasejump', + 4: 'challenges/showZiplineOrBasejump', + 5: 'challenges/showBonfire', + 7: 'challenges/showStep' }; function isChallengeCompleted(user, challengeId) { @@ -50,9 +50,11 @@ function isChallengeCompleted(user, challengeId) { challenge.id === challengeId ); } +/* function numberWithCommas(x) { return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } +*/ function updateUserProgress(user, challengeId, completedChallenge) { let { completedChallenges } = user; @@ -116,6 +118,131 @@ function shouldNotFilterComingSoon({ isComingSoon, isBeta: challengeIsBeta }) { (isBeta && challengeIsBeta); } +function getRenderData$(user, challenge$, origChallengeName, solution) { + const challengeName = unDasherize(origChallengeName) + .replace(challengesRegex, ''); + + const testChallengeName = new RegExp(challengeName, 'i'); + debug('looking for %s', testChallengeName); + + return challenge$ + .filter((challenge) => { + return testChallengeName.test(challenge.name) && + shouldNotFilterComingSoon(challenge); + }) + .last({ defaultValue: null }) + .flatMap(challenge => { + if (challenge && isDev) { + return getFromDisk$(challenge); + } + return Observable.just(challenge); + }) + .flatMap(challenge => { + + // Handle not found + if (!challenge) { + debug('did not find challenge for ' + origChallengeName); + return Observable.just({ + type: 'redirect', + redirectUrl: '/map', + message: dedent` + 404: We couldn\'t find a challenge with the name ${origChallengeName}. + Please double check the name. + ` + }); + } + + if (dasherize(challenge.name) !== origChallengeName) { + let redirectUrl = `/challenges/${dasherize(challenge.name)}`; + + if (solution) { + redirectUrl += `?solution=${encodeURIComponent(solution)}`; + } + + return Observable.just({ + type: 'redirect', + redirectUrl + }); + } + + // save user does nothing if user does not exist + return Observable.just({ + data: { + ...challenge, + // identifies if a challenge is completed + isCompleted: isChallengeCompleted(user, challenge.id), + + // video challenges + video: challenge.challengeSeed[0], + + // bonfires specific + bonfires: challenge, + MDNkeys: challenge.MDNlinks, + MDNlinks: getMDNLinks(challenge.MDNlinks), + + // htmls specific + verb: randomVerb(), + phrase: randomPhrase(), + compliment: randomCompliment() + } + }); + }); +} + +// create a stream of an array of all the challenge blocks +function getSuperBlocks$(challenge$, completedChallenges) { + return challenge$ + // mark challenge completed + .map(challengeModel => { + const challenge = challengeModel.toJSON(); + if (completedChallenges.indexOf(challenge.id) !== -1) { + challenge.completed = true; + } + challenge.markNew = shouldShowNew(challenge); + return challenge; + }) + // group challenges by block | returns a stream of observables + .groupBy(challenge => challenge.block) + // turn block group stream into an array + .flatMap(block$ => block$.toArray()) + .map(blockArray => { + const completedCount = blockArray.reduce((sum, { completed }) => { + if (completed) { + return sum + 1; + } + return sum; + }, 0); + const isBeta = _.every(blockArray, 'isBeta'); + const isComingSoon = _.every(blockArray, 'isComingSoon'); + + return { + isBeta, + isComingSoon, + name: blockArray[0].block, + superBlock: blockArray[0].superBlock, + dashedName: dasherize(blockArray[0].block), + markNew: shouldShowNew(null, blockArray), + challenges: blockArray, + completed: completedCount / blockArray.length * 100, + time: blockArray[0] && blockArray[0].time || '???' + }; + }) + // filter out hikes + .filter(({ superBlock }) => { + return !(/hikes/i).test(superBlock); + }) + // turn stream of blocks into a stream of an array + .toArray() + .flatMap(blocks => Observable.from(blocks, null, null, Scheduler.default)) + .groupBy(block => block.superBlock) + .flatMap(blocks$ => blocks$.toArray()) + .map(superBlockArray => ({ + name: superBlockArray[0].superBlock, + blocks: superBlockArray + })) + .toArray(); +} + module.exports = function(app) { const router = app.loopback.Router(); @@ -167,8 +294,6 @@ module.exports = function(app) { .shareReplay(); const User = app.models.User; - const userCount$ = observeMethod(User, 'count'); - const send200toNonUser = ifNoUserSend(true); router.post( @@ -182,13 +307,13 @@ module.exports = function(app) { completedZiplineOrBasejump ); - router.get('/map', challengeMap); + router.get('/map', showMap); router.get( '/challenges/next-challenge', returnNextChallenge ); - router.get('/challenges/:challengeName', returnIndividualChallenge); + router.get('/challenges/:challengeName', showChallenge); app.use(router); @@ -279,92 +404,20 @@ module.exports = function(app) { ); } - function returnIndividualChallenge(req, res, next) { - const origChallengeName = req.params.challengeName; - const solutionCode = req.query.solution; - const unDashedName = unDasherize(origChallengeName); + function showChallenge(req, res, next) { + const solution = req.query.solution; - const challengeName = challengesRegex.test(unDashedName) ? - // remove first word if matches - unDashedName.split(' ').slice(1).join(' ') : - unDashedName; - - const testChallengeName = new RegExp(challengeName, 'i'); - debug('looking for %s', testChallengeName); - challenge$ - .filter((challenge) => { - return testChallengeName.test(challenge.name) && - shouldNotFilterComingSoon(challenge); - }) - .last({ defaultValue: null }) - .flatMap(challenge => { - if (challenge && isDev) { - return getFromDisk$(challenge); - } - return Observable.just(challenge); - }) - .flatMap(challenge => { - - // Handle not found - if (!challenge) { - debug('did not find challenge for ' + origChallengeName); - req.flash('errors', { - msg: - '404: We couldn\'t find a challenge with the name `' + - origChallengeName + - '` Please double check the name.' - }); - return Observable.just('/map'); - } - - if (dasherize(challenge.name) !== origChallengeName) { - let redirectUrl = `/challenges/${dasherize(challenge.name)}`; - - if (solutionCode) { - redirectUrl += `?solution=${encodeURIComponent(solutionCode)}`; - } - - return Observable.just(redirectUrl); - } - - // save user does nothing if user does not exist - return Observable.just({ - - title: challenge.name, - name: challenge.name, - details: challenge.description, - description: challenge.description, - challengeId: challenge.id, - challengeType: challenge.challengeType, - dashedName: origChallengeName, - - challengeSeed: challenge.challengeSeed, - head: challenge.head, - tail: challenge.tail, - tests: challenge.tests, - - // identifies if a challenge is completed - isCompleted: isChallengeCompleted(req.user, challenge.id), - - // video challenges - video: challenge.challengeSeed[0], - - // bonfires specific - bonfires: challenge, - MDNkeys: challenge.MDNlinks, - MDNlinks: getMDNLinks(challenge.MDNlinks), - - // htmls specific - verb: randomVerb(), - phrase: randomPhrase(), - compliment: randomCompliment() - }); - }) + getRenderData$(req.user, challenge$, req.params.challengeName, solution) .subscribe( - function(data) { - if (typeof data === 'string') { - debug('redirecting to %s', data); - return res.redirect(data); + ({ type, redirectUrl, message, data }) => { + if (message) { + req.flash('info', { + msg: message + }); + } + if (type === 'redirect') { + debug('redirecting to %s', redirectUrl); + return res.redirect(redirectUrl); } var view = challengeView[data.challengeType]; res.render(view, data); @@ -500,93 +553,18 @@ module.exports = function(app) { ); } - function challengeMap({ user = {} }, res, next) { - - let lastCompleted; - const daysRunning = moment().diff(new Date('10/15/2014'), 'days'); - + function showMap({ user = {} }, res, next) { // if user // get the id's of all the users completed challenges const completedChallenges = !user.completedChallenges ? [] : _.uniq(user.completedChallenges).map(({ id, _id }) => id || _id); - const camperCount$ = userCount$() - .map(camperCount => numberWithCommas(camperCount)); - - // create a stream of an array of all the challenge blocks - const superBlocks$ = challenge$ - // mark challenge completed - .map(challengeModel => { - const challenge = challengeModel.toJSON(); - if (completedChallenges.indexOf(challenge.id) !== -1) { - challenge.completed = true; - } - challenge.markNew = shouldShowNew(challenge); - return challenge; - }) - // group challenges by block | returns a stream of observables - .groupBy(challenge => challenge.block) - // turn block group stream into an array - .flatMap(block$ => block$.toArray()) - .map(blockArray => { - const completedCount = blockArray.reduce((sum, { completed }) => { - if (completed) { - return sum + 1; - } - return sum; - }, 0); - const isBeta = _.every(blockArray, 'isBeta'); - const isComingSoon = _.every(blockArray, 'isComingSoon'); - - return { - isBeta, - isComingSoon, - name: blockArray[0].block, - superBlock: blockArray[0].superBlock, - dashedName: dasherize(blockArray[0].block), - markNew: shouldShowNew(null, blockArray), - challenges: blockArray, - completed: completedCount / blockArray.length * 100, - time: blockArray[0] && blockArray[0].time || '???' - }; - }) - // filter out hikes - .filter(({ superBlock }) => { - return !(/hikes/i).test(superBlock); - }) - // turn stream of blocks into a stream of an array - .toArray() - .doOnNext(blocks => { - const lastCompletedBlock = _.findLast(blocks, (block) => { - return block.completed === 100; - }); - lastCompleted = lastCompletedBlock && lastCompletedBlock.name || null; - }) - .flatMap(blocks => Observable.from(blocks, null, null, Scheduler.default)) - .groupBy(block => block.superBlock) - .flatMap(blocks$ => blocks$.toArray()) - .map(superBlockArray => ({ - name: superBlockArray[0].superBlock, - blocks: superBlockArray - })) - .toArray(); - - Observable.combineLatest( - camperCount$, - superBlocks$, - (camperCount, superBlocks) => ({ camperCount, superBlocks }) - ) + getSuperBlocks$(challenge$, completedChallenges) .subscribe( - ({ camperCount, superBlocks }) => { - res.render('challengeMap/show', { + superBlocks => { + res.render('map/show', { superBlocks, - daysRunning, - globalCompletedCount: numberWithCommas( - 5612952 + (Math.floor((Date.now() - 1446268581061) / 2000)) - ), - camperCount, - lastCompleted, title: 'A Map to Learn to Code and Become a Software Engineer' }); }, diff --git a/server/utils/getFromDisk$.js b/server/utils/getFromDisk$.js index b6e24fe3ca..87517465be 100644 --- a/server/utils/getFromDisk$.js +++ b/server/utils/getFromDisk$.js @@ -1,4 +1,3 @@ -import _ from 'lodash'; import path from 'path'; import { Observable } from 'rx'; @@ -22,10 +21,7 @@ export default function getFromDisk$(challenge) { challenge.tail = challenge.tail || []; challenge.challengeType = '' + challenge.challengeType; - challenge.name = - _.capitalize(challenge.type) + - ': ' + - challenge.title.replace(/[^a-zA-Z0-9\s]/g, ''); + challenge.name = challenge.title.replace(/[^a-zA-Z0-9\s]/g, ''); challenge.dashedName = challenge.name .toLowerCase() diff --git a/server/views/coursewares/showBonfire.jade b/server/views/challenges/showBonfire.jade similarity index 96% rename from server/views/coursewares/showBonfire.jade rename to server/views/challenges/showBonfire.jade index 7e0d32ac6e..6529ca175b 100644 --- a/server/views/coursewares/showBonfire.jade +++ b/server/views/challenges/showBonfire.jade @@ -17,7 +17,7 @@ block content .row .col-xs-12 .bonfire-instructions - for sentence in details + for sentence in description if (/blockquote|h4|table/.test(sentence)) !=sentence else @@ -83,7 +83,7 @@ block content i.fa.fa-twitter   = phrase else - a#next-challenge.btn.btn-lg.btn-primary.btn-block(href="/challenges/next-challenge?id="+challengeId) Go to my next challenge (ctrl + enter) + a#next-challenge.btn.btn-lg.btn-primary.btn-block(href="/challenges/next-challenge?id="+id) Go to my next challenge (ctrl + enter) include ../partials/challenge-modals script(type="text/javascript"). var common = window.common = window.common || { init: [] }; @@ -92,7 +92,7 @@ block content common.head = !{JSON.stringify(head)}; common.tail = !{JSON.stringify(tail)}; - common.challengeId = !{JSON.stringify(challengeId)}; + common.challengeId = !{JSON.stringify(id)}; common.challengeName = !{JSON.stringify(name)}; common.challengeSeed = !{JSON.stringify(challengeSeed)}; common.challengeType = !{JSON.stringify(challengeType)}; diff --git a/server/views/coursewares/showHTML.jade b/server/views/challenges/showHTML.jade similarity index 95% rename from server/views/coursewares/showHTML.jade rename to server/views/challenges/showHTML.jade index 0aec76b139..0ca3eb9bf5 100644 --- a/server/views/coursewares/showHTML.jade +++ b/server/views/challenges/showHTML.jade @@ -16,7 +16,7 @@ block content i.ion-checkmark-circled.text-primary(title="Completed") hr .bonfire-instructions - for sentence in details + for sentence in description p.wrappable.negative-10!= sentence .negative-bottom-margin-30 .button-spacer @@ -74,7 +74,7 @@ block content if(user) #submit-challenge.animated.fadeIn.btn.btn-lg.btn-primary.btn-block Submit and go to my next challenge (ctrl + enter) else - a#next-challenge.btn.btn-lg.btn-primary.btn-block(href="/challenges/next-challenge?id="+challengeId) Go to my next challenge (ctrl + enter) + a#next-challenge.btn.btn-lg.btn-primary.btn-block(href="/challenges/next-challenge?id="+id) Go to my next challenge (ctrl + enter) include ../partials/challenge-modals script(type="text/javascript"). $('#next-courseware-button').attr('disabled', 'disabled'); @@ -84,7 +84,7 @@ block content common.head = !{JSON.stringify(head)}; common.tail = !{JSON.stringify(tail)}; - common.challengeId = !{JSON.stringify(challengeId)}; + common.challengeId = !{JSON.stringify(id)}; common.challengeName = !{JSON.stringify(name)}; common.challengeSeed = !{JSON.stringify(challengeSeed)}; common.challengeType = !{JSON.stringify(challengeType)}; diff --git a/server/views/coursewares/showJS.jade b/server/views/challenges/showJS.jade similarity index 96% rename from server/views/coursewares/showJS.jade rename to server/views/challenges/showJS.jade index 4eb7c2bb59..b726ec1978 100644 --- a/server/views/coursewares/showJS.jade +++ b/server/views/challenges/showJS.jade @@ -17,7 +17,7 @@ block content .row .col-xs-12 .bonfire-instructions - for sentence in details + for sentence in description if (/blockquote|h4|table/.test(sentence)) !=sentence else @@ -82,7 +82,7 @@ block content if (user) #submit-challenge.animated.fadeIn.btn.btn-lg.btn-primary.btn-block Submit and go to my next challenge (ctrl + enter) else - a#next-challenge.animated.fadeIn.btn.btn-lg.btn-primary.btn-block(href="/challenges/next-challenge?id="+challengeId) Go to my next challenge (ctrl + enter) + a#next-challenge.animated.fadeIn.btn.btn-lg.btn-primary.btn-block(href="/challenges/next-challenge?id="+id) Go to my next challenge (ctrl + enter) include ../partials/challenge-modals script(type="text/javascript"). var common = window.common = { init: [] }; @@ -91,7 +91,7 @@ block content common.head = !{JSON.stringify(head)}; common.tail = !{JSON.stringify(tail)}; - common.challengeId = !{JSON.stringify(challengeId)}; + common.challengeId = !{JSON.stringify(id)}; common.challengeName = !{JSON.stringify(name)}; common.challengeSeed = !{JSON.stringify(challengeSeed)}; common.challengeType = !{JSON.stringify(challengeType)}; diff --git a/server/views/coursewares/showStep.jade b/server/views/challenges/showStep.jade similarity index 95% rename from server/views/coursewares/showStep.jade rename to server/views/challenges/showStep.jade index 96bda010c0..b3d90e484f 100644 --- a/server/views/coursewares/showStep.jade +++ b/server/views/challenges/showStep.jade @@ -37,11 +37,11 @@ block content if (user) #challenge-step-btn-submit.animated.fadeIn.btn.btn-lg.btn-primary.btn-block Submit and go to my next challenge else - a.btn.btn-lg.btn-primary.btn-block(href='/challenges/next-challenge?id=' + challengeId) Go to my next challenge + a.btn.btn-lg.btn-primary.btn-block(href='/challenges/next-challenge?id=' + id) Go to my next challenge script. var common = window.common || { init: [] }; - common.challengeId = !{JSON.stringify(challengeId)}; + common.challengeId = !{JSON.stringify(id)}; common.challengeName = !{JSON.stringify(name)}; common.challengeType = !{JSON.stringify(challengeType)}; common.dashedName = !{JSON.stringify(dashedName || '')}; diff --git a/server/views/coursewares/showVideo.jade b/server/views/challenges/showVideo.jade similarity index 92% rename from server/views/coursewares/showVideo.jade rename to server/views/challenges/showVideo.jade index df8d00dc92..1655d94ecc 100644 --- a/server/views/coursewares/showVideo.jade +++ b/server/views/challenges/showVideo.jade @@ -6,7 +6,7 @@ block content .well h4 ol - for step, index in details + for step, index in description .row.checklist-element(id="#{dashedName + index}") .col-xs-3.col-sm-1.col-md-2.padded-ionic-icon.text-center input(type='checkbox' class='challenge-list-checkbox') @@ -19,7 +19,7 @@ block content if (user) a.btn.btn-primary.btn-big.btn-block#completed-courseware-editorless I've completed this challenge (ctrl + enter) else - a.btn.btn-big.btn-primary.btn-block(href='/challenges/next-challenge?id=' + challengeId) I've completed this challenge (ctrl + enter) + a.btn.btn-big.btn-primary.btn-block(href='/challenges/next-challenge?id=' + id) I've completed this challenge (ctrl + enter) script. var userLoggedIn = true; .button-spacer @@ -51,13 +51,13 @@ block content if (user) a.btn.btn-lg.btn-primary.btn-block#next-courseware-button(name='_csrf', value=_csrf) I've completed this challenge (ctrl + enter) else - a.animated.fadeIn.btn.btn-lg.btn-primary.btn-block(href='/challenges/next-challenge?id=' + challengeId) I've completed this challenge (ctrl + enter) + a.animated.fadeIn.btn.btn-lg.btn-primary.btn-block(href='/challenges/next-challenge?id=' + id) I've completed this challenge (ctrl + enter) include ../partials/challenge-modals script. var common = window.common || { init: [] }; - common.challengeId = !{JSON.stringify(challengeId)}; + common.challengeId = !{JSON.stringify(id)}; common.challengeName = !{JSON.stringify(name)}; common.challengeType = !{JSON.stringify(challengeType)}; common.dashedName = !{JSON.stringify(dashedName)}; diff --git a/server/views/coursewares/showZiplineOrBasejump.jade b/server/views/challenges/showZiplineOrBasejump.jade similarity index 95% rename from server/views/coursewares/showZiplineOrBasejump.jade rename to server/views/challenges/showZiplineOrBasejump.jade index 5d14b605d6..d3daf29848 100644 --- a/server/views/coursewares/showZiplineOrBasejump.jade +++ b/server/views/challenges/showZiplineOrBasejump.jade @@ -6,7 +6,7 @@ block content hr h4 ol - for step, index in details + for step, index in description .row.checklist-element(id="#{dashedName + index}") .col-xs-3.col-sm-1.col-md-2.padded-ionic-icon.text-center input(type='checkbox' class='challenge-list-checkbox') @@ -21,7 +21,7 @@ block content script. var userLoggedIn = true; else - a.btn.btn-big.btn-primary.btn-block(href='/challenges/next-challenge?id=' + challengeId) Go to my next challenge (ctrl + enter) + a.btn.btn-big.btn-primary.btn-block(href='/challenges/next-challenge?id=' + id) Go to my next challenge (ctrl + enter) .button-spacer .btn-group.input-group.btn-group-justified .btn.btn-primary.btn-primary-ghost.btn-big#challenge-help-btn @@ -65,12 +65,12 @@ block content a.btn.btn-lg.btn-block.btn-twitter(target="_blank", href="https://twitter.com/intent/tweet?text=Check%20out%20the%20project%20I%20just%20built%20with%20%40FreeCodeCamp:%20PASTE_YOUR_CODEPEN_URL_HERE_USING_FULL_INSTEAD_OF_PEN%20%0A%20%23LearnToCode%20%23JavaScript", onclick="ga('send', 'event', 'twitter', 'share', 'challenge completion share');") i.fa.fa-twitter  Tweet this project else - a.btn.btn-lg.btn-primary.btn-block(href='/challenges/next-challenge?id=' + challengeId) Go to my next challenge + a.btn.btn-lg.btn-primary.btn-block(href='/challenges/next-challenge?id=' + id) Go to my next challenge include ../partials/challenge-modals script. var common = window.common || { init: [] }; - common.challengeId = !{JSON.stringify(challengeId)}; + common.challengeId = !{JSON.stringify(id)}; common.challengeName = !{JSON.stringify(name)}; common.dashedName = !{JSON.stringify(dashedName)}; common.challengeType = !{JSON.stringify(challengeType)}; diff --git a/server/views/challengeMap/show.jade b/server/views/map/show.jade similarity index 100% rename from server/views/challengeMap/show.jade rename to server/views/map/show.jade