From f12b440c192fb993b6aee14695545c44201b10ce Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Wed, 27 Jul 2022 08:41:18 +0000 Subject: [PATCH] Finished base articles and pages --- api/article/routes.mjs | 115 ++--------- api/article/util.mjs | 41 +++- api/authentication/routes.mjs | 2 +- api/db.mjs | 2 +- api/media/util.mjs | 25 +++ api/page/model.mjs | 48 ----- api/page/routes.mjs | 125 +++++++++++- api/page/security.mjs | 0 api/page/util.mjs | 23 +++ api/serve.mjs | 5 +- api/server.mjs | 21 +- app/_common.scss | 69 ------- app/admin/articles.js | 22 ++- app/admin/editarticle.js | 61 +++--- app/admin/editpage.js | 350 ++++++++++++++++++---------------- app/admin/pages.js | 66 +++---- app/api/page.js | 15 ++ app/api/page.p.js | 28 +++ app/app.scss | 70 +++++++ app/article/article.js | 77 +++++--- app/authentication.js | 4 +- app/footer/footer.js | 2 +- app/frontpage/frontpage.js | 2 +- app/index.js | 7 - app/login/login.js | 1 - app/menu/menu.js | 9 +- app/pages/page.js | 4 +- app/widgets/editorblock.js | 61 ++++++ app/widgets/newsentry.js | 104 +++++++--- app/widgets/newsitem.js | 71 ++++--- 30 files changed, 850 insertions(+), 580 deletions(-) create mode 100644 api/media/util.mjs delete mode 100644 api/page/model.mjs delete mode 100644 api/page/security.mjs create mode 100644 api/page/util.mjs create mode 100644 app/widgets/editorblock.js diff --git a/api/article/routes.mjs b/api/article/routes.mjs index e8a223f..8ff966f 100644 --- a/api/article/routes.mjs +++ b/api/article/routes.mjs @@ -1,6 +1,7 @@ import { parseFiles } from '../file/util.mjs' import { parseArticles, parseArticle } from './util.mjs' import { upload } from '../media/upload.mjs' +import { mediaToDatabase } from '../media/util.mjs' export default class ArticleRoutes { constructor(opts = {}) { @@ -11,7 +12,7 @@ export default class ArticleRoutes { /** GET: /api/articles/[path] */ async getArticle(ctx) { - let res = await ctx.db.safeCallProc('common.article_get_single', [ctx.params.path]) + let res = await ctx.db.safeCallProc('article_get_single', [ctx.params.path]) let out = { article: parseArticle(res.results[0][0]), @@ -23,7 +24,7 @@ export default class ArticleRoutes { /** GET: /api/auth/articles */ async auth_getAllArticles(ctx) { - let res = await ctx.db.safeCallProc('common.article_auth_get_all', [ + 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) @@ -40,7 +41,7 @@ export default class ArticleRoutes { async private_getUpdateArticle(ctx, body = null, banner = null, media = null) { let params = [ ctx.state.auth_token, - ctx.params.path + ctx.params.id === '0' ? null : ctx.params.id ] if (body) { params = params.concat([ @@ -53,100 +54,28 @@ export default class ArticleRoutes { body.is_featured === 'true' ? 1 : 0, 0, ]) - if (banner) { - params = params.concat([ - banner.filename, - banner.type, - banner.path, - banner.size, - banner.preview.base64, - banner.sizes.small.avif.path.replace(/_small\.avif$/, ''), - JSON.stringify(banner.sizes), - 0, - ]) - } else { - params = params.concat([ - null, - null, - null, - null, - null, - null, - null, - null, - ]) - } - if (media) { - params = params.concat([ - media.filename, - media.type, - media.path, - media.size, - media.preview.base64, - media.sizes.small.avif.path.replace(/_small\.avif$/, ''), - JSON.stringify(media.sizes), - 0, - ]) - } else { - params = params.concat([ - null, - null, - null, - null, - null, - null, - null, - null, - ]) - } + params = params.concat(mediaToDatabase(banner, body.remove_banner === 'true')) + params = params.concat(mediaToDatabase(media, body.remove_media === 'true')) } console.log(params) - let res = await ctx.db.safeCallProc('common.article_auth_get_update_create', params) + let res = await ctx.db.safeCallProc('article_auth_get_update_create', params) let out = { - article: parseArticle(res.results[0][0]), + article: parseArticle(res.results[0][0]) || { publish_at: new Date() }, files: parseFiles(res.results[1]), staff: res.results[2], } - - if (out.article) { - if (out.article.content[0] === '{') { - try { - out.article.content = JSON.parse(out.article.content) - } catch (err) { - out.article.content = { - time: new Date().getTime(), - blocks: [ - {id: '1', type: 'paragraph', data: { text: 'Error parsing article content: ' + err.message }}, - ], - version: '2.25.0' - } - } - } else if (out.article.content) { - out.article.content = { - time: new Date().getTime(), - blocks: [ - {id: '1', type: 'htmlraw', data: { html: out.article.content }}, - ], - version: '2.25.0' - } - } - } else { - out.article = { - publish_at: new Date() - } - } ctx.body = out } - /** GET: /api/auth/articles/:path */ + /** GET: /api/auth/articles/:id */ auth_getSingleArticle(ctx) { return this.private_getUpdateArticle(ctx) } - /** PUT: /api/auth/articles/:path */ + /** PUT: /api/auth/articles/:id */ async auth_updateCreateSingleArticle(ctx) { console.log(ctx.req.body) @@ -173,11 +102,11 @@ export default class ArticleRoutes { return this.private_getUpdateArticle(ctx, ctx.req.body, newBanner, newMedia) } - /** DELETE: /api/auth/articles/:path */ + /** DELETE: /api/auth/articles/:id */ async auth_removeSingleArticle(ctx) { let params = [ ctx.state.auth_token, - ctx.params.path, + ctx.params.id, // Article data null, null, @@ -187,25 +116,9 @@ export default class ArticleRoutes { null, null, 1, - // Banner data - null, - null, - null, - null, - null, - null, - null, - 1, - // Media data - null, - null, - null, - null, - null, - null, - null, - 1, ] + params = params.concat(mediaToDatabase(null, true)) + params = params.concat(mediaToDatabase(null, true)) await ctx.db.safeCallProc('article_auth_get_update_create', params) diff --git a/api/article/util.mjs b/api/article/util.mjs index 37f434d..007fc2b 100644 --- a/api/article/util.mjs +++ b/api/article/util.mjs @@ -1,3 +1,5 @@ +import { parseFile } from '../file/util.mjs' + export function parseArticles(articles) { for (let i = 0; i < articles.length; i++) { parseArticle(articles[i]) @@ -5,17 +7,52 @@ export function parseArticles(articles) { return articles } +export function combineFilesWithArticles(articles, files) { + let articleMap = new Map() + + articles.forEach(article => { + article.files = [] + articleMap.set(article.id, article) + }) + files.forEach(file => { + articleMap.get(file.id).files.push(parseFile(file)) + }) +} + export function parseArticle(article) { if (!article) { return null } + if (article.content) { + if (article.content[0] === '{') { + try { + article.content = JSON.parse(article.content) + } catch (err) { + article.content = { + time: new Date().getTime(), + blocks: [ + {id: '1', type: 'paragraph', data: { text: 'Error parsing article content: ' + err.message }}, + ], + version: '2.25.0' + } + } + } else { + article.content = { + time: new Date().getTime(), + blocks: [ + {id: '1', type: 'htmlraw', data: { html: article.content }}, + ], + version: '2.25.0' + } + } + } if (article.banner_path) { article.banner_path = 'https://cdn.nfp.is' + article.banner_path - article.banner_prefix = 'https://cdn.nfp.is' + article.banner_prefix + article.banner_alt_prefix = 'https://cdn.nfp.is' + article.banner_alt_prefix } if (article.media_path) { article.media_path = 'https://cdn.nfp.is' + article.media_path - article.media_prefix = 'https://cdn.nfp.is' + article.media_prefix + article.media_alt_prefix = 'https://cdn.nfp.is' + article.media_alt_prefix } return article } \ No newline at end of file diff --git a/api/authentication/routes.mjs b/api/authentication/routes.mjs index 0ba4f43..839bf33 100644 --- a/api/authentication/routes.mjs +++ b/api/authentication/routes.mjs @@ -13,7 +13,7 @@ export default class AuthenticationRoutes { /** GET: /api/authentication/login */ async login(ctx) { - let res = await ctx.db.safeCallProc('common.auth_login', [ + let res = await ctx.db.safeCallProc('auth_login', [ ctx.req.body.email, ctx.req.body.password, ]) diff --git a/api/db.mjs b/api/db.mjs index c042dfd..61a1e41 100644 --- a/api/db.mjs +++ b/api/db.mjs @@ -43,7 +43,7 @@ export function initPool(core, config) { return { safeCallProc: function(name, params, options) { if (name.indexOf('.') < 0) { - name = config.schema + '.' + name + name = 'common.' + name } return pool.promises.callProc(name, params, options) .catch(function(err) { diff --git a/api/media/util.mjs b/api/media/util.mjs new file mode 100644 index 0000000..54159eb --- /dev/null +++ b/api/media/util.mjs @@ -0,0 +1,25 @@ +export function mediaToDatabase(media, removeFlag) { + if (media) { + return [ + media.filename, + media.type, + media.path, + media.size, + media.preview.base64, + media.sizes.small.avif.path.replace(/_small\.avif$/, ''), + JSON.stringify(media.sizes), + removeFlag ? 1 : 0, + ] + } else { + return [ + null, + null, + null, + null, + null, + null, + null, + removeFlag ? 1 : 0, + ] + } +} \ No newline at end of file diff --git a/api/page/model.mjs b/api/page/model.mjs deleted file mode 100644 index 3bc8456..0000000 --- a/api/page/model.mjs +++ /dev/null @@ -1,48 +0,0 @@ -import { parseFile } from '../file/util.mjs' -import { parseArticle, parseArticles } from '../article/util.mjs' - -export async function getTree(ctx) { - let res = await ctx.db.safeCallProc('common.pages_get_tree', []) - let out = [] - let children = [] - let map = new Map() - for (let page of res.first) { - if (!page.parent_id) { - out.push(page) - } else { - children.push(page) - } - map.set(page.id, page) - } - for (let page of children) { - let parent = map.get(page.parent_id) - if (!parent.children) { - parent.children = [] - } - parent.children.push(page) - } - return { - tree: out - } -} - -export async function getPage(ctx, path, page = 0, per_page = 10) { - let res = await ctx.db.safeCallProc('common.pages_get_single', [path, page, per_page]) - - let articleMap = new Map() - - let out = { - page: res.results[0][0] || null, - articles: parseArticles(res.results[1]), - total_articles: res.results[2][0].total_articles, - featured: parseArticle(res.results[4][0]), - } - out.articles.forEach(article => { - article.files = [] - articleMap.set(article.id, article) - }) - res.results[3].forEach(file => { - articleMap.get(file.id).files.push(parseFile(file)) - }) - return out -} diff --git a/api/page/routes.mjs b/api/page/routes.mjs index 776c8e4..f39f2ed 100644 --- a/api/page/routes.mjs +++ b/api/page/routes.mjs @@ -1,26 +1,131 @@ -import * as Page from './model.mjs' -import * as security from './security.mjs' +import { parsePagesToTree } from './util.mjs' +import { upload } from '../media/upload.mjs' +import { combineFilesWithArticles, parseArticle, parseArticles } from '../article/util.mjs' +import { mediaToDatabase } from '../media/util.mjs' + export default class PageRoutes { constructor(opts = {}) { Object.assign(this, { - Page: opts.Page || Page, - security: opts.security || security, + upload: upload, }) } /** GET: /api/pagetree */ - async getPageTree(ctx) { - ctx.body = await this.Page.getTree(ctx) + async getPageTree(ctx, onlyReturn = false) { + let res = await ctx.db.safeCallProc('pages_get_tree', []) + + if (onlyReturn) { + return parsePagesToTree(res.first) + } + ctx.body = parsePagesToTree(res.first) } /** GET: /api/pages/[path] */ async getPage(ctx) { - ctx.body = await this.Page.getPage( - ctx, + let res = await ctx.db.safeCallProc('pages_get_single', [ ctx.params.path || null, Math.max(ctx.query.get('page') || 1, 1), - Math.min(ctx.query.get('per_page') || 10, 25) - ) + Math.min(ctx.query.get('per_page') || 10, 25), + ]) + + let out = { + page: res.results[0][0] || null, + articles: parseArticles(res.results[1]), + total_articles: res.results[2][0].total_articles, + featured: parseArticle(res.results[4][0]), + } + + combineFilesWithArticles(out.articles, res.results[3]) + + ctx.body = out + } + + /** GET: /api/auth/pages */ + async auth_getAllPages(ctx) { + let res = await ctx.db.safeCallProc('pages_auth_get_all', [ + ctx.state.auth_token + ]) + + ctx.body = parsePagesToTree(res.first) + } + + async private_getUpdatePage(ctx, body = null, banner = null, media = null) { + let params = [ + ctx.state.auth_token, + ctx.params.id === '0' ? null : ctx.params.id + ] + if (body) { + params = params.concat([ + body.name, + body.parent_id === 'null' ? null : Number(body.parent_id), + body.path, + body.content, + 0, + ]) + params = params.concat(mediaToDatabase(banner, body.remove_banner === 'true')) + params = params.concat(mediaToDatabase(media, body.remove_media === 'true')) + } + console.log(params) + let res = await ctx.db.safeCallProc('pages_auth_get_update_create', params) + + let out = { + page: res.results[0][0] || {}, + } + + ctx.body = out + } + + + /** GET: /api/auth/pages/:id */ + auth_getSinglePage(ctx) { + return this.private_getUpdatePage(ctx) + } + + /** PUT: /api/auth/pages/:id */ + async auth_updateCreateSinglePage(ctx) { + console.log(ctx.req.body) + + let newBanner = null + let newMedia = null + + let promises = [] + + if (ctx.req.files.banner) { + promises.push( + this.upload(ctx.req.files.banner) + .then(res => { newBanner = res }) + ) + } + if (ctx.req.files.media) { + promises.push( + this.upload(ctx.req.files.media) + .then(res => { newMedia = res }) + ) + } + + await Promise.all(promises) + + return this.private_getUpdatePage(ctx, ctx.req.body, newBanner, newMedia) + } + + /** DELETE: /api/auth/pages/:id */ + async auth_removeSinglePage(ctx) { + let params = [ + ctx.state.auth_token, + ctx.params.id, + // Page data + null, + null, + null, + null, + 1, + ] + params = params.concat(mediaToDatabase(null, true)) + params = params.concat(mediaToDatabase(null, true)) + + await ctx.db.safeCallProc('pages_auth_get_update_create', params) + + ctx.status = 204 } } diff --git a/api/page/security.mjs b/api/page/security.mjs deleted file mode 100644 index e69de29..0000000 diff --git a/api/page/util.mjs b/api/page/util.mjs new file mode 100644 index 0000000..af79f75 --- /dev/null +++ b/api/page/util.mjs @@ -0,0 +1,23 @@ +export function parsePagesToTree(pages) { + let out = [] + let children = [] + let map = new Map() + for (let page of pages) { + if (!page.parent_id) { + out.push(page) + } else { + children.push(page) + } + map.set(page.id, page) + } + for (let page of children) { + let parent = map.get(page.parent_id) + if (!parent.children) { + parent.children = [] + } + parent.children.push(page) + } + return { + tree: out + } +} \ No newline at end of file diff --git a/api/serve.mjs b/api/serve.mjs index a4abaac..906eada 100644 --- a/api/serve.mjs +++ b/api/serve.mjs @@ -4,11 +4,10 @@ import { FileResponse, HttpError } from 'flaska' import fs from 'fs/promises' import fsSync from 'fs' -import { getTree } from './page/model.mjs' - export default class ServeHandler { constructor(opts = {}) { Object.assign(this, { + pageRoutes: opts.pageRoutes, fs: opts.fs || fs, fsSync: opts.fsSync || fsSync, root: opts.root, @@ -66,7 +65,7 @@ export default class ServeHandler { } try { - payload.payloadTree = JSON.stringify(await getTree(ctx)) + payload.payloadTree = JSON.stringify(await this.pageRoutes.getPageTree(ctx, true)) } catch (e) { ctx.log.error(e) } diff --git a/api/server.mjs b/api/server.mjs index f0d35dd..1be6fe2 100644 --- a/api/server.mjs +++ b/api/server.mjs @@ -21,7 +21,7 @@ export function run(http, port, core) { nonce: ['script-src'], nonceCacheLength: 50, }, http) - + // Create our database pool let pool = initPool(core, config.get('mssql')) @@ -60,19 +60,30 @@ export function run(http, port, core) { flaska.get('/api/pagetree', page.getPageTree.bind(page)) flaska.get('/api/frontpage', page.getPage.bind(page)) flaska.get('/api/pages/:path', page.getPage.bind(page)) - // flaska.get('/api/pages/:pageId', page.getSinglePage.bind(page)) + flaska.get('/api/auth/pages', authenticate(), page.auth_getAllPages.bind(page)) + flaska.get('/api/auth/pages/:id', authenticate(), page.auth_getSinglePage.bind(page)) + flaska.put('/api/auth/pages/:id', [ + authenticate(), + FormidableHandler(formidable, { maxFileSize: 20 * 1024 * 1024, }), + ], page.auth_updateCreateSinglePage.bind(page)) + flaska.delete('/api/auth/pages/:id', authenticate(), page.auth_removeSinglePage.bind(page)) + const article = new ArticleRoutes() flaska.get('/api/articles/:path', article.getArticle.bind(article)) flaska.get('/api/auth/articles', authenticate(), article.auth_getAllArticles.bind(article)) - flaska.get('/api/auth/articles/:path', authenticate(), article.auth_getSingleArticle.bind(article)) - flaska.put('/api/auth/articles/:path', [authenticate(), FormidableHandler(formidable) ], article.auth_updateCreateSingleArticle.bind(article)) - // flaska.get('/api/pages/:pageId/articles/public', article.getPublicAllPageArticles.bind(article)) + flaska.get('/api/auth/articles/:id', authenticate(), article.auth_getSingleArticle.bind(article)) + flaska.put('/api/auth/articles/:id', [ + authenticate(), + FormidableHandler(formidable, { maxFileSize: 20 * 1024 * 1024, }), + ], article.auth_updateCreateSingleArticle.bind(article)) + flaska.delete('/api/auth/articles/:id', authenticate(), article.auth_removeSingleArticle.bind(article)) const authentication = new AuthenticationRoutes() flaska.post('/api/authentication/login', JsonHandler(), authentication.login.bind(authentication)) const serve = new ServeHandler({ + pageRoutes: page, root: localUtil.getPathFromRoot('../public'), version: core.app.running, frontend: config.get('frontend:url'), diff --git a/app/_common.scss b/app/_common.scss index 5388d8c..95d52f3 100644 --- a/app/_common.scss +++ b/app/_common.scss @@ -1,72 +1,3 @@ -:root { - --primary-bg: #01579b; - --primary-fg: white; - --primary-fg-url: #FFC7C7; - --primary-light-bg: #3D77C7; // #4f83cc; - --primary-light-fg: white; - --primary-dark-bg: #002f6c; - --primary-dark-fg: white; - - --secondary-bg: #f57c00; - --secondary-fg: black; - --secondary-light-bg: #ffad42; - --secondary-light-fg: black; - --secondary-dark-bg: #bb4d00; - --secondary-dark-fg: white; - - --table-fg: #333; - --border: #ccc; - --border-fg: black; - --border-fg-url: #8f3c00; - --title-fg: #555; - --meta-fg: #757575; // #999 - --meta-light-fg: #999999; - - --main-bg: white; - --main-fg: black; - --input-bg: white; - --input-border: #333; - --input-fg: black; - - --newsitem-bg: transparent; - --newsitem-border: none; -} - -.darkmodeon { - --primary-bg: #013b68; - --primary-fg: white; - --primary-fg-url: #FFC7C7; - --primary-light-bg: #28518B; - --primary-light-fg: white; - --primary-dark-bg: #002f6c; - --primary-dark-fg: white; - - --secondary-bg: #e05e00; - --secondary-fg: black; - --secondary-light-bg: #ffad42; - --secondary-light-fg: black; - --secondary-dark-bg: #e05e00; - --secondary-dark-fg: white; - --secondary-darker-fg: #fe791b; - - --table-fg: #333; - --border: #343536; - --border-fg: #d7dadc;; - --border-fg-url: #e05e00; - --title-fg: #808080; - --meta-fg: hsl(0, 0%, 55%); - --meta-light-fg: #999999; - - --main-bg: black; - --main-fg: #d7dadc; - --input-bg: #272729; - --input-border: #343536; - --input-fg: white; - - --newsitem-bg: #1a1a1b; - --newsitem-border: 1px solid #343536; -} - $primary-bg: #01579b; $primary-fg: white; diff --git a/app/admin/articles.js b/app/admin/articles.js index 02e44cf..2937f2d 100644 --- a/app/admin/articles.js +++ b/app/admin/articles.js @@ -52,7 +52,6 @@ const AdminArticles = { url: '/api/auth/articles?page=' + (this.lastpage || 1), }) .then((result) => { - console.log(result) this.data = result this.data.articles.forEach((article) => { @@ -75,13 +74,20 @@ const AdminArticles = { let removingArticle = this.removeArticle this.removeArticle = null this.loading = true - Article.removeArticle(removingArticle, removingArticle.id) - .then(this.oninit.bind(this, vnode)) - .catch(function(err) { - vnode.state.error = err.message - vnode.state.loading = false + m.redraw() + + return common.sendRequest({ + method: 'DELETE', + url: '/api/auth/articles/' + removingArticle.id, + }) + .then( + () => this.fetchArticles(vnode), + (err) => { + this.error = err.message + this.loading = false m.redraw() - }) + } + ) }, drawArticle: function(vnode, article) { @@ -93,7 +99,7 @@ const AdminArticles = { ? 'rowfeatured' : '' }, [ - m('td', m(m.route.Link, { href: '/admin/articles/' + article.path }, article.name)), + m('td', m(m.route.Link, { href: '/admin/articles/' + article.id }, article.name)), 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]), diff --git a/app/admin/editarticle.js b/app/admin/editarticle.js index 8e76abf..5d95cc5 100644 --- a/app/admin/editarticle.js +++ b/app/admin/editarticle.js @@ -16,7 +16,7 @@ const EditArticle = { staff: [], } this.pages = [{id: null, name: 'Frontpage'}] - this.addPageTree('', Page.Tree) + this.pages = this.pages.concat(Page.getFlatTree()) this.newBanner = null this.newMedia = null this.dateInstance = null @@ -25,15 +25,6 @@ const EditArticle = { this.fetchArticle(vnode) }, - addPageTree: function(prefix, branches) { - branches.forEach((page) => { - this.pages.push({ id: page.id, name: prefix + page.name }) - if (page.children && page.children.length) { - this.addPageTree(page.name + ' -> ', page.children) - } - }) - }, - onbeforeupdate: function(vnode) { if (this.lastid !== m.route.param('id')) { this.fetchArticle(vnode) @@ -42,16 +33,11 @@ const EditArticle = { fetchArticle: function(vnode) { this.lastid = m.route.param('id') - let id = this.lastid - if (id === 'add') { - id = '0' - } - this.error = '' return this.requestArticle( common.sendRequest({ method: 'GET', - url: '/api/auth/articles/' + id, + url: '/api/auth/articles/' + (this.lastid === 'add' ? '0' : this.lastid), })) }, @@ -75,9 +61,11 @@ const EditArticle = { data .then((result) => { this.data = result - if (this.data.article) { - this.data.article.publish_at = new Date(this.data.article.publish_at) + this.data.article.publish_at = new Date(this.data.article.publish_at) + + if (this.data.article.id) { document.title = 'Editing: ' + this.data.article.name + ' - Admin NFP Moe' + this.editedPath = true } else { document.title = 'Create Article - Admin NFP Moe' } @@ -122,16 +110,20 @@ const EditArticle = { }, mediaRemoved: function(type) { - this.data.article[type] = null + this.data.article['remove_' + type] = true + this.data.article[type + '_prefix'] = null }, save: function(vnode, e) { e.preventDefault() - - let id = this.lastid - if (id === 'add') { - id = '0' + if (!this.data.article.name) { + this.error = 'Name is missing' + } else if (!this.data.article.path) { + this.error = 'Path is missing' + } else { + this.error = '' } + if (this.error) return let formData = new FormData() if (this.newBanner) { @@ -150,6 +142,8 @@ const EditArticle = { formData.append('path', this.data.article.path) formData.append('page_id', this.data.article.page_id || null) formData.append('publish_at', this.dateInstance.inputElem.value.replace(', ', 'T') + 'Z') + formData.append('remove_banner', this.data.article.remove_banner ? true : false) + formData.append('remove_media', this.data.article.remove_media ? true : false) this.loading = true @@ -160,10 +154,19 @@ const EditArticle = { return common.sendRequest({ method: 'PUT', - url: '/api/auth/articles/' + id, + url: '/api/auth/articles/' + (this.lastid === 'add' ? '0' : this.lastid), body: formData, }) }) + .then(data => { + if (!data.article.id) { + throw new Error('Something went wrong with saving, try again later') + } else if (this.lastid === 'add') { + this.lastid = data.article.id.toString() + m.route.set('/admin/articles/' + data.article.id) + } + return data + }) ) }, @@ -198,6 +201,10 @@ const EditArticle = { ] : null), m('article.editarticle', [ + m('header', m('h1', + (this.data.article.id ? 'Edit ' : 'Create Article ') + (this.data.article.name || '(untitled)') + ) + ), m('header', m('h1', this.creating ? 'Create Article' : 'Edit ' + (this.data.article.name || '(untitled)'))), m('div.error', { hidden: !this.error, @@ -279,7 +286,7 @@ const EditArticle = { this.data.staff.map((item) => { return m('option', { value: item.id, - selected: item.id === this.data.article.staff_id + selected: item.id === this.data.article.admin_id }, item.name) }) ), @@ -293,7 +300,9 @@ const EditArticle = { }), ]), ]), - m('div', [ + m('div', { + hidden: !this.data.article.name || !this.data.article.path + }, [ m('input', { type: 'submit', value: 'Save', diff --git a/app/admin/editpage.js b/app/admin/editpage.js index cc4defc..b2d39ec 100644 --- a/app/admin/editpage.js +++ b/app/admin/editpage.js @@ -1,245 +1,261 @@ -const Authentication = require('../authentication') const FileUpload = require('../widgets/fileupload') -const Froala = require('./froala') -const Page = require('../api/page') +const Page = require('../api/page.p') + +const common = require('../api/common') +const Editor = require('./editor') const EditPage = { - getFroalaOptions: function() { - return { - theme: 'gray', - heightMin: 150, - videoUpload: false, - imageUploadURL: '/api/media', - imageManagerLoadURL: '/api/media', - imageManagerDeleteMethod: 'DELETE', - imageManagerDeleteURL: '/api/media', - events: { - 'imageManager.beforeDeleteImage': function(img) { - this.opts.imageManagerDeleteURL = '/api/media/' + img.data('id') - }, - }, - requestHeaders: { - 'Authorization': 'Bearer ' + Authentication.getToken(), - }, - } - }, - oninit: function(vnode) { - this.froala = null - this.loadedFroala = Froala.loadedFroala - - if (!this.loadedFroala) { - Froala.createFroalaScript() - .then(function() { - vnode.state.loadedFroala = true - m.redraw() - }) + this.loading = false + this.showLoading = null + this.data = { + page: null, } + this.pages = [{id: null, name: 'Frontpage'}] + this.pages = this.pages.concat(Page.getFlatTree()) + + this.newBanner = null + this.newMedia = null + this.editor = null this.fetchPage(vnode) }, - onupdate: function(vnode) { + onbeforeupdate: function(vnode) { if (this.lastid !== m.route.param('id')) { this.fetchPage(vnode) - if (this.lastid === 'add') { - m.redraw() - } } }, fetchPage: function(vnode) { this.lastid = m.route.param('id') - this.loading = this.lastid !== 'add' - this.creating = this.lastid === 'add' - this.error = '' - this.page = { - name: '', - path: '', - description: '', - media: null, - } - this.editedPath = false - if (this.lastid !== 'add') { - Page.getPage(this.lastid) - .then(function(result) { - vnode.state.editedPath = true - vnode.state.page = result - document.title = 'Editing: ' + result.name + ' - Admin NFP Moe' - }) - .catch(function(err) { - vnode.state.error = err.message - }) - .then(function() { - vnode.state.loading = false - m.redraw() - }) - } else { - document.title = 'Create Page - Admin NFP Moe' + return this.requestPage( + common.sendRequest({ + method: 'GET', + url: '/api/auth/pages/' + (this.lastid === 'add' ? '0' : this.lastid), + })) + }, + + requestPage: function(data) { + this.error = '' + + 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 + } + + data + .then((result) => { + this.data = result + if (this.data.page.id) { + document.title = 'Editing: ' + this.data.page.name + ' - Admin NFP Moe' + this.editedPath = true + } else { + document.title = 'Create Page - Admin NFP Moe' + } + }, (err) => { + this.error = err.message + }) + .then(() => { + clearTimeout(this.showLoading) + this.showLoading = null + this.loading = false + m.redraw() + }) }, updateValue: function(name, e) { - this.page[name] = e.currentTarget.value + this.data.page[name] = e.currentTarget.value if (name === 'path') { this.editedPath = true } else if (name === 'name' && !this.editedPath) { - this.page.path = this.page.name.toLowerCase().replace(/ /g, '-') + this.data.page.path = this.data.page.name.toLowerCase().replace(/ /g, '-') } }, updateParent: function(e) { - this.page.parent_id = Number(e.currentTarget.value) - if (this.page.parent_id === -1) { - this.page.parent_id = null + this.data.page.parent_id = Number(e.currentTarget.value) || null + }, + + mediaUploaded: function(type, file) { + if (type === 'banner') { + this.newBanner = file + } else { + this.newMedia = file } }, - fileUploaded: function(type, media) { - this.page[type] = media - }, - - fileRemoved: function(type) { - this.page[type] = null + mediaRemoved: function(type) { + this.data.page['remove_' + type] = true + this.data.page[type + '_prefix'] = null }, save: function(vnode, e) { e.preventDefault() - if (!this.page.name) { + if (!this.data.page.name) { this.error = 'Name is missing' - } else if (!this.page.path) { + } else if (!this.data.page.path) { this.error = 'Path is missing' } else { this.error = '' } if (this.error) return - this.page.description = vnode.state.froala ? vnode.state.froala.html.get() : this.page.description - if (this.page.description) { - this.page.description = this.page.description.replace(/]+data-f-id="pbf"[^>]+>[^>]+>[^>]+>[^>]+>/, '') + let formData = new FormData() + if (this.newBanner) { + formData.append('banner', this.newBanner.file) } + if (this.newMedia) { + formData.append('media', this.newMedia.file) + } + if (this.data.page.id) { + formData.append('id', this.data.page.id) + } + + formData.append('name', this.data.page.name) + formData.append('parent_id', this.data.page.parent_id || null) + formData.append('path', this.data.page.path) + formData.append('remove_banner', this.data.page.remove_banner ? true : false) + formData.append('remove_media', this.data.page.remove_media ? true : false) this.loading = true - let promise + this.requestPage( + this.editor.save() + .then(body => { + formData.append('content', JSON.stringify(body)) - if (this.page.id) { - promise = Page.updatePage(this.page.id, { - name: this.page.name, - path: this.page.path, - parent_id: this.page.parent_id, - description: this.page.description, - banner_id: this.page.banner && this.page.banner.id || null, - media_id: this.page.media && this.page.media.id || null, + return common.sendRequest({ + method: 'PUT', + url: '/api/auth/pages/' + (this.lastid === 'add' ? '0' : this.lastid), + body: formData, + }) }) - } else { - promise = Page.createPage({ - name: this.page.name, - path: this.page.path, - parent_id: this.page.parent_id, - description: this.page.description, - banner_id: this.page.banner && this.page.banner.id || null, - media_id: this.page.media && this.page.media.id || null, + .then(data => { + if (!data.page.id) { + throw new Error('Something went wrong with saving, try again later') + } else if (this.lastid === 'add') { + this.lastid = data.page.id.toString() + m.route.set('/admin/pages/' + data.page.id) + } + return Page.refreshTree().then(() => { + this.pages = [{id: null, name: 'Frontpage'}] + this.pages = this.pages.concat(Page.getFlatTree()) + + return data + }) }) - } - - promise.then(function(res) { - if (vnode.state.page.id) { - res.media = vnode.state.page.media - res.banner = vnode.state.page.banner - vnode.state.page = res - console.log(res) - } else { - m.route.set('/admin/pages/' + res.id) - } - }) - .catch(function(err) { - vnode.state.error = err.message - }) - .then(function() { - vnode.state.loading = false - m.redraw() - }) - - return false + ) }, view: function(vnode) { - const parents = [{id: null, name: '-- Frontpage --'}].concat(Page.Tree).filter(function (page) { return !vnode.state.page || page.id !== vnode.state.page.id}) - return ( - this.loading ? - m('div.loading-spinner') - : m('div.admin-wrapper', [ - m('div.admin-actions', this.page.id + const bannerImage = this.data.page && this.data.page.banner_prefix + ? this.data.page.banner_prefix + '_large.avif' + : null + const mediaImage = this.data.page && this.data.page.media_prefix + ? this.data.page.media_prefix + '_large.avif' + : null + + return [ + this.loading && !this.data.page + ? m('div.admin-spinner.loading-spinner') + : null, + this.data.page + ? m('div.admin-wrapper', [ + this.loading + ? m('div.loading-spinner') + : null, + m('div.admin-actions', this.data.page.id ? [ m('span', 'Actions:'), - m(m.route.Link, { href: '/page/' + this.page.path }, 'View page'), - m(m.route.Link, { href: '/admin/pages/add' }, 'Create new page'), + m(m.route.Link, { href: '/page/' + this.data.page.path }, 'View page'), ] : null), - m('article.editpage', [ - m('header', m('h1', this.creating ? 'Create Page' : 'Edit ' + (this.page.name || '(untitled)'))), + m('article.editarticle', [ + m('header', m('h1', + (this.data.page.id ? 'Edit ' : 'Create Page ') + (this.data.page.name || '(untitled)') + ) + ), m('div.error', { hidden: !this.error, - onclick: function() { vnode.state.error = '' }, + onclick: () => { vnode.state.error = '' }, }, this.error), m(FileUpload, { - onupload: this.fileUploaded.bind(this, 'banner'), - ondelete: this.fileRemoved.bind(this, 'banner'), - onerror: function(e) { vnode.state.error = e }, - media: this.page && this.page.banner, + height: 300, + onfile: this.mediaUploaded.bind(this, 'banner'), + ondelete: this.mediaRemoved.bind(this, 'banner'), + media: bannerImage, }), m(FileUpload, { class: 'cover', useimg: true, - onupload: this.fileUploaded.bind(this, 'media'), - ondelete: this.fileRemoved.bind(this, 'media'), - onerror: function(e) { vnode.state.error = e }, - media: this.page && this.page.media, + onfile: this.mediaUploaded.bind(this, 'media'), + ondelete: this.mediaRemoved.bind(this, 'media'), + media: mediaImage, }), - m('form.editpage.content', { + m('form.editarticle.content', { onsubmit: this.save.bind(this, vnode), }, [ m('label', 'Parent'), m('select', { onchange: this.updateParent.bind(this), - }, parents.map(function(item) { - return m('option', { value: item.id || -1, selected: item.id === vnode.state.page.parent_id }, item.name) - })), - m('label', 'Name'), - m('input', { - type: 'text', - value: this.page.name, - oninput: this.updateValue.bind(this, 'name'), - }), + }, this.pages.filter(item => !this.data.page || item.id !== this.data.page.id).map((item) => { + return m('option', { + value: item.id || 0, + selected: item.id === this.data.page.parent_id + }, item.name) + })), + m('div.input-row', [ + m('div.input-group', [ + m('label', 'Name'), + m('input', { + type: 'text', + value: this.data.page.name, + oninput: this.updateValue.bind(this, 'name'), + }), + ]), + m('div.input-group', [ + m('label', 'Path'), + m('input', { + type: 'text', + value: this.data.page.path, + oninput: this.updateValue.bind(this, 'path'), + }), + ]), + ]), m('label', 'Description'), - ( - this.loadedFroala ? - m('div', { - oncreate: function(div) { - vnode.state.froala = new FroalaEditor(div.dom, EditPage.getFroalaOptions(), function() { - vnode.state.froala.html.set(vnode.state.page.description) - }) - }, - }) - : null - ), - m('label', 'Path'), - m('input', { - type: 'text', - value: this.page.path, - oninput: this.updateValue.bind(this, 'path'), - }), - m('div.loading-spinner', { hidden: this.loadedFroala }), - m('input', { - type: 'submit', - value: 'Save', + m(Editor, { + oncreate: (subnode) => { + this.editor = subnode.state.editor + }, + contentdata: this.data.page.content, }), + m('div', { + hidden: !this.data.page.name || !this.data.page.path + }, [ + m('input', { + type: 'submit', + value: 'Save', + }), + ]), ]), ]), ]) - ) + : m('div.error', { + hidden: !this.error, + onclick: () => { this.fetchPage(vnode) }, + }, this.error),, + ] }, } diff --git a/app/admin/pages.js b/app/admin/pages.js index acc0972..e55a4d0 100644 --- a/app/admin/pages.js +++ b/app/admin/pages.js @@ -1,40 +1,32 @@ -const Page = require('../api/page') +const Page = require('../api/page.p') const Dialogue = require('../widgets/dialogue') +const common = require('../api/common') const AdminPages = { - parseTree: function(pages) { - let map = new Map() - for (let i = 0; i < pages.length; i++) { - pages[i].children = [] - map.set(pages[i].id, pages[i]) - } - for (let i = 0; i < pages.length; i++) { - if (pages[i].parent_id && map.has(pages[i].parent_id)) { - map.get(pages[i].parent_id).children.push(pages[i]) - pages.splice(i, 1) - i-- - } - } - return pages - }, - oninit: function(vnode) { - this.loading = true this.error = '' this.pages = [] this.removePage = null document.title = 'Pages - Admin NFP Moe' + this.fetchPages(vnode) + }, - Page.getAllPages() - .then(function(result) { - vnode.state.pages = AdminPages.parseTree(result) + fetchPages: function(vnode) { + this.loading = true + this.error = '' + + return common.sendRequest({ + method: 'GET', + url: '/api/auth/pages', }) - .catch(function(err) { - vnode.state.error = err.message + .then((result) => { + this.pages = result.tree + }, (err) => { + this.error = err.message }) - .then(function() { - vnode.state.loading = false + .then(() => { + this.loading = false m.redraw() }) }, @@ -43,13 +35,21 @@ const AdminPages = { let removingPage = this.removePage this.removePage = null this.loading = true - Page.removePage(removingPage, removingPage.id) - .then(this.oninit.bind(this, vnode)) - .catch(function(err) { - vnode.state.error = err.message - vnode.state.loading = false + m.redraw() + + return common.sendRequest({ + method: 'DELETE', + url: '/api/auth/pages/' + removingPage.id, + }) + .then(() => Page.refreshTree()) + .then( + () => this.fetchPages(vnode), + (err) => { + this.error = err.message + this.loading = false m.redraw() - }) + } + ) }, drawPage: function(vnode, page) { @@ -63,7 +63,7 @@ const AdminPages = { m('td.right', page.updated_at.replace('T', ' ').split('.')[0]), m('td.right', m('button', { onclick: function() { vnode.state.removePage = page } }, 'Remove')), ]), - ].concat(page.children.map(AdminPages.drawPage.bind(this, vnode))) + ].concat(page.children ? page.children.map(AdminPages.drawPage.bind(this, vnode)) : []) }, view: function(vnode) { @@ -79,7 +79,7 @@ const AdminPages = { m('header', m('h1', 'All pages')), m('div.error', { hidden: !this.error, - onclick: function() { vnode.state.error = '' }, + onclick: () => { this.fetchPages(vnode) }, }, this.error), m('table', [ m('thead', diff --git a/app/api/page.js b/app/api/page.js index 41923c7..8cf03ef 100644 --- a/app/api/page.js +++ b/app/api/page.js @@ -25,6 +25,21 @@ exports.createPage = function(body) { }) } +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', diff --git a/app/api/page.p.js b/app/api/page.p.js index 4a44b96..990c9ca 100644 --- a/app/api/page.p.js +++ b/app/api/page.p.js @@ -18,9 +18,37 @@ function parseLeaf(tree) { parseLeaf(Tree) +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.getPage = function(path, page) { return common.sendRequest({ method: 'GET', url: '/api/' + (path ? 'pages/' + path : 'frontpage') + '?page=' + (page || 1), }) } + +exports.refreshTree = function() { + return common.sendRequest({ + method: 'GET', + url: '/api/pagetree', + }) + .then(pages => { + Tree.splice(0, Tree.length) + Tree.push.apply(Tree, pages.tree) + TreeMap.clear() + parseLeaf(Tree) + }) +} diff --git a/app/app.scss b/app/app.scss index 5ed100a..5645989 100644 --- a/app/app.scss +++ b/app/app.scss @@ -32,6 +32,7 @@ ol, ul { img { max-width: 100%; height: auto; + display: block; } @keyframes spinner-loader { @@ -274,3 +275,72 @@ input[type="reset"]::-moz-focus-inner { a { color: $dark_secondary-dark-bg; } } } + +:root { + --primary-bg: #01579b; + --primary-fg: white; + --primary-fg-url: #FFC7C7; + --primary-light-bg: #3D77C7; // #4f83cc; + --primary-light-fg: white; + --primary-dark-bg: #002f6c; + --primary-dark-fg: white; + + --secondary-bg: #f57c00; + --secondary-fg: black; + --secondary-light-bg: #ffad42; + --secondary-light-fg: black; + --secondary-dark-bg: #bb4d00; + --secondary-dark-fg: white; + + --table-fg: #333; + --border: #ccc; + --border-fg: black; + --border-fg-url: #8f3c00; + --title-fg: #555; + --meta-fg: #757575; // #999 + --meta-light-fg: #999999; + + --main-bg: white; + --main-fg: black; + --input-bg: white; + --input-border: #333; + --input-fg: black; + + --newsitem-bg: transparent; + --newsitem-border: none; +} + +.darkmodeon { + --primary-bg: #013b68; + --primary-fg: white; + --primary-fg-url: #FFC7C7; + --primary-light-bg: #28518B; + --primary-light-fg: white; + --primary-dark-bg: #002f6c; + --primary-dark-fg: white; + + --secondary-bg: #e05e00; + --secondary-fg: black; + --secondary-light-bg: #ffad42; + --secondary-light-fg: black; + --secondary-dark-bg: #e05e00; + --secondary-dark-fg: white; + --secondary-darker-fg: #fe791b; + + --table-fg: #333; + --border: #343536; + --border-fg: #d7dadc;; + --border-fg-url: #e05e00; + --title-fg: #808080; + --meta-fg: hsl(0, 0%, 55%); + --meta-light-fg: #999999; + + --main-bg: black; + --main-fg: #d7dadc; + --input-bg: #272729; + --input-border: #343536; + --input-fg: white; + + --newsitem-bg: #1a1a1b; + --newsitem-border: 1px solid #343536; +} diff --git a/app/article/article.js b/app/article/article.js index 0b9d1f6..cb5e003 100644 --- a/app/article/article.js +++ b/app/article/article.js @@ -2,6 +2,7 @@ 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) { @@ -11,6 +12,10 @@ const Article = { this.data = { article: null, files: [], + pictureFallback: null, + pictureJpeg: null, + pictureAvif: null, + pictureCover: null, } this.showcomments = false @@ -52,6 +57,24 @@ const Article = { .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' } @@ -67,19 +90,7 @@ const Article = { }, view: function(vnode) { - var deviceWidth = window.innerWidth - var imagePath = '' - - if (this.data.article && this.data.article.media) { - var pixelRatio = window.devicePixelRatio || 1 - if ((deviceWidth < 800 && pixelRatio <= 1) - || (deviceWidth < 600 && pixelRatio > 1)) { - imagePath = this.data.article.media.medium_url - } else { - imagePath = this.data.article.media.large_url - } - } - + let article = this.data.article return ( this.loading ? m('article.article', m('div.loading-spinner')) @@ -91,37 +102,53 @@ const Article = { }, }, 'Article error: ' + this.error)) : m('article.article', [ - this.data.article.page_path - ? m('div.goback', ['« ', m(m.route.Link, { href: '/page/' + this.data.article.page_path }, this.data.article.page_name)]) + article.page_path + ? m('div.goback', ['« ', m(m.route.Link, { href: '/page/' + article.page_path }, article.page_name)]) : null, - m('header', m('h1', this.data.article.name)), + m('header', m('h1', article.name)), m('.fr-view', [ - this.data.article.media + article.pictureFallback ? m('a.cover', { rel: 'noopener', - href: this.data.article.media.link, - }, m('img', { src: imagePath, alt: 'Cover image for ' + this.data.article.name })) + 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, - this.data.article.content ? m.trust(this.data.article.content) : 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 ', - this.data.article.page_path + article.page_path ? [ 'in', - m(m.route.Link, { href: '/page/' + this.data.article.page_path }, this.data.article.page_name) + m(m.route.Link, { href: '/page/' + article.page_path }, article.page_name) ] : '', - 'at ' + (this.data.article.publish_at.replace('T', ' ').split('.')[0]).substr(0, 16), - ' by ' + (this.data.article.admin_name || 'Admin'), + '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/' + this.data.article.path }, 'Edit article'), + m(m.route.Link, { href: '/admin/articles/' + article.path }, 'Edit article'), ]) : null, this.showcomments diff --git a/app/authentication.js b/app/authentication.js index 7463db8..873c769 100644 --- a/app/authentication.js +++ b/app/authentication.js @@ -1,4 +1,4 @@ -const storageName = 'logintoken' +const storageName = 'nfp_sites_logintoken' const Authentication = { currentUser: null, @@ -37,4 +37,6 @@ const Authentication = { Authentication.updateToken(localStorage.getItem(storageName)) +window.Authentication = Authentication + module.exports = Authentication diff --git a/app/footer/footer.js b/app/footer/footer.js index a661a71..f9740fa 100644 --- a/app/footer/footer.js +++ b/app/footer/footer.js @@ -16,7 +16,7 @@ const Footer = { Page.Tree.map(function(page) { return [ m(m.route.Link, { class: 'root', href: '/page/' + page.path }, page.name), - (page.children.length + (page.children ? m('ul', page.children.map(function(subpage) { return m('li', m(m.route.Link, { class: 'child', href: '/page/' + subpage.path }, subpage.name)) })) diff --git a/app/frontpage/frontpage.js b/app/frontpage/frontpage.js index 8058804..8e9cb29 100644 --- a/app/frontpage/frontpage.js +++ b/app/frontpage/frontpage.js @@ -74,7 +74,7 @@ const Frontpage = { view: function(vnode) { var deviceWidth = window.innerWidth - var bannerPath = this.data.featured && this.data.featured.banner_prefix + var bannerPath = this.data.featured && this.data.featured.banner_alt_prefix if (bannerPath) { var pixelRatio = window.devicePixelRatio || 1 diff --git a/app/index.js b/app/index.js index 0d6860f..16a2029 100644 --- a/app/index.js +++ b/app/index.js @@ -9,12 +9,6 @@ m.route.set = function(path, data, options){ window.scrollTo(0, 0) } -/*console.log('tree', window.__nfptree) -console.log('featured', window.__nfpfeatured) -console.log('data', window.__nfpdata) -console.log('subdata', window.__nfpsubdata) -console.log('links', window.__nfplinks)*/ - m.route.linkOrig = m.route.link m.route.link = function(vnode){ m.route.linkOrig(vnode) @@ -40,7 +34,6 @@ const onLoaded = function() { } const onError = function(a, b, c) { - console.log('onError', this, a, b, c) elements.forEach(function(x) { x.remove() }) loadedAdmin = loadingAdmin = false loaded = 0 diff --git a/app/login/login.js b/app/login/login.js index fe817b3..b4c19da 100644 --- a/app/login/login.js +++ b/app/login/login.js @@ -45,7 +45,6 @@ const Login = { if (!result.token) { return Promise.reject(new Error('Server authentication down.')) } - console.log(result) Authentication.updateToken(result.token) m.route.set(Login.redirect || '/') }) diff --git a/app/menu/menu.js b/app/menu/menu.js index be892e1..c14adfe 100644 --- a/app/menu/menu.js +++ b/app/menu/menu.js @@ -22,11 +22,7 @@ const Menu = { Menu.loading = true - Page.getTree() - .then(function(results) { - Page.Tree.splice(0, Page.Tree.length) - Page.Tree.push.apply(Page.Tree, results) - }) + Page.refreshTree() .catch(function(err) { Menu.error = err.message }) @@ -37,6 +33,7 @@ const Menu = { }, view: function() { + console.log('menu view', Boolean(Authentication.currentUser)) return [ m('div.top', [ m(m.route.Link, @@ -73,7 +70,7 @@ const Menu = { class: Menu.currentActive === 'home' ? 'active' : '', }, 'Home'), Menu.loading ? m('div.loading-spinner') : Page.Tree.map(function(page) { - if (page.children.length) { + if (page.children) { return m('div.hassubmenu', [ m(m.route.Link, { href: '/page/' + page.path, diff --git a/app/pages/page.js b/app/pages/page.js index 409e64c..accc23d 100644 --- a/app/pages/page.js +++ b/app/pages/page.js @@ -156,7 +156,7 @@ const Page = { ? m('aside.news', [ m('h4', 'Latest posts under ' + this.data.page.name + ':'), this.data.articles.map(function(article) { - return m(Newsentry, article) + return m(Newsentry, { article: article }) }), m(Pages, { base: '/page/' + this.data.page.path, @@ -171,7 +171,7 @@ const Page = { imagePath ? m('a', { href: this.data.page.media.link}, m('img.page-cover', { src: imagePath, alt: 'Cover image for ' + this.data.page.name } )) : null, m('h4', 'Latest posts under ' + this.data.page.name + ':'), this.data.articles.map(function(article) { - return m(Newsentry, article) + return m(Newsentry, { article: article }) }), m(Pages, { base: '/page/' + this.data.page.path, diff --git a/app/widgets/editorblock.js b/app/widgets/editorblock.js new file mode 100644 index 0000000..08e74a4 --- /dev/null +++ b/app/widgets/editorblock.js @@ -0,0 +1,61 @@ +/* +Blocks: + * Paragraph + * Header + * SimpleImage + * Quote + * CodeTool + * List + * Delimiter + * RawTool + +Other: + * InlineCode +*/ + +const EditorBlock = { + oninit: function(vnode) { + this.id = null + this.output = null + this.onbeforeupdate(vnode) + }, + + onbeforeupdate: function(vnode) { + if (!vnode.attrs.block && !this.id) { + return false + } + if (vnode.attrs.block && vnode.attrs.block.id + && vnode.attrs.block.id === this.id) { + return false + } + + if (vnode.attrs.block && vnode.attrs.block.id + && vnode.attrs.block.id !== this.id) { + this.renderblock(vnode) + } else { + this.output = null + } + }, + + renderblock: function(vnode) { + let block = vnode.attrs.block + this.id = block.id + switch (block.type) { + case 'paragraph': + this.output = m('p', m.trust(block.data.text)) + break + case 'htmlraw': + this.output = m.trust(block.data.html) + break + default: + this.output = m('p', m.trust(block)) + break + } + }, + + view: function(vnode) { + return this.output + } +} + +module.exports = EditorBlock diff --git a/app/widgets/newsentry.js b/app/widgets/newsentry.js index f9aa244..f0ca51a 100644 --- a/app/widgets/newsentry.js +++ b/app/widgets/newsentry.js @@ -1,53 +1,95 @@ const Fileinfo = require('./fileinfo') const Newsentry = { + oninit: function(vnode) { + this.lastId = null + this.onbeforeupdate(vnode) + }, + strip: function(html) { - var doc = new DOMParser().parseFromString(html, 'text/html') - var out = doc.body.textContent || '' - var splitted = out.split('.') - if (splitted.length > 2) { + var doc = new DOMParser().parseFromString(html, 'text/html') + var out = doc.body.textContent || '' + var splitted = out.split('.') + if (splitted.length > 2) { return splitted.slice(0, 2).join('.') + '...' - } - return out + } + return out + }, + + onbeforeupdate: function(vnode) { + let article = vnode.attrs.article + + if (this.lastId !== article.id) { + 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.media_alt_prefix) { + this.pictureFallback = article.media_alt_prefix + '_small.jpg' + this.pictureJpeg = article.media_alt_prefix + '_small.jpg' + ' 720w, ' + + article.media_alt_prefix + '_medium.jpg' + ' 1300w, ' + + article.media_alt_prefix + '_large.jpg 1920w' + this.pictureAvif = article.media_alt_prefix + '_small.avif' + ' 720w, ' + + article.media_alt_prefix + '_medium.avif' + ' 1300w, ' + + article.media_alt_prefix + '_large.avif 1920w' + + this.pictureCover = '(max-width: 440px) calc(100vw - 40px), ' + + '124px' + } else { + this.pictureFallback = null + this.pictureJpeg = null + this.pictureAvif = null + this.pictureCover = null + } + } }, view: function(vnode) { - var deviceWidth = window.innerWidth - var pixelRatio = window.devicePixelRatio || 1 - var imagePath = '' + let article = vnode.attrs.article - if (vnode.attrs.media) { - if (deviceWidth > 440 || pixelRatio <= 1) { - imagePath = vnode.attrs.media.small_url - } else { - imagePath = vnode.attrs.media.medium_url - } - } return m('newsentry', [ - imagePath + this.pictureFallback ? m(m.route.Link, { - class: 'cover', - href: '/article/' + vnode.attrs.path, - }, m('picture', [ - m('source', { srcset: - vnode.attrs.media.small_url + '' - }), - m('img', { src: imagePath, alt: 'Article image for ' + vnode.attrs.name }), - ])) + class: 'cover', + href: '/article/' + article.path, + }, + m('picture', [ + m('source', { + srcset: this.pictureAvif, + sizes: this.pictureCover, + type: 'image/avif', + }), + m('img', { + srcset: this.pictureJpeg, + sizes: this.pictureCover, + alt: 'Image for news item ' + article.name, + src: this.pictureFallback, + }), + ]) + ) : m('a.cover.nobg'), m('div.entrycontent', [ m('div.title', [ m(m.route.Link, - { href: '/article/' + vnode.attrs.path }, - m('h3', [vnode.attrs.name]) + { href: '/article/' + article.path }, + m('h3', [article.name]) ), ]), - (vnode.attrs.files && vnode.attrs.files.length - ? vnode.attrs.files.map(function(file) { + (article.files && article.files.length + ? article.files.map(function(file) { return m(Fileinfo, { file: file, slim: true }) }) - : vnode.attrs.description - ? m('span.entrydescription', Newsentry.strip(vnode.attrs.description)) + : this.description + ? m('span.entrydescription', this.description) : null), ]), ]) diff --git a/app/widgets/newsitem.js b/app/widgets/newsitem.js index b0403af..fb4b7f5 100644 --- a/app/widgets/newsitem.js +++ b/app/widgets/newsitem.js @@ -1,25 +1,36 @@ const Fileinfo = require('./fileinfo') +const EditorBlock = require('./editorblock') const Newsitem = { oninit: function(vnode) { - let article = vnode.attrs.article - if (article.media_prefix) { - this.fallbackImage = article.media_prefix + '_small.jpg' - this.srcsetJpeg = article.media_prefix + '_small.jpg' + ' 720w, ' - + article.media_prefix + '_medium.jpg' + ' 1300w, ' - + article.media_prefix + '_large.jpg' - this.srcsetAvif = article.media_prefix + '_small.avif' + ' 720w, ' - + article.media_prefix + '_medium.avif' + ' 1300w, ' - + article.media_prefix + '_large.avif' + this.lastId = null + this.onbeforeupdate(vnode) + }, - this.coverSizes = '(max-width: 639px) calc(100vw - 40px), ' - + '(max-width: 1000px) 300px, ' - + '400px' - } else { - this.fallbackImage = null - this.srcsetJpeg = null - this.srcsetAvif = null - this.coverSizes = null + onbeforeupdate: function(vnode) { + let article = vnode.attrs.article + + if (this.lastId !== article.id) { + this.lastId = article.id + + if (article.media_alt_prefix) { + this.pictureFallback = article.media_alt_prefix + '_small.jpg' + this.pictureJpeg = article.media_alt_prefix + '_small.jpg' + ' 720w, ' + + article.media_alt_prefix + '_medium.jpg' + ' 1300w, ' + + article.media_alt_prefix + '_large.jpg 1920w' + this.pictureAvif = article.media_alt_prefix + '_small.avif' + ' 720w, ' + + article.media_alt_prefix + '_medium.avif' + ' 1300w, ' + + article.media_alt_prefix + '_large.avif 1920w' + + this.pictureCover = '(max-width: 639px) calc(100vw - 40px), ' + + '(max-width: 1000px) 300px, ' + + '400px' + } else { + this.pictureFallback = null + this.pictureJpeg = null + this.pictureAvif = null + this.pictureCover = null + } } }, @@ -32,32 +43,30 @@ const Newsitem = { m('h3', [article.name]) ), m('div.newsitemcontent', [ - this.fallbackImage + this.pictureFallback ? m(m.route.Link, { class: 'cover', href: '/article/' + article.path, }, m('picture', [ - this.srcsetAvif ? m('source', { - srcset: this.srcsetAvif, - sizes: this.coverSizes, + m('source', { + srcset: this.pictureAvif, + sizes: this.pictureCover, type: 'image/avif', - }) : null, + }), m('img', { - srcset: this.srcsetJpeg, - sizes: this.coverSizes, + srcset: this.pictureJpeg, + sizes: this.pictureCover, alt: 'Image for news item ' + article.name, - src: this.fallbackImage, + src: this.pictureFallback, }), ]) ) : null, - m('div.entrycontent', { - class: article.media ? '' : 'extrapadding', - }, [ - (article.content - ? m('.fr-view', m.trust(article.content)) - : null), + m('div.entrycontent', [ + article.content.blocks.map(block => { + return m(EditorBlock, { block: block }) + }), (article.files && article.files.length ? article.files.map(function(file) { return m(Fileinfo, { file: file, trim: true })