dev
This commit is contained in:
parent
cbd38bc212
commit
e9c7cdfb7a
29 changed files with 802 additions and 352 deletions
|
@ -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 })
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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 })
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
84
nfp_moe/api/article_routes.mjs
Normal file
84
nfp_moe/api/article_routes.mjs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
19
nfp_moe/api/page_routes.mjs
Normal file
19
nfp_moe/api/page_routes.mjs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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'),
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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', [
|
||||
|
|
|
@ -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
|
|
@ -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 },
|
||||
}),
|
||||
]
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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),,*/
|
||||
]
|
||||
},
|
||||
}
|
|
@ -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,
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
|
||||
|
|
|
@ -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))),
|
||||
],
|
||||
),
|
||||
]),
|
||||
]),
|
||||
]),
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
])
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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; }
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
BIN
nfp_moe/public/assets/img/correct_asuna_frontpage.xcf
Normal file
BIN
nfp_moe/public/assets/img/correct_asuna_frontpage.xcf
Normal file
Binary file not shown.
BIN
nfp_moe/public/assets/img/correct_asuna_frontpage_night.xcf
Normal file
BIN
nfp_moe/public/assets/img/correct_asuna_frontpage_night.xcf
Normal file
Binary file not shown.
BIN
nfp_moe/public/assets/img/correct_footer.xcf
Normal file
BIN
nfp_moe/public/assets/img/correct_footer.xcf
Normal file
Binary file not shown.
BIN
nfp_moe/public/assets/img/correct_logo.xcf
Normal file
BIN
nfp_moe/public/assets/img/correct_logo.xcf
Normal file
Binary file not shown.
Loading…
Reference in a new issue