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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import config from '../config.mjs'
import Client from './client.mjs'
export function upload(file) {
export function uploadMedia(file) {
const media = config.get('media')
const client = new Client()
@ -53,3 +53,22 @@ export function upload(file) {
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": {
"dot": "^2.0.0-beta.1",
"flaska": "^1.3.0",
"flaska": "^1.3.1",
"formidable": "^1.2.6",
"msnodesqlv8": "^2.4.7",
"nconf-lite": "^1.0.1"

View file

@ -1,13 +1,14 @@
import { parsePage, parsePagesToTree } from './util.mjs'
import { upload } from '../media/upload.mjs'
import { combineFilesWithArticles, parseArticle, parseArticles } from '../article/util.mjs'
import { uploadMedia, uploadFile } from '../media/upload.mjs'
import { parseArticle, parseArticles } from '../article/util.mjs'
import { mediaToDatabase } from '../media/util.mjs'
export default class PageRoutes {
constructor(opts = {}) {
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),
])
let out = {
ctx.body = this.getPage_resOut(res)
}
getPage_resOut(res) {
return {
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])
ctx.body = out
}
/** GET: /api/auth/pages */
@ -106,13 +107,13 @@ export default class PageRoutes {
if (ctx.req.files.banner) {
promises.push(
this.upload(ctx.req.files.banner)
this.uploadMedia(ctx.req.files.banner)
.then(res => { newBanner = res })
)
}
if (ctx.req.files.media) {
promises.push(
this.upload(ctx.req.files.media)
this.uploadMedia(ctx.req.files.media)
.then(res => { newMedia = res })
)
}

View file

@ -26,29 +26,17 @@ export default class Server {
this.authenticate = authenticate
this.formidable = FormidableHandler.bind(this, formidable)
this.jsonHandler = JsonHandler
this.routes = [
new PageRoutes(),
new ArticleRoutes(),
new AuthenticationRoutes(),
]
this.routes = {
page: new PageRoutes(),
article: new ArticleRoutes(),
auth: new AuthenticationRoutes(),
}
this.init()
}
init() { }
getRouteInstance(type) {
for (let route of this.routes) {
if (route instanceof type) {
return route
}
}
}
addCustomRoutes() {
}
runCreateServer() {
// Create our server
this.flaska = new Flaska(this.flaskaOptions, this.http)
@ -89,8 +77,9 @@ export default class Server {
}
runRegisterRoutes() {
for (let route of this.routes) {
route.register(this)
let keys = Object.keys(this.routes)
for (let key of keys) {
this.routes[key].register(this)
}
}
@ -105,7 +94,6 @@ export default class Server {
}
run() {
this.addCustomRoutes()
this.runCreateServer()
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'
/*
@ -9,10 +12,9 @@ Taken from parse-torrent
* @param {Buffer|Object} torrent
* @return {Object} parsed torrent
*/
export function decodeTorrentFile (torrent) {
if (Buffer.isBuffer(torrent)) {
torrent = bencode.decode(torrent)
}
export async function decodeTorrentFile (file) {
let buffer = await fs.readFile(file)
let torrent = bencode.decode(buffer)
// sanity check
ensure(torrent.info, 'info')
@ -36,7 +38,9 @@ export function decodeTorrentFile (torrent) {
announce: []
}
result.infoHash = sha1.sync(result.infoBuffer)
result.infoHash = crypto.createHash('sha1')
.update(result.infoBuffer)
.digest('hex')
result.infoHashBuffer = Buffer.from(result.infoHash, 'hex')
if (torrent.info.private !== undefined) result.private = !!torrent.info.private
@ -103,3 +107,7 @@ function splitPieces (buf) {
function ensure (bool, 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
}
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) {
file.url = 'https://cdn.nfp.is' + file.path
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 Parent from '../base/server.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 {
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'`
}
addCustomRoutes() {
let page = this.getRouteInstance(PageRoutes)
super.init()
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'),
version: this.core.app.running,
frontend: config.get('frontend:url'),
}))
})
}
}

View file

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

View file

@ -29,7 +29,9 @@ const Dialogue = {
view: function(vnode) {
let data = Dialogue.showDialogueData
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('p', data.message),
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: [],
total_articles: 0,
}
this.removeArticle = null
this.currentPage = Number(m.route.param('page')) || 1
this.fetchArticles(vnode)
@ -70,24 +69,34 @@ const AdminArticles = {
})
},
confirmRemoveArticle: function(vnode) {
let removingArticle = this.removeArticle
this.removeArticle = null
confirmRemoveArticle: function(vnode, article) {
this.loading = true
m.redraw()
return common.sendRequest({
return api.sendRequest({
method: 'DELETE',
url: '/api/auth/articles/' + removingArticle.id,
url: '/api/auth/articles/' + article.id,
})
.then(
() => this.fetchArticles(vnode),
(err) => {
this.error = err.message
this.loading = false
m.redraw()
}
(err) => { this.error = err.message }
)
.then(() => {
this.loading = false
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) {
@ -104,7 +113,7 @@ const AdminArticles = {
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.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,51 +122,42 @@ const AdminArticles = {
return [
m('div.admin', [
m('div.inside.vertical', [
m('div.spacer'),
m('h2.title', 'All articles'),
m('div.actions', [
m('div.filler'),
m('span', 'Actions:'),
m(m.route.Link, { href: '/admin/articles/add' }, 'Create new article'),
]),
m('div.error', {
hidden: !this.error,
onclick: function() { vnode.state.error = '' },
}, this.error),
this.loading
? m('div.loading-spinner.full')
: m('table', [
m('thead',
m('tr', [
m('th', 'Title'),
m('th', 'Path'),
m('th', 'Page'),
m('th.right', 'Publish'),
m('th.right', 'By'),
m('th.right', 'Actions'),
])
),
m('tbody', this.data.articles.map((article) => this.drawArticle(vnode, article))),
],
),
m(Paginator, {
base: '/admin/articles',
page: this.currentPage,
perPage: ItemsPerPage,
total: this.data.total_articles,
}),
m('h2.title', 'All articles'),
m('div.container', [
m('div.error', {
hidden: !this.error,
onclick: function() { vnode.state.error = '' },
}, this.error),
this.loading
? m('div.loading-spinner.full')
: m('table', [
m('thead',
m('tr', [
m('th', 'Title'),
m('th', 'Path'),
m('th', 'Page'),
m('th.right', 'Publish'),
m('th.right', 'By'),
m('th.right', 'Actions'),
])
),
m('tbody', this.data.articles.map((article) => this.drawArticle(vnode, article))),
],
),
m(Paginator, {
base: '/admin/articles',
page: this.currentPage,
perPage: ItemsPerPage,
total: this.data.total_articles,
}),
]),
]),
]),
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 = this.pages.concat(PageTree.getFlatTree())
this.newBanner = null
this.newMedia = null
this.dateInstance = null
@ -62,12 +63,13 @@ const EditArticle = {
data
.then((result) => {
this.data = result
this.data.article.publish_at = new Date(this.data.article.publish_at)
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'
this.editedPath = true
} else {
this.data.article.publish_at = new Date('3000-01-01')
document.title = 'Create Article - Admin NFP Moe'
}
}, (err) => {
@ -172,41 +174,249 @@ const EditArticle = {
},
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) {
const showPublish = this.data.article
? this.data.article.publish_at > new Date()
let article = this.data.article
const showPublish = article
? article.publish_at > new Date()
: false
const bannerImage = this.data.article && this.data.article.banner_prefix
? this.data.article.banner_prefix + '_large.avif'
console.log(!!article, article && article.publish_at > new Date(),'=', showPublish)
const bannerImage = article && article.banner_alt_prefix
? article.banner_alt_prefix + '_large.avif'
: null
const mediaImage = this.data.article && this.data.article.media_prefix
? this.data.article.media_prefix + '_large.avif'
const mediaImage = article && article.media_alt_prefix
? article.media_alt_prefix + '_large.avif'
: null
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')
: null,
this.data.article
article
? m('div.admin-wrapper', [
this.loading
? m('div.loading-spinner')
: null,
m('div.admin-actions', this.data.article.id
m('div.admin-actions', article.id
? [
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),
m('article.editarticle', [
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', {
hidden: !this.error,
onclick: () => { vnode.state.error = '' },
@ -233,7 +443,7 @@ const EditArticle = {
}, this.pages.map((item) => {
return m('option', {
value: item.id || 0,
selected: item.id === this.data.article.page_id
selected: item.id === article.page_id
}, item.name)
})),
m('div.input-row', [
@ -241,7 +451,7 @@ const EditArticle = {
m('label', 'Name'),
m('input', {
type: 'text',
value: this.data.article.name,
value: article.name,
oninput: this.updateValue.bind(this, 'name'),
}),
]),
@ -249,7 +459,7 @@ const EditArticle = {
m('label', 'Path'),
m('input', {
type: 'text',
value: this.data.article.path,
value: article.path,
oninput: this.updateValue.bind(this, 'path'),
}),
]),
@ -259,7 +469,7 @@ const EditArticle = {
oncreate: (subnode) => {
this.editor = subnode.state.editor
},
contentdata: this.data.article.content,
contentdata: article.content,
}),
m('div.input-row', [
m('div.input-group', [
@ -276,7 +486,7 @@ const EditArticle = {
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', [
@ -287,7 +497,7 @@ const EditArticle = {
this.data.staff.map((item) => {
return m('option', {
value: item.id,
selected: item.id === this.data.article.admin_id
selected: item.id === article.admin_id
}, item.name)
})
),
@ -296,13 +506,13 @@ const EditArticle = {
m('label', 'Make featured'),
m('input', {
type: 'checkbox',
checked: this.data.article.is_featured,
checked: article.is_featured,
oninput: this.updateValue.bind(this, 'is_featured'),
}),
]),
]),
m('div', {
hidden: !this.data.article.name || !this.data.article.path
hidden: !article.name || !article.path
}, [
m('input', {
type: 'submit',
@ -311,7 +521,7 @@ const EditArticle = {
showPublish
? m('button.submit', {
onclick: () => {
this.data.article.publish_at = new Date().toISOString()
article.publish_at = new Date().toISOString()
}
}, 'Publish')
: null,
@ -325,7 +535,7 @@ const EditArticle = {
}),
])
: null,
this.data.article.id
article.id
? m('div.fileupload', [
'Add file',
m('input', {
@ -341,7 +551,7 @@ const EditArticle = {
: m('div.error', {
hidden: !this.error,
onclick: () => { this.fetchArticle(vnode) },
}, this.error),,
}, this.error),,*/
]
},
}

View file

@ -182,78 +182,84 @@ const AdminEditPage = {
media: bannerImage,
}, 'Click to upload banner image')
: null,
m('div.inside.vertical', {
onsubmit: this.save.bind(this, vnode),
}, [
m('div.page-goback', [
m('div.inside.vertical', [
m('div.actions', [
'« ',
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)')),
page
? m('div.actions', [
m('span', 'Actions:'),
m(m.route.Link, { href: '/page/' + page.path }, 'View page'),
])
: null,
m('div.error', {
hidden: !this.error,
onclick: function() { vnode.state.error = '' },
}, this.error),
this.loading
? m('div.loading-spinner')
: [
m(FileUpload, {
class: 'cover',
useimg: true,
onfile: this.mediaUploaded.bind(this, 'media'),
ondelete: this.mediaRemoved.bind(this, 'media'),
media: mediaImage,
}, 'Click to upload page image'),
m('form', [
m('label', 'Parent'),
m('select', {
onchange: this.updateParent.bind(this),
}, this.pages.filter(item => !page || item.id !== page.id).map((item) => {
return m('option', {
value: item.id || 0,
selected: item.id === page.parent_id
}, item.name)
})),
m('div.input-row', [
m('div', [
m('label', 'Name'),
m('input', {
type: 'text',
value: page.name,
oninput: this.updateValue.bind(this, 'name'),
}),
m('div.container', [
m('div.error', {
hidden: !this.error,
onclick: function() { vnode.state.error = '' },
}, this.error),
this.loading
? m('div.loading-spinner')
: null,
page
? [
m(FileUpload, {
class: 'cover',
useimg: true,
onfile: this.mediaUploaded.bind(this, 'media'),
ondelete: this.mediaRemoved.bind(this, 'media'),
media: mediaImage,
}, 'Click to upload page image'),
m('form', {
onsubmit: this.save.bind(this, vnode),
}, [
m('label', 'Parent'),
m('select', {
onchange: this.updateParent.bind(this),
}, this.pages.filter(item => !page || item.id !== page.id).map((item) => {
return m('option', {
value: item.id || 0,
selected: item.id === page.parent_id
}, item.name)
})),
m('div.input-row', [
m('div', [
m('label', 'Name'),
m('input', {
type: 'text',
value: page.name,
oninput: this.updateValue.bind(this, 'name'),
}),
]),
m('div', [
m('label', 'Path'),
m('input', {
type: 'text',
value: page.path,
oninput: this.updateValue.bind(this, 'path'),
}),
]),
]),
m('div', [
m('label', 'Path'),
m('input', {
type: 'text',
value: page.path,
oninput: this.updateValue.bind(this, 'path'),
}),
]),
]),
m('label', 'Description'),
m('div.content',
m(Editor, {
oncreate: (subnode) => {
this.editor = subnode.state.editor
},
contentdata: page.content,
})
),
m('input', {
hidden: !page.name || !page.path,
type: 'submit',
value: 'Save',
}),
])
],
m('label', 'Description'),
m('div.content',
m(Editor, {
oncreate: (subnode) => {
this.editor = subnode.state.editor
},
contentdata: page.content,
})
),
m('input', {
hidden: !page.name || !page.path,
type: 'submit',
value: 'Save',
}),
])
]
: null,
]),
]),
]),

View file

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

View file

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

View file

@ -73,7 +73,7 @@ const Menu = {
'Welcome ' + Authentication.currentUser.name + '. ',
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' }, 'Articles'),
m(m.route.Link, { href: '/admin/pages' }, 'Pages'),

View file

@ -90,12 +90,21 @@ const SiteArticle = {
: null,
(article
? m('.inside.vertical', [
m('div.page-goback', ['« ', m(m.route.Link, {
href: article.page_path
? '/page/' + article.page_path
: '/'
}, article.page_name || 'Home')]
),
m('div.actions', [
'« ',
m(m.route.Link, {
href: article.page_path
? '/page/' + article.page_path
: '/'
}, 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,
window.LoadComments
? m('div#hyvor-talk-view', { oncreate: function() {

View file

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

View file

@ -3,6 +3,7 @@
*/
:root {
--admin-bg: hsl(213.9, 100%, 95%);
--admin-bg-highlight: hsl(213.9, 100%, 85%);
--admin-color: #000;
--admin-table-border: #01579b;
--admin-table-header-bg: #3D77C7;
@ -18,27 +19,27 @@
.admin {
background: var(--admin-bg);
color: var(--admin-color);
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
min-height: calc(100vh - 200px);
}
.admin .inside {
padding: 0 1rem 1rem;
}
.admin .spacer {
height: 40px;
min-height: calc(100vh - 390px);
}
.admin .loading-spinner {
position: relative;
left: unset;
top: unset;
min-height: 300px;
height: calc(100vh - 300px);
position: absolute;
left: 0;
top: 0;
width: 100%;
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 {
@ -47,6 +48,14 @@
border-spacing: 0;
font-size: 0.75em;
margin-bottom: 1rem;
width: 100%;
}
input[type=checkbox] {
display: block;
height: 20px;
margin: 0.5rem 0;
width: 20px;
}
.admin table thead th,
@ -65,6 +74,10 @@
color: var(--alt-color);
}
.admin table tr:hover td {
background: var(--admin-bg-highlight);
}
.admin table button {
color: var(--link);
background: transparent;
@ -78,17 +91,6 @@
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 {
margin: 1rem 0 0;
}
@ -108,12 +110,55 @@
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 {
width: 100%;
}
fileupload,
.fileupload {
position: relative;
display: block;
width: 100%;
}
fileupload.banner {
@ -151,7 +196,8 @@ fileupload .text {
color: var(--seperator);
}
fileupload input {
fileupload input,
.fileupload input {
position: absolute;
top: 0;
left: 0;
@ -189,9 +235,6 @@ dialogue {
color: var(--color);
}
dialogue h2 {
}
dialogue p {
padding: 1rem;
}
@ -233,6 +276,8 @@ dialogue button.cancel {
===================== 3rd party =====================
*/
/* ************** Editor ************** */
.ce-block__content,
.ce-toolbar__content { max-width:calc(100% - 120px) !important; }
.cdx-block { max-width: 100% !important; }
@ -241,6 +286,7 @@ dialogue button.cancel {
border: 1px solid var(--color);
background: var(--bg);
color: var(--color);
padding-top: 0.5rem;
}
.codex-editor:hover,
@ -248,3 +294,102 @@ dialogue button.cancel {
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 {
max-width: 100%;
margin: 0 auto;
display: block;
}
@ -130,6 +131,7 @@ select {
color: var(--color);
border-radius: 0;
padding: 0.25rem;
line-height: 1rem;
}
label {
@ -272,9 +274,13 @@ header aside {
padding: 0.5rem 0.5rem;
}
header aside .actions a {
margin-left: 1rem;
display: inline-block;
header aside a,
header aside button {
margin-left: 0.5rem;
}
header aside p button {
margin-left: 0;
}
.avifsupport header .logo {
@ -335,12 +341,12 @@ main {
text-shadow: 0 0 .3em #000;
}
.page-goback {
.actions {
padding: 0.5rem 1rem;
display: flex;
}
.page-goback a {
.actions a {
margin-left: 0.375rem;
}
@ -520,6 +526,7 @@ fileinfo {
line-height: 1rem;
font-size: 0.75rem;
display: block;
position: relative;
}
fileinfo.slim {

Binary file not shown.

Binary file not shown.

Binary file not shown.