From bd8cdc85093975bf518fb9d9dfc95d0896690ed3 Mon Sep 17 00:00:00 2001 From: rebel Date: Wed, 17 May 2023 02:04:35 +0200 Subject: [PATCH] Add initial frontend --- client/css/banned-posts.styl | 29 +++++ client/html/banned_post_entry.tpl | 13 +++ client/html/banned_post_list.tpl | 25 +++++ .../js/controllers/banned_post_controller.js | 59 ++++++++++ client/js/main.js | 1 + client/js/models/banned_post.js | 57 ++++++++++ client/js/models/banned_post_list.js | 47 ++++++++ client/js/views/banned_posts_view.js | 103 ++++++++++++++++++ 8 files changed, 334 insertions(+) create mode 100644 client/css/banned-posts.styl create mode 100644 client/html/banned_post_entry.tpl create mode 100644 client/html/banned_post_list.tpl create mode 100644 client/js/controllers/banned_post_controller.js create mode 100644 client/js/models/banned_post.js create mode 100644 client/js/models/banned_post_list.js create mode 100644 client/js/views/banned_posts_view.js diff --git a/client/css/banned-posts.styl b/client/css/banned-posts.styl new file mode 100644 index 00000000..331f35bf --- /dev/null +++ b/client/css/banned-posts.styl @@ -0,0 +1,29 @@ +@import colors + +.content-wrapper.banned-posts + width: 100% + max-width: 45em + table + border-spacing: 0 + width: 100% + tr.default td + background: $default-banned-post-background-color + td, th + padding: .4em + &.color + input[type=text] + width: 8em + &.usages + text-align: center + &.remove, &.set-default + white-space: pre + th + white-space: nowrap + &:first-child + padding-left: 0 + &:last-child + padding-right: 0 + tfoot + display: none + form + width: auto diff --git a/client/html/banned_post_entry.tpl b/client/html/banned_post_entry.tpl new file mode 100644 index 00000000..defc6a92 --- /dev/null +++ b/client/html/banned_post_entry.tpl @@ -0,0 +1,13 @@ +<% + + <%- ctx.postBan.checksum %> + + + <%- ctx.makeRelativeTime(ctx.postBan.time) %> + + <% if (ctx.canDelete) { %> + + Unban + + <% } %> + diff --git a/client/html/banned_post_list.tpl b/client/html/banned_post_list.tpl new file mode 100644 index 00000000..a27aa0b3 --- /dev/null +++ b/client/html/banned_post_list.tpl @@ -0,0 +1,25 @@ +
+
+

Banned posts

