diff --git a/public_html/css/core.css b/public_html/css/core.css new file mode 100644 index 00000000..21c92654 --- /dev/null +++ b/public_html/css/core.css @@ -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; +} diff --git a/public_html/css/forms.css b/public_html/css/forms.css new file mode 100644 index 00000000..e91e2aca --- /dev/null +++ b/public_html/css/forms.css @@ -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; +} diff --git a/public_html/css/login-form.css b/public_html/css/login-form.css new file mode 100644 index 00000000..5122ce5d --- /dev/null +++ b/public_html/css/login-form.css @@ -0,0 +1,3 @@ +#login-form p { + text-align: center; +} diff --git a/public_html/css/messages.css b/public_html/css/messages.css new file mode 100644 index 00000000..5e2628c0 --- /dev/null +++ b/public_html/css/messages.css @@ -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; +} diff --git a/public_html/css/registration-form.css b/public_html/css/registration-form.css new file mode 100644 index 00000000..c3feef76 --- /dev/null +++ b/public_html/css/registration-form.css @@ -0,0 +1,3 @@ +#registration-form p { + text-align: center; +} diff --git a/public_html/css/top-navigation.css b/public_html/css/top-navigation.css new file mode 100644 index 00000000..3a25b2a7 --- /dev/null +++ b/public_html/css/top-navigation.css @@ -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; +} diff --git a/public_html/index.html b/public_html/index.html new file mode 100644 index 00000000..2bf34047 --- /dev/null +++ b/public_html/index.html @@ -0,0 +1,166 @@ + + + + + szurubooru + + + + + + + + + + + + + + +
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + diff --git a/public_html/js/Api.js b/public_html/js/Api.js new file mode 100644 index 00000000..ebad8e44 --- /dev/null +++ b/public_html/js/Api.js @@ -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); diff --git a/public_html/js/Auth.js b/public_html/js/Auth.js new file mode 100644 index 00000000..2a11cc32 --- /dev/null +++ b/public_html/js/Auth.js @@ -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); diff --git a/public_html/js/Bootstrap.js b/public_html/js/Bootstrap.js new file mode 100644 index 00000000..635e40a3 --- /dev/null +++ b/public_html/js/Bootstrap.js @@ -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'); diff --git a/public_html/js/DI.js b/public_html/js/DI.js new file mode 100644 index 00000000..b2b87f91 --- /dev/null +++ b/public_html/js/DI.js @@ -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, + }; + +})(); diff --git a/public_html/js/Presenters/LoginPresenter.js b/public_html/js/Presenters/LoginPresenter.js new file mode 100644 index 00000000..386f43d0 --- /dev/null +++ b/public_html/js/Presenters/LoginPresenter.js @@ -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); diff --git a/public_html/js/Presenters/LogoutPresenter.js b/public_html/js/Presenters/LogoutPresenter.js new file mode 100644 index 00000000..ac0b6e35 --- /dev/null +++ b/public_html/js/Presenters/LogoutPresenter.js @@ -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. Back to main page'); + $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); diff --git a/public_html/js/Presenters/MessagePresenter.js b/public_html/js/Presenters/MessagePresenter.js new file mode 100644 index 00000000..0e5b5802 --- /dev/null +++ b/public_html/js/Presenters/MessagePresenter.js @@ -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 = $('
'); + $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); diff --git a/public_html/js/Presenters/RegistrationPresenter.js b/public_html/js/Presenters/RegistrationPresenter.js new file mode 100644 index 00000000..2a713b2f --- /dev/null +++ b/public_html/js/Presenters/RegistrationPresenter.js @@ -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); diff --git a/public_html/js/Presenters/TopNavigationPresenter.js b/public_html/js/Presenters/TopNavigationPresenter.js new file mode 100644 index 00000000..340010ed --- /dev/null +++ b/public_html/js/Presenters/TopNavigationPresenter.js @@ -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); diff --git a/public_html/js/Presenters/UserListPresenter.js b/public_html/js/Presenters/UserListPresenter.js new file mode 100644 index 00000000..c8794742 --- /dev/null +++ b/public_html/js/Presenters/UserListPresenter.js @@ -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); diff --git a/public_html/js/Router.js b/public_html/js/Router.js new file mode 100644 index 00000000..3c68755f --- /dev/null +++ b/public_html/js/Router.js @@ -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); diff --git a/public_html/js/State.js b/public_html/js/State.js new file mode 100644 index 00000000..22d4a2a2 --- /dev/null +++ b/public_html/js/State.js @@ -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);