Added frontend outline

This commit is contained in:
Marcin Kurczewski
2014-08-31 23:22:56 +02:00
parent 03b65c196c
commit 16dec4894f
19 changed files with 905 additions and 0 deletions

57
public_html/js/Api.js Normal file
View File

@ -0,0 +1,57 @@
var App = App || {};
App.API = function() {
var baseUrl = '/api/';
function get(url, data) {
return request('GET', url, data);
};
function post(url, data) {
return request('POST', url, data);
};
function put(url, data) {
return request('PUT', url, data);
};
function _delete(url, data) {
return request('DELETE', url, data);
};
function request(method, url, data) {
var fullUrl = baseUrl + '/' + url;
fullUrl = fullUrl.replace(/\/{2,}/, '/');
return new Promise(function(resolve, reject) {
$.ajax({
success: function(data, textStatus, xhr) {
resolve({
status: xhr.status,
json: data});
},
error: function(xhr, textStatus, errorThrown) {
reject({
status: xhr.status,
json: xhr.responseJSON
? xhr.responseJSON
: {error: errorThrown}});
},
type: method,
url: fullUrl,
data: data,
});
});
};
return {
get: get,
post: post,
put: put,
delete: _delete
};
};
App.DI.registerSingleton('api', App.API);

92
public_html/js/Auth.js Normal file
View File

@ -0,0 +1,92 @@
var App = App || {};
App.Auth = function(jQuery, api, appState) {
function loginFromCredentials(userName, password, remember) {
return new Promise(function(resolve, reject) {
api.post('/login', {userName: userName, password: password})
.then(function(response) {
appState.set('loggedIn', true);
appState.set('loggedInUser', response.json.user);
appState.set('loginToken', response.json.token);
jQuery.cookie(
'auth',
response.json.token.name,
remember ? { expires: 365 } : {});
resolve(response);
}).catch(function(response) {
reject(response);
});
});
};
function loginFromToken(token) {
return new Promise(function(resolve, reject) {
api.post('/login', {token: token})
.then(function(response) {
appState.set('loggedIn', response.json.user && response.json.user.id);
appState.set('loggedInUser', response.json.user);
appState.set('loginToken', response.json.token.name);
resolve(response);
}).catch(function(response) {
reject(response);
});
});
};
function loginAnonymous() {
return new Promise(function(resolve, reject) {
api.post('/login')
.then(function(response) {
appState.set('loggedIn', false);
appState.set('loggedInUser', response.json.user);
appState.set('loginToken', null);
resolve(response);
}).catch(function(response) {
reject(response);
});
});
};
function logout() {
return new Promise(function(resolve, reject) {
appState.set('loggedIn', false);
appState.set('loginToken', null);
jQuery.removeCookie('auth');
resolve();
});
};
function tryLoginFromCookie() {
return new Promise(function(resolve, reject) {
if (appState.get('loggedIn')) {
resolve();
return;
}
var authCookie = jQuery.cookie('auth');
if (!authCookie) {
reject();
return;
}
loginFromToken(authCookie).then(function(response) {
resolve();
}).catch(function(response) {
jQuery.removeCookie('auth');
reject();
});
});
};
return {
loginFromCredentials: loginFromCredentials,
loginFromToken: loginFromToken,
loginAnonymous: loginAnonymous,
tryLoginFromCookie: tryLoginFromCookie,
logout: logout,
};
};
App.DI.registerSingleton('auth', App.Auth);

View File

@ -0,0 +1,33 @@
var App = App || {};
App.Bootstrap = function(auth, router) {
auth.tryLoginFromCookie()
.then(startRouting)
.catch(function(error) {
auth.loginAnonymous()
.then(startRouting)
.catch(function(response) {
console.log(response);
alert('Fatal authentication error: ' + response.json.error);
});
});
function startRouting() {
try {
router.start();
} catch (err) {
console.log(err);
}
}
return {
startRouting: startRouting,
};
};
App.DI.registerSingleton('bootstrap', App.Bootstrap);
App.DI.registerManual('jQuery', function() { return $; });
var bootstrap = App.DI.get('bootstrap');

65
public_html/js/DI.js Normal file
View File

