This commit is contained in:
Jonatan Nilsson 2022-08-12 23:33:50 +00:00
parent cbd38bc212
commit e9c7cdfb7a
29 changed files with 802 additions and 352 deletions

View file

@ -1,13 +1,12 @@
import { FormidableHandler } from 'flaska'
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 { uploadMedia, uploadFile } from '../media/upload.mjs'
import { mediaToDatabase } from '../media/util.mjs' import { mediaToDatabase } from '../media/util.mjs'
export default class ArticleRoutes { export default class ArticleRoutes {
constructor(opts = {}) { constructor(opts = {}) {
Object.assign(this, { Object.assign(this, {
upload: upload, uploadMedia: uploadMedia,
uploadFile: uploadFile,
}) })
} }
@ -26,12 +25,13 @@ export default class ArticleRoutes {
async getArticle(ctx) { async getArticle(ctx) {
let res = await ctx.db.safeCallProc('article_get_single', [ctx.params.path]) let res = await ctx.db.safeCallProc('article_get_single', [ctx.params.path])
let out = { ctx.body = this.getArticle_resOutput(res)
article: parseArticle(res.results[0][0]),
files: parseFiles(res.results[1]),
} }
ctx.body = out getArticle_resOutput(res) {
return {
article: parseArticle(res.results[0][0]),
}
} }
/** GET: /api/auth/articles */ /** GET: /api/auth/articles */
@ -72,15 +72,15 @@ export default class ArticleRoutes {
console.log(params) console.log(params)
let res = await ctx.db.safeCallProc('article_auth_get_update_create', params) let res = await ctx.db.safeCallProc('article_auth_get_update_create', params)
let out = { ctx.body = this.private_getUpdateArticle_resOutput(res)
article: parseArticle(res.results[0][0]) || { publish_at: new Date() },
files: parseFiles(res.results[1]),
staff: res.results[2],
} }
ctx.body = out private_getUpdateArticle_resOutput(res) {
return {
article: parseArticle(res.results[0][0] || {}),
staff: res.results[1],
}
} }
/** GET: /api/auth/articles/:id */ /** GET: /api/auth/articles/:id */
auth_getSingleArticle(ctx) { auth_getSingleArticle(ctx) {
@ -98,13 +98,13 @@ export default class ArticleRoutes {
if (ctx.req.files.banner) { if (ctx.req.files.banner) {
promises.push( promises.push(
this.upload(ctx.req.files.banner) this.uploadMedia(ctx.req.files.banner)
.then(res => { newBanner = res }) .then(res => { newBanner = res })
) )
} }
if (ctx.req.files.media) { if (ctx.req.files.media) {
promises.push( promises.push(
this.upload(ctx.req.files.media) this.uploadMedia(ctx.req.files.media)
.then(res => { newMedia = res }) .then(res => { newMedia = res })
) )
} }

View file

@ -1,4 +1,3 @@
import { parseFile } from '../file/util.mjs'
import { contentToBlocks, parseMediaAndBanner } from '../util.mjs' import { contentToBlocks, parseMediaAndBanner } from '../util.mjs'
export function parseArticles(articles) { export function parseArticles(articles) {
@ -8,18 +7,6 @@ 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

View file

@ -66,6 +66,7 @@ nconf.defaults({
"secret": "upload-secret-key-here", "secret": "upload-secret-key-here",
"iss": "dev", "iss": "dev",
"path": "https://media.nfp.is/media/resize", "path": "https://media.nfp.is/media/resize",
"filePath": "https://media.nfp.is/media",
"preview": { "preview": {
"out": "base64", "out": "base64",
"format": "avif", "format": "avif",

View file

@ -1,7 +1,7 @@
import config from '../config.mjs' import config from '../config.mjs'
import Client from './client.mjs' import Client from './client.mjs'
export function upload(file) { export function uploadMedia(file) {
const media = config.get('media') const media = config.get('media')
const client = new Client() const client = new Client()
@ -53,3 +53,22 @@ export function upload(file) {
return out return out
}) })
} }
export function uploadFile(file) {
const media = config.get('media')
const client = new Client()
let token = client.createJwt({ iss: media.iss }, media.secret)
return client.upload(media.filePath + '?token=' + token, { file: {
file: file.path,
filename: file.name,
} }, 'POST').then(res => {
return {
filename: res.filename,
path: res.path,
size: file.size,
type: file.type,
}
})
}

View file

@ -1,7 +1,7 @@
{ {
"dependencies": { "dependencies": {
"dot": "^2.0.0-beta.1", "dot": "^2.0.0-beta.1",
"flaska": "^1.3.0", "flaska": "^1.3.1",
"formidable": "^1.2.6", "formidable": "^1.2.6",
"msnodesqlv8": "^2.4.7", "msnodesqlv8": "^2.4.7",
"nconf-lite": "^1.0.1" "nconf-lite": "^1.0.1"

View file

@ -1,13 +1,14 @@
import { parsePage, parsePagesToTree } from './util.mjs' import { parsePage, parsePagesToTree } from './util.mjs'
import { upload } from '../media/upload.mjs' import { uploadMedia, uploadFile } from '../media/upload.mjs'
import { combineFilesWithArticles, parseArticle, parseArticles } from '../article/util.mjs' import { parseArticle, parseArticles } from '../article/util.mjs'
import { mediaToDatabase } from '../media/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, {
upload: upload, uploadMedia: uploadMedia,
uploadFile: uploadFile,
}) })
} }
@ -42,16 +43,16 @@ export default class PageRoutes {
Math.min(ctx.query.get('per_page') || 10, 25), Math.min(ctx.query.get('per_page') || 10, 25),
]) ])
let out = { ctx.body = this.getPage_resOut(res)
}
getPage_resOut(res) {
return {
page: parsePage(res.results[0][0]), page: parsePage(res.results[0][0]),
articles: parseArticles(res.results[1]), articles: parseArticles(res.results[1]),
total_articles: res.results[2][0].total_articles, total_articles: res.results[2][0].total_articles,
featured: parseArticle(res.results[4][0]), featured: parseArticle(res.results[4][0]),
} }
combineFilesWithArticles(out.articles, res.results[3])
ctx.body = out
} }
/** GET: /api/auth/pages */ /** GET: /api/auth/pages */
@ -106,13 +107,13 @@ export default class PageRoutes {
if (ctx.req.files.banner) { if (ctx.req.files.banner) {
promises.push( promises.push(
this.upload(ctx.req.files.banner) this.uploadMedia(ctx.req.files.banner)
.then(res => { newBanner = res }) .then(res => { newBanner = res })
) )
} }
if (ctx.req.files.media) { if (ctx.req.files.media) {
promises.push( promises.push(
this.upload(ctx.req.files.media) this.uploadMedia(ctx.req.files.media)
.then(res => { newMedia = res }) .then(res => { newMedia = res })
) )
} }

View file

@ -26,29 +26,17 @@ export default class Server {
this.authenticate = authenticate this.authenticate = authenticate
this.formidable = FormidableHandler.bind(this, formidable) this.formidable = FormidableHandler.bind(this, formidable)
this.jsonHandler = JsonHandler this.jsonHandler = JsonHandler
this.routes = [ this.routes = {
new PageRoutes(), page: new PageRoutes(),
new ArticleRoutes(), article: new ArticleRoutes(),
new AuthenticationRoutes(), auth: new AuthenticationRoutes(),
] }
this.init() this.init()
} }
init() { } init() { }
getRouteInstance(type) {
for (let route of this.routes) {
if (route instanceof type) {
return route
}
}
}
addCustomRoutes() {
}
runCreateServer() { runCreateServer() {
// Create our server // Create our server
this.flaska = new Flaska(this.flaskaOptions, this.http) this.flaska = new Flaska(this.flaskaOptions, this.http)
@ -89,8 +77,9 @@ export default class Server {
} }
runRegisterRoutes() { runRegisterRoutes() {
for (let route of this.routes) { let keys = Object.keys(this.routes)
route.register(this) for (let key of keys) {
this.routes[key].register(this)
} }
} }
@ -105,7 +94,6 @@ export default class Server {
} }
run() { run() {
this.addCustomRoutes()
this.runCreateServer() this.runCreateServer()
this.runRegisterRoutes() this.runRegisterRoutes()

View file

@ -0,0 +1,84 @@
import { parseArticles, parseArticle } from '../base/article/util.mjs'
import { parseFiles } from './file/util.mjs'
import Parent from '../base/article/routes.mjs'
import { decodeTorrentFile } from './file/torrent.mjs'
export default class ArticleRoutes extends Parent {
register(server) {
super.register(server)
server.flaska.post('/api/auth/articles/:id/files', [
server.authenticate(),
server.formidable({ maxFileSize: 100 * 1024 * 1024, }),
], this.auth_addFileToArticle.bind(this))
}
getArticle_resOutput(res) {
return {
article: parseArticle(res.results[0][0]),
files: parseFiles(res.results[1]),
}
}
private_getUpdateArticle_resOutput(res) {
return {
article: parseArticle(res.results[0][0] || {}),
files: parseFiles(res.results[1]),
staff: res.results[2],
}
}
/** POST: /api/auth/articles/:id/files */
async auth_addFileToArticle(ctx) {
if (!ctx.req.files.file) {
throw new HttpError(422, 'Missing file in upload')
}
let meta = {}
if (ctx.req.files.file.name.endsWith('.torrent')) {
try {
let torrent = await decodeTorrentFile(ctx.req.files.file.path)
meta.torrent = {
name: torrent.name,
announce: torrent.announce,
hash: torrent.infoHash,
files: torrent.files.map(file => ({ name: file.name, size: file.length })),
}
} catch (err) {
ctx.log.error(err)
}
}
let file = await this.uploadFile(ctx.req.files.file)
let params = [
ctx.state.auth_token,
ctx.params.id,
file.filename,
file.type,
file.path,
file.size,
JSON.stringify(meta),
null,
0,
]
await ctx.db.safeCallProc('article_auth_file_create_delete', params)
}
/** DELETE: /api/auth/articles/:id/files/:fileId */
async auth_addFileToArticle(ctx) {
let params = [
ctx.state.auth_token,
ctx.params.id,
null,
null,
null,
null,
null,
ctx.params.fileId,
0,
]
let res = await ctx.db.safeCallProc('article_auth_file_create_delete', params)
console.log(res)
}
}

View file

@ -1,3 +1,6 @@
import fs from 'fs/promises'
import crypto from 'crypto'
import path from 'path'
import bencode from 'bencode' import bencode from 'bencode'
/* /*
@ -9,10 +12,9 @@ Taken from parse-torrent
* @param {Buffer|Object} torrent * @param {Buffer|Object} torrent
* @return {Object} parsed torrent * @return {Object} parsed torrent
*/ */
export function decodeTorrentFile (torrent) { export async function decodeTorrentFile (file) {
if (Buffer.isBuffer(torrent)) { let buffer = await fs.readFile(file)
torrent = bencode.decode(torrent) let torrent = bencode.decode(buffer)
}
// sanity check // sanity check
ensure(torrent.info, 'info') ensure(torrent.info, 'info')
@ -36,7 +38,9 @@ export function decodeTorrentFile (torrent) {
announce: [] announce: []
} }
result.infoHash = sha1.sync(result.infoBuffer) result.infoHash = crypto.createHash('sha1')
.update(result.infoBuffer)
.digest('hex')
result.infoHashBuffer = Buffer.from(result.infoHash, 'hex') result.infoHashBuffer = Buffer.from(result.infoHash, 'hex')
if (torrent.info.private !== undefined) result.private = !!torrent.info.private if (torrent.info.private !== undefined) result.private = !!torrent.info.private
@ -103,3 +107,7 @@ function splitPieces (buf) {
function ensure (bool, fieldName) { function ensure (bool, fieldName) {
if (!bool) throw new Error(`Torrent is missing required field: ${fieldName}`) if (!bool) throw new Error(`Torrent is missing required field: ${fieldName}`)
} }
function sumLength (sum, file) {
return sum + file.length
}

View file

@ -5,6 +5,18 @@ export function parseFiles(files) {
return files return files
} }
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 parseFile(file) { export function parseFile(file) {
file.url = 'https://cdn.nfp.is' + file.path file.url = 'https://cdn.nfp.is' + file.path
file.magnet = null file.magnet = null

View file

@ -0,0 +1,19 @@
import { parseArticles, parseArticle } from '../../base/article/util.mjs'
import Parent from '../../base/page/routes.mjs'
import { parsePage } from '../../base/page/util.mjs'
import { combineFilesWithArticles } from './file/util.mjs'
export default class PageRoutes extends Parent {
getPage_resOut(res) {
let out = {
page: parsePage(res.results[0][0]),
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])
return out
}
}

View file

@ -1,22 +1,22 @@
import config from '../base/config.mjs' import config from '../base/config.mjs'
import Parent from '../base/server.mjs' import Parent from '../base/server.mjs'
import ServeHandler from '../base/serve.mjs' import ServeHandler from '../base/serve.mjs'
import PageRoutes from '../base/page/routes.mjs' import ArticleRoutes from './article_routes.mjs'
import PageRoutes from './page_routes.mjs'
export default class Server extends Parent { export default class Server extends Parent {
init() { init() {
this.flaskaOptions.appendHeaders['Content-Security-Policy'] = `default-src 'self'; script-src 'self' talk.hyvor.com; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-src talk.hyvor.com` //; frame-ancestors 'none'` super.init()
}
addCustomRoutes() {
let page = this.getRouteInstance(PageRoutes)
let localUtil = new this.core.sc.Util(import.meta.url) let localUtil = new this.core.sc.Util(import.meta.url)
this.routes.push(new ServeHandler({
pageRoutes: page, this.flaskaOptions.appendHeaders['Content-Security-Policy'] = `default-src 'self'; script-src 'self' talk.hyvor.com; style-src 'self' 'unsafe-inline'; img-src * data: blob:; font-src 'self' data:; object-src 'none'; frame-src talk.hyvor.com` //; frame-ancestors 'none'`
this.routes.article = new ArticleRoutes()
this.routes.page = new PageRoutes()
this.routes.serve = new ServeHandler({
pageRoutes: this.routes.page,
root: localUtil.getPathFromRoot('../public'), root: localUtil.getPathFromRoot('../public'),
version: this.core.app.running, version: this.core.app.running,
frontend: config.get('frontend:url'), frontend: config.get('frontend:url'),
})) })
} }
} }

View file

@ -1,7 +1,7 @@
const EditPage = require('./site_editpage') const EditPage = require('./site_editpage')
const AllPages = require('./site_pages') const AllPages = require('./site_pages')
const AllArticles = require('./site_articles') const AllArticles = require('./site_articles')
const EditArticle = require('./editarticle') const EditArticle = require('./site_editarticle')
const AllStaff = require('./stafflist') const AllStaff = require('./stafflist')
const EditStaff = require('./editstaff') const EditStaff = require('./editstaff')
const Dialogue = require('./dialogue') const Dialogue = require('./dialogue')

View file

@ -29,7 +29,9 @@ const Dialogue = {
view: function(vnode) { view: function(vnode) {
let data = Dialogue.showDialogueData let data = Dialogue.showDialogueData
return data return data
? m('div.floating-container.main', m('dialogue', [ ? m('div.floating-container.main', {
onclick: this.onclose.bind(this),
}, m('dialogue', { onclick: function(e) { e.stopPropagation() } }, [
m('h2.title', data.title), m('h2.title', data.title),
m('p', data.message), m('p', data.message),
m('div.buttons', [ m('div.buttons', [

View file

@ -1,46 +0,0 @@
const Froala = {
files: [
{ type: 'css', url: 'https://cdn.jsdelivr.net/npm/froala-editor@3.0.4/css/froala_editor.pkgd.min.css' },
{ type: 'css', url: 'https://cdn.jsdelivr.net/npm/froala-editor@3.0.4/css/themes/gray.min.css' },
{ type: 'js', url: 'https://cdn.jsdelivr.net/npm/froala-editor@3.0.4/js/froala_editor.pkgd.min.js' },
],
loadedFiles: 0,
loadedFroala: false,
checkLoadedAll: function(res) {
if (Froala.loadedFiles < Froala.files.length) {
return
}
Froala.loadedFroala = true
res()
},
createFroalaScript: function() {
if (Froala.loadedFroala) return Promise.resolve()
return new Promise(function(res) {
let onload = function() {
Froala.loadedFiles++
Froala.checkLoadedAll(res)
}
let head = document.getElementsByTagName('head')[0]
for (var i = 0; i < Froala.files.length; i++) {
let element
if (Froala.files[i].type === 'css') {
element = document.createElement('link')
element.setAttribute('rel', 'stylesheet')
element.setAttribute('type', 'text/css')
element.setAttribute('href', Froala.files[i].url)
} else {
element = document.createElement('script')
element.setAttribute('type', 'text/javascript')
element.setAttribute('src', Froala.files[i].url)
}
element.onload = onload
head.insertBefore(element, head.firstChild)
}
})
},
}
module.exports = Froala

View file

@ -13,7 +13,6 @@ const AdminArticles = {
articles: [], articles: [],
total_articles: 0, total_articles: 0,
} }
this.removeArticle = null
this.currentPage = Number(m.route.param('page')) || 1 this.currentPage = Number(m.route.param('page')) || 1
this.fetchArticles(vnode) this.fetchArticles(vnode)
@ -70,24 +69,34 @@ const AdminArticles = {
}) })
}, },
confirmRemoveArticle: function(vnode) { confirmRemoveArticle: function(vnode, article) {
let removingArticle = this.removeArticle
this.removeArticle = null
this.loading = true this.loading = true
m.redraw() m.redraw()
return common.sendRequest({ return api.sendRequest({
method: 'DELETE', method: 'DELETE',
url: '/api/auth/articles/' + removingArticle.id, url: '/api/auth/articles/' + article.id,
}) })
.then( .then(
() => this.fetchArticles(vnode), () => this.fetchArticles(vnode),
(err) => { (err) => { this.error = err.message }
this.error = err.message )
.then(() => {
this.loading = false this.loading = false
m.redraw() m.redraw()
} })
) },
askConfirmRemovePage: function(vnode, article) {
Dialogue.showDialogue(
'Delete ' + article.name,
'Are you sure you want to remove "' + article.name + '" (' + article.path + ')',
'Remove',
'alert',
'Don\'t remove',
'',
article,
this.confirmRemoveArticle.bind(this, vnode))
}, },
drawArticle: function(vnode, article) { drawArticle: function(vnode, article) {
@ -104,7 +113,7 @@ const AdminArticles = {
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.right', article.publish_at.replace('T', ' ').split('.')[0]), m('td.right', article.publish_at.replace('T', ' ').split('.')[0]),
m('td.right', article.admin_name), m('td.right', article.admin_name),
m('td.right', m('button', { onclick: function() { vnode.state.removeArticle = article } }, 'Remove')), m('td.right', m('button', { onclick: this.askConfirmRemovePage.bind(this, vnode, article) }, 'Remove')),
]), ]),
] ]
}, },
@ -113,12 +122,13 @@ const AdminArticles = {
return [ return [
m('div.admin', [ m('div.admin', [
m('div.inside.vertical', [ m('div.inside.vertical', [
m('div.spacer'),
m('h2.title', 'All articles'),
m('div.actions', [ m('div.actions', [
m('div.filler'),
m('span', 'Actions:'), m('span', 'Actions:'),
m(m.route.Link, { href: '/admin/articles/add' }, 'Create new article'), m(m.route.Link, { href: '/admin/articles/add' }, 'Create new article'),
]), ]),
m('h2.title', 'All articles'),
m('div.container', [
m('div.error', { m('div.error', {
hidden: !this.error, hidden: !this.error,
onclick: function() { vnode.state.error = '' }, onclick: function() { vnode.state.error = '' },
@ -147,17 +157,7 @@ const AdminArticles = {
}), }),
]), ]),
]), ]),
m(Dialogue, { ]),
hidden: vnode.state.removeArticle === null,
title: 'Delete ' + (vnode.state.removeArticle ? vnode.state.removeArticle.name : ''),
message: 'Are you sure you want to remove "' + (vnode.state.removeArticle ? vnode.state.removeArticle.name : '') + '" (' + (vnode.state.removeArticle ? vnode.state.removeArticle.path : '') + ')',
yes: 'Remove',
yesclass: 'alert',
no: 'Cancel',
noclass: 'cancel',
onyes: this.confirmRemoveArticle.bind(this, vnode),
onno: function() { vnode.state.removeArticle = null },
}),
] ]
}, },
} }

View file

@ -17,6 +17,7 @@ const EditArticle = {
} }
this.pages = [{id: null, name: 'Frontpage'}] this.pages = [{id: null, name: 'Frontpage'}]
this.pages = this.pages.concat(PageTree.getFlatTree()) this.pages = this.pages.concat(PageTree.getFlatTree())
this.newBanner = null this.newBanner = null
this.newMedia = null this.newMedia = null
this.dateInstance = null this.dateInstance = null
@ -62,12 +63,13 @@ const EditArticle = {
data data
.then((result) => { .then((result) => {
this.data = result this.data = result
this.data.article.publish_at = new Date(this.data.article.publish_at)
if (this.data.article.id) { if (this.data.article.id) {
this.data.article.publish_at = new Date(this.data.article.publish_at)
document.title = 'Editing: ' + this.data.article.name + ' - Admin NFP Moe' document.title = 'Editing: ' + this.data.article.name + ' - Admin NFP Moe'
this.editedPath = true this.editedPath = true
} else { } else {
this.data.article.publish_at = new Date('3000-01-01')
document.title = 'Create Article - Admin NFP Moe' document.title = 'Create Article - Admin NFP Moe'
} }
}, (err) => { }, (err) => {
@ -172,41 +174,249 @@ const EditArticle = {
}, },
uploadFile: function(vnode, e) { uploadFile: function(vnode, e) {
if (!e.target.files[0]) return
if (this.lastid === 'add') return
let file = e.target.files[0]
e.target.value = null
let formData = new FormData()
formData.append('file', file)
return this.refreshFiles(api.sendRequest({
method: 'POST',
url: '/api/auth/articles/' + this.lastid + '/files',
body: formData,
}))
},
refreshFiles: function(vnode, prom) {
prom.then(() => {
return api.sendRequest({
method: 'GET',
url: '/api/auth/articles/' + this.lastid,
})
})
.then((result) => {
this.data.files = result.files
}, (err) => {
this.error = err.message
})
.then(() => {
m.redraw()
})
},
askConfirmRemoveFile: function(vnode, file) {
console.log(file)
/*Dialogue.showDialogue(
'Delete ' + page.name,
'Are you sure you want to remove "' + page.name + '" (' + page.path + ')',
'Remove',
'alert',
'Don\'t remove',
'',
page,
this.confirmRemovePage.bind(this, vnode))*/
}, },
view: function(vnode) { view: function(vnode) {
const showPublish = this.data.article let article = this.data.article
? this.data.article.publish_at > new Date() const showPublish = article
? article.publish_at > new Date()
: false : false
const bannerImage = this.data.article && this.data.article.banner_prefix console.log(!!article, article && article.publish_at > new Date(),'=', showPublish)
? this.data.article.banner_prefix + '_large.avif' const bannerImage = article && article.banner_alt_prefix
? article.banner_alt_prefix + '_large.avif'
: null : null
const mediaImage = this.data.article && this.data.article.media_prefix const mediaImage = article && article.media_alt_prefix
? this.data.article.media_prefix + '_large.avif' ? article.media_alt_prefix + '_large.avif'
: null : null
return [ return [
this.loading && !this.data.article m('div.admin', [
!this.loading
? m(FileUpload, {
class: 'banner',
height: 150,
onfile: this.mediaUploaded.bind(this, 'banner'),
ondelete: this.mediaRemoved.bind(this, 'banner'),
media: bannerImage,
}, 'Click to upload banner image (only visible when featured)')
: null,
m('div.inside.vertical', [
m('div.actions', [
'« ',
m(m.route.Link, { href: '/admin/articles' }, 'Articles'),
article && article.id
? [
m('div.filler'),
m('span', 'Actions:'),
m(m.route.Link, { href: '/article/' + article.path }, 'View article'),
]
: null,
]),
m('h2.title', this.lastid === 'add' ? 'Create article' : 'Edit ' + (article && article.name || '(untitled)')),
m('div.container', [
m('div.error', {
hidden: !this.error,
onclick: function() { vnode.state.error = '' },
}, this.error),
this.loading
? m('div.loading-spinner')
: null,
article
? [
m(FileUpload, {
class: 'cover',
useimg: true,
onfile: this.mediaUploaded.bind(this, 'media'),
ondelete: this.mediaRemoved.bind(this, 'media'),
media: mediaImage,
}, 'Click to upload article image'),
m('form', {
onsubmit: this.save.bind(this, vnode),
}, [
m('label', 'Parent'),
m('select', {
onchange: this.updateParent.bind(this),
}, this.pages.map((item) => {
return m('option', {
value: item.id || 0,
selected: item.id === article.page_id
}, item.name)
})),
m('div.input-row', [
m('div.input-group', [
m('label', 'Name'),
m('input', {
type: 'text',
value: article.name,
oninput: this.updateValue.bind(this, 'name'),
}),
]),
m('div.input-group', [
m('label', 'Path'),
m('input', {
type: 'text',
value: article.path,
oninput: this.updateValue.bind(this, 'path'),
}),
]),
]),
m('label', 'Description'),
m(Editor, {
oncreate: (subnode) => {
this.editor = subnode.state.editor
},
contentdata: article.content,
}),
m('div.input-row', [
m('div', [
m('label', 'Published at'),
m('input', {
type: 'text',
oncreate: (div) => {
if (!this.dateInstance) {
this.dateInstance = new dtsel.DTS(div.dom, {
dateFormat: 'yyyy-mm-dd',
timeFormat: 'HH:MM:SS',
showTime: true,
})
window.temp = this.dateInstance
}
},
value: article.publish_at.toISOString().replace('T', ', ').split('.')[0],
}),
]),
m('div', [
m('label', 'Published by'),
m('select', {
onchange: this.updateStaffer.bind(this),
},
this.data.staff.map((item) => {
return m('option', {
value: item.id,
selected: item.id === article.admin_id
}, item.name)
})
),
]),
m('div.slim', [
m('label', 'Make featured'),
m('input', {
type: 'checkbox',
checked: article.is_featured,
oninput: this.updateValue.bind(this, 'is_featured'),
}),
]),
]),
m('div.actions', {
hidden: !article.name || !article.path
}, [
m('input', {
type: 'submit',
value: article.id ? 'Save' : 'Create',
}),
showPublish
? m('button', {
onclick: () => {
this.dateInstance.inputElem.value = (new Date().toISOString()).replace('T', ', ').split('.')[0]
}
}, 'Publish now')
: null,
]),
]),
m('files', [
m('h5', 'Files'),
this.data.files.map((file) => {
return m(
Fileinfo,
{ file: file },
m('div.remove',
m('button', { onclick: () => this.askConfirmRemoveFile(vnode, file) }, 'remove')
)
)
}),
]),
article.id
? m('div.actions', [
m('button.fileupload', [
'Add file',
m('input', {
accept: '*',
type: 'file',
onchange: this.uploadFile.bind(this, vnode),
}),
])
])
: null,
]
: null,
]),
]),
]),
/*
this.loading && !article
? m('div.admin-spinner.loading-spinner') ? m('div.admin-spinner.loading-spinner')
: null, : null,
this.data.article article
? m('div.admin-wrapper', [ ? m('div.admin-wrapper', [
this.loading this.loading
? m('div.loading-spinner') ? m('div.loading-spinner')
: null, : null,
m('div.admin-actions', this.data.article.id m('div.admin-actions', article.id
? [ ? [
m('span', 'Actions:'), m('span', 'Actions:'),
m(m.route.Link, { href: '/article/' + this.data.article.path }, 'View article'), m(m.route.Link, { href: '/article/' + article.path }, 'View article'),
] ]
: null), : null),
m('article.editarticle', [ m('article.editarticle', [
m('header', m('h1', m('header', m('h1',
(this.data.article.id ? 'Edit ' : 'Create Article ') + (this.data.article.name || '(untitled)') (article.id ? 'Edit ' : 'Create Article ') + (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 ' + (article.name || '(untitled)'))),
m('div.error', { m('div.error', {
hidden: !this.error, hidden: !this.error,
onclick: () => { vnode.state.error = '' }, onclick: () => { vnode.state.error = '' },
@ -233,7 +443,7 @@ const EditArticle = {
}, this.pages.map((item) => { }, this.pages.map((item) => {
return m('option', { return m('option', {
value: item.id || 0, value: item.id || 0,
selected: item.id === this.data.article.page_id selected: item.id === article.page_id
}, item.name) }, item.name)
})), })),
m('div.input-row', [ m('div.input-row', [
@ -241,7 +451,7 @@ const EditArticle = {
m('label', 'Name'), m('label', 'Name'),
m('input', { m('input', {
type: 'text', type: 'text',
value: this.data.article.name, value: article.name,
oninput: this.updateValue.bind(this, 'name'), oninput: this.updateValue.bind(this, 'name'),
}), }),
]), ]),
@ -249,7 +459,7 @@ const EditArticle = {
m('label', 'Path'), m('label', 'Path'),
m('input', { m('input', {
type: 'text', type: 'text',
value: this.data.article.path, value: article.path,
oninput: this.updateValue.bind(this, 'path'), oninput: this.updateValue.bind(this, 'path'),
}), }),
]), ]),
@ -259,7 +469,7 @@ const EditArticle = {
oncreate: (subnode) => { oncreate: (subnode) => {
this.editor = subnode.state.editor this.editor = subnode.state.editor
}, },
contentdata: this.data.article.content, contentdata: article.content,
}), }),
m('div.input-row', [ m('div.input-row', [
m('div.input-group', [ m('div.input-group', [
@ -276,7 +486,7 @@ const EditArticle = {
window.temp = this.dateInstance window.temp = this.dateInstance
} }
}, },
value: this.data.article.publish_at.toISOString().replace('T', ', ').split('.')[0], value: article.publish_at.toISOString().replace('T', ', ').split('.')[0],
}), }),
]), ]),
m('div.input-group', [ m('div.input-group', [
@ -287,7 +497,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.admin_id selected: item.id === article.admin_id
}, item.name) }, item.name)
}) })
), ),
@ -296,13 +506,13 @@ const EditArticle = {
m('label', 'Make featured'), m('label', 'Make featured'),
m('input', { m('input', {
type: 'checkbox', type: 'checkbox',
checked: this.data.article.is_featured, checked: article.is_featured,
oninput: this.updateValue.bind(this, 'is_featured'), oninput: this.updateValue.bind(this, 'is_featured'),
}), }),
]), ]),
]), ]),
m('div', { m('div', {
hidden: !this.data.article.name || !this.data.article.path hidden: !article.name || !article.path
}, [ }, [
m('input', { m('input', {
type: 'submit', type: 'submit',
@ -311,7 +521,7 @@ const EditArticle = {
showPublish showPublish
? m('button.submit', { ? m('button.submit', {
onclick: () => { onclick: () => {
this.data.article.publish_at = new Date().toISOString() article.publish_at = new Date().toISOString()
} }
}, 'Publish') }, 'Publish')
: null, : null,
@ -325,7 +535,7 @@ const EditArticle = {
}), }),
]) ])
: null, : null,
this.data.article.id article.id
? m('div.fileupload', [ ? m('div.fileupload', [
'Add file', 'Add file',
m('input', { m('input', {
@ -341,7 +551,7 @@ const EditArticle = {
: m('div.error', { : m('div.error', {
hidden: !this.error, hidden: !this.error,
onclick: () => { this.fetchArticle(vnode) }, onclick: () => { this.fetchArticle(vnode) },
}, this.error),, }, this.error),,*/
] ]
}, },
} }

View file

@ -182,27 +182,29 @@ const AdminEditPage = {
media: bannerImage, media: bannerImage,
}, 'Click to upload banner image') }, 'Click to upload banner image')
: null, : null,
m('div.inside.vertical', { m('div.inside.vertical', [
onsubmit: this.save.bind(this, vnode), m('div.actions', [
}, [
m('div.page-goback', [
'« ', '« ',
m(m.route.Link, { href: '/admin/pages' }, 'Pages') m(m.route.Link, { href: '/admin/pages' }, 'Pages'),
page
? [
m('div.filler'),
'Actions:',
m(m.route.Link, { href: '/page/' + page.path }, 'View page'),
]
: null,
]), ]),
m('h2.title', this.lastid === 'add' ? 'Create page' : 'Edit ' + (page && page.name || '(untitled)')), m('h2.title', this.lastid === 'add' ? 'Create page' : 'Edit ' + (page && page.name || '(untitled)')),
page m('div.container', [
? m('div.actions', [
m('span', 'Actions:'),
m(m.route.Link, { href: '/page/' + page.path }, 'View page'),
])
: null,
m('div.error', { m('div.error', {
hidden: !this.error, hidden: !this.error,
onclick: function() { vnode.state.error = '' }, onclick: function() { vnode.state.error = '' },
}, this.error), }, this.error),
this.loading this.loading
? m('div.loading-spinner') ? m('div.loading-spinner')
: [ : null,
page
? [
m(FileUpload, { m(FileUpload, {
class: 'cover', class: 'cover',
useimg: true, useimg: true,
@ -210,7 +212,9 @@ const AdminEditPage = {
ondelete: this.mediaRemoved.bind(this, 'media'), ondelete: this.mediaRemoved.bind(this, 'media'),
media: mediaImage, media: mediaImage,
}, 'Click to upload page image'), }, 'Click to upload page image'),
m('form', [ m('form', {
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),
@ -253,7 +257,9 @@ const AdminEditPage = {
value: 'Save', value: 'Save',
}), }),
]) ])
], ]
: null,
]),
]), ]),
]), ]),

