diff --git a/package.json b/package.json index 050a0a18..6ed32b0f 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "ini": "^1.3.4", "merge": "^1.2.0", "page": "^1.7.1", + "superagent": "^1.8.3", "uglify-js": "git://github.com/mishoo/UglifyJS2.git#harmony" }, "devDependencies": { diff --git a/static/css/main.css b/static/css/main.css index f176df1d..611405ec 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -4,6 +4,8 @@ --text-color: #111; --inactive-link-color: #888; --line-color: #DDD; + --message-error-border-color: #FCC; + --message-error-background-color: #FFF5F5; --input-bad-border-color: #FCC; --input-bad-background-color: #FFF5F5; --input-good-border-color: #D3E3D3; @@ -91,3 +93,11 @@ nav.text-nav ul li.active a { #top-nav ul li[data-name=help] { float: none; } + +.messages .message { + padding: 0.5em; +} +.message.error { + border: 1px solid var(--message-error-border-color); + background: var(--message-error-background-color); +} diff --git a/static/html/login.tpl b/static/html/login.tpl index 98a5980a..2fa511e4 100644 --- a/static/html/login.tpl +++ b/static/html/login.tpl @@ -5,11 +5,11 @@ +
Forgot the password? diff --git a/static/html/user_registration.tpl b/static/html/user_registration.tpl index 2b005021..d88f630a 100644 --- a/static/html/user_registration.tpl +++ b/static/html/user_registration.tpl @@ -18,6 +18,7 @@
+
diff --git a/static/js/api.js b/static/js/api.js new file mode 100644 index 00000000..4c3e3bdd --- /dev/null +++ b/static/js/api.js @@ -0,0 +1,39 @@ +'use strict'; + +const request = require('superagent'); +const config = require('./config.js'); + +class Api { + get(url) { + const fullUrl = this.getFullUrl(url); + return this.process(fullUrl, () => request.get(fullUrl)); + } + + post(url, data) { + const fullUrl = this.getFullUrl(url); + return this.process(fullUrl, () => request.post(fullUrl).send(data)); + } + + process(url, requestFactory) { + return new Promise((resolve, reject) => { + let req = requestFactory(); + if (this.userName && this.userPassword) { + req.auth(this.userName, this.userPassword); + } + req.set('Accept', 'application/json') + .end((error, response) => { + if (error) { + reject(response.body); + } else { + resolve(response.body); + } + }); + }); + } + + getFullUrl(url) { + return (config.basic.apiUrl + '/' + url).replace(/([^:])\/+/g, '$1/'); + } +} + +module.exports = Api; diff --git a/static/js/controllers/auth_controller.js b/static/js/controllers/auth_controller.js index 9372af0e..5490f456 100644 --- a/static/js/controllers/auth_controller.js +++ b/static/js/controllers/auth_controller.js @@ -1,10 +1,15 @@ 'use strict'; +const page = require('page'); +const config = require('../config.js'); + class AuthController { - constructor(topNavigationController, loginView) { + constructor(api, topNavigationController, loginView) { + this.api = api; this.topNavigationController = topNavigationController; this.loginView = loginView; this.currentUser = null; + /* TODO: load from cookies */ } isLoggedIn() { @@ -15,20 +20,40 @@ class AuthController { return true; } - login(user) { - this.currentUser = user; + login(userName, userPassword) { + return new Promise((resolve, reject) => { + this.api.userName = userName; + this.api.userPassword = userPassword; + this.api.get('/user/' + userName) + .then(resolve) + .catch(reject); + }); } logout(user) { this.currentUser = null; + this.api.userName = null; + this.api.userPassword = null; + /* TODO: clear cookie */ } loginRoute() { this.topNavigationController.activate('login'); this.loginView.render({ - login: (user, password) => { - alert(user, password); - //self.authController.login(user); + login: (userName, userPassword, doRemember) => { + return new Promise((resolve, reject) => { + this + .login(userName, userPassword) + .then(response => { + if (doRemember) { + /* TODO: set cookie */ + } + resolve(); + page('/'); + /* TODO: update top navigation */ + }) + .catch(response => { reject(response.description); }); + }); }}); } diff --git a/static/js/controllers/users_controller.js b/static/js/controllers/users_controller.js index 24494928..b0f81238 100644 --- a/static/js/controllers/users_controller.js +++ b/static/js/controllers/users_controller.js @@ -1,7 +1,11 @@ 'use strict'; +const page = require('page'); + class UsersController { - constructor(topNavigationController, authController, registrationView) { + constructor( + api, topNavigationController, authController, registrationView) { + this.api = api; this.topNavigationController = topNavigationController; this.authController = authController; this.registrationView = registrationView; @@ -12,12 +16,31 @@ class UsersController { } createUserRoute() { - const self = this; this.topNavigationController.activate('register'); this.registrationView.render({ - register: (user) => { - alert(user); - self.authController.login(user); + register: (userName, userPassword, userEmail) => { + const data = { + 'name': userName, + 'password': userPassword, + 'email': userEmail + }; + // TODO: reduce callback hell + return new Promise((resolve, reject) => { + this.api.post('/users/', data) + .then(() => { + this.authController.login(userName, userPassword) + .then(() => { + resolve(); + page('/'); + }) + .catch(response => { + reject(response.description); + }); + }) + .catch(response => { + reject(response.description); + }); + }); }}); } diff --git a/static/js/main.js b/static/js/main.js index d1d16bf6..ce1e73c2 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -6,6 +6,7 @@ const page = require('page'); const handlebars = require('handlebars'); +const Api = require('./api.js'); const LoginView = require('./views/login_view.js'); const RegistrationView = require('./views/registration_view.js'); const TopNavigationView = require('./views/top_navigation_view.js'); @@ -24,11 +25,13 @@ const TagsController = require('./controllers/tags_controller.js'); // ------------------- // - resolve objects - // ------------------- +const api = new Api(); + const topNavigationView = new TopNavigationView(handlebars); const loginView = new LoginView(handlebars); const registrationView = new RegistrationView(handlebars); -const authController = new AuthController(null, loginView); +const authController = new AuthController(api, null, loginView); const topNavigationController = new TopNavigationController(topNavigationView, authController); // break cyclic dependency topNavigationView<->authController @@ -37,6 +40,7 @@ authController.topNavigationController = topNavigationController; const homeController = new HomeController(topNavigationController); const postsController = new PostsController(topNavigationController); const usersController = new UsersController( + api, topNavigationController, authController, registrationView); @@ -52,13 +56,13 @@ page('/', () => { homeController.indexRoute(); }); page('/upload', () => { postsController.uploadPostsRoute(); }); page('/posts', () => { postsController.listPostsRoute(); }); -page('/post/:id', (id) => { postsController.showPostRoute(id); }); -page('/post/:id/edit', (id) => { postsController.editPostRoute(id); }); +page('/post/:id', id => { postsController.showPostRoute(id); }); +page('/post/:id/edit', id => { postsController.editPostRoute(id); }); page('/register', () => { usersController.createUserRoute(); }); page('/users', () => { usersController.listUsersRoute(); }); -page('/user/:user', (user) => { usersController.showUserRoute(user); }); -page('/user/:user/edit', (user) => { usersController.editUserRoute(user); }); +page('/user/:user', user => { usersController.showUserRoute(user); }); +page('/user/:user/edit', user => { usersController.editUserRoute(user); }); page('/history', () => { historyController.showHistoryRoute(); }); page('/tags', () => { tagsController.listTagsRoute(); }); diff --git a/static/js/views/base_view.js b/static/js/views/base_view.js index 06baabd7..694e44f2 100644 --- a/static/js/views/base_view.js +++ b/static/js/views/base_view.js @@ -19,14 +19,32 @@ class BaseView { return this.handlebars.compile(templateText); } + showError(messagesHolder, errorMessage) { + /* TODO: animate this */ + const node = document.createElement('div'); + node.innerHTML = errorMessage; + node.classList.add('message'); + node.classList.add('error'); + messagesHolder.appendChild(node); + } + + clearMessages(messagesHolder) { + /* TODO: animate that */ + while (messagesHolder.lastChild) { + messagesHolder.removeChild(messagesHolder.lastChild); + } + } + decorateValidator(form) { // postpone showing form fields validity until user actually tries // to submit it (seeing red/green form w/o doing anything breaks POLA) - const submitButton - = document.querySelector('#content-holder .buttons input'); - submitButton.addEventListener('click', (e) => { + const submitButton = form.querySelector('.buttons input'); + submitButton.addEventListener('click', e => { form.classList.add('show-validation'); }); + form.addEventListener('submit', e => { + form.classList.remove('show-validation'); + }); } showView(html) { diff --git a/static/js/views/login_view.js b/static/js/views/login_view.js index 5d13b3b9..e2b08c0d 100644 --- a/static/js/views/login_view.js +++ b/static/js/views/login_view.js @@ -11,17 +11,32 @@ class LoginView extends BaseView { render(options) { this.showView(this.template()); - const form = document.querySelector('#content-holder form'); + const messagesHolder = this.contentHolder.querySelector('.messages'); + const form = this.contentHolder.querySelector('form'); this.decorateValidator(form); const userNameField = document.getElementById('user-name'); const passwordField = document.getElementById('user-password'); + const rememberUserField = document.getElementById('remember-user'); userNameField.setAttribute('pattern', config.service.userNameRegex); passwordField.setAttribute('pattern', config.service.passwordRegex); - form.addEventListener('submit', (e) => { + form.addEventListener('submit', e => { e.preventDefault(); - options.login(userNameField.value, passwordField.value); + this.clearMessages(messagesHolder); + form.setAttribute('disabled', true); + options + .login( + userNameField.value, + passwordField.value, + rememberUserField.checked) + .then(() => { + form.setAttribute('disabled', false); + }) + .catch(errorMessage => { + form.setAttribute('disabled', false); + this.showError(messagesHolder, errorMessage); + }); }); } } diff --git a/static/js/views/registration_view.js b/static/js/views/registration_view.js index a414be29..fb5841c3 100644 --- a/static/js/views/registration_view.js +++ b/static/js/views/registration_view.js @@ -11,6 +11,7 @@ class RegistrationView extends BaseView { render(options) { this.showView(this.template()); + const messagesHolder = this.contentHolder.querySelector('.messages'); const form = document.querySelector('#content-holder form'); this.decorateValidator(form); @@ -20,14 +21,22 @@ class RegistrationView extends BaseView { userNameField.setAttribute('pattern', config.service.userNameRegex); passwordField.setAttribute('pattern', config.service.passwordRegex); - form.addEventListener('submit', (e) => { + form.addEventListener('submit', e => { e.preventDefault(); - const user = { - name: userNameField.value, - password: passwordField.value, - email: emailField.value, - }; - options.register(user); + this.clearMessages(messagesHolder); + form.setAttribute('disabled', true); + options + .register( + userNameField.value, + passwordField.value, + emailField.value) + .then(() => { + form.setAttribute('disabled', false); + }) + .catch(errorMessage => { + form.setAttribute('disabled', false); + this.showError(messagesHolder, errorMessage); + }); }); } }