From 5a28b5bbe6f70c996684aa2e6bd31ab51492df98 Mon Sep 17 00:00:00 2001 From: Eric Leung Date: Sun, 17 Apr 2016 19:46:54 -0700 Subject: [PATCH 1/9] Add tests Get Geo-location Data challenge --- .../json-apis-and-ajax.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/seed/challenges/01-front-end-development-certification/json-apis-and-ajax.json b/seed/challenges/01-front-end-development-certification/json-apis-and-ajax.json index d9198a6935..f7116fe364 100644 --- a/seed/challenges/01-front-end-development-certification/json-apis-and-ajax.json +++ b/seed/challenges/01-front-end-development-certification/json-apis-and-ajax.json @@ -447,7 +447,7 @@ "You will see a prompt to allow or block this site from knowing your current location. The challenge can be completed either way, as long as the code is correct.", "By selecting allow you will see the text on the output phone change to your latitude and longitude", "Here's some code that does this:", - "
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(position) {
$(\"#data\").html(\"latitude: \" + position.coords.latitude + \"<br>longitude: \" + position.coords.longitude);
});
}
" + "
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(position) {
$(\"#data\").html(\"latitude: \" + position.coords.latitude + \"<br>longitude: \" + position.coords.longitude);
});
}
" ], "challengeSeed": [ "fccss", @@ -463,7 +463,10 @@ "" ], "tests": [ - "assert(code.match(/navigator\\.geolocation\\.getCurrentPosition/gi), 'message: You should make use of the navigator.geolocation to access the users current location.');" + "assert(code.match(/navigator\\.geolocation\\.getCurrentPosition/gi), 'message: You should make use of navigator.geolocation to access the users current location.');", + "assert(code.match(/position\\.coords\\.latitude/gi), 'message: You should make use of position.coords.latitude to display the users' latitudinal location.');", + "assert(code.match(/position\\.coords\\.longitude/gi), 'message: You should make use of position.coords.longitude to display the users' longitudinal location.');", + "assert(code.match(/\\$\\(\\s*\"#data\"\\s*\\)\\s*\\.html\\([.\\w\\W]*?\\);/gi), 'message: You should display the users' position within the data div element');" ], "type": "waypoint", "challengeType": 0, From 535e68f9e3d8a2342fe7ed949e3ae6779bd9c95f Mon Sep 17 00:00:00 2001 From: hangaebal Date: Mon, 9 May 2016 23:31:29 +0900 Subject: [PATCH 2/9] Fix Claim Certificates - Challenges - Text Rendering Issues --- client/commonFramework/step-challenge.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'); From 7f19520ee69b9662b93c0e8ee8fe1b457e69c6f5 Mon Sep 17 00:00:00 2001 From: Riyazuddin M Date: Tue, 10 May 2016 23:13:32 +1000 Subject: [PATCH 3/9] Fixed the Ordered List Issue --- .../01-front-end-development-certification/html5-and-css.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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(/
    /g).length, 'message: Make sure your ul element has a closing tag.');", From 71b2d2e2a0590cb3bd0039e81ec67ea81d5ac173 Mon Sep 17 00:00:00 2001 From: Arun Date: Wed, 11 May 2016 00:02:40 +0530 Subject: [PATCH 4/9] Change number of arguments in sorted-union challenge's function to 1 --- .../intermediate-bonfires.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/seed/challenges/01-front-end-development-certification/intermediate-bonfires.json b/seed/challenges/01-front-end-development-certification/intermediate-bonfires.json index 839a72e3f1..48e6dae67b 100644 --- a/seed/challenges/01-front-end-development-certification/intermediate-bonfires.json +++ b/seed/challenges/01-front-end-development-certification/intermediate-bonfires.json @@ -481,14 +481,14 @@ "Remember to use Read-Search-Ask if you get stuck. Try to pair program. Write your own code." ], "challengeSeed": [ - "function uniteUnique(arr1, arr2, arr3) {", - " return arr1;", + "function uniteUnique(arr) {", + " return arr;", "}", "", "uniteUnique([1, 3, 2], [5, 2, 1, 4], [2, 1]);" ], "solutions": [ - "function uniteUnique(arr1, arr2, arr3) {\n return [].slice.call(arguments).reduce(function(a, b) {\n return [].concat(a, b.filter(function(e) {return a.indexOf(e) === -1;}));\n }, []);\n}" + "function uniteUnique(arr) {\n return [].slice.call(arguments).reduce(function(a, b) {\n return [].concat(a, b.filter(function(e) {return a.indexOf(e) === -1;}));\n }, []);\n}" ], "tests": [ "assert.deepEqual(uniteUnique([1, 3, 2], [5, 2, 1, 4], [2, 1]), [1, 3, 2, 5, 4], 'message: uniteUnique([1, 3, 2], [5, 2, 1, 4], [2, 1]) should return [1, 3, 2, 5, 4].');", From 833e246f8aabffd8ee02faa290dca180eab92ba3 Mon Sep 17 00:00:00 2001 From: Sarbbottam Bandyopadhyay Date: Wed, 11 May 2016 10:40:05 -0700 Subject: [PATCH 5/9] fix(a11y): using button instead of div div can be styled to look like a button, but it does not behave as a button semantically --- server/views/challenges/showStep.jade | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/views/challenges/showStep.jade b/server/views/challenges/showStep.jade index d276c836c8..732c0fb988 100644 --- a/server/views/challenges/showStep.jade +++ b/server/views/challenges/showStep.jade @@ -19,12 +19,12 @@ block content if index === 0 .col-sm-4.hidden-xs   else - .btn.btn-primary.btn-primary-ghost.col-sm-4.col-xs-12.challenge-step-btn-prev.btn-lg(id='#{index - 1}') Go to my previous step + button.btn.btn-primary.btn-primary-ghost.col-sm-4.col-xs-12.challenge-step-btn-prev.btn-lg(id='#{index - 1}') Go to my previous step .challenge-step-counter.large-p.col-sm-4.col-xs-12.text-center (#{index + 1} / #{description.length}) if index + 1 === description.length - .btn.btn-primary.col-sm-4.col-xs-12.challenge-step-btn-finish.btn-lg(id='last' class=step[3] && !isCompleted ? 'disabled' : '') Finish challenge + button.btn.btn-primary.col-sm-4.col-xs-12.challenge-step-btn-finish.btn-lg(id='last' class=step[3] && !isCompleted ? 'disabled' : '') Finish challenge else - .btn.btn-primary.col-sm-4.col-xs-12.challenge-step-btn-next.btn-lg(id='#{index}' class=step[3] && !isCompleted ? 'disabled' : '') Go to my next step + button.btn.btn-primary.col-sm-4.col-xs-12.challenge-step-btn-next.btn-lg(id='#{index}' class=step[3] && !isCompleted ? 'disabled' : '') Go to my next step .clearfix .spacer #challenge-step-modal.modal(tabindex='-1') From 5181adaf180e627d990340473b9ddf5045ca4a7a Mon Sep 17 00:00:00 2001 From: Pranay Berry Date: Fri, 13 May 2016 00:30:47 +0530 Subject: [PATCH 6/9] Fixed increment and decrement issue Implemented a more flexible check to ensure both pre and post operations are accepted. --- .../basic-javascript.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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", From 70945c2ed65fd5e1fe83ef43833c03f1a68a5b2f Mon Sep 17 00:00:00 2001 From: Bouncey Date: Mon, 9 May 2016 22:30:35 +0100 Subject: [PATCH 7/9] Fix/ btn-block Bootstrap challenge Fixes issue with image for describing the difference between btn and btn-block. The image was deemed confusing with regards to the Bootstrap btn classes PR updated inline with feedback More detailed description give with regards to `btn` and `btn-block` classes --- .../bootstrap.json | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) 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." ] From 04b874de4515b852945e0cf616345d6f27be4fa6 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 12 May 2016 15:48:34 -0700 Subject: [PATCH 8/9] Fix email validations --- common/models/user.js | 27 +++++++++++++++++++++++---- server/boot/a-extendUser.js | 3 ++- server/boot/user.js | 1 + 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/common/models/user.js b/common/models/user.js index 0ced33a968..a9584b4447 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.` diff --git a/server/boot/a-extendUser.js b/server/boot/a-extendUser.js index e43cdeff2b..932bd7dcdb 100644 --- a/server/boot/a-extendUser.js +++ b/server/boot/a-extendUser.js @@ -1,5 +1,6 @@ import { Observable } from 'rx'; import debugFactory from 'debug'; +import { isEmail } from 'validator'; const debug = debugFactory('fcc:user:remote'); @@ -59,7 +60,7 @@ module.exports = function(app) { // send welcome email to new camper User.afterRemote('create', function({ req, res }, user, next) { debug('user created, sending email'); - if (!user.email) { return next(); } + if (!user.email || !isEmail(user.email)) { return next(); } const redirect = req.session && req.session.returnTo ? req.session.returnTo : '/'; diff --git a/server/boot/user.js b/server/boot/user.js index 989a37edd6..685a2aed5c 100644 --- a/server/boot/user.js +++ b/server/boot/user.js @@ -550,6 +550,7 @@ module.exports = function(app) { } function postForgot(req, res) { + req.validate('email', 'Email format is not valid').isEmail(); const errors = req.validationErrors(); const email = req.body.email.toLowerCase(); From e72ad8c513dc07e8c8df787d988ebf75d4130661 Mon Sep 17 00:00:00 2001 From: Berkeley Martinez Date: Thu, 12 May 2016 18:52:03 -0700 Subject: [PATCH 9/9] fix(ui): Fix flash shown to user on page refresh Store user theme preference on user object. Must be logged in to use themes --- client/less/main.less | 1 - client/main.js | 90 +++++++++++-------- common/models/user.js | 128 ++++++++++++++++++---------- common/models/user.json | 11 +++ server/middlewares/global-locals.js | 4 + server/views/account/settings.jade | 4 +- server/views/layout-wide.jade | 12 +-- server/views/partials/scripts.jade | 6 ++ 8 files changed, 168 insertions(+), 88 deletions(-) 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..4ac008e5da 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('Must be logged in to use 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..b26b5cb13d 100644 --- a/common/models/user.js +++ b/common/models/user.js @@ -307,49 +307,6 @@ 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.giveBrowniePoints = function giveBrowniePoints(receiver, giver, data = {}, dev = false, cb) { const findUser = observeMethod(User, 'findOne'); @@ -467,6 +424,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/server/middlewares/global-locals.js b/server/middlewares/global-locals.js index 1871eb95ff..aa2d7e32d0 100644 --- a/server/middlewares/global-locals.js +++ b/server/middlewares/global-locals.js @@ -6,6 +6,10 @@ export default function globalLocals() { if (req.csrfToken) { res.expose({ token: res.locals._csrf }, 'csrf'); } + res.locals.theme = req.user && req.user.theme || + req.cookies.theme || + 'default'; + next(); }; } diff --git a/server/views/account/settings.jade b/server/views/account/settings.jade index c992ece48e..e904dbc413 100644 --- a/server/views/account/settings.jade +++ b/server/views/account/settings.jade @@ -5,7 +5,7 @@ block content h2.text-center Actions .row .col-xs-12 - a.btn.btn-lg.btn-block.btn-primary.btn-link-social(class = "nightMode-btn") Night Mode + a#night-mode.btn.btn-lg.btn-block.btn-primary.btn-link-social Night Mode .row .col-xs-12 if (!user.isGithubCool) @@ -133,4 +133,4 @@ block content form(action='/account/delete', method='POST') input(type='hidden', name='_csrf', value=_csrf) button.btn.btn-danger.btn-block(type='submit') - | I am 100% sure I want to delete my account and all of my progress \ No newline at end of file + | I am 100% sure I want to delete my account and all of my progress diff --git a/server/views/layout-wide.jade b/server/views/layout-wide.jade index 4ffc168436..cf548c3edc 100644 --- a/server/views/layout-wide.jade +++ b/server/views/layout-wide.jade @@ -8,9 +8,9 @@ html(lang='en') include partials/scripts block content else - body.no-top-and-bottom-margins - include partials/scripts - include partials/navbar - include partials/flash - block content - include partials/footer + body.no-top-and-bottom-margins(class=theme !== 'default' ? theme : '') + include partials/scripts + include partials/navbar + include partials/flash + block content + include partials/footer diff --git a/server/views/partials/scripts.jade b/server/views/partials/scripts.jade index a002f935eb..988b26bd9f 100644 --- a/server/views/partials/scripts.jade +++ b/server/views/partials/scripts.jade @@ -7,5 +7,11 @@ script. ga('require', 'displayfeatures'); ga('send', 'pageview'); // Leave the below lines alone! +script. + (function(global) { + global.main = global.main || {}; + global.main.isLoggedIn = !{JSON.stringify(!!user)}; + global.main.userId = !{JSON.stringify(user && user.id || false)}; + }(window)) script(src=rev('/js', 'vendor-main.js')) script(src=rev('/js', 'main.js'))