diff --git a/client/commonFramework/step-challenge.js b/client/commonFramework/step-challenge.js index f70bc469bb..97a19754d3 100644 --- a/client/commonFramework/step-challenge.js +++ b/client/commonFramework/step-challenge.js @@ -125,7 +125,7 @@ window.common = (function({ $, common = { init: [] }}) { // assume api returns string when fails return $el.parent() .find('.disabled') - .replaceWith('

' + data + '

'); + .replaceWith('

' + data + '

'); }) .fail(function() { console.log('failed'); diff --git a/client/less/main.less b/client/less/main.less index 60320c5d6a..0bd7729021 100644 --- a/client/less/main.less +++ b/client/less/main.less @@ -64,7 +64,6 @@ body.top-and-bottom-margins { } body.no-top-and-bottom-margins { - display: none; margin: 75px 20px 0px 20px; } diff --git a/client/main.js b/client/main.js index 05fd4dc69f..1efdb3e0d6 100644 --- a/client/main.js +++ b/client/main.js @@ -165,6 +165,7 @@ main.setMapShare = function setMapShare(id) { $(document).ready(function() { + const { Observable } = window.Rx; var CSRF_HEADER = 'X-CSRF-Token'; var setCSRFToken = function(securityToken) { @@ -548,51 +549,68 @@ $(document).ready(function() { // keyboard shortcuts: open map window.Mousetrap.bind('g m', toggleMap); - // Night Mode - function changeMode() { - var newValue = false; - try { - newValue = !JSON.parse(localStorage.getItem('nightMode')); - } catch (e) { - console.error('Error parsing value form local storage:', 'nightMode', e); - } - localStorage.setItem('nightMode', String(newValue)); - toggleNightMode(newValue); + function addAlert(message = '', type = 'alert-info') { + return $('.flashMessage').append($(` +
+ +
${message}
+
+ `)); } - function toggleNightMode(nightModeEnabled) { - var iframe = document.getElementById('map-aside-frame'); - if (iframe) { - iframe.src = iframe.src; + function toggleNightMode() { + if (!main.userId) { + return addAlert('You must be logged in to use our themes.'); } - var body = $('body'); - body.hide(); - if (nightModeEnabled) { - body.addClass('night'); + const iframe$ = document.getElementById('map-aside-frame'); + const body$ = $('body'); + if (iframe$) { + iframe$.src = iframe$.src; + } + body$.hide(); + let updateThemeTo; + if (body$.hasClass('night')) { + body$.removeClass('night'); + updateThemeTo = 'default'; } else { - body.removeClass('night'); + body$.addClass('night'); + updateThemeTo = 'night'; } - body.fadeIn('100'); + body$.fadeIn('100'); + const options = { + url: `/api/users/${main.userId}/update-theme`, + type: 'POST', + data: { theme: updateThemeTo }, + dataType: 'json' + }; + return $.ajax(options) + .success(() => console.log('theme updated successfully')) + .fail(err => { + let message; + try { + message = JSON.parse(err.responseText).error.message; + } catch (error) { + return null; + } + if (!message) { + return null; + } + return addAlert(message); + }); } - if (typeof localStorage.getItem('nightMode') !== 'undefined') { - var oldVal = false; - try { - oldVal = JSON.parse(localStorage.getItem('nightMode')); - } catch (e) { - console.error('Error parsing value form local storage:', 'nightMode', e); - } - toggleNightMode(oldVal); - $('.nightMode-btn').on('click', function() { - changeMode(); - }); - } else { - localStorage.setItem('nightMode', 'false'); - toggleNightMode('false'); - } + Observable.merge( + Observable.fromEvent($('#night-mode'), 'click'), + Observable.create(observer => { + window.Mousetrap.bind('g t n', () => observer.onNext()); + }) + ) + .debounce(500) + .subscribe(toggleNightMode, err => console.error(err)); // Hot Keys - window.Mousetrap.bind('g t n', changeMode); window.Mousetrap.bind('g n n', () => { // Next Challenge window.location = '/challenges/next-challenge'; diff --git a/common/models/user.js b/common/models/user.js index 0ced33a968..4d2def2304 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -3,6 +3,7 @@ import uuid from 'node-uuid'; import moment from 'moment'; import dedent from 'dedent'; import debugFactory from 'debug'; +import { isEmail } from 'validator'; import { saveUser, observeMethod } from '../../server/utils/rx'; import { blacklistedUsernames } from '../../server/utils/constants'; @@ -62,6 +63,9 @@ module.exports = function(User) { User.observe('before save', function({ instance: user }, next) { if (user) { + if (user.email && !isEmail(user.email)) { + return next(new Error('Email format is not valid')); + } user.username = user.username.trim().toLowerCase(); user.email = typeof user.email === 'string' ? user.email.trim().toLowerCase() : @@ -75,7 +79,7 @@ module.exports = function(User) { user.progressTimestamps.push({ timestamp: Date.now() }); } } - next(); + return next(); }); debug('setting up user hooks'); @@ -93,6 +97,9 @@ module.exports = function(User) { if (!req.body.email) { return next(); } + if (!isEmail(req.body.email)) { + return next(new Error('Email format is not valid')); + } return User.doesExist(null, req.body.email) .then(exists => { if (!exists) { @@ -118,6 +125,10 @@ module.exports = function(User) { }); User.on('resetPasswordRequest', function(info) { + if (!isEmail(info.email)) { + console.error(new Error('Email format is not valid')); + return null; + } let url; const host = User.app.get('host'); const { id: token } = info.accessToken; @@ -150,7 +161,7 @@ module.exports = function(User) { ` }; - User.app.models.Email.send(mailOptions, function(err) { + return User.app.models.Email.send(mailOptions, function(err) { if (err) { console.error(err); } debug('email reset sent'); }); @@ -159,9 +170,12 @@ module.exports = function(User) { User.beforeRemote('login', function(ctx, notUsed, next) { const { body } = ctx.req; if (body && typeof body.email === 'string') { + if (!isEmail(body.email)) { + return next(new Error('Email format is not valid')); + } body.email = body.email.toLowerCase(); } - next(); + return next(); }); User.afterRemote('login', function(ctx, accessToken, next) { @@ -216,7 +230,7 @@ module.exports = function(User) { }); User.doesExist = function doesExist(username, email) { - if (!username && !email) { + if (!username && (!email || !isEmail(email))) { return Promise.resolve(false); } debug('checking existence'); @@ -309,6 +323,11 @@ module.exports = function(User) { ); User.prototype.updateEmail = function updateEmail(email) { + if (!isEmail(email)) { + return Promise.reject( + new Error('The submitted email not valid') + ); + } if (this.email && this.email === email) { return Promise.reject(new Error( `${email} is already associated with this account.` @@ -467,6 +486,91 @@ module.exports = function(User) { } ); + User.prototype.updateEmail = function updateEmail(email) { + if (this.email && this.email === email) { + return Promise.reject(new Error( + `${email} is already associated with this account.` + )); + } + return User.doesExist(null, email) + .then(exists => { + if (exists) { + return Promise.reject( + new Error(`${email} is already associated with another account.`) + ); + } + return this.update$({ email }).toPromise(); + }); + }; + + User.remoteMethod( + 'updateEmail', + { + isStatic: false, + description: 'updates the email of the user object', + accepts: [ + { + arg: 'email', + type: 'string', + required: true + } + ], + returns: [ + { + arg: 'status', + type: 'object' + } + ], + http: { + path: '/update-email', + verb: 'POST' + } + } + ); + + User.themes = { + night: true, + default: true + }; + User.prototype.updateTheme = function updateTheme(theme) { + if (!this.constructor.themes[theme]) { + const err = new Error( + 'Theme is not valid.' + ); + err.messageType = 'info'; + err.userMessage = err.message; + return Promise.reject(err); + } + return this.update$({ theme }) + .map({ updatedTo: theme }) + .toPromise(); + }; + + User.remoteMethod( + 'updateTheme', + { + isStatic: false, + description: 'updates the users chosen theme', + accepts: [ + { + arg: 'theme', + type: 'string', + required: true + } + ], + returns: [ + { + arg: 'status', + type: 'object' + } + ], + http: { + path: '/update-theme', + verb: 'POST' + } + } + ); + // user.updateTo$(updateData: Object) => Observable[Number] User.prototype.update$ = function update$(updateData) { const id = this.getId(); diff --git a/common/models/user.json b/common/models/user.json index ad7294688d..cdbff24c58 100644 --- a/common/models/user.json +++ b/common/models/user.json @@ -188,6 +188,10 @@ }, "timezone": { "type": "string" + }, + "theme": { + "type": "string", + "default": "default" } }, "validations": [], @@ -242,6 +246,13 @@ "principalId": "$owner", "permission": "ALLOW", "property": "updateEmail" + }, + { + "accessType": "EXECUTE", + "principalType": "ROLE", + "principalId": "$owner", + "permission": "ALLOW", + "property": "updateTheme" } ], "methods": [] diff --git a/seed/challenges/01-front-end-development-certification/basic-javascript.json b/seed/challenges/01-front-end-development-certification/basic-javascript.json index 579fd5c4c9..4edf9b445e 100644 --- a/seed/challenges/01-front-end-development-certification/basic-javascript.json +++ b/seed/challenges/01-front-end-development-certification/basic-javascript.json @@ -453,7 +453,7 @@ "tests": [ "assert(myVar === 88, 'message: myVar should equal 88');", "assert(/myVar\\s*\\=.*myVar/.test(code) === false, 'message: myVar = myVar should be changed');", - "assert(/myVar\\s*[+]{2}/.test(code), 'message: Use the ++ operator');", + "assert(/[+]{2}\\s*myVar|myVar\\s*[+]{2}/.test(code), 'message: Use the ++ operator');", "assert(/var myVar = 87;/.test(code), 'message: Do not change code above the line');" ], "type": "waypoint", @@ -497,7 +497,7 @@ ], "tests": [ "assert(myVar === 10, 'message: myVar should equal 10');", - "assert(/myVar\\s*[-]{2}/.test(code), 'message: Use the -- operator on myVar');", + "assert(/[-]{2}\\s*myVar|myVar\\s*[-]{2}/.test(code), 'message: Use the -- operator on myVar');", "assert(/var myVar = 11;/.test(code), 'message: Do not change code above the line');" ], "type": "waypoint", diff --git a/seed/challenges/01-front-end-development-certification/bootstrap.json b/seed/challenges/01-front-end-development-certification/bootstrap.json index 323a88e233..c8ea98fb5e 100644 --- a/seed/challenges/01-front-end-development-certification/bootstrap.json +++ b/seed/challenges/01-front-end-development-certification/bootstrap.json @@ -365,9 +365,14 @@ "id": "bad87fee1348cd8acef08812", "title": "Create a Block Element Bootstrap Button", "description": [ - "Normally, your button elements are only as wide as the text that they contain. By making them block elements, your button will stretch to fill your page's entire horizontal space and any elements following it will flow onto a \"new line\" below the block.", - "This image illustrates the difference between inline elements and block-level elements:", - "\"An", + "Normally, your button elements with a class of btn are only as wide as the text that they contain. For example:", + "<button class=\"btn\">Submit</button>", + "This button would only be as wide as the word \"Submit\".", + "", + "By making them block elements with the additional class of btn-block, your button will stretch to fill your page's entire horizontal space and any elements following it will flow onto a \"new line\" below the block.", + "<button class=\"btn btn-block\">Submit</button>", + "This button would take up 100% of the available width.", + "", "Note that these buttons still need the btn class.", "Add Bootstrap's btn-block class to your Bootstrap button." ], @@ -440,17 +445,27 @@ "challengeType": 0, "titleEs": "Crea un elemento botón de bloque con Bootstrap", "descriptionEs": [ - "Normalmente, tus elementos button son sólo tan anchos como el texto que contienen. Al convertir un botón en elemento a nivel de bloque, este se estirará para llenar todo el espacio horizontal y cualquier elemento que lo siga se desplazará a una \"nueva línea\" debajo del bloque.", - "Esta imagen es un ejemplo de la diferencia entre elementos en línea (inline) y elementos a nivel de bloque (block-level):", - "\"Un", + "Normalmente , los elementos de button con una clase de btn sólo son tan ancha como el texto que contienen. Por ejemplo:", + "<button class=\"btn\">Enviar</button>", + "Este botón sólo sería tan amplia como la palabra \"Enviar\"", + "", + "Haciéndolos bloquean elementos con la clase adicional de btn-block, el botón se amplía para llenar toda espacio horizontal de la página y los elementos siguientes fluirá sobre una \"nueva línea\" debajo del bloque .", + "<button class=\"btn btn-block\">Enviar</button>", + "Este botón llevaría hasta el 100% de la anchura disponible.", + "", "Ten en cuenta que estos botones todavía necesitan la clase btn.", "Agrega la clase de Bootstrap btn-block a tu botón Bootstrap." ], "nameFr": "Créer un bouton bloc Bootstrap", "descriptionFr": [ - "Normalement, vos éléments button sont aussi large que le texte qu'ils contiennent. En les transformants en éléments blocs, vos boutons vont s'ajuster pour remplir l'intégralité de l'espace horizontal de la page et tous les éléments qui le suivront se placeront sur une \"nouvelle ligne\" en dessous du bloc.", - "Cette image illustre la différence entre éléments inline (sans briser la ligne) et éléments block-level (en blocs)", - "\"Un", + "Normalement , vos éléments de button avec une classe de btn ne sont aussi larges que le texte qu'ils contiennent . Par exemple:", + "<button class=\"btn\">Soumettre</button>", + "Ce bouton ne serait plus large que le mot \"Soumettre\" .", + "", + "En leur faisant bloquer les éléments avec la classe supplémentaire de btn-block, votre bouton étirer pour remplir tout l'espace horizontal de votre page et tous les éléments suivants, il coulera sur une \"nouvelle ligne\" en dessous du bloc .", + "<button class=\"btn btn-block\">Soumettre</button>", + "Ce bouton prendrait 100% de la largeur disponible .", + "", "Notez que ces boutons ont toujours besoin de la classe btn", "Ajoutez la classe Bootstrap btn-block à votre bouton Bootstrap." ] diff --git a/seed/challenges/01-front-end-development-certification/html5-and-css.json b/seed/challenges/01-front-end-development-certification/html5-and-css.json index 886b2bf8d5..5719312ee4 100644 --- a/seed/challenges/01-front-end-development-certification/html5-and-css.json +++ b/seed/challenges/01-front-end-development-certification/html5-and-css.json @@ -1953,8 +1953,8 @@ "

Top 3 things cats hate:

" ], "tests": [ - "assert.equal($(\"ol\").prev().text(), 'Top 3 things cats hate:', 'message: Your should have an ordered list for \"Top 3 things cats hate\"');", - "assert.equal($(\"ul\").prev().text(), \"Things cats love:\", 'message: You should have an unordered list for \"Things Cats Love\"');", + "assert.equal($(\"ol\").prev().text(), 'Top 3 things cats hate:', 'message: You should have an ordered list for \"Top 3 things cats hate:\"');", + "assert.equal($(\"ul\").prev().text(), \"Things cats love:\", 'message: You should have an unordered list for \"Things cats love:\"');", "assert.equal($(\"ul li\").length, 3, 'message: You should have three li elements within your ul element.');", "assert.equal($(\"ol li\").length, 3, 'message: You should have three li elements within your ol element.');", "assert(code.match(/<\\/ul>/g) && code.match(/<\\/ul>/g).length === code.match(/