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

25
public_html/css/core.css Normal file
View file

@ -0,0 +1,25 @@
body {
margin: 0;
padding: 0;
text-align: center;
background: #fff;
color: #000;
font-family: 'Droid Sans', sans-serif;
font-size: 17px;
}
#middle {
padding: 0 2em;
}
#content {
margin: 1.5em 0;
padding: 0;
display: inline-block;
text-align: left;
}
a {
color: #6a2;
text-decoration: none;
}

46
public_html/css/forms.css Normal file
View file

@ -0,0 +1,46 @@
.form-wrapper {
display: table;
}
.form-row {
display: table-row;
}
.form-label,
.form-input {
display: table-cell;
line-height: 37px;
vertical-align: text-bottom;
}
.form-input {
overflow: hidden;
text-overflow: ellipsis;
}
.form-row label {
padding-right: 1em;
text-align: right;
}
textarea,
input[type=text],
input[type=password] {
padding: 2px 4px;
border: 1px solid #eee;
background: #fafafa;
font-family: monospace;
font-size: 17px;
text-overflow: ellipsis;
width: 100%;
box-sizing: border-box;
}
button,
input[type=button] {
padding: 0.1em 0.5em;
border: 1px solid #eee;
background: #eee;
font-family: 'Droid Sans', sans-serif;
font-size: 17px;
}

View file

@ -0,0 +1,3 @@
#login-form p {
text-align: center;
}

View file

@ -0,0 +1,15 @@
.message {
margin-bottom: 0.2em;
padding: 0.4em 0.5em;
text-align: center;
}
.message.error {
background: #fdd;
box-shadow: 0 0 0 1px #fcc inset;
}
.message.info {
background: #def;
box-shadow: 0 0 0 1px #cdf inset;
}

View file

@ -0,0 +1,3 @@
#registration-form p {
text-align: center;
}

View file

@ -0,0 +1,38 @@
#top-navigation {
width: 100%;
}
#top-navigation ul {
list-style-type: none;
padding: 0 2em;
margin: 0;
}
#top-navigation li {
display: inline-block;
}
#top-navigation li a {
display: inline-block;
padding: 0.5em 1em;
color: #000;
}
#top-navigation li.active a {
background: #faffca;
}
#top-navigation li a:focus,
#top-navigation li a:hover {
outline: 0;
background: #efa;
color: #000;
}
#top-navigation li:first-child a {
margin-left: -1em;
}
#top-navigation li:last-child a {
margin-right: -1em;
}

166
public_html/index.html Normal file
View file

@ -0,0 +1,166 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>szurubooru</title>
<script src="//cdnjs.cloudflare.com/ajax/libs/path.js/0.8.4/path.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
<link rel="stylesheet" type="text/css" href="http://fonts.googleapis.com/css?family=Droid+Sans:400,700"/>
<link rel="stylesheet" type="text/css" href="/css/core.css"/>
<link rel="stylesheet" type="text/css" href="/css/forms.css"/>
<link rel="stylesheet" type="text/css" href="/css/messages.css"/>
<link rel="stylesheet" type="text/css" href="/css/top-navigation.css"/>
<link rel="stylesheet" type="text/css" href="/css/login-form.css"/>
<link rel="stylesheet" type="text/css" href="/css/registration-form.css"/>
</head>
<body>
<div id="main">
<div id="top-navigation"></div>
<div id="middle">
<div id="sidebar"></div>
<div id="content"></div>
</div>
</div>
<script type="text/template" id="top-navigation-template">
<ul>
<!-- todo: check privileges -->
<li class="users">
<a href="#/users">Users</a>
</li>
<% if (!loggedIn) { %>
<li class="login">
<a href="#/login">Login</a>
</li>
<li class="register">
<a href="#/register">Register</a>
</li>
<% } else { %>
<li class="logout">
<a href="#/logout">Logout</a>
</li>
<% } %>
</ul>
</script>
<script type="text/template" id="login-form-template">
<div id="login-form">
<p>
If you don't have an account yet,<br/>
<a href="#/register">click here</a> to create a new one.
</p>
<div class="messages"></div>
<form method="post" class="form-wrapper">
<div class="form-row">
<label for="login-user" class="form-label">User name:</label>
<div class="form-input">
<input autocomplete="off" type="text" name="user" id="login-user"/>
</div>
</div>
<div class="form-row">
<label for="login-password" class="form-label">Password:</label>
<div class="form-input">
<input autocomplete="off" type="password" name="password" id="login-password"/>
</div>
</div>
<div class="form-row">
<label class="form-label"></label>
<div class="form-input">
<button class="submit" type="submit">Log in</button>
&nbsp;
<input type="hidden" name="remember" value="0"/>
<label class="checkbox-wrapper">
<input type="checkbox" name="remember" value="1"/>
<span></span>
Remember me
</label>
</div>
</div>
</form>
<div class="help">
<p>Problems logging in?</p>
<ul>
<li><a href="#/password-reset">I don't remember my password</a></li>
<li><a href="#/activate-account">I haven't received activation e-mail</a></li>
<li><a href="#/register">I don't have an account</a></li>
</ul>
</div>
</div>
</script>
<script type="text/template" id="registration-form-template">
<div id="registration-form">
<p>
Registered users can view more content,<br/>
upload files and add posts to favorites.
</p>
<div class="messages"></div>
<form method="post" class="form-wrapper">
<div class="form-row">
<label for="registration-user" class="form-label">User name:</label>
<div class="form-input">
<input autocomplete="off" type="text" name="user" id="registration-user" placeholder="e.g. darth_vader" value=""/>
</div>
</div>
<div class="form-row">
<label for="registration-password" class="form-label">Password:</label>
<div class="form-input">
<input autocomplete="off" type="password" name="password1" id="registration-password" placeholder="e.g. &#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;" value=""/>
</div>
</div>
<div class="form-row">
<label for="registration-password-confirm" class="form-label">Password (repeat):</label>
<div class="form-input">
<input autocomplete="off" type="password" name="password2" id="registration-password-confirm" placeholder="e.g. &#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;&#x25cf;" value=""/>
</div>
</div>
<div class="form-row">
<label for="registration-email" class="form-label">E-mail address:</label>
<div class="form-input">
<input autocomplete="off" type="text" name="email" id="registration-email" placeholder="e.g. vader@empire.gov" value=""/>
</div>
</div>
<div class="form-row">
<label class="form-label"></label>
<div class="form-input">
<button class="submit" type="submit">Register</button>
</div>
</div>
</form>
<p id="email-info">
Your e-mail will be used to show your <a href="http://gravatar.com/">Gravatar</a>.<br/>
Leave blank for random Gravatar.
</p>
</div>
</script>
<script type="text/javascript" src="/js/DI.js"></script>
<script type="text/javascript" src="/js/State.js"></script>
<script type="text/javascript" src="/js/Api.js"></script>
<script type="text/javascript" src="/js/Auth.js"></script>
<script type="text/javascript" src="/js/Presenters/TopNavigationPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/LoginPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/LogoutPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/MessagePresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/RegistrationPresenter.js"></script>
<script type="text/javascript" src="/js/Presenters/UserListPresenter.js"></script>
<script type="text/javascript" src="/js/Router.js"></script>
<script type="text/javascript" src="/js/Bootstrap.js"></script>
</body>
</html>

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);