aboutsummaryrefslogtreecommitdiff
path: root/static
diff options
context:
space:
mode:
Diffstat (limited to 'static')
-rw-r--r--static/css/forms.css79
-rw-r--r--static/css/main.css74
-rw-r--r--static/css/users.css16
-rw-r--r--static/html/index.htm71
-rw-r--r--static/js/.gitignore1
-rw-r--r--static/js/config.js4
-rw-r--r--static/js/controllers/auth_controller.js34
-rw-r--r--static/js/controllers/comments_controller.js13
-rw-r--r--static/js/controllers/help_controller.js13
-rw-r--r--static/js/controllers/history_controller.js13
-rw-r--r--static/js/controllers/home_controller.js17
-rw-r--r--static/js/controllers/posts_controller.js25
-rw-r--r--static/js/controllers/tags_controller.js13
-rw-r--r--static/js/controllers/top_navigation_controller.js69
-rw-r--r--static/js/controllers/users_controller.js38
-rw-r--r--static/js/main.js70
-rw-r--r--static/js/views/base_view.js37
-rw-r--r--static/js/views/registration_view.js35
-rw-r--r--static/js/views/top_navigation_view.js30
-rw-r--r--static/prepare_config.js33
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));

© 2015 - 2026 Jakob L. Kreuze