Finished base articles and pages

master
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 { parseArticles, parseArticle } from './util.mjs'
import { upload } from '../media/upload.mjs'
import { mediaToDatabase } from '../media/util.mjs'
export default class ArticleRoutes {
constructor(opts = {}) {
@ -11,7 +12,7 @@ export default class ArticleRoutes {
/** GET: /api/articles/[path] */
async getArticle(ctx) {
let res = await ctx.db.safeCallProc('common.article_get_single', [ctx.params.path])
let res = await ctx.db.safeCallProc('article_get_single', [ctx.params.path])
let out = {
article: parseArticle(res.results[0][0]),
@ -23,7 +24,7 @@ export default class ArticleRoutes {
/** GET: /api/auth/articles */
async auth_getAllArticles(ctx) {
let res = await ctx.db.safeCallProc('common.article_auth_get_all', [
let res = await ctx.db.safeCallProc('article_auth_get_all', [
ctx.state.auth_token,
Math.max(ctx.query.get('page') || 1, 1),
Math.min(ctx.query.get('per_page') || 10, 25)
@ -40,7 +41,7 @@ export default class ArticleRoutes {
async private_getUpdateArticle(ctx, body = null, banner = null, media = null) {
let params = [
ctx.state.auth_token,
ctx.params.path
ctx.params.id === '0' ? null : ctx.params.id
]
if (body) {
params = params.concat([
@ -53,100 +54,28 @@ export default class ArticleRoutes {
body.is_featured === 'true' ? 1 : 0,
0,
])
if (banner) {
params = params.concat([
banner.filename,
banner.type,
banner.path,
banner.size,
banner.preview.base64,
banner.sizes.small.avif.path.replace(/_small\.avif$/, ''),
JSON.stringify(banner.sizes),
0,
])
} else {
params = params.concat([
null,
null,
null,
null,
null,
null,
null,
null,
])
}
if (media) {
params = params.concat([
media.filename,
media.type,
media.path,
media.size,
media.preview.base64,
media.sizes.small.avif.path.replace(/_small\.avif$/, ''),
JSON.stringify(media.sizes),
0,
])
} else {
params = params.concat([
null,
null,
null,
null,
null,
null,
null,
null,
])
}
params = params.concat(mediaToDatabase(banner, body.remove_banner === 'true'))
params = params.concat(mediaToDatabase(media, body.remove_media === 'true'))
}
console.log(params)
let res = await ctx.db.safeCallProc('common.article_auth_get_update_create', params)
let res = await ctx.db.safeCallProc('article_auth_get_update_create', params)
let out = {
article: parseArticle(res.results[0][0]),
article: parseArticle(res.results[0][0]) || { publish_at: new Date() },
files: parseFiles(res.results[1]),
staff: res.results[2],
}
if (out.article) {
if (out.article.content[0] === '{') {
try {
out.article.content = JSON.parse(out.article.content)
} catch (err) {
out.article.content = {
time: new Date().getTime(),
blocks: [
{id: '1', type: 'paragraph', data: { text: 'Error parsing article content: ' + err.message }},
],
version: '2.25.0'
}
}
} else if (out.article.content) {
out.article.content = {
time: new Date().getTime(),
blocks: [
{id: '1', type: 'htmlraw', data: { html: out.article.content }},
],
version: '2.25.0'
}
}
} else {
out.article = {
publish_at: new Date()
}
}
ctx.body = out
}
/** GET: /api/auth/articles/:path */
/** GET: /api/auth/articles/:id */
auth_getSingleArticle(ctx) {
return this.private_getUpdateArticle(ctx)
}
/** PUT: /api/auth/articles/:path */
/** PUT: /api/auth/articles/:id */
async auth_updateCreateSingleArticle(ctx) {
console.log(ctx.req.body)
@ -173,11 +102,11 @@ export default class ArticleRoutes {
return this.private_getUpdateArticle(ctx, ctx.req.body, newBanner, newMedia)
}
/** DELETE: /api/auth/articles/:path */
/** DELETE: /api/auth/articles/:id */
async auth_removeSingleArticle(ctx) {
let params = [
ctx.state.auth_token,
ctx.params.path,
ctx.params.id,
// Article data
null,
null,
@ -187,25 +116,9 @@ export default class ArticleRoutes {
null,
null,
1,
// Banner data
null,
null,
null,
null,
null,
null,
null,
1,
// Media data
null,
null,
null,
null,
null,
null,
null,
1,
]
params = params.concat(mediaToDatabase(null, true))
params = params.concat(mediaToDatabase(null, true))
await ctx.db.safeCallProc('article_auth_get_update_create', params)

View File

@ -1,3 +1,5 @@
import { parseFile } from '../file/util.mjs'
export function parseArticles(articles) {
for (let i = 0; i < articles.length; i++) {
parseArticle(articles[i])
@ -5,17 +7,52 @@ export function parseArticles(articles) {
return articles
}
export function combineFilesWithArticles(articles, files) {
let articleMap = new Map()
articles.forEach(article => {
article.files = []
articleMap.set(article.id, article)
})
files.forEach(file => {
articleMap.get(file.id).files.push(parseFile(file))
})
}
export function parseArticle(article) {
if (!article) {
return null
}
if (article.content) {
if (article.content[0] === '{') {
try {
article.content = JSON.parse(article.content)
} catch (err) {
article.content = {
time: new Date().getTime(),
blocks: [
{id: '1', type: 'paragraph', data: { text: 'Error parsing article content: ' + err.message }},
],
version: '2.25.0'
}
}
} else {
article.content = {
time: new Date().getTime(),
blocks: [
{id: '1', type: 'htmlraw', data: { html: article.content }},
],
version: '2.25.0'
}
}
}
if (article.banner_path) {
article.banner_path = 'https://cdn.nfp.is' + article.banner_path
article.banner_prefix = 'https://cdn.nfp.is' + article.banner_prefix
article.banner_alt_prefix = 'https://cdn.nfp.is' + article.banner_alt_prefix
}
if (article.media_path) {
article.media_path = 'https://cdn.nfp.is' + article.media_path
article.media_prefix = 'https://cdn.nfp.is' + article.media_prefix
article.media_alt_prefix = 'https://cdn.nfp.is' + article.media_alt_prefix
}
return article
}

View File

@ -13,7 +13,7 @@ export default class AuthenticationRoutes {
/** GET: /api/authentication/login */
async login(ctx) {
let res = await ctx.db.safeCallProc('common.auth_login', [
let res = await ctx.db.safeCallProc('auth_login', [
ctx.req.body.email,
ctx.req.body.password,
])

View File

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

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 * as security from './security.mjs'
import { parsePagesToTree } from './util.mjs'
import { upload } from '../media/upload.mjs'
import { combineFilesWithArticles, parseArticle, parseArticles } from '../article/util.mjs'
import { mediaToDatabase } from '../media/util.mjs'
export default class PageRoutes {
constructor(opts = {}) {
Object.assign(this, {
Page: opts.Page || Page,
security: opts.security || security,
upload: upload,
})
}
/** GET: /api/pagetree */
async getPageTree(ctx) {
ctx.body = await this.Page.getTree(ctx)
async getPageTree(ctx, onlyReturn = false) {
let res = await ctx.db.safeCallProc('pages_get_tree', [])
if (onlyReturn) {
return parsePagesToTree(res.first)
}
ctx.body = parsePagesToTree(res.first)
}
/** GET: /api/pages/[path] */
async getPage(ctx) {
ctx.body = await this.Page.getPage(
ctx,
let res = await ctx.db.safeCallProc('pages_get_single', [
ctx.params.path || null,
Math.max(ctx.query.get('page') || 1, 1),
Math.min(ctx.query.get('per_page') || 10, 25)
)
Math.min(ctx.query.get('per_page') || 10, 25),
])
let out = {
page: res.results[0][0] || null,
articles: parseArticles(res.results[1]),
total_articles: res.results[2][0].total_articles,
featured: parseArticle(res.results[4][0]),
}
combineFilesWithArticles(out.articles, res.results[3])
ctx.body = out
}
/** GET: /api/auth/pages */
async auth_getAllPages(ctx) {
let res = await ctx.db.safeCallProc('pages_auth_get_all', [
ctx.state.auth_token
])
ctx.body = parsePagesToTree(res.first)
}
async private_getUpdatePage(ctx, body = null, banner = null, media = null) {
let params = [
ctx.state.auth_token,
ctx.params.id === '0' ? null : ctx.params.id
]
if (body) {
params = params.concat([
body.name,
body.parent_id === 'null' ? null : Number(body.parent_id),
body.path,
body.content,
0,
])
params = params.concat(mediaToDatabase(banner, body.remove_banner === 'true'))
params = params.concat(mediaToDatabase(media, body.remove_media === 'true'))
}
console.log(params)
let res = await ctx.db.safeCallProc('pages_auth_get_update_create', params)
let out = {
page: res.results[0][0] || {},
}
ctx.body = out
}
/** GET: /api/auth/pages/:id */
auth_getSinglePage(ctx) {
return this.private_getUpdatePage(ctx)
}
/** PUT: /api/auth/pages/:id */
async auth_updateCreateSinglePage(ctx) {
console.log(ctx.req.body)
let newBanner = null
let newMedia = null
let promises = []
if (ctx.req.files.banner) {
promises.push(
this.upload(ctx.req.files.banner)
.then(res => { newBanner = res })
)
}
if (ctx.req.files.media) {
promises.push(
this.upload(ctx.req.files.media)
.then(res => { newMedia = res })
)
}
await Promise.all(promises)
return this.private_getUpdatePage(ctx, ctx.req.body, newBanner, newMedia)
}
/** DELETE: /api/auth/pages/:id */
async auth_removeSinglePage(ctx) {
let params = [
ctx.state.auth_token,
ctx.params.id,
// Page data
null,
null,
null,
null,
1,
]
params = params.concat(mediaToDatabase(null, true))
params = params.concat(mediaToDatabase(null, true))
await ctx.db.safeCallProc('pages_auth_get_update_create', params)
ctx.status = 204
}
}

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

View File

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

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-fg: white;

View File

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

View File

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

View File

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

View File

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

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

View File

@ -18,9 +18,37 @@ function parseLeaf(tree) {
parseLeaf(Tree)
function processPageBranch(arr, branches, prefix) {
branches.forEach((page) => {
arr.push({ id: page.id, name: prefix + page.name })
if (page.children && page.children.length) {
processPageBranch(arr, page.children, page.name + ' -> ')
}
})
}
exports.getFlatTree = function() {
let arr = []
processPageBranch(arr, Tree, '')
return arr
}
exports.getPage = function(path, page) {
return common.sendRequest({
method: 'GET',
url: '/api/' + (path ? 'pages/' + path : 'frontpage') + '?page=' + (page || 1),
})
}
exports.refreshTree = function() {
return common.sendRequest({
method: 'GET',
url: '/api/pagetree',
})
.then(pages => {
Tree.splice(0, Tree.length)
Tree.push.apply(Tree, pages.tree)
TreeMap.clear()
parseLeaf(Tree)
})
}

View File

@ -32,6 +32,7 @@ ol, ul {
img {
max-width: 100%;
height: auto;
display: block;
}
@keyframes spinner-loader {
@ -274,3 +275,72 @@ input[type="reset"]::-moz-focus-inner {
a { color: $dark_secondary-dark-bg; }
}
}
:root {
--primary-bg: #01579b;
--primary-fg: white;
--primary-fg-url: #FFC7C7;
--primary-light-bg: #3D77C7; // #4f83cc;
--primary-light-fg: white;
--primary-dark-bg: #002f6c;
--primary-dark-fg: white;
--secondary-bg: #f57c00;
--secondary-fg: black;
--secondary-light-bg: #ffad42;
--secondary-light-fg: black;
--secondary-dark-bg: #bb4d00;
--secondary-dark-fg: white;
--table-fg: #333;
--border: #ccc;
--border-fg: black;
--border-fg-url: #8f3c00;
--title-fg: #555;
--meta-fg: #757575; // #999
--meta-light-fg: #999999;
--main-bg: white;
--main-fg: black;
--input-bg: white;
--input-border: #333;
--input-fg: black;
--newsitem-bg: transparent;
--newsitem-border: none;
}
.darkmodeon {
--primary-bg: #013b68;
--primary-fg: white;
--primary-fg-url: #FFC7C7;
--primary-light-bg: #28518B;
--primary-light-fg: white;
--primary-dark-bg: #002f6c;
--primary-dark-fg: white;
--secondary-bg: #e05e00;
--secondary-fg: black;
--secondary-light-bg: #ffad42;
--secondary-light-fg: black;
--secondary-dark-bg: #e05e00;
--secondary-dark-fg: white;
--secondary-darker-fg: #fe791b;
--table-fg: #333;
--border: #343536;
--border-fg: #d7dadc;;
--border-fg-url: #e05e00;
--title-fg: #808080;
--meta-fg: hsl(0, 0%, 55%);
--meta-light-fg: #999999;
--main-bg: black;
--main-fg: #d7dadc;
--input-bg: #272729;
--input-border: #343536;
--input-fg: white;
--newsitem-bg: #1a1a1b;
--newsitem-border: 1px solid #343536;
}

View File

@ -2,6 +2,7 @@ const m = require('mithril')
const ApiArticle = require('../api/article.p')
const Authentication = require('../authentication')
const Fileinfo = require('../widgets/fileinfo')
const EditorBlock = require('../widgets/editorblock')
const Article = {
oninit: function(vnode) {
@ -11,6 +12,10 @@ const Article = {
this.data = {
article: null,
files: [],
pictureFallback: null,
pictureJpeg: null,
pictureAvif: null,
pictureCover: null,
}
this.showcomments = false
@ -52,6 +57,24 @@ const Article = {
.then((result) => {
this.data = result
if (this.data.article.media_alt_prefix) {
this.data.article.pictureFallback = this.data.article.media_alt_prefix + '_small.jpg'
this.data.article.pictureJpeg = this.data.article.media_alt_prefix + '_small.jpg' + ' 720w, '
+ this.data.article.media_alt_prefix + '_medium.jpg' + ' 1300w, '
+ this.data.article.media_alt_prefix + '_large.jpg 1920w'
this.data.article.pictureAvif = this.data.article.media_alt_prefix + '_small.avif' + ' 720w, '
+ this.data.article.media_alt_prefix + '_medium.avif' + ' 1300w, '
+ this.data.article.media_alt_prefix + '_large.avif 1920w'
this.data.article.pictureCover = '(max-width: 840px) calc(100vw - 82px), '
+ '758px'
} else {
this.data.article.pictureFallback = null
this.data.article.pictureJpeg = null
this.data.article.pictureAvif = null
this.data.article.pictureCover = null
}
if (!this.data.article) {
this.error = 'Article not found'
}
@ -67,19 +90,7 @@ const Article = {
},
view: function(vnode) {
var deviceWidth = window.innerWidth
var imagePath = ''
if (this.data.article && this.data.article.media) {
var pixelRatio = window.devicePixelRatio || 1
if ((deviceWidth < 800 && pixelRatio <= 1)
|| (deviceWidth < 600 && pixelRatio > 1)) {
imagePath = this.data.article.media.medium_url
} else {
imagePath = this.data.article.media.large_url
}
}
let article = this.data.article
return (
this.loading ?
m('article.article', m('div.loading-spinner'))
@ -91,37 +102,53 @@ const Article = {
},
}, 'Article error: ' + this.error))
: m('article.article', [
this.data.article.page_path
? m('div.goback', ['« ', m(m.route.Link, { href: '/page/' + this.data.article.page_path }, this.data.article.page_name)])
article.page_path
? m('div.goback', ['« ', m(m.route.Link, { href: '/page/' + article.page_path }, article.page_name)])
: null,
m('header', m('h1', this.data.article.name)),
m('header', m('h1', article.name)),
m('.fr-view', [
this.data.article.media
article.pictureFallback
? m('a.cover', {
rel: 'noopener',
href: this.data.article.media.link,
}, m('img', { src: imagePath, alt: 'Cover image for ' + this.data.article.name }))
href: article.media_path,
}, [
m('picture', [
m('source', {
srcset: article.pictureAvif,
sizes: article.pictureCover,
type: 'image/avif',
}),
m('img', {
srcset: article.pictureJpeg,
sizes: article.pictureCover,
alt: 'Image for news item ' + article.name,
src: article.pictureFallback,
}),
]),
])
: null,
this.data.article.content ? m.trust(this.data.article.content) : null,
article.content.blocks.map(block => {
return m(EditorBlock, { block: block })
}),
this.data.files.map(function(file) {
return m(Fileinfo, { file: file })
}),
m('div.entrymeta', [
'Posted ',
this.data.article.page_path
article.page_path
? [
'in',
m(m.route.Link, { href: '/page/' + this.data.article.page_path }, this.data.article.page_name)
m(m.route.Link, { href: '/page/' + article.page_path }, article.page_name)
]
: '',
'at ' + (this.data.article.publish_at.replace('T', ' ').split('.')[0]).substr(0, 16),
' by ' + (this.data.article.admin_name || 'Admin'),
'at ' + (article.publish_at.replace('T', ' ').split('.')[0]).substr(0, 16),
' by ' + (article.admin_name || 'Admin'),
]),
]),
Authentication.currentUser
? m('div.admin-actions', [
m('span', 'Admin controls:'),
m(m.route.Link, { href: '/admin/articles/' + this.data.article.path }, 'Edit article'),
m(m.route.Link, { href: '/admin/articles/' + article.path }, 'Edit article'),
])
: null,
this.showcomments

View File

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

View File

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

View File

@ -74,7 +74,7 @@ const Frontpage = {
view: function(vnode) {
var deviceWidth = window.innerWidth
var bannerPath = this.data.featured && this.data.featured.banner_prefix
var bannerPath = this.data.featured && this.data.featured.banner_alt_prefix
if (bannerPath) {
var pixelRatio = window.devicePixelRatio || 1

View File

@ -9,12 +9,6 @@ m.route.set = function(path, data, options){
window.scrollTo(0, 0)
}
/*console.log('tree', window.__nfptree)
console.log('featured', window.__nfpfeatured)
console.log('data', window.__nfpdata)
console.log('subdata', window.__nfpsubdata)
console.log('links', window.__nfplinks)*/
m.route.linkOrig = m.route.link
m.route.link = function(vnode){
m.route.linkOrig(vnode)
@ -40,7 +34,6 @@ const onLoaded = function() {
}
const onError = function(a, b, c) {
console.log('onError', this, a, b, c)
elements.forEach(function(x) { x.remove() })
loadedAdmin = loadingAdmin = false
loaded = 0

View File

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

View File

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

View File

@ -156,7 +156,7 @@ const Page = {
? m('aside.news', [
m('h4', 'Latest posts under ' + this.data.page.name + ':'),
this.data.articles.map(function(article) {
return m(Newsentry, article)
return m(Newsentry, { article: article })
}),
m(Pages, {
base: '/page/' + this.data.page.path,
@ -171,7 +171,7 @@ const Page = {
imagePath ? m('a', { href: this.data.page.media.link}, m('img.page-cover', { src: imagePath, alt: 'Cover image for ' + this.data.page.name } )) : null,
m('h4', 'Latest posts under ' + this.data.page.name + ':'),
this.data.articles.map(function(article) {
return m(Newsentry, article)
return m(Newsentry, { article: article })
}),
m(Pages, {
base: '/page/' + this.data.page.path,

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 Newsentry = {
oninit: function(vnode) {
this.lastId = null
this.onbeforeupdate(vnode)
},
strip: function(html) {
var doc = new DOMParser().parseFromString(html, 'text/html')
var out = doc.body.textContent || ''
var splitted = out.split('.')
if (splitted.length > 2) {
var doc = new DOMParser().parseFromString(html, 'text/html')
var out = doc.body.textContent || ''
var splitted = out.split('.')
if (splitted.length > 2) {
return splitted.slice(0, 2).join('.') + '...'
}
return out
}
return out
},
onbeforeupdate: function(vnode) {
let article = vnode.attrs.article
if (this.lastId !== article.id) {
this.lastId = article.id
this.description = null
for (let i = 0; i < article.content.blocks.length; i++) {
if (article.content.blocks[i].type === 'paragraph') {
this.description = article.content.blocks[i].data.text
break
} else if (article.content.blocks[i].type === 'htmlraw') {
this.description = this.strip(article.content.blocks[i].data.html)
break
}
}
if (article.media_alt_prefix) {
this.pictureFallback = article.media_alt_prefix + '_small.jpg'
this.pictureJpeg = article.media_alt_prefix + '_small.jpg' + ' 720w, '
+ article.media_alt_prefix + '_medium.jpg' + ' 1300w, '
+ article.media_alt_prefix + '_large.jpg 1920w'
this.pictureAvif = article.media_alt_prefix + '_small.avif' + ' 720w, '
+ article.media_alt_prefix + '_medium.avif' + ' 1300w, '
+ article.media_alt_prefix + '_large.avif 1920w'
this.pictureCover = '(max-width: 440px) calc(100vw - 40px), '
+ '124px'
} else {
this.pictureFallback = null
this.pictureJpeg = null
this.pictureAvif = null
this.pictureCover = null
}
}
},
view: function(vnode) {
var deviceWidth = window.innerWidth
var pixelRatio = window.devicePixelRatio || 1
var imagePath = ''
let article = vnode.attrs.article
if (vnode.attrs.media) {
if (deviceWidth > 440 || pixelRatio <= 1) {
imagePath = vnode.attrs.media.small_url
} else {
imagePath = vnode.attrs.media.medium_url
}
}
return m('newsentry', [
imagePath
this.pictureFallback
? m(m.route.Link, {
class: 'cover',
href: '/article/' + vnode.attrs.path,
}, m('picture', [
m('source', { srcset:
vnode.attrs.media.small_url + ''
}),
m('img', { src: imagePath, alt: 'Article image for ' + vnode.attrs.name }),
]))
class: 'cover',
href: '/article/' + article.path,
},
m('picture', [
m('source', {
srcset: this.pictureAvif,
sizes: this.pictureCover,
type: 'image/avif',
}),
m('img', {
srcset: this.pictureJpeg,
sizes: this.pictureCover,
alt: 'Image for news item ' + article.name,
src: this.pictureFallback,
}),
])
)
: m('a.cover.nobg'),
m('div.entrycontent', [
m('div.title', [
m(m.route.Link,
{ href: '/article/' + vnode.attrs.path },
m('h3', [vnode.attrs.name])
{ href: '/article/' + article.path },
m('h3', [article.name])
),
]),
(vnode.attrs.files && vnode.attrs.files.length
? vnode.attrs.files.map(function(file) {
(article.files && article.files.length
? article.files.map(function(file) {
return m(Fileinfo, { file: file, slim: true })
})
: vnode.attrs.description
? m('span.entrydescription', Newsentry.strip(vnode.attrs.description))
: this.description
? m('span.entrydescription', this.description)
: null),
]),
])

View File

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