+
+ + + + + + + + + +
ChecksumTime of ban
+
+ +
+ + <% if (ctx.canDelete) { %> +
+ +
+ <% } %> +
+
diff --git a/client/js/controllers/banned_post_controller.js b/client/js/controllers/banned_post_controller.js new file mode 100644 index 00000000..1daf7a86 --- /dev/null +++ b/client/js/controllers/banned_post_controller.js @@ -0,0 +1,59 @@ +"use strict"; + +const api = require("../api.js"); +const BannedPostList = require("../models/banned_post_list.js"); +const topNavigation = require("../models/top_navigation.js"); +const BannedPostsView = require("../views/banned_posts_view.js"); +const EmptyView = require("../views/empty_view.js"); + +class BannedPostController { + constructor() { + if (!api.hasPrivilege("posts:ban:list")) { + this._view = new EmptyView(); + this._view.showError( + "You don't have privileges to view banned posts." + ); + return; + } + + topNavigation.activate("banned-posts"); + topNavigation.setTitle("Listing banned posts"); + BannedPostList.get().then( + (response) => { + this._bannedPosts = response.results; + this._view = new BannedPostsView({ + bannedPosts: this._bannedPosts, + canDelete: api.hasPrivilege("poolCategories:delete") + }); + this._view.addEventListener("submit", (e) => + this._evtSubmit(e) + ); + }, + (error) => { + this._view = new EmptyView(); + this._view.showError(error.message); + } + ); + } + + _evtSubmit(e) { + this._view.clearMessages(); + this._view.disableForm(); + this._bannedPosts.save().then( + () => { + this._view.enableForm(); + this._view.showSuccess("Changes saved."); + }, + (error) => { + this._view.enableForm(); + this._view.showError(error.message); + } + ); + } +} + +module.exports = (router) => { + router.enter(["banned-posts"], (ctx, next) => { + ctx.controller = new BannedPostController(ctx, next); + }); +}; diff --git a/client/js/main.js b/client/js/main.js index c5bdc537..a89de2e1 100644 --- a/client/js/main.js +++ b/client/js/main.js @@ -83,6 +83,7 @@ Promise.resolve() controllers.push( require("./controllers/user_registration_controller.js") ); + controllers.push(require("./controllers/banned_post_controller.js")); // 404 controller needs to be registered last controllers.push(require("./controllers/not_found_controller.js")); diff --git a/client/js/models/banned_post.js b/client/js/models/banned_post.js new file mode 100644 index 00000000..cfa80c38 --- /dev/null +++ b/client/js/models/banned_post.js @@ -0,0 +1,57 @@ +"use strict"; + +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const events = require("../events.js"); + +class BannedPost extends events.EventTarget { + constructor() { + super(); + this._checksum = ""; + this._time = new Date(); + } + + get checksum() { + return this._checksum; + } + + get time() { + return this._time; + } + + set checksum(value) { + this._checksum = value; + } + + set time(value) { + this._time = value; + } + + static fromResponse(response) { + const ret = new BannedPost(); + ret._updateFromResponse(response); + return ret; + } + + delete() { + return api + .delete(uri.formatApiLink("post-ban", this._checksum)) + .then((response) => { + this.dispatchEvent( + new CustomEvent("delete", { + detail: { + bannedPost: this, + }, + }) + ); + return Promise.resolve(); + }); + } + + _updateFromResponse(response) { + this._checksum = response.checksum; + this.time = response.time; + } +} + +module.exports = BannedPost; diff --git a/client/js/models/banned_post_list.js b/client/js/models/banned_post_list.js new file mode 100644 index 00000000..2665b4c1 --- /dev/null +++ b/client/js/models/banned_post_list.js @@ -0,0 +1,47 @@ +const api = require("../api.js"); +const uri = require("../util/uri.js"); +const AbstractList = require("./abstract_list.js"); +const BannedPost = require("./banned_post.js"); + +class BannedPostList extends AbstractList { + constructor() { + super(); + this._deletedBans = []; + this.addEventListener("remove", (e) => this._evtBannedPostDeleted(e)); + } + + static get() { + return api + .get(uri.formatApiLink("post-ban")) + .then((response) => { + return Promise.resolve( + Object.assign({}, response, { + results: BannedPostList.fromResponse( + response.results + ), + }) + ); + }); + } + + save() { + let promises = []; + for (let BannedPost of this._deletedBans) { + promises.push(BannedPost.delete()); + } + + return Promise.all(promises).then((response) => { + this._deletedBans = []; + return Promise.resolve(); + }); + } + + _evtBannedPostDeleted(e) { + this._deletedBans.push(e.detail.BannedPost); + } +} + +BannedPostList._itemClass = BannedPost; +BannedPostList._itemName = "bannedPost"; + +module.exports = BannedPostList; diff --git a/client/js/views/banned_posts_view.js b/client/js/views/banned_posts_view.js new file mode 100644 index 00000000..89fe3b4b --- /dev/null +++ b/client/js/views/banned_posts_view.js @@ -0,0 +1,103 @@ +"use strict"; + +const events = require("../events.js"); +const views = require("../util/views.js"); +const BannedPost = require("../models/banned_post.js"); + +const template = views.getTemplate("banned-post-list"); +const rowTemplate = views.getTemplate("banned-post-entry"); + +class BannedPostsView extends events.EventTarget { + constructor(ctx) { + super(); + this._ctx = ctx; + this._hostNode = document.getElementById("content-holder"); + + views.replaceContent(this._hostNode, template(ctx)); + views.syncScrollPosition(); + views.decorateValidator(this._formNode); + + const bannedPostsToAdd = Array.from(ctx.bannedPosts); + for (let bannedPost of bannedPostsToAdd) { + this._addBannedPostRowNode(bannedPost); + } + + ctx.bannedPosts.addEventListener("remove", (e) => + this._evtBannedPostDeleted(e) + ); + + this._formNode.addEventListener("submit", (e) => + this._evtSaveButtonClick(e, ctx) + ); + } + + enableForm() { + views.enableForm(this._formNode); + } + + disableForm() { + views.disableForm(this._formNode); + } + + clearMessages() { + views.clearMessages(this._hostNode); + } + + showSuccess(message) { + views.showSuccess(this._hostNode, message); + } + + showError(message) { + views.showError(this._hostNode, message); + } + + get _formNode() { + return this._hostNode.querySelector("form"); + } + + get _tableBodyNode() { + return this._hostNode.querySelector("tbody"); + } + + _addBannedPostRowNode(bannedPost) { + const rowNode = rowTemplate( + Object.assign({}, this._ctx, { postBan: bannedPost }) + ); + + const removeLinkNode = rowNode.querySelector(".remove a"); + if (removeLinkNode) { + removeLinkNode.addEventListener("click", (e) => + this._evtDeleteButtonClick(e, rowNode) + ); + } + + this._tableBodyNode.appendChild(rowNode); + + rowNode._bannedPost = bannedPost; + bannedPost._rowNode = rowNode; + } + + _removeBannedPostRowNode(bannedPost) { + const rowNode = bannedPost._rowNode; + rowNode.parentNode.removeChild(rowNode); + } + + _evtBannedPostDeleted(e) { + this._removeBannedPostRowNode(e.detail.poolCategory); + } + + _evtDeleteButtonClick(e, rowNode, link) { + e.preventDefault(); + if (e.target.classList.contains("inactive")) { + return; + } + this._ctx.bannedPosts.remove(rowNode._bannedPost); + } + + _evtSaveButtonClick(e, ctx) { + e.preventDefault(); + this.dispatchEvent(new CustomEvent("submit")); + } +} + +module.exports = BannedPostsView;