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($(`
+
+ `));
}
- 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 a9584b4447..4d2def2304 100644
--- a/common/models/user.js
+++ b/common/models/user.js
@@ -486,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/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/utils/middleware.js b/server/utils/middleware.js
index 1e5243f5da..458168bac3 100644
--- a/server/utils/middleware.js
+++ b/server/utils/middleware.js
@@ -6,7 +6,7 @@ export function ifNoUserRedirectTo(url, message, type = 'errors') {
}
req.flash(type, {
- msg: message || `You must be signed to go to ${path}`
+ msg: message || `You must be signed in to access ${path}`
});
return res.redirect(url);
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/map/show.jade b/server/views/map/show.jade
index 896032cbad..cf42fab570 100644
--- a/server/views/map/show.jade
+++ b/server/views/map/show.jade
@@ -1,6 +1,6 @@
extends ../layout-wide
block content
- .mapWrapper
+ .mapWrapper
.text-center.map-fixed-header
p Challenges required for certifications are marked with a *
.row.map-buttons
@@ -9,7 +9,7 @@ block content
.input-group
input#map-filter.form-control(type="text" placeholder="Type a challenge name" autocomplete="off" value="")
span.input-group-addon
- i.fa.fa-search
+ i.fa.fa-search
hr
include ../partials/flash
#accordion.map-accordion
@@ -122,3 +122,4 @@ block content
p.challenge-title.disabled.text-primary.ion-locked.padded-ionic-icon.negative-15(name="Mock Interview #2") Mock Interview #2
p.challenge-title.disabled.text-primary.ion-locked.padded-ionic-icon.negative-15(name="Mock Interview #3") Mock Interview #3
.spacer
+ include ../partials/flash
diff --git a/server/views/partials/flash.jade b/server/views/partials/flash.jade
index 8668a4174d..8b9fa3e431 100644
--- a/server/views/partials/flash.jade
+++ b/server/views/partials/flash.jade
@@ -1,6 +1,6 @@
.container
.row.flashMessage.negative-30
- .col-xs-12
+ .col-xs-12.col-sm-8.col-sm-offset-2.col-md-6.col-md-offset-3
if (messages.errors || messages.error)
.alert.alert-danger.fade.in
button.close(type='button', data-dismiss='alert')
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'))