Development

This commit is contained in:
Jonatan Nilsson 2022-07-22 11:18:33 +00:00
parent bd2e0d0fff
commit ebd3cd4d26
8 changed files with 335 additions and 174 deletions

View file

@ -1,4 +1,5 @@
import { parseFiles } from '../file/util.mjs' import { parseFiles } from '../file/util.mjs'
import { parseArticles, parseArticle } from './util.mjs'
import { upload } from '../media/upload.mjs' import { upload } from '../media/upload.mjs'
export default class ArticleRoutes { 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 res = await ctx.db.safeCallProc('article_get_single', [ctx.params.path])
let out = { let out = {
article: res.results[0][0] || null, article: parseArticle(res.results[0][0]),
files: parseFiles(res.results[1]), files: parseFiles(res.results[1]),
} }
@ -29,33 +30,124 @@ export default class ArticleRoutes {
]) ])
let out = { let out = {
articles: res.results[0], articles: parseArticles(res.results[0]),
total_articles: res.results[0][0].total_articles, total_articles: res.results[0][0].total_articles,
} }
ctx.body = out 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 */ /** GET: /api/auth/articles/:path */
async auth_getSingleArticle(ctx) { auth_getSingleArticle(ctx) {
let res = await ctx.db.safeCallProc('article_auth_get_update_create', [ return this.private_getUpdateArticle(ctx)
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
} }
/** PUT: /api/auth/articles/:path */ /** PUT: /api/auth/articles/:path */
async auth_updateCreateSingleArticle(ctx) { async auth_updateCreateSingleArticle(ctx) {
console.log(ctx.req.files)
console.log(ctx.req.body) console.log(ctx.req.body)
let newBanner = null let newBanner = null
@ -78,9 +170,45 @@ export default class ArticleRoutes {
await Promise.all(promises) await Promise.all(promises)
console.log(newBanner) return this.private_getUpdateArticle(ctx, ctx.req.body, newBanner, newMedia)
console.log(newMedia) }
ctx.body = {} /** 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
} }
} }

View file

@ -8,9 +8,11 @@ export function upload(file) {
let token = client.createJwt({ iss: media.iss }, media.secret) let token = client.createJwt({ iss: media.iss }, media.secret)
let out = { let out = {
small: {}, sizes: {
medium: {}, small: {},
large: {}, medium: {},
large: {},
}
} }
return client.upload(media.path + '?token=' + token, { file: { return client.upload(media.path + '?token=' + token, { file: {
@ -30,9 +32,11 @@ export function upload(file) {
out.filename = res.filename out.filename = res.filename
out.path = res.path out.path = res.path
out.preview = res.preview out.preview = res.preview
out.small.avif = res.small out.sizes.small.avif = res.small
out.medium.avif = res.medium out.sizes.medium.avif = res.medium
out.large.avif = res.large out.sizes.large.avif = res.large
out.size = file.size
out.type = file.type
return client.post(media.path + '/' + out.filename + '?token=' + token, { return client.post(media.path + '/' + out.filename + '?token=' + token, {
small: media.small.jpeg, small: media.small.jpeg,
@ -40,9 +44,9 @@ export function upload(file) {
large: media.large.jpeg, large: media.large.jpeg,
}) })
.then(res => { .then(res => {
out.small.jpeg = res.small out.sizes.small.jpeg = res.small
out.medium.jpeg = res.medium out.sizes.medium.jpeg = res.medium
out.large.jpeg = res.large out.sizes.large.jpeg = res.large
}) })
}) })
.then(() => { .then(() => {

View file

@ -18,6 +18,7 @@ $headtext: $primary-light-fg;
border-collapse: collapse; border-collapse: collapse;
border-spacing: 0; border-spacing: 0;
font-size: 0.8em; font-size: 0.8em;
position: relative;
thead th { thead th {
background-color: $headcolor; background-color: $headcolor;
@ -65,9 +66,49 @@ $headtext: $primary-light-fg;
align-items: center; 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 'admin/admin';
@import 'widgets/admin'; @import 'widgets/admin';
.codex-editor {
margin-bottom: 0.5rem;
}
input[type=checkbox] {
display: block;
height: 20px;
margin: 0.5rem 0;
width: 20px;
}
.darkmodeon { .darkmodeon {
.maincontainer .admin-wrapper { .maincontainer .admin-wrapper {
color: $main-fg; color: $main-fg;

View file

@ -52,6 +52,7 @@
-moz-user-select: none; -moz-user-select: none;
-ms-user-select: none; -ms-user-select: none;
-o-user-select: none; -o-user-select: none;
z-index: 10;
/* user-select: none; */ /* user-select: none; */
} }
.cal-header, .cal-row { .cal-header, .cal-row {

View file

@ -34,7 +34,7 @@ article.editarticle {
} }
form { form {
padding: 0 40px 20px; padding: 0 2rem 1rem;
textarea { textarea {
height: 300px; height: 300px;
@ -83,15 +83,16 @@ article.editarticle {
} }
.fileupload { .fileupload {
align-self: center; align-self: flex-start;
padding: 0.5em; padding: 0.5rem;
margin: 0.5em 0; margin: 0.5rem 0 0.5rem 2rem;
min-width: 250px; min-width: 150px;
border: none; border: none;
border: 1px solid $secondary-bg; border: 1px solid $secondary-bg;
background: $secondary-light-bg; background: $secondary-light-bg;
color: $secondary-light-fg; color: $secondary-light-fg;
position: relative; position: relative;
text-align: center;
input { input {
position: absolute; position: absolute;
@ -112,7 +113,7 @@ article.editarticle {
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 10px 40px 0; padding: 1rem 2rem 0;
text-align: left; text-align: left;
h4 { h4 {

View file

@ -8,8 +8,6 @@ const Editor = require('./editor')
const EditArticle = { const EditArticle = {
oninit: function(vnode) { oninit: function(vnode) {
this.editor = null
this.loading = false this.loading = false
this.showLoading = null this.showLoading = null
this.data = { this.data = {
@ -44,6 +42,20 @@ const EditArticle = {
fetchArticle: function(vnode) { fetchArticle: function(vnode) {
this.lastid = m.route.param('id') 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 = '' this.error = ''
if (this.showLoading) { if (this.showLoading) {
@ -60,12 +72,8 @@ const EditArticle = {
this.loading = true this.loading = true
} }
return common.sendRequest({ data
method: 'GET',
url: '/api/auth/articles/' + this.lastid,
})
.then((result) => { .then((result) => {
console.log('result', result)
this.data = result this.data = result
if (this.data.article) { 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)
@ -85,7 +93,6 @@ const EditArticle = {
}, },
updateValue: function(name, e) { updateValue: function(name, e) {
console.log(name, e.currentTarget.value)
if (name === 'is_featured') { if (name === 'is_featured') {
this.data.article[name] = e.currentTarget.checked this.data.article[name] = e.currentTarget.checked
} else { } else {
@ -120,7 +127,11 @@ const EditArticle = {
save: function(vnode, e) { save: function(vnode, e) {
e.preventDefault() e.preventDefault()
console.log(this.data)
let id = this.lastid
if (id === 'add') {
id = '0'
}
let formData = new FormData() let formData = new FormData()
if (this.newBanner) { if (this.newBanner) {
@ -133,93 +144,27 @@ const EditArticle = {
formData.append('id', this.data.article.id) 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('name', this.data.article.name)
formData.append('content', this.data.article.content) formData.append('is_featured', this.data.article.is_featured || false)
formData.append('is_featured', this.data.article.is_featured)
formData.append('path', this.data.article.path) formData.append('path', this.data.article.path)
formData.append('page_id', this.data.article.page_id) formData.append('page_id', this.data.article.page_id || null)
formData.append('publish_at', this.data.article.publish_at) formData.append('publish_at', this.dateInstance.inputElem.value.replace(', ', 'T') + 'Z')
this.loading = true this.loading = true
common.sendRequest({ this.requestArticle(
method: 'PUT', this.editor.save()
url: '/api/auth/articles/' + this.lastid, .then(body => {
body: formData, formData.append('content', JSON.stringify(body))
})
.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.data.article.description = vnode.state.froala && vnode.state.froala.html.get() || this.data.article.description return common.sendRequest({
if (this.data.article.description) { method: 'PUT',
this.data.article.description = this.data.article.description.replace(/<p[^>]+data-f-id="pbf"[^>]+>[^>]+>[^>]+>[^>]+>/, '') url: '/api/auth/articles/' + id,
} body: formData,
})
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,
}) })
} 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) { uploadFile: function(vnode, e) {
@ -230,13 +175,22 @@ const EditArticle = {
const showPublish = this.data.article const showPublish = this.data.article
? this.data.article.publish_at > new Date() ? this.data.article.publish_at > new Date()
: false : 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 [ return [
this.loading ? this.loading && !this.data.article
m('div.loading-spinner') ? m('div.admin-spinner.loading-spinner')
: null, : null,
this.data.article this.data.article
? m('div.admin-wrapper', [ ? m('div.admin-wrapper', [
this.loading
? m('div.loading-spinner')
: null,
m('div.admin-actions', this.data.article.id m('div.admin-actions', this.data.article.id
? [ ? [
m('span', 'Actions:'), m('span', 'Actions:'),
@ -253,14 +207,14 @@ const EditArticle = {
height: 300, height: 300,
onfile: this.mediaUploaded.bind(this, 'banner'), onfile: this.mediaUploaded.bind(this, 'banner'),
ondelete: this.mediaRemoved.bind(this, 'banner'), ondelete: this.mediaRemoved.bind(this, 'banner'),
media: this.data.article && this.data.article.banner, media: bannerImage,
}), }),
m(FileUpload, { m(FileUpload, {
class: 'cover', class: 'cover',
useimg: true, useimg: true,
onfile: this.mediaUploaded.bind(this, 'media'), onfile: this.mediaUploaded.bind(this, 'media'),
ondelete: this.mediaRemoved.bind(this, 'media'), ondelete: this.mediaRemoved.bind(this, 'media'),
media: this.data.article && this.data.article.media, media: mediaImage,
}), }),
m('form.editarticle.content', { m('form.editarticle.content', {
onsubmit: this.save.bind(this, vnode), onsubmit: this.save.bind(this, vnode),
@ -274,53 +228,71 @@ const EditArticle = {
selected: item.id === this.data.article.page_id selected: item.id === this.data.article.page_id
}, item.name) }, item.name)
})), })),
m('label', 'Name'), m('div.input-row', [
m('input', { m('div.input-group', [
type: 'text', m('label', 'Name'),
value: this.data.article.name, m('input', {
oninput: this.updateValue.bind(this, 'name'), type: 'text',
}), value: this.data.article.name,
m('label.slim', 'Path'), oninput: this.updateValue.bind(this, 'name'),
m('input.slim', { }),
type: 'text', ]),
value: this.data.article.path, m('div.input-group', [
oninput: this.updateValue.bind(this, 'path'), m('label', 'Path'),
}), m('input', {
type: 'text',
value: this.data.article.path,
oninput: this.updateValue.bind(this, 'path'),
}),
]),
]),
m('label', 'Description'), m('label', 'Description'),
m(Editor, { m(Editor, {
oncreate: (subnode) => {
this.editor = subnode.state.editor
},
contentdata: this.data.article.content,
}), }),
m('label', 'Published at'), m('div.input-row', [
m('input', { m('div.input-group', [
type: 'text', m('label', 'Published at'),
oncreate: (div) => { m('input', {
if (!this.dateInstance) { type: 'text',
this.dateInstance = new dtsel.DTS(div.dom, { oncreate: (div) => {
dateFormat: 'yyyy-mm-dd', if (!this.dateInstance) {
timeFormat: 'HH:MM:SS', this.dateInstance = new dtsel.DTS(div.dom, {
showTime: true, 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('div.input-group.small', [
}), m('label', 'Make featured'),
m('label', 'Published by'), m('input', {
m('select', { type: 'checkbox',
onchange: this.updateStaffer.bind(this), checked: this.data.article.is_featured,
}, oninput: this.updateValue.bind(this, 'is_featured'),
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', [ m('div', [
m('input', { m('input', {
type: 'submit', type: 'submit',
@ -356,7 +328,10 @@ const EditArticle = {
: null, : null,
]), ]),
]) ])
: null, : m('div.error', {
hidden: !this.error,
onclick: () => { this.fetchArticle(vnode) },
}, this.error),,
] ]
}, },
} }

View file

@ -1,6 +1,7 @@
const Editor = { const Editor = {
oninit: function(vnode) { oninit: function(vnode) {
this.editor = null this.editor = null
this.lastData = null
}, },
oncreate: function(vnode) { oncreate: function(vnode) {
@ -25,8 +26,17 @@ const Editor = {
}, },
delimiter: window.Delimiter, delimiter: window.Delimiter,
htmlraw: window.RawTool, 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) { view: function(vnode) {

View file

@ -52,6 +52,7 @@ img {
border: 2px solid #ccc; border: 2px solid #ccc;
border-top-color: #333; border-top-color: #333;
animation: spinner-loader .6s linear infinite; animation: spinner-loader .6s linear infinite;
z-index: 1000;
} }
.maincontainer { .maincontainer {