@ -0,0 +1,65 @@
var App = App || {};
App.DI = (function() {
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var ARGUMENT_NAMES = /([^\s,]+)/g;
var factories = {};
var instances = {};
function get(key) {
var instance = instances[key];
if (!instance) {
var factory = factories[key];
if (!factory)
throw new Error('Unregistered key: ' + key);
var objectInitializer = factory.initializer;
var singleton = factory.singleton;
var deps = resolveDependencies(objectInitializer);
var instance = {};
instance = objectInitializer.apply(instance, deps);
if (singleton)
instances[key] = instance;
}
return instance;
}
function resolveDependencies(objectIntializer) {
var deps = [];
var depKeys = getFunctionParameterNames(objectIntializer);
for (var i = 0; i < depKeys.length; i ++) {
deps[i] = get(depKeys[i]);
}
return deps;
}
function register(key, objectInitializer) {
factories[key] = {initializer: objectInitializer, singleton: false};
};
function registerSingleton(key, objectInitializer) {
factories[key] = {initializer: objectInitializer, singleton: true};
};
function registerManual(key, objectInitializer) {
instances[key] = objectInitializer();
};
function getFunctionParameterNames(func) {
var fnStr = func.toString().replace(STRIP_COMMENTS, '');
var result = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')')).match(ARGUMENT_NAMES);
if (result === null) {
result = [];
}
return result;
}
return {
get: get,
register: register,
registerManual: registerManual,
registerSingleton: registerSingleton,
};
})();

View File

@ -0,0 +1,59 @@
var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.LoginPresenter = function(
jQuery,
topNavigationPresenter,
messagePresenter,
auth,
router,
appState) {
topNavigationPresenter.select('login');
var $el = jQuery('#content');
var $messages;
var template = _.template(jQuery('#login-form-template').html());
var eventHandlers = {
loginFormSubmit: function(e) {
e.preventDefault();
messagePresenter.hideMessages($messages);
var userName = $el.find('[name=user]').val();
var password = $el.find('[name=password]').val();
var remember = $el.find('[name=remember]').val();
//todo: client side error reporting
auth.loginFromCredentials(userName, password, remember)
.then(function(response) {
router.navigateToMainPage();
//todo: "redirect" to main page
}).catch(function(response) {
messagePresenter.showError($messages, response.json && response.json.error || response);
});
},
};
if (appState.get('loggedIn'))
router.navigateToMainPage();
render();
function render() {
$el.html(template());
$el.find('form').submit(eventHandlers.loginFormSubmit);
$messages = $el.find('.messages');
$messages.width($el.find('form').width());
};
return {
render: render,
};
};
App.DI.register('loginPresenter', App.Presenters.LoginPresenter);

View File

@ -0,0 +1,34 @@
var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.LogoutPresenter = function(
jQuery,
topNavigationPresenter,
messagePresenter,
auth,
router) {
topNavigationPresenter.select('logout');
var $messages = jQuery('#content');
var eventHandlers = {
mainPageLinkClick: function(e) {
e.preventDefault();
router.navigateToMainPage();
},
};
auth.logout().then(function() {
var $messageDiv = messagePresenter.showInfo($messages, 'Logged out. <a href="">Back to main page</a>');
$messageDiv.find('a').click(eventHandlers.mainPageLinkClick);
}).catch(function(response) {
messagePresenter.showError($messages, response.json && response.json.error || response);
});
return {
};
};
App.DI.register('logoutPresenter', App.Presenters.LogoutPresenter);

View File

@ -0,0 +1,41 @@
var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.MessagePresenter = function(jQuery) {
function showInfo($el, message) {
return showMessage($el, 'info', message);
};
function showError($el, message) {
return showMessage($el, 'error', message);
};
function hideMessages($el) {
$el.children('.message').each(function() {
$(this).slideUp('fast', function() {
$(this).remove();
});
});
};
function showMessage($el, className, message) {
var $messageDiv = $('<div>');
$messageDiv.addClass('message');
$messageDiv.addClass(className);
$messageDiv.html(message);
$messageDiv.hide();
$el.append($messageDiv);
$messageDiv.slideDown('fast');
return $messageDiv;
};
return {
showInfo: showInfo,
showError: showError,
hideMessages: hideMessages,
};
};
App.DI.register('messagePresenter', App.Presenters.MessagePresenter);

View File

