From ebd3cd4d26e38803f7ba5bb9e9030113354088d2 Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Fri, 22 Jul 2022 11:18:33 +0000 Subject: [PATCH] Development --- api/article/routes.mjs | 168 ++++++++++++++++++++++---- api/media/upload.mjs | 22 ++-- app/admin.scss | 41 +++++++ app/admin/admin.scss | 1 + app/admin/articles.scss | 13 +- app/admin/editarticle.js | 251 ++++++++++++++++++--------------------- app/admin/editor.js | 12 +- app/app.scss | 1 + 8 files changed, 335 insertions(+), 174 deletions(-) diff --git a/api/article/routes.mjs b/api/article/routes.mjs index 27f950b..47764c9 100644 --- a/api/article/routes.mjs +++ b/api/article/routes.mjs @@ -1,4 +1,5 @@ import { parseFiles } from '../file/util.mjs' +import { parseArticles, parseArticle } from './util.mjs' import { upload } from '../media/upload.mjs' export default class ArticleRoutes { @@ -13,7 +14,7 @@ export default class ArticleRoutes { let res = await ctx.db.safeCallProc('article_get_single', [ctx.params.path]) let out = { - article: res.results[0][0] || null, + article: parseArticle(res.results[0][0]), files: parseFiles(res.results[1]), } @@ -29,33 +30,124 @@ export default class ArticleRoutes { ]) let out = { - articles: res.results[0], + articles: parseArticles(res.results[0]), total_articles: res.results[0][0].total_articles, } ctx.body = out } + async private_getUpdateArticle(ctx, body = null, banner = null, media = null) { + let params = [ + ctx.state.auth_token, + ctx.params.path + ] + if (body) { + params = params.concat([ + body.name, + body.page_id === 'null' ? null : Number(body.page_id), + body.path, + body.content, + new Date(body.publish_at), + Number(body.admin_id), + 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, + ]) + } + } + console.log(params) + let res = await ctx.db.safeCallProc('article_auth_get_update_create', params) + + let out = { + article: parseArticle(res.results[0][0]), + 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 */ - async auth_getSingleArticle(ctx) { - let res = await ctx.db.safeCallProc('article_auth_get_update_create', [ - ctx.state.auth_token, - ctx.params.path - ]) - - let out = { - article: res.results[0][0] || null, - files: parseFiles(res.results[1]), - staff: res.results[2], - } - - ctx.body = out + auth_getSingleArticle(ctx) { + return this.private_getUpdateArticle(ctx) } /** PUT: /api/auth/articles/:path */ async auth_updateCreateSingleArticle(ctx) { - console.log(ctx.req.files) console.log(ctx.req.body) let newBanner = null @@ -78,9 +170,45 @@ export default class ArticleRoutes { await Promise.all(promises) - console.log(newBanner) - console.log(newMedia) - - ctx.body = {} + return this.private_getUpdateArticle(ctx, ctx.req.body, newBanner, newMedia) + } + + /** DELETE: /api/auth/articles/:path */ + async auth_removeSingleArticle(ctx) { + let params = [ + ctx.state.auth_token, + ctx.params.path, + // Article data + null, + null, + null, + null, + null, + null, + null, + 1, + // Banner data + null, + null, + null, + null, + null, + null, + null, + 1, + // Media data + null, + null, + null, + null, + null, + null, + null, + 1, + ] + + await ctx.db.safeCallProc('article_auth_get_update_create', params) + + ctx.status = 204 } } diff --git a/api/media/upload.mjs b/api/media/upload.mjs index b153341..577be0b 100644 --- a/api/media/upload.mjs +++ b/api/media/upload.mjs @@ -8,9 +8,11 @@ export function upload(file) { let token = client.createJwt({ iss: media.iss }, media.secret) let out = { - small: {}, - medium: {}, - large: {}, + sizes: { + small: {}, + medium: {}, + large: {}, + } } return client.upload(media.path + '?token=' + token, { file: { @@ -30,9 +32,11 @@ export function upload(file) { out.filename = res.filename out.path = res.path out.preview = res.preview - out.small.avif = res.small - out.medium.avif = res.medium - out.large.avif = res.large + out.sizes.small.avif = res.small + out.sizes.medium.avif = res.medium + out.sizes.large.avif = res.large + out.size = file.size + out.type = file.type return client.post(media.path + '/' + out.filename + '?token=' + token, { small: media.small.jpeg, @@ -40,9 +44,9 @@ export function upload(file) { large: media.large.jpeg, }) .then(res => { - out.small.jpeg = res.small - out.medium.jpeg = res.medium - out.large.jpeg = res.large + out.sizes.small.jpeg = res.small + out.sizes.medium.jpeg = res.medium + out.sizes.large.jpeg = res.large }) }) .then(() => { diff --git a/app/admin.scss b/app/admin.scss index 003daec..4a82d1f 100644 --- a/app/admin.scss +++ b/app/admin.scss @@ -18,6 +18,7 @@ $headtext: $primary-light-fg; border-collapse: collapse; border-spacing: 0; font-size: 0.8em; + position: relative; thead th { background-color: $headcolor; @@ -65,9 +66,49 @@ $headtext: $primary-light-fg; align-items: center; } +.input-group {} + +.input-row { + display: flex; + + & > * { + margin-right: 1rem; + flex: 2 1 auto; + } + + & > .small { + flex: 0 0 auto; + } + + & > *:last-child { + margin-right: 0; + } +} + +.admin-wrapper .loading-spinner { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #00000066; + z-index: 1000; +} + @import 'admin/admin'; @import 'widgets/admin'; +.codex-editor { + margin-bottom: 0.5rem; +} + +input[type=checkbox] { + display: block; + height: 20px; + margin: 0.5rem 0; + width: 20px; +} + .darkmodeon { .maincontainer .admin-wrapper { color: $main-fg; diff --git a/app/admin/admin.scss b/app/admin/admin.scss index 4d34347..188f6ac 100644 --- a/app/admin/admin.scss +++ b/app/admin/admin.scss @@ -52,6 +52,7 @@ -moz-user-select: none; -ms-user-select: none; -o-user-select: none; + z-index: 10; /* user-select: none; */ } .cal-header, .cal-row { diff --git a/app/admin/articles.scss b/app/admin/articles.scss index 49a8064..f08320d 100644 --- a/app/admin/articles.scss +++ b/app/admin/articles.scss @@ -34,7 +34,7 @@ article.editarticle { } form { - padding: 0 40px 20px; + padding: 0 2rem 1rem; textarea { height: 300px; @@ -83,15 +83,16 @@ article.editarticle { } .fileupload { - align-self: center; - padding: 0.5em; - margin: 0.5em 0; - min-width: 250px; + align-self: flex-start; + padding: 0.5rem; + margin: 0.5rem 0 0.5rem 2rem; + min-width: 150px; border: none; border: 1px solid $secondary-bg; background: $secondary-light-bg; color: $secondary-light-fg; position: relative; + text-align: center; input { position: absolute; @@ -112,7 +113,7 @@ article.editarticle { width: 100%; display: flex; flex-direction: column; - padding: 10px 40px 0; + padding: 1rem 2rem 0; text-align: left; h4 { diff --git a/app/admin/editarticle.js b/app/admin/editarticle.js index 9fbdd4f..8e76abf 100644 --- a/app/admin/editarticle.js +++ b/app/admin/editarticle.js @@ -8,8 +8,6 @@ const Editor = require('./editor') const EditArticle = { oninit: function(vnode) { - this.editor = null - this.loading = false this.showLoading = null this.data = { @@ -44,6 +42,20 @@ 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, + })) + }, + + requestArticle: function(data) { this.error = '' if (this.showLoading) { @@ -60,12 +72,8 @@ const EditArticle = { this.loading = true } - return common.sendRequest({ - method: 'GET', - url: '/api/auth/articles/' + this.lastid, - }) + data .then((result) => { - console.log('result', result) this.data = result if (this.data.article) { this.data.article.publish_at = new Date(this.data.article.publish_at) @@ -85,7 +93,6 @@ const EditArticle = { }, updateValue: function(name, e) { - console.log(name, e.currentTarget.value) if (name === 'is_featured') { this.data.article[name] = e.currentTarget.checked } else { @@ -120,7 +127,11 @@ const EditArticle = { save: function(vnode, e) { e.preventDefault() - console.log(this.data) + + let id = this.lastid + if (id === 'add') { + id = '0' + } let formData = new FormData() if (this.newBanner) { @@ -133,93 +144,27 @@ const EditArticle = { formData.append('id', this.data.article.id) } - formData.append('admin_id', this.data.article.admin_id) + formData.append('admin_id', this.data.article.admin_id || this.data.staff[0].id) formData.append('name', this.data.article.name) - formData.append('content', this.data.article.content) - formData.append('is_featured', this.data.article.is_featured) + formData.append('is_featured', this.data.article.is_featured || false) formData.append('path', this.data.article.path) - formData.append('page_id', this.data.article.page_id) - formData.append('publish_at', this.data.article.publish_at) + formData.append('page_id', this.data.article.page_id || null) + formData.append('publish_at', this.dateInstance.inputElem.value.replace(', ', 'T') + 'Z') this.loading = true - common.sendRequest({ - method: 'PUT', - url: '/api/auth/articles/' + this.lastid, - body: formData, - }) - .then((result) => { - console.log('result', result) - }, (err) => { - this.error = err.message - }) - .then(() => { - this.loading = false - m.redraw() - }) - /*e.preventDefault() - 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 + this.requestArticle( + this.editor.save() + .then(body => { + formData.append('content', JSON.stringify(body)) - this.data.article.description = vnode.state.froala && vnode.state.froala.html.get() || this.data.article.description - if (this.data.article.description) { - this.data.article.description = this.data.article.description.replace(/]+data-f-id="pbf"[^>]+>[^>]+>[^>]+>[^>]+>/, '') - } - - this.loading = true - - let promise - - if (this.data.article.id) { - promise = Article.updateArticle(this.data.article.id, { - name: this.data.article.name, - path: this.data.article.path, - page_id: this.data.article.page_id, - description: this.data.article.description, - banner_id: this.data.article.banner && this.data.article.banner.id, - media_id: this.data.article.media && this.data.article.media.id, - publish_at: new Date(this.data.article.publish_at), - is_featured: this.data.article.is_featured, - staff_id: this.data.article.staff_id, + return common.sendRequest({ + method: 'PUT', + url: '/api/auth/articles/' + id, + body: formData, + }) }) - } else { - promise = Article.createArticle({ - name: this.data.article.name, - path: this.data.article.path, - page_id: this.data.article.page_id, - description: this.data.article.description, - banner_id: this.data.article.banner && this.data.article.banner.id, - media_id: this.data.article.media && this.data.article.media.id, - publish_at: new Date(this.data.article.publish_at), - is_featured: this.data.article.is_featured, - staff_id: this.data.article.staff_id, - }) - } - - promise.then(function(res) { - if (vnode.state.article.id) { - res.media = vnode.state.article.media - res.banner = vnode.state.article.banner - res.files = vnode.state.article.files - vnode.state.article = res - EditArticle.parsePublishedAt(vnode, null) - } else { - m.route.set('/admin/articles/' + res.id) - } - }) - .catch(function(err) { - vnode.state.error = err.message - }) - .then(function() { - vnode.state.loading = false - m.redraw() - })*/ + ) }, uploadFile: function(vnode, e) { @@ -230,13 +175,22 @@ const EditArticle = { const showPublish = this.data.article ? this.data.article.publish_at > new Date() : false + const bannerImage = this.data.article && this.data.article.banner_prefix + ? this.data.article.banner_prefix + '_large.avif' + : null + const mediaImage = this.data.article && this.data.article.media_prefix + ? this.data.article.media_prefix + '_large.avif' + : null return [ - this.loading ? - m('div.loading-spinner') - : null, + this.loading && !this.data.article + ? m('div.admin-spinner.loading-spinner') + : null, this.data.article ? m('div.admin-wrapper', [ + this.loading + ? m('div.loading-spinner') + : null, m('div.admin-actions', this.data.article.id ? [ m('span', 'Actions:'), @@ -253,14 +207,14 @@ const EditArticle = { height: 300, onfile: this.mediaUploaded.bind(this, 'banner'), ondelete: this.mediaRemoved.bind(this, 'banner'), - media: this.data.article && this.data.article.banner, + media: bannerImage, }), m(FileUpload, { class: 'cover', useimg: true, onfile: this.mediaUploaded.bind(this, 'media'), ondelete: this.mediaRemoved.bind(this, 'media'), - media: this.data.article && this.data.article.media, + media: mediaImage, }), m('form.editarticle.content', { onsubmit: this.save.bind(this, vnode), @@ -274,53 +228,71 @@ const EditArticle = { selected: item.id === this.data.article.page_id }, item.name) })), - m('label', 'Name'), - m('input', { - type: 'text', - value: this.data.article.name, - oninput: this.updateValue.bind(this, 'name'), - }), - m('label.slim', 'Path'), - m('input.slim', { - type: 'text', - value: this.data.article.path, - oninput: this.updateValue.bind(this, 'path'), - }), + m('div.input-row', [ + m('div.input-group', [ + m('label', 'Name'), + m('input', { + type: 'text', + value: this.data.article.name, + oninput: this.updateValue.bind(this, 'name'), + }), + ]), + m('div.input-group', [ + m('label', 'Path'), + m('input', { + type: 'text', + value: this.data.article.path, + oninput: this.updateValue.bind(this, 'path'), + }), + ]), + ]), m('label', 'Description'), m(Editor, { - + oncreate: (subnode) => { + this.editor = subnode.state.editor + }, + contentdata: this.data.article.content, }), - m('label', 'Published at'), - m('input', { - type: 'text', - oncreate: (div) => { - if (!this.dateInstance) { - this.dateInstance = new dtsel.DTS(div.dom, { - dateFormat: 'yyyy-mm-dd', - timeFormat: 'HH:MM:SS', - showTime: true, + m('div.input-row', [ + m('div.input-group', [ + m('label', 'Published at'), + m('input', { + type: 'text', + oncreate: (div) => { + if (!this.dateInstance) { + this.dateInstance = new dtsel.DTS(div.dom, { + dateFormat: 'yyyy-mm-dd', + timeFormat: 'HH:MM:SS', + showTime: true, + }) + window.temp = this.dateInstance + } + }, + value: this.data.article.publish_at.toISOString().replace('T', ', ').split('.')[0], + }), + ]), + m('div.input-group', [ + m('label', 'Published by'), + m('select', { + onchange: this.updateStaffer.bind(this), + }, + this.data.staff.map((item) => { + return m('option', { + value: item.id, + selected: item.id === this.data.article.staff_id + }, item.name) }) - } - }, - value: this.data.article.publish_at.toISOString().replace('T', ', ').split('.')[0], - }), - m('label', 'Published by'), - m('select', { - onchange: this.updateStaffer.bind(this), - }, - this.data.staff.map((item) => { - return m('option', { - value: item.id, - selected: item.id === this.data.article.staff_id - }, item.name) - }) - ), - m('label', 'Make featured'), - m('input', { - type: 'checkbox', - checked: this.data.article.is_featured, - oninput: this.updateValue.bind(this, 'is_featured'), - }), + ), + ]), + m('div.input-group.small', [ + m('label', 'Make featured'), + m('input', { + type: 'checkbox', + checked: this.data.article.is_featured, + oninput: this.updateValue.bind(this, 'is_featured'), + }), + ]), + ]), m('div', [ m('input', { type: 'submit', @@ -356,7 +328,10 @@ const EditArticle = { : null, ]), ]) - : null, + : m('div.error', { + hidden: !this.error, + onclick: () => { this.fetchArticle(vnode) }, + }, this.error),, ] }, } diff --git a/app/admin/editor.js b/app/admin/editor.js index 9799cad..0135c4d 100644 --- a/app/admin/editor.js +++ b/app/admin/editor.js @@ -1,6 +1,7 @@ const Editor = { oninit: function(vnode) { this.editor = null + this.lastData = null }, oncreate: function(vnode) { @@ -25,8 +26,17 @@ const Editor = { }, delimiter: window.Delimiter, htmlraw: window.RawTool, - }, + }, + data: vnode.attrs.contentdata, }) + this.lastData = vnode.attrs.contentdata + }, + + onupdate: function(vnode) { + if (this.lastData !== vnode.attrs.contentdata) { + this.lastData = vnode.attrs.contentdata + this.editor.render(this.lastData) + } }, view: function(vnode) { diff --git a/app/app.scss b/app/app.scss index 62118ef..5ed100a 100644 --- a/app/app.scss +++ b/app/app.scss @@ -52,6 +52,7 @@ img { border: 2px solid #ccc; border-top-color: #333; animation: spinner-loader .6s linear infinite; + z-index: 1000; } .maincontainer {