Finished base articles and pages

This commit is contained in:
Jonatan Nilsson 2022-07-27 08:41:18 +00:00
parent 8e0b4c29e1
commit f12b440c19
30 changed files with 850 additions and 580 deletions

View file

@ -1,6 +1,7 @@
import { parseFiles } from '../file/util.mjs' import { parseFiles } from '../file/util.mjs'
import { parseArticles, parseArticle } from './util.mjs' import { parseArticles, parseArticle } from './util.mjs'
import { upload } from '../media/upload.mjs' import { upload } from '../media/upload.mjs'
import { mediaToDatabase } from '../media/util.mjs'
export default class ArticleRoutes { export default class ArticleRoutes {
constructor(opts = {}) { constructor(opts = {}) {
@ -11,7 +12,7 @@ export default class ArticleRoutes {
/** GET: /api/articles/[path] */ /** GET: /api/articles/[path] */
async getArticle(ctx) { 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 = { let out = {
article: parseArticle(res.results[0][0]), article: parseArticle(res.results[0][0]),
@ -23,7 +24,7 @@ export default class ArticleRoutes {
/** GET: /api/auth/articles */ /** GET: /api/auth/articles */
async auth_getAllArticles(ctx) { 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, ctx.state.auth_token,
Math.max(ctx.query.get('page') || 1, 1), 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)
@ -40,7 +41,7 @@ export default class ArticleRoutes {
async private_getUpdateArticle(ctx, body = null, banner = null, media = null) { async private_getUpdateArticle(ctx, body = null, banner = null, media = null) {
let params = [ let params = [
ctx.state.auth_token, ctx.state.auth_token,
ctx.params.path ctx.params.id === '0' ? null : ctx.params.id
] ]
if (body) { if (body) {
params = params.concat([ params = params.concat([
@ -53,100 +54,28 @@ export default class ArticleRoutes {
body.is_featured === 'true' ? 1 : 0, body.is_featured === 'true' ? 1 : 0,
0, 0,
]) ])
if (banner) { params = params.concat(mediaToDatabase(banner, body.remove_banner === 'true'))
params = params.concat([ params = params.concat(mediaToDatabase(media, body.remove_media === 'true'))
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) 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 = { let out = {
article: parseArticle(res.results[0][0]), article: parseArticle(res.results[0][0]) || { publish_at: new Date() },
files: parseFiles(res.results[1]), files: parseFiles(res.results[1]),
staff: res.results[2], 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 ctx.body = out
} }
/** GET: /api/auth/articles/:path */ /** GET: /api/auth/articles/:id */
auth_getSingleArticle(ctx) { auth_getSingleArticle(ctx) {
return this.private_getUpdateArticle(ctx) return this.private_getUpdateArticle(ctx)
} }
/** PUT: /api/auth/articles/:path */ /** PUT: /api/auth/articles/:id */
async auth_updateCreateSingleArticle(ctx) { async auth_updateCreateSingleArticle(ctx) {
console.log(ctx.req.body) console.log(ctx.req.body)
@ -173,11 +102,11 @@ export default class ArticleRoutes {
return this.private_getUpdateArticle(ctx, ctx.req.body, newBanner, newMedia) return this.private_getUpdateArticle(ctx, ctx.req.body, newBanner, newMedia)
} }
/** DELETE: /api/auth/articles/:path */ /** DELETE: /api/auth/articles/:id */
async auth_removeSingleArticle(ctx) { async auth_removeSingleArticle(ctx) {
let params = [ let params = [
ctx.state.auth_token, ctx.state.auth_token,
ctx.params.path, ctx.params.id,
// Article data // Article data
null, null,
null, null,
@ -187,25 +116,9 @@ export default class ArticleRoutes {
null, null,
null, null,
1, 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) await ctx.db.safeCallProc('article_auth_get_update_create', params)

View file

@ -1,3 +1,5 @@
import { parseFile } from '../file/util.mjs'
export function parseArticles(articles) { export function parseArticles(articles) {
for (let i = 0; i < articles.length; i++) { for (let i = 0; i < articles.length; i++) {
parseArticle(articles[i]) parseArticle(articles[i])
@ -5,17 +7,52 @@ export function parseArticles(articles) {
return 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) { export function parseArticle(article) {
if (!article) { if (!article) {
return null 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) { if (article.banner_path) {
article.banner_path = 'https://cdn.nfp.is' + 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) { if (article.media_path) {
article.media_path = 'https://cdn.nfp.is' + 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 return article
} }

View file

@ -13,7 +13,7 @@ export default class AuthenticationRoutes {
/** GET: /api/authentication/login */ /** GET: /api/authentication/login */
async login(ctx) { 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.email,
ctx.req.body.password, ctx.req.body.password,
]) ])

View file

@ -43,7 +43,7 @@ export function initPool(core, config) {
return { return {
safeCallProc: function(name, params, options) { safeCallProc: function(name, params, options) {
if (name.indexOf('.') < 0) { if (name.indexOf('.') < 0) {
name = config.schema + '.' + name name = 'common.' + name
} }
return pool.promises.callProc(name, params, options) return pool.promises.callProc(name, params, options)
.catch(function(err) { .catch(function(err) {

25
api/media/util.mjs Normal file
View file

@ -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,
]
}
}

View file

@ -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
}

View file

@ -1,26 +1,131 @@
import * as Page from './model.mjs' import { parsePagesToTree } from './util.mjs'
import * as security from './security.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 { export default class PageRoutes {
constructor(opts = {}) { constructor(opts = {}) {
Object.assign(this, { Object.assign(this, {
Page: opts.Page || Page, upload: upload,
security: opts.security || security,
}) })
} }
/** GET: /api/pagetree */ /** GET: /api/pagetree */
async getPageTree(ctx) { async getPageTree(ctx, onlyReturn = false) {
ctx.body = await this.Page.getTree(ctx) let res = await ctx.db.safeCallProc('pages_get_tree', [])
if (onlyReturn) {
return parsePagesToTree(res.first)
}
ctx.body = parsePagesToTree(res.first)
} }
/** GET: /api/pages/[path] */ /** GET: /api/pages/[path] */
async getPage(ctx) { async getPage(ctx) {
ctx.body = await this.Page.getPage( let res = await ctx.db.safeCallProc('pages_get_single', [
ctx,
ctx.params.path || null, ctx.params.path || null,
Math.max(ctx.query.get('page') || 1, 1), 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
} }
} }

View file

23
api/page/util.mjs Normal file
View file

@ -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
}
}

View file

@ -4,11 +4,10 @@ import { FileResponse, HttpError } from 'flaska'
import fs from 'fs/promises' import fs from 'fs/promises'
import fsSync from 'fs' import fsSync from 'fs'
import { getTree } from './page/model.mjs'
export default class ServeHandler { export default class ServeHandler {
constructor(opts = {}) { constructor(opts = {}) {
Object.assign(this, { Object.assign(this, {
pageRoutes: opts.pageRoutes,
fs: opts.fs || fs, fs: opts.fs || fs,
fsSync: opts.fsSync || fsSync, fsSync: opts.fsSync || fsSync,
root: opts.root, root: opts.root,
@ -66,7 +65,7 @@ export default class ServeHandler {
} }
try { try {
payload.payloadTree = JSON.stringify(await getTree(ctx)) payload.payloadTree = JSON.stringify(await this.pageRoutes.getPageTree(ctx, true))
} catch (e) { } catch (e) {
ctx.log.error(e) ctx.log.error(e)
} }

View file

@ -21,7 +21,7 @@ export function run(http, port, core) {
nonce: ['script-src'], nonce: ['script-src'],
nonceCacheLength: 50, nonceCacheLength: 50,
}, http) }, http)
// Create our database pool // Create our database pool
let pool = initPool(core, config.get('mssql')) 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/pagetree', page.getPageTree.bind(page))
flaska.get('/api/frontpage', page.getPage.bind(page)) flaska.get('/api/frontpage', page.getPage.bind(page))
flaska.get('/api/pages/:path', 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() const article = new ArticleRoutes()
flaska.get('/api/articles/:path', article.getArticle.bind(article)) 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', authenticate(), article.auth_getAllArticles.bind(article))
flaska.get('/api/auth/articles/:path', authenticate(), article.auth_getSingleArticle.bind(article)) flaska.get('/api/auth/articles/:id', authenticate(), article.auth_getSingleArticle.bind(article))
flaska.put('/api/auth/articles/:path', [authenticate(), FormidableHandler(formidable) ], article.auth_updateCreateSingleArticle.bind(article)) flaska.put('/api/auth/articles/:id', [
// flaska.get('/api/pages/:pageId/articles/public', article.getPublicAllPageArticles.bind(article)) 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() const authentication = new AuthenticationRoutes()
flaska.post('/api/authentication/login', JsonHandler(), authentication.login.bind(authentication)) flaska.post('/api/authentication/login', JsonHandler(), authentication.login.bind(authentication))
const serve = new ServeHandler({ const serve = new ServeHandler({
pageRoutes: page,
root: localUtil.getPathFromRoot('../public'), root: localUtil.getPathFromRoot('../public'),
version: core.app.running, version: core.app.running,
frontend: config.get('frontend:url'), frontend: config.get('frontend:url'),

View file

@ -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-bg: #01579b;
$primary-fg: white; $primary-fg: white;

View file

@ -52,7 +52,6 @@ const AdminArticles = {
url: '/api/auth/articles?page=' + (this.lastpage || 1), url: '/api/auth/articles?page=' + (this.lastpage || 1),
}) })
.then((result) => { .then((result) => {
console.log(result)
this.data = result this.data = result
this.data.articles.forEach((article) => { this.data.articles.forEach((article) => {
@ -75,13 +74,20 @@ const AdminArticles = {
let removingArticle = this.removeArticle let removingArticle = this.removeArticle
this.removeArticle = null this.removeArticle = null
this.loading = true this.loading = true
Article.removeArticle(removingArticle, removingArticle.id) m.redraw()
.then(this.oninit.bind(this, vnode))
.catch(function(err) { return common.sendRequest({
vnode.state.error = err.message method: 'DELETE',
vnode.state.loading = false url: '/api/auth/articles/' + removingArticle.id,
})
.then(
() => this.fetchArticles(vnode),
(err) => {
this.error = err.message
this.loading = false
m.redraw() m.redraw()
}) }
)
}, },
drawArticle: function(vnode, article) { drawArticle: function(vnode, article) {
@ -93,7 +99,7 @@ const AdminArticles = {
? 'rowfeatured' ? '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.page_path }, article.page_name)),
m('td', m(m.route.Link, { href: '/article/' + article.path }, '/article/' + article.path)), m('td', m(m.route.Link, { href: '/article/' + article.path }, '/article/' + article.path)),
m('td.right', article.publish_at.replace('T', ' ').split('.')[0]), m('td.right', article.publish_at.replace('T', ' ').split('.')[0]),

View file

@ -16,7 +16,7 @@ const EditArticle = {
staff: [], staff: [],
} }
this.pages = [{id: null, name: 'Frontpage'}] this.pages = [{id: null, name: 'Frontpage'}]
this.addPageTree('', Page.Tree) this.pages = this.pages.concat(Page.getFlatTree())
this.newBanner = null this.newBanner = null
this.newMedia = null this.newMedia = null
this.dateInstance = null this.dateInstance = null
@ -25,15 +25,6 @@ const EditArticle = {
this.fetchArticle(vnode) 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) { onbeforeupdate: function(vnode) {
if (this.lastid !== m.route.param('id')) { if (this.lastid !== m.route.param('id')) {
this.fetchArticle(vnode) this.fetchArticle(vnode)
@ -42,16 +33,11 @@ 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( return this.requestArticle(
common.sendRequest({ common.sendRequest({
method: 'GET', method: 'GET',
url: '/api/auth/articles/' + id, url: '/api/auth/articles/' + (this.lastid === 'add' ? '0' : this.lastid),
})) }))
}, },
@ -75,9 +61,11 @@ const EditArticle = {
data data
.then((result) => { .then((result) => {
this.data = 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' document.title = 'Editing: ' + this.data.article.name + ' - Admin NFP Moe'
this.editedPath = true
} else { } else {
document.title = 'Create Article - Admin NFP Moe' document.title = 'Create Article - Admin NFP Moe'
} }
@ -122,16 +110,20 @@ const EditArticle = {
}, },
mediaRemoved: function(type) { mediaRemoved: function(type) {
this.data.article[type] = null this.data.article['remove_' + type] = true
this.data.article[type + '_prefix'] = null
}, },
save: function(vnode, e) { save: function(vnode, e) {
e.preventDefault() e.preventDefault()
if (!this.data.article.name) {
let id = this.lastid this.error = 'Name is missing'
if (id === 'add') { } else if (!this.data.article.path) {
id = '0' this.error = 'Path is missing'
} else {
this.error = ''
} }
if (this.error) return
let formData = new FormData() let formData = new FormData()
if (this.newBanner) { if (this.newBanner) {
@ -150,6 +142,8 @@ const EditArticle = {
formData.append('path', this.data.article.path) formData.append('path', this.data.article.path)
formData.append('page_id', this.data.article.page_id || null) formData.append('page_id', this.data.article.page_id || null)
formData.append('publish_at', this.dateInstance.inputElem.value.replace(', ', 'T') + 'Z') 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 this.loading = true
@ -160,10 +154,19 @@ const EditArticle = {
return common.sendRequest({ return common.sendRequest({
method: 'PUT', method: 'PUT',
url: '/api/auth/articles/' + id, url: '/api/auth/articles/' + (this.lastid === 'add' ? '0' : this.lastid),
body: formData, 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), : null),
m('article.editarticle', [ 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('header', m('h1', this.creating ? 'Create Article' : 'Edit ' + (this.data.article.name || '(untitled)'))),
m('div.error', { m('div.error', {
hidden: !this.error, hidden: !this.error,
@ -279,7 +286,7 @@ const EditArticle = {
this.data.staff.map((item) => { this.data.staff.map((item) => {
return m('option', { return m('option', {
value: item.id, value: item.id,
selected: item.id === this.data.article.staff_id selected: item.id === this.data.article.admin_id
}, item.name) }, item.name)
}) })
), ),
@ -293,7 +300,9 @@ const EditArticle = {
}), }),
]), ]),
]), ]),
m('div', [ m('div', {
hidden: !this.data.article.name || !this.data.article.path
}, [
m('input', { m('input', {
type: 'submit', type: 'submit',
value: 'Save', value: 'Save',

View file

@ -1,245 +1,261 @@
const Authentication = require('../authentication')
const FileUpload = require('../widgets/fileupload') const FileUpload = require('../widgets/fileupload')
const Froala = require('./froala') const Page = require('../api/page.p')
const Page = require('../api/page')
const common = require('../api/common')
const Editor = require('./editor')
const EditPage = { 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) { oninit: function(vnode) {
this.froala = null this.loading = false
this.loadedFroala = Froala.loadedFroala this.showLoading = null
this.data = {
if (!this.loadedFroala) { page: null,
Froala.createFroalaScript()
.then(function() {
vnode.state.loadedFroala = true
m.redraw()
})
} }
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) this.fetchPage(vnode)
}, },
onupdate: function(vnode) { onbeforeupdate: function(vnode) {
if (this.lastid !== m.route.param('id')) { if (this.lastid !== m.route.param('id')) {
this.fetchPage(vnode) this.fetchPage(vnode)
if (this.lastid === 'add') {
m.redraw()
}
} }
}, },
fetchPage: function(vnode) { fetchPage: function(vnode) {
this.lastid = m.route.param('id') 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') { return this.requestPage(
Page.getPage(this.lastid) common.sendRequest({
.then(function(result) { method: 'GET',
vnode.state.editedPath = true url: '/api/auth/pages/' + (this.lastid === 'add' ? '0' : this.lastid),
vnode.state.page = result }))
document.title = 'Editing: ' + result.name + ' - Admin NFP Moe' },
})
.catch(function(err) { requestPage: function(data) {
vnode.state.error = err.message this.error = ''
})
.then(function() { if (this.showLoading) {
vnode.state.loading = false clearTimeout(this.showLoading)
m.redraw()
})
} else {
document.title = 'Create Page - Admin NFP Moe'
} }
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) { updateValue: function(name, e) {
this.page[name] = e.currentTarget.value this.data.page[name] = e.currentTarget.value
if (name === 'path') { if (name === 'path') {
this.editedPath = true this.editedPath = true
} else if (name === 'name' && !this.editedPath) { } 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) { updateParent: function(e) {
this.page.parent_id = Number(e.currentTarget.value) this.data.page.parent_id = Number(e.currentTarget.value) || null
if (this.page.parent_id === -1) { },
this.page.parent_id = null
mediaUploaded: function(type, file) {
if (type === 'banner') {
this.newBanner = file
} else {
this.newMedia = file
} }
}, },
fileUploaded: function(type, media) { mediaRemoved: function(type) {
this.page[type] = media this.data.page['remove_' + type] = true
}, this.data.page[type + '_prefix'] = null
fileRemoved: function(type) {
this.page[type] = null
}, },
save: function(vnode, e) { save: function(vnode, e) {
e.preventDefault() e.preventDefault()
if (!this.page.name) { if (!this.data.page.name) {
this.error = 'Name is missing' this.error = 'Name is missing'
} else if (!this.page.path) { } else if (!this.data.page.path) {
this.error = 'Path is missing' this.error = 'Path is missing'
} else { } else {
this.error = '' this.error = ''
} }
if (this.error) return if (this.error) return
this.page.description = vnode.state.froala ? vnode.state.froala.html.get() : this.page.description let formData = new FormData()
if (this.page.description) { if (this.newBanner) {
this.page.description = this.page.description.replace(/<p[^>]+data-f-id="pbf"[^>]+>[^>]+>[^>]+>[^>]+>/, '') 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 this.loading = true
let promise this.requestPage(
this.editor.save()
.then(body => {
formData.append('content', JSON.stringify(body))
if (this.page.id) { return common.sendRequest({
promise = Page.updatePage(this.page.id, { method: 'PUT',
name: this.page.name, url: '/api/auth/pages/' + (this.lastid === 'add' ? '0' : this.lastid),
path: this.page.path, body: formData,
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,
}) })
} else { .then(data => {
promise = Page.createPage({ if (!data.page.id) {
name: this.page.name, throw new Error('Something went wrong with saving, try again later')
path: this.page.path, } else if (this.lastid === 'add') {
parent_id: this.page.parent_id, this.lastid = data.page.id.toString()
description: this.page.description, m.route.set('/admin/pages/' + data.page.id)
banner_id: this.page.banner && this.page.banner.id || null, }
media_id: this.page.media && this.page.media.id || null, 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) { 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}) const bannerImage = this.data.page && this.data.page.banner_prefix
return ( ? this.data.page.banner_prefix + '_large.avif'
this.loading ? : null
m('div.loading-spinner') const mediaImage = this.data.page && this.data.page.media_prefix
: m('div.admin-wrapper', [ ? this.data.page.media_prefix + '_large.avif'
m('div.admin-actions', this.page.id : 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('span', 'Actions:'),
m(m.route.Link, { href: '/page/' + this.page.path }, 'View page'), m(m.route.Link, { href: '/page/' + this.data.page.path }, 'View page'),
m(m.route.Link, { href: '/admin/pages/add' }, 'Create new page'),
] ]
: null), : null),
m('article.editpage', [ m('article.editarticle', [
m('header', m('h1', this.creating ? 'Create Page' : 'Edit ' + (this.page.name || '(untitled)'))), m('header', m('h1',
(this.data.page.id ? 'Edit ' : 'Create Page ') + (this.data.page.name || '(untitled)')
)
),
m('div.error', { m('div.error', {
hidden: !this.error, hidden: !this.error,
onclick: function() { vnode.state.error = '' }, onclick: () => { vnode.state.error = '' },
}, this.error), }, this.error),
m(FileUpload, { m(FileUpload, {
onupload: this.fileUploaded.bind(this, 'banner'), height: 300,
ondelete: this.fileRemoved.bind(this, 'banner'), onfile: this.mediaUploaded.bind(this, 'banner'),
onerror: function(e) { vnode.state.error = e }, ondelete: this.mediaRemoved.bind(this, 'banner'),
media: this.page && this.page.banner, media: bannerImage,
}), }),
m(FileUpload, { m(FileUpload, {
class: 'cover', class: 'cover',
useimg: true, useimg: true,
onupload: this.fileUploaded.bind(this, 'media'), onfile: this.mediaUploaded.bind(this, 'media'),
ondelete: this.fileRemoved.bind(this, 'media'), ondelete: this.mediaRemoved.bind(this, 'media'),
onerror: function(e) { vnode.state.error = e }, media: mediaImage,
media: this.page && this.page.media,
}), }),
m('form.editpage.content', { m('form.editarticle.content', {
onsubmit: this.save.bind(this, vnode), onsubmit: this.save.bind(this, vnode),
}, [ }, [
m('label', 'Parent'), m('label', 'Parent'),
m('select', { m('select', {
onchange: this.updateParent.bind(this), onchange: this.updateParent.bind(this),
}, parents.map(function(item) { }, this.pages.filter(item => !this.data.page || item.id !== this.data.page.id).map((item) => {
return m('option', { value: item.id || -1, selected: item.id === vnode.state.page.parent_id }, item.name) return m('option', {
})), value: item.id || 0,
m('label', 'Name'), selected: item.id === this.data.page.parent_id
m('input', { }, item.name)
type: 'text', })),
value: this.page.name, m('div.input-row', [
oninput: this.updateValue.bind(this, 'name'), 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'), m('label', 'Description'),
( m(Editor, {
this.loadedFroala ? oncreate: (subnode) => {
m('div', { this.editor = subnode.state.editor
oncreate: function(div) { },
vnode.state.froala = new FroalaEditor(div.dom, EditPage.getFroalaOptions(), function() { contentdata: this.data.page.content,
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('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),,
]
}, },
} }

View file

@ -1,40 +1,32 @@
const Page = require('../api/page') const Page = require('../api/page.p')
const Dialogue = require('../widgets/dialogue') const Dialogue = require('../widgets/dialogue')
const common = require('../api/common')
const AdminPages = { 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) { oninit: function(vnode) {
this.loading = true
this.error = '' this.error = ''
this.pages = [] this.pages = []
this.removePage = null this.removePage = null
document.title = 'Pages - Admin NFP Moe' document.title = 'Pages - Admin NFP Moe'
this.fetchPages(vnode)
},
Page.getAllPages() fetchPages: function(vnode) {
.then(function(result) { this.loading = true
vnode.state.pages = AdminPages.parseTree(result) this.error = ''
return common.sendRequest({
method: 'GET',
url: '/api/auth/pages',
}) })
.catch(function(err) { .then((result) => {
vnode.state.error = err.message this.pages = result.tree
}, (err) => {
this.error = err.message
}) })
.then(function() { .then(() => {
vnode.state.loading = false this.loading = false
m.redraw() m.redraw()
}) })
}, },
@ -43,13 +35,21 @@ const AdminPages = {
let removingPage = this.removePage let removingPage = this.removePage
this.removePage = null this.removePage = null
this.loading = true this.loading = true
Page.removePage(removingPage, removingPage.id) m.redraw()
.then(this.oninit.bind(this, vnode))
.catch(function(err) { return common.sendRequest({
vnode.state.error = err.message method: 'DELETE',
vnode.state.loading = false url: '/api/auth/pages/' + removingPage.id,
})
.then(() => Page.refreshTree())
.then(
() => this.fetchPages(vnode),
(err) => {
this.error = err.message
this.loading = false
m.redraw() m.redraw()
}) }
)
}, },
drawPage: function(vnode, page) { drawPage: function(vnode, page) {
@ -63,7 +63,7 @@ const AdminPages = {
m('td.right', page.updated_at.replace('T', ' ').split('.')[0]), m('td.right', page.updated_at.replace('T', ' ').split('.')[0]),
m('td.right', m('button', { onclick: function() { vnode.state.removePage = page } }, 'Remove')), 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) { view: function(vnode) {
@ -79,7 +79,7 @@ const AdminPages = {
m('header', m('h1', 'All pages')), m('header', m('h1', 'All pages')),
m('div.error', { m('div.error', {
hidden: !this.error, hidden: !this.error,
onclick: function() { vnode.state.error = '' }, onclick: () => { this.fetchPages(vnode) },
}, this.error), }, this.error),
m('table', [ m('table', [
m('thead', m('thead',

View file

@ -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() { exports.getTree = function() {
return common.sendRequest({ return common.sendRequest({
method: 'GET', method: 'GET',

View file

@ -18,9 +18,37 @@ function parseLeaf(tree) {
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) { exports.getPage = function(path, page) {
return common.sendRequest({ return common.sendRequest({
method: 'GET', method: 'GET',
url: '/api/' + (path ? 'pages/' + path : 'frontpage') + '?page=' + (page || 1), 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)
})
}

View file

@ -32,6 +32,7 @@ ol, ul {
img { img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
display: block;
} }
@keyframes spinner-loader { @keyframes spinner-loader {
@ -274,3 +275,72 @@ input[type="reset"]::-moz-focus-inner {
a { color: $dark_secondary-dark-bg; } 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;
}

View file

@ -2,6 +2,7 @@ const m = require('mithril')
const ApiArticle = require('../api/article.p') const ApiArticle = require('../api/article.p')
const Authentication = require('../authentication') const Authentication = require('../authentication')
const Fileinfo = require('../widgets/fileinfo') const Fileinfo = require('../widgets/fileinfo')
const EditorBlock = require('../widgets/editorblock')
const Article = { const Article = {
oninit: function(vnode) { oninit: function(vnode) {
@ -11,6 +12,10 @@ const Article = {
this.data = { this.data = {
article: null, article: null,
files: [], files: [],
pictureFallback: null,
pictureJpeg: null,
pictureAvif: null,
pictureCover: null,
} }
this.showcomments = false this.showcomments = false
@ -52,6 +57,24 @@ const Article = {
.then((result) => { .then((result) => {
this.data = 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) { if (!this.data.article) {
this.error = 'Article not found' this.error = 'Article not found'
} }
@ -67,19 +90,7 @@ const Article = {
}, },
view: function(vnode) { view: function(vnode) {
var deviceWidth = window.innerWidth let article = this.data.article
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
}
}
return ( return (
this.loading ? this.loading ?
m('article.article', m('div.loading-spinner')) m('article.article', m('div.loading-spinner'))
@ -91,37 +102,53 @@ const Article = {
}, },
}, 'Article error: ' + this.error)) }, 'Article error: ' + this.error))
: m('article.article', [ : m('article.article', [
this.data.article.page_path article.page_path
? m('div.goback', ['« ', m(m.route.Link, { href: '/page/' + this.data.article.page_path }, this.data.article.page_name)]) ? m('div.goback', ['« ', m(m.route.Link, { href: '/page/' + article.page_path }, article.page_name)])
: null, : null,
m('header', m('h1', this.data.article.name)), m('header', m('h1', article.name)),
m('.fr-view', [ m('.fr-view', [
this.data.article.media article.pictureFallback
? m('a.cover', { ? m('a.cover', {
rel: 'noopener', rel: 'noopener',
href: this.data.article.media.link, href: article.media_path,
}, m('img', { src: imagePath, alt: 'Cover image for ' + this.data.article.name })) }, [
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, : 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) { this.data.files.map(function(file) {
return m(Fileinfo, { file: file }) return m(Fileinfo, { file: file })
}), }),
m('div.entrymeta', [ m('div.entrymeta', [
'Posted ', 'Posted ',
this.data.article.page_path article.page_path
? [ ? [
'in', '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), 'at ' + (article.publish_at.replace('T', ' ').split('.')[0]).substr(0, 16),
' by ' + (this.data.article.admin_name || 'Admin'), ' by ' + (article.admin_name || 'Admin'),
]), ]),
]), ]),
Authentication.currentUser Authentication.currentUser
? m('div.admin-actions', [ ? m('div.admin-actions', [
m('span', 'Admin controls:'), 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, : null,
this.showcomments this.showcomments

View file

@ -1,4 +1,4 @@
const storageName = 'logintoken' const storageName = 'nfp_sites_logintoken'
const Authentication = { const Authentication = {
currentUser: null, currentUser: null,
@ -37,4 +37,6 @@ const Authentication = {
Authentication.updateToken(localStorage.getItem(storageName)) Authentication.updateToken(localStorage.getItem(storageName))
window.Authentication = Authentication
module.exports = Authentication module.exports = Authentication

View file

@ -16,7 +16,7 @@ const Footer = {
Page.Tree.map(function(page) { Page.Tree.map(function(page) {
return [ return [
m(m.route.Link, { class: 'root', href: '/page/' + page.path }, page.name), m(m.route.Link, { class: 'root', href: '/page/' + page.path }, page.name),
(page.children.length (page.children
? m('ul', page.children.map(function(subpage) { ? m('ul', page.children.map(function(subpage) {
return m('li', m(m.route.Link, { class: 'child', href: '/page/' + subpage.path }, subpage.name)) return m('li', m(m.route.Link, { class: 'child', href: '/page/' + subpage.path }, subpage.name))
})) }))

View file

@ -74,7 +74,7 @@ const Frontpage = {
view: function(vnode) { view: function(vnode) {
var deviceWidth = window.innerWidth 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) { if (bannerPath) {
var pixelRatio = window.devicePixelRatio || 1 var pixelRatio = window.devicePixelRatio || 1

View file

@ -9,12 +9,6 @@ m.route.set = function(path, data, options){
window.scrollTo(0, 0) 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.linkOrig = m.route.link
m.route.link = function(vnode){ m.route.link = function(vnode){
m.route.linkOrig(vnode) m.route.linkOrig(vnode)
@ -40,7 +34,6 @@ const onLoaded = function() {
} }
const onError = function(a, b, c) { const onError = function(a, b, c) {
console.log('onError', this, a, b, c)
elements.forEach(function(x) { x.remove() }) elements.forEach(function(x) { x.remove() })
loadedAdmin = loadingAdmin = false loadedAdmin = loadingAdmin = false
loaded = 0 loaded = 0

View file

@ -45,7 +45,6 @@ const Login = {
if (!result.token) { if (!result.token) {
return Promise.reject(new Error('Server authentication down.')) return Promise.reject(new Error('Server authentication down.'))
} }
console.log(result)
Authentication.updateToken(result.token) Authentication.updateToken(result.token)
m.route.set(Login.redirect || '/') m.route.set(Login.redirect || '/')
}) })

View file

@ -22,11 +22,7 @@ const Menu = {
Menu.loading = true Menu.loading = true
Page.getTree() Page.refreshTree()
.then(function(results) {
Page.Tree.splice(0, Page.Tree.length)
Page.Tree.push.apply(Page.Tree, results)
})
.catch(function(err) { .catch(function(err) {
Menu.error = err.message Menu.error = err.message
}) })
@ -37,6 +33,7 @@ const Menu = {
}, },
view: function() { view: function() {
console.log('menu view', Boolean(Authentication.currentUser))
return [ return [
m('div.top', [ m('div.top', [
m(m.route.Link, m(m.route.Link,
@ -73,7 +70,7 @@ const Menu = {
class: Menu.currentActive === 'home' ? 'active' : '', class: Menu.currentActive === 'home' ? 'active' : '',
}, 'Home'), }, 'Home'),
Menu.loading ? m('div.loading-spinner') : Page.Tree.map(function(page) { Menu.loading ? m('div.loading-spinner') : Page.Tree.map(function(page) {
if (page.children.length) { if (page.children) {
return m('div.hassubmenu', [ return m('div.hassubmenu', [
m(m.route.Link, { m(m.route.Link, {
href: '/page/' + page.path, href: '/page/' + page.path,

View file

@ -156,7 +156,7 @@ const Page = {
? m('aside.news', [ ? m('aside.news', [
m('h4', 'Latest posts under ' + this.data.page.name + ':'), m('h4', 'Latest posts under ' + this.data.page.name + ':'),
this.data.articles.map(function(article) { this.data.articles.map(function(article) {
return m(Newsentry, article) return m(Newsentry, { article: article })
}), }),
m(Pages, { m(Pages, {
base: '/page/' + this.data.page.path, 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, 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 + ':'), m('h4', 'Latest posts under ' + this.data.page.name + ':'),
this.data.articles.map(function(article) { this.data.articles.map(function(article) {
return m(Newsentry, article) return m(Newsentry, { article: article })
}), }),
m(Pages, { m(Pages, {
base: '/page/' + this.data.page.path, base: '/page/' + this.data.page.path,

View file

@ -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

View file

@ -1,53 +1,95 @@
const Fileinfo = require('./fileinfo') const Fileinfo = require('./fileinfo')
const Newsentry = { const Newsentry = {
oninit: function(vnode) {
this.lastId = null
this.onbeforeupdate(vnode)
},
strip: function(html) { strip: function(html) {
var doc = new DOMParser().parseFromString(html, 'text/html') var doc = new DOMParser().parseFromString(html, 'text/html')
var out = doc.body.textContent || '' var out = doc.body.textContent || ''
var splitted = out.split('.') var splitted = out.split('.')
if (splitted.length > 2) { if (splitted.length > 2) {
return splitted.slice(0, 2).join('.') + '...' 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) { view: function(vnode) {
var deviceWidth = window.innerWidth let article = vnode.attrs.article
var pixelRatio = window.devicePixelRatio || 1
var imagePath = ''
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', [ return m('newsentry', [
imagePath this.pictureFallback
? m(m.route.Link, { ? m(m.route.Link, {
class: 'cover', class: 'cover',
href: '/article/' + vnode.attrs.path, href: '/article/' + article.path,
}, m('picture', [ },
m('source', { srcset: m('picture', [
vnode.attrs.media.small_url + '' m('source', {
}), srcset: this.pictureAvif,
m('img', { src: imagePath, alt: 'Article image for ' + vnode.attrs.name }), 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('a.cover.nobg'),
m('div.entrycontent', [ m('div.entrycontent', [
m('div.title', [ m('div.title', [
m(m.route.Link, m(m.route.Link,
{ href: '/article/' + vnode.attrs.path }, { href: '/article/' + article.path },
m('h3', [vnode.attrs.name]) m('h3', [article.name])
), ),
]), ]),
(vnode.attrs.files && vnode.attrs.files.length (article.files && article.files.length
? vnode.attrs.files.map(function(file) { ? article.files.map(function(file) {
return m(Fileinfo, { file: file, slim: true }) return m(Fileinfo, { file: file, slim: true })
}) })
: vnode.attrs.description : this.description
? m('span.entrydescription', Newsentry.strip(vnode.attrs.description)) ? m('span.entrydescription', this.description)
: null), : null),
]), ]),
]) ])

View file

@ -1,25 +1,36 @@
const Fileinfo = require('./fileinfo') const Fileinfo = require('./fileinfo')
const EditorBlock = require('./editorblock')
const Newsitem = { const Newsitem = {
oninit: function(vnode) { oninit: function(vnode) {
let article = vnode.attrs.article this.lastId = null
if (article.media_prefix) { this.onbeforeupdate(vnode)
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.coverSizes = '(max-width: 639px) calc(100vw - 40px), ' onbeforeupdate: function(vnode) {
+ '(max-width: 1000px) 300px, ' let article = vnode.attrs.article
+ '400px'
} else { if (this.lastId !== article.id) {
this.fallbackImage = null this.lastId = article.id
this.srcsetJpeg = null
this.srcsetAvif = null if (article.media_alt_prefix) {
this.coverSizes = null 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('h3', [article.name])
), ),
m('div.newsitemcontent', [ m('div.newsitemcontent', [
this.fallbackImage this.pictureFallback
? m(m.route.Link, { ? m(m.route.Link, {
class: 'cover', class: 'cover',
href: '/article/' + article.path, href: '/article/' + article.path,
}, },
m('picture', [ m('picture', [
this.srcsetAvif ? m('source', { m('source', {
srcset: this.srcsetAvif, srcset: this.pictureAvif,
sizes: this.coverSizes, sizes: this.pictureCover,
type: 'image/avif', type: 'image/avif',
}) : null, }),
m('img', { m('img', {
srcset: this.srcsetJpeg, srcset: this.pictureJpeg,
sizes: this.coverSizes, sizes: this.pictureCover,
alt: 'Image for news item ' + article.name, alt: 'Image for news item ' + article.name,
src: this.fallbackImage, src: this.pictureFallback,
}), }),
]) ])
) )
: null, : null,
m('div.entrycontent', { m('div.entrycontent', [
class: article.media ? '' : 'extrapadding', article.content.blocks.map(block => {
}, [ return m(EditorBlock, { block: block })
(article.content }),
? m('.fr-view', m.trust(article.content))
: null),
(article.files && article.files.length (article.files && article.files.length
? article.files.map(function(file) { ? article.files.map(function(file) {
return m(Fileinfo, { file: file, trim: true }) return m(Fileinfo, { file: file, trim: true })