@ -0,0 +1,68 @@
var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.RegistrationPresenter = function(
jQuery,
topNavigationPresenter,
messagePresenter,
api) {
topNavigationPresenter.select('register');
var $el = jQuery('#content');
var template = _.template(jQuery('#registration-form-template').html());
var eventHandlers = {
registrationFormSubmit: function(e) {
e.preventDefault();
messagePresenter.hideMessages($messages);
var userName = $el.find('[name=user]').val();
var password = $el.find('[name=password1]').val();
var passwordConfirmation = $el.find('[name=password2]').val();
var email = $el.find('[name=email]').val();
if (userName.length == 0) {
messagePresenter.showError($messages, 'User name cannot be empty.');
return;
}
if (password.length == 0) {
messagePresenter.showError($messages, 'Password cannot be empty.');
return;
}
if (password != passwordConfirmation) {
messagePresenter.showError($messages, 'Passwords must be the same.');
return;
}
api.post('/users', {userName: userName, password: password, email: email})
.then(function(response) {
//todo: show message about registration success
//if it turned out that user needs to confirm his e-mail, notify about it
//also, show link to login
}).catch(function(response) {
messagePresenter.showError($messages, response.json && response.json.error || response);
});
}
};
render();
function render() {
$el.html(template());
$el.find('form').submit(eventHandlers.registrationFormSubmit);
$messages = $el.find('.messages');
$messages.width($el.find('form').width());
};
return {
render: render,
};
};
App.DI.register('registrationPresenter', App.Presenters.RegistrationPresenter);

View File

@ -0,0 +1,37 @@
var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.TopNavigationPresenter = function(jQuery, appState) {
var selectedElement = null;
var template = _.template(jQuery('#top-navigation-template').html());
var $el = jQuery('#top-navigation');
var eventHandlers = {
loginStateChanged: function() {
render();
},
};
appState.startObserving('loggedIn', 'top-navigation', eventHandlers.loginStateChanged);
render();
function select(newSelectedElement) {
selectedElement = newSelectedElement;
$el.find('li').removeClass('active');
$el.find('li.' + selectedElement).addClass('active');
};
function render() {
$el.html(template({loggedIn: appState.get('loggedIn')}));
$el.find('li.' + selectedElement).addClass('active');
};
return {
render: render,
select: select,
};
};
App.DI.register('topNavigationPresenter', App.Presenters.TopNavigationPresenter);

View File

@ -0,0 +1,22 @@
var App = App || {};
App.Presenters = App.Presenters || {};
App.Presenters.UserListPresenter = function(jQuery, topNavigationPresenter, appState) {
topNavigationPresenter.select('users');
var $el = jQuery('#content');
render();
function render() {
$el.html('Logged in: ' + appState.get('loggedIn'));
};
return {
render: render
};
};
App.DI.register('userListPresenter', App.Presenters.UserListPresenter);

53
public_html/js/Router.js Normal file
View File

@ -0,0 +1,53 @@
var App = App || {};
App.Router = function(jQuery) {
var root = '#/';
injectRoutes();
function navigateToMainPage() {
window.location.href = root;
};
function navigate(url) {
window.location.href = url;
};
function start() {
Path.listen();
};
function changePresenter(presenterGetter) {
jQuery('#content').empty();
var presenter = presenterGetter();
};
function injectRoutes() {
inject('#/login', function() { return new App.DI.get('loginPresenter'); });
inject('#/logout', function() { return new App.DI.get('logoutPresenter'); });
inject('#/register', function() { return new App.DI.get('registrationPresenter'); });
inject('#/users', function() { return App.DI.get('userListPresenter'); });
setRoot('#/users');
};
function setRoot(newRoot) {
root = newRoot;
Path.root(newRoot);
};
function inject(path, presenterGetter) {
Path.map(path).to(function() {
changePresenter(presenterGetter);
});
};
return {
start: start,
navigate: navigate,
navigateToMainPage: navigateToMainPage,
};
};
App.DI.registerSingleton('router', App.Router);

48
public_html/js/State.js Normal file
View File

@ -0,0 +1,48 @@
var App = App || {};
App.State = function() {
var properties = {};
var observers = {};
function get(key) {
return properties[key];
};
function set(key, value) {
properties[key] = value;
if (key in observers) {
for (observerName in observers[key]) {
if (observers[key].hasOwnProperty(observerName)) {
observers[key][observerName](key, value);
}
}
}
};
function startObserving(key, observerName, callback) {
if (!(key in observers))
observers[key] = {};
if (!(observerName in observers[key]))
observers[key][observerName] = {};
observers[key][observerName] = callback;
};
function stopObserving(key, observerName) {
if (!(key in observers))
return;
if (!(observerName in observers[key]))
return;
delete observers[key][observerName];
};
return {
get: get,
set: set,
startObserving: startObserving,
stopObserving: stopObserving,
};
};
App.DI.registerSingleton('appState', App.State);