Abstract map and showChallenge logic

Rename some legacy naming conventions
Remove logic for waypoint,basejumps, etc...
This commit is contained in:
Berkeley Martinez
2016-01-11 22:47:49 -08:00
parent 5b681bfdfd
commit b03e71e7c0
10 changed files with 173 additions and 203 deletions

View File

@ -31,11 +31,7 @@ destroy()
var challenges = challengeSpec.challenges var challenges = challengeSpec.challenges
.map(function(challenge, index) { .map(function(challenge, index) {
// NOTE(berks): add title for displaying in views challenge.name = challenge.title.replace(/[^a-zA-Z0-9\s]/g, '');
challenge.name =
_.capitalize(challenge.type) +
': ' +
challenge.title.replace(/[^a-zA-Z0-9\s]/g, '');
challenge.dashedName = challenge.name challenge.dashedName = challenge.name
.toLowerCase() .toLowerCase()

View File

@ -33,13 +33,13 @@ const debug = debugFactory('freecc:challenges');
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i; const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
const firstChallenge = 'waypoint-learn-how-free-code-camp-works'; const firstChallenge = 'waypoint-learn-how-free-code-camp-works';
const challengeView = { const challengeView = {
0: 'coursewares/showHTML', 0: 'challenges/showHTML',
1: 'coursewares/showJS', 1: 'challenges/showJS',
2: 'coursewares/showVideo', 2: 'challenges/showVideo',
3: 'coursewares/showZiplineOrBasejump', 3: 'challenges/showZiplineOrBasejump',
4: 'coursewares/showZiplineOrBasejump', 4: 'challenges/showZiplineOrBasejump',
5: 'coursewares/showBonfire', 5: 'challenges/showBonfire',
7: 'coursewares/showStep' 7: 'challenges/showStep'
}; };
function isChallengeCompleted(user, challengeId) { function isChallengeCompleted(user, challengeId) {
@ -50,9 +50,11 @@ function isChallengeCompleted(user, challengeId) {
challenge.id === challengeId ); challenge.id === challengeId );
} }
/*
function numberWithCommas(x) { function numberWithCommas(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
} }
*/
function updateUserProgress(user, challengeId, completedChallenge) { function updateUserProgress(user, challengeId, completedChallenge) {
let { completedChallenges } = user; let { completedChallenges } = user;
@ -116,6 +118,131 @@ function shouldNotFilterComingSoon({ isComingSoon, isBeta: challengeIsBeta }) {
(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) { module.exports = function(app) {
const router = app.loopback.Router(); const router = app.loopback.Router();
@ -167,8 +294,6 @@ module.exports = function(app) {
.shareReplay(); .shareReplay();
const User = app.models.User; const User = app.models.User;
const userCount$ = observeMethod(User, 'count');
const send200toNonUser = ifNoUserSend(true); const send200toNonUser = ifNoUserSend(true);
router.post( router.post(
@ -182,13 +307,13 @@ module.exports = function(app) {
completedZiplineOrBasejump completedZiplineOrBasejump
); );
router.get('/map', challengeMap); router.get('/map', showMap);
router.get( router.get(
'/challenges/next-challenge', '/challenges/next-challenge',
returnNextChallenge returnNextChallenge
); );
router.get('/challenges/:challengeName', returnIndividualChallenge); router.get('/challenges/:challengeName', showChallenge);
app.use(router); app.use(router);
@ -279,92 +404,20 @@ module.exports = function(app) {
); );
} }
function returnIndividualChallenge(req, res, next) { function showChallenge(req, res, next) {
const origChallengeName = req.params.challengeName; const solution = req.query.solution;
const solutionCode = req.query.solution;
const unDashedName = unDasherize(origChallengeName);
const challengeName = challengesRegex.test(unDashedName) ? getRenderData$(req.user, challenge$, req.params.challengeName, solution)
// 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()
});
})
.subscribe( .subscribe(
function(data) { ({ type, redirectUrl, message, data }) => {
if (typeof data === 'string') { if (message) {
debug('redirecting to %s', data); req.flash('info', {
return res.redirect(data); msg: message
});
}
if (type === 'redirect') {
debug('redirecting to %s', redirectUrl);
return res.redirect(redirectUrl);
} }
var view = challengeView[data.challengeType]; var view = challengeView[data.challengeType];
res.render(view, data); res.render(view, data);
@ -500,93 +553,18 @@ module.exports = function(app) {
); );
} }
function challengeMap({ user = {} }, res, next) { function showMap({ user = {} }, res, next) {
let lastCompleted;
const daysRunning = moment().diff(new Date('10/15/2014'), 'days');
// if user // if user
// get the id's of all the users completed challenges // get the id's of all the users completed challenges
const completedChallenges = !user.completedChallenges ? const completedChallenges = !user.completedChallenges ?
[] : [] :
_.uniq(user.completedChallenges).map(({ id, _id }) => id || _id); _.uniq(user.completedChallenges).map(({ id, _id }) => id || _id);
const camperCount$ = userCount$() getSuperBlocks$(challenge$, completedChallenges)
.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 })
)
.subscribe( .subscribe(
({ camperCount, superBlocks }) => { superBlocks => {
res.render('challengeMap/show', { res.render('map/show', {
superBlocks, 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' title: 'A Map to Learn to Code and Become a Software Engineer'
}); });
}, },

View File

@ -1,4 +1,3 @@
import _ from 'lodash';
import path from 'path'; import path from 'path';
import { Observable } from 'rx'; import { Observable } from 'rx';
@ -22,10 +21,7 @@ export default function getFromDisk$(challenge) {
challenge.tail = challenge.tail || []; challenge.tail = challenge.tail || [];
challenge.challengeType = '' + challenge.challengeType; challenge.challengeType = '' + challenge.challengeType;
challenge.name = challenge.name = challenge.title.replace(/[^a-zA-Z0-9\s]/g, '');
_.capitalize(challenge.type) +
': ' +
challenge.title.replace(/[^a-zA-Z0-9\s]/g, '');
challenge.dashedName = challenge.name challenge.dashedName = challenge.name
.toLowerCase() .toLowerCase()

View File

@ -17,7 +17,7 @@ block content
.row .row
.col-xs-12 .col-xs-12
.bonfire-instructions .bonfire-instructions
for sentence in details for sentence in description
if (/blockquote|h4|table/.test(sentence)) if (/blockquote|h4|table/.test(sentence))
!=sentence !=sentence
else else
@ -83,7 +83,7 @@ block content
i.fa.fa-twitter   i.fa.fa-twitter  
= phrase = phrase
else 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 include ../partials/challenge-modals
script(type="text/javascript"). script(type="text/javascript").
var common = window.common = window.common || { init: [] }; var common = window.common = window.common || { init: [] };
@ -92,7 +92,7 @@ block content
common.head = !{JSON.stringify(head)}; common.head = !{JSON.stringify(head)};
common.tail = !{JSON.stringify(tail)}; common.tail = !{JSON.stringify(tail)};
common.challengeId = !{JSON.stringify(challengeId)}; common.challengeId = !{JSON.stringify(id)};
common.challengeName = !{JSON.stringify(name)}; common.challengeName = !{JSON.stringify(name)};
common.challengeSeed = !{JSON.stringify(challengeSeed)}; common.challengeSeed = !{JSON.stringify(challengeSeed)};
common.challengeType = !{JSON.stringify(challengeType)}; common.challengeType = !{JSON.stringify(challengeType)};

View File

@ -16,7 +16,7 @@ block content
i.ion-checkmark-circled.text-primary(title="Completed") i.ion-checkmark-circled.text-primary(title="Completed")
hr hr
.bonfire-instructions .bonfire-instructions
for sentence in details for sentence in description
p.wrappable.negative-10!= sentence p.wrappable.negative-10!= sentence
.negative-bottom-margin-30 .negative-bottom-margin-30
.button-spacer .button-spacer
@ -74,7 +74,7 @@ block content
if(user) if(user)
#submit-challenge.animated.fadeIn.btn.btn-lg.btn-primary.btn-block Submit and go to my next challenge (ctrl + enter) #submit-challenge.animated.fadeIn.btn.btn-lg.btn-primary.btn-block Submit and go to my next challenge (ctrl + enter)
else 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 include ../partials/challenge-modals
script(type="text/javascript"). script(type="text/javascript").
$('#next-courseware-button').attr('disabled', 'disabled'); $('#next-courseware-button').attr('disabled', 'disabled');
@ -84,7 +84,7 @@ block content
common.head = !{JSON.stringify(head)}; common.head = !{JSON.stringify(head)};
common.tail = !{JSON.stringify(tail)}; common.tail = !{JSON.stringify(tail)};
common.challengeId = !{JSON.stringify(challengeId)}; common.challengeId = !{JSON.stringify(id)};
common.challengeName = !{JSON.stringify(name)}; common.challengeName = !{JSON.stringify(name)};
common.challengeSeed = !{JSON.stringify(challengeSeed)}; common.challengeSeed = !{JSON.stringify(challengeSeed)};
common.challengeType = !{JSON.stringify(challengeType)}; common.challengeType = !{JSON.stringify(challengeType)};

View File

@ -17,7 +17,7 @@ block content
.row .row
.col-xs-12 .col-xs-12
.bonfire-instructions .bonfire-instructions
for sentence in details for sentence in description
if (/blockquote|h4|table/.test(sentence)) if (/blockquote|h4|table/.test(sentence))
!=sentence !=sentence
else else
@ -82,7 +82,7 @@ block content
if (user) if (user)
#submit-challenge.animated.fadeIn.btn.btn-lg.btn-primary.btn-block Submit and go to my next challenge (ctrl + enter) #submit-challenge.animated.fadeIn.btn.btn-lg.btn-primary.btn-block Submit and go to my next challenge (ctrl + enter)
else 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 include ../partials/challenge-modals
script(type="text/javascript"). script(type="text/javascript").
var common = window.common = { init: [] }; var common = window.common = { init: [] };
@ -91,7 +91,7 @@ block content
common.head = !{JSON.stringify(head)}; common.head = !{JSON.stringify(head)};
common.tail = !{JSON.stringify(tail)}; common.tail = !{JSON.stringify(tail)};
common.challengeId = !{JSON.stringify(challengeId)}; common.challengeId = !{JSON.stringify(id)};
common.challengeName = !{JSON.stringify(name)}; common.challengeName = !{JSON.stringify(name)};
common.challengeSeed = !{JSON.stringify(challengeSeed)}; common.challengeSeed = !{JSON.stringify(challengeSeed)};
common.challengeType = !{JSON.stringify(challengeType)}; common.challengeType = !{JSON.stringify(challengeType)};

View File

@ -37,11 +37,11 @@ block content
if (user) if (user)
#challenge-step-btn-submit.animated.fadeIn.btn.btn-lg.btn-primary.btn-block Submit and go to my next challenge #challenge-step-btn-submit.animated.fadeIn.btn.btn-lg.btn-primary.btn-block Submit and go to my next challenge
else 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. script.
var common = window.common || { init: [] }; var common = window.common || { init: [] };
common.challengeId = !{JSON.stringify(challengeId)}; common.challengeId = !{JSON.stringify(id)};
common.challengeName = !{JSON.stringify(name)}; common.challengeName = !{JSON.stringify(name)};
common.challengeType = !{JSON.stringify(challengeType)}; common.challengeType = !{JSON.stringify(challengeType)};
common.dashedName = !{JSON.stringify(dashedName || '')}; common.dashedName = !{JSON.stringify(dashedName || '')};

View File

@ -6,7 +6,7 @@ block content
.well .well
h4 h4
ol ol
for step, index in details for step, index in description
.row.checklist-element(id="#{dashedName + index}") .row.checklist-element(id="#{dashedName + index}")
.col-xs-3.col-sm-1.col-md-2.padded-ionic-icon.text-center .col-xs-3.col-sm-1.col-md-2.padded-ionic-icon.text-center
input(type='checkbox' class='challenge-list-checkbox') input(type='checkbox' class='challenge-list-checkbox')
@ -19,7 +19,7 @@ block content
if (user) if (user)
a.btn.btn-primary.btn-big.btn-block#completed-courseware-editorless I've completed this challenge (ctrl + enter) a.btn.btn-primary.btn-big.btn-block#completed-courseware-editorless I've completed this challenge (ctrl + enter)
else 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. script.
var userLoggedIn = true; var userLoggedIn = true;
.button-spacer .button-spacer
@ -51,13 +51,13 @@ block content
if (user) if (user)
a.btn.btn-lg.btn-primary.btn-block#next-courseware-button(name='_csrf', value=_csrf) I've completed this challenge (ctrl + enter) a.btn.btn-lg.btn-primary.btn-block#next-courseware-button(name='_csrf', value=_csrf) I've completed this challenge (ctrl + enter)
else 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 include ../partials/challenge-modals
script. script.
var common = window.common || { init: [] }; var common = window.common || { init: [] };
common.challengeId = !{JSON.stringify(challengeId)}; common.challengeId = !{JSON.stringify(id)};
common.challengeName = !{JSON.stringify(name)}; common.challengeName = !{JSON.stringify(name)};
common.challengeType = !{JSON.stringify(challengeType)}; common.challengeType = !{JSON.stringify(challengeType)};
common.dashedName = !{JSON.stringify(dashedName)}; common.dashedName = !{JSON.stringify(dashedName)};

View File

@ -6,7 +6,7 @@ block content
hr hr
h4 h4
ol ol
for step, index in details for step, index in description
.row.checklist-element(id="#{dashedName + index}") .row.checklist-element(id="#{dashedName + index}")
.col-xs-3.col-sm-1.col-md-2.padded-ionic-icon.text-center .col-xs-3.col-sm-1.col-md-2.padded-ionic-icon.text-center
input(type='checkbox' class='challenge-list-checkbox') input(type='checkbox' class='challenge-list-checkbox')
@ -21,7 +21,7 @@ block content
script. script.
var userLoggedIn = true; var userLoggedIn = true;
else 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 .button-spacer
.btn-group.input-group.btn-group-justified .btn-group.input-group.btn-group-justified
.btn.btn-primary.btn-primary-ghost.btn-big#challenge-help-btn .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');") 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 i.fa.fa-twitter  Tweet this project
else 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 include ../partials/challenge-modals
script. script.
var common = window.common || { init: [] }; var common = window.common || { init: [] };
common.challengeId = !{JSON.stringify(challengeId)}; common.challengeId = !{JSON.stringify(id)};
common.challengeName = !{JSON.stringify(name)}; common.challengeName = !{JSON.stringify(name)};
common.dashedName = !{JSON.stringify(dashedName)}; common.dashedName = !{JSON.stringify(dashedName)};
common.challengeType = !{JSON.stringify(challengeType)}; common.challengeType = !{JSON.stringify(challengeType)};