diff options
Diffstat (limited to 'static')
| -rw-r--r-- | static/css/forms.css | 79 | ||||
| -rw-r--r-- | static/css/main.css | 74 | ||||
| -rw-r--r-- | static/css/users.css | 16 | ||||
| -rw-r--r-- | static/html/index.htm | 71 | ||||
| -rw-r--r-- | static/js/.gitignore | 1 | ||||
| -rw-r--r-- | static/js/config.js | 4 | ||||
| -rw-r--r-- | static/js/controllers/auth_controller.js | 34 | ||||
| -rw-r--r-- | static/js/controllers/comments_controller.js | 13 | ||||
| -rw-r--r-- | static/js/controllers/help_controller.js | 13 | ||||
| -rw-r--r-- | static/js/controllers/history_controller.js | 13 | ||||
| -rw-r--r-- | static/js/controllers/home_controller.js | 17 | ||||
| -rw-r--r-- | static/js/controllers/posts_controller.js | 25 | ||||
| -rw-r--r-- | static/js/controllers/tags_controller.js | 13 | ||||
| -rw-r--r-- | static/js/controllers/top_navigation_controller.js | 69 | ||||
| -rw-r--r-- | static/js/controllers/users_controller.js | 38 | ||||
| -rw-r--r-- | static/js/main.js | 70 | ||||
| -rw-r--r-- | static/js/views/base_view.js | 37 | ||||
| -rw-r--r-- | static/js/views/registration_view.js | 35 | ||||
| -rw-r--r-- | static/js/views/top_navigation_view.js | 30 | ||||
| -rw-r--r-- | static/prepare_config.js | 33 |
20 files changed, 685 insertions, 0 deletions
diff --git a/static/css/forms.css b/static/css/forms.css new file mode 100644 index 0000000..f486549 --- /dev/null +++ b/static/css/forms.css @@ -0,0 +1,79 @@ +form fieldset { + margin: 0; + padding: 0; + border: 0; +} + +form fieldset legend { + display: block; + text-align: center; + width: 100%; + font-size: 17pt; +} + +form ul { + list-style-type: none; + margin: 0; + padding: 0; +} + +form.tabular ul { + display: table; + border-spacing: 0.5em; + margin: 0.5em -0.5em; + width: 100%; +} +form.tabular ul li { + display: table-row; +} +form.tabular ul li label { + display: table-cell; + width: 33%; + padding: 0; +} +form.tabular .buttons { + margin-left: 33%; +} + +form:not(.tabular) ul li label { + display: block; + padding: 0.5em 0; +} + +textarea, +input[type=text], +input[type=email], +input[type=password] { + font-size: 100%; + font-family: 'Inconsolata', monospace; + padding: 0.3em; + border: 1px solid #EEE; + background: #FAFAFA; + text-overflow: ellipsis; + width: 100%; + box-sizing: border-box; + box-shadow: none; /* :-moz-submit-invalid on FF */ +} + +form.show-validation fieldset.input input:invalid { + outline: none; + border: 1px solid #FCC; + background: #FFF5F5; +} +form.show-validation fieldset.input input:valid { + outline: none; + border: 1px solid #D3E3D3; + background: #F5FFF5; +} + +button, +input[type=button], +input[type=submit] { + cursor: pointer; + font-size: 100%; + line-height: 100%; + padding: 0.3em 0.7em; + border: 1px solid #24AADD; + background: #24AADD; + color: white; +} diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..1a338e0 --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,74 @@ +body { + margin: 0; + color: #111; + font-family: 'Droid Sans' !important; + font-size: 12pt; +} + +#content-holder { + margin-top: 1em; + text-align: center; +} +#content-holder>.center { + text-align: left; + display: inline-block; + margin: 0 auto; +} +hr { + border: 0; + border-top: 1px solid #ddd; + margin: 1em 0; + padding: 0; +} + +nav ul { + list-style-type: none; + padding: 0; + margin: 0; + display: inline-block; +} +nav ul li { + display: inline-block; + padding: 0; + margin: 0; +} +nav ul li a { + display: inline-block; +} +nav ul li img { + margin: 0; + vertical-align: top; /* fix ghost margin under the image */ +} + +nav.text-nav { + margin: 1em 0; +} +nav.text-nav ul li a { + padding: 0.3em 1.2em; + text-decoration: none; +} +nav.text-nav ul li:not(.active) a { + color: #888; +} +nav.text-nav ul li.active a { + background: #24AADD; + color: white; +} + +#top-nav { + background: #F5F5F5; + margin: 0; +} +#top-nav ul { + display: block; + text-align: right; +} +#top-nav ul li { + float: left; +} +#top-nav ul li[data-name=register], +#top-nav ul li[data-name=login], +#top-nav ul li[data-name=logout], +#top-nav ul li[data-name=help] { + float: none; +} diff --git a/static/css/users.css b/static/css/users.css new file mode 100644 index 0000000..cf2e10d --- /dev/null +++ b/static/css/users.css @@ -0,0 +1,16 @@ +#user-registration form { + display: block; + width: 20em; + float: left; +} +#user-registration div.info { + float: left; + margin-left: 3em; + border-radius: 0.2em; + width: 20em; +} +#user-registration .info p:first-child, +#user-registration form li:first-child label { + padding-top: 0; + margin-top: 0; +} diff --git a/static/html/index.htm b/static/html/index.htm new file mode 100644 index 0000000..543d50c --- /dev/null +++ b/static/html/index.htm @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<html> +<head> + <title>szurubooru</title> + <meta charset='utf-8'/> + <link href='/bundle.min.css' rel='stylesheet' type='text/css'> + <link href='//fonts.googleapis.com/css?family=Droid+Sans' rel='stylesheet' type='text/css'> + <link href='//fonts.googleapis.com/css?family=Inconsolata' rel='stylesheet' type='text/css'> + <!-- TODO: configurable favicon --> + <template id='top-nav-template'> + <nav id='top-nav' class='text-nav'> + <ul><!-- + -->{{#each items}}<!-- + -->{{#if this.available}}<!-- + --><li data-name='{{@key}}'><!-- + --><a href='{{this.url}}'>{{this.name}}</a><!-- + --></li><!-- + -->{{/if}}<!-- + -->{{/each}}<!-- + --></ul> + </nav> + </template> + + <template id='user-registration-template'> + <div class='center' id='user-registration'> + <h1>Registration</h1> + <br/> + <form autocomplete='off'> + <fieldset class='input'> + <ul> + <li> + <label for='user-name'>User name:</label> + <input id='user-name' name='user-name' type='text' autocomplete='off' placeholder='e.g. darth_vader' required/> + </li> + <li> + <label for='user-password'>Password:</label> + <input id='user-password' name='user-password' type='password' autocomplete='off' placeholder='e.g. cupcake' required/> + </li> + <li> + <label for='user-email'>Email (optional):</label> + <input id='user-email' name='user-email' type='email' autocomplete='off' placeholder='e.g. vader@empire.gov'/> + </li> + </ul> + </fieldset> + <hr/> + <p>By clicking "Create an account" button below, you are agreeing to the <a href='/help/tos'>Terms of Service</a>.</p> + <hr/> + <fieldset class='buttons'> + <input type='submit' value='Create an account'/> + </fieldset> + </form> + <div class='info'> + <p>Registered users can:</p> + <ul> + <li>upload new posts</li> + <li>mark them as favorite</li> + <li>add comments</li> + <li>vote up/down on posts and comments</li> + </ul> + <p>Your e-mail will be used to show your <a href='http://gravatar.com/'>Gravatar</a> and for password reminders only. Leave blank for random Gravatar.</p> + </div> + </div> + </template> + +</head> +<body> + <div id='top-nav-holder'></div> + <div id='content-holder'></div> +<script type='text/javascript' src='/bundle.min.js'></script> +</body> +</html> diff --git a/static/js/.gitignore b/static/js/.gitignore new file mode 100644 index 0000000..16e3bbe --- /dev/null +++ b/static/js/.gitignore @@ -0,0 +1 @@ +.config.autogen.json diff --git a/static/js/config.js b/static/js/config.js new file mode 100644 index 0000000..4524be6 --- /dev/null +++ b/static/js/config.js @@ -0,0 +1,4 @@ +'use strict'; + +const config = require('./.config.autogen.json'); +module.exports = config; diff --git a/static/js/controllers/auth_controller.js b/static/js/controllers/auth_controller.js new file mode 100644 index 0000000..e23d753 --- /dev/null +++ b/static/js/controllers/auth_controller.js @@ -0,0 +1,34 @@ +'use strict'; + +class AuthController { + constructor(topNavigationController) { + this.topNavigationController = topNavigationController; + this.currentUser = null; + } + + isLoggedIn() { + return this.currentUser !== null; + } + + hasPrivilege() { + return true; + } + + login(user) { + this.currentUser = user; + } + + logout(user) { + this.currentUser = null; + } + + loginRoute() { + this.topNavigationController.activate('login'); + } + + logoutRoute() { + this.topNavigationController.activate('logout'); + } +} + +module.exports = AuthController; diff --git a/static/js/controllers/comments_controller.js b/static/js/controllers/comments_controller.js new file mode 100644 index 0000000..b6ff02c --- /dev/null +++ b/static/js/controllers/comments_controller.js @@ -0,0 +1,13 @@ +'use strict'; + +class CommentsController { + constructor(topNavigationController) { + this.topNavigationController = topNavigationController; + } + + listCommentsRoute() { + this.topNavigationController.activate('comments'); + } +} + +module.exports = CommentsController; diff --git a/static/js/controllers/help_controller.js b/static/js/controllers/help_controller.js new file mode 100644 index 0000000..78eeab3 --- /dev/null +++ b/static/js/controllers/help_controller.js @@ -0,0 +1,13 @@ +'use strict'; + +class HelpController { + constructor(topNavigationController) { + this.topNavigationController = topNavigationController; + } + + showHelpRoute() { + this.topNavigationController.activate('help'); + } +} + +module.exports = HelpController; diff --git a/static/js/controllers/history_controller.js b/static/js/controllers/history_controller.js new file mode 100644 index 0000000..ff0b264 --- /dev/null +++ b/static/js/controllers/history_controller.js @@ -0,0 +1,13 @@ +'use strict'; + +class HistoryController { + constructor(topNavigationController) { + this.topNavigationController = topNavigationController; + } + + listHistoryRoute() { + this.topNavigationController.activate(''); + } +} + +module.exports = HistoryController; diff --git a/static/js/controllers/home_controller.js b/static/js/controllers/home_controller.js new file mode 100644 index 0000000..1b913fa --- /dev/null +++ b/static/js/controllers/home_controller.js @@ -0,0 +1,17 @@ +'use strict'; + +class HomeController { + constructor(topNavigationController) { + this.topNavigationController = topNavigationController; + } + + indexRoute() { + this.topNavigationController.activate('home'); + } + + notFoundRoute() { + this.topNavigationController.activate(''); + } +} + +module.exports = HomeController; diff --git a/static/js/controllers/posts_controller.js b/static/js/controllers/posts_controller.js new file mode 100644 index 0000000..bbd540c --- /dev/null +++ b/static/js/controllers/posts_controller.js @@ -0,0 +1,25 @@ +'use strict'; + +class PostsController { + constructor(topNavigationController) { + this.topNavigationController = topNavigationController; + } + + uploadPostsRoute() { + this.topNavigationController.activate('upload'); + } + + listPostsRoute() { + this.topNavigationController.activate('posts'); + } + + showPostRoute(id) { + this.topNavigationController.activate('posts'); + } + + editPostRoute(id) { + this.topNavigationController.activate('posts'); + } +} + +module.exports = PostsController; diff --git a/static/js/controllers/tags_controller.js b/static/js/controllers/tags_controller.js new file mode 100644 index 0000000..3124d40 --- /dev/null +++ b/static/js/controllers/tags_controller.js @@ -0,0 +1,13 @@ +'use strict'; + +class TagsController { + constructor(topNavigationController) { + this.topNavigationController = topNavigationController; + } + + listTagsRoute() { + this.topNavigationController.activate('tags'); + } +} + +module.exports = TagsController; diff --git a/static/js/controllers/top_navigation_controller.js b/static/js/controllers/top_navigation_controller.js new file mode 100644 index 0000000..2bd098d --- /dev/null +++ b/static/js/controllers/top_navigation_controller.js @@ -0,0 +1,69 @@ +'use strict'; + +class NavigationItem { + constructor(name, url) { + this.name = name; + this.url = url; + this.available = true; + } +} + +class TopNavigationController { + constructor(topNavigationView, authController) { + this.authController = authController; + this.topNavigationView = topNavigationView; + + this.items = { + 'home': new NavigationItem('Home', '/'), + 'posts': new NavigationItem('Posts', '/posts'), + 'upload': new NavigationItem('Upload', '/upload'), + 'comments': new NavigationItem('Comments', '/comments'), + 'tags': new NavigationItem('Tags', '/tags'), + 'users': new NavigationItem('Users', '/users'), + 'account': new NavigationItem('Account', '/user/{me}'), + 'register': new NavigationItem('Register', '/register'), + 'login': new NavigationItem('Login', '/login'), + 'logout': new NavigationItem('Logout', '/logout'), + 'help': new NavigationItem('Help', '/help'), + }; + + this.updateVisibility(); + + this.topNavigationView.render(this.items); + } + + updateVisibility() { + const b = Object.keys(this.items); + for (let key of b) { + this.items[key].available = true; + } + if (!this.authController.hasPrivilege('posts:list')) { + this.items.posts.available = false; + } + if (!this.authController.hasPrivilege('posts:upload')) { + this.items.upload.available = false; + } + if (!this.authController.hasPrivilege('comments:list')) { + this.items.comments.available = false; + } + if (!this.authController.hasPrivilege('tags:list')) { + this.items.tags.available = false; + } + if (!this.authController.hasPrivilege('users:list')) { + this.items.users.available = false; + } + if (this.authController.isLoggedIn()) { + this.items.register.available = false; + this.items.login.available = false; + } else { + this.items.account.available = false; + this.items.logout.available = false; + } + } + + activate(itemName) { + this.topNavigationView.activate(itemName); + } +} + +module.exports = TopNavigationController; diff --git a/static/js/controllers/users_controller.js b/static/js/controllers/users_controller.js new file mode 100644 index 0000000..a5e800d --- /dev/null +++ b/static/js/controllers/users_controller.js @@ -0,0 +1,38 @@ +'use strict'; + +class UsersController { + constructor(topNavigationController, authController, registrationView) { + this.topNavigationController = topNavigationController; + this.authController = authController; + this.registrationView = registrationView; + } + + listUsersRoute() { + this.topNavigationController.activate('users'); + } + + createUserRoute() { + const self = this; + this.topNavigationController.activate('register'); + this.registrationView.render({ + onRegistered: (user) => { + alert(user); + self.authController.login(user); + }}); + } + + showUserRoute(user) { + if (this.authController.isLoggedIn() && + user == this.authController.getCurrentUser().name) { + this.topNavigationController.activate('account'); + } else { + this.topNavigationController.activate('users'); + } + } + + editUserRoute(user) { + this.topNavigationController.activate('users'); + } +} + +module.exports = UsersController; diff --git a/static/js/main.js b/static/js/main.js new file mode 100644 index 0000000..bd9216e --- /dev/null +++ b/static/js/main.js @@ -0,0 +1,70 @@ +'use strict'; + +// ------------------ +// - import objects - +// ------------------ +const page = require('page'); +const handlebars = require('handlebars'); + +const RegistrationView = require('./views/registration_view.js'); +const TopNavigationView = require('./views/top_navigation_view.js'); +const TopNavigationController + = require('./controllers/top_navigation_controller.js'); + +const HomeController = require('./controllers/home_controller.js'); +const PostsController = require('./controllers/posts_controller.js'); +const UsersController = require('./controllers/users_controller.js'); +const HelpController = require('./controllers/help_controller.js'); +const AuthController = require('./controllers/auth_controller.js'); +const CommentsController = require('./controllers/comments_controller.js'); +const HistoryController = require('./controllers/history_controller.js'); +const TagsController = require('./controllers/tags_controller.js'); + +// ------------------- +// - resolve objects - +// ------------------- +const topNavigationView = new TopNavigationView(handlebars); +const registrationView = new RegistrationView(handlebars); + +const authController = new AuthController(null); +const topNavigationController + = new TopNavigationController(topNavigationView, authController); +// break cyclic dependency topNavigationView<->authController +authController.topNavigationController = topNavigationController; + +const homeController = new HomeController(topNavigationController); +const postsController = new PostsController(topNavigationController); +const usersController = new UsersController( + topNavigationController, + authController, + registrationView); +const helpController = new HelpController(topNavigationController); +const commentsController = new CommentsController(topNavigationController); +const historyController = new HistoryController(topNavigationController); +const tagsController = new TagsController(topNavigationController); + +// ----------------- +// - setup routing - +// ----------------- +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('/register', () => { usersController.createUserRoute(); }); +page('/users', () => { usersController.listUsersRoute(); }); +page('/user/:user', (user) => { usersController.showUserRoute(user); }); +page('/user/:user/edit', (user) => { usersController.editUserRoute(user); }); + +page('/history', () => { historyController.showHistoryRoute(); }); +page('/tags', () => { tagsController.listTagsRoute(); }); +page('/comments', () => { commentsController.listCommentsRoute(); }); +page('/login', () => { authController.loginRoute(); }); +page('/logout', () => { authController.logoutRoute(); }); +page('/help', () => { helpController.showHelpRoute(); }); + +page('*', () => { homeController.notFoundRoute(); }); + +page(); diff --git a/static/js/views/base_view.js b/static/js/views/base_view.js new file mode 100644 index 0000000..06baabd --- /dev/null +++ b/static/js/views/base_view.js @@ -0,0 +1,37 @@ +'use strict'; + +// fix iterating over NodeList in Chrome and Opera +NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; + +class BaseView { + constructor(handlebars) { + this.handlebars = handlebars; + this.contentHolder = document.getElementById('content-holder'); + } + + getTemplate(templatePath) { + const templateElement = document.getElementById(templatePath); + if (!templateElement) { + console.log('Missing template: ' + templatePath); + return null; + } + const templateText = templateElement.innerHTML; + return this.handlebars.compile(templateText); + } + + 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) => { + form.classList.add('show-validation'); + }); + } + + showView(html) { + this.contentHolder.innerHTML = html; + } +} + +module.exports = BaseView; diff --git a/static/js/views/registration_view.js b/static/js/views/registration_view.js new file mode 100644 index 0000000..2f1a31b --- /dev/null +++ b/static/js/views/registration_view.js @@ -0,0 +1,35 @@ +'use strict'; + +const config = require('../config.js'); +const BaseView = require('./base_view.js'); + +class RegistrationView extends BaseView { + constructor(handlebars) { + super(handlebars); + this.template = this.getTemplate('user-registration-template'); + } + + render(settings) { + this.showView(this.template()); + const form = document.querySelector('#content-holder form'); + this.decorateValidator(form); + + const userNameField = document.getElementById('user-name'); + const passwordField = document.getElementById('user-password'); + const emailField = document.getElementById('user-email'); + userNameField.setAttribute('pattern', config.service.userNameRegex); + passwordField.setAttribute('pattern', config.service.passwordRegex); + + form.addEventListener('submit', (e) => { + e.preventDefault(); + const user = { + name: userNameField.value, + password: passwordField.value, + email: emailField.value, + }; + settings.onRegistered(user); + }); + } +} + +module.exports = RegistrationView; diff --git a/static/js/views/top_navigation_view.js b/static/js/views/top_navigation_view.js new file mode 100644 index 0000000..e7a6d0e --- /dev/null +++ b/static/js/views/top_navigation_view.js @@ -0,0 +1,30 @@ +'use strict'; + +const BaseView = require('./base_view.js'); + +class TopNavigationView extends BaseView { + constructor(handlebars) { + super(handlebars); + this.template = this.getTemplate('top-nav-template'); + this.navHolder = document.getElementById('top-nav-holder'); + } + + render(items) { + this.navHolder.innerHTML = this.template({items: items}); + } + + activate(itemName) { + const allItemsSelector = '#top-nav-holder [data-name]'; + const currentItemSelector = + '#top-nav-holder [data-name="' + itemName + '"]'; + for (let item of document.querySelectorAll(allItemsSelector)) { + item.className = ''; + } + const currentItem = document.querySelectorAll(currentItemSelector); + if (currentItem.length > 0) { + currentItem[0].className = 'active'; + } + } +} + +module.exports = TopNavigationView; diff --git a/static/prepare_config.js b/static/prepare_config.js new file mode 100644 index 0000000..356e9f9 --- /dev/null +++ b/static/prepare_config.js @@ -0,0 +1,33 @@ +'use strict'; + +const fs = require('fs'); +const ini = require('ini'); +const merge = require('merge'); +const camelcaseKeys = require('camelcase-keys'); + +function parseIniFile(path) { + let result = ini.parse(fs.readFileSync(path, 'utf-8') + .replace(/#.+$/gm, '') + .replace(/\s+$/gm, '')); + Object.keys(result).map((key, _) => { + result[key] = camelcaseKeys(result[key]); + }); + return result; +} + +let config = parseIniFile('./config.ini.dist'); + +try { + const localConfig = parseIniFile('./config.ini'); + config = merge.recursive(config, localConfig); +} catch (e) { + console.warn('Local config does not exist, ignoring'); +} + +delete config.basic.secret; +delete config.smtp; +delete config.database; +config.service.userRanks = config.service.userRanks.split(/,\s*/); +config.service.tagCategories = config.service.tagCategories.split(/,\s*/); + +fs.writeFileSync('./static/js/.config.autogen.json', JSON.stringify(config)); |