View file

@ -6,7 +6,6 @@ const AdminPages = {
oninit: function(vnode) { oninit: function(vnode) {
this.error = '' this.error = ''
this.pages = [] this.pages = []
this.removePage = null
document.title = 'Pages - Admin NFP Moe' document.title = 'Pages - Admin NFP Moe'
this.fetchPages(vnode) this.fetchPages(vnode)
@ -32,24 +31,24 @@ const AdminPages = {
}, },
confirmRemovePage: function(vnode, page) { confirmRemovePage: function(vnode, page) {
let removingPage = this.removePage
this.removePage = null
this.loading = true this.loading = true
m.redraw() m.redraw()
return api.sendRequest({ return api.sendRequest({
method: 'DELETE', method: 'DELETE',
url: '/api/auth/pages/' + removingPage.id, url: '/api/auth/pages/' + page.id,
}) })
.then(() => PageTree.refreshTree())
.then( .then(
() => this.fetchPages(vnode), () => Promise.all([
(err) => { PageTree.refreshTree(),
this.error = err.message this.fetchPages(),
]),
(err) => { this.error = err.message }
)
.then(() => {
this.loading = false this.loading = false
m.redraw() m.redraw()
} })
)
}, },
askConfirmRemovePage: function(vnode, page) { askConfirmRemovePage: function(vnode, page) {
@ -61,7 +60,7 @@ const AdminPages = {
'Don\'t remove', 'Don\'t remove',
'', '',
page, page,
this.confirmRemovePage.bind(this)) this.confirmRemovePage.bind(this, vnode))
}, },
drawPage: function(vnode, page) { drawPage: function(vnode, page) {
@ -82,12 +81,13 @@ const AdminPages = {
return [ return [
m('div.admin', [ m('div.admin', [
m('div.inside.vertical', [ m('div.inside.vertical', [
m('div.spacer'),
m('h2.title', 'All pages'),
m('div.actions', [ m('div.actions', [
m('div.filler'),
m('span', 'Actions:'), m('span', 'Actions:'),
m(m.route.Link, { href: '/admin/pages/add' }, 'Create new page'), m(m.route.Link, { href: '/admin/pages/add' }, 'Create new page'),
]), ]),
m('h2.title', 'All pages'),
m('div.container', [
m('div.error', { m('div.error', {
hidden: !this.error, hidden: !this.error,
onclick: function() { vnode.state.error = '' }, onclick: function() { vnode.state.error = '' },
@ -106,10 +106,7 @@ const AdminPages = {
m('tbody', this.pages.map(AdminPages.drawPage.bind(this, vnode))), m('tbody', this.pages.map(AdminPages.drawPage.bind(this, vnode))),
], ],
), ),
/*m(Pages, { ]),
base: '/admin/articles',
links: this.links,
}),*/
]), ]),
]), ]),
] ]

View file

@ -85,6 +85,7 @@ const Fileinfo = {
&& vnode.attrs.file.meta.torrent.files.length > 4 && vnode.attrs.file.meta.torrent.files.length > 4
? m('div.trimmed', '...' + vnode.attrs.file.meta.torrent.files.length + ' files...') ? m('div.trimmed', '...' + vnode.attrs.file.meta.torrent.files.length + ' files...')
: null, : null,
vnode.children,
]) ])
}, },
} }

