From 95e3737e91412e58189449c43b7a667583225c2e Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Fri, 5 Aug 2022 14:26:29 +0000 Subject: [PATCH] Big development, almost finished a complete refactor --- base/article/routes.mjs | 4 +- base/server.mjs | 21 +- nfp_moe/api/server.mjs | 4 + nfp_moe/app/{widgets => admin}/dialogue.js | 0 nfp_moe/app/admin/editarticle.js | 21 +- nfp_moe/app/admin/editpage.js | 23 +- nfp_moe/app/admin/editstaff.js | 5 +- nfp_moe/app/{widgets => admin}/fileupload.js | 0 .../admin/{articles.js => site_articles.js} | 32 +-- nfp_moe/app/admin/{pages.js => site_pages.js} | 68 ++--- nfp_moe/app/admin/stafflist.js | 8 +- nfp_moe/app/admin_loader.js | 83 +++++++ nfp_moe/app/{api/common.js => api.js} | 2 +- nfp_moe/app/api/article.js | 64 ----- nfp_moe/app/api/article.p.js | 8 - nfp_moe/app/api/file.js | 12 - nfp_moe/app/api/media.js | 17 -- nfp_moe/app/api/page.js | 114 --------- nfp_moe/app/api/pagination.js | 81 ------ nfp_moe/app/api/staff.js | 38 --- .../app/{widgets/newsitem.js => article.js} | 46 ++-- nfp_moe/app/article/article.js | 199 --------------- .../{widgets/newsentry.js => article_slim.js} | 40 +-- nfp_moe/app/authentication.js | 3 + nfp_moe/app/darkmode.js | 25 -- nfp_moe/app/{widgets => }/editorblock.js | 0 nfp_moe/app/{widgets => }/fileinfo.js | 6 +- nfp_moe/app/footer.js | 39 +++ nfp_moe/app/footer/footer.js | 42 ---- nfp_moe/app/frontpage/frontpage.js | 146 ----------- nfp_moe/app/header.js | 115 +++++++++ nfp_moe/app/index.js | 113 +-------- nfp_moe/app/login/login.js | 101 -------- nfp_moe/app/login/logout.js | 15 -- nfp_moe/app/media.js | 50 ++++ nfp_moe/app/menu/menu.js | 92 ------- nfp_moe/app/{api/page.p.js => page_tree.js} | 13 +- nfp_moe/app/pages/page.js | 233 ------------------ nfp_moe/app/paginator.js | 39 +++ nfp_moe/app/polyfill.js | 8 - nfp_moe/app/site_article.js | 155 ++++++++++++ nfp_moe/app/site_login.js | 90 +++++++ nfp_moe/app/site_page.js | 224 +++++++++++++++++ nfp_moe/app/widgets/pages.js | 44 ---- nfp_moe/package.json | 4 +- nfp_moe/public/index.html | 11 +- 46 files changed, 972 insertions(+), 1486 deletions(-) rename nfp_moe/app/{widgets => admin}/dialogue.js (100%) rename nfp_moe/app/{widgets => admin}/fileupload.js (100%) rename nfp_moe/app/admin/{articles.js => site_articles.js} (89%) rename nfp_moe/app/admin/{pages.js => site_pages.js} (64%) create mode 100644 nfp_moe/app/admin_loader.js rename nfp_moe/app/{api/common.js => api.js} (96%) delete mode 100644 nfp_moe/app/api/article.js delete mode 100644 nfp_moe/app/api/article.p.js delete mode 100644 nfp_moe/app/api/file.js delete mode 100644 nfp_moe/app/api/media.js delete mode 100644 nfp_moe/app/api/page.js delete mode 100644 nfp_moe/app/api/pagination.js delete mode 100644 nfp_moe/app/api/staff.js rename nfp_moe/app/{widgets/newsitem.js => article.js} (65%) delete mode 100644 nfp_moe/app/article/article.js rename nfp_moe/app/{widgets/newsentry.js => article_slim.js} (73%) delete mode 100644 nfp_moe/app/darkmode.js rename nfp_moe/app/{widgets => }/editorblock.js (100%) rename nfp_moe/app/{widgets => }/fileinfo.js (95%) create mode 100644 nfp_moe/app/footer.js delete mode 100644 nfp_moe/app/footer/footer.js delete mode 100644 nfp_moe/app/frontpage/frontpage.js create mode 100644 nfp_moe/app/header.js delete mode 100644 nfp_moe/app/login/login.js delete mode 100644 nfp_moe/app/login/logout.js create mode 100644 nfp_moe/app/media.js delete mode 100644 nfp_moe/app/menu/menu.js rename nfp_moe/app/{api/page.p.js => page_tree.js} (78%) delete mode 100644 nfp_moe/app/pages/page.js create mode 100644 nfp_moe/app/paginator.js delete mode 100644 nfp_moe/app/polyfill.js create mode 100644 nfp_moe/app/site_article.js create mode 100644 nfp_moe/app/site_login.js create mode 100644 nfp_moe/app/site_page.js delete mode 100644 nfp_moe/app/widgets/pages.js diff --git a/base/article/routes.mjs b/base/article/routes.mjs index 491d570..49c44c8 100644 --- a/base/article/routes.mjs +++ b/base/article/routes.mjs @@ -39,12 +39,12 @@ export default class ArticleRoutes { let res = await ctx.db.safeCallProc('article_auth_get_all', [ ctx.state.auth_token, Math.max(ctx.query.get('page') || 1, 1), - Math.min(ctx.query.get('per_page') || 10, 25) + Math.min(ctx.query.get('per_page') || 20, 100) ]) let out = { articles: parseArticles(res.results[0]), - total_articles: res.results[0][0].total_articles, + total_articles: res.results[1][0].total_articles, } ctx.body = out diff --git a/base/server.mjs b/base/server.mjs index 35d842f..3984c78 100644 --- a/base/server.mjs +++ b/base/server.mjs @@ -15,6 +15,14 @@ export default class Server { this.port = port this.core = core + this.flaskaOptions = { + appendHeaders: { + 'Content-Security-Policy': `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'`, + }, + log: this.core.log, + nonce: ['script-src'], + nonceCacheLength: 50, + } this.authenticate = authenticate this.formidable = FormidableHandler.bind(this, formidable) this.jsonHandler = JsonHandler @@ -23,8 +31,12 @@ export default class Server { new ArticleRoutes(), new AuthenticationRoutes(), ] + + this.init() } + init() { } + getRouteInstance(type) { for (let route of this.routes) { if (route instanceof type) { @@ -39,14 +51,7 @@ export default class Server { runCreateServer() { // Create our server - this.flaska = new Flaska({ - appendHeaders: { - 'Content-Security-Policy': `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-ancestors 'none'`, - }, - log: this.core.log, - nonce: ['script-src'], - nonceCacheLength: 50, - }, this.http) + this.flaska = new Flaska(this.flaskaOptions, this.http) // Create our database pool let pool = this.runCreateDatabase() diff --git a/nfp_moe/api/server.mjs b/nfp_moe/api/server.mjs index c8df354..f9f1b2f 100644 --- a/nfp_moe/api/server.mjs +++ b/nfp_moe/api/server.mjs @@ -4,6 +4,10 @@ import ServeHandler from '../base/serve.mjs' import PageRoutes from '../base/page/routes.mjs' export default class Server extends Parent { + init() { + this.flaskaOptions.appendHeaders['Content-Security-Policy'] = `default-src 'self'; script-src 'self' talk.hyvor.com; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; iframe-src talk.hyvor.com` //; frame-ancestors 'none'` + } + addCustomRoutes() { let page = this.getRouteInstance(PageRoutes) diff --git a/nfp_moe/app/widgets/dialogue.js b/nfp_moe/app/admin/dialogue.js similarity index 100% rename from nfp_moe/app/widgets/dialogue.js rename to nfp_moe/app/admin/dialogue.js diff --git a/nfp_moe/app/admin/editarticle.js b/nfp_moe/app/admin/editarticle.js index 5d95cc5..0e077ae 100644 --- a/nfp_moe/app/admin/editarticle.js +++ b/nfp_moe/app/admin/editarticle.js @@ -1,8 +1,8 @@ require('./dtsel') -const FileUpload = require('../widgets/fileupload') -const Page = require('../api/page') -const Fileinfo = require('../widgets/fileinfo') -const common = require('../api/common') +const FileUpload = require('./fileupload') +const PageTree = require('../page_tree') +const Fileinfo = require('../fileinfo') +const api = require('../api') const Editor = require('./editor') const EditArticle = { @@ -16,7 +16,7 @@ const EditArticle = { staff: [], } this.pages = [{id: null, name: 'Frontpage'}] - this.pages = this.pages.concat(Page.getFlatTree()) + this.pages = this.pages.concat(PageTree.getFlatTree()) this.newBanner = null this.newMedia = null this.dateInstance = null @@ -35,10 +35,11 @@ const EditArticle = { this.lastid = m.route.param('id') return this.requestArticle( - common.sendRequest({ - method: 'GET', - url: '/api/auth/articles/' + (this.lastid === 'add' ? '0' : this.lastid), - })) + api.sendRequest({ + method: 'GET', + url: '/api/auth/articles/' + (this.lastid === 'add' ? '0' : this.lastid), + }) + ) }, requestArticle: function(data) { @@ -152,7 +153,7 @@ const EditArticle = { .then(body => { formData.append('content', JSON.stringify(body)) - return common.sendRequest({ + return api.sendRequest({ method: 'PUT', url: '/api/auth/articles/' + (this.lastid === 'add' ? '0' : this.lastid), body: formData, diff --git a/nfp_moe/app/admin/editpage.js b/nfp_moe/app/admin/editpage.js index b2d39ec..b2361a3 100644 --- a/nfp_moe/app/admin/editpage.js +++ b/nfp_moe/app/admin/editpage.js @@ -1,7 +1,7 @@ -const FileUpload = require('../widgets/fileupload') -const Page = require('../api/page.p') +const FileUpload = require('./fileupload') +const PageTree = require('../page_tree') -const common = require('../api/common') +const api = require('../api') const Editor = require('./editor') const EditPage = { @@ -12,7 +12,7 @@ const EditPage = { page: null, } this.pages = [{id: null, name: 'Frontpage'}] - this.pages = this.pages.concat(Page.getFlatTree()) + this.pages = this.pages.concat(PageTree.getFlatTree()) this.newBanner = null this.newMedia = null @@ -31,10 +31,11 @@ const EditPage = { this.lastid = m.route.param('id') return this.requestPage( - common.sendRequest({ - method: 'GET', - url: '/api/auth/pages/' + (this.lastid === 'add' ? '0' : this.lastid), - })) + api.sendRequest({ + method: 'GET', + url: '/api/auth/pages/' + (this.lastid === 'add' ? '0' : this.lastid), + }) + ) }, requestPage: function(data) { @@ -135,7 +136,7 @@ const EditPage = { .then(body => { formData.append('content', JSON.stringify(body)) - return common.sendRequest({ + return api.sendRequest({ method: 'PUT', url: '/api/auth/pages/' + (this.lastid === 'add' ? '0' : this.lastid), body: formData, @@ -148,9 +149,9 @@ const EditPage = { this.lastid = data.page.id.toString() m.route.set('/admin/pages/' + data.page.id) } - return Page.refreshTree().then(() => { + return PageTree.refreshTree().then(() => { this.pages = [{id: null, name: 'Frontpage'}] - this.pages = this.pages.concat(Page.getFlatTree()) + this.pages = this.pages.concat(PageTree.getFlatTree()) return data }) diff --git a/nfp_moe/app/admin/editstaff.js b/nfp_moe/app/admin/editstaff.js index 2340421..8d3f0d6 100644 --- a/nfp_moe/app/admin/editstaff.js +++ b/nfp_moe/app/admin/editstaff.js @@ -1,6 +1,5 @@ -const Staff = require('../api/staff') - const EditStaff = { + /* oninit: function(vnode) { this.fetchStaff(vnode) }, @@ -146,7 +145,7 @@ const EditStaff = { ]), ]) ) - }, + },*/ } module.exports = EditStaff diff --git a/nfp_moe/app/widgets/fileupload.js b/nfp_moe/app/admin/fileupload.js similarity index 100% rename from nfp_moe/app/widgets/fileupload.js rename to nfp_moe/app/admin/fileupload.js diff --git a/nfp_moe/app/admin/articles.js b/nfp_moe/app/admin/site_articles.js similarity index 89% rename from nfp_moe/app/admin/articles.js rename to nfp_moe/app/admin/site_articles.js index 2937f2d..f617141 100644 --- a/nfp_moe/app/admin/articles.js +++ b/nfp_moe/app/admin/site_articles.js @@ -1,8 +1,8 @@ -const Article = require('../api/article') -const pagination = require('../api/pagination') -const Dialogue = require('../widgets/dialogue') -const Pages = require('../widgets/pages') -const common = require('../api/common') +const Dialogue = require('./dialogue') +const api = require('../api') +const Paginator = require('../paginator') + +const ItemsPerPage = 20 const AdminArticles = { oninit: function(vnode) { @@ -47,7 +47,7 @@ const AdminArticles = { this.loading = true } - return common.sendRequest({ + return api.sendRequest({ method: 'GET', url: '/api/auth/articles?page=' + (this.lastpage || 1), }) @@ -100,8 +100,8 @@ const AdminArticles = { : '' }, [ m('td', m(m.route.Link, { href: '/admin/articles/' + article.id }, article.name)), + m('td', m(m.route.Link, { href: '/article/' + article.path }, 'View')), m('td', m(m.route.Link, { href: article.page_path }, article.page_name)), - m('td', m(m.route.Link, { href: '/article/' + article.path }, '/article/' + article.path)), m('td.right', article.publish_at.replace('T', ' ').split('.')[0]), m('td.right', article.admin_name), m('td.right', m('button', { onclick: function() { vnode.state.removeArticle = article } }, 'Remove')), @@ -111,13 +111,13 @@ const AdminArticles = { view: function(vnode) { return [ - m('div.admin-wrapper', [ - m('div.admin-actions', [ + m('div.wrapper.admin', [ + m('div.inside', [ + m('h2', 'All articles'), + m('div.actions', [ m('span', 'Actions:'), m(m.route.Link, { href: '/admin/articles/add' }, 'Create new article'), ]), - m('article.editarticle', [ - m('header', m('h1', 'All articles')), m('div.error', { hidden: !this.error, onclick: function() { vnode.state.error = '' }, @@ -128,8 +128,8 @@ const AdminArticles = { m('thead', m('tr', [ m('th', 'Title'), - m('th', 'Page'), m('th', 'Path'), + m('th', 'Page'), m('th.right', 'Publish'), m('th.right', 'By'), m('th.right', 'Actions'), @@ -138,10 +138,12 @@ const AdminArticles = { m('tbody', this.data.articles.map((article) => this.drawArticle(vnode, article))), ], ), - /*m(Pages, { + m(Paginator, { base: '/admin/articles', - links: this.links, - }),*/ + page: this.currentPage, + perPage: ItemsPerPage, + total: this.data.total_articles, + }), ]), ]), m(Dialogue, { diff --git a/nfp_moe/app/admin/pages.js b/nfp_moe/app/admin/site_pages.js similarity index 64% rename from nfp_moe/app/admin/pages.js rename to nfp_moe/app/admin/site_pages.js index e55a4d0..2b453f8 100644 --- a/nfp_moe/app/admin/pages.js +++ b/nfp_moe/app/admin/site_pages.js @@ -1,6 +1,6 @@ -const Page = require('../api/page.p') -const Dialogue = require('../widgets/dialogue') -const common = require('../api/common') +const PageTree = require('../page_tree') +const Dialogue = require('./dialogue') +const api = require('../api') const AdminPages = { oninit: function(vnode) { @@ -16,7 +16,7 @@ const AdminPages = { this.loading = true this.error = '' - return common.sendRequest({ + return api.sendRequest({ method: 'GET', url: '/api/auth/pages', }) @@ -37,11 +37,11 @@ const AdminPages = { this.loading = true m.redraw() - return common.sendRequest({ + return api.sendRequest({ method: 'DELETE', url: '/api/auth/pages/' + removingPage.id, }) - .then(() => Page.refreshTree()) + .then(() => PageTree.refreshTree()) .then( () => this.fetchPages(vnode), (err) => { @@ -68,33 +68,37 @@ const AdminPages = { view: function(vnode) { return [ - (this.loading ? - m('div.loading-spinner') - : m('div.admin-wrapper', [ - m('div.admin-actions', [ - m('span', 'Actions:'), - m(m.route.Link, { href: '/admin/pages/add' }, 'Create new page'), - ]), - m('article.editpage', [ - m('header', m('h1', 'All pages')), - m('div.error', { - hidden: !this.error, - onclick: () => { this.fetchPages(vnode) }, - }, this.error), - m('table', [ - m('thead', - m('tr', [ - m('th', 'Title'), - m('th', 'Path'), - m('th.right', 'Updated'), - m('th.right', 'Actions'), - ]) - ), - m('tbody', this.pages.map(AdminPages.drawPage.bind(this, vnode))), - ]), + m('div.wrapper.admin', [ + m('div.inside', [ + m('h2', 'All pages'), + m('div.actions', [ + m('span', 'Actions:'), + m(m.route.Link, { href: '/admin/pages/add' }, 'Create new page'), ]), - ]) - ), + m('div.error', { + hidden: !this.error, + onclick: function() { vnode.state.error = '' }, + }, this.error), + this.loading + ? m('div.loading-spinner.full') + : m('table', [ + m('thead', + m('tr', [ + m('th', 'Title'), + m('th', 'Path'), + m('th.right', 'Updated'), + m('th.right', 'Actions'), + ]) + ), + m('tbody', this.pages.map(AdminPages.drawPage.bind(this, vnode))), + ], + ), + /*m(Pages, { + base: '/admin/articles', + links: this.links, + }),*/ + ]), + ]), m(Dialogue, { hidden: vnode.state.removePage === null, title: 'Delete ' + (vnode.state.removePage ? vnode.state.removePage.name : ''), diff --git a/nfp_moe/app/admin/stafflist.js b/nfp_moe/app/admin/stafflist.js index 7ffeb9c..5517906 100644 --- a/nfp_moe/app/admin/stafflist.js +++ b/nfp_moe/app/admin/stafflist.js @@ -1,8 +1,8 @@ -const Staff = require('../api/staff') -const Dialogue = require('../widgets/dialogue') -const Pages = require('../widgets/pages') +const Dialogue = require('./dialogue') +const Pages = require('../paginator') const AdminStaffList = { + /* oninit: function(vnode) { this.error = '' this.lastpage = m.route.param('page') || '1' @@ -104,7 +104,7 @@ const AdminStaffList = { onno: function() { vnode.state.removeStaff = null }, }), ] - }, + },*/ } module.exports = AdminStaffList diff --git a/nfp_moe/app/admin_loader.js b/nfp_moe/app/admin_loader.js new file mode 100644 index 0000000..4cc2c1c --- /dev/null +++ b/nfp_moe/app/admin_loader.js @@ -0,0 +1,83 @@ +const Authentication = require('./authentication') + +window.adminRoutes = {} + +let loadingAdmin = false +let loadedAdmin = false +let loaded = 0 +let elements = [] + +const onLoaded = function() { + loaded++ + if (loaded < 2) return + + Authentication.setAdmin(Authentication.currentUser && Authentication.currentUser.rank >= 10) + loadedAdmin = true + m.route.set(m.route.get()) +} + +const onError = function(a, b, c) { + Authentication.clearToken() + elements.forEach(function(x) { x.remove() }) + loadedAdmin = loadingAdmin = false + loaded = 0 + m.route.set('/') +} + +const loadAdmin = function(user) { + if (loadingAdmin) { + if (loadedAdmin) { + Authentication.setAdmin(user && user.rank >= 10) + } + return + } + if (!user || user.rank < 10) return + + loadingAdmin = true + + let token = Authentication.getToken() + let element = document.createElement('link') + elements.push(element) + element.setAttribute('rel', 'stylesheet') + element.setAttribute('type', 'text/css') + element.setAttribute('href', '/assets/admin.css?token=' + token) + element.onload = onLoaded + element.onerror = onError + document.getElementsByTagName('head')[0].appendChild(element) + + element = document.createElement('script') + elements.push(element) + element.setAttribute('type', 'text/javascript') + element.setAttribute('src', '/assets/admin.js?token=' + token) + element.onload = onLoaded + element.onerror = onError + document.body.appendChild(element) + + element = document.createElement('script') + elements.push(element) + element.setAttribute('type', 'text/javascript') + element.setAttribute('src', '/assets/editor.js') + element.onload = onLoaded + element.onerror = onError + document.body.appendChild(element) +} + +Authentication.addEvent(loadAdmin) +if (Authentication.currentUser) { + loadAdmin(Authentication.currentUser) +} + +const Loader = { + view: function() { return m('div.loading-spinner') }, +} +const AdminResolver = { + onmatch: function(args, requestedPath) { + if (window.adminRoutes[args.path]) { + return window.adminRoutes[args.path][args.id && 1 || 0] + } + return Loader + }, + render: function(vnode) { return vnode }, +} + +module.exports = AdminResolver diff --git a/nfp_moe/app/api/common.js b/nfp_moe/app/api.js similarity index 96% rename from nfp_moe/app/api/common.js rename to nfp_moe/app/api.js index 83f34e3..cf9c3bf 100644 --- a/nfp_moe/app/api/common.js +++ b/nfp_moe/app/api.js @@ -1,4 +1,4 @@ -const Authentication = require('../authentication') +const Authentication = require('./authentication') exports.sendRequest = function(options, isPagination) { let token = Authentication.getToken() diff --git a/nfp_moe/app/api/article.js b/nfp_moe/app/api/article.js deleted file mode 100644 index 14b885d..0000000 --- a/nfp_moe/app/api/article.js +++ /dev/null @@ -1,64 +0,0 @@ -const common = require('./common') - -exports.createArticle = function(body) { - return common.sendRequest({ - method: 'POST', - url: '/api/articles', - body: body, - }) -} - -exports.updateArticle = function(id, body) { - return common.sendRequest({ - method: 'PUT', - url: '/api/articles/' + id, - body: body, - }) -} - -exports.getAllArticles = function() { - return common.sendRequest({ - method: 'GET', - url: '/api/articles?includes=parent', - }) -} - -exports.getAllArticlesPagination = function(options) { - let extra = '' - - if (options.sort) { - extra += '&sort=' + options.sort - } - if (options.per_page) { - extra += '&perPage=' + options.per_page - } - if (options.page) { - extra += '&page=' + options.page - } - if (options.includes) { - extra += '&includes=' + options.includes.join(',') - } - - return '/api/articles?' + extra -} - -exports.getAllPageArticles = function(pageId, includes) { - return common.sendRequest({ - method: 'GET', - url: '/api/pages/' + pageId + '/articles?includes=' + includes.join(','), - }) -} - -exports.getArticle = function(id) { - return common.sendRequest({ - method: 'GET', - url: '/api/articles/' + id + '?includes=media,parent,banner,files', - }) -} - -exports.removeArticle = function(article, id) { - return common.sendRequest({ - method: 'DELETE', - url: '/api/articles/' + id, - }) -} diff --git a/nfp_moe/app/api/article.p.js b/nfp_moe/app/api/article.p.js deleted file mode 100644 index 713fcf1..0000000 --- a/nfp_moe/app/api/article.p.js +++ /dev/null @@ -1,8 +0,0 @@ -const common = require('./common') - -exports.getArticle = function(id) { - return common.sendRequest({ - method: 'GET', - url: '/api/articles/' + id, - }) -} diff --git a/nfp_moe/app/api/file.js b/nfp_moe/app/api/file.js deleted file mode 100644 index 3866ea5..0000000 --- a/nfp_moe/app/api/file.js +++ /dev/null @@ -1,12 +0,0 @@ -const common = require('./common') - -exports.uploadFile = function(articleId, file) { - let formData = new FormData() - formData.append('file', file) - - return common.sendRequest({ - method: 'POST', - url: '/api/articles/' + articleId + '/file', - body: formData, - }) -} diff --git a/nfp_moe/app/api/media.js b/nfp_moe/app/api/media.js deleted file mode 100644 index b6a641b..0000000 --- a/nfp_moe/app/api/media.js +++ /dev/null @@ -1,17 +0,0 @@ -const common = require('./common') - -exports.uploadMedia = function(file, height) { - let formData = new FormData() - formData.append('file', file) - - let extra = '' - if (height) { - extra = '?height=' + height - } - - return common.sendRequest({ - method: 'POST', - url: '/api/media' + extra, - body: formData, - }) -} diff --git a/nfp_moe/app/api/page.js b/nfp_moe/app/api/page.js deleted file mode 100644 index 8cf03ef..0000000 --- a/nfp_moe/app/api/page.js +++ /dev/null @@ -1,114 +0,0 @@ -const common = require('./common') - -const Tree = window.__nfptree && window.__nfptree.tree || [] - -exports.Tree = Tree - -exports.createPage = function(body) { - return common.sendRequest({ - method: 'POST', - url: '/api/pages', - body: body, - }).then(function(res) { - res.children = [] - if (!res.parent_id) { - Tree.push(res) - } else { - for (let i = 0; i < Tree.length; i++) { - if (Tree[i].id === res.parent_id) { - Tree[i].children.push(res) - break - } - } - } - return res - }) -} - -function processPageBranch(arr, branches, prefix) { - branches.forEach((page) => { - arr.push({ id: page.id, name: prefix + page.name }) - if (page.children && page.children.length) { - processPageBranch(arr, page.children, page.name + ' -> ') - } - }) -} - -exports.getFlatTree = function() { - let arr = [] - processPageBranch(arr, Tree, '') - return arr -} - -exports.getTree = function() { - return common.sendRequest({ - method: 'GET', - url: '/api/pages?tree=true&includes=children&fields=id,name,path,children(id,name,path)', - }) -} - -exports.updatePage = function(id, body) { - return common.sendRequest({ - method: 'PUT', - url: '/api/pages/' + id, - body: body, - }).then(function(res) { - for (let i = 0; i < Tree.length; i++) { - if (Tree[i].id === res.id) { - res.children = Tree[i].children - Tree[i] = res - break - } else if (Tree[i].id === res.parent_id) { - for (let x = 0; x < Tree[i].children.length; x++) { - if (Tree[i].children[x].id === res.id) { - res.children = Tree[i].children[x].children - Tree[i].children[x] = res - break - } - } - break - } - } - if (!res.children) { - res.children = [] - } - return res - }) -} - -exports.getAllPages = function() { - return common.sendRequest({ - method: 'GET', - url: '/api/pages', - }) -} - -exports.getPage = function(id) { - return common.sendRequest({ - method: 'GET', - url: '/api/pages/' + id + '?includes=media,banner', - }) -} - -exports.removePage = function(page, id) { - return common.sendRequest({ - method: 'DELETE', - url: '/api/pages/' + id, - }).then(function() { - for (let i = 0; i < Tree.length; i++) { - if (Tree[i].id === page.id) { - Tree.splice(i, 1) - break - } else if (Tree[i].id === page.parent_id) { - for (let x = 0; x < Tree[i].children.length; x++) { - if (Tree[i].children[x].id === page.id) { - Tree[i].children.splice(x, 1) - break - } - } - break - } - } - return null - }) -} diff --git a/nfp_moe/app/api/pagination.js b/nfp_moe/app/api/pagination.js deleted file mode 100644 index bca9076..0000000 --- a/nfp_moe/app/api/pagination.js +++ /dev/null @@ -1,81 +0,0 @@ -const common = require('./common') - -function hasRel(x) { - return x && x.rel; -} - -function intoRels (acc, x) { - function splitRel (rel) { - acc[rel] = xtend(x, { rel: rel }); - } - - x.rel.split(/\s+/).forEach(splitRel); - - return acc; -} - -function createObjects (acc, p) { - // rel="next" => 1: rel 2: next - var m = p.match(/\s*(.+)\s*=\s*"?([^"]+)"?/) - if (m) acc[m[1]] = m[2]; - return acc; -} - -var hasOwnProperty = Object.prototype.hasOwnProperty; - -function extend() { - var target = {} - - for (var i = 0; i < arguments.length; i++) { - var source = arguments[i] - - for (var key in source) { - if (hasOwnProperty.call(source, key)) { - target[key] = source[key] - } - } - } - - return target -} - -function parseLink(link) { - try { - var m = link.match(/]*)>(.*)/) - , linkUrl = m[1] - , parts = m[2].split(';') - , qry = new URL(linkUrl).searchParams; - - parts.shift(); - - var info = parts - .reduce(createObjects, {}); - - info = extend(qry, info); - info.url = linkUrl; - return info; - } catch (e) { - return null; - } -} - -function parse(linkHeader) { - return linkHeader.split(/,\s* { - return m(EditorBlock, { block: block }) + m('div', [ + m('div.description', + article.content.blocks.map(block => { + return m(EditorBlock, { block: block }) + }), + ), + files.map(function(file) { + return m(Fileinfo, { file: file, trim: true }) }), - (article.files && article.files.length - ? article.files.map(function(file) { - return m(Fileinfo, { file: file, trim: true }) - }) - : null), - m('span.entrymeta', [ + m('p.meta', [ 'Posted ', - (article.page_path ? 'in' : ''), - (article.page_path ? m(m.route.Link, { href: '/page/' + article.page_path }, article.page_name) : null), + (article.page_path ? ['in ', m(m.route.Link, { href: '/page/' + article.page_path }, article.page_name), ' '] : ''), 'at ' + (article.publish_at.replace('T', ' ').split('.')[0]).substr(0, 16), ' by ' + (article.admin_name || 'Admin'), ]), @@ -85,4 +93,4 @@ const Newsitem = { }, } -module.exports = Newsitem +module.exports = Article diff --git a/nfp_moe/app/article/article.js b/nfp_moe/app/article/article.js deleted file mode 100644 index f8aaabf..0000000 --- a/nfp_moe/app/article/article.js +++ /dev/null @@ -1,199 +0,0 @@ -const m = require('mithril') -const ApiArticle = require('../api/article.p') -const Authentication = require('../authentication') -const Fileinfo = require('../widgets/fileinfo') -const EditorBlock = require('../widgets/editorblock') - -const Article = { - oninit: function(vnode) { - this.error = '' - this.loading = false - this.showLoading = null - this.data = { - article: null, - files: [], - } - this.showcomments = false - - if (window.__nfpdata) { - this.path = m.route.param('id') - this.data.article = window.__nfpdata - window.__nfpdata = null - } else { - this.fetchArticle(vnode) - } - }, - - onbeforeupdate: function(vnode) { - if (this.path !== m.route.param('id')) { - this.fetchArticle(vnode) - } - }, - - fetchArticle: function(vnode) { - this.error = '' - this.path = m.route.param('id') - this.showcomments = false - - if (this.showLoading) { - clearTimeout(this.showLoading) - } - - if (this.data.article) { - this.showLoading = setTimeout(() => { - this.showLoading = null - this.loading = true - m.redraw() - }, 150) - } else { - this.loading = true - } - - ApiArticle.getArticle(this.path) - .then((result) => { - this.data = result - - if (this.data.article.media_alt_prefix) { - this.data.article.pictureFallback = this.data.article.media_alt_prefix + '_small.jpg' - this.data.article.pictureJpeg = this.data.article.media_alt_prefix + '_small.jpg' + ' 720w, ' - + this.data.article.media_alt_prefix + '_medium.jpg' + ' 1300w, ' - + this.data.article.media_alt_prefix + '_large.jpg 1920w' - this.data.article.pictureAvif = this.data.article.media_alt_prefix + '_small.avif' + ' 720w, ' - + this.data.article.media_alt_prefix + '_medium.avif' + ' 1300w, ' - + this.data.article.media_alt_prefix + '_large.avif 1920w' - - this.data.article.pictureCover = '(max-width: 840px) calc(100vw - 82px), ' - + '758px' - } else { - this.data.article.pictureFallback = null - this.data.article.pictureJpeg = null - this.data.article.pictureAvif = null - this.data.article.pictureCover = null - } - - if (!this.data.article) { - this.error = 'Article not found' - } - }, (err) => { - this.error = err.message - }) - .then(() => { - clearTimeout(this.showLoading) - this.showLoading = null - this.loading = false - m.redraw() - }) - }, - - view: function(vnode) { - let article = this.data.article - return ( - this.loading ? - m('article.article', m('div.loading-spinner')) - : this.error - ? m('div.error-wrapper', m('div.error', { - onclick: function() { - vnode.state.error = '' - vnode.state.fetchArticle(vnode) - }, - }, 'Article error: ' + this.error)) - : m('article.article', [ - article.page_path - ? m('div.goback', ['« ', m(m.route.Link, { href: '/page/' + article.page_path }, article.page_name)]) - : null, - m('header', m('h1', article.name)), - m('.fr-view', [ - article.pictureFallback - ? m('a.cover', { - rel: 'noopener', - href: article.media_path, - }, [ - m('picture', [ - m('source', { - srcset: article.pictureAvif, - sizes: article.pictureCover, - type: 'image/avif', - }), - m('img', { - srcset: article.pictureJpeg, - sizes: article.pictureCover, - alt: 'Image for news item ' + article.name, - src: article.pictureFallback, - }), - ]), - ]) - : null, - article.content.blocks.map(block => { - return m(EditorBlock, { block: block }) - }), - this.data.files.map(function(file) { - return m(Fileinfo, { file: file }) - }), - m('div.entrymeta', [ - 'Posted ', - article.page_path - ? [ - 'in', - m(m.route.Link, { href: '/page/' + article.page_path }, article.page_name) - ] - : '', - 'at ' + (article.publish_at.replace('T', ' ').split('.')[0]).substr(0, 16), - ' by ' + (article.admin_name || 'Admin'), - ]), - ]), - Authentication.currentUser - ? m('div.admin-actions', [ - m('span', 'Admin controls:'), - m(m.route.Link, { href: '/admin/articles/' + article.path }, 'Edit article'), - ]) - : null, - this.showcomments - ? m('div.commentcontainer', [ - m('div#disqus_thread', { oncreate: function() { - let fullhost = window.location.protocol + '//' + window.location.host - /*eslint-disable */ - window.disqus_config = function () { - this.page.url = fullhost + '/article/' + vnode.state.article.path - this.page.identifier = 'article-' + vnode.state.article.id - }; - (function() { // DON'T EDIT BELOW THIS LINE - var d = document, s = d.createElement('script'); - s.src = 'https://nfp-moe.disqus.com/embed.js'; - s.setAttribute('data-timestamp', +new Date()); - (d.head || d.body).appendChild(s); - })() - /*eslint-enable */ - }}, m('div.loading-spinner')), - ]) - : m('button.opencomments', { - onclick: function() { vnode.state.showcomments = true }, - }, 'Open comment discussion'), - ]) - ) - }, -} - -module.exports = Article - -/* -
- - -*/ diff --git a/nfp_moe/app/widgets/newsentry.js b/nfp_moe/app/article_slim.js similarity index 73% rename from nfp_moe/app/widgets/newsentry.js rename to nfp_moe/app/article_slim.js index f0ca51a..64df03b 100644 --- a/nfp_moe/app/widgets/newsentry.js +++ b/nfp_moe/app/article_slim.js @@ -1,6 +1,6 @@ const Fileinfo = require('./fileinfo') -const Newsentry = { +const Articleslim = { oninit: function(vnode) { this.lastId = null this.onbeforeupdate(vnode) @@ -8,10 +8,10 @@ const Newsentry = { strip: function(html) { var doc = new DOMParser().parseFromString(html, 'text/html') - var out = doc.body.textContent || '' + var out = (doc.body.textContent || '').replace(/([\.!?])/g, '$1 ') var splitted = out.split('.') if (splitted.length > 2) { - return splitted.slice(0, 2).join('.') + '...' + return splitted.slice(0, 2).join('.') + '...' } return out }, @@ -23,13 +23,15 @@ const Newsentry = { this.lastId = article.id this.description = null - for (let i = 0; i < article.content.blocks.length; i++) { - if (article.content.blocks[i].type === 'paragraph') { - this.description = article.content.blocks[i].data.text - break - } else if (article.content.blocks[i].type === 'htmlraw') { - this.description = this.strip(article.content.blocks[i].data.html) - break + if (article.content) { + for (let i = 0; i < article.content.blocks.length; i++) { + if (article.content.blocks[i].type === 'paragraph') { + this.description = article.content.blocks[i].data.text + break + } else if (article.content.blocks[i].type === 'htmlraw') { + this.description = this.strip(article.content.blocks[i].data.html) + break + } } } @@ -56,7 +58,7 @@ const Newsentry = { view: function(vnode) { let article = vnode.attrs.article - return m('newsentry', [ + return m('articleslim', [ this.pictureFallback ? m(m.route.Link, { class: 'cover', @@ -77,23 +79,21 @@ const Newsentry = { ]) ) : m('a.cover.nobg'), - m('div.entrycontent', [ - m('div.title', [ - m(m.route.Link, - { href: '/article/' + article.path }, - m('h3', [article.name]) - ), - ]), + m('div', [ + m(m.route.Link, + { class: 'title', href: '/article/' + article.path }, + article.name + ), (article.files && article.files.length ? article.files.map(function(file) { return m(Fileinfo, { file: file, slim: true }) }) : this.description - ? m('span.entrydescription', this.description) + ? m('p.description', this.description) : null), ]), ]) }, } -module.exports = Newsentry +module.exports = Articleslim diff --git a/nfp_moe/app/authentication.js b/nfp_moe/app/authentication.js index 873c769..8283e44 100644 --- a/nfp_moe/app/authentication.js +++ b/nfp_moe/app/authentication.js @@ -7,6 +7,7 @@ const Authentication = { authListeners: [], updateToken: function(token) { + console.log('updateToken', token) if (!token) return Authentication.clearToken() localStorage.setItem(storageName, token) Authentication.currentUser = JSON.parse(atob(token.split('.')[1])) @@ -17,6 +18,8 @@ const Authentication = { }, clearToken: function() { + var err = new Error() + console.log('clearing', err.stack) Authentication.currentUser = null localStorage.removeItem(storageName) Authentication.isAdmin = false diff --git a/nfp_moe/app/darkmode.js b/nfp_moe/app/darkmode.js deleted file mode 100644 index 61e29fd..0000000 --- a/nfp_moe/app/darkmode.js +++ /dev/null @@ -1,25 +0,0 @@ -const storageName = 'darkmode' - -const Darkmode = { - darkIsOn: false, - - setDarkMode: function(setOn) { - if (setOn) { - localStorage.setItem(storageName, true) - document.body.className = 'darkmodeon' + ' ' + (window.supportsavif ? 'avifsupport' : 'jpegonly') - Darkmode.darkIsOn = true - } else { - localStorage.removeItem(storageName) - document.body.className = 'daymode' + ' ' + (window.supportsavif ? 'avifsupport' : 'jpegonly') - Darkmode.darkIsOn = false - } - }, - - isOn: function() { - return Darkmode.darkIsOn - }, -} - -Darkmode.darkIsOn = localStorage.getItem(storageName) - -module.exports = Darkmode diff --git a/nfp_moe/app/widgets/editorblock.js b/nfp_moe/app/editorblock.js similarity index 100% rename from nfp_moe/app/widgets/editorblock.js rename to nfp_moe/app/editorblock.js diff --git a/nfp_moe/app/widgets/fileinfo.js b/nfp_moe/app/fileinfo.js similarity index 95% rename from nfp_moe/app/widgets/fileinfo.js rename to nfp_moe/app/fileinfo.js index 854dc60..bac2d82 100644 --- a/nfp_moe/app/widgets/fileinfo.js +++ b/nfp_moe/app/fileinfo.js @@ -55,8 +55,8 @@ const Fileinfo = { view: function(vnode) { return m('fileinfo', { class: vnode.attrs.slim ? 'slim' : ''}, [ - m('div.filetitle', [ - m('span.prefix', this.getPrefix(vnode) + ':'), + m('p', [ + m('span', this.getPrefix(vnode) + ':'), m('a', { target: '_blank', rel: 'noopener', @@ -67,7 +67,7 @@ const Fileinfo = { href: vnode.attrs.file.magnet, }, 'Magnet') : null, - m('span', this.getTitle(vnode)), + this.getTitle(vnode), ]), vnode.attrs.file.meta.torrent && !vnode.attrs.slim diff --git a/nfp_moe/app/footer.js b/nfp_moe/app/footer.js new file mode 100644 index 0000000..c0ab5a6 --- /dev/null +++ b/nfp_moe/app/footer.js @@ -0,0 +1,39 @@ +const m = require('mithril') +const PageTree = require('./page_tree') +const Authentication = require('./authentication') + +const Footer = { + oninit: function(vnode) { + this.year = new Date().getFullYear() + }, + + view: function() { + return [ + m('span', 'Sitemap'), + m(m.route.Link, { class: 'root', href: '/' }, 'Home'), + PageTree.Tree.map(function(page) { + return [ + m(m.route.Link, { class: 'root', href: '/page/' + page.path }, page.name), + (page.children + ? m('ul', page.children.map(function(subpage) { + return m('li', m(m.route.Link, { class: 'child', href: '/page/' + subpage.path }, subpage.name)) + })) + : null), + ] + }), + + !Authentication.currentUser + ? m(m.route.Link, { class: 'root', href: '/login' }, 'Login') + : null, + m('div.meta', [ + '©' + + this.year + + ' NFP Encodes - nfp@nfp.moe - ', + m('a', { rel: 'noopener', href: 'https://www.iubenda.com/privacy-policy/31076050', target: '_blank' }, 'Privacy Policy'), + ' (Fuck EU)', + ]), + ] + }, +} + +module.exports = Footer diff --git a/nfp_moe/app/footer/footer.js b/nfp_moe/app/footer/footer.js deleted file mode 100644 index f9740fa..0000000 --- a/nfp_moe/app/footer/footer.js +++ /dev/null @@ -1,42 +0,0 @@ -const m = require('mithril') -const Page = require('../api/page.p') -const Authentication = require('../authentication') - -const Footer = { - oninit: function(vnode) { - this.year = new Date().getFullYear() - }, - - view: function() { - return [ - m('div.footer-filler'), - m('div.sitemap', [ - m('div', 'Sitemap'), - m(m.route.Link, { class: 'root', href: '/' }, 'Home'), - Page.Tree.map(function(page) { - return [ - m(m.route.Link, { class: 'root', href: '/page/' + page.path }, page.name), - (page.children - ? m('ul', page.children.map(function(subpage) { - return m('li', m(m.route.Link, { class: 'child', href: '/page/' + subpage.path }, subpage.name)) - })) - : null), - ] - }), - !Authentication.currentUser - ? m(m.route.Link, { class: 'root', href: '/login' }, 'Login') - : null, - m('div.meta', [ - '©' - + this.year - + ' NFP Encodes - nfp@nfp.moe - ', - m('a', { rel: 'noopener', href: 'https://www.iubenda.com/privacy-policy/31076050', target: '_blank' }, 'Privacy Policy'), - ' (Fuck EU)', - ]), - ]), - m('div.footer-logo'), - ] - }, -} - -module.exports = Footer diff --git a/nfp_moe/app/frontpage/frontpage.js b/nfp_moe/app/frontpage/frontpage.js deleted file mode 100644 index 8e9cb29..0000000 --- a/nfp_moe/app/frontpage/frontpage.js +++ /dev/null @@ -1,146 +0,0 @@ -const m = require('mithril') - -const Page = require('../api/page.p') -const Article = require('../api/article.p') -const Pagination = require('../api/pagination') -const Pages = require('../widgets/pages') -const Newsitem = require('../widgets/newsitem') - -const Frontpage = { - oninit: function(vnode) { - this.error = '' - this.loading = false - this.showLoading = null - this.data = { - page: null, - articles: [], - total_articles: 0, - featured: null, - } - this.currentPage = Number(m.route.param('page')) || 1 - - if (window.__nfpdata) { - this.lastpage = this.currentPage - window.__nfpdata = null - - if (this.articles.length === 0) { - m.route.set('/') - } - } else { - this.fetchPage(vnode) - } - }, - - onbeforeupdate: function(vnode) { - this.currentPage = Number(m.route.param('page')) || 1 - if (this.lastpage !== this.currentPage) { - this.fetchPage(vnode) - } - }, - - fetchPage: function(vnode) { - this.error = '' - this.lastpage = this.currentPage - - if (this.showLoading) { - clearTimeout(this.showLoading) - } - - this.showLoading = setTimeout(() => { - this.showLoading = null - this.loading = true - m.redraw() - }, 150) - - if (this.lastpage !== 1) { - document.title = 'Page ' + this.lastpage + ' - NFP Moe - Anime/Manga translation group' - } else { - document.title = 'NFP Moe - Anime/Manga translation group' - } - - return Page.getPage(null, this.lastpage) - .then((result) => { - this.data = result - }, (err) => { - this.error = err.message - }) - .then(() => { - clearTimeout(this.showLoading) - this.showLoading = null - this.loading = false - m.redraw() - }) - }, - - view: function(vnode) { - var deviceWidth = window.innerWidth - var bannerPath = this.data.featured && this.data.featured.banner_alt_prefix - - if (bannerPath) { - var pixelRatio = window.devicePixelRatio || 1 - if ((deviceWidth < 720 && pixelRatio <= 1) - || (deviceWidth < 360 && pixelRatio <= 2)) { - bannerPath += '_small' - } else if ((deviceWidth < 1300 && pixelRatio <= 1) - || (deviceWidth < 650 && pixelRatio <= 2)) { - bannerPath += '_medium' - } else { - bannerPath += '_large' - } - if (window.supportsavif) { - bannerPath += '.avif' - } else { - bannerPath += '.jpg' - } - } - - return [ - (bannerPath - ? m(m.route.Link, { - class: 'frontpage-banner', - href: '/article/' + this.data.featured.path, - style: { 'background-image': 'url("' + bannerPath + '")' }, - }, - this.data.featured.name - ) - : null), - m('frontpage', [ - m('aside.sidebar', [ - m('div.categories', [ - m('h4', 'Categories'), - m('div', - Page.Tree.map(function(page) { - return [ - m(m.route.Link, { class: 'root', href: '/page/' + page.path }, page.name), - (page.children.length - ? m('ul', page.children.map(function(subpage) { - return m('li', m(m.route.Link, { class: 'child', href: '/page/' + subpage.path }, subpage.name)) - })) - : null), - ] - }) - ), - ]), - m('div.asunaside', { - class: window.supportsavif ? 'avif' : 'jpeg' - }), - ]), - m('.frontpage-news', [ - (this.loading - ? m('div.loading-spinner') - : null), - this.data.articles.map(function(article) { - return m(Newsitem, { article: article }) - }), - m(Pages, { - base: '/', - total: this.data.total_articles, - page: this.currentPage, - }), - ]), - ]), - ] - }, -} - -module.exports = Frontpage diff --git a/nfp_moe/app/header.js b/nfp_moe/app/header.js new file mode 100644 index 0000000..fbe7865 --- /dev/null +++ b/nfp_moe/app/header.js @@ -0,0 +1,115 @@ +const m = require('mithril') +const Authentication = require('./authentication') +const PageTree = require('./page_tree') + +const DarkModeStorageName = 'nfp_sites_darkmode' + +const Menu = { + oninit: function(vnode) { + this.currentActive = 'home' + this.error = '' + this.loading = false + this.onbeforeupdate() + + if (!PageTree.Tree.length) { + this.refreshTree() + } + + + }, + + onbeforeupdate: function() { + this.darkIsOn = localStorage.getItem(DarkModeStorageName) + + let currentPath = m.route.get() + + if (currentPath === '/') this.currentActive = 'home' + else if (currentPath === '/login') this.currentActive = 'login' + else if (currentPath && currentPath.startsWith('/page')) this.currentActive = currentPath.slice(currentPath.lastIndexOf('/') + 1) + }, + + refreshTree: function(vnode) { + this.loading = true + this.error = '' + + PageTree.refreshTree() + .catch((err) => { + this.error = 'Error while getting menu tree: ' + err.message + '. Click here to try again.' + }) + .then(() => { + this.loading = false + m.redraw() + }) + }, + + logOut: function() { + Authentication.clearToken() + m.route.set('/') + }, + + toggleDarkMode: function() { + this.darkIsOn = !this.darkIsOn + if (this.darkIsOn) { + localStorage.setItem(DarkModeStorageName, true) + } else { + localStorage.removeItem(DarkModeStorageName) + } + document.body.className = (this.darkIsOn ? 'darkmode ' : 'daymode ') + + (window.supportsavif ? 'avifsupport' : 'jpegonly') + }, + + view: function() { + return [ + m('header', [ + m('div.inside', [ + m(m.route.Link, + { href: '/', class: 'logo' }, + m('h1', 'NFP Moe') + ), + m('aside', [ + Authentication.currentUser + ? [ + m('p', [ + 'Welcome ' + Authentication.currentUser.name + '. ', + m('button', { onclick: this.logOut }, '(Log out)'), + ]), + m('div.actions', [ + m(m.route.Link, { href: '/admin/articles/add' }, 'Create article'), + m(m.route.Link, { href: '/admin/articles' }, 'Articles'), + m(m.route.Link, { href: '/admin/pages' }, 'Pages'), + m(m.route.Link, { hidden: Authentication.currentUser.rank < 100, href: '/admin/staff' }, 'Staff'), + ]) + ] + : null, + m('button', + { onclick: this.toggleDarkMode.bind(this) }, + this.darkIsOn ? 'Day mode' : 'Night mode' + ), + ]) + ]), + ]), + m('nav', [ + m('div.inside', [ + m(m.route.Link, { + href: '/', + class: this.currentActive === 'home' ? 'active' : '', + }, 'Home'), + this.loading ? m('div.loading-spinner') : null, + PageTree.Tree.map((page) => { + let className = '' + if (this.currentActive === page.path) { + className += 'active ' + } + return m(m.route.Link, { + href: '/page/' + page.path, + class: className, + }, page.name) + }), + ]) + ]), + this.error ? m('div.error', { onclick: this.refreshTree.bind(this) }, this.error) : null, + ] + }, +} + +module.exports = Menu diff --git a/nfp_moe/app/index.js b/nfp_moe/app/index.js index 16a2029..bb39307 100644 --- a/nfp_moe/app/index.js +++ b/nfp_moe/app/index.js @@ -1,6 +1,11 @@ -require('./polyfill') - const m = require('mithril') +const Authentication = require('./authentication') +const AdminResolver = require('./admin_loader') +const Header = require('./header') +const Footer = require('./footer') +const Login = require('./site_login') +const SitePage = require('./site_page') +const SiteArticle = require('./site_article') window.m = m m.route.setOrig = m.route.set @@ -15,105 +20,13 @@ m.route.link = function(vnode){ window.scrollTo(0, 0) } -const Authentication = require('./authentication') - m.route.prefix = '' -window.adminRoutes = {} -let loadingAdmin = false -let loadedAdmin = false -let loaded = 0 -let elements = [] - -const onLoaded = function() { - loaded++ - if (loaded < 2) return - - Authentication.setAdmin(Authentication.currentUser && Authentication.currentUser.rank >= 10) - loadedAdmin = true - m.route.set(m.route.get()) -} - -const onError = function(a, b, c) { - elements.forEach(function(x) { x.remove() }) - loadedAdmin = loadingAdmin = false - loaded = 0 - m.route.set('/logout') -} - -const loadAdmin = function(user) { - if (loadingAdmin) { - if (loadedAdmin) { - Authentication.setAdmin(user && user.rank >= 10) - } - return - } - if (!user || user.rank < 10) return - - loadingAdmin = true - - let token = Authentication.getToken() - let element = document.createElement('link') - elements.push(element) - element.setAttribute('rel', 'stylesheet') - element.setAttribute('type', 'text/css') - element.setAttribute('href', '/assets/admin.css?token=' + token) - element.onload = onLoaded - element.onerror = onError - document.getElementsByTagName('head')[0].appendChild(element) - - element = document.createElement('script') - elements.push(element) - element.setAttribute('type', 'text/javascript') - element.setAttribute('src', '/assets/admin.js?token=' + token) - element.onload = onLoaded - element.onerror = onError - document.body.appendChild(element) - - element = document.createElement('script') - elements.push(element) - element.setAttribute('type', 'text/javascript') - element.setAttribute('src', '/assets/editor.js') - element.onload = onLoaded - element.onerror = onError - document.body.appendChild(element) -} - -Authentication.addEvent(loadAdmin) -if (Authentication.currentUser) { - loadAdmin(Authentication.currentUser) -} - -const Menu = require('./menu/menu') -const Footer = require('./footer/footer') -const Frontpage = require('./frontpage/frontpage') -const Login = require('./login/login') -const Logout = require('./login/logout') -const Page = require('./pages/page') -const Article = require('./article/article') - -const menuRoot = document.getElementById('nav') -const mainRoot = document.getElementById('main') -const footerRoot = document.getElementById('footer') - -const Loader = { - view: function() { return m('div.loading-spinner') }, -} -const AdminResolver = { - onmatch: function(args, requestedPath) { - if (window.adminRoutes[args.path]) { - return window.adminRoutes[args.path][args.id && 1 || 0] - } - return Loader - }, - render: function(vnode) { return vnode }, -} const allRoutes = { - '/': Frontpage, + '/': SitePage, // Frontpage '/login': Login, - '/logout': Logout, - '/page/:id': Page, - '/article/:id': Article, + '/page/:id': SitePage, + '/article/:id': SiteArticle, '/admin/:path': AdminResolver, '/admin/:path/:id': AdminResolver, } @@ -128,8 +41,8 @@ AVIF.onload = AVIF.onerror = function () { window.supportsavif = (AVIF.height === 2) document.body.className = document.body.className + ' ' + (window.supportsavif ? 'avifsupport' : 'jpegonly') - m.route(mainRoot, '/', allRoutes) - m.mount(menuRoot, Menu) - m.mount(footerRoot, Footer) + m.mount(document.getElementById('header'), Header) + m.route(document.getElementById('main'), '/', allRoutes) + m.mount(document.getElementById('footer'), Footer) } AVIF.src = 'data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAAB0AAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAIAAAACAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACVtZGF0EgAKCBgANogQEAwgMg8f8D///8WfhwB8+ErK42A='; diff --git a/nfp_moe/app/login/login.js b/nfp_moe/app/login/login.js deleted file mode 100644 index b4c19da..0000000 --- a/nfp_moe/app/login/login.js +++ /dev/null @@ -1,101 +0,0 @@ -const m = require('mithril') -const Authentication = require('../authentication') -const Api = require('../api/common') - -const Login = { - loading: false, - redirect: '', - error: '', - - oninit: function(vnode) { - Login.redirect = vnode.attrs.redirect || '' - if (Authentication.currentUser) return m.route.set('/') - Login.error = '' - - this.username = '' - this.password = '' - }, - - oncreate: function() { - if (Authentication.currentUser) return - }, - - loginuser: function(vnode, e) { - e.preventDefault() - if (!this.username) { - Login.error = 'Email is missing' - } else if (!this.password) { - Login.error = 'Password is missing' - } else { - Login.error = '' - } - if (Login.error) return - - Login.loading = true - - Api.sendRequest({ - method: 'POST', - url: '/api/authentication/login', - body: { - email: this.username, - password: this.password, - }, - }) - .then(function(result) { - if (!result.token) { - return Promise.reject(new Error('Server authentication down.')) - } - Authentication.updateToken(result.token) - m.route.set(Login.redirect || '/') - }) - .catch(function(error) { - Login.error = 'Error while logging into NFP! ' + error.message - vnode.state.password = '' - }) - .then(function () { - Login.loading = false - m.redraw() - }) - }, - - view: function(vnode) { - return [ - m('div.login-wrapper', [ - m('div.login-icon'), - m('article.login', [ - m('header', [ - m('h1', 'NFP.moe login'), - ]), - m('div.content', [ - m('h5', 'Please login to access restricted area'), - Login.error ? m('div.error', Login.error) : null, - Login.loading ? m('div.loading-spinner') : null, - m('form', { - hidden: Login.loading, - onsubmit: this.loginuser.bind(this, vnode), - }, [ - m('label', 'Email'), - m('input', { - type: 'text', - value: this.username, - oninput: function(e) { vnode.state.username = e.currentTarget.value }, - }), - m('label', 'Password'), - m('input', { - type: 'password', - value: this.password, - oninput: function(e) { vnode.state.password = e.currentTarget.value }, - }), - m('input', { - type: 'submit', - value: 'Login', - }), - ]), - ]), - ]), - ]), - ] - }, -} - -module.exports = Login diff --git a/nfp_moe/app/login/logout.js b/nfp_moe/app/login/logout.js deleted file mode 100644 index 8ca407f..0000000 --- a/nfp_moe/app/login/logout.js +++ /dev/null @@ -1,15 +0,0 @@ -const m = require('mithril') -const Authentication = require('../authentication') - -const Logout = { - oninit: function() { - Authentication.clearToken() - m.route.set('/') - }, - - view: function() { - return m('div.loading-spinner') - }, -} - -module.exports = Logout diff --git a/nfp_moe/app/media.js b/nfp_moe/app/media.js new file mode 100644 index 0000000..d22ead4 --- /dev/null +++ b/nfp_moe/app/media.js @@ -0,0 +1,50 @@ +export function generateSource(item, cover) { + if (!item) return + + if (item.media_alt_prefix) { + item.pictureFallback = item.media_alt_prefix + '_small.jpg' + item.pictureJpeg = item.media_alt_prefix + '_small.jpg' + ' 720w, ' + + item.media_alt_prefix + '_medium.jpg' + ' 1300w, ' + + item.media_alt_prefix + '_large.jpg 1920w' + item.pictureAvif = item.media_alt_prefix + '_small.avif' + ' 720w, ' + + item.media_alt_prefix + '_medium.avif' + ' 1300w, ' + + item.media_alt_prefix + '_large.avif 1920w' + + item.pictureCover = cover + } else { + item.pictureFallback = null + item.pictureJpeg = null + item.pictureAvif = null + item.pictureCover = null + } +} + +export function getBannerImage(item, prefix) { + if (!item || !item.banner_alt_prefix) return null + + let out = { + path: prefix + item.path, + name: item.name, + original: item.banner_path, + banner: item.banner_alt_prefix + } + + var deviceWidth = window.innerWidth + var pixelRatio = window.devicePixelRatio || 1 + if ((deviceWidth < 720 && pixelRatio <= 1) + || (deviceWidth < 360 && pixelRatio <= 2)) { + out.banner += '_small' + } else if ((deviceWidth < 1300 && pixelRatio <= 1) + || (deviceWidth < 650 && pixelRatio <= 2)) { + out.banner += '_medium' + } else { + out.banner += '_large' + } + if (window.supportsavif) { + out.banner += '.avif' + } else { + out.banner += '.jpg' + } + + return out +} \ No newline at end of file diff --git a/nfp_moe/app/menu/menu.js b/nfp_moe/app/menu/menu.js deleted file mode 100644 index c14adfe..0000000 --- a/nfp_moe/app/menu/menu.js +++ /dev/null @@ -1,92 +0,0 @@ -const m = require('mithril') -const Authentication = require('../authentication') -const Darkmode = require('../darkmode') -const Page = require('../api/page.p') - -const Menu = { - currentActive: 'home', - error: '', - loading: false, - - onbeforeupdate: function() { - let currentPath = m.route.get() - if (currentPath === '/') Menu.currentActive = 'home' - else if (currentPath === '/login') Menu.currentActive = 'login' - else Menu.currentActive = currentPath - }, - - oninit: function(vnode) { - Menu.onbeforeupdate() - - if (Page.Tree.length) return - - Menu.loading = true - - Page.refreshTree() - .catch(function(err) { - Menu.error = err.message - }) - .then(function() { - Menu.loading = false - m.redraw() - }) - }, - - view: function() { - console.log('menu view', Boolean(Authentication.currentUser)) - return [ - m('div.top', [ - m(m.route.Link, - { href: '/', class: 'logo' }, - m('h2', 'NFP Moe') - ), - m('aside', Authentication.currentUser ? [ - m('p', [ - 'Welcome ' + Authentication.currentUser.name, - m(m.route.Link, { href: '/logout' }, 'Logout'), - (Darkmode.darkIsOn - ? m('button.dark', { onclick: Darkmode.setDarkMode.bind(Darkmode, false) }, 'Day mode') - : m('button.dark', { onclick: Darkmode.setDarkMode.bind(Darkmode, true) }, 'Night mode') - ), - ]), - (Authentication.isAdmin - ? m('div.adminlinks', [ - m(m.route.Link, { href: '/admin/articles/add' }, 'Create article'), - m(m.route.Link, { href: '/admin/articles' }, 'Articles'), - m(m.route.Link, { href: '/admin/pages' }, 'Pages'), - m(m.route.Link, { hidden: Authentication.currentUser.rank < 100, href: '/admin/staff' }, 'Staff'), - ]) - : (Authentication.currentUser.rank > 10 ? m('div.loading-spinner') : null) - ), - ] : (Darkmode.darkIsOn - ? m('button.dark', { onclick: Darkmode.setDarkMode.bind(Darkmode, false) }, 'Day mode') - : m('button.dark', { onclick: Darkmode.setDarkMode.bind(Darkmode, true) }, 'Night mode') - ) - ), - ]), - m('nav', [ - m(m.route.Link, { - href: '/', - class: Menu.currentActive === 'home' ? 'active' : '', - }, 'Home'), - Menu.loading ? m('div.loading-spinner') : Page.Tree.map(function(page) { - if (page.children) { - return m('div.hassubmenu', [ - m(m.route.Link, { - href: '/page/' + page.path, - class: Menu.currentActive === ('/page/' + page.path) ? 'active' : '', - }, page.name), - ]) - } - return m(m.route.Link, { - href: '/page/' + page.path, - class: Menu.currentActive === ('/page/' + page.path) ? 'active' : '', - }, page.name) - }), - ]), - Menu.error ? m('div.menuerror', Menu.error) : null, - ] - }, -} - -module.exports = Menu diff --git a/nfp_moe/app/api/page.p.js b/nfp_moe/app/page_tree.js similarity index 78% rename from nfp_moe/app/api/page.p.js rename to nfp_moe/app/page_tree.js index 990c9ca..38aea57 100644 --- a/nfp_moe/app/api/page.p.js +++ b/nfp_moe/app/page_tree.js @@ -1,4 +1,4 @@ -const common = require('./common') +const api = require('./api') const Tree = window.__nfptree && window.__nfptree.tree || [] const TreeMap = new Map() @@ -33,15 +33,8 @@ exports.getFlatTree = function() { return arr } -exports.getPage = function(path, page) { - return common.sendRequest({ - method: 'GET', - url: '/api/' + (path ? 'pages/' + path : 'frontpage') + '?page=' + (page || 1), - }) -} - exports.refreshTree = function() { - return common.sendRequest({ + return api.sendRequest({ method: 'GET', url: '/api/pagetree', }) @@ -51,4 +44,4 @@ exports.refreshTree = function() { TreeMap.clear() parseLeaf(Tree) }) -} +} \ No newline at end of file diff --git a/nfp_moe/app/pages/page.js b/nfp_moe/app/pages/page.js deleted file mode 100644 index 54d2af8..0000000 --- a/nfp_moe/app/pages/page.js +++ /dev/null @@ -1,233 +0,0 @@ -const m = require('mithril') -const ApiPage = require('../api/page.p') -const Article = require('../api/article.p') -const pagination = require('../api/pagination') -const Authentication = require('../authentication') -const Newsentry = require('../widgets/newsentry') -const Pages = require('../widgets/pages') - -const Page = { - oninit: function(vnode) { - this.error = '' - this.loading = false - this.showLoading = null - this.data = { - page: null, - articles: [], - total_articles: 0, - featured: null, - } - this.children = [] - this.currentPage = Number(m.route.param('page')) || 1 - - if (window.__nfpdata) { - this.path = m.route.param('id') - this.data = window.__nfpdata - - window.__nfpdata = null - window.__nfpsubdata = null - } else { - this.fetchPage(vnode) - } - }, - - onbeforeupdate: function(vnode) { - this.currentPage = Number(m.route.param('page')) || 1 - - if (this.path !== m.route.param('id') || this.currentPage !== this.lastpage) { - this.fetchPage(vnode) - } - }, - - fetchPage: function(vnode) { - this.error = '' - this.lastpage = this.currentPage - this.path = m.route.param('id') - - if (this.showLoading) { - clearTimeout(this.showLoading) - } - - if (this.data.page) { - this.showLoading = setTimeout(() => { - this.showLoading = null - this.loading = true - m.redraw() - }, 150) - } else { - this.loading = true - } - - this.children = ApiPage.TreeMap.get(this.path) - this.children = this.children && this.children.children || [] - - ApiPage.getPage(this.path, this.lastpage) - .then((result) => { - this.data = result - - if (!this.data.page) { - this.error = 'Page not found' - return - } - - if (this.data.page.media_alt_prefix) { - this.data.page.pictureFallback = this.data.page.media_alt_prefix + '_small.jpg' - this.data.page.pictureJpeg = this.data.page.media_alt_prefix + '_small.jpg' + ' 720w, ' - + this.data.page.media_alt_prefix + '_medium.jpg' + ' 1300w, ' - + this.data.page.media_alt_prefix + '_large.jpg 1920w' - this.data.page.pictureAvif = this.data.page.media_alt_prefix + '_small.avif' + ' 720w, ' - + this.data.page.media_alt_prefix + '_medium.avif' + ' 1300w, ' - + this.data.page.media_alt_prefix + '_large.avif 1920w' - - this.data.page.pictureCover = '(max-width: 840px) calc(100vw - 82px), ' - + '758px' - } else { - this.data.page.pictureFallback = null - this.data.page.pictureJpeg = null - this.data.page.pictureAvif = null - this.data.page.pictureCover = null - } - - if (this.lastpage !== 1) { - document.title = 'Page ' + this.lastpage + ' - ' + this.data.page.name + ' - NFP Moe' - } else { - document.title = this.data.page.name + ' - NFP Moe' - } - }, (err) => { - this.error = err.message - }) - .then(() => { - clearTimeout(this.showLoading) - this.showLoading = null - this.loading = false - m.redraw() - }) - }, - - view: function(vnode) { - let page = this.data.page - let bannerPath = '' - - return ([ - this.loading - ? m('article.page', m('div.loading-spinner')) - : null, - !this.loading && this.error - ? m('div.error-wrapper', m('div.error', { - onclick: function() { - vnode.state.error = '' - vnode.state.fetchPage(vnode) - }, - }, 'Page error: ' + this.error)) - : null, - !this.loading && !this.error - ? m('article.page', [ - bannerPath - ? m('.div.page-banner', { style: { 'background-image': 'url("' + bannerPath + '")' } } ) - : null, - m('div.goback', ['« ', m(m.route.Link, { - href: page.parent_path - ? '/page/' + page.parent_path - : '/' - }, page.parent_name || 'Home')]), - m('header', m('h1', page.name)), - m('.container', { - class: this.children.length ? 'multi' : '', - }, [ - this.children.length - ? m('aside.sidebar', [ - m('h4', 'View ' + page.name + ':'), - this.children.map(function(page) { - return m(m.route.Link, { href: '/page/' + page.path }, page.name) - }), - ]) - : null, - page.content - ? m('.fr-view', [ - page.pictureFallback - ? m('a.cover', { - rel: 'noopener', - href: page.media_path, - }, [ - m('picture', [ - m('source', { - srcset: page.pictureAvif, - sizes: page.pictureCover, - type: 'image/avif', - }), - m('img', { - srcset: page.pictureJpeg, - sizes: page.pictureCover, - alt: 'Image for news item ' + page.name, - src: page.pictureFallback, - }), - ]), - ]) - : null, - page.content.blocks.map(block => { - return m(EditorBlock, { block: block }) - }), - this.data.articles.length && page.content - ? m('aside.news', [ - m('h4', 'Latest posts under ' + page.name + ':'), - this.data.articles.map(function(article) { - return m(Newsentry, { article: article }) - }), - m(Pages, { - base: '/page/' + page.path, - total: this.data.total_articles, - page: this.currentPage, - }), - ]) - : null, - ]) - : this.data.articles.length - ? m('aside.news.single', - [ - page.pictureFallback - ? m('a', { - rel: 'noopener', - href: page.media_path, - }, [ - m('picture.page-cover', [ - m('source', { - srcset: page.pictureAvif, - sizes: page.pictureCover, - type: 'image/avif', - }), - m('img', { - srcset: page.pictureJpeg, - sizes: page.pictureCover, - alt: 'Cover image for ' + page.name, - src: page.pictureFallback, - }), - ]), - ]) - : null, - m('h4', 'Latest posts under ' + page.name + ':'), - this.data.articles.map(function(article) { - return m(Newsentry, { article: article }) - }), - m(Pages, { - base: '/page/' + page.path, - total: this.data.total_articles, - page: this.currentPage, - }), - ]) - : page.media - ? m('img.page-cover.single', { src: page.media.medium_url, alt: 'Cover image for ' + page.name } ) - : null, - ]), - Authentication.currentUser - ? m('div.admin-actions', [ - m('span', 'Admin controls:'), - m(m.route.Link, { href: '/admin/pages/' + page.path }, 'Edit page'), - ]) - : null, - ]) - : null, - ]) - }, -} - -module.exports = Page diff --git a/nfp_moe/app/paginator.js b/nfp_moe/app/paginator.js new file mode 100644 index 0000000..a9f500b --- /dev/null +++ b/nfp_moe/app/paginator.js @@ -0,0 +1,39 @@ +const Paginator = { + view: function(vnode) { + let total = vnode.attrs.total + let currentPage = vnode.attrs.page + let perPage = vnode.attrs.perPage || 10 + let maxPage = total / perPage + 1 + + if (total <= perPage) return null + + return m('paginator', [ + currentPage > 1 + ? [ + m(m.route.Link, { + href: vnode.attrs.base, + }, 'First'), + m(m.route.Link, { + href: vnode.attrs.base + (currentPage > 2 + ? '?page=' + (currentPage - 1) + : '' + ), + }, 'Previous'), + ] + : m('div'), + m('div', 'Page ' + currentPage), + currentPage < maxPage + ? [ + m(m.route.Link, { + href: vnode.attrs.base + '?page=' + (currentPage + 1), + }, 'Next'), + m(m.route.Link, { + href: vnode.attrs.base + '?page=' + maxPage, + }, 'Last') + ] + : m('div'), + ]) + }, +} + +module.exports = Paginator diff --git a/nfp_moe/app/polyfill.js b/nfp_moe/app/polyfill.js deleted file mode 100644 index f9db4ec..0000000 --- a/nfp_moe/app/polyfill.js +++ /dev/null @@ -1,8 +0,0 @@ -if (!String.prototype.endsWith) { - String.prototype.endsWith = function(search, this_len) { - if (this_len === undefined || this_len > this.length) { - this_len = this.length; - } - return this.substring(this_len - search.length, this_len) === search; - }; -} diff --git a/nfp_moe/app/site_article.js b/nfp_moe/app/site_article.js new file mode 100644 index 0000000..cc34541 --- /dev/null +++ b/nfp_moe/app/site_article.js @@ -0,0 +1,155 @@ +const m = require('mithril') +const Article = require('./article') +const api = require('./api') +const media = require('./media') + +window.LoadComments = false +window.HyvorLoaded = false +window.HYVOR_TALK_WEBSITE = 7544 + +const SiteArticle = { + oninit: function(vnode) { + this.error = '' + this.loading = false + this.showLoading = null + this.data = { + article: null, + files: [], + } + this.showcomments = false + + if (window.__nfpdata) { + this.path = m.route.param('id') + this.data.article = window.__nfpdata + window.__nfpdata = null + } else { + this.fetchArticle(vnode) + } + }, + + onbeforeupdate: function(vnode) { + if (this.path !== m.route.param('id')) { + this.fetchArticle(vnode) + } + }, + + fetchArticle: function(vnode) { + this.error = '' + this.path = m.route.param('id') + this.showcomments = false + + if (this.showLoading) { + clearTimeout(this.showLoading) + } + + if (this.data.article) { + this.showLoading = setTimeout(() => { + this.showLoading = null + this.loading = true + m.redraw() + }, 150) + } else { + this.loading = true + } + + api.sendRequest({ + method: 'GET', + url: '/api/articles/' + this.path, + }) + .then((result) => { + this.data = result + + if (this.data.article.media_alt_prefix) { + this.data.article.pictureFallback = this.data.article.media_alt_prefix + '_small.jpg' + this.data.article.pictureJpeg = this.data.article.media_alt_prefix + '_small.jpg' + ' 720w, ' + + this.data.article.media_alt_prefix + '_medium.jpg' + ' 1300w, ' + + this.data.article.media_alt_prefix + '_large.jpg 1920w' + this.data.article.pictureAvif = this.data.article.media_alt_prefix + '_small.avif' + ' 720w, ' + + this.data.article.media_alt_prefix + '_medium.avif' + ' 1300w, ' + + this.data.article.media_alt_prefix + '_large.avif 1920w' + + this.data.article.pictureCover = '(max-width: 840px) calc(100vw - 82px), ' + + '758px' + } else { + this.data.article.pictureFallback = null + this.data.article.pictureJpeg = null + this.data.article.pictureAvif = null + this.data.article.pictureCover = null + } + + if (!this.data.article) { + this.error = 'Article not found' + } + }, (err) => { + this.error = err.message + }) + .then(() => { + clearTimeout(this.showLoading) + this.showLoading = null + this.loading = false + m.redraw() + }) + }, + + view: function(vnode) { + let article = this.data.article + let banner = media.getBannerImage(article, '/article/') + + return [ + this.loading + ? m('div.loading-spinner') + : null, + !this.loading && this.error + ? m('div.wrapper', m('div.error', { + onclick: () => { + this.error = '' + this.fetchPage(vnode) + }, + }, 'Page error: ' + this.error + '. Click here to try again')) + : null, + /*(banner + ? m('a.page-banner', { + href: banner.original, + target: '_blank', + style: { 'background-image': 'url("' + banner.banner + '")' }, + }, + ) + : null),*/ + (article + ? m('.inside.vertical', [ + m('div.page-goback', ['« ', m(m.route.Link, { + href: article.page_path + ? '/page/' + article.page_path + : '/' + }, article.page_name || 'Home')] + ), + article ? m(Article, { full: true, files: this.data.files, article: article }) : null, + window.LoadComments + ? m('div#hyvor-talk-view', { oncreate: function() { + window.HYVOR_TALK_CONFIG = { + url: false, + id: article.path, + loadMode: scroll, + } + if (!window.HyvorLoaded) { + window.HyvorLoaded = true + var s = document.createElement('script') + s.src = 'https://talk.hyvor.com/web-api/embed.js'; + s.type = 'text/javascript' + // s.setAttribute('crossorigin', '') + // s.setAttribute('data-timestamp', +new Date()); + document.body.appendChild(s); + } else { + hyvor_talk.reload() + } + }}, m('div.loading-spinner')) + : m('button', { + onclick: function() { window.LoadComments = true }, + }, 'Open comment discussion'), + ]) + : null), + ] + }, +} + +module.exports = SiteArticle diff --git a/nfp_moe/app/site_login.js b/nfp_moe/app/site_login.js new file mode 100644 index 0000000..6bc5e69 --- /dev/null +++ b/nfp_moe/app/site_login.js @@ -0,0 +1,90 @@ +const m = require('mithril') +const Authentication = require('./authentication') +const api = require('./api') + +const Login = { + oninit: function(vnode) { + this.redirect = vnode.attrs.redirect || '' + if (Authentication.currentUser) return m.route.set('/') + + this.error = '' + this.loading = false + this.username = '' + this.password = '' + }, + + oncreate: function() { + if (Authentication.currentUser) return + }, + + loginuser: function(vnode, e) { + e.preventDefault() + if (!this.username) { + this.error = 'Email is missing' + } else if (!this.password) { + this.error = 'Password is missing' + } else { + this.error = '' + } + if (this.error) return + + this.loading = true + + api.sendRequest({ + method: 'POST', + url: '/api/authentication/login', + body: { + email: this.username, + password: this.password, + }, + }) + .then((result) => { + if (!result.token) { + return Promise.reject(new Error('Server authentication down.')) + } + Authentication.updateToken(result.token) + m.route.set(this.redirect || '/') + }) + .catch((error) => { + this.error = 'Error while logging into NFP! ' + error.message + vnode.state.password = '' + }) + .then(() => { + this.loading = false + m.redraw() + }) + }, + + view: function(vnode) { + return [ + m('div.wrapper', [ + this.loading ? m('div.loading-spinner') : null, + m('form.inside.login', { + hidden: this.loading, + onsubmit: this.loginuser.bind(this, vnode), + }, [ + m('div.title', 'NFP.moe login'), + this.error ? m('div.error', this.error) : null, + m('label', 'Email'), + m('input', { + type: 'text', + value: this.username, + oninput: (e) => { this.username = e.currentTarget.value }, + }), + m('label', 'Password'), + m('input', { + type: 'password', + value: this.password, + oninput: (e) => { this.password = e.currentTarget.value }, + }), + m('input', { + type: 'submit', + value: 'Log in', + }), + ]), + ]), + ] + }, +} + +module.exports = Login diff --git a/nfp_moe/app/site_page.js b/nfp_moe/app/site_page.js new file mode 100644 index 0000000..069b532 --- /dev/null +++ b/nfp_moe/app/site_page.js @@ -0,0 +1,224 @@ +const m = require('mithril') +const api = require('./api') +const PageTree = require('./page_tree') +const Paginator = require('./paginator') +// const Authentication = require('./authentication') +const Article = require('./article') +const Articleslim = require('./article_slim') +const media = require('./media') + +const ArticlesPerPage = 10 + +const SitePage = { + oninit: function(vnode) { + this.error = '' + this.loading = false + this.showLoading = null + this.data = { + page: null, + articles: [], + total_articles: 0, + featured: null, + } + this.children = [] + this.currentPage = Number(m.route.param('page')) || 1 + + if (window.__nfpdata) { + this.path = m.route.param('id') + this.data = window.__nfpdata + + window.__nfpdata = null + window.__nfpsubdata = null + } else { + this.fetchPage(vnode) + } + }, + + onbeforeupdate: function(vnode) { + this.currentPage = Number(m.route.param('page')) || 1 + + if (this.path !== m.route.param('id') || this.currentPage !== this.lastpage) { + this.fetchPage(vnode) + } + }, + + fetchPage: function(vnode) { + this.error = '' + this.lastpage = this.currentPage + this.path = m.route.param('id') + + if (this.showLoading) { + clearTimeout(this.showLoading) + } + + if (this.data.page) { + this.showLoading = setTimeout(() => { + this.showLoading = null + this.loading = true + m.redraw() + }, 300) + } else { + this.loading = true + } + + if (this.path) { + this.children = PageTree.TreeMap.get(this.path) + this.children = this.children && this.children.children || [] + } else { + this.children = PageTree.Tree + } + + api.sendRequest({ + method: 'GET', + url: '/api/' + (this.path ? 'pages/' + this.path : 'frontpage') + '?page=' + (this.lastpage || 1), + }) + .then((result) => { + this.data = result + + if (!this.data.page && this.path) { + this.error = 'Page not found' + return + } + + let title = 'Page not found - NFP Moe - Anime/Manga translation group' + if (this.data.page) { + title = this.data.page.name + ' - NFP Moe' + } else if (!this.path) { + title = 'NFP Moe - Anime/Manga translation group' + } + + media.generateSource(this.data.page, + '(max-width: 840px) calc(100vw - 82px), ' + + '758px') + + if (this.lastpage !== 1) { + document.title = 'Page ' + this.lastpage + ' - ' + title + } else { + document.title = title + } + }, (err) => { + this.error = err.message + }) + .then(() => { + clearTimeout(this.showLoading) + this.showLoading = null + this.loading = false + m.redraw() + }) + }, + + view: function(vnode) { + let page = this.data.page + let featuredBanner = media.getBannerImage(this.data.featured, '/article/') + let pageBanner = media.getBannerImage(this.data.page, '/page/') + let paginatorMaxPath = Math.floor(this.data.total_articles / ArticlesPerPage) + 1 + let paginatorPrefix = page ? '/page/' + page.path : '/' + + return ([ + this.loading + ? m('div.loading-spinner') + : null, + !this.loading && this.error + ? m('div.wrapper', m('div.error', { + onclick: () => { + this.error = '' + this.fetchPage(vnode) + }, + }, 'Page error: ' + this.error + '. Click here to try again')) + : null, + (featuredBanner + ? m(m.route.Link, { + class: 'page-banner', + href: featuredBanner.path, + style: { 'background-image': 'url("' + featuredBanner.banner + '")' }, + }, + m('div.inside', m('div.page-banner-title', featuredBanner.name)) + ) + : null), + (!featuredBanner && pageBanner + ? m('a.page-banner', { + href: pageBanner.original, + target: '_blank', + style: { 'background-image': 'url("' + pageBanner.banner + '")' }, + }, + ) + : null), + (page + ? m('.inside.vertical', [ + m('div.page-goback', ['« ', m(m.route.Link, { + href: page.parent_path + ? '/page/' + page.parent_path + : '/' + }, page.parent_name || 'Home')] + ), + m('h2', page.name) + ]) + : null), + m('.inside', [ + this.children.length + ? m('aside', [ + m('h5', page ? 'View ' + page.name + ':' : 'Categories'), + this.children.map((page) => { + return [ + m(m.route.Link, { class: 'root', href: '/page/' + page.path }, page.name), + (page.children && page.children.length + ? m('ul', page.children.map(function(subpage) { + return m('li', m(m.route.Link, { class: 'child', href: '/page/' + subpage.path }, subpage.name)) + })) + : null), + ] + }) + ]) + : null, + m('div.container', [ + (page && page.pictureFallback + ? m('a.cover', { + rel: 'noopener', + href: page.media_path, + }, [ + m('picture', [ + m('source', { + srcset: page.pictureAvif, + sizes: page.pictureCover, + type: 'image/avif', + }), + m('img', { + srcset: page.pictureJpeg, + sizes: page.pictureCover, + alt: 'Image for news item ' + page.name, + src: page.pictureFallback, + }), + ]), + ]) + : null), + (page && page.content + ? page.content.blocks.map(block => { + return m(EditorBlock, { block: block }) + }) + : null), + (page && this.data.articles.length + ? [ + m('h5', 'Latest posts under ' + page.name + ':'), + this.data.articles.map(function(article) { + return m(Articleslim, { article: article }) + }), + ] + : null), + (!page && this.data.articles.length + ? this.data.articles.map(function(article) { + return m(Article, { article: article }) + }) + : null), + m(Paginator, { + base: page ? '/page/' + page.path : '/', + page: this.currentPage, + perPage: ArticlesPerPage, + total: this.data.total_articles, + }), + ]), + ]), + ]) + }, +} + +module.exports = SitePage diff --git a/nfp_moe/app/widgets/pages.js b/nfp_moe/app/widgets/pages.js deleted file mode 100644 index 50807c4..0000000 --- a/nfp_moe/app/widgets/pages.js +++ /dev/null @@ -1,44 +0,0 @@ -const Pages = { - oninit: function(vnode) { - this.onbeforeupdate(vnode) - }, - - onbeforeupdate: function(vnode) { - this.total = vnode.attrs.total - this.currentPage = vnode.attrs.page - this.perPage = vnode.attrs.perPage || 10 - this.maxPage = this.total / this.perPage + 1 - }, - - view: function(vnode) { - if (this.total <= this.perPage) return null - return m('pages', [ - this.currentPage > 1 - ? [ - m(m.route.Link, { - href: vnode.attrs.base, - }, 'First'), - m(m.route.Link, { - href: vnode.attrs.base + (this.currentPage > 2 - ? '?page=' + (this.currentPage - 1) - : '' - ), - }, 'Previous'), - ] - : m('div'), - m('div', 'Page ' + this.currentPage), - this.currentPage < this.maxPage - ? [ - m(m.route.Link, { - href: vnode.attrs.base + '?page=' + (this.currentPage + 1), - }, 'Next'), - m(m.route.Link, { - href: vnode.attrs.base + '?page=' + this.maxPage, - }, 'Last') - ] - : m('div'), - ]) - }, -} - -module.exports = Pages diff --git a/nfp_moe/package.json b/nfp_moe/package.json index 7ac7d4a..aa7c3fa 100644 --- a/nfp_moe/package.json +++ b/nfp_moe/package.json @@ -9,8 +9,8 @@ "scripts": { "start": "node --experimental-modules index.mjs", "test": "echo \"Error: no test specified\" && exit 1", - "build:prod": "asbundle app/index.js public/assets/app.js && asbundle app/admin.js public/assets/admin.js", - "build": "asbundle app/index.js public/assets/app.js && asbundle app/admin.js public/assets/admin.js", + "build:prod": "asbundle app/index.js public/assets/app.js && asbundle app/admin/admin.js public/assets/admin.js", + "build": "asbundle app/index.js public/assets/app.js && asbundle app/admin/admin.js public/assets/admin.js", "dev:build": "npm-watch build", "dev:server": "node dev.mjs | bunyan", "dev": "npm-watch dev:server", diff --git a/nfp_moe/public/index.html b/nfp_moe/public/index.html index aef6b53..d2dd760 100644 --- a/nfp_moe/public/index.html +++ b/nfp_moe/public/index.html @@ -23,16 +23,13 @@ -
- -
-
-
+ +
+