View file

@ -73,7 +73,7 @@ const Menu = {
'Welcome ' + Authentication.currentUser.name + '. ', 'Welcome ' + Authentication.currentUser.name + '. ',
m('button', { onclick: this.logOut }, '(Log out)'), m('button', { onclick: this.logOut }, '(Log out)'),
]), ]),
m('div.actions', [ m('div', [
m(m.route.Link, { href: '/admin/articles/add' }, 'Create article'), m(m.route.Link, { href: '/admin/articles/add' }, 'Create article'),
m(m.route.Link, { href: '/admin/articles' }, 'Articles'), m(m.route.Link, { href: '/admin/articles' }, 'Articles'),
m(m.route.Link, { href: '/admin/pages' }, 'Pages'), m(m.route.Link, { href: '/admin/pages' }, 'Pages'),

View file

@ -90,12 +90,21 @@ const SiteArticle = {
: null, : null,
(article (article
? m('.inside.vertical', [ ? m('.inside.vertical', [
m('div.page-goback', ['« ', m(m.route.Link, { m('div.actions', [
'« ',
m(m.route.Link, {
href: article.page_path href: article.page_path
? '/page/' + article.page_path ? '/page/' + article.page_path
: '/' : '/'
}, article.page_name || 'Home')] }, article.page_name || 'Home'),
), Authentication.currentUser
? [
m('div.filler'),
'Actions:',
m(m.route.Link, { href: '/admin/articles/' + article.id }, 'Edit article'),
]
: null,
]),
article ? m(Article, { full: true, files: this.data.files, article: article }) : null, article ? m(Article, { full: true, files: this.data.files, article: article }) : null,
window.LoadComments window.LoadComments
? m('div#hyvor-talk-view', { oncreate: function() { ? m('div#hyvor-talk-view', { oncreate: function() {

View file

@ -145,7 +145,7 @@ const SitePage = {
: null), : null),
(page (page
? m('.inside.vertical', [ ? m('.inside.vertical', [
m('div.page-goback', [ m('div.actions', [
'« ', '« ',
m(m.route.Link, { m(m.route.Link, {
href: page.parent_path href: page.parent_path

View file

@ -3,6 +3,7 @@
*/ */
:root { :root {
--admin-bg: hsl(213.9, 100%, 95%); --admin-bg: hsl(213.9, 100%, 95%);
--admin-bg-highlight: hsl(213.9, 100%, 85%);
--admin-color: #000; --admin-color: #000;
--admin-table-border: #01579b; --admin-table-border: #01579b;
--admin-table-header-bg: #3D77C7; --admin-table-header-bg: #3D77C7;
@ -18,27 +19,27 @@
.admin { .admin {
background: var(--admin-bg); background: var(--admin-bg);
color: var(--admin-color); color: var(--admin-color);
display: flex; min-height: calc(100vh - 390px);
justify-content: center;
align-items: center;
flex-direction: column;
min-height: calc(100vh - 200px);
}
.admin .inside {
padding: 0 1rem 1rem;
}
.admin .spacer {
height: 40px;
} }
.admin .loading-spinner { .admin .loading-spinner {
position: relative; position: absolute;
left: unset; left: 0;
top: unset; top: 0;
min-height: 300px; width: 100%;
height: calc(100vh - 300px); height: 100%;
background: #0002
}
.admin .container .actions {
margin-left: -2rem;
}
.admin .container .actions button,
.admin .container .actions input {
margin-left: 1rem;
font-weight: normal;
min-width: 150px;
} }
.admin table { .admin table {
@ -47,6 +48,14 @@
border-spacing: 0; border-spacing: 0;
font-size: 0.75em; font-size: 0.75em;
margin-bottom: 1rem; margin-bottom: 1rem;
width: 100%;
}
input[type=checkbox] {
display: block;
height: 20px;
margin: 0.5rem 0;
width: 20px;
} }
.admin table thead th, .admin table thead th,
@ -65,6 +74,10 @@
color: var(--alt-color); color: var(--alt-color);
} }
.admin table tr:hover td {
background: var(--admin-bg-highlight);
}
.admin table button { .admin table button {
color: var(--link); color: var(--link);
background: transparent; background: transparent;
@ -78,17 +91,6 @@
text-align: right; text-align: right;
} }
.admin .actions {
margin: 0.5rem 0;
display: flex;
justify-content: flex-end;
font-size: 0.875rem;
}
.admin .actions a {
margin-left: 0.5rem;
}
.admin form { .admin form {
margin: 1rem 0 0; margin: 1rem 0 0;
} }
@ -108,12 +110,55 @@
flex: 2 1 50px; flex: 2 1 50px;
} }
.admin .input-row > .slim {
flex: 0 0 auto;
}
.admin .error {
position: fixed;
top: 0;
left: 50%;
margin-left: -30%;
width: 60%;
z-index: 10;
}
/* ************** fileinfo ************** */
.admin fileinfo:hover {
background: var(--admin-bg-highlight);
}
.admin fileinfo .remove {
position: absolute;
right: 0;
top: 0;
height: 100%;
background: linear-gradient(to right, transparent, var(--admin-bg) 2rem);
padding-left: 3rem;
display: flex;
align-items: center;
justify-content: center;
}
.admin fileinfo:hover .remove {
background: linear-gradient(to right, transparent, var(--admin-bg-highlight) 2rem);
}
.admin fileinfo .remove button {
margin: 0;
}
/* ************** fileupload ************** */ /* ************** fileupload ************** */
fileupload { fileupload {
width: 100%;
}
fileupload,
.fileupload {
position: relative; position: relative;
display: block; display: block;
width: 100%;
} }
fileupload.banner { fileupload.banner {
@ -151,7 +196,8 @@ fileupload .text {
color: var(--seperator); color: var(--seperator);
} }
fileupload input { fileupload input,
.fileupload input {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
@ -189,9 +235,6 @@ dialogue {
color: var(--color); color: var(--color);
} }
dialogue h2 {
}
dialogue p { dialogue p {
padding: 1rem; padding: 1rem;
} }
@ -233,6 +276,8 @@ dialogue button.cancel {
===================== 3rd party ===================== ===================== 3rd party =====================
*/ */
/* ************** Editor ************** */
.ce-block__content, .ce-block__content,
.ce-toolbar__content { max-width:calc(100% - 120px) !important; } .ce-toolbar__content { max-width:calc(100% - 120px) !important; }
.cdx-block { max-width: 100% !important; } .cdx-block { max-width: 100% !important; }
@ -241,6 +286,7 @@ dialogue button.cancel {
border: 1px solid var(--color); border: 1px solid var(--color);
background: var(--bg); background: var(--bg);
color: var(--color); color: var(--color);
padding-top: 0.5rem;
} }
.codex-editor:hover, .codex-editor:hover,
@ -248,3 +294,102 @@ dialogue button.cancel {
border-color: var(--link); border-color: var(--link);
} }
/* ************** dte ************** */
.date-selector-wrapper {
width: 200px;
padding: 3px;
background-color: #fff;
box-shadow: 1px 1px 10px 1px #5c5c5c;
position: absolute;
font-size: 12px;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
z-index: 10;
/* user-select: none; */
}
.cal-header, .cal-row {
display: flex;
width: 100%;
height: 30px;
line-height: 30px;
text-align: center;
}
.cal-cell, .cal-nav {
cursor: pointer;
}
.cal-day-names {
height: 25px;
line-height: 25px;
}
.cal-day-names .cal-cell {
cursor: default;
font-weight: bold;
}
.cal-cell-prev, .cal-cell-next {
color: #777;
}
.cal-months .cal-row, .cal-years .cal-row {
height: 60px;
line-height: 60px;
}
.cal-nav-prev, .cal-nav-next {
flex: 0.15;
}
.cal-nav-current {
flex: 0.75;
font-weight: bold;
}
.cal-months .cal-cell, .cal-years .cal-cell {
flex: 0.25;
}
.cal-days .cal-cell {
flex: 0.143;
}
.cal-value {
color: #fff;
background-color: #286090;
}
.cal-cell:hover, .cal-nav:hover {
background-color: #eee;
}
.cal-value:hover {
background-color: #204d74;
}
/* time footer */
.cal-time {
display: flex;
justify-content: flex-start;
height: 27px;
line-height: 27px;
}
.cal-time-label, .cal-time-value {
flex: 0.12;
text-align: center;
}
.cal-time-slider {
flex: 0.77;
background-image: linear-gradient(to right, #d1d8dd, #d1d8dd);
background-repeat: no-repeat;
background-size: 100% 1px;
background-position: left 50%;
height: 100%;
}
.cal-time-slider input {
width: 100%;
-webkit-appearance: none;
background: 0 0;
cursor: pointer;
height: 100%;
outline: 0;
user-select: auto;
}
.ce-block__content,
.ce-toolbar__content { max-width:calc(100% - 120px) !important; }
.cdx-block { max-width: 100% !important; }

View file

@ -82,6 +82,7 @@ a {
img { img {
max-width: 100%; max-width: 100%;
margin: 0 auto;
display: block; display: block;
} }
@ -130,6 +131,7 @@ select {
color: var(--color); color: var(--color);
border-radius: 0; border-radius: 0;
padding: 0.25rem; padding: 0.25rem;
line-height: 1rem;
} }
label { label {
@ -272,9 +274,13 @@ header aside {
padding: 0.5rem 0.5rem; padding: 0.5rem 0.5rem;
} }
header aside .actions a { header aside a,
margin-left: 1rem; header aside button {
display: inline-block; margin-left: 0.5rem;
}
header aside p button {
margin-left: 0;
} }
.avifsupport header .logo { .avifsupport header .logo {
@ -335,12 +341,12 @@ main {
text-shadow: 0 0 .3em #000; text-shadow: 0 0 .3em #000;
} }
.page-goback { .actions {
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
display: flex; display: flex;
} }
.page-goback a { .actions a {
margin-left: 0.375rem; margin-left: 0.375rem;
} }
@ -520,6 +526,7 @@ fileinfo {
line-height: 1rem; line-height: 1rem;
font-size: 0.75rem; font-size: 0.75rem;
display: block; display: block;
position: relative;
} }
fileinfo.slim { fileinfo.slim {

Binary file not shown.

Binary file not shown.

Binary file not shown.